@lmaksym/agent-mem 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/context.md +24 -0
- package/.claude/skills/agent-mem/SKILL.md +66 -0
- package/.claude/skills/agent-mem/references/branching-merging.md +34 -0
- package/.claude/skills/agent-mem/references/coexistence.md +19 -0
- package/.claude/skills/agent-mem/references/collaboration.md +33 -0
- package/.claude/skills/agent-mem/references/reflection-compaction.md +104 -0
- package/.claude/skills/agent-mem/references/sub-agent-patterns.md +60 -0
- package/LICENSE +21 -0
- package/README.md +235 -0
- package/bin/agent-context.js +95 -0
- package/bin/parse-args.js +85 -0
- package/package.json +58 -0
- package/src/commands/branch.js +57 -0
- package/src/commands/branch.test.js +91 -0
- package/src/commands/branches.js +34 -0
- package/src/commands/commit.js +55 -0
- package/src/commands/compact.js +307 -0
- package/src/commands/compact.test.js +110 -0
- package/src/commands/config.js +47 -0
- package/src/commands/core.test.js +166 -0
- package/src/commands/diff.js +157 -0
- package/src/commands/diff.test.js +64 -0
- package/src/commands/forget.js +77 -0
- package/src/commands/forget.test.js +68 -0
- package/src/commands/help.js +99 -0
- package/src/commands/import.js +83 -0
- package/src/commands/init.js +269 -0
- package/src/commands/init.test.js +80 -0
- package/src/commands/lesson.js +95 -0
- package/src/commands/lesson.test.js +93 -0
- package/src/commands/merge.js +105 -0
- package/src/commands/pin.js +34 -0
- package/src/commands/pull.js +80 -0
- package/src/commands/push.js +80 -0
- package/src/commands/read.js +62 -0
- package/src/commands/reflect.js +328 -0
- package/src/commands/remember.js +95 -0
- package/src/commands/resolve.js +230 -0
- package/src/commands/resolve.test.js +167 -0
- package/src/commands/search.js +70 -0
- package/src/commands/share.js +65 -0
- package/src/commands/snapshot.js +106 -0
- package/src/commands/status.js +37 -0
- package/src/commands/switch.js +31 -0
- package/src/commands/sync.js +328 -0
- package/src/commands/track.js +61 -0
- package/src/commands/unpin.js +28 -0
- package/src/commands/write.js +58 -0
- package/src/core/auto-commit.js +22 -0
- package/src/core/config.js +93 -0
- package/src/core/context-root.js +28 -0
- package/src/core/fs.js +137 -0
- package/src/core/git.js +182 -0
- package/src/core/importers.js +210 -0
- package/src/core/lock.js +62 -0
- package/src/core/reflect-defrag.js +287 -0
- package/src/core/reflect-gather.js +360 -0
- package/src/core/reflect-parse.js +168 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, cpSync } from 'node:fs';
|
|
2
|
+
import { join, basename, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { execSync } from 'node:child_process';
|
|
5
|
+
import { writeContextFile, readContextFile } from '../core/fs.js';
|
|
6
|
+
import { writeConfig } from '../core/config.js';
|
|
7
|
+
import { initGit, commitContext } from '../core/git.js';
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = dirname(__filename);
|
|
11
|
+
const PACKAGE_ROOT = join(__dirname, '..', '..');
|
|
12
|
+
|
|
13
|
+
export default async function init({ args, flags }) {
|
|
14
|
+
const cwd = process.cwd();
|
|
15
|
+
const contextDir = join(cwd, '.context');
|
|
16
|
+
|
|
17
|
+
if (existsSync(contextDir) && !flags.force) {
|
|
18
|
+
console.log(`â ī¸ .context/ already exists. Use --force to reinitialize.`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log(`đ Scanning project...`);
|
|
23
|
+
|
|
24
|
+
// Detect project info
|
|
25
|
+
const projectName = basename(cwd);
|
|
26
|
+
const projectInfo = detectProject(cwd);
|
|
27
|
+
|
|
28
|
+
// Create directory structure
|
|
29
|
+
mkdirSync(join(contextDir, 'system', 'humans'), { recursive: true });
|
|
30
|
+
mkdirSync(join(contextDir, 'memory'), { recursive: true });
|
|
31
|
+
mkdirSync(join(contextDir, 'branches'), { recursive: true });
|
|
32
|
+
mkdirSync(join(contextDir, 'reflections'), { recursive: true });
|
|
33
|
+
|
|
34
|
+
// Generate main.md
|
|
35
|
+
writeContextFile(
|
|
36
|
+
contextDir,
|
|
37
|
+
'main.md',
|
|
38
|
+
[
|
|
39
|
+
`# ${projectName}`,
|
|
40
|
+
'',
|
|
41
|
+
'## Goals',
|
|
42
|
+
'- [ ] Define project goals',
|
|
43
|
+
'',
|
|
44
|
+
'## Milestones',
|
|
45
|
+
'- [ ] Define milestones',
|
|
46
|
+
'',
|
|
47
|
+
'## Status',
|
|
48
|
+
`Initialized: ${new Date().toISOString().slice(0, 10)}`,
|
|
49
|
+
'',
|
|
50
|
+
].join('\n'),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// Generate system/project.md
|
|
54
|
+
writeContextFile(
|
|
55
|
+
contextDir,
|
|
56
|
+
'system/project.md',
|
|
57
|
+
[
|
|
58
|
+
'---',
|
|
59
|
+
`description: "${projectInfo.description || `Project: ${projectName}`}"`,
|
|
60
|
+
'limit: 10000',
|
|
61
|
+
'---',
|
|
62
|
+
'',
|
|
63
|
+
`# ${projectName}`,
|
|
64
|
+
'',
|
|
65
|
+
projectInfo.description ? `${projectInfo.description}\n` : '',
|
|
66
|
+
projectInfo.stack ? `## Stack\n${projectInfo.stack}\n` : '',
|
|
67
|
+
projectInfo.structure ? `## Structure\n${projectInfo.structure}\n` : '',
|
|
68
|
+
].join('\n'),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Generate system/conventions.md
|
|
72
|
+
writeContextFile(
|
|
73
|
+
contextDir,
|
|
74
|
+
'system/conventions.md',
|
|
75
|
+
[
|
|
76
|
+
'---',
|
|
77
|
+
'description: "Coding conventions and style rules"',
|
|
78
|
+
'limit: 5000',
|
|
79
|
+
'---',
|
|
80
|
+
'',
|
|
81
|
+
'# Conventions',
|
|
82
|
+
'',
|
|
83
|
+
'Add project-specific coding conventions here.',
|
|
84
|
+
'',
|
|
85
|
+
].join('\n'),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Import from CLAUDE.md if it exists
|
|
89
|
+
if (flags['from-claude'] || existsSync(join(cwd, 'CLAUDE.md'))) {
|
|
90
|
+
importClaudeMd(cwd, contextDir);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Import session histories
|
|
94
|
+
if (flags['from-claude']) {
|
|
95
|
+
const { importClaudeHistory } = await import('../core/importers.js');
|
|
96
|
+
importClaudeHistory(cwd, contextDir);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (flags['from-codex']) {
|
|
100
|
+
const { importCodexHistory } = await import('../core/importers.js');
|
|
101
|
+
importCodexHistory(cwd, contextDir);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Write default config
|
|
105
|
+
writeConfig(contextDir, {
|
|
106
|
+
auto_commit: false,
|
|
107
|
+
auto_commit_interval: 10,
|
|
108
|
+
reflection: {
|
|
109
|
+
trigger: 'manual',
|
|
110
|
+
frequency: 5,
|
|
111
|
+
model: null,
|
|
112
|
+
},
|
|
113
|
+
system_files_max: 10,
|
|
114
|
+
memory_files_max: 25,
|
|
115
|
+
branch: 'main',
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Install agent skill to detected IDE directories
|
|
119
|
+
const installedSkills = installSkills(cwd);
|
|
120
|
+
|
|
121
|
+
// Initialize git
|
|
122
|
+
initGit(contextDir);
|
|
123
|
+
const hash = commitContext(contextDir, 'init: bootstrap context');
|
|
124
|
+
|
|
125
|
+
// Auto-sync to detected IDE rule files
|
|
126
|
+
const syncMod = await import('./sync.js');
|
|
127
|
+
await syncMod.default({ args: [], flags: { ...flags, _contextRoot: cwd } });
|
|
128
|
+
|
|
129
|
+
// Output
|
|
130
|
+
const systemFiles = readdirSync(join(contextDir, 'system')).filter((f) => !f.startsWith('.'));
|
|
131
|
+
console.log(
|
|
132
|
+
`
|
|
133
|
+
â
INITIALIZED: .context/
|
|
134
|
+
Project: ${projectName}
|
|
135
|
+
${projectInfo.stack ? `Stack: ${projectInfo.stack.split('\n')[0]}` : ''}
|
|
136
|
+
|
|
137
|
+
Files created:
|
|
138
|
+
main.md â project roadmap
|
|
139
|
+
system/project.md â project overview
|
|
140
|
+
system/conventions.md â coding conventions
|
|
141
|
+
config.yaml â settings
|
|
142
|
+
memory/ â learned context (empty)
|
|
143
|
+
branches/ â exploration branches (empty)
|
|
144
|
+
${installedSkills.length ? `\nSkill installed to:\n${installedSkills.map((s) => ` ${s}`).join('\n')}` : ''}
|
|
145
|
+
${hash ? `\nGit commit: ${hash}` : ''}
|
|
146
|
+
|
|
147
|
+
Next steps:
|
|
148
|
+
agent-mem snapshot â view your context
|
|
149
|
+
agent-mem write system/conventions.md â add your coding rules
|
|
150
|
+
agent-mem commit "added conventions" â checkpoint
|
|
151
|
+
`.trim(),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Install the agent-mem skill to the 3 universal Agent Skills directories.
|
|
157
|
+
* These cover ~14 of the 16 tools that support the agentskills.io standard:
|
|
158
|
+
*
|
|
159
|
+
* .claude/skills/ â Claude Code, Goose, Amp, OpenCode, Cline
|
|
160
|
+
* .agents/skills/ â Codex CLI, Gemini CLI, Amp, OpenCode, Warp, Roo Code, Goose
|
|
161
|
+
* .github/skills/ â VS Code Copilot, GitHub Copilot
|
|
162
|
+
*/
|
|
163
|
+
function installSkills(cwd) {
|
|
164
|
+
const sourceDir = join(PACKAGE_ROOT, '.claude', 'skills', 'agent-mem');
|
|
165
|
+
if (!existsSync(sourceDir)) return [];
|
|
166
|
+
|
|
167
|
+
const targets = [
|
|
168
|
+
'.claude/skills/agent-mem',
|
|
169
|
+
'.agents/skills/agent-mem',
|
|
170
|
+
'.github/skills/agent-mem',
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
const installed = [];
|
|
174
|
+
|
|
175
|
+
for (const skills of targets) {
|
|
176
|
+
const targetDir = join(cwd, skills);
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
cpSync(sourceDir, targetDir, { recursive: true });
|
|
180
|
+
installed.push(skills);
|
|
181
|
+
} catch {
|
|
182
|
+
// Silent fail â permissions or other issues
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return installed;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Detect project type, stack, and structure from common files.
|
|
191
|
+
*/
|
|
192
|
+
function detectProject(cwd) {
|
|
193
|
+
const info = { description: '', stack: '', structure: '' };
|
|
194
|
+
|
|
195
|
+
// package.json
|
|
196
|
+
const pkgPath = join(cwd, 'package.json');
|
|
197
|
+
if (existsSync(pkgPath)) {
|
|
198
|
+
try {
|
|
199
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
200
|
+
info.description = pkg.description || '';
|
|
201
|
+
const deps = Object.keys({ ...pkg.dependencies, ...pkg.devDependencies });
|
|
202
|
+
const stackItems = [];
|
|
203
|
+
|
|
204
|
+
if (deps.includes('next')) stackItems.push('Next.js');
|
|
205
|
+
if (deps.includes('react')) stackItems.push('React');
|
|
206
|
+
if (deps.includes('vue')) stackItems.push('Vue');
|
|
207
|
+
if (deps.includes('svelte') || deps.includes('@sveltejs/kit')) stackItems.push('Svelte');
|
|
208
|
+
if (deps.includes('express')) stackItems.push('Express');
|
|
209
|
+
if (deps.includes('fastify')) stackItems.push('Fastify');
|
|
210
|
+
if (deps.includes('typescript')) stackItems.push('TypeScript');
|
|
211
|
+
if (deps.includes('tailwindcss')) stackItems.push('Tailwind CSS');
|
|
212
|
+
if (deps.includes('prisma') || deps.includes('@prisma/client')) stackItems.push('Prisma');
|
|
213
|
+
if (deps.includes('drizzle-orm')) stackItems.push('Drizzle');
|
|
214
|
+
|
|
215
|
+
if (stackItems.length) info.stack = stackItems.join(', ');
|
|
216
|
+
} catch {}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// pyproject.toml / requirements.txt
|
|
220
|
+
if (existsSync(join(cwd, 'pyproject.toml')) || existsSync(join(cwd, 'requirements.txt'))) {
|
|
221
|
+
const stackItems = ['Python'];
|
|
222
|
+
try {
|
|
223
|
+
const content = existsSync(join(cwd, 'pyproject.toml'))
|
|
224
|
+
? readFileSync(join(cwd, 'pyproject.toml'), 'utf-8')
|
|
225
|
+
: readFileSync(join(cwd, 'requirements.txt'), 'utf-8');
|
|
226
|
+
|
|
227
|
+
if (content.includes('fastapi')) stackItems.push('FastAPI');
|
|
228
|
+
if (content.includes('django')) stackItems.push('Django');
|
|
229
|
+
if (content.includes('flask')) stackItems.push('Flask');
|
|
230
|
+
if (content.includes('langchain')) stackItems.push('LangChain');
|
|
231
|
+
if (content.includes('langgraph')) stackItems.push('LangGraph');
|
|
232
|
+
if (content.includes('sqlalchemy') || content.includes('alembic'))
|
|
233
|
+
stackItems.push('SQLAlchemy');
|
|
234
|
+
|
|
235
|
+
info.stack = (info.stack ? info.stack + ', ' : '') + stackItems.join(', ');
|
|
236
|
+
} catch {}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Top-level structure
|
|
240
|
+
try {
|
|
241
|
+
const items = readdirSync(cwd)
|
|
242
|
+
.filter(
|
|
243
|
+
(n) =>
|
|
244
|
+
!n.startsWith('.') &&
|
|
245
|
+
!['node_modules', '__pycache__', 'venv', '.venv', 'dist', 'build'].includes(n),
|
|
246
|
+
)
|
|
247
|
+
.slice(0, 20);
|
|
248
|
+
info.structure = items.map((n) => `- ${n}/`).join('\n');
|
|
249
|
+
} catch {}
|
|
250
|
+
|
|
251
|
+
return info;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Import existing CLAUDE.md into context.
|
|
256
|
+
*/
|
|
257
|
+
function importClaudeMd(cwd, contextDir) {
|
|
258
|
+
const claudePath = join(cwd, 'CLAUDE.md');
|
|
259
|
+
if (!existsSync(claudePath)) return;
|
|
260
|
+
|
|
261
|
+
const content = readFileSync(claudePath, 'utf-8');
|
|
262
|
+
writeContextFile(
|
|
263
|
+
contextDir,
|
|
264
|
+
'memory/imported-claude-md.md',
|
|
265
|
+
['---', 'description: "Imported from CLAUDE.md"', '---', '', content].join('\n'),
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
console.log(` đĨ Imported CLAUDE.md â memory/imported-claude-md.md`);
|
|
269
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, it, before, after } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync, rmSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { execSync } from 'node:child_process';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
|
|
8
|
+
const CLI = join(import.meta.dirname, '../../bin/agent-context.js');
|
|
9
|
+
const run = (args, cwd) =>
|
|
10
|
+
execSync(`node ${CLI} ${args}`, { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
11
|
+
|
|
12
|
+
describe('init', () => {
|
|
13
|
+
let dir;
|
|
14
|
+
|
|
15
|
+
before(() => {
|
|
16
|
+
dir = mkdtempSync(join(tmpdir(), 'amem-test-init-'));
|
|
17
|
+
writeFileSync(
|
|
18
|
+
join(dir, 'package.json'),
|
|
19
|
+
JSON.stringify({
|
|
20
|
+
name: 'test-proj',
|
|
21
|
+
dependencies: { next: '15', react: '19', typescript: '5' },
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
after(() => rmSync(dir, { recursive: true, force: true }));
|
|
27
|
+
|
|
28
|
+
it('creates .context/ directory', () => {
|
|
29
|
+
const out = run('init', dir);
|
|
30
|
+
assert.ok(existsSync(join(dir, '.context')));
|
|
31
|
+
assert.ok(out.includes('INITIALIZED'));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('detects stack from package.json', () => {
|
|
35
|
+
const project = readFileSync(join(dir, '.context/system/project.md'), 'utf-8');
|
|
36
|
+
assert.ok(project.includes('Next.js'));
|
|
37
|
+
assert.ok(project.includes('React'));
|
|
38
|
+
assert.ok(project.includes('TypeScript'));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('creates required files', () => {
|
|
42
|
+
assert.ok(existsSync(join(dir, '.context/main.md')));
|
|
43
|
+
assert.ok(existsSync(join(dir, '.context/config.yaml')));
|
|
44
|
+
assert.ok(existsSync(join(dir, '.context/system/project.md')));
|
|
45
|
+
assert.ok(existsSync(join(dir, '.context/system/conventions.md')));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('initializes git repo', () => {
|
|
49
|
+
assert.ok(existsSync(join(dir, '.context/.git')));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('refuses reinit without --force', () => {
|
|
53
|
+
const out = run('init', dir);
|
|
54
|
+
assert.ok(out.includes('already exists'));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('reinitializes with --force', () => {
|
|
58
|
+
const out = run('init --force', dir);
|
|
59
|
+
assert.ok(out.includes('INITIALIZED'));
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('init --from-claude', () => {
|
|
64
|
+
let dir;
|
|
65
|
+
|
|
66
|
+
before(() => {
|
|
67
|
+
dir = mkdtempSync(join(tmpdir(), 'amem-test-claude-'));
|
|
68
|
+
writeFileSync(join(dir, 'package.json'), '{}');
|
|
69
|
+
writeFileSync(join(dir, 'CLAUDE.md'), '# Project Rules\n\nAlways use TypeScript.\n');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
after(() => rmSync(dir, { recursive: true, force: true }));
|
|
73
|
+
|
|
74
|
+
it('imports CLAUDE.md', () => {
|
|
75
|
+
const out = run('init --from-claude', dir);
|
|
76
|
+
assert.ok(out.includes('Imported CLAUDE.md'));
|
|
77
|
+
const imported = readFileSync(join(dir, '.context/memory/imported-claude-md.md'), 'utf-8');
|
|
78
|
+
assert.ok(imported.includes('Always use TypeScript'));
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { contextDir as getContextDir } from '../core/context-root.js';
|
|
2
|
+
import { readContextFile, writeContextFile } from '../core/fs.js';
|
|
3
|
+
import { readConfig } from '../core/config.js';
|
|
4
|
+
|
|
5
|
+
const LESSON_FILE = 'memory/lessons.md';
|
|
6
|
+
const LESSON_TITLE = 'Lessons Learned';
|
|
7
|
+
const LESSON_DESC = 'Lessons learned â problem/resolution pairs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve memory file path based on active branch.
|
|
11
|
+
* On main: memory/lessons.md
|
|
12
|
+
* On branch: branches/<name>/memory/lessons.md
|
|
13
|
+
*/
|
|
14
|
+
function resolvePath(target, branch) {
|
|
15
|
+
if (branch && branch !== 'main') {
|
|
16
|
+
return target.replace(/^memory\//, `branches/${branch}/memory/`);
|
|
17
|
+
}
|
|
18
|
+
return target;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default async function lesson({ args, flags }) {
|
|
22
|
+
const root = flags._contextRoot;
|
|
23
|
+
const ctxDir = getContextDir(root);
|
|
24
|
+
|
|
25
|
+
if (!args.length && !flags.problem) {
|
|
26
|
+
console.error(
|
|
27
|
+
'â Usage: agent-mem lesson <title> --problem <text> --resolution <text> [--tags <tags>]',
|
|
28
|
+
);
|
|
29
|
+
console.error('');
|
|
30
|
+
console.error('Or use shorthand with -> separator:');
|
|
31
|
+
console.error(' agent-mem lesson "Hit 429 rate limit -> implement exponential backoff"');
|
|
32
|
+
console.error('');
|
|
33
|
+
console.error('Examples:');
|
|
34
|
+
console.error(
|
|
35
|
+
' agent-mem lesson "API backoff" --problem "Hit 429 on rapid calls" --resolution "Added exponential backoff"',
|
|
36
|
+
);
|
|
37
|
+
console.error(' agent-mem lesson "VAD fix -> Lower threshold to 0.3" --tags "voice, audio"');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const config = readConfig(ctxDir);
|
|
42
|
+
const branch = config.branch || 'main';
|
|
43
|
+
const target = resolvePath(LESSON_FILE, branch);
|
|
44
|
+
const text = args.join(' ');
|
|
45
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
46
|
+
const time = new Date().toISOString().slice(11, 16);
|
|
47
|
+
|
|
48
|
+
let title, problem, resolution, tags;
|
|
49
|
+
|
|
50
|
+
if (flags.problem && flags.resolution) {
|
|
51
|
+
title = text || flags.problem;
|
|
52
|
+
problem = typeof flags.problem === 'string' ? flags.problem : '';
|
|
53
|
+
resolution = typeof flags.resolution === 'string' ? flags.resolution : '';
|
|
54
|
+
tags = typeof flags.tags === 'string' ? flags.tags : null;
|
|
55
|
+
} else if (text.includes('->')) {
|
|
56
|
+
const sepIdx = text.indexOf('->');
|
|
57
|
+
const before = text.slice(0, sepIdx).trim();
|
|
58
|
+
const after = text.slice(sepIdx + 2).trim();
|
|
59
|
+
if (!before || !after) {
|
|
60
|
+
console.error('â Both sides of -> must have content.');
|
|
61
|
+
console.error(' Example: "Hit 429 rate limit -> implement exponential backoff"');
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
title = text.replace('->', 'â');
|
|
65
|
+
problem = before;
|
|
66
|
+
resolution = after;
|
|
67
|
+
tags = typeof flags.tags === 'string' ? flags.tags : null;
|
|
68
|
+
} else {
|
|
69
|
+
console.error('â Lessons need a problem and resolution.');
|
|
70
|
+
console.error(' Use --problem and --resolution flags, or -> shorthand:');
|
|
71
|
+
console.error(' agent-mem lesson "problem -> resolution"');
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Build lesson block
|
|
76
|
+
let block = `\n### [${date} ${time}] ${title}\n`;
|
|
77
|
+
block += `**Problem:** ${problem}\n`;
|
|
78
|
+
block += `**Resolution:** ${resolution}\n`;
|
|
79
|
+
if (tags) block += `**Tags:** ${tags}\n`;
|
|
80
|
+
|
|
81
|
+
let existing =
|
|
82
|
+
readContextFile(ctxDir, target) ||
|
|
83
|
+
['---', `description: "${LESSON_DESC}"`, '---', '', `# ${LESSON_TITLE}`, ''].join('\n');
|
|
84
|
+
|
|
85
|
+
existing += block;
|
|
86
|
+
|
|
87
|
+
writeContextFile(ctxDir, target, existing);
|
|
88
|
+
const branchLabel = branch !== 'main' ? ` [branch: ${branch}]` : '';
|
|
89
|
+
console.log(`â
LESSON${branchLabel} â .context/${target}`);
|
|
90
|
+
console.log(` "${title}"`);
|
|
91
|
+
|
|
92
|
+
// Auto-commit if enabled
|
|
93
|
+
const { maybeAutoCommit } = await import('../core/auto-commit.js');
|
|
94
|
+
maybeAutoCommit(ctxDir, 'lesson learned');
|
|
95
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, it, before, after } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync, rmSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { execSync } from 'node:child_process';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
|
|
8
|
+
const CLI = join(import.meta.dirname, '../../bin/agent-context.js');
|
|
9
|
+
const run = (args, cwd) =>
|
|
10
|
+
execSync(`node ${CLI} ${args}`, { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
11
|
+
|
|
12
|
+
describe('lesson', () => {
|
|
13
|
+
let dir;
|
|
14
|
+
|
|
15
|
+
before(() => {
|
|
16
|
+
dir = mkdtempSync(join(tmpdir(), 'amem-test-lesson-'));
|
|
17
|
+
writeFileSync(join(dir, 'package.json'), '{}');
|
|
18
|
+
run('init', dir);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
after(() => rmSync(dir, { recursive: true, force: true }));
|
|
22
|
+
|
|
23
|
+
it('creates lesson with --problem and --resolution flags', () => {
|
|
24
|
+
run(
|
|
25
|
+
'lesson "API backoff" --problem "Hit 429 rate limit" --resolution "Added exponential backoff"',
|
|
26
|
+
dir,
|
|
27
|
+
);
|
|
28
|
+
const content = readFileSync(join(dir, '.context/memory/lessons.md'), 'utf-8');
|
|
29
|
+
assert.ok(content.includes('API backoff'));
|
|
30
|
+
assert.ok(content.includes('**Problem:** Hit 429 rate limit'));
|
|
31
|
+
assert.ok(content.includes('**Resolution:** Added exponential backoff'));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('creates lesson with -> shorthand', () => {
|
|
35
|
+
run('lesson "Voice cut off -> Lower VAD threshold to 0.3"', dir);
|
|
36
|
+
const content = readFileSync(join(dir, '.context/memory/lessons.md'), 'utf-8');
|
|
37
|
+
assert.ok(content.includes('**Problem:** Voice cut off'));
|
|
38
|
+
assert.ok(content.includes('**Resolution:** Lower VAD threshold to 0.3'));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('includes tags when provided', () => {
|
|
42
|
+
run(
|
|
43
|
+
'lesson "Memory leak" --problem "OOM after 1hr" --resolution "Close DB connections" --tags "infra, database"',
|
|
44
|
+
dir,
|
|
45
|
+
);
|
|
46
|
+
const content = readFileSync(join(dir, '.context/memory/lessons.md'), 'utf-8');
|
|
47
|
+
assert.ok(content.includes('**Tags:** infra, database'));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('appends multiple lessons to same file', () => {
|
|
51
|
+
const content = readFileSync(join(dir, '.context/memory/lessons.md'), 'utf-8');
|
|
52
|
+
const headings = content.split('\n').filter((l) => l.startsWith('### ['));
|
|
53
|
+
assert.ok(headings.length >= 3, `Expected at least 3 lessons, got ${headings.length}`);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('has correct frontmatter and title', () => {
|
|
57
|
+
const content = readFileSync(join(dir, '.context/memory/lessons.md'), 'utf-8');
|
|
58
|
+
assert.ok(content.includes('# Lessons Learned'));
|
|
59
|
+
assert.ok(content.includes('description:'));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('fails without problem/resolution or -> separator', () => {
|
|
63
|
+
assert.throws(() => run('lesson "just some text"', dir));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('fails with empty -> sides', () => {
|
|
67
|
+
assert.throws(() => run('lesson "-> only resolution"', dir));
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('lesson on branch', () => {
|
|
72
|
+
let dir;
|
|
73
|
+
|
|
74
|
+
before(() => {
|
|
75
|
+
dir = mkdtempSync(join(tmpdir(), 'amem-test-lesson-branch-'));
|
|
76
|
+
writeFileSync(join(dir, 'package.json'), '{}');
|
|
77
|
+
run('init', dir);
|
|
78
|
+
run('branch test-branch exploring', dir);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
after(() => rmSync(dir, { recursive: true, force: true }));
|
|
82
|
+
|
|
83
|
+
it('saves lesson to branch-scoped path', () => {
|
|
84
|
+
run('lesson "Branch fix -> Applied workaround"', dir);
|
|
85
|
+
const content = readFileSync(
|
|
86
|
+
join(dir, '.context/branches/test-branch/memory/lessons.md'),
|
|
87
|
+
'utf-8',
|
|
88
|
+
);
|
|
89
|
+
assert.ok(content.includes('Branch fix'));
|
|
90
|
+
assert.ok(content.includes('**Problem:** Branch fix'));
|
|
91
|
+
assert.ok(content.includes('**Resolution:** Applied workaround'));
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { contextDir as getContextDir } from '../core/context-root.js';
|
|
2
|
+
import { readContextFile, writeContextFile, listFiles } from '../core/fs.js';
|
|
3
|
+
import { readConfig, writeConfig } from '../core/config.js';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
export default async function merge({ args, flags }) {
|
|
8
|
+
const root = flags._contextRoot;
|
|
9
|
+
const ctxDir = getContextDir(root);
|
|
10
|
+
|
|
11
|
+
if (!args.length) {
|
|
12
|
+
console.error('â Usage: agent-mem merge <branch-name> [summary]');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const name = args[0];
|
|
17
|
+
const summary = args.slice(1).join(' ') || '';
|
|
18
|
+
const branchDir = join(ctxDir, 'branches', name);
|
|
19
|
+
|
|
20
|
+
if (!existsSync(branchDir)) {
|
|
21
|
+
console.error(`â Branch "${name}" not found.`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Read branch files
|
|
26
|
+
const purpose = readContextFile(ctxDir, `branches/${name}/purpose.md`) || '';
|
|
27
|
+
const commits = readContextFile(ctxDir, `branches/${name}/commits.md`) || '';
|
|
28
|
+
const trace = readContextFile(ctxDir, `branches/${name}/trace.md`) || '';
|
|
29
|
+
|
|
30
|
+
// Append to memory/decisions.md
|
|
31
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
32
|
+
const decisionEntry = [
|
|
33
|
+
`\n## [${date}] Merged branch: ${name}`,
|
|
34
|
+
'',
|
|
35
|
+
purpose.trim() ? `### Purpose\n${purpose.replace(/^#.*\n/, '').trim()}` : '',
|
|
36
|
+
summary ? `### Summary\n${summary}` : '',
|
|
37
|
+
commits.trim() ? `### Commits\n${commits.replace(/^#.*\n/, '').trim()}` : '',
|
|
38
|
+
'',
|
|
39
|
+
]
|
|
40
|
+
.filter(Boolean)
|
|
41
|
+
.join('\n');
|
|
42
|
+
|
|
43
|
+
let decisions =
|
|
44
|
+
readContextFile(ctxDir, 'memory/decisions.md') ||
|
|
45
|
+
[
|
|
46
|
+
'---',
|
|
47
|
+
'description: "Architectural decisions and branch merge outcomes"',
|
|
48
|
+
'---',
|
|
49
|
+
'',
|
|
50
|
+
'# Decisions',
|
|
51
|
+
'',
|
|
52
|
+
].join('\n');
|
|
53
|
+
|
|
54
|
+
decisions += decisionEntry;
|
|
55
|
+
writeContextFile(ctxDir, 'memory/decisions.md', decisions);
|
|
56
|
+
|
|
57
|
+
// Merge branch-scoped memory into main memory
|
|
58
|
+
const branchMemDir = join(ctxDir, 'branches', name, 'memory');
|
|
59
|
+
let mergedCount = 0;
|
|
60
|
+
if (existsSync(branchMemDir)) {
|
|
61
|
+
const branchMemFiles = listFiles(ctxDir, `branches/${name}/memory`);
|
|
62
|
+
for (const memFile of branchMemFiles) {
|
|
63
|
+
if (!memFile.endsWith('.md')) continue;
|
|
64
|
+
const branchContent = readContextFile(ctxDir, `branches/${name}/memory/${memFile}`);
|
|
65
|
+
if (!branchContent) continue;
|
|
66
|
+
|
|
67
|
+
// Extract entries (lines starting with "- [")
|
|
68
|
+
const branchEntries = branchContent.split('\n').filter((l) => /^- \[/.test(l));
|
|
69
|
+
if (!branchEntries.length) continue;
|
|
70
|
+
|
|
71
|
+
const mainPath = `memory/${memFile}`;
|
|
72
|
+
let mainContent = readContextFile(ctxDir, mainPath);
|
|
73
|
+
|
|
74
|
+
if (!mainContent) {
|
|
75
|
+
// Create the main file with branch content header
|
|
76
|
+
const headerMatch = branchContent.match(/^---\n[\s\S]*?\n---\n[\s\S]*?(?=\n- \[)/);
|
|
77
|
+
mainContent = headerMatch ? headerMatch[0] : `# ${memFile.replace('.md', '')}\n`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Deduplicate: only append entries not already in main
|
|
81
|
+
const mainLines = new Set(mainContent.split('\n'));
|
|
82
|
+
const newEntries = branchEntries.filter((e) => !mainLines.has(e));
|
|
83
|
+
|
|
84
|
+
if (newEntries.length) {
|
|
85
|
+
mainContent += '\n' + newEntries.join('\n');
|
|
86
|
+
writeContextFile(ctxDir, mainPath, mainContent);
|
|
87
|
+
mergedCount += newEntries.length;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Switch back to main
|
|
93
|
+
const config = readConfig(ctxDir);
|
|
94
|
+
config.branch = 'main';
|
|
95
|
+
writeConfig(ctxDir, config);
|
|
96
|
+
|
|
97
|
+
console.log(`â
MERGED: ${name} â main`);
|
|
98
|
+
if (summary) console.log(`Summary: ${summary}`);
|
|
99
|
+
console.log(`Branch findings saved to memory/decisions.md`);
|
|
100
|
+
if (mergedCount > 0) {
|
|
101
|
+
console.log(`Merged ${mergedCount} branch memory entries into main memory`);
|
|
102
|
+
}
|
|
103
|
+
console.log(`Switched to: main`);
|
|
104
|
+
console.log(`\nNote: branch files preserved at branches/${name}/ for reference.`);
|
|
105
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { contextDir as getContextDir } from '../core/context-root.js';
|
|
2
|
+
import { moveContextFile, readContextFile } from '../core/fs.js';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { join, basename } from 'node:path';
|
|
5
|
+
|
|
6
|
+
export default async function pin({ args, flags }) {
|
|
7
|
+
const root = flags._contextRoot;
|
|
8
|
+
const ctxDir = getContextDir(root);
|
|
9
|
+
|
|
10
|
+
if (!args.length) {
|
|
11
|
+
console.error('â Usage: agent-mem pin <path>');
|
|
12
|
+
console.error('Example: agent-mem pin memory/decisions.md');
|
|
13
|
+
console.error("Moves the file into system/ so it's always in agent context.");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const relPath = args[0];
|
|
18
|
+
if (relPath.startsWith('system/')) {
|
|
19
|
+
console.log('âšī¸ Already pinned (in system/).');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const name = basename(relPath);
|
|
24
|
+
const dest = `system/${name}`;
|
|
25
|
+
|
|
26
|
+
if (!existsSync(join(ctxDir, relPath))) {
|
|
27
|
+
console.error(`â File not found: .context/${relPath}`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
moveContextFile(ctxDir, relPath, dest);
|
|
32
|
+
console.log(`đ PINNED: ${relPath} â ${dest}`);
|
|
33
|
+
console.log(`This file will now always be included in agent context.`);
|
|
34
|
+
}
|