@kevin0181/memoc 1.0.0 → 1.0.1

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.
Files changed (4) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +169 -155
  3. package/bin/cli.js +1476 -1421
  4. package/package.json +37 -34
package/bin/cli.js CHANGED
@@ -1,1489 +1,1544 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const fs = require('fs');
5
- const path = require('path');
6
-
7
- const VERSION = (() => {
8
- try { return JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).version; }
9
- catch { return 'unknown'; }
10
- })();
11
-
12
- // ═══════════════════════════════════════════════════════════════════
13
- // SCANNER — detects project type from filesystem
14
- // ═══════════════════════════════════════════════════════════════════
15
-
16
- function scanProject(dir, depth = 0) {
17
- const info = {
18
- name: path.basename(dir),
19
- root: dir,
20
- stack: [],
21
- scripts: {},
22
- configFiles: [],
23
- srcDirs: [],
24
- isEmpty: true,
25
- };
26
-
27
- let entries = [];
28
- try { entries = fs.readdirSync(dir); } catch { return info; }
29
-
30
- const IGNORE = new Set([
31
- 'node_modules', '.git', '.next', 'dist', 'build', 'out',
32
- 'Saved', 'Intermediate', 'DerivedDataCache', 'Binaries',
33
- '.memoc', 'skills', '.DS_Store', '.obsidian',
34
- 'CLAUDE.md', 'AGENTS.md', 'llms.txt',
35
- ]);
36
-
37
- info.srcDirs = entries.filter(e => {
38
- try { return !IGNORE.has(e) && fs.statSync(path.join(dir, e)).isDirectory(); }
39
- catch { return false; }
40
- });
41
-
42
- const KNOWN_CONFIGS = [
43
- 'package.json', 'tsconfig.json', 'jsconfig.json',
44
- 'next.config.js', 'next.config.ts', 'next.config.mjs',
45
- 'vite.config.js', 'vite.config.ts',
46
- 'tailwind.config.js', 'tailwind.config.ts',
47
- 'webpack.config.js', 'astro.config.mjs',
48
- 'svelte.config.js', 'nuxt.config.ts',
49
- '.env', '.env.example', '.env.local',
50
- 'Makefile', 'CMakeLists.txt',
51
- 'Dockerfile', 'docker-compose.yml', 'compose.yml',
52
- 'pyproject.toml', 'requirements.txt', 'setup.py', 'setup.cfg',
53
- 'Cargo.toml', 'go.mod',
54
- 'pom.xml', 'build.gradle', 'build.gradle.kts',
55
- 'pubspec.yaml',
56
- ];
57
- info.configFiles = entries.filter(e => KNOWN_CONFIGS.includes(e));
58
-
59
- // ── Node.js
60
- const pkgPath = path.join(dir, 'package.json');
61
- if (fs.existsSync(pkgPath)) {
62
- info.isEmpty = false;
63
- try {
64
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
65
- if (pkg.name) info.name = pkg.name;
66
- info.stack.push('Node.js');
67
- if (pkg.scripts) info.scripts = pkg.scripts;
68
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
69
- if (deps['next']) info.stack.push('Next.js');
70
- else if (deps['react']) info.stack.push('React');
71
- if (deps['vue']) info.stack.push('Vue');
72
- if (deps['svelte']) info.stack.push('Svelte');
73
- if (deps['@angular/core']) info.stack.push('Angular');
74
- if (deps['nuxt']) info.stack.push('Nuxt');
75
- if (deps['astro']) info.stack.push('Astro');
76
- if (deps['express']) info.stack.push('Express');
77
- if (deps['fastify']) info.stack.push('Fastify');
78
- if (deps['hono']) info.stack.push('Hono');
79
- if (deps['electron']) info.stack.push('Electron');
80
- if (deps['typescript'] || deps['ts-node']) info.stack.push('TypeScript');
81
- if (deps['prisma'] || deps['@prisma/client']) info.stack.push('Prisma');
82
- if (deps['drizzle-orm']) info.stack.push('Drizzle');
83
- if (deps['@supabase/supabase-js']) info.stack.push('Supabase');
84
- if (deps['@tauri-apps/api']) info.stack.push('Tauri');
85
- } catch {}
86
- }
87
-
88
- // ── Unreal Engine
89
- const uproject = entries.find(e => e.endsWith('.uproject'));
90
- if (uproject) {
91
- info.isEmpty = false;
92
- info.name = uproject.replace('.uproject', '');
93
- info.stack.push('Unreal Engine');
94
- }
95
-
96
- // ── Python
97
- if (['requirements.txt', 'pyproject.toml', 'setup.py'].some(f => fs.existsSync(path.join(dir, f)))) {
98
- info.isEmpty = false;
99
- info.stack.push('Python');
100
- try {
101
- const req = fs.existsSync(path.join(dir, 'requirements.txt'))
102
- ? fs.readFileSync(path.join(dir, 'requirements.txt'), 'utf8') : '';
103
- if (/fastapi/i.test(req)) info.stack.push('FastAPI');
104
- else if (/django/i.test(req)) info.stack.push('Django');
105
- else if (/flask/i.test(req)) info.stack.push('Flask');
106
- if (/torch|pytorch/i.test(req)) info.stack.push('PyTorch');
107
- } catch {}
108
- }
109
-
110
- // ── Rust
111
- if (fs.existsSync(path.join(dir, 'Cargo.toml'))) {
112
- info.isEmpty = false;
113
- info.stack.push('Rust');
114
- }
115
-
116
- // ── Go
117
- if (fs.existsSync(path.join(dir, 'go.mod'))) {
118
- info.isEmpty = false;
119
- info.stack.push('Go');
120
- }
121
-
122
- // ── C++ / CMake
123
- if (fs.existsSync(path.join(dir, 'CMakeLists.txt'))) {
124
- info.isEmpty = false;
125
- info.stack.push('C++ / CMake');
126
- }
127
-
128
- // ── .NET
129
- if (entries.some(e => e.endsWith('.csproj') || e.endsWith('.sln'))) {
130
- info.isEmpty = false;
131
- info.stack.push('.NET');
132
- }
133
-
134
- // ── Java
135
- if (fs.existsSync(path.join(dir, 'pom.xml')) || fs.existsSync(path.join(dir, 'build.gradle'))) {
136
- info.isEmpty = false;
137
- info.stack.push('Java');
138
- }
139
-
140
- // ── Flutter / Dart
141
- if (fs.existsSync(path.join(dir, 'pubspec.yaml'))) {
142
- info.isEmpty = false;
143
- info.stack.push('Flutter');
144
- }
145
-
146
- // ── Monorepo: scan 1 level deep inside common workspace roots
147
- if (depth === 0) {
148
- for (const monoRoot of ['packages', 'apps', 'services', 'libs']) {
149
- const monoPath = path.join(dir, monoRoot);
150
- if (!fs.existsSync(monoPath)) continue;
151
- try {
152
- for (const sub of fs.readdirSync(monoPath)) {
153
- try {
154
- const subPath = path.join(monoPath, sub);
155
- if (!fs.statSync(subPath).isDirectory()) continue;
156
- const subInfo = scanProject(subPath, 1);
157
- if (!subInfo.isEmpty) info.isEmpty = false;
158
- for (const s of subInfo.stack) {
159
- if (!info.stack.includes(s)) info.stack.push(s);
160
- }
161
- } catch {}
162
- }
163
- } catch {}
164
- }
165
- }
166
-
167
- return info;
168
- }
169
-
170
- // ═══════════════════════════════════════════════════════════════════
171
- // UTILITIES
172
- // ═══════════════════════════════════════════════════════════════════
173
-
174
- function nowISO() { return new Date().toISOString().slice(0, 19); }
175
-
176
- function stackStr(stack) { return stack.length ? stack.join(', ') : 'Not detected'; }
177
-
178
- function listMd(arr, empty = '_None detected._') {
179
- return arr.length ? arr.map(x => `- \`${x}\``).join('\n') : empty;
180
- }
181
-
182
- function scriptsMd(scripts) {
183
- const pairs = Object.entries(scripts);
184
- return pairs.length
185
- ? pairs.map(([k, v]) => `- \`${k}\`: \`${v}\``).join('\n')
186
- : '_None detected._';
187
- }
188
-
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const VERSION = (() => {
8
+ try { return JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).version; }
9
+ catch { return 'unknown'; }
10
+ })();
11
+
12
+ // ═══════════════════════════════════════════════════════════════════
13
+ // SCANNER — detects project type from filesystem
14
+ // ═══════════════════════════════════════════════════════════════════
15
+
16
+ function scanProject(dir, depth = 0) {
17
+ const info = {
18
+ name: path.basename(dir),
19
+ root: dir,
20
+ stack: [],
21
+ scripts: {},
22
+ configFiles: [],
23
+ srcDirs: [],
24
+ isEmpty: true,
25
+ };
26
+
27
+ let entries = [];
28
+ try { entries = fs.readdirSync(dir); } catch { return info; }
29
+
30
+ const IGNORE = new Set([
31
+ 'node_modules', '.git', '.next', 'dist', 'build', 'out',
32
+ 'Saved', 'Intermediate', 'DerivedDataCache', 'Binaries',
33
+ '.memoc', 'skills', '.DS_Store', '.obsidian',
34
+ 'CLAUDE.md', 'AGENTS.md', 'llms.txt',
35
+ ]);
36
+
37
+ info.srcDirs = entries.filter(e => {
38
+ try { return !IGNORE.has(e) && fs.statSync(path.join(dir, e)).isDirectory(); }
39
+ catch { return false; }
40
+ });
41
+
42
+ const KNOWN_CONFIGS = [
43
+ 'package.json', 'tsconfig.json', 'jsconfig.json',
44
+ 'next.config.js', 'next.config.ts', 'next.config.mjs',
45
+ 'vite.config.js', 'vite.config.ts',
46
+ 'tailwind.config.js', 'tailwind.config.ts',
47
+ 'webpack.config.js', 'astro.config.mjs',
48
+ 'svelte.config.js', 'nuxt.config.ts',
49
+ '.env', '.env.example', '.env.local',
50
+ 'Makefile', 'CMakeLists.txt',
51
+ 'Dockerfile', 'docker-compose.yml', 'compose.yml',
52
+ 'pyproject.toml', 'requirements.txt', 'setup.py', 'setup.cfg',
53
+ 'Cargo.toml', 'go.mod',
54
+ 'pom.xml', 'build.gradle', 'build.gradle.kts',
55
+ 'pubspec.yaml',
56
+ ];
57
+ info.configFiles = entries.filter(e => KNOWN_CONFIGS.includes(e));
58
+
59
+ // ── Node.js
60
+ const pkgPath = path.join(dir, 'package.json');
61
+ if (fs.existsSync(pkgPath)) {
62
+ info.isEmpty = false;
63
+ try {
64
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
65
+ if (pkg.name) info.name = pkg.name;
66
+ info.stack.push('Node.js');
67
+ if (pkg.scripts) info.scripts = pkg.scripts;
68
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
69
+ if (deps['next']) info.stack.push('Next.js');
70
+ else if (deps['react']) info.stack.push('React');
71
+ if (deps['vue']) info.stack.push('Vue');
72
+ if (deps['svelte']) info.stack.push('Svelte');
73
+ if (deps['@angular/core']) info.stack.push('Angular');
74
+ if (deps['nuxt']) info.stack.push('Nuxt');
75
+ if (deps['astro']) info.stack.push('Astro');
76
+ if (deps['express']) info.stack.push('Express');
77
+ if (deps['fastify']) info.stack.push('Fastify');
78
+ if (deps['hono']) info.stack.push('Hono');
79
+ if (deps['electron']) info.stack.push('Electron');
80
+ if (deps['typescript'] || deps['ts-node']) info.stack.push('TypeScript');
81
+ if (deps['prisma'] || deps['@prisma/client']) info.stack.push('Prisma');
82
+ if (deps['drizzle-orm']) info.stack.push('Drizzle');
83
+ if (deps['@supabase/supabase-js']) info.stack.push('Supabase');
84
+ if (deps['@tauri-apps/api']) info.stack.push('Tauri');
85
+ } catch {}
86
+ }
87
+
88
+ // ── Unreal Engine
89
+ const uproject = entries.find(e => e.endsWith('.uproject'));
90
+ if (uproject) {
91
+ info.isEmpty = false;
92
+ info.name = uproject.replace('.uproject', '');
93
+ info.stack.push('Unreal Engine');
94
+ }
95
+
96
+ // ── Python
97
+ if (['requirements.txt', 'pyproject.toml', 'setup.py'].some(f => fs.existsSync(path.join(dir, f)))) {
98
+ info.isEmpty = false;
99
+ info.stack.push('Python');
100
+ try {
101
+ const req = fs.existsSync(path.join(dir, 'requirements.txt'))
102
+ ? fs.readFileSync(path.join(dir, 'requirements.txt'), 'utf8') : '';
103
+ if (/fastapi/i.test(req)) info.stack.push('FastAPI');
104
+ else if (/django/i.test(req)) info.stack.push('Django');
105
+ else if (/flask/i.test(req)) info.stack.push('Flask');
106
+ if (/torch|pytorch/i.test(req)) info.stack.push('PyTorch');
107
+ } catch {}
108
+ }
109
+
110
+ // ── Rust
111
+ if (fs.existsSync(path.join(dir, 'Cargo.toml'))) {
112
+ info.isEmpty = false;
113
+ info.stack.push('Rust');
114
+ }
115
+
116
+ // ── Go
117
+ if (fs.existsSync(path.join(dir, 'go.mod'))) {
118
+ info.isEmpty = false;
119
+ info.stack.push('Go');
120
+ }
121
+
122
+ // ── C++ / CMake
123
+ if (fs.existsSync(path.join(dir, 'CMakeLists.txt'))) {
124
+ info.isEmpty = false;
125
+ info.stack.push('C++ / CMake');
126
+ }
127
+
128
+ // ── .NET
129
+ if (entries.some(e => e.endsWith('.csproj') || e.endsWith('.sln'))) {
130
+ info.isEmpty = false;
131
+ info.stack.push('.NET');
132
+ }
133
+
134
+ // ── Java
135
+ if (fs.existsSync(path.join(dir, 'pom.xml')) || fs.existsSync(path.join(dir, 'build.gradle'))) {
136
+ info.isEmpty = false;
137
+ info.stack.push('Java');
138
+ }
139
+
140
+ // ── Flutter / Dart
141
+ if (fs.existsSync(path.join(dir, 'pubspec.yaml'))) {
142
+ info.isEmpty = false;
143
+ info.stack.push('Flutter');
144
+ }
145
+
146
+ // ── Monorepo: scan 1 level deep inside common workspace roots
147
+ if (depth === 0) {
148
+ for (const monoRoot of ['packages', 'apps', 'services', 'libs']) {
149
+ const monoPath = path.join(dir, monoRoot);
150
+ if (!fs.existsSync(monoPath)) continue;
151
+ try {
152
+ for (const sub of fs.readdirSync(monoPath)) {
153
+ try {
154
+ const subPath = path.join(monoPath, sub);
155
+ if (!fs.statSync(subPath).isDirectory()) continue;
156
+ const subInfo = scanProject(subPath, 1);
157
+ if (!subInfo.isEmpty) info.isEmpty = false;
158
+ for (const s of subInfo.stack) {
159
+ if (!info.stack.includes(s)) info.stack.push(s);
160
+ }
161
+ } catch {}
162
+ }
163
+ } catch {}
164
+ }
165
+ }
166
+
167
+ return info;
168
+ }
169
+
170
+ // ═══════════════════════════════════════════════════════════════════
171
+ // UTILITIES
172
+ // ═══════════════════════════════════════════════════════════════════
173
+
174
+ function nowISO() { return new Date().toISOString().slice(0, 19); }
175
+
176
+ function stackStr(stack) { return stack.length ? stack.join(', ') : 'Not detected'; }
177
+
178
+ function listMd(arr, empty = '_None detected._') {
179
+ return arr.length ? arr.map(x => `- \`${x}\``).join('\n') : empty;
180
+ }
181
+
182
+ function scriptsMd(scripts) {
183
+ const pairs = Object.entries(scripts);
184
+ return pairs.length
185
+ ? pairs.map(([k, v]) => `- \`${k}\`: \`${v}\``).join('\n')
186
+ : '_None detected._';
187
+ }
188
+
189
189
  function hideOnWindows(dirPath) {
190
190
  if (process.platform === 'win32') {
191
191
  try { require('child_process').execSync(`attrib +h "${dirPath}"`, { stdio: 'ignore' }); } catch {}
192
192
  }
193
193
  }
194
194
 
195
+ function chmodExecutable(filePath) {
196
+ try { fs.chmodSync(filePath, 0o755); } catch {}
197
+ }
198
+
195
199
  function ensure(filePath, content) {
196
200
  if (fs.existsSync(filePath)) return false;
197
201
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
198
202
  fs.writeFileSync(filePath, content, 'utf8');
199
- return true;
200
- }
201
-
203
+ return true;
204
+ }
205
+
202
206
  function write(filePath, content) {
203
207
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
204
208
  fs.writeFileSync(filePath, content, 'utf8');
205
209
  }
206
210
 
207
- function updateSection(filePath, startMark, endMark, inner) {
208
- if (!fs.existsSync(filePath)) return false;
209
- const src = fs.readFileSync(filePath, 'utf8');
210
- const s = src.indexOf(startMark);
211
- const e = src.indexOf(endMark);
212
- if (s === -1 || e === -1) return false;
213
- write(filePath,
214
- src.slice(0, s) + startMark + '\n' + inner + '\n' + endMark + src.slice(e + endMark.length)
215
- );
216
- return true;
211
+ function tplMemocCmdWrapper() {
212
+ return `@echo off\r\nnpx @kevin0181/memoc %*\r\n`;
217
213
  }
218
214
 
219
- // ═══════════════════════════════════════════════════════════════════
220
- // SECTION MARKERS
221
- // ═══════════════════════════════════════════════════════════════════
222
-
223
- const mk = n => [`<!-- context-forge:${n}:start -->`, `<!-- context-forge:${n}:end -->`];
224
- const [MGMT_S, MGMT_E] = mk('managed');
225
- const [ID_S, ID_E] = mk('identity');
226
- const [SNAP_S, SNAP_E] = mk('snapshot');
227
- const [CORE_S, CORE_E] = mk('core');
228
- const [HDR_S, HDR_E] = mk('header');
229
- const [SYS_S, SYS_E] = mk('systems');
230
- const [WIKI_S, WIKI_E] = mk('wiki');
231
-
232
- // ═══════════════════════════════════════════════════════════════════
233
- // AGENT REGISTRY — third-party agent entry files (added via `add`)
234
- // ═══════════════════════════════════════════════════════════════════
235
-
236
- const AGENT_REGISTRY = {
237
- cursor: { file: '.cursorrules', label: 'Cursor' },
238
- windsurf: { file: '.windsurfrules', label: 'Windsurf' },
239
- copilot: { file: '.github/copilot-instructions.md', label: 'GitHub Copilot' },
240
- gemini: { file: 'GEMINI.md', label: 'Gemini CLI' },
241
- };
242
-
243
- // ═══════════════════════════════════════════════════════════════════
244
- // DYNAMIC CONTENT (re-generated on update)
245
- // ═══════════════════════════════════════════════════════════════════
246
-
247
- function managedBlock() {
248
- return `${MGMT_S}
249
- ## Session Start
250
- - [ ] Read \`.memoc/session-summary.md\`
251
- - [ ] \`.pending\` exists? → review changed files → update memory if needed → delete it
252
-
253
- ## Before Opening More Files
254
- - [ ] Run \`memoc search "<query>"\` first
255
- - [ ] Open on demand: \`02\` status · \`04\` resume · \`06\` rules · \`llms.txt\` map
256
- - [ ] Keep output small: \`summary\`, \`search --limit\`, \`search --snippets\`
257
-
258
- ## Before Finishing _(update only applicable files; skip Q&A / throwaway exploration)_
259
- - [ ] Code/config/deps changed → \`02\` (version, commands list, Last synced) + \`session-summary.md\` (status, changed, open tasks)
260
- - [ ] Decision made → \`03-decisions.md\` (what & why) + \`02\`
261
- - [ ] Work incomplete or risky → \`04-handoff.md\` (verified commands, unverified items, next steps)
262
- - [ ] Rule/preference set → \`06-project-rules.md\`
263
- - [ ] Wiki/systems work → read \`skills/project-memory-maintainer/SKILL.md\`
264
- ${MGMT_E}`;
265
- }
266
-
267
- function identityInner(p) {
268
- return [
269
- `- Project name: \`${p.name}\``,
270
- `- Detected stack: ${stackStr(p.stack)}`,
271
- ].join('\n');
272
- }
273
-
274
- function snapshotInner(p) {
275
- const lines = [
276
- `- Last synced: ${nowISO()}`,
277
- `- Detected stack: ${stackStr(p.stack)}`,
278
- ];
279
- if (p.configFiles.length)
280
- lines.push(`\n### Config Files\n\n${listMd(p.configFiles)}`);
281
- if (p.srcDirs.length)
282
- lines.push(`\n### Source Directories\n\n${listMd(p.srcDirs)}`);
283
- const sc = scriptsMd(p.scripts);
284
- if (sc !== '_None detected._')
285
- lines.push(`\n### Package Scripts\n\n${sc}`);
286
- return lines.join('\n');
287
- }
288
-
289
- function coreLlmsInner() {
290
- return `- [Session Summary](.memoc/session-summary.md): only required startup read.
291
- - [Current State](.memoc/02-current-project-state.md): status, tasks, commands.
292
- - [Handoff](.memoc/04-handoff.md): resume context, blockers, verification.
293
- - [Rules](.memoc/06-project-rules.md): durable preferences.
294
- - [Agent Index](.memoc/00-agent-index.md): compact file map.
295
- - [Project Brief](.memoc/00-project-brief.md): short identity and direction.
296
- - [Workflow](.memoc/01-agent-workflow.md): update trigger matrix.
297
- - [Decisions](.memoc/03-decisions.md): durable decisions.
298
- - [Log](.memoc/log.md): append-only history.
299
- - [Systems](.memoc/systems/README.md): subsystem docs.
300
- - [Wiki](.memoc/wiki/index.md): synthesized knowledge.`;
301
- }
302
-
303
- function headerInner(p) {
304
- return `# ${p.name}\n\n> LLM-facing project map for this project.`;
305
- }
306
-
307
- function systemsLlmsInner(dir) {
308
- const systemsDir = path.join(dir, '.memoc', 'systems');
309
- if (!fs.existsSync(systemsDir)) return '_None yet._';
310
- const files = fs.readdirSync(systemsDir)
311
- .filter(f => f.endsWith('.md') && f !== 'README.md')
312
- .sort();
313
- if (!files.length) return '_None yet._';
314
- return files.map(f => `- [${f.replace('.md', '')}](.memoc/systems/${f}): subsystem context.`).join('\n');
315
- }
316
-
317
- function wikiLlmsInner(dir) {
318
- const wikiDir = path.join(dir, '.memoc', 'wiki');
319
- if (!fs.existsSync(wikiDir)) return '_None yet._';
320
- const lines = [];
321
- const SKIP = new Set(['index.md']);
322
- try {
323
- for (const f of fs.readdirSync(wikiDir).sort()) {
324
- if (!f.endsWith('.md') || SKIP.has(f)) continue;
325
- try { if (fs.statSync(path.join(wikiDir, f)).isDirectory()) continue; } catch { continue; }
326
- lines.push(`- [${f.replace('.md', '')}](.memoc/wiki/${f}): wiki page.`);
327
- }
328
- for (const sub of ['sources', 'topics', 'global']) {
329
- const subDir = path.join(wikiDir, sub);
330
- if (!fs.existsSync(subDir)) continue;
331
- for (const f of fs.readdirSync(subDir).sort()) {
332
- if (!f.endsWith('.md')) continue;
333
- lines.push(`- [${f.replace('.md', '')}](.memoc/wiki/${sub}/${f}): wiki page.`);
334
- }
335
- }
336
- } catch {}
337
- return lines.length ? lines.join('\n') : '_None yet._';
338
- }
339
-
340
- // ═══════════════════════════════════════════════════════════════════
341
- // TEMPLATES — entry files
342
- // ═══════════════════════════════════════════════════════════════════
343
-
344
- function tplClaude() {
345
- return `# CLAUDE.md
346
-
347
- This is the Claude Code entry file for the project.
348
-
349
- ${managedBlock()}
350
- `;
351
- }
352
-
353
- function tplAgents() {
354
- return `# AGENTS.md
355
-
356
- This is the Codex entry file for the project.
357
-
358
- ${managedBlock()}
359
- `;
360
- }
361
-
362
- function tplAgentEntry(label) {
363
- return `# ${label}
364
-
365
- This is the ${label} entry file for this project.
366
-
367
- ${managedBlock()}
368
- `;
369
- }
370
-
371
- function tplLlmsTxt(p) {
372
- return `${HDR_S}
373
- # ${p.name}
374
-
375
- > LLM-facing project map for this project.
376
- ${HDR_E}
377
-
378
- This file is a map, not a startup read. Start from the entry-file protocol and open only what the task needs.
379
-
380
- ## Core
381
-
382
- ${CORE_S}
383
- ${coreLlmsInner()}
384
- ${CORE_E}
385
-
386
- ## Systems
387
-
388
- ${SYS_S}
389
- _None yet._
390
- ${SYS_E}
391
-
392
- ## Wiki
393
-
394
- ${WIKI_S}
395
- _None yet._
396
- ${WIKI_E}
397
-
398
- ## Optional
399
-
400
- - [AGENTS.md](AGENTS.md): Codex entry file.
401
- - [CLAUDE.md](CLAUDE.md): Claude Code entry file.
402
- - [Project Memory Maintainer](skills/project-memory-maintainer/SKILL.md): local maintenance skill.
403
- `;
215
+ function tplMemocPs1Wrapper() {
216
+ return `npx @kevin0181/memoc @args\nexit $LASTEXITCODE\n`;
404
217
  }
405
218
 
406
- // ═══════════════════════════════════════════════════════════════════
407
- // TEMPLATES dynamic .memoc files
408
- // ═══════════════════════════════════════════════════════════════════
409
-
410
- function tplProjectBrief(p) {
411
- return `# Project Brief
412
-
413
- This is the shortest project summary for a fresh agent. Keep it factual and easy to scan.
414
-
415
- ## Identity
416
-
417
- ${ID_S}
418
- ${identityInner(p)}
419
- ${ID_E}
420
-
421
- ## Current Direction
422
-
423
- _Not set yet._
424
-
425
- ## How To Approach
426
-
427
- - Start from \`session-summary.md\`; search before opening more files.
428
- - Open status, handoff, rules, map, systems, or wiki docs only when the task needs them.
429
- - After durable work, update the smallest relevant memory set.
430
- - Do not treat generated output folders as source unless the user explicitly asks.
431
-
432
- ## Next Useful Work
433
-
434
- _None yet._
435
-
436
- ## Important Notes
437
-
438
- _None yet._
439
- `;
219
+ function tplMemocShWrapper() {
220
+ return `#!/bin/sh\nexec npx @kevin0181/memoc "$@"\n`;
440
221
  }
441
222
 
442
- function tplAgentIndex(p) {
443
- return `# Agent Index
444
-
445
- This is the fast entry map for agents. Start here, then open only the docs relevant to the task.
446
-
447
- ## Read Order
448
-
449
- 1. Entry file managed block.
450
- 2. \`.memoc/session-summary.md\`.
451
- 3. Search first, then open only task-relevant files.
452
-
453
- ## Project Snapshot
454
-
455
- ${SNAP_S}
456
- ${snapshotInner(p)}
457
- ${SNAP_E}
458
-
459
- ## Core Docs
460
-
461
- - [Boot](boot.md)
462
- - [Project Brief](00-project-brief.md)
463
- - [memoc Usage](memoc-usage.md)
464
- - [Agent Workflow](01-agent-workflow.md)
465
- - [Current Project State](02-current-project-state.md)
466
- - [Decisions](03-decisions.md)
467
- - [Handoff](04-handoff.md)
468
- - [Done Checklist](05-done-checklist.md)
469
- - [Project Rules](06-project-rules.md)
470
- - [Session Summary](session-summary.md)
471
- - [Project Log](log.md)
472
- - [Wiki Index](wiki/index.md)
473
- - [Systems Index](systems/README.md)
474
-
475
- ## System Docs
476
-
477
- _None yet. Add entries when subsystems are documented._
478
-
479
- ## Wiki
480
-
481
- _None yet. Add entries when wiki pages are created._
482
- `;
223
+ function tplEnvPs1() {
224
+ return `$memocBin = Join-Path $PSScriptRoot 'bin'\n$parts = $env:PATH -split [IO.Path]::PathSeparator\nif ($parts -notcontains $memocBin) {\n $env:PATH = \"$memocBin$([IO.Path]::PathSeparator)$env:PATH\"\n}\n`;
483
225
  }
484
226
 
485
- function tplCurrentState(p) {
486
- return `# Current Project State
487
-
488
- Last synced: ${nowISO()}
489
-
490
- ## Current Status
491
-
492
- _See Project Snapshot below. Keep only current human-written status notes here._
493
-
494
- ## Project Snapshot
495
-
496
- ${SNAP_S}
497
- ${snapshotInner(p)}
498
- ${SNAP_E}
499
-
500
- ## Open Tasks
501
-
502
- _None yet._
503
-
504
- ## Completed Tasks
505
-
506
- See \`.memoc/log.md\` for full history.
507
-
508
- ## Commands
509
-
510
- _None recorded yet._
511
-
512
- ## Notes
513
-
514
- _None yet._
515
-
516
- ## Change Log
517
-
518
- See \`.memoc/log.md\`.
519
- `;
227
+ function tplEnvSh() {
228
+ return `# Source this from the project root to put the local memoc wrapper first in PATH.\nMEMOC_DIR="$(pwd)/.memoc"\ncase ":$PATH:" in\n *":$MEMOC_DIR/bin:"*) ;;\n *) PATH="$MEMOC_DIR/bin:$PATH"; export PATH ;;\nesac\n`;
520
229
  }
521
230
 
522
- function tplSessionSummary() {
523
- return `# Session Summary
524
- Last: ${nowISO()}
525
- Keep each section ≤ 3 bullets. Agent-owned — updated by you, not by \`memoc update\`.
526
-
527
- ## Status
528
- _What is the current state of the project?_
529
-
530
- ## Changed
531
- _What changed in the last session? (code, config, decisions)_
532
-
533
- ## Open Tasks
534
- _What still needs to be done?_
231
+ function ensurePathHelpers(dir, mark) {
232
+ const files = [
233
+ [path.join(dir, '.memoc', 'bin', 'memoc.cmd'), tplMemocCmdWrapper, false],
234
+ [path.join(dir, '.memoc', 'bin', 'memoc.ps1'), tplMemocPs1Wrapper, false],
235
+ [path.join(dir, '.memoc', 'bin', 'memoc'), tplMemocShWrapper, true],
236
+ [path.join(dir, '.memoc', 'env.ps1'), tplEnvPs1, false],
237
+ [path.join(dir, '.memoc', 'env.sh'), tplEnvSh, true],
238
+ ];
535
239
 
536
- ## Resume
537
- _Where should the next agent pick up?_
538
- `;
240
+ for (const [fp, tpl, executable] of files) {
241
+ const rel = path.relative(dir, fp);
242
+ const added = ensure(fp, tpl());
243
+ if (executable) chmodExecutable(fp);
244
+ mark(added ? 'add' : 'skip', rel);
245
+ }
539
246
  }
540
247
 
541
- // ═══════════════════════════════════════════════════════════════════
542
- // TEMPLATES — static .memoc files (same for every project)
543
- // ═══════════════════════════════════════════════════════════════════
544
-
545
- function tplBoot() {
546
- return `# Agent Boot
547
-
548
- On-demand reference only. The entry-file managed block is authoritative.
549
-
550
- ## Open Only When Needed
551
-
552
- | File | When to open |
553
- | --- | --- |
554
- | \`.memoc/session-summary.md\` | Every session start (only required read) |
555
- | \`.memoc/02-current-project-state.md\` | Before changing behavior or checking tasks |
556
- | \`.memoc/04-handoff.md\` | When resuming incomplete work |
557
- | \`.memoc/06-project-rules.md\` | When unsure about preferences or conventions |
558
- | \`.memoc/01-agent-workflow.md\` | When update routing is unclear |
559
- | \`.memoc/05-done-checklist.md\` | Before finishing substantial work |
560
- | \`.memoc/03-decisions.md\` | When a durable decision was made |
561
- | \`.memoc/log.md\` | For append-only history |
562
- | \`.memoc/memoc-usage.md\` | For command details |
563
- | \`.memoc/systems/*.md\` | Before touching a specific subsystem |
564
- | \`.memoc/wiki/*.md\` | For synthesized project knowledge |
565
- | \`llms.txt\` | For full project file map |
248
+ function updateSection(filePath, startMark, endMark, inner) {
249
+ if (!fs.existsSync(filePath)) return false;
250
+ const src = fs.readFileSync(filePath, 'utf8');
251
+ const s = src.indexOf(startMark);
252
+ const e = src.indexOf(endMark);
253
+ if (s === -1 || e === -1) return false;
254
+ write(filePath,
255
+ src.slice(0, s) + startMark + '\n' + inner + '\n' + endMark + src.slice(e + endMark.length)
256
+ );
257
+ return true;
258
+ }
259
+
260
+ // ═══════════════════════════════════════════════════════════════════
261
+ // SECTION MARKERS
262
+ // ═══════════════════════════════════════════════════════════════════
263
+
264
+ const mk = n => [`<!-- context-forge:${n}:start -->`, `<!-- context-forge:${n}:end -->`];
265
+ const [MGMT_S, MGMT_E] = mk('managed');
266
+ const [ID_S, ID_E] = mk('identity');
267
+ const [SNAP_S, SNAP_E] = mk('snapshot');
268
+ const [CORE_S, CORE_E] = mk('core');
269
+ const [HDR_S, HDR_E] = mk('header');
270
+ const [SYS_S, SYS_E] = mk('systems');
271
+ const [WIKI_S, WIKI_E] = mk('wiki');
272
+
273
+ // ═══════════════════════════════════════════════════════════════════
274
+ // AGENT REGISTRY — third-party agent entry files (added via `add`)
275
+ // ═══════════════════════════════════════════════════════════════════
276
+
277
+ const AGENT_REGISTRY = {
278
+ cursor: { file: '.cursorrules', label: 'Cursor' },
279
+ windsurf: { file: '.windsurfrules', label: 'Windsurf' },
280
+ copilot: { file: '.github/copilot-instructions.md', label: 'GitHub Copilot' },
281
+ gemini: { file: 'GEMINI.md', label: 'Gemini CLI' },
282
+ };
283
+
284
+ // ═══════════════════════════════════════════════════════════════════
285
+ // DYNAMIC CONTENT (re-generated on update)
286
+ // ═══════════════════════════════════════════════════════════════════
287
+
288
+ function managedBlock() {
289
+ return `${MGMT_S}
290
+ ## Session Start
291
+ - [ ] Read \`.memoc/session-summary.md\`
292
+ - [ ] \`.pending\` exists? → review changed files → update memory if needed → delete it
293
+ - [ ] Put the project-local memoc wrapper on PATH when needed: PowerShell \`. .\\.memoc\\env.ps1\`; sh \`. ./.memoc/env.sh\`
566
294
 
295
+ ## Before Opening More Files
296
+ - [ ] Run memoc commands in this order: \`memoc search "<query>"\` → \`.\\.memoc\\bin\\memoc.cmd search "<query>"\` (Windows) or \`.memoc/bin/memoc search "<query>"\` (sh) → \`npx @kevin0181/memoc search "<query>"\`
297
+ - [ ] Open on demand: \`02\` status · \`04\` resume · \`06\` rules · \`llms.txt\` map
298
+ - [ ] Keep output small: \`summary\`, \`search --limit\`, \`search --snippets\`
299
+
300
+ ## Before Finishing _(update only applicable files; skip Q&A / throwaway exploration)_
301
+ - [ ] Code/config/deps changed → \`02\` (version, commands list, Last synced) + \`session-summary.md\` (status, changed, open tasks)
302
+ - [ ] Decision made → \`03-decisions.md\` (what & why) + \`02\`
303
+ - [ ] Work incomplete or risky → \`04-handoff.md\` (verified commands, unverified items, next steps)
304
+ - [ ] Rule/preference set → \`06-project-rules.md\`
305
+ - [ ] Wiki/systems work → read \`skills/project-memory-maintainer/SKILL.md\`
306
+ ${MGMT_E}`;
307
+ }
308
+
309
+ function identityInner(p) {
310
+ return [
311
+ `- Project name: \`${p.name}\``,
312
+ `- Detected stack: ${stackStr(p.stack)}`,
313
+ ].join('\n');
314
+ }
315
+
316
+ function snapshotInner(p) {
317
+ const lines = [
318
+ `- Last synced: ${nowISO()}`,
319
+ `- Detected stack: ${stackStr(p.stack)}`,
320
+ ];
321
+ if (p.configFiles.length)
322
+ lines.push(`\n### Config Files\n\n${listMd(p.configFiles)}`);
323
+ if (p.srcDirs.length)
324
+ lines.push(`\n### Source Directories\n\n${listMd(p.srcDirs)}`);
325
+ const sc = scriptsMd(p.scripts);
326
+ if (sc !== '_None detected._')
327
+ lines.push(`\n### Package Scripts\n\n${sc}`);
328
+ return lines.join('\n');
329
+ }
330
+
331
+ function coreLlmsInner() {
332
+ return `- [Session Summary](.memoc/session-summary.md): only required startup read.
333
+ - [Current State](.memoc/02-current-project-state.md): status, tasks, commands.
334
+ - [Handoff](.memoc/04-handoff.md): resume context, blockers, verification.
335
+ - [Rules](.memoc/06-project-rules.md): durable preferences.
336
+ - [Agent Index](.memoc/00-agent-index.md): compact file map.
337
+ - [Project Brief](.memoc/00-project-brief.md): short identity and direction.
338
+ - [Workflow](.memoc/01-agent-workflow.md): update trigger matrix.
339
+ - [Decisions](.memoc/03-decisions.md): durable decisions.
340
+ - [Log](.memoc/log.md): append-only history.
341
+ - [Systems](.memoc/systems/README.md): subsystem docs.
342
+ - [Wiki](.memoc/wiki/index.md): synthesized knowledge.`;
343
+ }
344
+
345
+ function headerInner(p) {
346
+ return `# ${p.name}\n\n> LLM-facing project map for this project.`;
347
+ }
348
+
349
+ function systemsLlmsInner(dir) {
350
+ const systemsDir = path.join(dir, '.memoc', 'systems');
351
+ if (!fs.existsSync(systemsDir)) return '_None yet._';
352
+ const files = fs.readdirSync(systemsDir)
353
+ .filter(f => f.endsWith('.md') && f !== 'README.md')
354
+ .sort();
355
+ if (!files.length) return '_None yet._';
356
+ return files.map(f => `- [${f.replace('.md', '')}](.memoc/systems/${f}): subsystem context.`).join('\n');
357
+ }
358
+
359
+ function wikiLlmsInner(dir) {
360
+ const wikiDir = path.join(dir, '.memoc', 'wiki');
361
+ if (!fs.existsSync(wikiDir)) return '_None yet._';
362
+ const lines = [];
363
+ const SKIP = new Set(['index.md']);
364
+ try {
365
+ for (const f of fs.readdirSync(wikiDir).sort()) {
366
+ if (!f.endsWith('.md') || SKIP.has(f)) continue;
367
+ try { if (fs.statSync(path.join(wikiDir, f)).isDirectory()) continue; } catch { continue; }
368
+ lines.push(`- [${f.replace('.md', '')}](.memoc/wiki/${f}): wiki page.`);
369
+ }
370
+ for (const sub of ['sources', 'topics', 'global']) {
371
+ const subDir = path.join(wikiDir, sub);
372
+ if (!fs.existsSync(subDir)) continue;
373
+ for (const f of fs.readdirSync(subDir).sort()) {
374
+ if (!f.endsWith('.md')) continue;
375
+ lines.push(`- [${f.replace('.md', '')}](.memoc/wiki/${sub}/${f}): wiki page.`);
376
+ }
377
+ }
378
+ } catch {}
379
+ return lines.length ? lines.join('\n') : '_None yet._';
380
+ }
381
+
382
+ // ═══════════════════════════════════════════════════════════════════
383
+ // TEMPLATES — entry files
384
+ // ═══════════════════════════════════════════════════════════════════
385
+
386
+ function tplClaude() {
387
+ return `# CLAUDE.md
388
+
389
+ This is the Claude Code entry file for the project.
390
+
391
+ ${managedBlock()}
392
+ `;
393
+ }
394
+
395
+ function tplAgents() {
396
+ return `# AGENTS.md
397
+
398
+ This is the Codex entry file for the project.
399
+
400
+ ${managedBlock()}
401
+ `;
402
+ }
403
+
404
+ function tplAgentEntry(label) {
405
+ return `# ${label}
406
+
407
+ This is the ${label} entry file for this project.
408
+
409
+ ${managedBlock()}
410
+ `;
411
+ }
412
+
413
+ function tplLlmsTxt(p) {
414
+ return `${HDR_S}
415
+ # ${p.name}
416
+
417
+ > LLM-facing project map for this project.
418
+ ${HDR_E}
419
+
420
+ This file is a map, not a startup read. Start from the entry-file protocol and open only what the task needs.
421
+
422
+ ## Core
423
+
424
+ ${CORE_S}
425
+ ${coreLlmsInner()}
426
+ ${CORE_E}
427
+
428
+ ## Systems
429
+
430
+ ${SYS_S}
431
+ _None yet._
432
+ ${SYS_E}
433
+
434
+ ## Wiki
435
+
436
+ ${WIKI_S}
437
+ _None yet._
438
+ ${WIKI_E}
439
+
440
+ ## Optional
441
+
442
+ - [AGENTS.md](AGENTS.md): Codex entry file.
443
+ - [CLAUDE.md](CLAUDE.md): Claude Code entry file.
444
+ - [Project Memory Maintainer](skills/project-memory-maintainer/SKILL.md): local maintenance skill.
445
+ `;
446
+ }
447
+
448
+ // ═══════════════════════════════════════════════════════════════════
449
+ // TEMPLATES — dynamic .memoc files
450
+ // ═══════════════════════════════════════════════════════════════════
451
+
452
+ function tplProjectBrief(p) {
453
+ return `# Project Brief
454
+
455
+ This is the shortest project summary for a fresh agent. Keep it factual and easy to scan.
456
+
457
+ ## Identity
458
+
459
+ ${ID_S}
460
+ ${identityInner(p)}
461
+ ${ID_E}
462
+
463
+ ## Current Direction
464
+
465
+ _Not set yet._
466
+
467
+ ## How To Approach
468
+
469
+ - Start from \`session-summary.md\`; search before opening more files.
470
+ - Open status, handoff, rules, map, systems, or wiki docs only when the task needs them.
471
+ - After durable work, update the smallest relevant memory set.
472
+ - Do not treat generated output folders as source unless the user explicitly asks.
473
+
474
+ ## Next Useful Work
475
+
476
+ _None yet._
477
+
478
+ ## Important Notes
479
+
480
+ _None yet._
481
+ `;
482
+ }
483
+
484
+ function tplAgentIndex(p) {
485
+ return `# Agent Index
486
+
487
+ This is the fast entry map for agents. Start here, then open only the docs relevant to the task.
488
+
489
+ ## Read Order
490
+
491
+ 1. Entry file managed block.
492
+ 2. \`.memoc/session-summary.md\`.
493
+ 3. Search first, then open only task-relevant files.
494
+
495
+ ## Project Snapshot
496
+
497
+ ${SNAP_S}
498
+ ${snapshotInner(p)}
499
+ ${SNAP_E}
500
+
501
+ ## Core Docs
502
+
503
+ - [Boot](boot.md)
504
+ - [Project Brief](00-project-brief.md)
505
+ - [memoc Usage](memoc-usage.md)
506
+ - [Agent Workflow](01-agent-workflow.md)
507
+ - [Current Project State](02-current-project-state.md)
508
+ - [Decisions](03-decisions.md)
509
+ - [Handoff](04-handoff.md)
510
+ - [Done Checklist](05-done-checklist.md)
511
+ - [Project Rules](06-project-rules.md)
512
+ - [Session Summary](session-summary.md)
513
+ - [Project Log](log.md)
514
+ - [Wiki Index](wiki/index.md)
515
+ - [Systems Index](systems/README.md)
516
+
517
+ ## System Docs
518
+
519
+ _None yet. Add entries when subsystems are documented._
520
+
521
+ ## Wiki
522
+
523
+ _None yet. Add entries when wiki pages are created._
524
+ `;
525
+ }
526
+
527
+ function tplCurrentState(p) {
528
+ return `# Current Project State
529
+
530
+ Last synced: ${nowISO()}
531
+
532
+ ## Current Status
533
+
534
+ _See Project Snapshot below. Keep only current human-written status notes here._
535
+
536
+ ## Project Snapshot
537
+
538
+ ${SNAP_S}
539
+ ${snapshotInner(p)}
540
+ ${SNAP_E}
541
+
542
+ ## Open Tasks
543
+
544
+ _None yet._
545
+
546
+ ## Completed Tasks
547
+
548
+ See \`.memoc/log.md\` for full history.
549
+
550
+ ## Commands
551
+
552
+ _None recorded yet._
553
+
554
+ ## Notes
555
+
556
+ _None yet._
557
+
558
+ ## Change Log
559
+
560
+ See \`.memoc/log.md\`.
561
+ `;
562
+ }
563
+
564
+ function tplSessionSummary() {
565
+ return `# Session Summary
566
+ Last: ${nowISO()}
567
+ Keep each section ≤ 3 bullets. Agent-owned — updated by you, not by \`memoc update\`.
568
+
569
+ ## Status
570
+ _What is the current state of the project?_
571
+
572
+ ## Changed
573
+ _What changed in the last session? (code, config, decisions)_
574
+
575
+ ## Open Tasks
576
+ _What still needs to be done?_
577
+
578
+ ## Resume
579
+ _Where should the next agent pick up?_
580
+ `;
581
+ }
582
+
583
+ // ═══════════════════════════════════════════════════════════════════
584
+ // TEMPLATES — static .memoc files (same for every project)
585
+ // ═══════════════════════════════════════════════════════════════════
586
+
587
+ function tplBoot() {
588
+ return `# Agent Boot
589
+
590
+ On-demand reference only. The entry-file managed block is authoritative.
591
+
592
+ ## Open Only When Needed
593
+
594
+ | File | When to open |
595
+ | --- | --- |
596
+ | \`.memoc/session-summary.md\` | Every session start (only required read) |
597
+ | \`.memoc/02-current-project-state.md\` | Before changing behavior or checking tasks |
598
+ | \`.memoc/04-handoff.md\` | When resuming incomplete work |
599
+ | \`.memoc/06-project-rules.md\` | When unsure about preferences or conventions |
600
+ | \`.memoc/01-agent-workflow.md\` | When update routing is unclear |
601
+ | \`.memoc/05-done-checklist.md\` | Before finishing substantial work |
602
+ | \`.memoc/03-decisions.md\` | When a durable decision was made |
603
+ | \`.memoc/log.md\` | For append-only history |
604
+ | \`.memoc/memoc-usage.md\` | For command details |
605
+ | \`.memoc/systems/*.md\` | Before touching a specific subsystem |
606
+ | \`.memoc/wiki/*.md\` | For synthesized project knowledge |
607
+ | \`llms.txt\` | For full project file map |
608
+
567
609
  ## Search First
568
610
 
569
611
  \`memoc search "<query>"\` — returns file:line matches across all memory files.
612
+ If \`memoc\` is not on PATH, try \`.\\.memoc\\bin\\memoc.cmd search "<query>"\` on Windows or \`.memoc/bin/memoc search "<query>"\` in sh, then \`npx @kevin0181/memoc search "<query>"\`.
570
613
  Use it before opening any file to avoid reading more than needed.
571
614
  `;
572
615
  }
573
-
574
- function tplWorkflow() {
575
- return `# Agent Workflow
576
-
577
- Shared protocol for any coding agent.
578
-
579
- ## Entry Routine
580
-
581
- 1. Read the entry-file managed block.
582
- 2. Read \`.memoc/session-summary.md\` only.
583
- 3. Search before opening broad docs.
584
- 4. Work from the smallest relevant file set.
585
- 5. Update memory only when durable context changed.
586
-
587
- ## Memory Update Triggers
588
-
589
- | Trigger | Update |
590
- | --- | --- |
591
- | User creates or changes a requirement | \`02-current-project-state.md\`, \`06-project-rules.md\`, \`log.md\` |
592
- | Code, config, data, or assets changed | \`02-current-project-state.md\`, relevant \`systems/*.md\`, \`log.md\` |
593
- | Architecture or system behavior changed | relevant \`systems/*.md\`, \`03-decisions.md\` |
594
- | A decision should affect future agents | \`03-decisions.md\`, \`02-current-project-state.md\` |
595
- | Work is substantial enough to resume later | \`04-handoff.md\`, \`02-current-project-state.md\`, \`log.md\` |
596
- | Durable knowledge was learned | \`wiki/*.md\`, \`wiki/index.md\` |
597
-
598
- ## Usually No Update Needed
599
-
600
- - Pure Q&A with no durable outcome.
601
- - Tiny typo-only edits.
602
- - Temporary exploration that finds nothing actionable.
603
-
604
- ## Documentation Shape
605
-
606
- - Entry files: protocol only.
607
- - \`session-summary.md\`: latest snapshot, max 3 bullets per section.
608
- - \`02-current-project-state.md\`: current status, tasks, commands, recent notes.
609
- - \`04-handoff.md\`: resume context, blockers, verified/unverified checks.
610
- - \`03-decisions.md\`: append durable decisions only.
611
- - \`log.md\`: full history; keep bulky completed work here.
612
- - \`systems/*.md\` and \`wiki/*.md\`: on-demand durable knowledge.
613
- `;
614
- }
615
-
616
- function tplDecisions() {
617
- return `# Decisions
618
-
619
- Durable project decisions live here. Keep entries short, dated, and useful to future agents.
620
-
621
- ## Decision Log
622
-
623
- _None yet._
624
- `;
625
- }
626
-
627
- function tplHandoff() {
628
- return `# Agent Handoff
629
-
630
- Last synced: ${nowISO()}
631
-
632
- ## What Changed
633
-
634
- _None yet._
635
-
636
- ## Next Steps
637
-
638
- _None yet._
639
-
640
- ## Blockers
641
-
642
- _None yet._
643
-
644
- ## Do Not Touch Without Asking
645
-
646
- _None yet._
647
-
648
- ## Verified
649
-
650
- _None yet._
651
-
652
- ## Not Verified
653
-
654
- _None yet._
655
-
656
- ## Resume Notes
657
-
658
- _None yet._
659
-
660
- ## Suggested Reads
661
-
662
- Search first, then open only files named above.
663
- `;
664
- }
665
-
666
- function tplDoneChecklist() {
667
- return `# Done Checklist
668
-
669
- Run through this before saying substantial work is complete.
670
-
671
- ## Code
672
-
673
- - [ ] Changes compile or run without errors.
674
- - [ ] Relevant tests pass (or new tests were added).
675
- - [ ] No obvious security issues introduced.
676
- - [ ] No hardcoded secrets or credentials.
677
-
678
- ## Memory
679
-
680
- - [ ] \`.memoc/02-current-project-state.md\` reflects the new status.
681
- - [ ] \`.memoc/03-decisions.md\` updated if a durable decision was made.
682
- - [ ] \`.memoc/04-handoff.md\` updated if work is incomplete or risky.
683
- - [ ] \`.memoc/log.md\` has a new entry for meaningful work.
684
- - [ ] Relevant \`.memoc/systems/*.md\` or wiki pages updated.
685
-
686
- ## Communication
687
-
688
- - [ ] Final answer states what was verified and what was not.
689
- - [ ] Unverified risks are noted in handoff.
690
- `;
691
- }
692
-
693
- function tplProjectRules() {
694
- return `# Project Rules
695
-
696
- Durable user and project preferences live here. Update when the user gives a rule that should persist across sessions.
697
-
698
- ## Operating Rules
699
-
700
- - Keep \`AGENTS.md\` and \`CLAUDE.md\` as short entry files; durable context belongs under \`.memoc/\`.
701
- - Do not track generated output folders such as \`out/\`, \`.next/\`, \`dist/\`, \`build/\` unless the user explicitly asks.
702
- - Update \`.memoc/04-handoff.md\` after substantial work so the next agent can resume quickly.
703
- - Use \`.memoc/05-done-checklist.md\` before saying substantial work is complete.
704
-
705
- ## Agent Behavior Preferences
706
-
707
- - Be factual and operational in memory docs.
708
- - Keep logs concise; do not paste temporary command output unless it changes future work.
709
- - Preserve user changes and avoid reverting unrelated work.
710
- - State unverified parts honestly in the final answer and handoff.
711
-
712
- ## Project-Specific Rules
713
-
714
- _None yet._
715
- `;
716
- }
717
-
718
- function tplLog() {
719
- return `# Project Log
720
-
721
- Append-only chronological log for project memory updates.
722
-
723
- ## [${nowISO()}] init | Initialized memoc memory structure.
724
- `;
725
- }
726
-
727
- function tplContextForgeUsage() {
728
- return `# memoc Usage
729
-
730
- This project uses \`memoc\` to maintain agent-readable project memory.
731
-
616
+
617
+ function tplWorkflow() {
618
+ return `# Agent Workflow
619
+
620
+ Shared protocol for any coding agent.
621
+
622
+ ## Entry Routine
623
+
624
+ 1. Read the entry-file managed block.
625
+ 2. Read \`.memoc/session-summary.md\` only.
626
+ 3. Search before opening broad docs.
627
+ 4. Work from the smallest relevant file set.
628
+ 5. Update memory only when durable context changed.
629
+
630
+ ## Memory Update Triggers
631
+
632
+ | Trigger | Update |
633
+ | --- | --- |
634
+ | User creates or changes a requirement | \`02-current-project-state.md\`, \`06-project-rules.md\`, \`log.md\` |
635
+ | Code, config, data, or assets changed | \`02-current-project-state.md\`, relevant \`systems/*.md\`, \`log.md\` |
636
+ | Architecture or system behavior changed | relevant \`systems/*.md\`, \`03-decisions.md\` |
637
+ | A decision should affect future agents | \`03-decisions.md\`, \`02-current-project-state.md\` |
638
+ | Work is substantial enough to resume later | \`04-handoff.md\`, \`02-current-project-state.md\`, \`log.md\` |
639
+ | Durable knowledge was learned | \`wiki/*.md\`, \`wiki/index.md\` |
640
+
641
+ ## Usually No Update Needed
642
+
643
+ - Pure Q&A with no durable outcome.
644
+ - Tiny typo-only edits.
645
+ - Temporary exploration that finds nothing actionable.
646
+
647
+ ## Documentation Shape
648
+
649
+ - Entry files: protocol only.
650
+ - \`session-summary.md\`: latest snapshot, max 3 bullets per section.
651
+ - \`02-current-project-state.md\`: current status, tasks, commands, recent notes.
652
+ - \`04-handoff.md\`: resume context, blockers, verified/unverified checks.
653
+ - \`03-decisions.md\`: append durable decisions only.
654
+ - \`log.md\`: full history; keep bulky completed work here.
655
+ - \`systems/*.md\` and \`wiki/*.md\`: on-demand durable knowledge.
656
+ `;
657
+ }
658
+
659
+ function tplDecisions() {
660
+ return `# Decisions
661
+
662
+ Durable project decisions live here. Keep entries short, dated, and useful to future agents.
663
+
664
+ ## Decision Log
665
+
666
+ _None yet._
667
+ `;
668
+ }
669
+
670
+ function tplHandoff() {
671
+ return `# Agent Handoff
672
+
673
+ Last synced: ${nowISO()}
674
+
675
+ ## What Changed
676
+
677
+ _None yet._
678
+
679
+ ## Next Steps
680
+
681
+ _None yet._
682
+
683
+ ## Blockers
684
+
685
+ _None yet._
686
+
687
+ ## Do Not Touch Without Asking
688
+
689
+ _None yet._
690
+
691
+ ## Verified
692
+
693
+ _None yet._
694
+
695
+ ## Not Verified
696
+
697
+ _None yet._
698
+
699
+ ## Resume Notes
700
+
701
+ _None yet._
702
+
703
+ ## Suggested Reads
704
+
705
+ Search first, then open only files named above.
706
+ `;
707
+ }
708
+
709
+ function tplDoneChecklist() {
710
+ return `# Done Checklist
711
+
712
+ Run through this before saying substantial work is complete.
713
+
714
+ ## Code
715
+
716
+ - [ ] Changes compile or run without errors.
717
+ - [ ] Relevant tests pass (or new tests were added).
718
+ - [ ] No obvious security issues introduced.
719
+ - [ ] No hardcoded secrets or credentials.
720
+
721
+ ## Memory
722
+
723
+ - [ ] \`.memoc/02-current-project-state.md\` reflects the new status.
724
+ - [ ] \`.memoc/03-decisions.md\` updated if a durable decision was made.
725
+ - [ ] \`.memoc/04-handoff.md\` updated if work is incomplete or risky.
726
+ - [ ] \`.memoc/log.md\` has a new entry for meaningful work.
727
+ - [ ] Relevant \`.memoc/systems/*.md\` or wiki pages updated.
728
+
729
+ ## Communication
730
+
731
+ - [ ] Final answer states what was verified and what was not.
732
+ - [ ] Unverified risks are noted in handoff.
733
+ `;
734
+ }
735
+
736
+ function tplProjectRules() {
737
+ return `# Project Rules
738
+
739
+ Durable user and project preferences live here. Update when the user gives a rule that should persist across sessions.
740
+
741
+ ## Operating Rules
742
+
743
+ - Keep \`AGENTS.md\` and \`CLAUDE.md\` as short entry files; durable context belongs under \`.memoc/\`.
744
+ - Do not track generated output folders such as \`out/\`, \`.next/\`, \`dist/\`, \`build/\` unless the user explicitly asks.
745
+ - Update \`.memoc/04-handoff.md\` after substantial work so the next agent can resume quickly.
746
+ - Use \`.memoc/05-done-checklist.md\` before saying substantial work is complete.
747
+
748
+ ## Agent Behavior Preferences
749
+
750
+ - Be factual and operational in memory docs.
751
+ - Keep logs concise; do not paste temporary command output unless it changes future work.
752
+ - Preserve user changes and avoid reverting unrelated work.
753
+ - State unverified parts honestly in the final answer and handoff.
754
+
755
+ ## Project-Specific Rules
756
+
757
+ _None yet._
758
+ `;
759
+ }
760
+
761
+ function tplLog() {
762
+ return `# Project Log
763
+
764
+ Append-only chronological log for project memory updates.
765
+
766
+ ## [${nowISO()}] init | Initialized memoc memory structure.
767
+ `;
768
+ }
769
+
770
+ function tplContextForgeUsage() {
771
+ return `# memoc Usage
772
+
773
+ This project uses \`memoc\` to maintain agent-readable project memory.
774
+
732
775
  ## Commands
733
776
 
734
777
  \`\`\`bash
778
+ # Optional: put the project-local wrapper first in PATH for this shell
779
+ # PowerShell: . .\\.memoc\\env.ps1
780
+ # sh/bash: . ./.memoc/env.sh
781
+
735
782
  # First-time setup (or re-run to update managed sections)
736
783
  memoc init
737
784
 
738
785
  # Explicitly update managed sections based on current project state
739
786
  memoc update
740
-
741
- # Tiny status overview
742
- memoc summary
743
-
744
- # Find files first; add --snippets only when needed
745
- memoc search "<query>" --limit 12
787
+
788
+ # Tiny status overview
789
+ memoc summary
790
+
791
+ # Find files first; add --snippets only when needed
792
+ memoc search "<query>" --limit 12
746
793
  memoc search "<query>" --snippets --limit 5
747
794
  \`\`\`
748
795
 
796
+ If \`memoc\` is not on PATH, use \`.\\.memoc\\bin\\memoc.cmd <command>\` on Windows or \`.memoc/bin/memoc <command>\` in sh. If that is unavailable, use \`npx @kevin0181/memoc <command>\`.
797
+
749
798
  ## Agent Read Order
750
799
 
751
800
  1. Entry-file managed block.
752
801
  2. \`.memoc/session-summary.md\` only.
753
- 3. \`memoc search "<query>"\` to get matching files first.
802
+ 3. Search in this order: \`memoc search "<query>"\`, \`.\\.memoc\\bin\\memoc.cmd search "<query>"\` or \`.memoc/bin/memoc search "<query>"\`, \`npx @kevin0181/memoc search "<query>"\`.
754
803
  4. Use \`--snippets\` only when file names are not enough.
755
804
  5. Open only task-relevant memory files.
756
-
757
- ## When To Run Memory Updates
758
-
759
- Use \`memoc update\` or \`skills/project-memory-maintainer/SKILL.md\` when:
760
-
761
- - Requirements, acceptance criteria, user preferences, or project rules changed.
762
- - Source code, config, data, content, or package scripts changed.
763
- - Architecture, data flow, routing, auth, or deployment behavior changed.
764
- - A decision was made that future agents should not revisit blindly.
765
- - Work is partial, multi-step, blocked, or likely to be resumed by another agent.
766
- - New durable knowledge belongs in \`.memoc/wiki/\` or a subsystem doc.
767
-
768
- Usually skip for pure Q&A, throwaway exploration, or tiny edits with no future impact.
769
-
770
- ## Updating The Wiki
771
-
772
- Create a new Markdown file under \`.memoc/wiki/\` when synthesized knowledge should compound across sessions.
773
-
774
- - \`.memoc/wiki/sources/\`: provenance records.
775
- - \`.memoc/wiki/topics/\`: synthesized topic pages.
776
- - \`.memoc/wiki/global/\`: project-wide principles.
777
-
778
- After creating or editing wiki pages:
779
- 1. Update \`.memoc/wiki/index.md\`.
780
- 2. Append \`.memoc/log.md\`.
781
-
782
- ## Updating System Docs
783
-
784
- Create or update \`.memoc/systems/*.md\` when a subsystem needs durable detail.
785
-
786
- Examples: \`frontend.md\`, \`deployment.md\`, \`data-sources.md\`, \`auth.md\`
787
- `;
788
- }
789
-
790
- function tplSystemsReadme() {
791
- return `# Systems
792
-
793
- Subsystem documentation for agents.
794
-
795
- ## How To Use
796
-
797
- Create a new \`.md\` file here when a subsystem becomes important enough that future agents should not rediscover it from scratch.
798
-
799
- ## Examples
800
-
801
- - \`frontend.md\` — component library, routing, state management
802
- - \`deployment.md\` — CI/CD, environment setup, release process
803
- - \`data-sources.md\` — databases, APIs, file sources
804
- - \`auth.md\` — authentication and authorization
805
- - \`design-system.md\` — colors, typography, spacing
806
- `;
807
- }
808
-
809
- function tplWikiIndex() {
810
- return `# Wiki Index
811
-
812
- Persistent LLM-maintained project wiki.
813
-
814
- ## Pages
815
-
816
- _None yet._
817
-
818
- ## Subdirectories
819
-
820
- - \`sources/\` — provenance records
821
- - \`topics/\` — synthesized topic pages
822
- - \`global/\` — project-wide principles
823
- `;
824
- }
825
-
826
- function tplWikiSources() { return `# Sources\n\n_No sources recorded yet._\n`; }
827
- function tplWikiGlossary() { return `# Glossary\n\n_No terms defined yet._\n`; }
828
- function tplWikiQuestions() { return `# Open Questions\n\n_No open questions yet._\n`; }
829
- function tplWikiSourcesReadme() { return `# Sources\n\nProvenance records for conversations, URLs, docs, and issues.\n`; }
830
- function tplWikiTopicsReadme() { return `# Topics\n\nSynthesized topic pages that compound knowledge across sessions.\n`; }
831
- function tplWikiGlobalReadme() { return `# Global\n\nProject-wide principles, positioning, and long-lived direction.\n`; }
832
- function tplWikiLint() {
833
- return `# Wiki Lint\n\nLast checked: ${nowISO()}\n\n## Issues\n\n_No issues found._\n\n## Warnings\n\n_None._\n`;
834
- }
835
-
836
- function tplSkillMaintainer() {
837
- return `---
838
- name: project-memory-maintainer
839
- description: Maintain this project's LLM-wiki memory files after durable context changes.
840
- ---
841
-
842
- # Project Memory Maintainer
843
-
844
- Use this local skill after meaningful project work so future agents can continue without rediscovering context.
845
-
805
+
806
+ ## When To Run Memory Updates
807
+
808
+ Use \`memoc update\` or \`skills/project-memory-maintainer/SKILL.md\` when:
809
+
810
+ - Requirements, acceptance criteria, user preferences, or project rules changed.
811
+ - Source code, config, data, content, or package scripts changed.
812
+ - Architecture, data flow, routing, auth, or deployment behavior changed.
813
+ - A decision was made that future agents should not revisit blindly.
814
+ - Work is partial, multi-step, blocked, or likely to be resumed by another agent.
815
+ - New durable knowledge belongs in \`.memoc/wiki/\` or a subsystem doc.
816
+
817
+ Usually skip for pure Q&A, throwaway exploration, or tiny edits with no future impact.
818
+
819
+ ## Updating The Wiki
820
+
821
+ Create a new Markdown file under \`.memoc/wiki/\` when synthesized knowledge should compound across sessions.
822
+
823
+ - \`.memoc/wiki/sources/\`: provenance records.
824
+ - \`.memoc/wiki/topics/\`: synthesized topic pages.
825
+ - \`.memoc/wiki/global/\`: project-wide principles.
826
+
827
+ After creating or editing wiki pages:
828
+ 1. Update \`.memoc/wiki/index.md\`.
829
+ 2. Append \`.memoc/log.md\`.
830
+
831
+ ## Updating System Docs
832
+
833
+ Create or update \`.memoc/systems/*.md\` when a subsystem needs durable detail.
834
+
835
+ Examples: \`frontend.md\`, \`deployment.md\`, \`data-sources.md\`, \`auth.md\`
836
+ `;
837
+ }
838
+
839
+ function tplSystemsReadme() {
840
+ return `# Systems
841
+
842
+ Subsystem documentation for agents.
843
+
844
+ ## How To Use
845
+
846
+ Create a new \`.md\` file here when a subsystem becomes important enough that future agents should not rediscover it from scratch.
847
+
848
+ ## Examples
849
+
850
+ - \`frontend.md\` — component library, routing, state management
851
+ - \`deployment.md\` — CI/CD, environment setup, release process
852
+ - \`data-sources.md\` — databases, APIs, file sources
853
+ - \`auth.md\` — authentication and authorization
854
+ - \`design-system.md\` — colors, typography, spacing
855
+ `;
856
+ }
857
+
858
+ function tplWikiIndex() {
859
+ return `# Wiki Index
860
+
861
+ Persistent LLM-maintained project wiki.
862
+
863
+ ## Pages
864
+
865
+ _None yet._
866
+
867
+ ## Subdirectories
868
+
869
+ - \`sources/\` — provenance records
870
+ - \`topics/\` — synthesized topic pages
871
+ - \`global/\` — project-wide principles
872
+ `;
873
+ }
874
+
875
+ function tplWikiSources() { return `# Sources\n\n_No sources recorded yet._\n`; }
876
+ function tplWikiGlossary() { return `# Glossary\n\n_No terms defined yet._\n`; }
877
+ function tplWikiQuestions() { return `# Open Questions\n\n_No open questions yet._\n`; }
878
+ function tplWikiSourcesReadme() { return `# Sources\n\nProvenance records for conversations, URLs, docs, and issues.\n`; }
879
+ function tplWikiTopicsReadme() { return `# Topics\n\nSynthesized topic pages that compound knowledge across sessions.\n`; }
880
+ function tplWikiGlobalReadme() { return `# Global\n\nProject-wide principles, positioning, and long-lived direction.\n`; }
881
+ function tplWikiLint() {
882
+ return `# Wiki Lint\n\nLast checked: ${nowISO()}\n\n## Issues\n\n_No issues found._\n\n## Warnings\n\n_None._\n`;
883
+ }
884
+
885
+ function tplSkillMaintainer() {
886
+ return `---
887
+ name: project-memory-maintainer
888
+ description: Maintain this project's LLM-wiki memory files after durable context changes.
889
+ ---
890
+
891
+ # Project Memory Maintainer
892
+
893
+ Use this local skill after meaningful project work so future agents can continue without rediscovering context.
894
+
846
895
  ## Required Reads
847
896
 
848
897
  1. \`.memoc/session-summary.md\`
849
- 2. \`memoc summary\` or \`memoc search "<query>"\`
898
+ 2. \`memoc summary\` or \`memoc search "<query>"\`; if unavailable, use \`.\\.memoc\\bin\\memoc.cmd <command>\` or \`.memoc/bin/memoc <command>\`, then \`npx @kevin0181/memoc <command>\`
850
899
  3. Open only files you will use or update.
851
-
852
- ## Maintenance Checklist
853
-
854
- - Keep \`llms.txt\` and \`.memoc/00-agent-index.md\` as concise maps.
855
- - Keep \`.memoc/00-project-brief.md\` as the shortest project summary.
856
- - Update \`.memoc/02-current-project-state.md\` with new status, tasks, commands, and change log entries.
857
- - Update \`.memoc/03-decisions.md\` when a durable decision is made.
858
- - Update \`.memoc/04-handoff.md\` before ending substantial work.
859
- - Check \`.memoc/05-done-checklist.md\` before saying substantial work is complete.
860
- - Update \`.memoc/06-project-rules.md\` when the user gives durable preferences.
861
- - Append \`.memoc/log.md\` for meaningful changes, decisions, and handoffs.
862
- - Create or update \`.memoc/systems/*.md\` when a subsystem needs durable explanation.
863
- - Create or update \`.memoc/wiki/*.md\` when synthesized knowledge should compound over time.
864
- - Keep completed history in \`.memoc/log.md\`; keep current-state files short.
865
- - Keep tool output small; prefer \`summary\`, file-only search, \`--limit\`, and targeted reads.
866
-
867
- ## Concrete Triggers
868
-
869
- Use this skill before finishing when any of these are true:
870
-
871
- - The user gives a durable preference, project rule, changed requirement, or acceptance criterion.
872
- - The agent edits code, config, package scripts, env, data, assets, routes, or deployment files.
873
- - A subsystem's behavior, architecture, data flow, or API contract changes.
874
- - A future agent would need to know why an approach was chosen or rejected.
875
- - The work is partial, blocked, risky, multi-step, or likely to be resumed later.
876
-
877
- Usually skip for pure Q&A, tiny edits with no future impact, or throwaway exploration.
878
- `;
879
- }
880
-
881
- // ═══════════════════════════════════════════════════════════════════
882
- // CLAUDE CODE HOOK SETTINGS
883
- // ═══════════════════════════════════════════════════════════════════
884
-
885
- function claudeStopHookCmd() {
886
- return `node -e "const fs=require('fs'),{execSync}=require('child_process');try{const o=execSync('git status --porcelain',{stdio:'pipe'}).toString();if(o.trim()){const files=o.trim().split('\\n').map(l=>l.slice(3).trim()).filter(Boolean).slice(0,8).join(', ');fs.writeFileSync('.memoc/.pending',new Date().toISOString()+'\\n'+files)}}catch{}" 2>/dev/null || true`;
887
- }
888
-
889
- function tplClaudeSettings() {
890
- return JSON.stringify({
891
- hooks: {
892
- Stop: [{ matcher: '', hooks: [{ type: 'command', command: claudeStopHookCmd() }] }],
893
- },
894
- }, null, 2) + '\n';
895
- }
896
-
897
- function ensureClaudeStopHook(settingsPath) {
898
- const cmd = claudeStopHookCmd();
899
- let settings;
900
- try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); }
901
- catch { settings = {}; }
902
-
903
- if (!settings.hooks) settings.hooks = {};
904
- if (!settings.hooks.Stop) settings.hooks.Stop = [];
905
-
906
- const alreadyPresent = settings.hooks.Stop.some(entry =>
907
- Array.isArray(entry.hooks) && entry.hooks.some(h => h.command === cmd)
908
- );
909
- if (alreadyPresent) return false; // no change needed
910
-
911
- settings.hooks.Stop.push({ matcher: '', hooks: [{ type: 'command', command: cmd }] });
912
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
913
- return true; // merged
914
- }
915
-
916
- // ═══════════════════════════════════════════════════════════════════
917
- // MANAGED BLOCK UPDATE (CLAUDE.md / AGENTS.md)
918
- // ═══════════════════════════════════════════════════════════════════
919
-
920
- function applyManagedBlock(filePath, tplFn) {
921
- if (!fs.existsSync(filePath)) {
922
- write(filePath, tplFn());
923
- return 'add';
924
- }
925
- const src = fs.readFileSync(filePath, 'utf8');
926
- const s = src.indexOf(MGMT_S);
927
- const e = src.indexOf(MGMT_E);
928
- if (s === -1 || e === -1) {
929
- // No managed block — inject at end, preserving all user content
930
- write(filePath, src.trimEnd() + '\n\n' + managedBlock() + '\n');
931
- return 'inject';
932
- }
933
- write(filePath, src.slice(0, s) + managedBlock() + src.slice(e + MGMT_E.length));
934
- return 'update';
935
- }
936
-
937
- // ═══════════════════════════════════════════════════════════════════
938
- // MAIN RUNNER
939
- // ═══════════════════════════════════════════════════════════════════
940
-
941
- function run(dir, forceUpdate) {
942
- const p = scanProject(dir);
943
- const memDir = path.join(dir, '.memoc');
944
- const isNew = !fs.existsSync(path.join(memDir, 'boot.md'));
945
- const mode = (isNew && !forceUpdate) ? 'init' : 'update';
946
-
947
- const log = [];
948
- const mark = (label, name) => log.push(` ${label.padEnd(8)} ${name}`);
949
-
950
- if (mode === 'init') {
951
- console.log(`\n memoc init — ${path.basename(dir)}`);
952
- console.log(p.isEmpty
953
- ? ' Empty project → using default values.'
954
- : ` Detected: ${stackStr(p.stack)}`
955
- );
956
- console.log();
957
-
958
- // Entry files — inject/update managed block, preserve existing user content
959
- mark(applyManagedBlock(path.join(dir, 'CLAUDE.md'), tplClaude), 'CLAUDE.md');
960
- mark(applyManagedBlock(path.join(dir, 'AGENTS.md'), tplAgents), 'AGENTS.md');
961
- if (ensure(path.join(dir, 'llms.txt'), tplLlmsTxt(p))) mark('add', 'llms.txt');
962
- else mark('skip', 'llms.txt');
963
-
964
- // Dynamic memory files
965
- const dynamicFiles = [
966
- [path.join(memDir, '00-project-brief.md'), () => tplProjectBrief(p)],
967
- [path.join(memDir, '00-agent-index.md'), () => tplAgentIndex(p)],
968
- [path.join(memDir, '02-current-project-state.md'), () => tplCurrentState(p)],
969
- [path.join(memDir, 'session-summary.md'), tplSessionSummary],
970
- ];
971
- for (const [fp, tpl] of dynamicFiles) {
972
- const rel = path.relative(dir, fp);
973
- if (ensure(fp, tpl())) mark('add', rel); else mark('skip', rel);
974
- }
975
-
976
- // Static memory files
977
- const staticFiles = [
978
- [path.join(memDir, 'boot.md'), tplBoot],
979
- [path.join(memDir, '01-agent-workflow.md'), tplWorkflow],
980
- [path.join(memDir, '03-decisions.md'), tplDecisions],
981
- [path.join(memDir, '04-handoff.md'), tplHandoff],
982
- [path.join(memDir, '05-done-checklist.md'), tplDoneChecklist],
983
- [path.join(memDir, '06-project-rules.md'), tplProjectRules],
984
- [path.join(memDir, 'log.md'), tplLog],
985
- [path.join(memDir, 'memoc-usage.md'), tplContextForgeUsage],
986
- [path.join(memDir, 'systems/README.md'), tplSystemsReadme],
987
- [path.join(memDir, 'wiki/index.md'), tplWikiIndex],
988
- [path.join(memDir, 'wiki/sources.md'), tplWikiSources],
989
- [path.join(memDir, 'wiki/glossary.md'), tplWikiGlossary],
990
- [path.join(memDir, 'wiki/questions.md'), tplWikiQuestions],
991
- [path.join(memDir, 'wiki/lint.md'), tplWikiLint],
992
- [path.join(memDir, 'wiki/sources/README.md'), tplWikiSourcesReadme],
993
- [path.join(memDir, 'wiki/topics/README.md'), tplWikiTopicsReadme],
994
- [path.join(memDir, 'wiki/global/README.md'), tplWikiGlobalReadme],
995
- [path.join(dir, 'skills/project-memory-maintainer/SKILL.md'), tplSkillMaintainer],
996
- ];
997
- for (const [fp, tpl] of staticFiles) {
998
- const rel = path.relative(dir, fp);
999
- if (ensure(fp, tpl())) mark('add', rel); else mark('skip', rel);
1000
- }
1001
-
1002
- // Claude Code Stop hook — writes .memoc/.pending when git detects changes
1003
- const claudeDir = path.join(dir, '.claude');
1004
- const claudeSettings = path.join(claudeDir, 'settings.json');
1005
- fs.mkdirSync(claudeDir, { recursive: true });
1006
- if (!fs.existsSync(claudeSettings)) {
1007
- write(claudeSettings, tplClaudeSettings());
1008
- mark('add', '.claude/settings.json');
1009
- } else {
1010
- const merged = ensureClaudeStopHook(claudeSettings);
1011
- mark(merged ? 'update' : 'skip', `.claude/settings.json (Stop hook ${merged ? 'merged' : 'already present'})`);
1012
- }
1013
-
1014
- // .gitignore — add .memoc/.pending if not already present
1015
- const gitignorePath = path.join(dir, '.gitignore');
1016
- const PENDING_ENTRY = '.memoc/.pending';
1017
- const gitignoreContent = fs.existsSync(gitignorePath)
1018
- ? fs.readFileSync(gitignorePath, 'utf8') : '';
1019
- if (!gitignoreContent.includes(PENDING_ENTRY)) {
1020
- fs.appendFileSync(gitignorePath, (gitignoreContent.endsWith('\n') ? '' : '\n') + PENDING_ENTRY + '\n', 'utf8');
1021
- mark('update', '.gitignore (.memoc/.pending added)');
900
+
901
+ ## Maintenance Checklist
902
+
903
+ - Keep \`llms.txt\` and \`.memoc/00-agent-index.md\` as concise maps.
904
+ - Keep \`.memoc/00-project-brief.md\` as the shortest project summary.
905
+ - Update \`.memoc/02-current-project-state.md\` with new status, tasks, commands, and change log entries.
906
+ - Update \`.memoc/03-decisions.md\` when a durable decision is made.
907
+ - Update \`.memoc/04-handoff.md\` before ending substantial work.
908
+ - Check \`.memoc/05-done-checklist.md\` before saying substantial work is complete.
909
+ - Update \`.memoc/06-project-rules.md\` when the user gives durable preferences.
910
+ - Append \`.memoc/log.md\` for meaningful changes, decisions, and handoffs.
911
+ - Create or update \`.memoc/systems/*.md\` when a subsystem needs durable explanation.
912
+ - Create or update \`.memoc/wiki/*.md\` when synthesized knowledge should compound over time.
913
+ - Keep completed history in \`.memoc/log.md\`; keep current-state files short.
914
+ - Keep tool output small; prefer \`summary\`, file-only search, \`--limit\`, and targeted reads.
915
+
916
+ ## Concrete Triggers
917
+
918
+ Use this skill before finishing when any of these are true:
919
+
920
+ - The user gives a durable preference, project rule, changed requirement, or acceptance criterion.
921
+ - The agent edits code, config, package scripts, env, data, assets, routes, or deployment files.
922
+ - A subsystem's behavior, architecture, data flow, or API contract changes.
923
+ - A future agent would need to know why an approach was chosen or rejected.
924
+ - The work is partial, blocked, risky, multi-step, or likely to be resumed later.
925
+
926
+ Usually skip for pure Q&A, tiny edits with no future impact, or throwaway exploration.
927
+ `;
928
+ }
929
+
930
+ // ═══════════════════════════════════════════════════════════════════
931
+ // CLAUDE CODE HOOK SETTINGS
932
+ // ═══════════════════════════════════════════════════════════════════
933
+
934
+ function claudeStopHookCmd() {
935
+ return `node -e "const fs=require('fs'),{execSync}=require('child_process');try{const o=execSync('git status --porcelain',{stdio:'pipe'}).toString();if(o.trim()){const files=o.trim().split('\\n').map(l=>l.slice(3).trim()).filter(Boolean).slice(0,8).join(', ');fs.writeFileSync('.memoc/.pending',new Date().toISOString()+'\\n'+files)}}catch{}" 2>/dev/null || true`;
936
+ }
937
+
938
+ function tplClaudeSettings() {
939
+ return JSON.stringify({
940
+ hooks: {
941
+ Stop: [{ matcher: '', hooks: [{ type: 'command', command: claudeStopHookCmd() }] }],
942
+ },
943
+ }, null, 2) + '\n';
944
+ }
945
+
946
+ function ensureClaudeStopHook(settingsPath) {
947
+ const cmd = claudeStopHookCmd();
948
+ let settings;
949
+ try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); }
950
+ catch { settings = {}; }
951
+
952
+ if (!settings.hooks) settings.hooks = {};
953
+ if (!settings.hooks.Stop) settings.hooks.Stop = [];
954
+
955
+ const alreadyPresent = settings.hooks.Stop.some(entry =>
956
+ Array.isArray(entry.hooks) && entry.hooks.some(h => h.command === cmd)
957
+ );
958
+ if (alreadyPresent) return false; // no change needed
959
+
960
+ settings.hooks.Stop.push({ matcher: '', hooks: [{ type: 'command', command: cmd }] });
961
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
962
+ return true; // merged
963
+ }
964
+
965
+ // ═══════════════════════════════════════════════════════════════════
966
+ // MANAGED BLOCK UPDATE (CLAUDE.md / AGENTS.md)
967
+ // ═══════════════════════════════════════════════════════════════════
968
+
969
+ function applyManagedBlock(filePath, tplFn) {
970
+ if (!fs.existsSync(filePath)) {
971
+ write(filePath, tplFn());
972
+ return 'add';
973
+ }
974
+ const src = fs.readFileSync(filePath, 'utf8');
975
+ const s = src.indexOf(MGMT_S);
976
+ const e = src.indexOf(MGMT_E);
977
+ if (s === -1 || e === -1) {
978
+ // No managed block — inject at end, preserving all user content
979
+ write(filePath, src.trimEnd() + '\n\n' + managedBlock() + '\n');
980
+ return 'inject';
981
+ }
982
+ write(filePath, src.slice(0, s) + managedBlock() + src.slice(e + MGMT_E.length));
983
+ return 'update';
984
+ }
985
+
986
+ // ═══════════════════════════════════════════════════════════════════
987
+ // MAIN RUNNER
988
+ // ═══════════════════════════════════════════════════════════════════
989
+
990
+ function run(dir, forceUpdate) {
991
+ const p = scanProject(dir);
992
+ const memDir = path.join(dir, '.memoc');
993
+ const isNew = !fs.existsSync(path.join(memDir, 'boot.md'));
994
+ const mode = (isNew && !forceUpdate) ? 'init' : 'update';
995
+
996
+ const log = [];
997
+ const mark = (label, name) => log.push(` ${label.padEnd(8)} ${name}`);
998
+
999
+ if (mode === 'init') {
1000
+ console.log(`\n memoc init — ${path.basename(dir)}`);
1001
+ console.log(p.isEmpty
1002
+ ? ' Empty project → using default values.'
1003
+ : ` Detected: ${stackStr(p.stack)}`
1004
+ );
1005
+ console.log();
1006
+
1007
+ // Entry files — inject/update managed block, preserve existing user content
1008
+ mark(applyManagedBlock(path.join(dir, 'CLAUDE.md'), tplClaude), 'CLAUDE.md');
1009
+ mark(applyManagedBlock(path.join(dir, 'AGENTS.md'), tplAgents), 'AGENTS.md');
1010
+ if (ensure(path.join(dir, 'llms.txt'), tplLlmsTxt(p))) mark('add', 'llms.txt');
1011
+ else mark('skip', 'llms.txt');
1012
+
1013
+ // Dynamic memory files
1014
+ const dynamicFiles = [
1015
+ [path.join(memDir, '00-project-brief.md'), () => tplProjectBrief(p)],
1016
+ [path.join(memDir, '00-agent-index.md'), () => tplAgentIndex(p)],
1017
+ [path.join(memDir, '02-current-project-state.md'), () => tplCurrentState(p)],
1018
+ [path.join(memDir, 'session-summary.md'), tplSessionSummary],
1019
+ ];
1020
+ for (const [fp, tpl] of dynamicFiles) {
1021
+ const rel = path.relative(dir, fp);
1022
+ if (ensure(fp, tpl())) mark('add', rel); else mark('skip', rel);
1023
+ }
1024
+
1025
+ // Static memory files
1026
+ const staticFiles = [
1027
+ [path.join(memDir, 'boot.md'), tplBoot],
1028
+ [path.join(memDir, '01-agent-workflow.md'), tplWorkflow],
1029
+ [path.join(memDir, '03-decisions.md'), tplDecisions],
1030
+ [path.join(memDir, '04-handoff.md'), tplHandoff],
1031
+ [path.join(memDir, '05-done-checklist.md'), tplDoneChecklist],
1032
+ [path.join(memDir, '06-project-rules.md'), tplProjectRules],
1033
+ [path.join(memDir, 'log.md'), tplLog],
1034
+ [path.join(memDir, 'memoc-usage.md'), tplContextForgeUsage],
1035
+ [path.join(memDir, 'systems/README.md'), tplSystemsReadme],
1036
+ [path.join(memDir, 'wiki/index.md'), tplWikiIndex],
1037
+ [path.join(memDir, 'wiki/sources.md'), tplWikiSources],
1038
+ [path.join(memDir, 'wiki/glossary.md'), tplWikiGlossary],
1039
+ [path.join(memDir, 'wiki/questions.md'), tplWikiQuestions],
1040
+ [path.join(memDir, 'wiki/lint.md'), tplWikiLint],
1041
+ [path.join(memDir, 'wiki/sources/README.md'), tplWikiSourcesReadme],
1042
+ [path.join(memDir, 'wiki/topics/README.md'), tplWikiTopicsReadme],
1043
+ [path.join(memDir, 'wiki/global/README.md'), tplWikiGlobalReadme],
1044
+ [path.join(dir, 'skills/project-memory-maintainer/SKILL.md'), tplSkillMaintainer],
1045
+ ];
1046
+ for (const [fp, tpl] of staticFiles) {
1047
+ const rel = path.relative(dir, fp);
1048
+ if (ensure(fp, tpl())) mark('add', rel); else mark('skip', rel);
1049
+ }
1050
+
1051
+ // Claude Code Stop hook — writes .memoc/.pending when git detects changes
1052
+ const claudeDir = path.join(dir, '.claude');
1053
+ const claudeSettings = path.join(claudeDir, 'settings.json');
1054
+ fs.mkdirSync(claudeDir, { recursive: true });
1055
+ if (!fs.existsSync(claudeSettings)) {
1056
+ write(claudeSettings, tplClaudeSettings());
1057
+ mark('add', '.claude/settings.json');
1058
+ } else {
1059
+ const merged = ensureClaudeStopHook(claudeSettings);
1060
+ mark(merged ? 'update' : 'skip', `.claude/settings.json (Stop hook ${merged ? 'merged' : 'already present'})`);
1061
+ }
1062
+
1063
+ // .gitignore — add .memoc/.pending if not already present
1064
+ const gitignorePath = path.join(dir, '.gitignore');
1065
+ const PENDING_ENTRY = '.memoc/.pending';
1066
+ const gitignoreContent = fs.existsSync(gitignorePath)
1067
+ ? fs.readFileSync(gitignorePath, 'utf8') : '';
1068
+ if (!gitignoreContent.includes(PENDING_ENTRY)) {
1069
+ fs.appendFileSync(gitignorePath, (gitignoreContent.endsWith('\n') ? '' : '\n') + PENDING_ENTRY + '\n', 'utf8');
1070
+ mark('update', '.gitignore (.memoc/.pending added)');
1022
1071
  } else {
1023
1072
  mark('skip', '.gitignore (.memoc/.pending already present)');
1024
1073
  }
1025
1074
 
1026
- } else {
1027
- // ── UPDATE MODE
1028
- console.log(`\n memoc update — ${path.basename(dir)}`);
1029
- console.log(` Re-scanning project: ${p.isEmpty ? 'nothing detected' : stackStr(p.stack)}`);
1030
- console.log();
1031
-
1032
- // Entry files — update managed blocks, preserve user content
1033
- mark(applyManagedBlock(path.join(dir, 'CLAUDE.md'), tplClaude), 'CLAUDE.md');
1034
- mark(applyManagedBlock(path.join(dir, 'AGENTS.md'), tplAgents), 'AGENTS.md');
1035
-
1036
- // Third-party agent files — update only if already added
1037
- for (const [, agent] of Object.entries(AGENT_REGISTRY)) {
1038
- const fp = path.join(dir, agent.file);
1039
- if (fs.existsSync(fp)) {
1040
- mark(applyManagedBlock(fp, () => tplAgentEntry(agent.label)), agent.file);
1041
- }
1042
- }
1075
+ // PATH helpers — let agents run memoc even when the npm bin is not on PATH
1076
+ ensurePathHelpers(dir, mark);
1043
1077
 
1044
- // llms.txt — update all managed sections
1045
- const llmsPath = path.join(dir, 'llms.txt');
1046
- if (fs.existsSync(llmsPath)) {
1047
- updateSection(llmsPath, HDR_S, HDR_E, headerInner(p));
1048
- updateSection(llmsPath, CORE_S, CORE_E, coreLlmsInner());
1049
- updateSection(llmsPath, SYS_S, SYS_E, systemsLlmsInner(dir));
1050
- updateSection(llmsPath, WIKI_S, WIKI_E, wikiLlmsInner(dir));
1051
- mark('update', 'llms.txt');
1052
- } else {
1053
- write(llmsPath, tplLlmsTxt(p));
1054
- mark('add', 'llms.txt');
1055
- }
1056
-
1057
- // Dynamic memory files — update managed sections only
1058
- const dynUpdates = [
1059
- [path.join(memDir, '00-project-brief.md'), () => tplProjectBrief(p), ID_S, ID_E, identityInner(p)],
1060
- [path.join(memDir, '00-agent-index.md'), () => tplAgentIndex(p), SNAP_S, SNAP_E, snapshotInner(p)],
1061
- [path.join(memDir, '02-current-project-state.md'), () => tplCurrentState(p), SNAP_S, SNAP_E, snapshotInner(p)],
1062
- ];
1063
- for (const [fp, tpl, s, e, inner] of dynUpdates) {
1064
- const rel = path.relative(dir, fp);
1065
- if (!fs.existsSync(fp)) {
1066
- write(fp, tpl());
1067
- mark('add', rel);
1068
- } else if (updateSection(fp, s, e, inner)) {
1069
- mark('update', `${rel} (managed section)`);
1070
- } else {
1071
- mark('skip', rel);
1072
- }
1073
- }
1074
-
1075
- // session-summary is agent-ownednever overwrite, only add if missing
1076
- const summaryPath = path.join(memDir, 'session-summary.md');
1077
- if (fs.existsSync(summaryPath)) {
1078
- const summarySize = Buffer.byteLength(fs.readFileSync(summaryPath, 'utf8'), 'utf8');
1079
- if (summarySize > 1000) {
1080
- console.log(` ⚠ session-summary.md is ${summarySize}B (recommended: <800B).`);
1081
- }
1082
- mark('skip', '.memoc/session-summary.md (agent-owned, not modified)');
1083
- } else {
1084
- write(summaryPath, tplSessionSummary());
1085
- mark('add', '.memoc/session-summary.md');
1086
- }
1087
-
1088
- // Static + user-owned files — only add if missing
1089
- const addIfMissing = [
1090
- [path.join(memDir, 'boot.md'), tplBoot],
1091
- [path.join(memDir, '01-agent-workflow.md'), tplWorkflow],
1092
- [path.join(memDir, '03-decisions.md'), tplDecisions],
1093
- [path.join(memDir, '04-handoff.md'), tplHandoff],
1094
- [path.join(memDir, '05-done-checklist.md'), tplDoneChecklist],
1095
- [path.join(memDir, '06-project-rules.md'), tplProjectRules],
1096
- [path.join(memDir, 'log.md'), tplLog],
1097
- [path.join(memDir, 'memoc-usage.md'), tplContextForgeUsage],
1098
- [path.join(memDir, 'systems/README.md'), tplSystemsReadme],
1099
- [path.join(memDir, 'wiki/index.md'), tplWikiIndex],
1100
- [path.join(memDir, 'wiki/sources.md'), tplWikiSources],
1101
- [path.join(memDir, 'wiki/glossary.md'), tplWikiGlossary],
1102
- [path.join(memDir, 'wiki/questions.md'), tplWikiQuestions],
1103
- [path.join(memDir, 'wiki/lint.md'), tplWikiLint],
1104
- [path.join(memDir, 'wiki/sources/README.md'), tplWikiSourcesReadme],
1105
- [path.join(memDir, 'wiki/topics/README.md'), tplWikiTopicsReadme],
1106
- [path.join(memDir, 'wiki/global/README.md'), tplWikiGlobalReadme],
1107
- [path.join(dir, 'skills/project-memory-maintainer/SKILL.md'), tplSkillMaintainer],
1108
- ];
1078
+ } else {
1079
+ // ── UPDATE MODE
1080
+ console.log(`\n memoc update — ${path.basename(dir)}`);
1081
+ console.log(` Re-scanning project: ${p.isEmpty ? 'nothing detected' : stackStr(p.stack)}`);
1082
+ console.log();
1083
+
1084
+ // Entry files — update managed blocks, preserve user content
1085
+ mark(applyManagedBlock(path.join(dir, 'CLAUDE.md'), tplClaude), 'CLAUDE.md');
1086
+ mark(applyManagedBlock(path.join(dir, 'AGENTS.md'), tplAgents), 'AGENTS.md');
1087
+
1088
+ // Third-party agent files — update only if already added
1089
+ for (const [, agent] of Object.entries(AGENT_REGISTRY)) {
1090
+ const fp = path.join(dir, agent.file);
1091
+ if (fs.existsSync(fp)) {
1092
+ mark(applyManagedBlock(fp, () => tplAgentEntry(agent.label)), agent.file);
1093
+ }
1094
+ }
1095
+
1096
+ // llms.txt — update all managed sections
1097
+ const llmsPath = path.join(dir, 'llms.txt');
1098
+ if (fs.existsSync(llmsPath)) {
1099
+ updateSection(llmsPath, HDR_S, HDR_E, headerInner(p));
1100
+ updateSection(llmsPath, CORE_S, CORE_E, coreLlmsInner());
1101
+ updateSection(llmsPath, SYS_S, SYS_E, systemsLlmsInner(dir));
1102
+ updateSection(llmsPath, WIKI_S, WIKI_E, wikiLlmsInner(dir));
1103
+ mark('update', 'llms.txt');
1104
+ } else {
1105
+ write(llmsPath, tplLlmsTxt(p));
1106
+ mark('add', 'llms.txt');
1107
+ }
1108
+
1109
+ // Dynamic memory filesupdate managed sections only
1110
+ const dynUpdates = [
1111
+ [path.join(memDir, '00-project-brief.md'), () => tplProjectBrief(p), ID_S, ID_E, identityInner(p)],
1112
+ [path.join(memDir, '00-agent-index.md'), () => tplAgentIndex(p), SNAP_S, SNAP_E, snapshotInner(p)],
1113
+ [path.join(memDir, '02-current-project-state.md'), () => tplCurrentState(p), SNAP_S, SNAP_E, snapshotInner(p)],
1114
+ ];
1115
+ for (const [fp, tpl, s, e, inner] of dynUpdates) {
1116
+ const rel = path.relative(dir, fp);
1117
+ if (!fs.existsSync(fp)) {
1118
+ write(fp, tpl());
1119
+ mark('add', rel);
1120
+ } else if (updateSection(fp, s, e, inner)) {
1121
+ mark('update', `${rel} (managed section)`);
1122
+ } else {
1123
+ mark('skip', rel);
1124
+ }
1125
+ }
1126
+
1127
+ // session-summary is agent-owned — never overwrite, only add if missing
1128
+ const summaryPath = path.join(memDir, 'session-summary.md');
1129
+ if (fs.existsSync(summaryPath)) {
1130
+ const summarySize = Buffer.byteLength(fs.readFileSync(summaryPath, 'utf8'), 'utf8');
1131
+ if (summarySize > 1000) {
1132
+ console.log(` ⚠ session-summary.md is ${summarySize}B (recommended: <800B).`);
1133
+ }
1134
+ mark('skip', '.memoc/session-summary.md (agent-owned, not modified)');
1135
+ } else {
1136
+ write(summaryPath, tplSessionSummary());
1137
+ mark('add', '.memoc/session-summary.md');
1138
+ }
1139
+
1140
+ // Static + user-owned files — only add if missing
1141
+ const addIfMissing = [
1142
+ [path.join(memDir, 'boot.md'), tplBoot],
1143
+ [path.join(memDir, '01-agent-workflow.md'), tplWorkflow],
1144
+ [path.join(memDir, '03-decisions.md'), tplDecisions],
1145
+ [path.join(memDir, '04-handoff.md'), tplHandoff],
1146
+ [path.join(memDir, '05-done-checklist.md'), tplDoneChecklist],
1147
+ [path.join(memDir, '06-project-rules.md'), tplProjectRules],
1148
+ [path.join(memDir, 'log.md'), tplLog],
1149
+ [path.join(memDir, 'memoc-usage.md'), tplContextForgeUsage],
1150
+ [path.join(memDir, 'systems/README.md'), tplSystemsReadme],
1151
+ [path.join(memDir, 'wiki/index.md'), tplWikiIndex],
1152
+ [path.join(memDir, 'wiki/sources.md'), tplWikiSources],
1153
+ [path.join(memDir, 'wiki/glossary.md'), tplWikiGlossary],
1154
+ [path.join(memDir, 'wiki/questions.md'), tplWikiQuestions],
1155
+ [path.join(memDir, 'wiki/lint.md'), tplWikiLint],
1156
+ [path.join(memDir, 'wiki/sources/README.md'), tplWikiSourcesReadme],
1157
+ [path.join(memDir, 'wiki/topics/README.md'), tplWikiTopicsReadme],
1158
+ [path.join(memDir, 'wiki/global/README.md'), tplWikiGlobalReadme],
1159
+ [path.join(dir, 'skills/project-memory-maintainer/SKILL.md'), tplSkillMaintainer],
1160
+ ];
1109
1161
  for (const [fp, tpl] of addIfMissing) {
1110
1162
  const rel = path.relative(dir, fp);
1111
1163
  if (ensure(fp, tpl())) mark('add', rel);
1112
1164
  // silently skip existing — user/agent owns them
1113
1165
  }
1114
1166
 
1115
- // Append update record to log.md
1116
- const logPath = path.join(memDir, 'log.md');
1117
- if (fs.existsSync(logPath)) {
1118
- fs.appendFileSync(logPath,
1119
- `\n## [${nowISO()}] update | Re-scanned: ${p.isEmpty ? 'nothing detected' : stackStr(p.stack)}\n`,
1120
- 'utf8'
1121
- );
1122
- mark('append', '.memoc/log.md');
1123
- }
1124
- }
1125
-
1126
- hideOnWindows(memDir);
1127
- console.log(log.join('\n'));
1128
- console.log('\n Done.');
1129
- }
1130
-
1131
- // ═══════════════════════════════════════════════════════════════════
1132
- // ADD — add entry file for a specific agent
1133
- // ═══════════════════════════════════════════════════════════════════
1134
-
1135
- function runAdd(dir) {
1136
- const agentKey = (process.argv[3] || '').toLowerCase();
1137
-
1138
- if (!agentKey) {
1139
- console.log('\n Available agents:\n');
1140
- for (const [key, agent] of Object.entries(AGENT_REGISTRY)) {
1141
- const exists = fs.existsSync(path.join(dir, agent.file)) ? ' (already added)' : '';
1142
- console.log(` ${key.padEnd(10)} → ${agent.file}${exists}`);
1143
- }
1144
- console.log('\n Usage: memoc add <agent>');
1145
- return;
1146
- }
1147
-
1148
- const agent = AGENT_REGISTRY[agentKey];
1149
- if (!agent) {
1150
- console.error(`\n Unknown agent: "${agentKey}"`);
1151
- console.error(` Available: ${Object.keys(AGENT_REGISTRY).join(', ')}`);
1152
- process.exit(1);
1153
- }
1154
-
1155
- const filePath = path.join(dir, agent.file);
1156
- const result = applyManagedBlock(filePath, () => tplAgentEntry(agent.label));
1157
- console.log(`\n ${result.padEnd(8)} ${agent.file} (${agent.label})`);
1158
- console.log('\n Done.');
1159
- }
1160
-
1161
- // ═══════════════════════════════════════════════════════════════════
1162
- // SEARCH
1163
- // ═══════════════════════════════════════════════════════════════════
1164
-
1165
- function runSearch(dir) {
1166
- const rawArgs = process.argv.slice(3);
1167
- const opts = { mode: 'files', limit: 12, all: false };
1168
- const queryParts = [];
1169
-
1170
- for (let i = 0; i < rawArgs.length; i++) {
1171
- const arg = rawArgs[i];
1172
- if (arg === '--snippets') { opts.mode = 'snippets'; continue; }
1173
- if (arg === '--files') { opts.mode = 'files'; continue; }
1174
- if (arg === '--all') { opts.all = true; continue; }
1175
- if (arg === '--limit') {
1176
- const n = Number(rawArgs[++i]);
1177
- if (Number.isFinite(n) && n > 0) opts.limit = Math.floor(n);
1178
- continue;
1179
- }
1180
- if (arg.startsWith('--limit=')) {
1181
- const n = Number(arg.slice('--limit='.length));
1182
- if (Number.isFinite(n) && n > 0) opts.limit = Math.floor(n);
1183
- continue;
1184
- }
1185
- queryParts.push(arg);
1186
- }
1187
-
1188
- const query = queryParts.join(' ').toLowerCase();
1189
-
1190
- const searchRoots = [
1191
- path.join(dir, '.memoc'),
1192
- path.join(dir, 'skills'),
1193
- path.join(dir, 'llms.txt'),
1194
- path.join(dir, 'AGENTS.md'),
1195
- path.join(dir, 'CLAUDE.md'),
1196
- ...Object.values(AGENT_REGISTRY).map(agent => path.join(dir, agent.file)),
1197
- ];
1198
-
1199
- if (!query) {
1200
- // No query — list all memory files sorted by recency
1201
- const allFiles = [];
1202
- function collectFile(fp) {
1203
- if (!fs.existsSync(fp)) return;
1204
- const rel = path.relative(dir, fp);
1205
- let mtime = 0;
1206
- try { mtime = fs.statSync(fp).mtimeMs; } catch {}
1207
- allFiles.push({ file: rel, mtime });
1208
- }
1209
- function collectDir(d) {
1210
- if (!fs.existsSync(d)) return;
1211
- for (const entry of fs.readdirSync(d)) {
1212
- const fp = path.join(d, entry);
1213
- try {
1214
- if (fs.statSync(fp).isDirectory()) collectDir(fp);
1215
- else if (entry.endsWith('.md') || entry === 'llms.txt' || entry.endsWith('rules')) collectFile(fp);
1216
- } catch {}
1217
- }
1218
- }
1219
- for (const root of searchRoots) {
1220
- try {
1221
- if (fs.statSync(root).isDirectory()) collectDir(root);
1222
- else collectFile(root);
1223
- } catch {}
1224
- }
1225
- allFiles.sort((a, b) => b.mtime - a.mtime || a.file.localeCompare(b.file));
1226
- const limited = opts.all ? allFiles : allFiles.slice(0, opts.limit);
1227
- console.log(limited.map(r => r.file).join('\n'));
1228
- if (!opts.all && allFiles.length > limited.length) {
1229
- console.log(`... ${allFiles.length - limited.length} more files. Use --all to show all.`);
1230
- }
1231
- return;
1232
- }
1233
-
1234
- const matchesByFile = new Map(); // rel -> { matches: [], mtime: number }
1235
-
1236
- function searchFile(fp) {
1237
- if (!fs.existsSync(fp)) return;
1238
- const rel = path.relative(dir, fp);
1239
- let mtime = 0;
1240
- try { mtime = fs.statSync(fp).mtimeMs; } catch {}
1241
- const lines = fs.readFileSync(fp, 'utf8').split('\n');
1242
- lines.forEach((line, i) => {
1243
- if (line.toLowerCase().includes(query)) {
1244
- if (!matchesByFile.has(rel)) matchesByFile.set(rel, { matches: [], mtime });
1245
- matchesByFile.get(rel).matches.push({ line: i + 1, text: line.trim() });
1246
- }
1247
- });
1248
- }
1249
-
1250
- function walkDir(d) {
1251
- if (!fs.existsSync(d)) return;
1252
- for (const entry of fs.readdirSync(d)) {
1253
- const fp = path.join(d, entry);
1254
- try {
1255
- if (fs.statSync(fp).isDirectory()) walkDir(fp);
1256
- else if (entry.endsWith('.md') || entry === 'llms.txt' || entry.endsWith('rules')) searchFile(fp);
1257
- } catch {}
1258
- }
1259
- }
1260
-
1261
- for (const root of searchRoots) {
1262
- try {
1263
- if (fs.statSync(root).isDirectory()) walkDir(root);
1264
- else searchFile(root);
1265
- } catch {}
1266
- }
1267
-
1268
- if (!matchesByFile.size) {
1269
- console.log('No matches found.');
1270
- } else if (opts.mode === 'files') {
1271
- const rows = [...matchesByFile.entries()]
1272
- .map(([file, { matches, mtime }]) => ({ file, count: matches.length, mtime }))
1273
- .sort((a, b) => b.count - a.count || b.mtime - a.mtime || a.file.localeCompare(b.file));
1274
- const limited = opts.all ? rows : rows.slice(0, opts.limit);
1275
- console.log(limited.map(r => `${r.file} ${r.count} match${r.count === 1 ? '' : 'es'}`).join('\n'));
1276
- if (!opts.all && rows.length > limited.length) {
1277
- console.log(`... ${rows.length - limited.length} more files. Use --all to show all, or --snippets for line matches.`);
1278
- }
1279
- } else {
1280
- const snippets = [];
1281
- for (const [file, { matches }] of matchesByFile.entries()) {
1282
- for (const m of matches) snippets.push(`${file}:${m.line} ${m.text}`);
1283
- }
1284
- const limited = opts.all ? snippets : snippets.slice(0, opts.limit);
1285
- console.log(limited.join('\n'));
1286
- if (!opts.all && snippets.length > limited.length) {
1287
- console.log(`... ${snippets.length - limited.length} more matches. Use --all to show all, or --limit N.`);
1288
- }
1289
- }
1290
- }
1291
-
1292
- // ═══════════════════════════════════════════════════════════════════
1293
- // TOKENS — estimate token cost of current memory state
1294
- // ═══════════════════════════════════════════════════════════════════
1295
-
1296
- function runTokens(dir) {
1297
- const est = text => Math.ceil(Buffer.byteLength(text, 'utf8') / 4);
1298
- const read = fp => { try { return fs.readFileSync(fp, 'utf8'); } catch { return ''; } };
1299
- const memDir = path.join(dir, '.memoc');
1300
-
1301
- const startup = [
1302
- ['CLAUDE.md', path.join(dir, 'CLAUDE.md')],
1303
- ['session-summary.md', path.join(memDir, 'session-summary.md')],
1304
- ];
1305
- const onDemand = [
1306
- ['llms.txt', path.join(dir, 'llms.txt')],
1307
- ['02-current-project-state.md', path.join(memDir, '02-current-project-state.md')],
1308
- ['03-decisions.md', path.join(memDir, '03-decisions.md')],
1309
- ['04-handoff.md', path.join(memDir, '04-handoff.md')],
1310
- ['06-project-rules.md', path.join(memDir, '06-project-rules.md')],
1311
- ['log.md', path.join(memDir, 'log.md')],
1312
- ];
1313
-
1314
- console.log('\n memoc tokens\n');
1315
- let startupTotal = 0;
1316
- console.log(' Startup (always loaded):');
1317
- for (const [name, fp] of startup) {
1318
- const content = read(fp);
1319
- const t = est(content);
1320
- const b = Buffer.byteLength(content, 'utf8');
1321
- startupTotal += t;
1322
- const warn = b > 1000 ? ' ⚠ large' : '';
1323
- console.log(` ${name.padEnd(32)} ${String(t).padStart(5)} tokens (${b}B)${warn}`);
1324
- }
1325
- console.log(` ${'── startup total'.padEnd(32)} ${String(startupTotal).padStart(5)} tokens`);
1326
-
1327
- console.log('\n On-demand (read when needed):');
1328
- let onDemandTotal = 0;
1329
- for (const [name, fp] of onDemand) {
1330
- const content = read(fp);
1331
- if (!content) continue;
1332
- const t = est(content);
1333
- const b = Buffer.byteLength(content, 'utf8');
1334
- onDemandTotal += t;
1335
- const warn = t > 500 ? ' ⚠ consider compress' : '';
1336
- console.log(` ${name.padEnd(32)} ${String(t).padStart(5)} tokens (${b}B)${warn}`);
1337
- }
1338
- console.log(` ${'── on-demand total'.padEnd(32)} ${String(onDemandTotal).padStart(5)} tokens`);
1339
- console.log(`\n If all loaded: ~${startupTotal + onDemandTotal} tokens`);
1340
-
1341
- const summaryContent = read(path.join(memDir, 'session-summary.md'));
1342
- const summaryBytes = Buffer.byteLength(summaryContent, 'utf8');
1343
- if (summaryBytes > 800) {
1344
- console.log(`\n ⚠ session-summary.md is ${summaryBytes}B — recommended <800B. Trim it manually.`);
1345
- }
1346
- console.log();
1347
- }
1348
-
1349
- // ═══════════════════════════════════════════════════════════════════
1350
- // COMPRESS — archive old log.md entries to keep file small
1351
- // ═══════════════════════════════════════════════════════════════════
1352
-
1353
- function runCompress(dir) {
1354
- const KEEP = 20;
1355
- const logPath = path.join(dir, '.memoc', 'log.md');
1356
- const archivePath = path.join(dir, '.memoc', 'log-archive.md');
1357
-
1358
- if (!fs.existsSync(logPath)) {
1359
- console.log('\n No .memoc/log.md found.\n');
1360
- return;
1361
- }
1362
-
1363
- const src = fs.readFileSync(logPath, 'utf8');
1364
- // Split on entry headers, keep header as part of each chunk
1365
- const parts = src.split(/(?=\n## \[)/);
1366
- const header = parts[0]; // everything before first entry
1367
- const entries = parts.slice(1).filter(e => e.trim());
1368
-
1369
- if (entries.length <= KEEP) {
1370
- console.log(`\n log.md has ${entries.length} entries — nothing to compress (threshold: ${KEEP}).\n`);
1371
- return;
1372
- }
1373
-
1374
- const toArchive = entries.slice(0, entries.length - KEEP);
1375
- const toKeep = entries.slice(entries.length - KEEP);
1376
-
1377
- // Append to archive
1378
- const archiveExists = fs.existsSync(archivePath);
1379
- const archiveHeader = archiveExists ? '' : '# Log Archive\n\nOlder entries moved from log.md by `memoc compress`.\n';
1380
- fs.appendFileSync(archivePath, archiveHeader + toArchive.join('') + '\n', 'utf8');
1381
-
1382
- // Rewrite log.md with only recent entries
1383
- write(logPath, header.trimEnd() + '\n' + toKeep.join('') + '\n');
1167
+ // PATH helpers let agents run memoc even when the npm bin is not on PATH
1168
+ ensurePathHelpers(dir, mark);
1384
1169
 
1385
- console.log(`\n memoc compress\n`);
1386
- console.log(` Archived ${toArchive.length} entries .memoc/log-archive.md`);
1387
- console.log(` Kept ${toKeep.length} recent entries in log.md`);
1388
- const saved = Buffer.byteLength(toArchive.join(''), 'utf8');
1389
- console.log(` Freed ~${saved}B from log.md`);
1390
- console.log('\n Done.\n');
1391
- }
1392
-
1393
- // ═══════════════════════════════════════════════════════════════════
1394
- // SUMMARY
1395
- // ═══════════════════════════════════════════════════════════════════
1396
-
1397
- function runSummary(dir) {
1398
- const files = [
1399
- path.join(dir, '.memoc/session-summary.md'),
1400
- path.join(dir, '.memoc/02-current-project-state.md'),
1401
- path.join(dir, '.memoc/04-handoff.md'),
1402
- ];
1403
-
1404
- function read(fp) {
1405
- try { return fs.readFileSync(fp, 'utf8'); } catch { return ''; }
1406
- }
1407
-
1408
- function section(src, heading) {
1409
- const re = new RegExp(`^## ${heading}\\n([\\s\\S]*?)(?=\\n## |$)`, 'm');
1410
- const m = src.match(re);
1411
- return m ? m[1].trim() : '';
1412
- }
1413
-
1414
- function bullets(text, max = 3) {
1415
- return text.split('\n')
1416
- .map(line => line.trim())
1417
- .filter(line => line.startsWith('- ') && !line.includes('_None yet._'))
1418
- .slice(0, max);
1419
- }
1420
-
1421
- const summary = read(files[0]);
1422
- const state = read(files[1]);
1423
- const handoff = read(files[2]);
1424
- try {
1425
- const pkg = JSON.parse(read(path.join(dir, 'package.json')));
1426
- if (pkg.name || pkg.version) {
1427
- console.log(`Project: ${pkg.name || path.basename(dir)}${pkg.version ? `@${pkg.version}` : ''}`);
1428
- }
1429
- } catch {}
1430
- const rows = [
1431
- ['Status', bullets(section(summary, 'Status')).concat(bullets(section(state, 'Current Status'))).slice(0, 3)],
1432
- ['Open Tasks', bullets(section(summary, 'Open Tasks')).concat(bullets(section(state, 'Open Tasks'))).slice(0, 3)],
1433
- ['Resume', bullets(section(summary, 'Resume')).concat(bullets(section(handoff, 'Next Steps'))).slice(0, 3)],
1434
- ['Verified', bullets(section(handoff, 'Verified'), 2)],
1435
- ];
1436
-
1437
- let printed = false;
1438
- for (const [label, items] of rows) {
1439
- if (!items.length) continue;
1440
- printed = true;
1441
- console.log(`${label}:`);
1442
- for (const item of items) console.log(item);
1443
- }
1444
- if (!printed) console.log('No summary bullets yet. Read .memoc/session-summary.md.');
1445
- }
1446
-
1447
- // ═══════════════════════════════════════════════════════════════════
1448
- // CLI ENTRY POINT
1449
- // ═══════════════════════════════════════════════════════════════════
1450
-
1451
- const cmd = process.argv[2];
1452
- const cwd = process.cwd();
1453
-
1454
- if (cmd === '--version' || cmd === '-v') {
1455
- console.log(VERSION);
1456
- process.exit(0);
1457
- }
1458
-
1459
- if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') {
1460
- console.log('Usage: memoc <command>\n');
1461
- console.log('Commands:');
1462
- console.log(' init Scaffold agent memory (auto-detects project, updates if already exists)');
1463
- console.log(' update Force-update managed sections based on current project state');
1464
- console.log(' summary Print a tiny status/resume overview');
1465
- console.log(' tokens Estimate token cost of current memory files');
1466
- console.log(' compress Archive old log.md entries to keep file small');
1467
- console.log(' add <agent> Add entry file for a specific agent (run without args to list)');
1468
- console.log(' search "<query>" Find matching files (use --snippets for line matches)');
1469
- console.log('\nSearch flags:');
1470
- console.log(' --files Show file names and match counts, sorted by relevance + recency (default)');
1471
- console.log(' --snippets Show matching lines');
1472
- console.log(' --limit N Limit output (default 12)');
1473
- console.log(' --all Show all matches');
1474
- console.log('\nFlags:');
1475
- console.log(' --version, -v Print version');
1476
- process.exit(0);
1477
- }
1478
-
1479
- if (cmd === 'init') { run(cwd, false); process.exit(0); }
1480
- if (cmd === 'update') { run(cwd, true); process.exit(0); }
1481
- if (cmd === 'summary') { runSummary(cwd); process.exit(0); }
1482
- if (cmd === 'tokens') { runTokens(cwd); process.exit(0); }
1483
- if (cmd === 'compress') { runCompress(cwd); process.exit(0); }
1484
- if (cmd === 'add') { runAdd(cwd); process.exit(0); }
1485
- if (cmd === 'search') { runSearch(cwd); process.exit(0); }
1486
-
1487
- console.error(`Unknown command: ${cmd}`);
1488
- console.error('Run "memoc --help" for usage.');
1489
- process.exit(1);
1170
+ // Append update record to log.md
1171
+ const logPath = path.join(memDir, 'log.md');
1172
+ if (fs.existsSync(logPath)) {
1173
+ fs.appendFileSync(logPath,
1174
+ `\n## [${nowISO()}] update | Re-scanned: ${p.isEmpty ? 'nothing detected' : stackStr(p.stack)}\n`,
1175
+ 'utf8'
1176
+ );
1177
+ mark('append', '.memoc/log.md');
1178
+ }
1179
+ }
1180
+
1181
+ hideOnWindows(memDir);
1182
+ console.log(log.join('\n'));
1183
+ console.log('\n Done.');
1184
+ }
1185
+
1186
+ // ═══════════════════════════════════════════════════════════════════
1187
+ // ADD — add entry file for a specific agent
1188
+ // ═══════════════════════════════════════════════════════════════════
1189
+
1190
+ function runAdd(dir) {
1191
+ const agentKey = (process.argv[3] || '').toLowerCase();
1192
+
1193
+ if (!agentKey) {
1194
+ console.log('\n Available agents:\n');
1195
+ for (const [key, agent] of Object.entries(AGENT_REGISTRY)) {
1196
+ const exists = fs.existsSync(path.join(dir, agent.file)) ? ' (already added)' : '';
1197
+ console.log(` ${key.padEnd(10)} → ${agent.file}${exists}`);
1198
+ }
1199
+ console.log('\n Usage: memoc add <agent>');
1200
+ return;
1201
+ }
1202
+
1203
+ const agent = AGENT_REGISTRY[agentKey];
1204
+ if (!agent) {
1205
+ console.error(`\n Unknown agent: "${agentKey}"`);
1206
+ console.error(` Available: ${Object.keys(AGENT_REGISTRY).join(', ')}`);
1207
+ process.exit(1);
1208
+ }
1209
+
1210
+ const filePath = path.join(dir, agent.file);
1211
+ const result = applyManagedBlock(filePath, () => tplAgentEntry(agent.label));
1212
+ console.log(`\n ${result.padEnd(8)} ${agent.file} (${agent.label})`);
1213
+ console.log('\n Done.');
1214
+ }
1215
+
1216
+ // ═══════════════════════════════════════════════════════════════════
1217
+ // SEARCH
1218
+ // ═══════════════════════════════════════════════════════════════════
1219
+
1220
+ function runSearch(dir) {
1221
+ const rawArgs = process.argv.slice(3);
1222
+ const opts = { mode: 'files', limit: 12, all: false };
1223
+ const queryParts = [];
1224
+
1225
+ for (let i = 0; i < rawArgs.length; i++) {
1226
+ const arg = rawArgs[i];
1227
+ if (arg === '--snippets') { opts.mode = 'snippets'; continue; }
1228
+ if (arg === '--files') { opts.mode = 'files'; continue; }
1229
+ if (arg === '--all') { opts.all = true; continue; }
1230
+ if (arg === '--limit') {
1231
+ const n = Number(rawArgs[++i]);
1232
+ if (Number.isFinite(n) && n > 0) opts.limit = Math.floor(n);
1233
+ continue;
1234
+ }
1235
+ if (arg.startsWith('--limit=')) {
1236
+ const n = Number(arg.slice('--limit='.length));
1237
+ if (Number.isFinite(n) && n > 0) opts.limit = Math.floor(n);
1238
+ continue;
1239
+ }
1240
+ queryParts.push(arg);
1241
+ }
1242
+
1243
+ const query = queryParts.join(' ').toLowerCase();
1244
+
1245
+ const searchRoots = [
1246
+ path.join(dir, '.memoc'),
1247
+ path.join(dir, 'skills'),
1248
+ path.join(dir, 'llms.txt'),
1249
+ path.join(dir, 'AGENTS.md'),
1250
+ path.join(dir, 'CLAUDE.md'),
1251
+ ...Object.values(AGENT_REGISTRY).map(agent => path.join(dir, agent.file)),
1252
+ ];
1253
+
1254
+ if (!query) {
1255
+ // No query list all memory files sorted by recency
1256
+ const allFiles = [];
1257
+ function collectFile(fp) {
1258
+ if (!fs.existsSync(fp)) return;
1259
+ const rel = path.relative(dir, fp);
1260
+ let mtime = 0;
1261
+ try { mtime = fs.statSync(fp).mtimeMs; } catch {}
1262
+ allFiles.push({ file: rel, mtime });
1263
+ }
1264
+ function collectDir(d) {
1265
+ if (!fs.existsSync(d)) return;
1266
+ for (const entry of fs.readdirSync(d)) {
1267
+ const fp = path.join(d, entry);
1268
+ try {
1269
+ if (fs.statSync(fp).isDirectory()) collectDir(fp);
1270
+ else if (entry.endsWith('.md') || entry === 'llms.txt' || entry.endsWith('rules')) collectFile(fp);
1271
+ } catch {}
1272
+ }
1273
+ }
1274
+ for (const root of searchRoots) {
1275
+ try {
1276
+ if (fs.statSync(root).isDirectory()) collectDir(root);
1277
+ else collectFile(root);
1278
+ } catch {}
1279
+ }
1280
+ allFiles.sort((a, b) => b.mtime - a.mtime || a.file.localeCompare(b.file));
1281
+ const limited = opts.all ? allFiles : allFiles.slice(0, opts.limit);
1282
+ console.log(limited.map(r => r.file).join('\n'));
1283
+ if (!opts.all && allFiles.length > limited.length) {
1284
+ console.log(`... ${allFiles.length - limited.length} more files. Use --all to show all.`);
1285
+ }
1286
+ return;
1287
+ }
1288
+
1289
+ const matchesByFile = new Map(); // rel -> { matches: [], mtime: number }
1290
+
1291
+ function searchFile(fp) {
1292
+ if (!fs.existsSync(fp)) return;
1293
+ const rel = path.relative(dir, fp);
1294
+ let mtime = 0;
1295
+ try { mtime = fs.statSync(fp).mtimeMs; } catch {}
1296
+ const lines = fs.readFileSync(fp, 'utf8').split('\n');
1297
+ lines.forEach((line, i) => {
1298
+ if (line.toLowerCase().includes(query)) {
1299
+ if (!matchesByFile.has(rel)) matchesByFile.set(rel, { matches: [], mtime });
1300
+ matchesByFile.get(rel).matches.push({ line: i + 1, text: line.trim() });
1301
+ }
1302
+ });
1303
+ }
1304
+
1305
+ function walkDir(d) {
1306
+ if (!fs.existsSync(d)) return;
1307
+ for (const entry of fs.readdirSync(d)) {
1308
+ const fp = path.join(d, entry);
1309
+ try {
1310
+ if (fs.statSync(fp).isDirectory()) walkDir(fp);
1311
+ else if (entry.endsWith('.md') || entry === 'llms.txt' || entry.endsWith('rules')) searchFile(fp);
1312
+ } catch {}
1313
+ }
1314
+ }
1315
+
1316
+ for (const root of searchRoots) {
1317
+ try {
1318
+ if (fs.statSync(root).isDirectory()) walkDir(root);
1319
+ else searchFile(root);
1320
+ } catch {}
1321
+ }
1322
+
1323
+ if (!matchesByFile.size) {
1324
+ console.log('No matches found.');
1325
+ } else if (opts.mode === 'files') {
1326
+ const rows = [...matchesByFile.entries()]
1327
+ .map(([file, { matches, mtime }]) => ({ file, count: matches.length, mtime }))
1328
+ .sort((a, b) => b.count - a.count || b.mtime - a.mtime || a.file.localeCompare(b.file));
1329
+ const limited = opts.all ? rows : rows.slice(0, opts.limit);
1330
+ console.log(limited.map(r => `${r.file} ${r.count} match${r.count === 1 ? '' : 'es'}`).join('\n'));
1331
+ if (!opts.all && rows.length > limited.length) {
1332
+ console.log(`... ${rows.length - limited.length} more files. Use --all to show all, or --snippets for line matches.`);
1333
+ }
1334
+ } else {
1335
+ const snippets = [];
1336
+ for (const [file, { matches }] of matchesByFile.entries()) {
1337
+ for (const m of matches) snippets.push(`${file}:${m.line} ${m.text}`);
1338
+ }
1339
+ const limited = opts.all ? snippets : snippets.slice(0, opts.limit);
1340
+ console.log(limited.join('\n'));
1341
+ if (!opts.all && snippets.length > limited.length) {
1342
+ console.log(`... ${snippets.length - limited.length} more matches. Use --all to show all, or --limit N.`);
1343
+ }
1344
+ }
1345
+ }
1346
+
1347
+ // ═══════════════════════════════════════════════════════════════════
1348
+ // TOKENS — estimate token cost of current memory state
1349
+ // ═══════════════════════════════════════════════════════════════════
1350
+
1351
+ function runTokens(dir) {
1352
+ const est = text => Math.ceil(Buffer.byteLength(text, 'utf8') / 4);
1353
+ const read = fp => { try { return fs.readFileSync(fp, 'utf8'); } catch { return ''; } };
1354
+ const memDir = path.join(dir, '.memoc');
1355
+
1356
+ const startup = [
1357
+ ['CLAUDE.md', path.join(dir, 'CLAUDE.md')],
1358
+ ['session-summary.md', path.join(memDir, 'session-summary.md')],
1359
+ ];
1360
+ const onDemand = [
1361
+ ['llms.txt', path.join(dir, 'llms.txt')],
1362
+ ['02-current-project-state.md', path.join(memDir, '02-current-project-state.md')],
1363
+ ['03-decisions.md', path.join(memDir, '03-decisions.md')],
1364
+ ['04-handoff.md', path.join(memDir, '04-handoff.md')],
1365
+ ['06-project-rules.md', path.join(memDir, '06-project-rules.md')],
1366
+ ['log.md', path.join(memDir, 'log.md')],
1367
+ ];
1368
+
1369
+ console.log('\n memoc tokens\n');
1370
+ let startupTotal = 0;
1371
+ console.log(' Startup (always loaded):');
1372
+ for (const [name, fp] of startup) {
1373
+ const content = read(fp);
1374
+ const t = est(content);
1375
+ const b = Buffer.byteLength(content, 'utf8');
1376
+ startupTotal += t;
1377
+ const warn = b > 1000 ? ' ⚠ large' : '';
1378
+ console.log(` ${name.padEnd(32)} ${String(t).padStart(5)} tokens (${b}B)${warn}`);
1379
+ }
1380
+ console.log(` ${'── startup total'.padEnd(32)} ${String(startupTotal).padStart(5)} tokens`);
1381
+
1382
+ console.log('\n On-demand (read when needed):');
1383
+ let onDemandTotal = 0;
1384
+ for (const [name, fp] of onDemand) {
1385
+ const content = read(fp);
1386
+ if (!content) continue;
1387
+ const t = est(content);
1388
+ const b = Buffer.byteLength(content, 'utf8');
1389
+ onDemandTotal += t;
1390
+ const warn = t > 500 ? ' ⚠ consider compress' : '';
1391
+ console.log(` ${name.padEnd(32)} ${String(t).padStart(5)} tokens (${b}B)${warn}`);
1392
+ }
1393
+ console.log(` ${'── on-demand total'.padEnd(32)} ${String(onDemandTotal).padStart(5)} tokens`);
1394
+ console.log(`\n If all loaded: ~${startupTotal + onDemandTotal} tokens`);
1395
+
1396
+ const summaryContent = read(path.join(memDir, 'session-summary.md'));
1397
+ const summaryBytes = Buffer.byteLength(summaryContent, 'utf8');
1398
+ if (summaryBytes > 800) {
1399
+ console.log(`\n ⚠ session-summary.md is ${summaryBytes}B — recommended <800B. Trim it manually.`);
1400
+ }
1401
+ console.log();
1402
+ }
1403
+
1404
+ // ═══════════════════════════════════════════════════════════════════
1405
+ // COMPRESS — archive old log.md entries to keep file small
1406
+ // ═══════════════════════════════════════════════════════════════════
1407
+
1408
+ function runCompress(dir) {
1409
+ const KEEP = 20;
1410
+ const logPath = path.join(dir, '.memoc', 'log.md');
1411
+ const archivePath = path.join(dir, '.memoc', 'log-archive.md');
1412
+
1413
+ if (!fs.existsSync(logPath)) {
1414
+ console.log('\n No .memoc/log.md found.\n');
1415
+ return;
1416
+ }
1417
+
1418
+ const src = fs.readFileSync(logPath, 'utf8');
1419
+ // Split on entry headers, keep header as part of each chunk
1420
+ const parts = src.split(/(?=\n## \[)/);
1421
+ const header = parts[0]; // everything before first entry
1422
+ const entries = parts.slice(1).filter(e => e.trim());
1423
+
1424
+ if (entries.length <= KEEP) {
1425
+ console.log(`\n log.md has ${entries.length} entries — nothing to compress (threshold: ${KEEP}).\n`);
1426
+ return;
1427
+ }
1428
+
1429
+ const toArchive = entries.slice(0, entries.length - KEEP);
1430
+ const toKeep = entries.slice(entries.length - KEEP);
1431
+
1432
+ // Append to archive
1433
+ const archiveExists = fs.existsSync(archivePath);
1434
+ const archiveHeader = archiveExists ? '' : '# Log Archive\n\nOlder entries moved from log.md by `memoc compress`.\n';
1435
+ fs.appendFileSync(archivePath, archiveHeader + toArchive.join('') + '\n', 'utf8');
1436
+
1437
+ // Rewrite log.md with only recent entries
1438
+ write(logPath, header.trimEnd() + '\n' + toKeep.join('') + '\n');
1439
+
1440
+ console.log(`\n memoc compress\n`);
1441
+ console.log(` Archived ${toArchive.length} entries → .memoc/log-archive.md`);
1442
+ console.log(` Kept ${toKeep.length} recent entries in log.md`);
1443
+ const saved = Buffer.byteLength(toArchive.join(''), 'utf8');
1444
+ console.log(` Freed ~${saved}B from log.md`);
1445
+ console.log('\n Done.\n');
1446
+ }
1447
+
1448
+ // ═══════════════════════════════════════════════════════════════════
1449
+ // SUMMARY
1450
+ // ═══════════════════════════════════════════════════════════════════
1451
+
1452
+ function runSummary(dir) {
1453
+ const files = [
1454
+ path.join(dir, '.memoc/session-summary.md'),
1455
+ path.join(dir, '.memoc/02-current-project-state.md'),
1456
+ path.join(dir, '.memoc/04-handoff.md'),
1457
+ ];
1458
+
1459
+ function read(fp) {
1460
+ try { return fs.readFileSync(fp, 'utf8'); } catch { return ''; }
1461
+ }
1462
+
1463
+ function section(src, heading) {
1464
+ const re = new RegExp(`^## ${heading}\\n([\\s\\S]*?)(?=\\n## |$)`, 'm');
1465
+ const m = src.match(re);
1466
+ return m ? m[1].trim() : '';
1467
+ }
1468
+
1469
+ function bullets(text, max = 3) {
1470
+ return text.split('\n')
1471
+ .map(line => line.trim())
1472
+ .filter(line => line.startsWith('- ') && !line.includes('_None yet._'))
1473
+ .slice(0, max);
1474
+ }
1475
+
1476
+ const summary = read(files[0]);
1477
+ const state = read(files[1]);
1478
+ const handoff = read(files[2]);
1479
+ try {
1480
+ const pkg = JSON.parse(read(path.join(dir, 'package.json')));
1481
+ if (pkg.name || pkg.version) {
1482
+ console.log(`Project: ${pkg.name || path.basename(dir)}${pkg.version ? `@${pkg.version}` : ''}`);
1483
+ }
1484
+ } catch {}
1485
+ const rows = [
1486
+ ['Status', bullets(section(summary, 'Status')).concat(bullets(section(state, 'Current Status'))).slice(0, 3)],
1487
+ ['Open Tasks', bullets(section(summary, 'Open Tasks')).concat(bullets(section(state, 'Open Tasks'))).slice(0, 3)],
1488
+ ['Resume', bullets(section(summary, 'Resume')).concat(bullets(section(handoff, 'Next Steps'))).slice(0, 3)],
1489
+ ['Verified', bullets(section(handoff, 'Verified'), 2)],
1490
+ ];
1491
+
1492
+ let printed = false;
1493
+ for (const [label, items] of rows) {
1494
+ if (!items.length) continue;
1495
+ printed = true;
1496
+ console.log(`${label}:`);
1497
+ for (const item of items) console.log(item);
1498
+ }
1499
+ if (!printed) console.log('No summary bullets yet. Read .memoc/session-summary.md.');
1500
+ }
1501
+
1502
+ // ═══════════════════════════════════════════════════════════════════
1503
+ // CLI ENTRY POINT
1504
+ // ═══════════════════════════════════════════════════════════════════
1505
+
1506
+ const cmd = process.argv[2];
1507
+ const cwd = process.cwd();
1508
+
1509
+ if (cmd === '--version' || cmd === '-v') {
1510
+ console.log(VERSION);
1511
+ process.exit(0);
1512
+ }
1513
+
1514
+ if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') {
1515
+ console.log('Usage: memoc <command>\n');
1516
+ console.log('Commands:');
1517
+ console.log(' init Scaffold agent memory (auto-detects project, updates if already exists)');
1518
+ console.log(' update Force-update managed sections based on current project state');
1519
+ console.log(' summary Print a tiny status/resume overview');
1520
+ console.log(' tokens Estimate token cost of current memory files');
1521
+ console.log(' compress Archive old log.md entries to keep file small');
1522
+ console.log(' add <agent> Add entry file for a specific agent (run without args to list)');
1523
+ console.log(' search "<query>" Find matching files (use --snippets for line matches)');
1524
+ console.log('\nSearch flags:');
1525
+ console.log(' --files Show file names and match counts, sorted by relevance + recency (default)');
1526
+ console.log(' --snippets Show matching lines');
1527
+ console.log(' --limit N Limit output (default 12)');
1528
+ console.log(' --all Show all matches');
1529
+ console.log('\nFlags:');
1530
+ console.log(' --version, -v Print version');
1531
+ process.exit(0);
1532
+ }
1533
+
1534
+ if (cmd === 'init') { run(cwd, false); process.exit(0); }
1535
+ if (cmd === 'update') { run(cwd, true); process.exit(0); }
1536
+ if (cmd === 'summary') { runSummary(cwd); process.exit(0); }
1537
+ if (cmd === 'tokens') { runTokens(cwd); process.exit(0); }
1538
+ if (cmd === 'compress') { runCompress(cwd); process.exit(0); }
1539
+ if (cmd === 'add') { runAdd(cwd); process.exit(0); }
1540
+ if (cmd === 'search') { runSearch(cwd); process.exit(0); }
1541
+
1542
+ console.error(`Unknown command: ${cmd}`);
1543
+ console.error('Run "memoc --help" for usage.');
1544
+ process.exit(1);