@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,31 @@
|
|
|
1
|
+
import { contextDir as getContextDir } from '../core/context-root.js';
|
|
2
|
+
import { readConfig, writeConfig } from '../core/config.js';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
export default async function switchBranch({ args, flags }) {
|
|
7
|
+
const root = flags._contextRoot;
|
|
8
|
+
const ctxDir = getContextDir(root);
|
|
9
|
+
|
|
10
|
+
if (!args.length) {
|
|
11
|
+
console.error('❌ Usage: agent-mem switch <branch-name>');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const name = args[0];
|
|
16
|
+
|
|
17
|
+
if (name !== 'main') {
|
|
18
|
+
const branchDir = join(ctxDir, 'branches', name);
|
|
19
|
+
if (!existsSync(branchDir)) {
|
|
20
|
+
console.error(`❌ Branch "${name}" not found. Run 'agent-mem branches' to see available.`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const config = readConfig(ctxDir);
|
|
26
|
+
const prev = config.branch || 'main';
|
|
27
|
+
config.branch = name;
|
|
28
|
+
writeConfig(ctxDir, config);
|
|
29
|
+
|
|
30
|
+
console.log(`✅ SWITCHED: ${prev} → ${name}`);
|
|
31
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { contextDir as getContextDir } from '../core/context-root.js';
|
|
4
|
+
import { buildTree, readContextFile } from '../core/fs.js';
|
|
5
|
+
import { readConfig } from '../core/config.js';
|
|
6
|
+
|
|
7
|
+
const TARGETS = {
|
|
8
|
+
claude: { file: 'CLAUDE.md', description: 'Claude Code' },
|
|
9
|
+
gemini: { file: 'GEMINI.md', description: 'Gemini CLI' },
|
|
10
|
+
codex: { file: 'AGENTS.md', description: 'Codex' },
|
|
11
|
+
cursor: { dir: '.cursor/rules', description: 'Cursor' },
|
|
12
|
+
windsurf: { file: '.windsurfrules', dir: '.windsurf/rules', description: 'Windsurf' },
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default async function sync({ args, flags }) {
|
|
16
|
+
const root = flags._contextRoot;
|
|
17
|
+
const ctxDir = getContextDir(root);
|
|
18
|
+
const config = readConfig(ctxDir);
|
|
19
|
+
|
|
20
|
+
// Determine targets
|
|
21
|
+
let targets = [];
|
|
22
|
+
|
|
23
|
+
if (flags.all) {
|
|
24
|
+
targets = Object.keys(TARGETS);
|
|
25
|
+
} else {
|
|
26
|
+
for (const key of Object.keys(TARGETS)) {
|
|
27
|
+
if (flags[key]) targets.push(key);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Auto-detect if no flags: sync to whatever already exists in the project
|
|
32
|
+
if (!targets.length) {
|
|
33
|
+
for (const [key, target] of Object.entries(TARGETS)) {
|
|
34
|
+
const fileExists = target.file && existsSync(join(root, target.file));
|
|
35
|
+
const dirExists = target.dir && existsSync(join(root, target.dir));
|
|
36
|
+
if (fileExists || dirExists) targets.push(key);
|
|
37
|
+
}
|
|
38
|
+
if (!targets.length) {
|
|
39
|
+
console.log('ℹ️ No sync targets detected. Use flags to specify:');
|
|
40
|
+
console.log(' agent-mem sync --claude → CLAUDE.md');
|
|
41
|
+
console.log(' agent-mem sync --gemini → GEMINI.md');
|
|
42
|
+
console.log(' agent-mem sync --codex → AGENTS.md');
|
|
43
|
+
console.log(' agent-mem sync --cursor → .cursor/rules/');
|
|
44
|
+
console.log(' agent-mem sync --windsurf → .windsurfrules');
|
|
45
|
+
console.log(' agent-mem sync --all → all of the above');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
console.log(
|
|
49
|
+
`🔍 Auto-detected targets: ${targets.map((t) => TARGETS[t].description).join(', ')}\n`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Gather system/ content
|
|
54
|
+
const systemContent = gatherSystemContent(ctxDir);
|
|
55
|
+
const memoryContent = gatherMemorySummary(ctxDir);
|
|
56
|
+
|
|
57
|
+
for (const target of targets) {
|
|
58
|
+
switch (target) {
|
|
59
|
+
case 'claude':
|
|
60
|
+
syncSingleFile(root, 'CLAUDE.md', buildClaudeMd(systemContent, memoryContent, config));
|
|
61
|
+
break;
|
|
62
|
+
case 'gemini':
|
|
63
|
+
syncSingleFile(root, 'GEMINI.md', buildGeminiMd(systemContent, memoryContent, config));
|
|
64
|
+
break;
|
|
65
|
+
case 'codex':
|
|
66
|
+
syncSingleFile(root, 'AGENTS.md', buildAgentsMd(systemContent, memoryContent, config));
|
|
67
|
+
break;
|
|
68
|
+
case 'cursor':
|
|
69
|
+
syncCursorRules(root, ctxDir);
|
|
70
|
+
break;
|
|
71
|
+
case 'windsurf':
|
|
72
|
+
syncWindsurf(root, ctxDir, systemContent, memoryContent);
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(`\n✅ SYNCED .context/ → ${targets.map((t) => TARGETS[t].description).join(', ')}`);
|
|
78
|
+
console.log("Tip: run after 'agent-mem commit' to keep IDE rules up to date.");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Read all system/ files and return as structured content.
|
|
83
|
+
*/
|
|
84
|
+
function gatherSystemContent(ctxDir) {
|
|
85
|
+
const tree = buildTree(ctxDir);
|
|
86
|
+
const files = tree.filter((e) => !e.isDir && e.path.startsWith('system/'));
|
|
87
|
+
const sections = [];
|
|
88
|
+
|
|
89
|
+
for (const f of files) {
|
|
90
|
+
const raw = readContextFile(ctxDir, f.path);
|
|
91
|
+
if (!raw) continue;
|
|
92
|
+
// Strip frontmatter
|
|
93
|
+
const content = raw.replace(/^---\n[\s\S]*?\n---\n?/, '').trim();
|
|
94
|
+
if (content) {
|
|
95
|
+
sections.push({ name: f.name, path: f.path, content });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return sections;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Build a short summary of memory/ files for inclusion.
|
|
104
|
+
*/
|
|
105
|
+
function gatherMemorySummary(ctxDir) {
|
|
106
|
+
const tree = buildTree(ctxDir);
|
|
107
|
+
const files = tree.filter((e) => !e.isDir && e.path.startsWith('memory/'));
|
|
108
|
+
if (!files.length) return '';
|
|
109
|
+
|
|
110
|
+
const lines = [
|
|
111
|
+
'## Key Decisions & Patterns',
|
|
112
|
+
'',
|
|
113
|
+
'From `.context/memory/` — run `agent-mem read <path>` for details:',
|
|
114
|
+
'',
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
for (const f of files) {
|
|
118
|
+
const raw = readContextFile(ctxDir, f.path);
|
|
119
|
+
if (!raw) continue;
|
|
120
|
+
// Extract entries (bullet entries or lesson headings)
|
|
121
|
+
const bullets = raw.split('\n').filter((l) => l.startsWith('- [') || l.startsWith('### ['));
|
|
122
|
+
if (bullets.length) {
|
|
123
|
+
const lastThree = bullets.slice(-3);
|
|
124
|
+
lines.push(`### ${f.name.replace('.md', '')}`);
|
|
125
|
+
lines.push(...lastThree);
|
|
126
|
+
if (bullets.length > 3)
|
|
127
|
+
lines.push(`_(${bullets.length - 3} more — run \`agent-mem read ${f.path}\`)_`);
|
|
128
|
+
lines.push('');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return lines.join('\n');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─── Shared trigger instructions ─────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
function buildTriggerInstructions() {
|
|
138
|
+
return [
|
|
139
|
+
'## agent-mem — when to use',
|
|
140
|
+
'',
|
|
141
|
+
'**Starting this session →** `amem snapshot` (load context + memory)',
|
|
142
|
+
'**You chose between alternatives →** `amem remember --decision "chose X because Y"`',
|
|
143
|
+
'**You noticed a repeatable approach →** `amem remember --pattern "always do X"`',
|
|
144
|
+
'**You did something wrong →** `amem remember --mistake "never do X"`',
|
|
145
|
+
'**You solved a problem after investigation →** `amem lesson "problem -> fix"`',
|
|
146
|
+
'**You completed meaningful work →** `amem commit "what you did"`',
|
|
147
|
+
'**You\'re about to try something risky →** `amem branch name "purpose"`',
|
|
148
|
+
'**Checking prior context →** `amem search "query"`',
|
|
149
|
+
'',
|
|
150
|
+
].join('\n');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ─── Target-specific builders ────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
function buildClaudeMd(systemContent, memorySummary, config) {
|
|
156
|
+
const sections = [
|
|
157
|
+
'# Project Context',
|
|
158
|
+
'',
|
|
159
|
+
'_Auto-generated by agent-mem. Edit `.context/system/` files instead._',
|
|
160
|
+
'_Run `agent-mem sync --claude` to regenerate._',
|
|
161
|
+
'',
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
for (const s of systemContent) {
|
|
165
|
+
sections.push(s.content);
|
|
166
|
+
sections.push('');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (memorySummary) {
|
|
170
|
+
sections.push(memorySummary);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
sections.push(buildTriggerInstructions());
|
|
174
|
+
|
|
175
|
+
return sections.join('\n');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function buildGeminiMd(systemContent, memorySummary, config) {
|
|
179
|
+
const sections = [
|
|
180
|
+
'# Project Context',
|
|
181
|
+
'',
|
|
182
|
+
'_Auto-generated by agent-mem. Edit `.context/system/` files instead._',
|
|
183
|
+
'_Run `agent-mem sync --gemini` to regenerate._',
|
|
184
|
+
'',
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
for (const s of systemContent) {
|
|
188
|
+
sections.push(s.content);
|
|
189
|
+
sections.push('');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (memorySummary) {
|
|
193
|
+
sections.push(memorySummary);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
sections.push(buildTriggerInstructions());
|
|
197
|
+
|
|
198
|
+
return sections.join('\n');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function buildAgentsMd(systemContent, memorySummary, config) {
|
|
202
|
+
const sections = [
|
|
203
|
+
'# AGENTS.md',
|
|
204
|
+
'',
|
|
205
|
+
'_Auto-generated by agent-mem. Edit `.context/system/` files instead._',
|
|
206
|
+
'_Run `agent-mem sync --codex` to regenerate._',
|
|
207
|
+
'',
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
for (const s of systemContent) {
|
|
211
|
+
sections.push(s.content);
|
|
212
|
+
sections.push('');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (memorySummary) {
|
|
216
|
+
sections.push(memorySummary);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
sections.push(buildTriggerInstructions());
|
|
220
|
+
|
|
221
|
+
return sections.join('\n');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Sync to Cursor: each system/ file becomes a .cursor/rules/ file.
|
|
226
|
+
* Cursor reads .cursor/rules/*.md with frontmatter.
|
|
227
|
+
*/
|
|
228
|
+
function syncCursorRules(root, ctxDir) {
|
|
229
|
+
const rulesDir = join(root, '.cursor', 'rules');
|
|
230
|
+
mkdirSync(rulesDir, { recursive: true });
|
|
231
|
+
|
|
232
|
+
const tree = buildTree(ctxDir);
|
|
233
|
+
const files = tree.filter((e) => !e.isDir && e.path.startsWith('system/'));
|
|
234
|
+
|
|
235
|
+
for (const f of files) {
|
|
236
|
+
const raw = readContextFile(ctxDir, f.path);
|
|
237
|
+
if (!raw) continue;
|
|
238
|
+
|
|
239
|
+
// Cursor rules use frontmatter with description and globs
|
|
240
|
+
const content = raw.replace(/^---\n[\s\S]*?\n---\n?/, '').trim();
|
|
241
|
+
const ruleName = f.name.replace('.md', '');
|
|
242
|
+
|
|
243
|
+
const cursorRule = [
|
|
244
|
+
'---',
|
|
245
|
+
`description: "Project ${ruleName} (from agent-mem)"`,
|
|
246
|
+
'globs: **/*',
|
|
247
|
+
'alwaysApply: true',
|
|
248
|
+
'---',
|
|
249
|
+
'',
|
|
250
|
+
`_Auto-generated by agent-mem from .context/${f.path}_`,
|
|
251
|
+
'',
|
|
252
|
+
content,
|
|
253
|
+
'',
|
|
254
|
+
].join('\n');
|
|
255
|
+
|
|
256
|
+
writeFileSync(join(rulesDir, `amem-${f.name}`), cursorRule);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Write trigger instructions as a separate rule file
|
|
260
|
+
const triggerRule = [
|
|
261
|
+
'---',
|
|
262
|
+
'description: "agent-mem trigger instructions"',
|
|
263
|
+
'globs: **/*',
|
|
264
|
+
'alwaysApply: true',
|
|
265
|
+
'---',
|
|
266
|
+
'',
|
|
267
|
+
'_Auto-generated by agent-mem._',
|
|
268
|
+
'',
|
|
269
|
+
buildTriggerInstructions(),
|
|
270
|
+
].join('\n');
|
|
271
|
+
|
|
272
|
+
writeFileSync(join(rulesDir, 'amem-triggers.md'), triggerRule);
|
|
273
|
+
|
|
274
|
+
console.log(` 📁 Cursor: ${files.length + 1} rules → .cursor/rules/amem-*.md`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Sync to Windsurf: .windsurfrules (single file) + .windsurf/rules/*.md
|
|
279
|
+
*/
|
|
280
|
+
function syncWindsurf(root, ctxDir, systemContent, memorySummary) {
|
|
281
|
+
// Single file version
|
|
282
|
+
const sections = [
|
|
283
|
+
'# Project Rules',
|
|
284
|
+
'',
|
|
285
|
+
'_Auto-generated by agent-mem. Edit `.context/system/` files instead._',
|
|
286
|
+
'',
|
|
287
|
+
];
|
|
288
|
+
|
|
289
|
+
for (const s of systemContent) {
|
|
290
|
+
sections.push(s.content);
|
|
291
|
+
sections.push('');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (memorySummary) sections.push(memorySummary);
|
|
295
|
+
|
|
296
|
+
sections.push(buildTriggerInstructions());
|
|
297
|
+
|
|
298
|
+
writeFileSync(join(root, '.windsurfrules'), sections.join('\n'));
|
|
299
|
+
console.log(` 📄 Windsurf: .windsurfrules updated`);
|
|
300
|
+
|
|
301
|
+
// Also write individual rule files if .windsurf/rules/ exists or --windsurf explicit
|
|
302
|
+
const rulesDir = join(root, '.windsurf', 'rules');
|
|
303
|
+
mkdirSync(rulesDir, { recursive: true });
|
|
304
|
+
|
|
305
|
+
const tree = buildTree(ctxDir);
|
|
306
|
+
const files = tree.filter((e) => !e.isDir && e.path.startsWith('system/'));
|
|
307
|
+
|
|
308
|
+
for (const f of files) {
|
|
309
|
+
const raw = readContextFile(ctxDir, f.path);
|
|
310
|
+
if (!raw) continue;
|
|
311
|
+
const content = raw.replace(/^---\n[\s\S]*?\n---\n?/, '').trim();
|
|
312
|
+
|
|
313
|
+
writeFileSync(
|
|
314
|
+
join(rulesDir, `amem-${f.name}`),
|
|
315
|
+
[`_Auto-generated by agent-mem from .context/${f.path}_`, '', content, ''].join('\n'),
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
console.log(` 📁 Windsurf: ${files.length} rules → .windsurf/rules/amem-*.md`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Write a single file to project root.
|
|
324
|
+
*/
|
|
325
|
+
function syncSingleFile(root, filename, content) {
|
|
326
|
+
writeFileSync(join(root, filename), content);
|
|
327
|
+
console.log(` 📄 ${filename} updated (${content.length} chars)`);
|
|
328
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { contextDir as getContextDir } from '../core/context-root.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Toggle whether .context/ is tracked in the project's git repo.
|
|
7
|
+
*
|
|
8
|
+
* agent-mem track — show current status
|
|
9
|
+
* agent-mem track --enable — add .context/ to project git
|
|
10
|
+
* agent-mem track --disable — add .context/ to .gitignore
|
|
11
|
+
*/
|
|
12
|
+
export default async function track({ args, flags }) {
|
|
13
|
+
const root = flags._contextRoot;
|
|
14
|
+
const gitignorePath = join(root, '.gitignore');
|
|
15
|
+
|
|
16
|
+
const hasGitignore = existsSync(gitignorePath);
|
|
17
|
+
const gitignoreContent = hasGitignore ? readFileSync(gitignorePath, 'utf-8') : '';
|
|
18
|
+
const isIgnored = gitignoreContent
|
|
19
|
+
.split('\n')
|
|
20
|
+
.some((l) => l.trim() === '.context/' || l.trim() === '.context');
|
|
21
|
+
|
|
22
|
+
if (flags.enable) {
|
|
23
|
+
if (!isIgnored) {
|
|
24
|
+
console.log('✅ .context/ is already tracked (not in .gitignore).');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Remove .context/ from .gitignore
|
|
29
|
+
const lines = gitignoreContent
|
|
30
|
+
.split('\n')
|
|
31
|
+
.filter((l) => l.trim() !== '.context/' && l.trim() !== '.context');
|
|
32
|
+
writeFileSync(gitignorePath, lines.join('\n'));
|
|
33
|
+
console.log('✅ TRACKED: .context/ removed from .gitignore');
|
|
34
|
+
console.log('Context will be committed with your project code.');
|
|
35
|
+
console.log("Run: git add .context/ && git commit -m 'track agent context'");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (flags.disable) {
|
|
40
|
+
if (isIgnored) {
|
|
41
|
+
console.log('✅ .context/ is already ignored.');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Add .context/ to .gitignore
|
|
46
|
+
const newContent = gitignoreContent.trimEnd() + '\n\n# agent-mem (local only)\n.context/\n';
|
|
47
|
+
writeFileSync(gitignorePath, newContent);
|
|
48
|
+
console.log('✅ UNTRACKED: .context/ added to .gitignore');
|
|
49
|
+
console.log("Context stays local — use 'agent-mem push' to sync across machines.");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Status
|
|
54
|
+
console.log(`📊 TRACKING STATUS:`);
|
|
55
|
+
console.log(
|
|
56
|
+
` .context/ is ${isIgnored ? 'IGNORED (local only)' : 'TRACKED (committed with project)'}`,
|
|
57
|
+
);
|
|
58
|
+
console.log('');
|
|
59
|
+
console.log(' agent-mem track --enable — commit context with project');
|
|
60
|
+
console.log(' agent-mem track --disable — keep context local only');
|
|
61
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { contextDir as getContextDir } from '../core/context-root.js';
|
|
2
|
+
import { moveContextFile } from '../core/fs.js';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { join, basename } from 'node:path';
|
|
5
|
+
|
|
6
|
+
export default async function unpin({ args, flags }) {
|
|
7
|
+
const root = flags._contextRoot;
|
|
8
|
+
const ctxDir = getContextDir(root);
|
|
9
|
+
|
|
10
|
+
if (!args.length) {
|
|
11
|
+
console.error('❌ Usage: agent-mem unpin <path>');
|
|
12
|
+
console.error('Example: agent-mem unpin system/old-conventions.md');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const relPath = args[0].startsWith('system/') ? args[0] : `system/${args[0]}`;
|
|
17
|
+
const name = basename(relPath);
|
|
18
|
+
const dest = `memory/${name}`;
|
|
19
|
+
|
|
20
|
+
if (!existsSync(join(ctxDir, relPath))) {
|
|
21
|
+
console.error(`❌ File not found: .context/${relPath}`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
moveContextFile(ctxDir, relPath, dest);
|
|
26
|
+
console.log(`📌 UNPINNED: ${relPath} → ${dest}`);
|
|
27
|
+
console.log(`File moved to memory/. It will show in tree but not be auto-loaded.`);
|
|
28
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { contextDir as getContextDir } from '../core/context-root.js';
|
|
2
|
+
import { writeContextFile, readContextFile } from '../core/fs.js';
|
|
3
|
+
|
|
4
|
+
export default async function write({ args, flags }) {
|
|
5
|
+
const root = flags._contextRoot;
|
|
6
|
+
const ctxDir = getContextDir(root);
|
|
7
|
+
|
|
8
|
+
if (!args.length) {
|
|
9
|
+
console.error('❌ Usage: agent-mem write <path> --content <text>');
|
|
10
|
+
console.error(" Or pipe: echo 'content' | agent-mem write <path>");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const relPath = args[0];
|
|
15
|
+
let content = flags.content;
|
|
16
|
+
|
|
17
|
+
// If --content not provided, read from stdin
|
|
18
|
+
if (!content) {
|
|
19
|
+
// Remaining args as content
|
|
20
|
+
if (args.length > 1) {
|
|
21
|
+
content = args.slice(1).join(' ');
|
|
22
|
+
} else {
|
|
23
|
+
// Try stdin
|
|
24
|
+
content = await readStdin();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!content) {
|
|
29
|
+
console.error('❌ No content provided. Use --content or pipe stdin.');
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const existed = readContextFile(ctxDir, relPath) !== null;
|
|
34
|
+
writeContextFile(ctxDir, relPath, content);
|
|
35
|
+
|
|
36
|
+
console.log(
|
|
37
|
+
`✅ ${existed ? 'UPDATED' : 'CREATED'}: .context/${relPath} (${content.length} chars)`,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// Auto-commit if enabled
|
|
41
|
+
const { maybeAutoCommit } = await import('../core/auto-commit.js');
|
|
42
|
+
maybeAutoCommit(ctxDir, `write ${relPath}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readStdin() {
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
if (process.stdin.isTTY) {
|
|
48
|
+
resolve(null);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
let data = '';
|
|
52
|
+
process.stdin.setEncoding('utf-8');
|
|
53
|
+
process.stdin.on('data', (chunk) => (data += chunk));
|
|
54
|
+
process.stdin.on('end', () => resolve(data || null));
|
|
55
|
+
// Timeout after 100ms if no data
|
|
56
|
+
setTimeout(() => resolve(data || null), 100);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { readConfig } from './config.js';
|
|
2
|
+
import { commitContext, commitCount } from './git.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Check if auto-commit should fire, and commit if so.
|
|
6
|
+
* Tracks mutations via commit count modulo interval.
|
|
7
|
+
*/
|
|
8
|
+
export function maybeAutoCommit(contextDir, changeDescription) {
|
|
9
|
+
const config = readConfig(contextDir);
|
|
10
|
+
if (!config.auto_commit) return;
|
|
11
|
+
|
|
12
|
+
const interval = config.auto_commit_interval || 10;
|
|
13
|
+
const count = commitCount(contextDir);
|
|
14
|
+
|
|
15
|
+
// Simple heuristic: commit if we have uncommitted changes
|
|
16
|
+
// and count of commits since last is divisible by interval
|
|
17
|
+
// For now: just auto-commit every mutation when enabled
|
|
18
|
+
const hash = commitContext(contextDir, `auto: ${changeDescription}`);
|
|
19
|
+
if (hash) {
|
|
20
|
+
console.log(` ⚡ Auto-committed: ${hash}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { readContextFile, writeContextFile } from './fs.js';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_CONFIG = {
|
|
4
|
+
auto_commit: false,
|
|
5
|
+
auto_commit_interval: 10,
|
|
6
|
+
reflection: {
|
|
7
|
+
trigger: 'manual',
|
|
8
|
+
frequency: 5,
|
|
9
|
+
model: null,
|
|
10
|
+
defrag_threshold: 50,
|
|
11
|
+
defrag_size_kb: 10,
|
|
12
|
+
stale_days: 30,
|
|
13
|
+
},
|
|
14
|
+
compact: {
|
|
15
|
+
retain_days: 7,
|
|
16
|
+
},
|
|
17
|
+
system_files_max: 10,
|
|
18
|
+
memory_files_max: 25,
|
|
19
|
+
branch: 'main',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Read config from .context/config.yaml (simple YAML-like parser).
|
|
24
|
+
*/
|
|
25
|
+
export function readConfig(contextDir) {
|
|
26
|
+
const raw = readContextFile(contextDir, 'config.yaml');
|
|
27
|
+
if (!raw) return { ...DEFAULT_CONFIG };
|
|
28
|
+
|
|
29
|
+
// Simple YAML parser for flat + one-level nested
|
|
30
|
+
const config = { ...DEFAULT_CONFIG };
|
|
31
|
+
let currentSection = null;
|
|
32
|
+
|
|
33
|
+
for (const line of raw.split('\n')) {
|
|
34
|
+
if (line.startsWith('#') || !line.trim()) continue;
|
|
35
|
+
|
|
36
|
+
const indent = line.length - line.trimStart().length;
|
|
37
|
+
const match = line.trim().match(/^(\w[\w_]*)\s*:\s*(.*)$/);
|
|
38
|
+
if (!match) continue;
|
|
39
|
+
|
|
40
|
+
const [, key, rawVal] = match;
|
|
41
|
+
const val = parseVal(rawVal);
|
|
42
|
+
|
|
43
|
+
if (indent === 0) {
|
|
44
|
+
if (rawVal === '') {
|
|
45
|
+
// Section header
|
|
46
|
+
currentSection = key;
|
|
47
|
+
if (typeof config[key] !== 'object') config[key] = {};
|
|
48
|
+
} else {
|
|
49
|
+
config[key] = val;
|
|
50
|
+
currentSection = null;
|
|
51
|
+
}
|
|
52
|
+
} else if (currentSection && config[currentSection]) {
|
|
53
|
+
config[currentSection][key] = val;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return config;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Write config to .context/config.yaml.
|
|
62
|
+
*/
|
|
63
|
+
export function writeConfig(contextDir, config) {
|
|
64
|
+
const lines = ['# agent-mem configuration'];
|
|
65
|
+
|
|
66
|
+
for (const [key, val] of Object.entries(config)) {
|
|
67
|
+
if (val && typeof val === 'object' && !Array.isArray(val)) {
|
|
68
|
+
lines.push(`${key}:`);
|
|
69
|
+
for (const [k, v] of Object.entries(val)) {
|
|
70
|
+
lines.push(` ${k}: ${serializeVal(v)}`);
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
lines.push(`${key}: ${serializeVal(val)}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
writeContextFile(contextDir, 'config.yaml', lines.join('\n') + '\n');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseVal(raw) {
|
|
81
|
+
const s = raw.trim().replace(/^['"]|['"]$/g, '');
|
|
82
|
+
if (s === 'true') return true;
|
|
83
|
+
if (s === 'false') return false;
|
|
84
|
+
if (s === 'null' || s === '') return null;
|
|
85
|
+
if (/^\d+$/.test(s)) return parseInt(s, 10);
|
|
86
|
+
return s;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function serializeVal(val) {
|
|
90
|
+
if (val === null || val === undefined) return 'null';
|
|
91
|
+
if (typeof val === 'boolean') return val ? 'true' : 'false';
|
|
92
|
+
return String(val);
|
|
93
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join, dirname, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Walk up from `cwd` looking for a .context/ directory.
|
|
6
|
+
* Returns the project root (parent of .context/) or null.
|
|
7
|
+
*/
|
|
8
|
+
export function findContextRoot(cwd) {
|
|
9
|
+
let dir = resolve(cwd);
|
|
10
|
+
const root = dirname(dir) === dir ? dir : '/'; // filesystem root
|
|
11
|
+
|
|
12
|
+
while (true) {
|
|
13
|
+
if (existsSync(join(dir, '.context'))) {
|
|
14
|
+
return dir;
|
|
15
|
+
}
|
|
16
|
+
const parent = dirname(dir);
|
|
17
|
+
if (parent === dir) break; // reached filesystem root
|
|
18
|
+
dir = parent;
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get the .context/ directory path from project root.
|
|
25
|
+
*/
|
|
26
|
+
export function contextDir(projectRoot) {
|
|
27
|
+
return join(projectRoot, '.context');
|
|
28
|
+
}
|