memoir-cli 3.6.1 → 3.8.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.
@@ -0,0 +1,117 @@
1
+ // Inject / update the pinned session block in target files.
2
+ //
3
+ // Primary target: ~/.claude/CLAUDE.md (user-global, always loaded by Claude Code
4
+ // regardless of MEMORY.md truncation).
5
+ //
6
+ // Rules:
7
+ // - If markers are present, replace the block in place. Nothing else is touched.
8
+ // - If markers are absent, prepend the block at the top of the file (after any
9
+ // leading frontmatter or title line).
10
+ // - If the file doesn't exist, create it containing only the block.
11
+ // - Never touch content outside the markers.
12
+
13
+ import fs from 'fs-extra';
14
+ import path from 'path';
15
+ import os from 'os';
16
+ import { BLOCK_START, BLOCK_END } from './render.js';
17
+
18
+ const home = os.homedir();
19
+ const isWin = process.platform === 'win32';
20
+ const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
21
+
22
+ // Every target that memoir knows how to inject the pinned block into.
23
+ // Added when we extend cross-tool support. Each target is a single file path.
24
+ //
25
+ // Claude: ~/.claude/CLAUDE.md (always loaded by Claude Code)
26
+ // Cursor: ~/.cursor/rules/memoir-session.mdc (global user rules — auto-loaded)
27
+ // Windsurf: {AppSupport}/Windsurf/User/memoir-session.md (user-global instructions)
28
+ // Gemini: ~/.gemini/GEMINI.md (user-global)
29
+ export const INJECTION_TARGETS = {
30
+ claude: path.join(home, '.claude', 'CLAUDE.md'),
31
+ cursor: path.join(home, '.cursor', 'rules', 'memoir-session.mdc'),
32
+ windsurf: isWin
33
+ ? path.join(appData, 'Windsurf', 'User', 'memoir-session.md')
34
+ : path.join(home, 'Library', 'Application Support', 'Windsurf', 'User', 'memoir-session.md'),
35
+ gemini: path.join(home, '.gemini', 'GEMINI.md'),
36
+ };
37
+
38
+ // Returns the target paths whose parent infrastructure exists — i.e. the tool
39
+ // is actually installed. Avoids creating empty tool dirs for tools the user
40
+ // doesn't use.
41
+ export function detectAvailableTargets() {
42
+ const detectors = {
43
+ claude: path.join(home, '.claude'),
44
+ cursor: path.join(home, '.cursor'),
45
+ windsurf: isWin
46
+ ? path.join(appData, 'Windsurf', 'User')
47
+ : path.join(home, 'Library', 'Application Support', 'Windsurf', 'User'),
48
+ gemini: path.join(home, '.gemini'),
49
+ };
50
+ const available = {};
51
+ for (const [name, dir] of Object.entries(detectors)) {
52
+ try {
53
+ if (fs.existsSync(dir)) available[name] = INJECTION_TARGETS[name];
54
+ } catch {}
55
+ }
56
+ return available;
57
+ }
58
+
59
+ // Pattern that matches the whole block including markers. Tolerant of the
60
+ // marker text evolving across versions — anchors on `memoir:session-block`.
61
+ const BLOCK_RE = /<!--\s*memoir:session-block[^>]*-->[\s\S]*?<!--\s*\/memoir:session-block\s*-->/;
62
+
63
+ export async function injectInto(targetPath, renderedBlock) {
64
+ await fs.ensureDir(path.dirname(targetPath));
65
+
66
+ let content = '';
67
+ let existed = false;
68
+ try {
69
+ content = await fs.readFile(targetPath, 'utf8');
70
+ existed = true;
71
+ } catch {
72
+ // Doesn't exist yet — will create.
73
+ }
74
+
75
+ const updated = applyBlock(content, renderedBlock, existed);
76
+
77
+ // Atomic write
78
+ const tmp = `${targetPath}.tmp-${process.pid}`;
79
+ await fs.writeFile(tmp, updated);
80
+ await fs.move(tmp, targetPath, { overwrite: true });
81
+
82
+ return { path: targetPath, created: !existed, replaced: existed && BLOCK_RE.test(content) };
83
+ }
84
+
85
+ // Apply the block to existing content. Exported for tests.
86
+ export function applyBlock(content, renderedBlock, existed = true) {
87
+ if (!existed || content.trim() === '') {
88
+ // Fresh file — block only, plus a trailing newline for poetry.
89
+ return renderedBlock + '\n';
90
+ }
91
+
92
+ if (BLOCK_RE.test(content)) {
93
+ // Replace in place.
94
+ return content.replace(BLOCK_RE, renderedBlock);
95
+ }
96
+
97
+ // No existing block — prepend. Preserve any H1 title at the top by placing
98
+ // the block immediately after it. Otherwise put it at the very top.
99
+ const h1Match = content.match(/^(#\s.+\n+)/);
100
+ if (h1Match) {
101
+ return h1Match[1] + renderedBlock + '\n\n' + content.slice(h1Match[1].length);
102
+ }
103
+ return renderedBlock + '\n\n' + content;
104
+ }
105
+
106
+ // Remove the block, if present. Used when user wants memoir to stop managing CLAUDE.md.
107
+ export async function uninjectFrom(targetPath) {
108
+ if (!await fs.pathExists(targetPath)) return { removed: false };
109
+ const content = await fs.readFile(targetPath, 'utf8');
110
+ if (!BLOCK_RE.test(content)) return { removed: false };
111
+ // Strip the block and any trailing blank lines that follow it
112
+ const cleaned = content.replace(BLOCK_RE, '').replace(/\n{3,}/g, '\n\n').trimStart();
113
+ const tmp = `${targetPath}.tmp-${process.pid}`;
114
+ await fs.writeFile(tmp, cleaned);
115
+ await fs.move(tmp, targetPath, { overwrite: true });
116
+ return { removed: true };
117
+ }
@@ -0,0 +1,114 @@
1
+ // Render session state → pinned markdown block.
2
+ // The block is wrapped in <!-- memoir:session-block v1 --> markers so inject.js
3
+ // can find and replace it without touching anything else in CLAUDE.md.
4
+
5
+ export const BLOCK_START = '<!-- memoir:session-block v1 — managed by memoir, edit via `memoir goal/next/note` -->';
6
+ export const BLOCK_END = '<!-- /memoir:session-block -->';
7
+
8
+ const MAX_RENDERED_GOALS = 2;
9
+ const MAX_RENDERED_NEXT = 6;
10
+ const MAX_RENDERED_QUESTIONS = 4;
11
+ const MAX_RENDERED_DECISIONS = 5;
12
+ const MAX_RENDERED_HISTORY = 5;
13
+
14
+ export function renderSession(state) {
15
+ if (!state) return renderEmpty();
16
+
17
+ const lines = [BLOCK_START, '## 🎯 Continuing from where we left off', ''];
18
+
19
+ const goals = (state.current?.goals || []).slice(0, MAX_RENDERED_GOALS);
20
+ const nexts = (state.current?.next_actions || []).slice(-MAX_RENDERED_NEXT).reverse();
21
+ const questions = (state.current?.open_questions || []).slice(-MAX_RENDERED_QUESTIONS).reverse();
22
+ const decisions = (state.current?.decisions || []).slice(0, MAX_RENDERED_DECISIONS);
23
+ const history = (state.history || []).slice(0, MAX_RENDERED_HISTORY);
24
+
25
+ const everythingEmpty = !goals.length && !nexts.length && !questions.length && !decisions.length && !history.length;
26
+ if (everythingEmpty) return renderEmpty();
27
+
28
+ // Goals — show current goal prominently
29
+ if (goals.length === 1) {
30
+ lines.push(`**Current goal:** ${goals[0].text}${machineTag(goals[0], state)}`);
31
+ lines.push('');
32
+ } else if (goals.length > 1) {
33
+ lines.push('**Goals:**');
34
+ for (const g of goals) lines.push(`- ${g.text}${machineTag(g, state)}`);
35
+ lines.push('');
36
+ }
37
+
38
+ // Next actions — checkbox format so they read as actionable
39
+ if (nexts.length) {
40
+ lines.push('**Next:**');
41
+ for (const n of nexts) {
42
+ lines.push(`- [ ] ${n.text}${machineTag(n, state)}`);
43
+ }
44
+ lines.push('');
45
+ }
46
+
47
+ // Open questions
48
+ if (questions.length) {
49
+ lines.push('**Open questions:**');
50
+ for (const q of questions) lines.push(`- ${q.text}${machineTag(q, state)}`);
51
+ lines.push('');
52
+ }
53
+
54
+ // Recent decisions
55
+ if (decisions.length) {
56
+ lines.push('**Recent decisions:**');
57
+ for (const d of decisions) {
58
+ let line = `- ${d.text}`;
59
+ if (d.why) line += ` — *${d.why}*`;
60
+ line += machineTag(d, state);
61
+ lines.push(line);
62
+ }
63
+ lines.push('');
64
+ }
65
+
66
+ // Recent session history — machine-tagged so user sees cross-machine trail
67
+ if (history.length) {
68
+ lines.push('**Recent sessions:**');
69
+ for (const h of history) {
70
+ const date = (h.date || '').slice(0, 10);
71
+ const machineLabel = labelFor(h.machine_id, state) || 'unknown';
72
+ const dur = h.duration_min ? ` (${formatDuration(h.duration_min)})` : '';
73
+ const summary = h.summary || '—';
74
+ lines.push(`- ${date} ${machineLabel}${dur}: ${summary}`);
75
+ }
76
+ lines.push('');
77
+ }
78
+
79
+ lines.push(BLOCK_END);
80
+ return lines.join('\n');
81
+ }
82
+
83
+ function machineTag(item, state) {
84
+ if (!item?.machine_id) return '';
85
+ const label = labelFor(item.machine_id, state);
86
+ // Only show machine tag if we have more than one machine — otherwise it's noise
87
+ const machineCount = Object.keys(state?.machines || {}).length;
88
+ if (machineCount <= 1 || !label) return '';
89
+ return ` _(${label})_`;
90
+ }
91
+
92
+ function labelFor(machineId, state) {
93
+ if (!machineId) return null;
94
+ return state?.machines?.[machineId]?.label || null;
95
+ }
96
+
97
+ function formatDuration(min) {
98
+ if (!min || min <= 0) return '';
99
+ if (min < 60) return `${min}m`;
100
+ const h = Math.floor(min / 60);
101
+ const m = min % 60;
102
+ return m ? `${h}h ${m}m` : `${h}h`;
103
+ }
104
+
105
+ function renderEmpty() {
106
+ return [
107
+ BLOCK_START,
108
+ '## 🎯 Continuing from where we left off',
109
+ '',
110
+ '_No session context yet. Set one with:_ `memoir goal "your current focus"`',
111
+ '',
112
+ BLOCK_END,
113
+ ].join('\n');
114
+ }
@@ -0,0 +1,296 @@
1
+ // Session state: the canonical source of truth for "what are we working on"
2
+ // across sessions and machines. Rendered into CLAUDE.md (and other tools) as a
3
+ // pinned block at the top, guaranteed to load.
4
+ //
5
+ // File: ~/.config/memoir/session.json
6
+ // See CLAUDE.md pinned block for how this gets displayed.
7
+
8
+ import fs from 'fs-extra';
9
+ import path from 'path';
10
+ import os from 'os';
11
+ import crypto from 'crypto';
12
+
13
+ const home = os.homedir();
14
+ const CONFIG_DIR = path.join(home, '.config', 'memoir');
15
+ const SESSION_PATH = path.join(CONFIG_DIR, 'session.json');
16
+ const MACHINE_ID_PATH = path.join(CONFIG_DIR, 'machine.id');
17
+
18
+ export const SCHEMA_VERSION = 1;
19
+
20
+ // Maximum items kept in each list before oldest entries rotate into history.
21
+ // Prevents unbounded growth of the live pinned block.
22
+ const MAX_GOALS = 3;
23
+ const MAX_NEXT = 8;
24
+ const MAX_QUESTIONS = 5;
25
+ const MAX_DECISIONS_RECENT = 10;
26
+ const MAX_HISTORY = 30;
27
+
28
+ // ── Machine identity ─────────────────────────────────────────────
29
+
30
+ // Stable per-machine identifier. Persisted once, reused forever.
31
+ // We pair a UUID (stable across hostname changes) with a human label (hostname)
32
+ // for display — "mac-mini (abc1234)".
33
+ export async function getMachineId() {
34
+ try {
35
+ if (await fs.pathExists(MACHINE_ID_PATH)) {
36
+ const id = (await fs.readFile(MACHINE_ID_PATH, 'utf8')).trim();
37
+ if (id) return { id, label: os.hostname() };
38
+ }
39
+ } catch {}
40
+
41
+ const id = crypto.randomUUID();
42
+ await fs.ensureDir(CONFIG_DIR);
43
+ await fs.writeFile(MACHINE_ID_PATH, id);
44
+ return { id, label: os.hostname() };
45
+ }
46
+
47
+ // ── Schema ───────────────────────────────────────────────────────
48
+
49
+ function emptySession() {
50
+ return {
51
+ version: SCHEMA_VERSION,
52
+ created_at: new Date().toISOString(),
53
+ updated_at: new Date().toISOString(),
54
+ machines: {}, // { [machineId]: { label, last_seen } }
55
+ current: {
56
+ goals: [], // { text, machine_id, set_on }
57
+ next_actions: [], // { text, machine_id, added, completed? }
58
+ open_questions: [],// { text, machine_id, asked }
59
+ decisions: [], // { text, why?, rejected?, machine_id, date }
60
+ },
61
+ history: [], // { date, machine_id, summary, files_touched, duration_min? }
62
+ };
63
+ }
64
+
65
+ // ── Read / write ─────────────────────────────────────────────────
66
+
67
+ // Atomic read with graceful recovery from corrupted JSON.
68
+ export async function readSession() {
69
+ if (!await fs.pathExists(SESSION_PATH)) return emptySession();
70
+
71
+ try {
72
+ const raw = await fs.readFile(SESSION_PATH, 'utf8');
73
+ const parsed = JSON.parse(raw);
74
+ return migrateIfNeeded(parsed);
75
+ } catch (err) {
76
+ // Corrupted — preserve it for inspection, start fresh.
77
+ const backup = `${SESSION_PATH}.corrupted-${Date.now()}`;
78
+ try { await fs.copy(SESSION_PATH, backup); } catch {}
79
+ return emptySession();
80
+ }
81
+ }
82
+
83
+ // Atomic write: write to tmp, rename. Prevents torn writes on crash.
84
+ export async function writeSession(state) {
85
+ await fs.ensureDir(CONFIG_DIR);
86
+ state.updated_at = new Date().toISOString();
87
+ const tmp = `${SESSION_PATH}.tmp-${process.pid}`;
88
+ await fs.writeFile(tmp, JSON.stringify(state, null, 2));
89
+ await fs.move(tmp, SESSION_PATH, { overwrite: true });
90
+ }
91
+
92
+ function migrateIfNeeded(state) {
93
+ if (state && state.version === SCHEMA_VERSION) return state;
94
+ // Future versions: add migration steps here.
95
+ // For now, if version mismatch, merge defaults to fill gaps.
96
+ const fresh = emptySession();
97
+ return {
98
+ ...fresh,
99
+ ...state,
100
+ version: SCHEMA_VERSION,
101
+ current: { ...fresh.current, ...(state?.current || {}) },
102
+ machines: { ...fresh.machines, ...(state?.machines || {}) },
103
+ history: Array.isArray(state?.history) ? state.history : [],
104
+ };
105
+ }
106
+
107
+ // ── Machine registration ────────────────────────────────────────
108
+
109
+ async function touchMachine(state) {
110
+ const { id, label } = await getMachineId();
111
+ state.machines[id] = {
112
+ label,
113
+ last_seen: new Date().toISOString(),
114
+ };
115
+ return id;
116
+ }
117
+
118
+ // ── Mutators ────────────────────────────────────────────────────
119
+
120
+ export async function addGoal(text) {
121
+ const state = await readSession();
122
+ const machineId = await touchMachine(state);
123
+ state.current.goals.unshift({
124
+ text,
125
+ machine_id: machineId,
126
+ set_on: new Date().toISOString(),
127
+ });
128
+ state.current.goals = state.current.goals.slice(0, MAX_GOALS);
129
+ await writeSession(state);
130
+ return state;
131
+ }
132
+
133
+ export async function addNext(text) {
134
+ const state = await readSession();
135
+ const machineId = await touchMachine(state);
136
+ // Dedupe by text (case-insensitive)
137
+ const normalized = text.trim().toLowerCase();
138
+ const exists = state.current.next_actions.some(a => a.text.trim().toLowerCase() === normalized);
139
+ if (!exists) {
140
+ state.current.next_actions.push({
141
+ text,
142
+ machine_id: machineId,
143
+ added: new Date().toISOString(),
144
+ });
145
+ state.current.next_actions = state.current.next_actions.slice(-MAX_NEXT);
146
+ }
147
+ await writeSession(state);
148
+ return state;
149
+ }
150
+
151
+ export async function completeNext(textOrIndex) {
152
+ const state = await readSession();
153
+ await touchMachine(state);
154
+ let idx = -1;
155
+ if (typeof textOrIndex === 'number') {
156
+ idx = textOrIndex;
157
+ } else {
158
+ const normalized = String(textOrIndex).trim().toLowerCase();
159
+ idx = state.current.next_actions.findIndex(a => a.text.trim().toLowerCase().includes(normalized));
160
+ }
161
+ if (idx >= 0) {
162
+ state.current.next_actions.splice(idx, 1);
163
+ }
164
+ await writeSession(state);
165
+ return state;
166
+ }
167
+
168
+ export async function addNote(text, opts = {}) {
169
+ const state = await readSession();
170
+ const machineId = await touchMachine(state);
171
+ const decision = {
172
+ text,
173
+ machine_id: machineId,
174
+ date: new Date().toISOString(),
175
+ };
176
+ if (opts.why) decision.why = opts.why;
177
+ if (opts.rejected) decision.rejected = opts.rejected;
178
+ state.current.decisions.unshift(decision);
179
+ state.current.decisions = state.current.decisions.slice(0, MAX_DECISIONS_RECENT);
180
+ await writeSession(state);
181
+ return state;
182
+ }
183
+
184
+ export async function addQuestion(text) {
185
+ const state = await readSession();
186
+ const machineId = await touchMachine(state);
187
+ state.current.open_questions.push({
188
+ text,
189
+ machine_id: machineId,
190
+ asked: new Date().toISOString(),
191
+ });
192
+ state.current.open_questions = state.current.open_questions.slice(-MAX_QUESTIONS);
193
+ await writeSession(state);
194
+ return state;
195
+ }
196
+
197
+ // Roll up the current state into a history entry. Use at session end / push.
198
+ // Does not clear `current` — these are "the working set," not per-session scratch.
199
+ export async function recordSessionEnd({ summary, filesTouched = [], durationMin = null } = {}) {
200
+ const state = await readSession();
201
+ const machineId = await touchMachine(state);
202
+ state.history.unshift({
203
+ date: new Date().toISOString(),
204
+ machine_id: machineId,
205
+ summary: summary || '',
206
+ files_touched: filesTouched.slice(0, 20),
207
+ duration_min: durationMin,
208
+ });
209
+ state.history = state.history.slice(0, MAX_HISTORY);
210
+ await writeSession(state);
211
+ return state;
212
+ }
213
+
214
+ // ── Cross-machine merge ─────────────────────────────────────────
215
+
216
+ // Merge a remote session (from another machine's backup) into local.
217
+ // Never clobbers — unions lists, dedupes by text, keeps newest timestamp.
218
+ // Machine entries accumulate so we can show "last seen on X".
219
+ export function mergeSessions(local, remote) {
220
+ if (!remote) return local;
221
+ if (!local) local = { ...remote };
222
+
223
+ const merged = {
224
+ version: SCHEMA_VERSION,
225
+ created_at: earliest(local.created_at, remote.created_at),
226
+ updated_at: latest(local.updated_at, remote.updated_at),
227
+ machines: { ...remote.machines, ...local.machines }, // local wins for same machine
228
+ current: {
229
+ goals: unionByText(local.current?.goals, remote.current?.goals, 'set_on', MAX_GOALS),
230
+ next_actions: unionByText(local.current?.next_actions, remote.current?.next_actions, 'added', MAX_NEXT),
231
+ open_questions: unionByText(local.current?.open_questions, remote.current?.open_questions, 'asked', MAX_QUESTIONS),
232
+ decisions: unionByText(local.current?.decisions, remote.current?.decisions, 'date', MAX_DECISIONS_RECENT),
233
+ },
234
+ history: mergeHistory(local.history, remote.history),
235
+ };
236
+
237
+ // machines: union last_seen per id (take the newer)
238
+ for (const [id, entry] of Object.entries(remote.machines || {})) {
239
+ const existing = merged.machines[id];
240
+ if (!existing || new Date(entry.last_seen) > new Date(existing.last_seen)) {
241
+ merged.machines[id] = entry;
242
+ }
243
+ }
244
+
245
+ return merged;
246
+ }
247
+
248
+ function unionByText(a = [], b = [], dateField, cap) {
249
+ const byText = new Map();
250
+ for (const item of [...a, ...b]) {
251
+ if (!item || !item.text) continue;
252
+ const key = item.text.trim().toLowerCase();
253
+ const existing = byText.get(key);
254
+ if (!existing || new Date(item[dateField] || 0) > new Date(existing[dateField] || 0)) {
255
+ byText.set(key, item);
256
+ }
257
+ }
258
+ return Array.from(byText.values())
259
+ .sort((x, y) => new Date(y[dateField] || 0) - new Date(x[dateField] || 0))
260
+ .slice(0, cap);
261
+ }
262
+
263
+ function mergeHistory(a = [], b = []) {
264
+ const seen = new Set();
265
+ const all = [...a, ...b].filter(h => h && h.date);
266
+ // Dedupe by (date + machine_id + summary) — the three keys that make a session unique
267
+ const unique = all.filter(h => {
268
+ const key = `${h.date}|${h.machine_id}|${(h.summary || '').slice(0, 50)}`;
269
+ if (seen.has(key)) return false;
270
+ seen.add(key);
271
+ return true;
272
+ });
273
+ return unique
274
+ .sort((x, y) => new Date(y.date) - new Date(x.date))
275
+ .slice(0, MAX_HISTORY);
276
+ }
277
+
278
+ function earliest(a, b) {
279
+ if (!a) return b;
280
+ if (!b) return a;
281
+ return new Date(a) < new Date(b) ? a : b;
282
+ }
283
+
284
+ function latest(a, b) {
285
+ if (!a) return b;
286
+ if (!b) return a;
287
+ return new Date(a) > new Date(b) ? a : b;
288
+ }
289
+
290
+ // ── Paths (exported for tests + other modules) ──────────────────
291
+
292
+ export const paths = {
293
+ config: CONFIG_DIR,
294
+ session: SESSION_PATH,
295
+ machineId: MACHINE_ID_PATH,
296
+ };
@@ -1,26 +0,0 @@
1
- ---
2
- name: Bug report
3
- about: Something isn't working
4
- title: ''
5
- labels: bug
6
- assignees: ''
7
- ---
8
-
9
- **What happened?**
10
- A clear description of the bug.
11
-
12
- **Steps to reproduce**
13
- 1. Run `memoir ...`
14
- 2. See error
15
-
16
- **Expected behavior**
17
- What you expected to happen.
18
-
19
- **Environment**
20
- - OS: [e.g. macOS 15, Windows 11, Ubuntu 24]
21
- - Node: [e.g. 20.11.0]
22
- - memoir version: [e.g. 3.2.2]
23
- - AI tools: [e.g. Claude Code, Cursor]
24
-
25
- **Logs / screenshots**
26
- Paste any error output here.
@@ -1,16 +0,0 @@
1
- ---
2
- name: Feature request
3
- about: Suggest an idea for memoir
4
- title: ''
5
- labels: enhancement
6
- assignees: ''
7
- ---
8
-
9
- **What would you like?**
10
- A clear description of the feature.
11
-
12
- **Why?**
13
- What problem does this solve for you?
14
-
15
- **Alternatives considered**
16
- Any workarounds you've tried.
package/CONTRIBUTING.md DELETED
@@ -1,47 +0,0 @@
1
- # Contributing to memoir
2
-
3
- Thanks for your interest in contributing! memoir is open source and welcomes contributions.
4
-
5
- ## Quick start
6
-
7
- ```bash
8
- git clone https://github.com/camgitt/memoir.git
9
- cd memoir
10
- npm install
11
- node bin/memoir.js status
12
- ```
13
-
14
- ## What to work on
15
-
16
- - **New tool adapters** — add support for more AI tools in `src/tools/`
17
- - **MCP improvements** — enhance the MCP server in `src/mcp.js`
18
- - **Bug fixes** — check [open issues](https://github.com/camgitt/memoir/issues)
19
- - **Documentation** — improve README, add examples
20
-
21
- ## Submitting changes
22
-
23
- 1. Fork the repo
24
- 2. Create a branch (`git checkout -b feature/my-feature`)
25
- 3. Make your changes
26
- 4. Test locally: `npm test`
27
- 5. Commit and push
28
- 6. Open a PR with a clear description
29
-
30
- ## Code style
31
-
32
- - ES modules (`import`/`export`)
33
- - No TypeScript (plain JS)
34
- - Keep dependencies minimal
35
-
36
- ## Adding a tool adapter
37
-
38
- See `src/tools/claude.js` for an example. Each adapter exports:
39
- - `name` — display name
40
- - `icon` — emoji
41
- - `source` — path to the tool's config directory
42
- - `files` — specific files to sync (if `customExtract` is true)
43
- - `filter` — function to include/exclude files
44
-
45
- ## Questions?
46
-
47
- Open an issue or start a discussion.