@kevin0181/memoc 1.1.2 → 1.1.3

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 +165 -165
  3. package/bin/cli.js +1953 -1711
  4. package/package.json +37 -37
package/bin/cli.js CHANGED
@@ -1,501 +1,520 @@
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').execFileSync('attrib', ['+h', dirPath], { stdio: 'ignore' }); } catch {}
192
192
  }
193
193
  }
194
-
195
- function chmodExecutable(filePath) {
196
- try { fs.chmodSync(filePath, 0o755); } catch {}
197
- }
198
-
199
- function ensure(filePath, content) {
200
- if (fs.existsSync(filePath)) return false;
201
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
202
- fs.writeFileSync(filePath, content, 'utf8');
203
- return true;
204
- }
205
-
206
- function write(filePath, content) {
207
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
208
- fs.writeFileSync(filePath, content, 'utf8');
209
- }
210
-
211
- function tplMemocCmdWrapper(cliPath = runtimeCliPath()) {
212
- return `@echo off\r\nnode "${escapeCmdPath(cliPath)}" %*\r\n`;
213
- }
214
-
215
- function tplMemocPs1Wrapper(cliPath = runtimeCliPath()) {
216
- return `& node ${psSingleQuote(cliPath)} @args\nexit $LASTEXITCODE\n`;
217
- }
218
-
219
- function tplMemocShWrapper(cliPath = runtimeCliPath()) {
220
- return `#!/bin/sh\nexec node ${shellSingleQuote(cliPath)} "$@"\n`;
221
- }
222
-
223
- function defaultUserBinDir() {
224
- if (process.env.MEMOC_USER_BIN_DIR) return process.env.MEMOC_USER_BIN_DIR;
225
- if (currentPlatform() === 'win32') {
226
- return path.join(process.env.LOCALAPPDATA || path.join(process.env.USERPROFILE || process.cwd(), 'AppData', 'Local'), 'memoc', 'bin');
227
- }
228
- return path.join(process.env.HOME || process.cwd(), '.local', 'bin');
229
- }
230
-
231
- function defaultRuntimeDir() {
232
- if (process.env.MEMOC_RUNTIME_DIR) return process.env.MEMOC_RUNTIME_DIR;
233
- if (currentPlatform() === 'win32') {
234
- return path.join(process.env.LOCALAPPDATA || path.join(process.env.USERPROFILE || process.cwd(), 'AppData', 'Local'), 'memoc', 'runtime');
235
- }
236
- return path.join(process.env.HOME || process.cwd(), '.local', 'share', 'memoc', 'runtime');
237
- }
238
-
239
- function runtimeCliPath() {
240
- return path.join(defaultRuntimeDir(), 'bin', 'cli.js');
241
- }
242
-
243
- function tplEnvPs1() {
244
- 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`;
245
- }
246
-
194
+
195
+ function chmodExecutable(filePath) {
196
+ try { fs.chmodSync(filePath, 0o755); } catch {}
197
+ }
198
+
199
+ function ensure(filePath, content) {
200
+ if (fs.existsSync(filePath)) return false;
201
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
202
+ fs.writeFileSync(filePath, content, 'utf8');
203
+ return true;
204
+ }
205
+
206
+ function write(filePath, content) {
207
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
208
+ fs.writeFileSync(filePath, content, 'utf8');
209
+ }
210
+
211
+ function tplMemocCmdWrapper(cliPath = runtimeCliPath()) {
212
+ return `@echo off\r\nnode "${escapeCmdPath(cliPath)}" %*\r\n`;
213
+ }
214
+
215
+ function tplMemocPs1Wrapper(cliPath = runtimeCliPath()) {
216
+ return `& node ${psSingleQuote(cliPath)} @args\nexit $LASTEXITCODE\n`;
217
+ }
218
+
219
+ function tplMemocShWrapper(cliPath = runtimeCliPath()) {
220
+ return `#!/bin/sh\nexec node ${shellSingleQuote(cliPath)} "$@"\n`;
221
+ }
222
+
223
+ function defaultUserBinDir() {
224
+ if (process.env.MEMOC_USER_BIN_DIR) return process.env.MEMOC_USER_BIN_DIR;
225
+ if (currentPlatform() === 'win32') {
226
+ return path.join(process.env.LOCALAPPDATA || path.join(process.env.USERPROFILE || process.cwd(), 'AppData', 'Local'), 'memoc', 'bin');
227
+ }
228
+ return path.join(process.env.HOME || process.cwd(), '.local', 'bin');
229
+ }
230
+
231
+ function defaultRuntimeDir() {
232
+ if (process.env.MEMOC_RUNTIME_DIR) return process.env.MEMOC_RUNTIME_DIR;
233
+ if (currentPlatform() === 'win32') {
234
+ return path.join(process.env.LOCALAPPDATA || path.join(process.env.USERPROFILE || process.cwd(), 'AppData', 'Local'), 'memoc', 'runtime');
235
+ }
236
+ return path.join(process.env.HOME || process.cwd(), '.local', 'share', 'memoc', 'runtime');
237
+ }
238
+
239
+ function runtimeCliPath() {
240
+ return path.join(defaultRuntimeDir(), 'bin', 'cli.js');
241
+ }
242
+
243
+ function tplEnvPs1() {
244
+ 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`;
245
+ }
246
+
247
247
  function tplEnvSh() {
248
248
  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`;
249
249
  }
250
-
251
- function ensurePathHelpers(dir, mark) {
252
- const cliPath = ensureRuntimeInstall(mark);
253
- const files = [
254
- [path.join(dir, '.memoc', 'bin', 'memoc.cmd'), () => tplMemocCmdWrapper(cliPath), false],
255
- [path.join(dir, '.memoc', 'bin', 'memoc.ps1'), () => tplMemocPs1Wrapper(cliPath), false],
256
- [path.join(dir, '.memoc', 'bin', 'memoc'), () => tplMemocShWrapper(cliPath), true],
257
- [path.join(dir, '.memoc', 'env.ps1'), tplEnvPs1, false],
258
- [path.join(dir, '.memoc', 'env.sh'), tplEnvSh, true],
259
- ];
260
-
261
- for (const [fp, tpl, executable] of files) {
262
- const rel = path.relative(dir, fp);
263
- const added = writeIfChanged(fp, tpl());
264
- if (executable) chmodExecutable(fp);
265
- mark(added, rel);
266
- }
267
- }
268
-
269
- function ensureUserLauncher(mark) {
270
- const userBin = defaultUserBinDir();
271
- writeLaunchers(userBin, mark, 'user bin', ensureRuntimeInstall(mark));
272
- return userBin;
273
- }
274
-
275
- function writeLaunchers(binDir, mark, label, cliPath = ensureRuntimeInstall(mark)) {
276
- const files = [
277
- [path.join(binDir, 'memoc.cmd'), () => tplMemocCmdWrapper(cliPath), false],
278
- [path.join(binDir, 'memoc.ps1'), () => tplMemocPs1Wrapper(cliPath), false],
279
- [path.join(binDir, 'memoc'), () => tplMemocShWrapper(cliPath), true],
280
- ];
281
-
282
- for (const [fp, tpl, executable] of files) {
283
- const added = writeIfChanged(fp, tpl());
284
- if (executable) chmodExecutable(fp);
285
- mark(added, `${label} ${path.basename(fp)}`);
286
- }
287
- }
288
-
289
- function writeIfChanged(filePath, content) {
290
- if (!fs.existsSync(filePath)) {
291
- write(filePath, content);
292
- return 'add';
293
- }
294
- try {
295
- if (fs.readFileSync(filePath, 'utf8') === content) return 'skip';
296
- } catch {}
297
- write(filePath, content);
298
- return 'update';
299
- }
300
-
301
- function ensurePathRegistration(dir, mark) {
302
- ensureCurrentPathLauncher(mark);
303
- const binDir = ensureUserLauncher(mark);
304
- const pathSep = path.delimiter;
305
-
306
- if ((process.env.PATH || '').split(pathSep).some(p => samePath(p, binDir))) {
307
- mark('skip', 'PATH (user memoc bin already active)');
308
- return;
309
- }
310
-
311
- process.env.PATH = `${binDir}${pathSep}${process.env.PATH || ''}`;
312
-
313
- if (process.env.MEMOC_SKIP_PATH_REGISTER === '1') {
314
- mark('skip', 'PATH registration (test mode)');
315
- return;
316
- }
317
-
318
- if (currentPlatform() !== 'win32') {
319
- const updated = ensureUnixPathRegistration(binDir);
320
- mark(updated ? 'update' : 'skip', `${currentPlatform()} PATH (${userPathShellHint(binDir)})`);
321
- return;
322
- }
323
-
324
- try {
325
- const current = require('child_process')
326
- .execFileSync('powershell.exe', [
327
- '-NoProfile',
328
- '-ExecutionPolicy', 'Bypass',
329
- '-Command',
330
- "[Environment]::GetEnvironmentVariable('Path','User')",
331
- ], { encoding: 'utf8' })
332
- .trim();
333
- const parts = current.split(pathSep).filter(Boolean);
334
- if (parts.some(p => samePath(p, binDir))) {
335
- mark('skip', 'User PATH (memoc bin already registered)');
336
- return;
337
- }
338
- const nextPath = [binDir, ...parts].join(pathSep);
339
- require('child_process').execFileSync('powershell.exe', [
340
- '-NoProfile',
341
- '-ExecutionPolicy', 'Bypass',
342
- '-Command',
343
- `[Environment]::SetEnvironmentVariable('Path', ${JSON.stringify(nextPath)}, 'User')`,
344
- ], { stdio: 'ignore' });
345
- mark('update', 'User PATH (memoc bin added; open a new terminal if needed)');
346
- } catch {
347
- mark('skip', 'User PATH registration failed (use . .\\.memoc\\env.ps1)');
348
- }
349
- }
350
-
351
- function ensureCurrentPathLauncher(mark) {
352
- const target = findWritablePathDir();
353
- if (!target) {
354
- mark('skip', 'active PATH launcher (no writable PATH directory found)');
355
- return false;
356
- }
357
- writeLaunchers(target, mark, 'active PATH', ensureRuntimeInstall(mark));
358
- return true;
359
- }
360
-
361
- function ensureRuntimeInstall(mark) {
362
- const runtimeDir = defaultRuntimeDir();
363
- const sourceRoot = path.join(__dirname, '..');
364
- const files = [
365
- [path.join(sourceRoot, 'bin', 'cli.js'), path.join(runtimeDir, 'bin', 'cli.js')],
366
- [path.join(sourceRoot, 'package.json'), path.join(runtimeDir, 'package.json')],
367
- ];
368
-
369
- for (const [src, dest] of files) {
370
- try {
371
- const content = fs.readFileSync(src, 'utf8');
372
- const changed = writeIfChanged(dest, content);
373
- mark(changed, `runtime ${path.relative(runtimeDir, dest)}`);
374
- } catch {
375
- mark('skip', `runtime ${path.basename(dest)} unavailable`);
376
- }
377
- }
378
-
379
- chmodExecutable(path.join(runtimeDir, 'bin', 'cli.js'));
380
- return path.join(runtimeDir, 'bin', 'cli.js');
381
- }
382
-
383
- function findWritablePathDir() {
384
- const dirs = [...new Set((process.env.PATH || '').split(path.delimiter).filter(Boolean))];
385
- const npmBin = npmGlobalBinDir();
386
- const ranked = dirs
387
- .filter(d => !isVolatilePathDir(d))
388
- .filter(d => {
389
- try { return fs.existsSync(d) && fs.statSync(d).isDirectory() && canWriteDir(d); }
390
- catch { return false; }
391
- })
392
- .sort((a, b) => pathRank(a, npmBin) - pathRank(b, npmBin));
393
- return ranked[0] || null;
394
- }
395
-
396
- function pathRank(dir, npmBin) {
397
- if (npmBin && samePath(dir, npmBin)) return 0;
398
- const lower = dir.toLowerCase();
399
- for (const root of userWritableRoots()) {
400
- if (root && lower.startsWith(root.toLowerCase())) return 1;
401
- }
402
- return 5;
403
- }
404
-
405
- function userWritableRoots() {
406
- return [
407
- process.env.APPDATA,
408
- process.env.LOCALAPPDATA,
409
- process.env.HOME,
410
- process.env.USERPROFILE,
411
- ].filter(Boolean).map(p => path.resolve(p));
412
- }
413
-
414
- function npmGlobalBinDir() {
415
- try {
416
- const prefix = require('child_process').execFileSync('npm', ['config', 'get', 'prefix'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
417
- if (!prefix) return null;
418
- return currentPlatform() === 'win32' ? prefix : path.join(prefix, 'bin');
419
- } catch {
420
- return null;
421
- }
422
- }
423
-
424
- function isVolatilePathDir(dir) {
425
- const lower = dir.toLowerCase();
426
- return lower.includes(`${path.sep}_npx${path.sep}`) ||
427
- lower.includes(`${path.sep}node_modules${path.sep}.bin`) ||
428
- lower.includes(`${path.sep}npm-cache${path.sep}_npx${path.sep}`);
429
- }
430
-
431
- function canWriteDir(dir) {
432
- const probe = path.join(dir, `.memoc-write-test-${process.pid}-${Date.now()}`);
433
- try {
434
- fs.writeFileSync(probe, '');
435
- fs.unlinkSync(probe);
436
- return true;
437
- } catch {
438
- return false;
439
- }
440
- }
441
-
442
- function ensureUnixPathRegistration(binDir) {
443
- if (process.env.MEMOC_SKIP_PATH_REGISTER === '1') return false;
444
-
445
- const home = process.env.HOME;
446
- if (!home) return false;
447
-
448
- const block = [
449
- '# memoc PATH',
450
- `MEMOC_BIN=${shellSingleQuote(binDir)}`,
451
- 'case ":$PATH:" in *":$MEMOC_BIN:"*) ;; *) PATH="$MEMOC_BIN:$PATH"; export PATH ;; esac',
452
- '# end memoc PATH',
453
- ].join('\n');
454
-
455
- const candidates = [
456
- path.join(home, '.profile'),
457
- path.join(home, '.zshrc'),
458
- path.join(home, '.bashrc'),
459
- ];
460
-
461
- let changed = false;
462
- for (const fp of candidates) {
463
- try {
464
- const src = fs.existsSync(fp) ? fs.readFileSync(fp, 'utf8') : '';
465
- if (src.includes(binDir) || src.includes('# memoc PATH')) continue;
466
- fs.appendFileSync(fp, `${src.endsWith('\n') || !src ? '' : '\n'}\n${block}\n`, 'utf8');
467
- changed = true;
468
- } catch {}
469
- }
470
- return changed;
471
- }
472
-
473
- function userPathShellHint(binDir) {
474
- return `user bin ${binDir} ${process.env.MEMOC_SKIP_PATH_REGISTER === '1' ? 'test mode' : 'registered; open a new terminal if needed'}`;
475
- }
476
-
477
- function currentPlatform() {
478
- return process.env.MEMOC_PLATFORM || process.platform;
479
- }
480
-
481
- function shellSingleQuote(value) {
482
- return `'${String(value).replace(/'/g, `'\\''`)}'`;
483
- }
484
-
485
- function psSingleQuote(value) {
486
- return `'${String(value).replace(/'/g, "''")}'`;
487
- }
488
-
489
- function escapeCmdPath(value) {
490
- return String(value).replace(/"/g, '""');
491
- }
492
-
493
- function samePath(a, b) {
494
- if (!a || !b) return false;
495
- const norm = p => path.resolve(p).toLowerCase().replace(/[\\/]+$/, '');
496
- try { return norm(a) === norm(b); } catch { return false; }
497
- }
498
-
250
+
251
+ function ensurePathHelpers(dir, mark) {
252
+ const cliPath = ensureRuntimeInstall(mark);
253
+ const files = [
254
+ [path.join(dir, '.memoc', 'bin', 'memoc.cmd'), () => tplMemocCmdWrapper(cliPath), false],
255
+ [path.join(dir, '.memoc', 'bin', 'memoc.ps1'), () => tplMemocPs1Wrapper(cliPath), false],
256
+ [path.join(dir, '.memoc', 'bin', 'memoc'), () => tplMemocShWrapper(cliPath), true],
257
+ [path.join(dir, '.memoc', 'env.ps1'), tplEnvPs1, false],
258
+ [path.join(dir, '.memoc', 'env.sh'), tplEnvSh, true],
259
+ ];
260
+
261
+ for (const [fp, tpl, executable] of files) {
262
+ const rel = path.relative(dir, fp);
263
+ const added = writeIfChanged(fp, tpl());
264
+ if (executable) chmodExecutable(fp);
265
+ mark(added, rel);
266
+ }
267
+ }
268
+
269
+ function ensureUserLauncher(mark) {
270
+ const userBin = defaultUserBinDir();
271
+ writeLaunchers(userBin, mark, 'user bin', ensureRuntimeInstall(mark));
272
+ return userBin;
273
+ }
274
+
275
+ function writeLaunchers(binDir, mark, label, cliPath = ensureRuntimeInstall(mark)) {
276
+ const files = [
277
+ [path.join(binDir, 'memoc.cmd'), () => tplMemocCmdWrapper(cliPath), false],
278
+ [path.join(binDir, 'memoc.ps1'), () => tplMemocPs1Wrapper(cliPath), false],
279
+ [path.join(binDir, 'memoc'), () => tplMemocShWrapper(cliPath), true],
280
+ ];
281
+
282
+ for (const [fp, tpl, executable] of files) {
283
+ const added = writeIfChanged(fp, tpl());
284
+ if (executable) chmodExecutable(fp);
285
+ mark(added, `${label} ${path.basename(fp)}`);
286
+ }
287
+ }
288
+
289
+ function writeIfChanged(filePath, content) {
290
+ if (!fs.existsSync(filePath)) {
291
+ write(filePath, content);
292
+ return 'add';
293
+ }
294
+ try {
295
+ if (fs.readFileSync(filePath, 'utf8') === content) return 'skip';
296
+ } catch {}
297
+ write(filePath, content);
298
+ return 'update';
299
+ }
300
+
301
+ function writeIfDefaultish(filePath, content, isDefaultish) {
302
+ if (!fs.existsSync(filePath)) {
303
+ write(filePath, content);
304
+ return 'add';
305
+ }
306
+ let src = '';
307
+ try { src = fs.readFileSync(filePath, 'utf8'); } catch { return 'skip'; }
308
+ if (!isDefaultish(src)) return 'skip';
309
+ if (src === content) return 'skip';
310
+ write(filePath, content);
311
+ return 'update';
312
+ }
313
+
314
+ function hasOnlyScaffold(src, required) {
315
+ if (!required.every(part => src.includes(part))) return false;
316
+ const nonEmpty = src.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
317
+ return nonEmpty.length <= 16;
318
+ }
319
+
320
+ function ensurePathRegistration(dir, mark) {
321
+ ensureCurrentPathLauncher(mark);
322
+ const binDir = ensureUserLauncher(mark);
323
+ const pathSep = path.delimiter;
324
+
325
+ if ((process.env.PATH || '').split(pathSep).some(p => samePath(p, binDir))) {
326
+ mark('skip', 'PATH (user memoc bin already active)');
327
+ return;
328
+ }
329
+
330
+ process.env.PATH = `${binDir}${pathSep}${process.env.PATH || ''}`;
331
+
332
+ if (process.env.MEMOC_SKIP_PATH_REGISTER === '1') {
333
+ mark('skip', 'PATH registration (test mode)');
334
+ return;
335
+ }
336
+
337
+ if (currentPlatform() !== 'win32') {
338
+ const updated = ensureUnixPathRegistration(binDir);
339
+ mark(updated ? 'update' : 'skip', `${currentPlatform()} PATH (${userPathShellHint(binDir)})`);
340
+ return;
341
+ }
342
+
343
+ try {
344
+ const current = require('child_process')
345
+ .execFileSync('powershell.exe', [
346
+ '-NoProfile',
347
+ '-ExecutionPolicy', 'Bypass',
348
+ '-Command',
349
+ "[Environment]::GetEnvironmentVariable('Path','User')",
350
+ ], { encoding: 'utf8' })
351
+ .trim();
352
+ const parts = current.split(pathSep).filter(Boolean);
353
+ if (parts.some(p => samePath(p, binDir))) {
354
+ mark('skip', 'User PATH (memoc bin already registered)');
355
+ return;
356
+ }
357
+ const nextPath = [binDir, ...parts].join(pathSep);
358
+ require('child_process').execFileSync('powershell.exe', [
359
+ '-NoProfile',
360
+ '-ExecutionPolicy', 'Bypass',
361
+ '-Command',
362
+ `[Environment]::SetEnvironmentVariable('Path', ${JSON.stringify(nextPath)}, 'User')`,
363
+ ], { stdio: 'ignore' });
364
+ mark('update', 'User PATH (memoc bin added; open a new terminal if needed)');
365
+ } catch {
366
+ mark('skip', 'User PATH registration failed (use . .\\.memoc\\env.ps1)');
367
+ }
368
+ }
369
+
370
+ function ensureCurrentPathLauncher(mark) {
371
+ const target = findWritablePathDir();
372
+ if (!target) {
373
+ mark('skip', 'active PATH launcher (no writable PATH directory found)');
374
+ return false;
375
+ }
376
+ writeLaunchers(target, mark, 'active PATH', ensureRuntimeInstall(mark));
377
+ return true;
378
+ }
379
+
380
+ function ensureRuntimeInstall(mark) {
381
+ const runtimeDir = defaultRuntimeDir();
382
+ const sourceRoot = path.join(__dirname, '..');
383
+ const files = [
384
+ [path.join(sourceRoot, 'bin', 'cli.js'), path.join(runtimeDir, 'bin', 'cli.js')],
385
+ [path.join(sourceRoot, 'package.json'), path.join(runtimeDir, 'package.json')],
386
+ ];
387
+
388
+ for (const [src, dest] of files) {
389
+ try {
390
+ const content = fs.readFileSync(src, 'utf8');
391
+ const changed = writeIfChanged(dest, content);
392
+ mark(changed, `runtime ${path.relative(runtimeDir, dest)}`);
393
+ } catch {
394
+ mark('skip', `runtime ${path.basename(dest)} unavailable`);
395
+ }
396
+ }
397
+
398
+ chmodExecutable(path.join(runtimeDir, 'bin', 'cli.js'));
399
+ return path.join(runtimeDir, 'bin', 'cli.js');
400
+ }
401
+
402
+ function findWritablePathDir() {
403
+ const dirs = [...new Set((process.env.PATH || '').split(path.delimiter).filter(Boolean))];
404
+ const npmBin = npmGlobalBinDir();
405
+ const ranked = dirs
406
+ .filter(d => !isVolatilePathDir(d))
407
+ .filter(d => {
408
+ try { return fs.existsSync(d) && fs.statSync(d).isDirectory() && canWriteDir(d); }
409
+ catch { return false; }
410
+ })
411
+ .sort((a, b) => pathRank(a, npmBin) - pathRank(b, npmBin));
412
+ return ranked[0] || null;
413
+ }
414
+
415
+ function pathRank(dir, npmBin) {
416
+ if (npmBin && samePath(dir, npmBin)) return 0;
417
+ const lower = dir.toLowerCase();
418
+ for (const root of userWritableRoots()) {
419
+ if (root && lower.startsWith(root.toLowerCase())) return 1;
420
+ }
421
+ return 5;
422
+ }
423
+
424
+ function userWritableRoots() {
425
+ return [
426
+ process.env.APPDATA,
427
+ process.env.LOCALAPPDATA,
428
+ process.env.HOME,
429
+ process.env.USERPROFILE,
430
+ ].filter(Boolean).map(p => path.resolve(p));
431
+ }
432
+
433
+ function npmGlobalBinDir() {
434
+ try {
435
+ const prefix = require('child_process').execFileSync('npm', ['config', 'get', 'prefix'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
436
+ if (!prefix) return null;
437
+ return currentPlatform() === 'win32' ? prefix : path.join(prefix, 'bin');
438
+ } catch {
439
+ return null;
440
+ }
441
+ }
442
+
443
+ function isVolatilePathDir(dir) {
444
+ const lower = dir.toLowerCase();
445
+ return lower.includes(`${path.sep}_npx${path.sep}`) ||
446
+ lower.includes(`${path.sep}node_modules${path.sep}.bin`) ||
447
+ lower.includes(`${path.sep}npm-cache${path.sep}_npx${path.sep}`);
448
+ }
449
+
450
+ function canWriteDir(dir) {
451
+ const probe = path.join(dir, `.memoc-write-test-${process.pid}-${Date.now()}`);
452
+ try {
453
+ fs.writeFileSync(probe, '');
454
+ fs.unlinkSync(probe);
455
+ return true;
456
+ } catch {
457
+ return false;
458
+ }
459
+ }
460
+
461
+ function ensureUnixPathRegistration(binDir) {
462
+ if (process.env.MEMOC_SKIP_PATH_REGISTER === '1') return false;
463
+
464
+ const home = process.env.HOME;
465
+ if (!home) return false;
466
+
467
+ const block = [
468
+ '# memoc PATH',
469
+ `MEMOC_BIN=${shellSingleQuote(binDir)}`,
470
+ 'case ":$PATH:" in *":$MEMOC_BIN:"*) ;; *) PATH="$MEMOC_BIN:$PATH"; export PATH ;; esac',
471
+ '# end memoc PATH',
472
+ ].join('\n');
473
+
474
+ const candidates = [
475
+ path.join(home, '.profile'),
476
+ path.join(home, '.zshrc'),
477
+ path.join(home, '.bashrc'),
478
+ ];
479
+
480
+ let changed = false;
481
+ for (const fp of candidates) {
482
+ try {
483
+ const src = fs.existsSync(fp) ? fs.readFileSync(fp, 'utf8') : '';
484
+ if (src.includes(binDir) || src.includes('# memoc PATH')) continue;
485
+ fs.appendFileSync(fp, `${src.endsWith('\n') || !src ? '' : '\n'}\n${block}\n`, 'utf8');
486
+ changed = true;
487
+ } catch {}
488
+ }
489
+ return changed;
490
+ }
491
+
492
+ function userPathShellHint(binDir) {
493
+ return `user bin ${binDir} ${process.env.MEMOC_SKIP_PATH_REGISTER === '1' ? 'test mode' : 'registered; open a new terminal if needed'}`;
494
+ }
495
+
496
+ function currentPlatform() {
497
+ return process.env.MEMOC_PLATFORM || process.platform;
498
+ }
499
+
500
+ function shellSingleQuote(value) {
501
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
502
+ }
503
+
504
+ function psSingleQuote(value) {
505
+ return `'${String(value).replace(/'/g, "''")}'`;
506
+ }
507
+
508
+ function escapeCmdPath(value) {
509
+ return String(value).replace(/"/g, '""');
510
+ }
511
+
512
+ function samePath(a, b) {
513
+ if (!a || !b) return false;
514
+ const norm = p => path.resolve(p).toLowerCase().replace(/[\\/]+$/, '');
515
+ try { return norm(a) === norm(b); } catch { return false; }
516
+ }
517
+
499
518
  function updateSection(filePath, startMark, endMark, inner) {
500
519
  if (!fs.existsSync(filePath)) return false;
501
520
  const src = fs.readFileSync(filePath, 'utf8');
@@ -506,11 +525,11 @@ function updateSection(filePath, startMark, endMark, inner) {
506
525
  );
507
526
  return true;
508
527
  }
509
-
510
- // ═══════════════════════════════════════════════════════════════════
511
- // SECTION MARKERS
512
- // ═══════════════════════════════════════════════════════════════════
513
-
528
+
529
+ // ═══════════════════════════════════════════════════════════════════
530
+ // SECTION MARKERS
531
+ // ═══════════════════════════════════════════════════════════════════
532
+
514
533
  const mk = n => [`<!-- memoc:${n}:start -->`, `<!-- memoc:${n}:end -->`];
515
534
  const [MGMT_S, MGMT_E] = mk('managed');
516
535
  const [ID_S, ID_E] = mk('identity');
@@ -536,44 +555,44 @@ function findMarkedRange(src, startMark, endMark) {
536
555
  }
537
556
  return null;
538
557
  }
539
-
540
- // ═══════════════════════════════════════════════════════════════════
541
- // AGENT REGISTRY — third-party agent entry files (added via `add`)
542
- // ═══════════════════════════════════════════════════════════════════
543
-
544
- const AGENT_REGISTRY = {
545
- cursor: { file: '.cursorrules', label: 'Cursor' },
546
- windsurf: { file: '.windsurfrules', label: 'Windsurf' },
547
- copilot: { file: '.github/copilot-instructions.md', label: 'GitHub Copilot' },
548
- gemini: { file: 'GEMINI.md', label: 'Gemini CLI' },
549
- };
550
-
551
- // ═══════════════════════════════════════════════════════════════════
552
- // DYNAMIC CONTENT (re-generated on update)
553
- // ═══════════════════════════════════════════════════════════════════
554
-
558
+
559
+ // ═══════════════════════════════════════════════════════════════════
560
+ // AGENT REGISTRY — third-party agent entry files (added via `add`)
561
+ // ═══════════════════════════════════════════════════════════════════
562
+
563
+ const AGENT_REGISTRY = {
564
+ cursor: { file: '.cursorrules', label: 'Cursor' },
565
+ windsurf: { file: '.windsurfrules', label: 'Windsurf' },
566
+ copilot: { file: '.github/copilot-instructions.md', label: 'GitHub Copilot' },
567
+ gemini: { file: 'GEMINI.md', label: 'Gemini CLI' },
568
+ };
569
+
570
+ // ═══════════════════════════════════════════════════════════════════
571
+ // DYNAMIC CONTENT (re-generated on update)
572
+ // ═══════════════════════════════════════════════════════════════════
573
+
555
574
  function legacyManagedBlock() {
556
- return `${MGMT_S}
557
- ## Session Start
558
- - [ ] Read \`.memoc/session-summary.md\`
559
- - [ ] \`.pending\` exists? → review changed files → update memory if needed → delete it
560
- - [ ] If \`memoc\` is not found in an existing shell, open a new terminal or load the local helper: PowerShell \`. .\\.memoc\\env.ps1\`; sh \`. ./.memoc/env.sh\`
561
-
562
- ## Before Opening More Files
563
- - [ ] 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>"\`
564
- - [ ] Open on demand: \`02\` status · \`04\` resume · \`06\` rules · \`llms.txt\` map
565
- - [ ] If memory search is not enough, search project files with \`memoc grep "<query>" --limit 5\`
566
- - [ ] Keep output small: \`summary\`, \`search --limit\`, \`grep --limit\`, \`--snippets\`
567
-
568
- ## Before Finishing _(update only applicable files; skip Q&A / throwaway exploration)_
569
- - [ ] Code/config/deps changed → \`02\` (version, commands list, Last synced) + \`session-summary.md\` (status, changed, open tasks)
570
- - [ ] Decision made → \`03-decisions.md\` (what & why) + \`02\`
571
- - [ ] Work incomplete or risky → \`04-handoff.md\` (verified commands, unverified items, next steps)
572
- - [ ] Rule/preference set → \`06-project-rules.md\`
573
- - [ ] Wiki/systems work → read \`skills/project-memory-maintainer/SKILL.md\`
574
- ${MGMT_E}`;
575
- }
576
-
575
+ return `${MGMT_S}
576
+ ## Session Start
577
+ - [ ] Read \`.memoc/session-summary.md\`
578
+ - [ ] \`.pending\` exists? → review changed files → update memory if needed → delete it
579
+ - [ ] If \`memoc\` is not found in an existing shell, open a new terminal or load the local helper: PowerShell \`. .\\.memoc\\env.ps1\`; sh \`. ./.memoc/env.sh\`
580
+
581
+ ## Before Opening More Files
582
+ - [ ] 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>"\`
583
+ - [ ] Open on demand: \`02\` status · \`04\` resume · \`06\` rules · \`llms.txt\` map
584
+ - [ ] If memory search is not enough, search project files with \`memoc grep "<query>" --limit 5\`
585
+ - [ ] Keep output small: \`summary\`, \`search --limit\`, \`grep --limit\`, \`--snippets\`
586
+
587
+ ## Before Finishing _(update only applicable files; skip Q&A / throwaway exploration)_
588
+ - [ ] Code/config/deps changed → \`02\` (version, commands list, Last synced) + \`session-summary.md\` (status, changed, open tasks)
589
+ - [ ] Decision made → \`03-decisions.md\` (what & why) + \`02\`
590
+ - [ ] Work incomplete or risky → \`04-handoff.md\` (verified commands, unverified items, next steps)
591
+ - [ ] Rule/preference set → \`06-project-rules.md\`
592
+ - [ ] Wiki/systems work → read \`skills/project-memory-maintainer/SKILL.md\`
593
+ ${MGMT_E}`;
594
+ }
595
+
577
596
  function managedBlock() {
578
597
  return `${MGMT_S}
579
598
  ## Session Start
@@ -597,502 +616,559 @@ ${MGMT_E}`;
597
616
  }
598
617
 
599
618
  function identityInner(p) {
600
- return [
601
- `- Project name: \`${p.name}\``,
602
- `- Detected stack: ${stackStr(p.stack)}`,
603
- ].join('\n');
604
- }
605
-
606
- function snapshotInner(p) {
607
- const lines = [
608
- `- Last synced: ${nowISO()}`,
609
- `- Detected stack: ${stackStr(p.stack)}`,
610
- ];
611
- if (p.configFiles.length)
612
- lines.push(`\n### Config Files\n\n${listMd(p.configFiles)}`);
613
- if (p.srcDirs.length)
614
- lines.push(`\n### Source Directories\n\n${listMd(p.srcDirs)}`);
615
- const sc = scriptsMd(p.scripts);
616
- if (sc !== '_None detected._')
617
- lines.push(`\n### Package Scripts\n\n${sc}`);
618
- return lines.join('\n');
619
- }
620
-
621
- function coreLlmsInner() {
622
- return `- [Session Summary](.memoc/session-summary.md): only required startup read.
623
- - [Current State](.memoc/02-current-project-state.md): status, tasks, commands.
624
- - [Handoff](.memoc/04-handoff.md): resume context, blockers, verification.
625
- - [Rules](.memoc/06-project-rules.md): durable preferences.
626
- - [Agent Index](.memoc/00-agent-index.md): compact file map.
627
- - [Project Brief](.memoc/00-project-brief.md): short identity and direction.
628
- - [Workflow](.memoc/01-agent-workflow.md): update trigger matrix.
629
- - [Decisions](.memoc/03-decisions.md): durable decisions.
630
- - [Log](.memoc/log.md): append-only history.
631
- - [Systems](.memoc/systems/README.md): subsystem docs.
632
- - [Wiki](.memoc/wiki/index.md): synthesized knowledge.`;
633
- }
634
-
635
- function headerInner(p) {
636
- return `# ${p.name}\n\n> LLM-facing project map for this project.`;
637
- }
638
-
639
- function systemsLlmsInner(dir) {
640
- const systemsDir = path.join(dir, '.memoc', 'systems');
641
- if (!fs.existsSync(systemsDir)) return '_None yet._';
642
- const files = fs.readdirSync(systemsDir)
643
- .filter(f => f.endsWith('.md') && f !== 'README.md')
644
- .sort();
645
- if (!files.length) return '_None yet._';
646
- return files.map(f => `- [${f.replace('.md', '')}](.memoc/systems/${f}): subsystem context.`).join('\n');
647
- }
648
-
649
- function wikiLlmsInner(dir) {
650
- const wikiDir = path.join(dir, '.memoc', 'wiki');
651
- if (!fs.existsSync(wikiDir)) return '_None yet._';
652
- const lines = [];
653
- const SKIP = new Set(['index.md']);
654
- try {
655
- for (const f of fs.readdirSync(wikiDir).sort()) {
656
- if (!f.endsWith('.md') || SKIP.has(f)) continue;
657
- try { if (fs.statSync(path.join(wikiDir, f)).isDirectory()) continue; } catch { continue; }
658
- lines.push(`- [${f.replace('.md', '')}](.memoc/wiki/${f}): wiki page.`);
659
- }
660
- for (const sub of ['sources', 'topics', 'global']) {
661
- const subDir = path.join(wikiDir, sub);
662
- if (!fs.existsSync(subDir)) continue;
663
- for (const f of fs.readdirSync(subDir).sort()) {
664
- if (!f.endsWith('.md')) continue;
665
- lines.push(`- [${f.replace('.md', '')}](.memoc/wiki/${sub}/${f}): wiki page.`);
666
- }
667
- }
668
- } catch {}
669
- return lines.length ? lines.join('\n') : '_None yet._';
670
- }
671
-
672
- // ═══════════════════════════════════════════════════════════════════
673
- // TEMPLATES — entry files
674
- // ═══════════════════════════════════════════════════════════════════
675
-
676
- function tplClaude() {
677
- return `# CLAUDE.md
678
-
679
- This is the Claude Code entry file for the project.
680
-
681
- ${managedBlock()}
682
- `;
683
- }
684
-
685
- function tplAgents() {
686
- return `# AGENTS.md
687
-
688
- This is the Codex entry file for the project.
689
-
690
- ${managedBlock()}
691
- `;
692
- }
693
-
694
- function tplAgentEntry(label) {
695
- return `# ${label}
696
-
697
- This is the ${label} entry file for this project.
698
-
699
- ${managedBlock()}
700
- `;
701
- }
702
-
703
- function tplLlmsTxt(p) {
704
- return `${HDR_S}
705
- # ${p.name}
706
-
707
- > LLM-facing project map for this project.
708
- ${HDR_E}
709
-
710
- This file is a map, not a startup read. Start from the entry-file protocol and open only what the task needs.
711
-
712
- ## Core
713
-
714
- ${CORE_S}
715
- ${coreLlmsInner()}
716
- ${CORE_E}
717
-
718
- ## Systems
719
-
720
- ${SYS_S}
721
- _None yet._
722
- ${SYS_E}
723
-
724
- ## Wiki
725
-
726
- ${WIKI_S}
727
- _None yet._
728
- ${WIKI_E}
729
-
730
- ## Optional
731
-
732
- - [AGENTS.md](AGENTS.md): Codex entry file.
733
- - [CLAUDE.md](CLAUDE.md): Claude Code entry file.
734
- - [Project Memory Maintainer](skills/project-memory-maintainer/SKILL.md): local maintenance skill.
735
- `;
736
- }
737
-
738
- // ═══════════════════════════════════════════════════════════════════
739
- // TEMPLATES — dynamic .memoc files
740
- // ═══════════════════════════════════════════════════════════════════
741
-
742
- function tplProjectBrief(p) {
743
- return `# Project Brief
744
-
745
- This is the shortest project summary for a fresh agent. Keep it factual and easy to scan.
746
-
747
- ## Identity
748
-
749
- ${ID_S}
750
- ${identityInner(p)}
751
- ${ID_E}
752
-
753
- ## Current Direction
754
-
755
- _Not set yet._
756
-
757
- ## How To Approach
758
-
759
- - Start from \`session-summary.md\`; search before opening more files.
760
- - Open status, handoff, rules, map, systems, or wiki docs only when the task needs them.
761
- - After durable work, update the smallest relevant memory set.
762
- - Do not treat generated output folders as source unless the user explicitly asks.
763
-
764
- ## Next Useful Work
765
-
766
- _None yet._
767
-
768
- ## Important Notes
769
-
770
- _None yet._
771
- `;
772
- }
773
-
774
- function tplAgentIndex(p) {
775
- return `# Agent Index
776
-
777
- This is the fast entry map for agents. Start here, then open only the docs relevant to the task.
778
-
779
- ## Read Order
780
-
781
- 1. Entry file managed block.
782
- 2. \`.memoc/session-summary.md\`.
783
- 3. Search first, then open only task-relevant files.
784
-
785
- ## Project Snapshot
786
-
787
- ${SNAP_S}
788
- ${snapshotInner(p)}
789
- ${SNAP_E}
790
-
791
- ## Core Docs
792
-
793
- - [Boot](boot.md)
794
- - [Project Brief](00-project-brief.md)
795
- - [memoc Usage](memoc-usage.md)
796
- - [Agent Workflow](01-agent-workflow.md)
797
- - [Current Project State](02-current-project-state.md)
798
- - [Decisions](03-decisions.md)
799
- - [Handoff](04-handoff.md)
800
- - [Done Checklist](05-done-checklist.md)
801
- - [Project Rules](06-project-rules.md)
802
- - [Session Summary](session-summary.md)
803
- - [Project Log](log.md)
804
- - [Wiki Index](wiki/index.md)
805
- - [Systems Index](systems/README.md)
806
-
807
- ## System Docs
808
-
809
- _None yet. Add entries when subsystems are documented._
810
-
811
- ## Wiki
812
-
813
- _None yet. Add entries when wiki pages are created._
814
- `;
815
- }
816
-
817
- function tplCurrentState(p) {
818
- return `# Current Project State
819
-
820
- Last synced: ${nowISO()}
821
-
822
- ## Current Status
823
-
824
- _See Project Snapshot below. Keep only current human-written status notes here._
825
-
826
- ## Project Snapshot
827
-
828
- ${SNAP_S}
829
- ${snapshotInner(p)}
830
- ${SNAP_E}
831
-
832
- ## Open Tasks
833
-
834
- _None yet._
835
-
836
- ## Completed Tasks
837
-
838
- See \`.memoc/log.md\` for full history.
839
-
840
- ## Commands
841
-
842
- _None recorded yet._
843
-
844
- ## Notes
845
-
846
- _None yet._
847
-
848
- ## Change Log
849
-
850
- See \`.memoc/log.md\`.
851
- `;
852
- }
853
-
854
- function tplSessionSummary() {
855
- return `# Session Summary
856
- Last: ${nowISO()}
857
- Keep each section ≤ 3 bullets. Agent-owned — updated by you, not by \`memoc update\`.
858
-
859
- ## Status
860
- _What is the current state of the project?_
861
-
862
- ## Changed
863
- _What changed in the last session? (code, config, decisions)_
864
-
865
- ## Open Tasks
866
- _What still needs to be done?_
867
-
868
- ## Resume
869
- _Where should the next agent pick up?_
870
- `;
871
- }
872
-
873
- // ═══════════════════════════════════════════════════════════════════
874
- // TEMPLATES — static .memoc files (same for every project)
875
- // ═══════════════════════════════════════════════════════════════════
876
-
877
- function tplBoot() {
878
- return `# Agent Boot
879
-
880
- On-demand reference only. The entry-file managed block is authoritative.
881
-
882
- ## Open Only When Needed
883
-
884
- | File | When to open |
885
- | --- | --- |
886
- | \`.memoc/session-summary.md\` | Every session start (only required read) |
887
- | \`.memoc/02-current-project-state.md\` | Before changing behavior or checking tasks |
888
- | \`.memoc/04-handoff.md\` | When resuming incomplete work |
889
- | \`.memoc/06-project-rules.md\` | When unsure about preferences or conventions |
890
- | \`.memoc/01-agent-workflow.md\` | When update routing is unclear |
891
- | \`.memoc/05-done-checklist.md\` | Before finishing substantial work |
892
- | \`.memoc/03-decisions.md\` | When a durable decision was made |
893
- | \`.memoc/log.md\` | For append-only history |
894
- | \`.memoc/memoc-usage.md\` | For command details |
895
- | \`.memoc/systems/*.md\` | Before touching a specific subsystem |
896
- | \`.memoc/wiki/*.md\` | For synthesized project knowledge |
897
- | \`llms.txt\` | For full project file map |
898
-
899
- ## Search First
900
-
901
- \`memoc search "<query>"\` — returns file:line matches across memory and agent docs only.
902
- \`memoc grep "<query>"\` — searches project source/text files when memory docs are not enough.
903
- 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>"\`.
904
- Use it before opening any file to avoid reading more than needed.
905
- `;
906
- }
907
-
908
- function tplWorkflow() {
909
- return `# Agent Workflow
910
-
911
- Shared protocol for any coding agent.
912
-
913
- ## Entry Routine
914
-
915
- 1. Read the entry-file managed block.
916
- 2. Read \`.memoc/session-summary.md\` only.
917
- 3. Search before opening broad docs.
918
- 4. Work from the smallest relevant file set.
919
- 5. Update memory only when durable context changed.
920
-
921
- ## Memory Update Triggers
922
-
923
- | Trigger | Update |
924
- | --- | --- |
925
- | User creates or changes a requirement | \`02-current-project-state.md\`, \`06-project-rules.md\`, \`log.md\` |
926
- | Code, config, data, or assets changed | \`02-current-project-state.md\`, relevant \`systems/*.md\`, \`log.md\` |
927
- | Architecture or system behavior changed | relevant \`systems/*.md\`, \`03-decisions.md\` |
928
- | A decision should affect future agents | \`03-decisions.md\`, \`02-current-project-state.md\` |
929
- | Work is substantial enough to resume later | \`04-handoff.md\`, \`02-current-project-state.md\`, \`log.md\` |
930
- | Durable knowledge was learned | \`wiki/*.md\`, \`wiki/index.md\` |
931
-
932
- ## Usually No Update Needed
933
-
934
- - Pure Q&A with no durable outcome.
935
- - Tiny typo-only edits.
936
- - Temporary exploration that finds nothing actionable.
937
-
938
- ## Documentation Shape
939
-
940
- - Entry files: protocol only.
941
- - \`session-summary.md\`: latest snapshot, max 3 bullets per section.
942
- - \`02-current-project-state.md\`: current status, tasks, commands, recent notes.
943
- - \`04-handoff.md\`: resume context, blockers, verified/unverified checks.
944
- - \`03-decisions.md\`: append durable decisions only.
945
- - \`log.md\`: full history; keep bulky completed work here.
946
- - \`systems/*.md\` and \`wiki/*.md\`: on-demand durable knowledge.
947
- `;
948
- }
949
-
950
- function tplDecisions() {
951
- return `# Decisions
952
-
953
- Durable project decisions live here. Keep entries short, dated, and useful to future agents.
954
-
955
- ## Decision Log
956
-
957
- _None yet._
958
- `;
959
- }
960
-
961
- function tplHandoff() {
962
- return `# Agent Handoff
963
-
964
- Last synced: ${nowISO()}
965
-
966
- ## What Changed
967
-
968
- _None yet._
969
-
970
- ## Next Steps
971
-
972
- _None yet._
973
-
974
- ## Blockers
975
-
976
- _None yet._
977
-
978
- ## Do Not Touch Without Asking
979
-
980
- _None yet._
981
-
982
- ## Verified
983
-
984
- _None yet._
985
-
986
- ## Not Verified
987
-
988
- _None yet._
989
-
990
- ## Resume Notes
991
-
992
- _None yet._
993
-
994
- ## Suggested Reads
995
-
996
- Search first, then open only files named above.
997
- `;
998
- }
999
-
1000
- function tplDoneChecklist() {
1001
- return `# Done Checklist
1002
-
1003
- Run through this before saying substantial work is complete.
1004
-
1005
- ## Code
1006
-
1007
- - [ ] Changes compile or run without errors.
1008
- - [ ] Relevant tests pass (or new tests were added).
1009
- - [ ] No obvious security issues introduced.
1010
- - [ ] No hardcoded secrets or credentials.
1011
-
1012
- ## Memory
1013
-
1014
- - [ ] \`.memoc/02-current-project-state.md\` reflects the new status.
1015
- - [ ] \`.memoc/03-decisions.md\` updated if a durable decision was made.
1016
- - [ ] \`.memoc/04-handoff.md\` updated if work is incomplete or risky.
1017
- - [ ] \`.memoc/log.md\` has a new entry for meaningful work.
1018
- - [ ] Relevant \`.memoc/systems/*.md\` or wiki pages updated.
1019
-
1020
- ## Communication
1021
-
1022
- - [ ] Final answer states what was verified and what was not.
1023
- - [ ] Unverified risks are noted in handoff.
1024
- `;
1025
- }
1026
-
1027
- function tplProjectRules() {
1028
- return `# Project Rules
1029
-
1030
- Durable user and project preferences live here. Update when the user gives a rule that should persist across sessions.
1031
-
1032
- ## Operating Rules
1033
-
1034
- - Keep \`AGENTS.md\` and \`CLAUDE.md\` as short entry files; durable context belongs under \`.memoc/\`.
1035
- - Do not track generated output folders such as \`out/\`, \`.next/\`, \`dist/\`, \`build/\` unless the user explicitly asks.
1036
- - Update \`.memoc/04-handoff.md\` after substantial work so the next agent can resume quickly.
1037
- - Use \`.memoc/05-done-checklist.md\` before saying substantial work is complete.
1038
-
1039
- ## Agent Behavior Preferences
1040
-
1041
- - Be factual and operational in memory docs.
1042
- - Keep logs concise; do not paste temporary command output unless it changes future work.
1043
- - Preserve user changes and avoid reverting unrelated work.
1044
- - State unverified parts honestly in the final answer and handoff.
1045
-
1046
- ## Project-Specific Rules
1047
-
1048
- _None yet._
1049
- `;
1050
- }
1051
-
1052
- function tplLog() {
1053
- return `# Project Log
1054
-
1055
- Append-only chronological log for project memory updates.
1056
-
1057
- ## [${nowISO()}] init | Initialized memoc memory structure.
1058
- `;
1059
- }
1060
-
619
+ return [
620
+ `- Project name: \`${p.name}\``,
621
+ `- Detected stack: ${stackStr(p.stack)}`,
622
+ ].join('\n');
623
+ }
624
+
625
+ function snapshotInner(p) {
626
+ const lines = [
627
+ `- Last synced: ${nowISO()}`,
628
+ `- Detected stack: ${stackStr(p.stack)}`,
629
+ ];
630
+ if (p.configFiles.length)
631
+ lines.push(`\n### Config Files\n\n${listMd(p.configFiles)}`);
632
+ if (p.srcDirs.length)
633
+ lines.push(`\n### Source Directories\n\n${listMd(p.srcDirs)}`);
634
+ const sc = scriptsMd(p.scripts);
635
+ if (sc !== '_None detected._')
636
+ lines.push(`\n### Package Scripts\n\n${sc}`);
637
+ return lines.join('\n');
638
+ }
639
+
640
+ function coreLlmsInner() {
641
+ return `- [Session Summary](.memoc/session-summary.md): only required startup read.
642
+ - [Current State](.memoc/02-current-project-state.md): status, tasks, commands.
643
+ - [Handoff](.memoc/04-handoff.md): resume context, blockers, verification.
644
+ - [Rules](.memoc/06-project-rules.md): durable preferences.
645
+ - [Agent Index](.memoc/00-agent-index.md): compact file map.
646
+ - [Project Brief](.memoc/00-project-brief.md): short identity and direction.
647
+ - [Workflow](.memoc/01-agent-workflow.md): update trigger matrix.
648
+ - [Decisions](.memoc/03-decisions.md): durable decisions.
649
+ - [Log](.memoc/log.md): append-only history.
650
+ - [Systems](.memoc/systems/README.md): subsystem docs.
651
+ - [Wiki](.memoc/wiki/index.md): synthesized knowledge.`;
652
+ }
653
+
654
+ function headerInner(p) {
655
+ return `# ${p.name}\n\n> LLM-facing project map for this project.`;
656
+ }
657
+
658
+ function systemsLlmsInner(dir) {
659
+ const systemsDir = path.join(dir, '.memoc', 'systems');
660
+ if (!fs.existsSync(systemsDir)) return '_None yet._';
661
+ const files = fs.readdirSync(systemsDir)
662
+ .filter(f => f.endsWith('.md') && f !== 'README.md')
663
+ .sort();
664
+ if (!files.length) return '_None yet._';
665
+ return files.map(f => `- [${f.replace('.md', '')}](.memoc/systems/${f}): subsystem context.`).join('\n');
666
+ }
667
+
668
+ function wikiLlmsInner(dir) {
669
+ const wikiDir = path.join(dir, '.memoc', 'wiki');
670
+ if (!fs.existsSync(wikiDir)) return '_None yet._';
671
+ const lines = [];
672
+ const SKIP = new Set(['index.md']);
673
+ try {
674
+ for (const f of fs.readdirSync(wikiDir).sort()) {
675
+ if (!f.endsWith('.md') || SKIP.has(f)) continue;
676
+ try { if (fs.statSync(path.join(wikiDir, f)).isDirectory()) continue; } catch { continue; }
677
+ lines.push(`- [${f.replace('.md', '')}](.memoc/wiki/${f}): wiki page.`);
678
+ }
679
+ for (const sub of ['sources', 'topics', 'global']) {
680
+ const subDir = path.join(wikiDir, sub);
681
+ if (!fs.existsSync(subDir)) continue;
682
+ for (const f of fs.readdirSync(subDir).sort()) {
683
+ if (!f.endsWith('.md')) continue;
684
+ lines.push(`- [${f.replace('.md', '')}](.memoc/wiki/${sub}/${f}): wiki page.`);
685
+ }
686
+ }
687
+ } catch {}
688
+ return lines.length ? lines.join('\n') : '_None yet._';
689
+ }
690
+
691
+ function wikiScaffoldFiles(memDir) {
692
+ return [
693
+ [
694
+ path.join(memDir, 'wiki/index.md'),
695
+ tplWikiIndex,
696
+ src => src.includes('# Wiki Index') && src.includes('Persistent LLM-maintained project wiki') &&
697
+ (src.includes('_None yet._') || !src.includes('## Graph Hubs')),
698
+ ],
699
+ [
700
+ path.join(memDir, 'wiki/sources.md'),
701
+ tplWikiSources,
702
+ src => hasOnlyScaffold(src, ['# Sources', '_No sources recorded yet._']) && !src.includes('## Related'),
703
+ ],
704
+ [
705
+ path.join(memDir, 'wiki/glossary.md'),
706
+ tplWikiGlossary,
707
+ src => hasOnlyScaffold(src, ['# Glossary', '_No terms defined yet._']) && !src.includes('## Related'),
708
+ ],
709
+ [
710
+ path.join(memDir, 'wiki/questions.md'),
711
+ tplWikiQuestions,
712
+ src => hasOnlyScaffold(src, ['# Open Questions', '_No open questions yet._']) && !src.includes('## Related'),
713
+ ],
714
+ [
715
+ path.join(memDir, 'wiki/lint.md'),
716
+ tplWikiLint,
717
+ src => src.includes('# Wiki Lint') && src.includes('_No issues found._') && !src.includes('## Graph Checks'),
718
+ ],
719
+ [
720
+ path.join(memDir, 'wiki/sources/README.md'),
721
+ tplWikiSourcesReadme,
722
+ src => hasOnlyScaffold(src, ['# Sources', 'Provenance records']) && !src.includes('## Related'),
723
+ ],
724
+ [
725
+ path.join(memDir, 'wiki/topics/README.md'),
726
+ tplWikiTopicsReadme,
727
+ src => hasOnlyScaffold(src, ['# Topics', 'Synthesized topic pages']) && !src.includes('## Related'),
728
+ ],
729
+ [
730
+ path.join(memDir, 'wiki/global/README.md'),
731
+ tplWikiGlobalReadme,
732
+ src => hasOnlyScaffold(src, ['# Global', 'Project-wide principles']) && !src.includes('## Related'),
733
+ ],
734
+ ];
735
+ }
736
+
737
+ function ensureWikiScaffoldLinks(memDir, mark) {
738
+ for (const [fp, tpl, isDefaultish] of wikiScaffoldFiles(memDir)) {
739
+ const result = writeIfDefaultish(fp, tpl(), isDefaultish);
740
+ if (result !== 'skip') mark(result, `${path.relative(path.dirname(memDir), fp)} (wiki links)`);
741
+ }
742
+ }
743
+
744
+ // ═══════════════════════════════════════════════════════════════════
745
+ // TEMPLATES — entry files
746
+ // ═══════════════════════════════════════════════════════════════════
747
+
748
+ function tplClaude() {
749
+ return `# CLAUDE.md
750
+
751
+ This is the Claude Code entry file for the project.
752
+
753
+ ${managedBlock()}
754
+ `;
755
+ }
756
+
757
+ function tplAgents() {
758
+ return `# AGENTS.md
759
+
760
+ This is the Codex entry file for the project.
761
+
762
+ ${managedBlock()}
763
+ `;
764
+ }
765
+
766
+ function tplAgentEntry(label) {
767
+ return `# ${label}
768
+
769
+ This is the ${label} entry file for this project.
770
+
771
+ ${managedBlock()}
772
+ `;
773
+ }
774
+
775
+ function tplLlmsTxt(p) {
776
+ return `${HDR_S}
777
+ # ${p.name}
778
+
779
+ > LLM-facing project map for this project.
780
+ ${HDR_E}
781
+
782
+ This file is a map, not a startup read. Start from the entry-file protocol and open only what the task needs.
783
+
784
+ ## Core
785
+
786
+ ${CORE_S}
787
+ ${coreLlmsInner()}
788
+ ${CORE_E}
789
+
790
+ ## Systems
791
+
792
+ ${SYS_S}
793
+ _None yet._
794
+ ${SYS_E}
795
+
796
+ ## Wiki
797
+
798
+ ${WIKI_S}
799
+ _None yet._
800
+ ${WIKI_E}
801
+
802
+ ## Optional
803
+
804
+ - [AGENTS.md](AGENTS.md): Codex entry file.
805
+ - [CLAUDE.md](CLAUDE.md): Claude Code entry file.
806
+ - [Project Memory Maintainer](skills/project-memory-maintainer/SKILL.md): local maintenance skill.
807
+ `;
808
+ }
809
+
810
+ // ═══════════════════════════════════════════════════════════════════
811
+ // TEMPLATES — dynamic .memoc files
812
+ // ═══════════════════════════════════════════════════════════════════
813
+
814
+ function tplProjectBrief(p) {
815
+ return `# Project Brief
816
+
817
+ This is the shortest project summary for a fresh agent. Keep it factual and easy to scan.
818
+
819
+ ## Identity
820
+
821
+ ${ID_S}
822
+ ${identityInner(p)}
823
+ ${ID_E}
824
+
825
+ ## Current Direction
826
+
827
+ _Not set yet._
828
+
829
+ ## How To Approach
830
+
831
+ - Start from \`session-summary.md\`; search before opening more files.
832
+ - Open status, handoff, rules, map, systems, or wiki docs only when the task needs them.
833
+ - After durable work, update the smallest relevant memory set.
834
+ - Do not treat generated output folders as source unless the user explicitly asks.
835
+
836
+ ## Next Useful Work
837
+
838
+ _None yet._
839
+
840
+ ## Important Notes
841
+
842
+ _None yet._
843
+ `;
844
+ }
845
+
846
+ function tplAgentIndex(p) {
847
+ return `# Agent Index
848
+
849
+ This is the fast entry map for agents. Start here, then open only the docs relevant to the task.
850
+
851
+ ## Read Order
852
+
853
+ 1. Entry file managed block.
854
+ 2. \`.memoc/session-summary.md\`.
855
+ 3. Search first, then open only task-relevant files.
856
+
857
+ ## Project Snapshot
858
+
859
+ ${SNAP_S}
860
+ ${snapshotInner(p)}
861
+ ${SNAP_E}
862
+
863
+ ## Core Docs
864
+
865
+ - [Boot](boot.md)
866
+ - [Project Brief](00-project-brief.md)
867
+ - [memoc Usage](memoc-usage.md)
868
+ - [Agent Workflow](01-agent-workflow.md)
869
+ - [Current Project State](02-current-project-state.md)
870
+ - [Decisions](03-decisions.md)
871
+ - [Handoff](04-handoff.md)
872
+ - [Done Checklist](05-done-checklist.md)
873
+ - [Project Rules](06-project-rules.md)
874
+ - [Session Summary](session-summary.md)
875
+ - [Project Log](log.md)
876
+ - [Wiki Index](wiki/index.md)
877
+ - [Systems Index](systems/README.md)
878
+
879
+ ## System Docs
880
+
881
+ _None yet. Add entries when subsystems are documented._
882
+
883
+ ## Wiki
884
+
885
+ - [Wiki Index](wiki/index.md) hub for every synthesized wiki page.
886
+ - [Sources](wiki/sources.md) — source provenance and ingest notes.
887
+ - [Glossary](wiki/glossary.md) — project terms and aliases.
888
+ - [Open Questions](wiki/questions.md) unresolved knowledge gaps.
889
+ - [Wiki Lint](wiki/lint.md) — orphan, stale, and contradiction checks.
890
+ `;
891
+ }
892
+
893
+ function tplCurrentState(p) {
894
+ return `# Current Project State
895
+
896
+ Last synced: ${nowISO()}
897
+
898
+ ## Current Status
899
+
900
+ _See Project Snapshot below. Keep only current human-written status notes here._
901
+
902
+ ## Project Snapshot
903
+
904
+ ${SNAP_S}
905
+ ${snapshotInner(p)}
906
+ ${SNAP_E}
907
+
908
+ ## Open Tasks
909
+
910
+ _None yet._
911
+
912
+ ## Completed Tasks
913
+
914
+ See \`.memoc/log.md\` for full history.
915
+
916
+ ## Commands
917
+
918
+ _None recorded yet._
919
+
920
+ ## Notes
921
+
922
+ _None yet._
923
+
924
+ ## Change Log
925
+
926
+ See \`.memoc/log.md\`.
927
+ `;
928
+ }
929
+
930
+ function tplSessionSummary() {
931
+ return `# Session Summary
932
+ Last: ${nowISO()}
933
+ Keep each section ≤ 3 bullets. Agent-owned — updated by you, not by \`memoc update\`.
934
+
935
+ ## Status
936
+ _What is the current state of the project?_
937
+
938
+ ## Changed
939
+ _What changed in the last session? (code, config, decisions)_
940
+
941
+ ## Open Tasks
942
+ _What still needs to be done?_
943
+
944
+ ## Resume
945
+ _Where should the next agent pick up?_
946
+ `;
947
+ }
948
+
949
+ // ═══════════════════════════════════════════════════════════════════
950
+ // TEMPLATES — static .memoc files (same for every project)
951
+ // ═══════════════════════════════════════════════════════════════════
952
+
953
+ function tplBoot() {
954
+ return `# Agent Boot
955
+
956
+ On-demand reference only. The entry-file managed block is authoritative.
957
+
958
+ ## Open Only When Needed
959
+
960
+ | File | When to open |
961
+ | --- | --- |
962
+ | \`.memoc/session-summary.md\` | Every session start (only required read) |
963
+ | \`.memoc/02-current-project-state.md\` | Before changing behavior or checking tasks |
964
+ | \`.memoc/04-handoff.md\` | When resuming incomplete work |
965
+ | \`.memoc/06-project-rules.md\` | When unsure about preferences or conventions |
966
+ | \`.memoc/01-agent-workflow.md\` | When update routing is unclear |
967
+ | \`.memoc/05-done-checklist.md\` | Before finishing substantial work |
968
+ | \`.memoc/03-decisions.md\` | When a durable decision was made |
969
+ | \`.memoc/log.md\` | For append-only history |
970
+ | \`.memoc/memoc-usage.md\` | For command details |
971
+ | \`.memoc/systems/*.md\` | Before touching a specific subsystem |
972
+ | \`.memoc/wiki/*.md\` | For synthesized project knowledge |
973
+ | \`llms.txt\` | For full project file map |
974
+
975
+ ## Search First
976
+
977
+ \`memoc search "<query>"\` — returns file:line matches across memory and agent docs only.
978
+ \`memoc grep "<query>"\` — searches project source/text files when memory docs are not enough.
979
+ 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>"\`.
980
+ Use it before opening any file to avoid reading more than needed.
981
+ `;
982
+ }
983
+
984
+ function tplWorkflow() {
985
+ return `# Agent Workflow
986
+
987
+ Shared protocol for any coding agent.
988
+
989
+ ## Entry Routine
990
+
991
+ 1. Read the entry-file managed block.
992
+ 2. Read \`.memoc/session-summary.md\` only.
993
+ 3. Search before opening broad docs.
994
+ 4. Work from the smallest relevant file set.
995
+ 5. Update memory only when durable context changed.
996
+
997
+ ## Memory Update Triggers
998
+
999
+ | Trigger | Update |
1000
+ | --- | --- |
1001
+ | User creates or changes a requirement | \`02-current-project-state.md\`, \`06-project-rules.md\`, \`log.md\` |
1002
+ | Code, config, data, or assets changed | \`02-current-project-state.md\`, relevant \`systems/*.md\`, \`log.md\` |
1003
+ | Architecture or system behavior changed | relevant \`systems/*.md\`, \`03-decisions.md\` |
1004
+ | A decision should affect future agents | \`03-decisions.md\`, \`02-current-project-state.md\` |
1005
+ | Work is substantial enough to resume later | \`04-handoff.md\`, \`02-current-project-state.md\`, \`log.md\` |
1006
+ | Durable knowledge was learned | \`wiki/*.md\`, \`wiki/index.md\` |
1007
+
1008
+ ## Usually No Update Needed
1009
+
1010
+ - Pure Q&A with no durable outcome.
1011
+ - Tiny typo-only edits.
1012
+ - Temporary exploration that finds nothing actionable.
1013
+
1014
+ ## Documentation Shape
1015
+
1016
+ - Entry files: protocol only.
1017
+ - \`session-summary.md\`: latest snapshot, max 3 bullets per section.
1018
+ - \`02-current-project-state.md\`: current status, tasks, commands, recent notes.
1019
+ - \`04-handoff.md\`: resume context, blockers, verified/unverified checks.
1020
+ - \`03-decisions.md\`: append durable decisions only.
1021
+ - \`log.md\`: full history; keep bulky completed work here.
1022
+ - \`systems/*.md\` and \`wiki/*.md\`: on-demand durable knowledge.
1023
+ `;
1024
+ }
1025
+
1026
+ function tplDecisions() {
1027
+ return `# Decisions
1028
+
1029
+ Durable project decisions live here. Keep entries short, dated, and useful to future agents.
1030
+
1031
+ ## Decision Log
1032
+
1033
+ _None yet._
1034
+ `;
1035
+ }
1036
+
1037
+ function tplHandoff() {
1038
+ return `# Agent Handoff
1039
+
1040
+ Last synced: ${nowISO()}
1041
+
1042
+ ## What Changed
1043
+
1044
+ _None yet._
1045
+
1046
+ ## Next Steps
1047
+
1048
+ _None yet._
1049
+
1050
+ ## Blockers
1051
+
1052
+ _None yet._
1053
+
1054
+ ## Do Not Touch Without Asking
1055
+
1056
+ _None yet._
1057
+
1058
+ ## Verified
1059
+
1060
+ _None yet._
1061
+
1062
+ ## Not Verified
1063
+
1064
+ _None yet._
1065
+
1066
+ ## Resume Notes
1067
+
1068
+ _None yet._
1069
+
1070
+ ## Suggested Reads
1071
+
1072
+ Search first, then open only files named above.
1073
+ `;
1074
+ }
1075
+
1076
+ function tplDoneChecklist() {
1077
+ return `# Done Checklist
1078
+
1079
+ Run through this before saying substantial work is complete.
1080
+
1081
+ ## Code
1082
+
1083
+ - [ ] Changes compile or run without errors.
1084
+ - [ ] Relevant tests pass (or new tests were added).
1085
+ - [ ] No obvious security issues introduced.
1086
+ - [ ] No hardcoded secrets or credentials.
1087
+
1088
+ ## Memory
1089
+
1090
+ - [ ] \`.memoc/02-current-project-state.md\` reflects the new status.
1091
+ - [ ] \`.memoc/03-decisions.md\` updated if a durable decision was made.
1092
+ - [ ] \`.memoc/04-handoff.md\` updated if work is incomplete or risky.
1093
+ - [ ] \`.memoc/log.md\` has a new entry for meaningful work.
1094
+ - [ ] Relevant \`.memoc/systems/*.md\` or wiki pages updated.
1095
+
1096
+ ## Communication
1097
+
1098
+ - [ ] Final answer states what was verified and what was not.
1099
+ - [ ] Unverified risks are noted in handoff.
1100
+ `;
1101
+ }
1102
+
1103
+ function tplProjectRules() {
1104
+ return `# Project Rules
1105
+
1106
+ Durable user and project preferences live here. Update when the user gives a rule that should persist across sessions.
1107
+
1108
+ ## Operating Rules
1109
+
1110
+ - Keep \`AGENTS.md\` and \`CLAUDE.md\` as short entry files; durable context belongs under \`.memoc/\`.
1111
+ - Do not track generated output folders such as \`out/\`, \`.next/\`, \`dist/\`, \`build/\` unless the user explicitly asks.
1112
+ - Update \`.memoc/04-handoff.md\` after substantial work so the next agent can resume quickly.
1113
+ - Use \`.memoc/05-done-checklist.md\` before saying substantial work is complete.
1114
+
1115
+ ## Agent Behavior Preferences
1116
+
1117
+ - Be factual and operational in memory docs.
1118
+ - Keep logs concise; do not paste temporary command output unless it changes future work.
1119
+ - Preserve user changes and avoid reverting unrelated work.
1120
+ - State unverified parts honestly in the final answer and handoff.
1121
+
1122
+ ## Project-Specific Rules
1123
+
1124
+ _None yet._
1125
+ `;
1126
+ }
1127
+
1128
+ function tplLog() {
1129
+ return `# Project Log
1130
+
1131
+ Append-only chronological log for project memory updates.
1132
+
1133
+ ## [${nowISO()}] init | Initialized memoc memory structure.
1134
+ `;
1135
+ }
1136
+
1061
1137
  function tplMemocUsage() {
1062
1138
  return `# memoc Usage
1063
-
1064
- This project uses \`memoc\` to maintain agent-readable project memory.
1065
-
1066
- ## Commands
1067
-
1068
- \`\`\`bash
1069
- # Optional: put the project-local wrapper first in PATH for this shell
1070
- # PowerShell: . .\\.memoc\\env.ps1
1071
- # sh/bash: . ./.memoc/env.sh
1072
-
1073
- # First-time setup (or re-run to update managed sections)
1074
- memoc init
1075
-
1139
+
1140
+ This project uses \`memoc\` to maintain agent-readable project memory.
1141
+
1142
+ ## Commands
1143
+
1144
+ \`\`\`bash
1145
+ # Optional: put the project-local wrapper first in PATH for this shell
1146
+ # PowerShell: . .\\.memoc\\env.ps1
1147
+ # sh/bash: . ./.memoc/env.sh
1148
+
1149
+ # First-time setup (or re-run to update managed sections)
1150
+ memoc init
1151
+
1076
1152
  # Refresh memoc itself when run through npx @latest, preserving project memory
1077
1153
  memoc upgrade
1078
1154
 
1079
1155
  # Explicitly update managed sections based on current project state
1080
1156
  memoc update
1081
-
1082
- # Tiny status overview
1083
- memoc summary
1084
-
1085
- # Search memory first; add --snippets only when needed
1086
- memoc search "<query>" --limit 12
1087
- memoc search "<query>" --snippets --limit 5
1088
-
1089
- # Search project source/text files when memory is not enough
1090
- memoc grep "<query>" --limit 12
1091
- memoc grep "<query>" --snippets --limit 5
1092
- \`\`\`
1093
-
1157
+
1158
+ # Tiny status overview
1159
+ memoc summary
1160
+
1161
+ # Search memory first; add --snippets only when needed
1162
+ memoc search "<query>" --limit 12
1163
+ memoc search "<query>" --snippets --limit 5
1164
+
1165
+ # Search project source/text files when memory is not enough
1166
+ memoc grep "<query>" --limit 12
1167
+ memoc grep "<query>" --snippets --limit 5
1168
+ \`\`\`
1169
+
1094
1170
  If \`memoc\` is not on PATH, use \`.\\.memoc\\bin\\memoc.cmd <command>\` on Windows or \`.memoc/bin/memoc <command>\` in sh for the rest of the session. If the local wrapper is missing, use \`npx @kevin0181/memoc <command>\` or re-run init.
1095
-
1171
+
1096
1172
  ## Agent Read Order
1097
1173
 
1098
1174
  1. Entry-file managed block.
@@ -1103,156 +1179,321 @@ If \`memoc\` is not on PATH, use \`.\\.memoc\\bin\\memoc.cmd <command>\` on Wind
1103
1179
  6. Use \`--snippets\` only when file names are not enough.
1104
1180
 
1105
1181
  Use \`memoc search\` for known concepts, changed areas, decisions, tasks, or handoff notes. Skip it for brand-new questions where no prior memory can exist.
1106
-
1107
- ## When To Run Memory Updates
1108
-
1109
- Use \`memoc update\` or \`skills/project-memory-maintainer/SKILL.md\` when:
1110
-
1111
- - Requirements, acceptance criteria, user preferences, or project rules changed.
1112
- - Source code, config, data, content, or package scripts changed.
1113
- - Architecture, data flow, routing, auth, or deployment behavior changed.
1114
- - A decision was made that future agents should not revisit blindly.
1115
- - Work is partial, multi-step, blocked, or likely to be resumed by another agent.
1116
- - New durable knowledge belongs in \`.memoc/wiki/\` or a subsystem doc.
1117
-
1118
- Usually skip for pure Q&A, throwaway exploration, or tiny edits with no future impact.
1119
-
1120
- ## Updating The Wiki
1121
-
1122
- Create a new Markdown file under \`.memoc/wiki/\` when synthesized knowledge should compound across sessions.
1123
-
1124
- - \`.memoc/wiki/sources/\`: provenance records.
1125
- - \`.memoc/wiki/topics/\`: synthesized topic pages.
1126
- - \`.memoc/wiki/global/\`: project-wide principles.
1127
-
1128
- After creating or editing wiki pages:
1129
- 1. Update \`.memoc/wiki/index.md\`.
1130
- 2. Append \`.memoc/log.md\`.
1131
-
1132
- ## Updating System Docs
1133
-
1134
- Create or update \`.memoc/systems/*.md\` when a subsystem needs durable detail.
1135
-
1136
- Examples: \`frontend.md\`, \`deployment.md\`, \`data-sources.md\`, \`auth.md\`
1137
- `;
1138
- }
1139
-
1140
- function tplSystemsReadme() {
1141
- return `# Systems
1142
-
1143
- Subsystem documentation for agents.
1144
-
1145
- ## How To Use
1146
-
1147
- Create a new \`.md\` file here when a subsystem becomes important enough that future agents should not rediscover it from scratch.
1148
-
1149
- ## Examples
1150
-
1151
- - \`frontend.md\` — component library, routing, state management
1152
- - \`deployment.md\` — CI/CD, environment setup, release process
1153
- - \`data-sources.md\` — databases, APIs, file sources
1154
- - \`auth.md\` — authentication and authorization
1155
- - \`design-system.md\` — colors, typography, spacing
1156
- `;
1157
- }
1158
-
1159
- function tplWikiIndex() {
1160
- return `# Wiki Index
1161
-
1162
- Persistent LLM-maintained project wiki.
1163
-
1164
- ## Pages
1165
-
1166
- _None yet._
1167
-
1168
- ## Subdirectories
1169
-
1170
- - \`sources/\`provenance records
1171
- - \`topics/\`synthesized topic pages
1172
- - \`global/\` — project-wide principles
1173
- `;
1174
- }
1175
-
1176
- function tplWikiSources() { return `# Sources\n\n_No sources recorded yet._\n`; }
1177
- function tplWikiGlossary() { return `# Glossary\n\n_No terms defined yet._\n`; }
1178
- function tplWikiQuestions() { return `# Open Questions\n\n_No open questions yet._\n`; }
1179
- function tplWikiSourcesReadme() { return `# Sources\n\nProvenance records for conversations, URLs, docs, and issues.\n`; }
1180
- function tplWikiTopicsReadme() { return `# Topics\n\nSynthesized topic pages that compound knowledge across sessions.\n`; }
1181
- function tplWikiGlobalReadme() { return `# Global\n\nProject-wide principles, positioning, and long-lived direction.\n`; }
1182
- function tplWikiLint() {
1183
- return `# Wiki Lint\n\nLast checked: ${nowISO()}\n\n## Issues\n\n_No issues found._\n\n## Warnings\n\n_None._\n`;
1184
- }
1185
-
1186
- function tplSkillMaintainer() {
1187
- return `---
1188
- name: project-memory-maintainer
1189
- description: Maintain this project's LLM-wiki memory files after durable context changes.
1190
- ---
1191
-
1192
- # Project Memory Maintainer
1193
-
1194
- Use this local skill after meaningful project work so future agents can continue without rediscovering context.
1195
-
1196
- ## Required Reads
1197
-
1198
- 1. \`.memoc/session-summary.md\`
1199
- 2. \`memoc summary\` or \`memoc search "<query>"\`; use \`memoc grep "<query>"\` only when source/text search is needed
1200
- 3. Open only files you will use or update.
1201
-
1202
- ## Maintenance Checklist
1203
-
1204
- - Keep \`llms.txt\` and \`.memoc/00-agent-index.md\` as concise maps.
1205
- - Keep \`.memoc/00-project-brief.md\` as the shortest project summary.
1206
- - Update \`.memoc/02-current-project-state.md\` with new status, tasks, commands, and change log entries.
1207
- - Update \`.memoc/03-decisions.md\` when a durable decision is made.
1208
- - Update \`.memoc/04-handoff.md\` before ending substantial work.
1209
- - Check \`.memoc/05-done-checklist.md\` before saying substantial work is complete.
1210
- - Update \`.memoc/06-project-rules.md\` when the user gives durable preferences.
1211
- - Append \`.memoc/log.md\` for meaningful changes, decisions, and handoffs.
1212
- - Create or update \`.memoc/systems/*.md\` when a subsystem needs durable explanation.
1213
- - Create or update \`.memoc/wiki/*.md\` when synthesized knowledge should compound over time.
1214
- - Keep completed history in \`.memoc/log.md\`; keep current-state files short.
1215
- - Keep tool output small; prefer \`summary\`, file-only search, \`--limit\`, and targeted reads.
1216
-
1217
- ## Concrete Triggers
1218
-
1219
- Use this skill before finishing when any of these are true:
1220
-
1221
- - The user gives a durable preference, project rule, changed requirement, or acceptance criterion.
1222
- - The agent edits code, config, package scripts, env, data, assets, routes, or deployment files.
1223
- - A subsystem's behavior, architecture, data flow, or API contract changes.
1224
- - A future agent would need to know why an approach was chosen or rejected.
1225
- - The work is partial, blocked, risky, multi-step, or likely to be resumed later.
1226
-
1227
- Usually skip for pure Q&A, tiny edits with no future impact, or throwaway exploration.
1228
- `;
1229
- }
1230
-
1231
- // ═══════════════════════════════════════════════════════════════════
1232
- // CLAUDE CODE HOOK SETTINGS
1233
- // ═══════════════════════════════════════════════════════════════════
1234
-
1182
+
1183
+ ## When To Run Memory Updates
1184
+
1185
+ Use \`memoc update\` or \`skills/project-memory-maintainer/SKILL.md\` when:
1186
+
1187
+ - Requirements, acceptance criteria, user preferences, or project rules changed.
1188
+ - Source code, config, data, content, or package scripts changed.
1189
+ - Architecture, data flow, routing, auth, or deployment behavior changed.
1190
+ - A decision was made that future agents should not revisit blindly.
1191
+ - Work is partial, multi-step, blocked, or likely to be resumed by another agent.
1192
+ - New durable knowledge belongs in \`.memoc/wiki/\` or a subsystem doc.
1193
+
1194
+ Usually skip for pure Q&A, throwaway exploration, or tiny edits with no future impact.
1195
+
1196
+ ## Updating The Wiki
1197
+
1198
+ Create a new Markdown file under \`.memoc/wiki/\` when synthesized knowledge should compound across sessions.
1199
+
1200
+ - \`.memoc/wiki/sources/\`: provenance records.
1201
+ - \`.memoc/wiki/topics/\`: synthesized topic pages.
1202
+ - \`.memoc/wiki/global/\`: project-wide principles.
1203
+
1204
+ After creating or editing wiki pages:
1205
+ 1. Update \`.memoc/wiki/index.md\`.
1206
+ 2. Append \`.memoc/log.md\`.
1207
+
1208
+ ## Updating System Docs
1209
+
1210
+ Create or update \`.memoc/systems/*.md\` when a subsystem needs durable detail.
1211
+
1212
+ Examples: \`frontend.md\`, \`deployment.md\`, \`data-sources.md\`, \`auth.md\`
1213
+ `;
1214
+ }
1215
+
1216
+ function tplSystemsReadme() {
1217
+ return `# Systems
1218
+
1219
+ Subsystem documentation for agents.
1220
+
1221
+ ## How To Use
1222
+
1223
+ Create a new \`.md\` file here when a subsystem becomes important enough that future agents should not rediscover it from scratch.
1224
+
1225
+ ## Examples
1226
+
1227
+ - \`frontend.md\` — component library, routing, state management
1228
+ - \`deployment.md\` — CI/CD, environment setup, release process
1229
+ - \`data-sources.md\` — databases, APIs, file sources
1230
+ - \`auth.md\` — authentication and authorization
1231
+ - \`design-system.md\` — colors, typography, spacing
1232
+ `;
1233
+ }
1234
+
1235
+ function tplWikiIndex() {
1236
+ return `# Wiki Index
1237
+
1238
+ Persistent LLM-maintained project wiki.
1239
+
1240
+ ## Graph Hubs
1241
+
1242
+ - [Sources](sources.md) — provenance, ingests, and source-to-topic links.
1243
+ - [Topics](topics/README.md) — synthesized topic pages.
1244
+ - [Global](global/README.md) — project-wide principles and long-lived direction.
1245
+ - [Glossary](glossary.md) — terms, aliases, and canonical page names.
1246
+ - [Open Questions](questions.md) unresolved questions and research leads.
1247
+ - [Wiki Lint](lint.md) graph health, orphan checks, contradictions, stale claims.
1248
+
1249
+ ## Pages
1250
+
1251
+ _None yet. Add every wiki page here with a relative Markdown link and one-line summary._
1252
+
1253
+ ## Subdirectories
1254
+
1255
+ - [sources/](sources/README.md) provenance records
1256
+ - [topics/](topics/README.md) synthesized topic pages
1257
+ - [global/](global/README.md) project-wide principles
1258
+
1259
+ ## Related Core Memory
1260
+
1261
+ - [Agent Index](../00-agent-index.md)
1262
+ - [Project Brief](../00-project-brief.md)
1263
+ - [Current Project State](../02-current-project-state.md)
1264
+ - [Project Log](../log.md)
1265
+ `;
1266
+ }
1267
+
1268
+ function tplWikiSources() {
1269
+ return `# Sources
1270
+
1271
+ Provenance index for conversations, URLs, docs, issues, and files that feed the wiki.
1272
+
1273
+ ## Source Records
1274
+
1275
+ _No sources recorded yet. Link each source record to the topic/global pages it affects._
1276
+
1277
+ ## Related
1278
+
1279
+ - [Wiki Index](index.md)
1280
+ - [Source Records Directory](sources/README.md)
1281
+ - [Topics](topics/README.md)
1282
+ - [Open Questions](questions.md)
1283
+ `;
1284
+ }
1285
+
1286
+ function tplWikiGlossary() {
1287
+ return `# Glossary
1288
+
1289
+ Canonical names, aliases, and short definitions for project terms.
1290
+
1291
+ ## Terms
1292
+
1293
+ _No terms defined yet. Link terms to their canonical topic, global, source, or system page._
1294
+
1295
+ ## Related
1296
+
1297
+ - [Wiki Index](index.md)
1298
+ - [Topics](topics/README.md)
1299
+ - [Global](global/README.md)
1300
+ - [Open Questions](questions.md)
1301
+ `;
1302
+ }
1303
+
1304
+ function tplWikiQuestions() {
1305
+ return `# Open Questions
1306
+
1307
+ Unresolved questions, data gaps, contradictions, and follow-up research leads.
1308
+
1309
+ ## Questions
1310
+
1311
+ _No open questions yet. Link each question to affected pages and sources._
1312
+
1313
+ ## Related
1314
+
1315
+ - [Wiki Index](index.md)
1316
+ - [Sources](sources.md)
1317
+ - [Topics](topics/README.md)
1318
+ - [Wiki Lint](lint.md)
1319
+ `;
1320
+ }
1321
+
1322
+ function tplWikiSourcesReadme() {
1323
+ return `# Sources
1324
+
1325
+ Provenance records for conversations, URLs, docs, and issues.
1326
+
1327
+ ## How To Link
1328
+
1329
+ - Link each source record back to [Sources](../sources.md).
1330
+ - Link outward to every topic, global page, system doc, or question that the source changes.
1331
+ - Prefer one source per file when the source is substantial enough to cite later.
1332
+
1333
+ ## Related
1334
+
1335
+ - [Wiki Index](../index.md)
1336
+ - [Sources](../sources.md)
1337
+ - [Topics](../topics/README.md)
1338
+ - [Open Questions](../questions.md)
1339
+ `;
1340
+ }
1341
+
1342
+ function tplWikiTopicsReadme() {
1343
+ return `# Topics
1344
+
1345
+ Synthesized topic pages that compound knowledge across sessions.
1346
+
1347
+ ## Topic Pages
1348
+
1349
+ _None yet. Add pages here when a concept deserves durable synthesis._
1350
+
1351
+ ## How To Link
1352
+
1353
+ - Each topic page should link back to [Wiki Index](../index.md) and this [Topics](README.md) page.
1354
+ - Link to related topics, source records, glossary terms, and open questions in prose or a \`## Related\` section.
1355
+ - Avoid orphan pages: every topic needs at least one inbound link from an index, source, or related topic.
1356
+
1357
+ ## Related
1358
+
1359
+ - [Wiki Index](../index.md)
1360
+ - [Sources](../sources.md)
1361
+ - [Glossary](../glossary.md)
1362
+ - [Wiki Lint](../lint.md)
1363
+ `;
1364
+ }
1365
+
1366
+ function tplWikiGlobalReadme() {
1367
+ return `# Global
1368
+
1369
+ Project-wide principles, positioning, and long-lived direction.
1370
+
1371
+ ## Global Pages
1372
+
1373
+ _None yet. Add pages here for broad context that many topic/system pages should reference._
1374
+
1375
+ ## How To Link
1376
+
1377
+ - Link global pages back to [Wiki Index](../index.md), this [Global](README.md) page, and affected topic/system docs.
1378
+ - Use global pages for durable synthesis, not temporary task notes.
1379
+
1380
+ ## Related
1381
+
1382
+ - [Wiki Index](../index.md)
1383
+ - [Project Brief](../../00-project-brief.md)
1384
+ - [Project Rules](../../06-project-rules.md)
1385
+ - [Topics](../topics/README.md)
1386
+ `;
1387
+ }
1388
+ function tplWikiLint() {
1389
+ return `# Wiki Lint
1390
+
1391
+ Last checked: ${nowISO()}
1392
+
1393
+ ## Graph Checks
1394
+
1395
+ - Every wiki page is listed from [Wiki Index](index.md) or a directory README.
1396
+ - Every wiki page links back to an index, hub, source, topic, or related page.
1397
+ - Important concepts mentioned in two or more places have their own linked page.
1398
+ - Source records link to the pages they update, and those pages link back to sources when provenance matters.
1399
+
1400
+ ## Issues
1401
+
1402
+ _No issues found._
1403
+
1404
+ ## Warnings
1405
+
1406
+ _None._
1407
+
1408
+ ## Related
1409
+
1410
+ - [Wiki Index](index.md)
1411
+ - [Sources](sources.md)
1412
+ - [Topics](topics/README.md)
1413
+ - [Open Questions](questions.md)
1414
+ `;
1415
+ }
1416
+
1417
+ function tplSkillMaintainer() {
1418
+ return `---
1419
+ name: project-memory-maintainer
1420
+ description: Maintain this project's LLM-wiki memory files after durable context changes.
1421
+ ---
1422
+
1423
+ # Project Memory Maintainer
1424
+
1425
+ Use this local skill after meaningful project work so future agents can continue without rediscovering context.
1426
+
1427
+ ## Required Reads
1428
+
1429
+ 1. \`.memoc/session-summary.md\`
1430
+ 2. \`memoc summary\` or \`memoc search "<query>"\`; use \`memoc grep "<query>"\` only when source/text search is needed
1431
+ 3. Open only files you will use or update.
1432
+
1433
+ ## Maintenance Checklist
1434
+
1435
+ - Keep \`llms.txt\` and \`.memoc/00-agent-index.md\` as concise maps.
1436
+ - Keep \`.memoc/00-project-brief.md\` as the shortest project summary.
1437
+ - Update \`.memoc/02-current-project-state.md\` with new status, tasks, commands, and change log entries.
1438
+ - Update \`.memoc/03-decisions.md\` when a durable decision is made.
1439
+ - Update \`.memoc/04-handoff.md\` before ending substantial work.
1440
+ - Check \`.memoc/05-done-checklist.md\` before saying substantial work is complete.
1441
+ - Update \`.memoc/06-project-rules.md\` when the user gives durable preferences.
1442
+ - Append \`.memoc/log.md\` for meaningful changes, decisions, and handoffs.
1443
+ - Create or update \`.memoc/systems/*.md\` when a subsystem needs durable explanation.
1444
+ - Create or update \`.memoc/wiki/*.md\` when synthesized knowledge should compound over time.
1445
+ - Keep the wiki graph connected: update \`.memoc/wiki/index.md\`, add relative Markdown links between related pages, and include a \`## Related\` section on every new wiki page.
1446
+ - Keep completed history in \`.memoc/log.md\`; keep current-state files short.
1447
+ - Keep tool output small; prefer \`summary\`, file-only search, \`--limit\`, and targeted reads.
1448
+
1449
+ ## Wiki Link Rules
1450
+
1451
+ - Use relative Markdown links that Obsidian can follow, for example \`[Glossary](glossary.md)\` or \`[Topics](topics/README.md)\`.
1452
+ - Every wiki page must have at least one inbound link from \`wiki/index.md\`, a directory README, a source page, or a related topic.
1453
+ - Every wiki page must link outward to its parent hub plus 1-5 genuinely related pages when they exist.
1454
+ - Prefer links in normal prose when the connection is meaningful; use \`## Related\` for compact navigation.
1455
+ - When a concept appears in multiple pages, create or update a topic/glossary page and link all mentions to it.
1456
+ - After wiki edits, check \`.memoc/wiki/lint.md\` and note orphan pages, missing backlinks, contradictions, or stale claims.
1457
+
1458
+ ## Concrete Triggers
1459
+
1460
+ Use this skill before finishing when any of these are true:
1461
+
1462
+ - The user gives a durable preference, project rule, changed requirement, or acceptance criterion.
1463
+ - The agent edits code, config, package scripts, env, data, assets, routes, or deployment files.
1464
+ - A subsystem's behavior, architecture, data flow, or API contract changes.
1465
+ - A future agent would need to know why an approach was chosen or rejected.
1466
+ - The work is partial, blocked, risky, multi-step, or likely to be resumed later.
1467
+
1468
+ Usually skip for pure Q&A, tiny edits with no future impact, or throwaway exploration.
1469
+ `;
1470
+ }
1471
+
1472
+ // ═══════════════════════════════════════════════════════════════════
1473
+ // CLAUDE CODE HOOK SETTINGS
1474
+ // ═══════════════════════════════════════════════════════════════════
1475
+
1235
1476
  function claudeStopHookCmd() {
1236
1477
  return `node -e "const fs=require('fs'),{execFileSync}=require('child_process');try{const o=execFileSync('git',['status','--porcelain'],{encoding:'utf8',stdio:['ignore','pipe','ignore']});if(o.trim()){const files=o.trim().split(/\\r?\\n/).map(l=>l.slice(3).trim()).filter(Boolean).slice(0,8).join(', ');fs.writeFileSync('.memoc/.pending',new Date().toISOString()+'\\n'+files)}}catch{}"`;
1237
1478
  }
1238
-
1239
- function tplClaudeSettings() {
1240
- return JSON.stringify({
1241
- hooks: {
1242
- Stop: [{ matcher: '', hooks: [{ type: 'command', command: claudeStopHookCmd() }] }],
1243
- },
1244
- }, null, 2) + '\n';
1245
- }
1246
-
1479
+
1480
+ function tplClaudeSettings() {
1481
+ return JSON.stringify({
1482
+ hooks: {
1483
+ Stop: [{ matcher: '', hooks: [{ type: 'command', command: claudeStopHookCmd() }] }],
1484
+ },
1485
+ }, null, 2) + '\n';
1486
+ }
1487
+
1247
1488
  function ensureClaudeStopHook(settingsPath) {
1248
1489
  const cmd = claudeStopHookCmd();
1249
1490
  let settings;
1250
1491
  try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); }
1251
1492
  catch { settings = {}; }
1252
-
1493
+
1253
1494
  if (!settings.hooks || typeof settings.hooks !== 'object' || Array.isArray(settings.hooks)) settings.hooks = {};
1254
1495
  if (!Array.isArray(settings.hooks.Stop)) settings.hooks.Stop = [];
1255
-
1496
+
1256
1497
  let hasCurrent = false;
1257
1498
  let changed = false;
1258
1499
  for (const entry of settings.hooks.Stop) {
@@ -1291,11 +1532,11 @@ function isMemocClaudeStopHook(hook) {
1291
1532
  command.includes('status') &&
1292
1533
  command.includes('--porcelain');
1293
1534
  }
1294
-
1295
- // ═══════════════════════════════════════════════════════════════════
1296
- // MANAGED BLOCK UPDATE (CLAUDE.md / AGENTS.md)
1297
- // ═══════════════════════════════════════════════════════════════════
1298
-
1535
+
1536
+ // ═══════════════════════════════════════════════════════════════════
1537
+ // MANAGED BLOCK UPDATE (CLAUDE.md / AGENTS.md)
1538
+ // ═══════════════════════════════════════════════════════════════════
1539
+
1299
1540
  function ensureClaudeStopHookFile(dir, mark) {
1300
1541
  const claudeDir = path.join(dir, '.claude');
1301
1542
  const claudeSettings = path.join(claudeDir, 'settings.json');
@@ -1334,349 +1575,350 @@ function printCommandHint() {
1334
1575
  }
1335
1576
 
1336
1577
  function applyManagedBlock(filePath, tplFn) {
1337
- if (!fs.existsSync(filePath)) {
1338
- write(filePath, tplFn());
1339
- return 'add';
1340
- }
1341
- const src = fs.readFileSync(filePath, 'utf8');
1578
+ if (!fs.existsSync(filePath)) {
1579
+ write(filePath, tplFn());
1580
+ return 'add';
1581
+ }
1582
+ const src = fs.readFileSync(filePath, 'utf8');
1342
1583
  const range = findMarkedRange(src, MGMT_S, MGMT_E);
1343
1584
  if (!range) {
1344
- // No managed block — inject at end, preserving all user content
1345
- write(filePath, src.trimEnd() + '\n\n' + managedBlock() + '\n');
1346
- return 'inject';
1347
- }
1585
+ // No managed block — inject at end, preserving all user content
1586
+ write(filePath, src.trimEnd() + '\n\n' + managedBlock() + '\n');
1587
+ return 'inject';
1588
+ }
1348
1589
  write(filePath, src.slice(0, range.s) + managedBlock() + src.slice(range.e + range.endMark.length));
1349
- return 'update';
1350
- }
1351
-
1352
- // ═══════════════════════════════════════════════════════════════════
1353
- // MAIN RUNNER
1354
- // ═══════════════════════════════════════════════════════════════════
1355
-
1590
+ return 'update';
1591
+ }
1592
+
1593
+ // ═══════════════════════════════════════════════════════════════════
1594
+ // MAIN RUNNER
1595
+ // ═══════════════════════════════════════════════════════════════════
1596
+
1356
1597
  function run(dir, forceUpdate, action = 'update') {
1357
- const p = scanProject(dir);
1358
- const memDir = path.join(dir, '.memoc');
1359
- const isNew = !fs.existsSync(path.join(memDir, 'boot.md'));
1360
- const mode = (isNew && !forceUpdate) ? 'init' : 'update';
1361
-
1362
- const log = [];
1363
- const mark = (label, name) => log.push(` ${label.padEnd(8)} ${name}`);
1364
-
1365
- if (mode === 'init') {
1366
- console.log(`\n memoc init — ${path.basename(dir)}`);
1367
- console.log(p.isEmpty
1368
- ? ' Empty project → using default values.'
1369
- : ` Detected: ${stackStr(p.stack)}`
1370
- );
1371
- console.log();
1372
-
1373
- // Entry files — inject/update managed block, preserve existing user content
1374
- mark(applyManagedBlock(path.join(dir, 'CLAUDE.md'), tplClaude), 'CLAUDE.md');
1375
- mark(applyManagedBlock(path.join(dir, 'AGENTS.md'), tplAgents), 'AGENTS.md');
1376
- if (ensure(path.join(dir, 'llms.txt'), tplLlmsTxt(p))) mark('add', 'llms.txt');
1377
- else mark('skip', 'llms.txt');
1378
-
1379
- // Dynamic memory files
1380
- const dynamicFiles = [
1381
- [path.join(memDir, '00-project-brief.md'), () => tplProjectBrief(p)],
1382
- [path.join(memDir, '00-agent-index.md'), () => tplAgentIndex(p)],
1383
- [path.join(memDir, '02-current-project-state.md'), () => tplCurrentState(p)],
1384
- [path.join(memDir, 'session-summary.md'), tplSessionSummary],
1385
- ];
1386
- for (const [fp, tpl] of dynamicFiles) {
1387
- const rel = path.relative(dir, fp);
1388
- if (ensure(fp, tpl())) mark('add', rel); else mark('skip', rel);
1389
- }
1390
-
1391
- // Static memory files
1392
- const staticFiles = [
1393
- [path.join(memDir, 'boot.md'), tplBoot],
1394
- [path.join(memDir, '01-agent-workflow.md'), tplWorkflow],
1395
- [path.join(memDir, '03-decisions.md'), tplDecisions],
1396
- [path.join(memDir, '04-handoff.md'), tplHandoff],
1397
- [path.join(memDir, '05-done-checklist.md'), tplDoneChecklist],
1398
- [path.join(memDir, '06-project-rules.md'), tplProjectRules],
1399
- [path.join(memDir, 'log.md'), tplLog],
1598
+ const p = scanProject(dir);
1599
+ const memDir = path.join(dir, '.memoc');
1600
+ const isNew = !fs.existsSync(path.join(memDir, 'boot.md'));
1601
+ const mode = (isNew && !forceUpdate) ? 'init' : 'update';
1602
+
1603
+ const log = [];
1604
+ const mark = (label, name) => log.push(` ${label.padEnd(8)} ${name}`);
1605
+
1606
+ if (mode === 'init') {
1607
+ console.log(`\n memoc init — ${path.basename(dir)}`);
1608
+ console.log(p.isEmpty
1609
+ ? ' Empty project → using default values.'
1610
+ : ` Detected: ${stackStr(p.stack)}`
1611
+ );
1612
+ console.log();
1613
+
1614
+ // Entry files — inject/update managed block, preserve existing user content
1615
+ mark(applyManagedBlock(path.join(dir, 'CLAUDE.md'), tplClaude), 'CLAUDE.md');
1616
+ mark(applyManagedBlock(path.join(dir, 'AGENTS.md'), tplAgents), 'AGENTS.md');
1617
+ if (ensure(path.join(dir, 'llms.txt'), tplLlmsTxt(p))) mark('add', 'llms.txt');
1618
+ else mark('skip', 'llms.txt');
1619
+
1620
+ // Dynamic memory files
1621
+ const dynamicFiles = [
1622
+ [path.join(memDir, '00-project-brief.md'), () => tplProjectBrief(p)],
1623
+ [path.join(memDir, '00-agent-index.md'), () => tplAgentIndex(p)],
1624
+ [path.join(memDir, '02-current-project-state.md'), () => tplCurrentState(p)],
1625
+ [path.join(memDir, 'session-summary.md'), tplSessionSummary],
1626
+ ];
1627
+ for (const [fp, tpl] of dynamicFiles) {
1628
+ const rel = path.relative(dir, fp);
1629
+ if (ensure(fp, tpl())) mark('add', rel); else mark('skip', rel);
1630
+ }
1631
+
1632
+ // Static memory files
1633
+ const staticFiles = [
1634
+ [path.join(memDir, 'boot.md'), tplBoot],
1635
+ [path.join(memDir, '01-agent-workflow.md'), tplWorkflow],
1636
+ [path.join(memDir, '03-decisions.md'), tplDecisions],
1637
+ [path.join(memDir, '04-handoff.md'), tplHandoff],
1638
+ [path.join(memDir, '05-done-checklist.md'), tplDoneChecklist],
1639
+ [path.join(memDir, '06-project-rules.md'), tplProjectRules],
1640
+ [path.join(memDir, 'log.md'), tplLog],
1400
1641
  [path.join(memDir, 'memoc-usage.md'), tplMemocUsage],
1401
- [path.join(memDir, 'systems/README.md'), tplSystemsReadme],
1402
- [path.join(memDir, 'wiki/index.md'), tplWikiIndex],
1403
- [path.join(memDir, 'wiki/sources.md'), tplWikiSources],
1404
- [path.join(memDir, 'wiki/glossary.md'), tplWikiGlossary],
1405
- [path.join(memDir, 'wiki/questions.md'), tplWikiQuestions],
1406
- [path.join(memDir, 'wiki/lint.md'), tplWikiLint],
1407
- [path.join(memDir, 'wiki/sources/README.md'), tplWikiSourcesReadme],
1408
- [path.join(memDir, 'wiki/topics/README.md'), tplWikiTopicsReadme],
1409
- [path.join(memDir, 'wiki/global/README.md'), tplWikiGlobalReadme],
1410
- [path.join(dir, 'skills/project-memory-maintainer/SKILL.md'), tplSkillMaintainer],
1411
- ];
1412
- for (const [fp, tpl] of staticFiles) {
1413
- const rel = path.relative(dir, fp);
1414
- if (ensure(fp, tpl())) mark('add', rel); else mark('skip', rel);
1415
- }
1416
-
1417
- // Claude Code Stop hook — writes .memoc/.pending when git detects changes
1642
+ [path.join(memDir, 'systems/README.md'), tplSystemsReadme],
1643
+ [path.join(memDir, 'wiki/index.md'), tplWikiIndex],
1644
+ [path.join(memDir, 'wiki/sources.md'), tplWikiSources],
1645
+ [path.join(memDir, 'wiki/glossary.md'), tplWikiGlossary],
1646
+ [path.join(memDir, 'wiki/questions.md'), tplWikiQuestions],
1647
+ [path.join(memDir, 'wiki/lint.md'), tplWikiLint],
1648
+ [path.join(memDir, 'wiki/sources/README.md'), tplWikiSourcesReadme],
1649
+ [path.join(memDir, 'wiki/topics/README.md'), tplWikiTopicsReadme],
1650
+ [path.join(memDir, 'wiki/global/README.md'), tplWikiGlobalReadme],
1651
+ [path.join(dir, 'skills/project-memory-maintainer/SKILL.md'), tplSkillMaintainer],
1652
+ ];
1653
+ for (const [fp, tpl] of staticFiles) {
1654
+ const rel = path.relative(dir, fp);
1655
+ if (ensure(fp, tpl())) mark('add', rel); else mark('skip', rel);
1656
+ }
1657
+
1658
+ // Claude Code Stop hook — writes .memoc/.pending when git detects changes
1418
1659
  ensureClaudeStopHookFile(dir, mark);
1419
-
1420
- // .gitignore — add .memoc/.pending if not already present
1660
+
1661
+ // .gitignore — add .memoc/.pending if not already present
1421
1662
  ensurePendingGitignore(dir, mark);
1422
-
1423
- // PATH helpers — let agents run memoc even when the npm bin is not on PATH
1663
+
1664
+ // PATH helpers — let agents run memoc even when the npm bin is not on PATH
1424
1665
  ensurePathHelpers(dir, mark);
1425
- ensurePathRegistration(dir, mark);
1426
-
1427
- } else {
1428
- // ── UPDATE MODE
1666
+ ensurePathRegistration(dir, mark);
1667
+
1668
+ } else {
1669
+ // ── UPDATE MODE
1429
1670
  console.log(`\n memoc ${action} — ${path.basename(dir)}`);
1430
- console.log(` Re-scanning project: ${p.isEmpty ? 'nothing detected' : stackStr(p.stack)}`);
1431
- console.log();
1432
-
1433
- // Entry files — update managed blocks, preserve user content
1434
- mark(applyManagedBlock(path.join(dir, 'CLAUDE.md'), tplClaude), 'CLAUDE.md');
1435
- mark(applyManagedBlock(path.join(dir, 'AGENTS.md'), tplAgents), 'AGENTS.md');
1436
-
1437
- // Third-party agent files — update only if already added
1438
- for (const [, agent] of Object.entries(AGENT_REGISTRY)) {
1439
- const fp = path.join(dir, agent.file);
1440
- if (fs.existsSync(fp)) {
1441
- mark(applyManagedBlock(fp, () => tplAgentEntry(agent.label)), agent.file);
1442
- }
1443
- }
1444
-
1445
- // llms.txt — update all managed sections
1446
- const llmsPath = path.join(dir, 'llms.txt');
1447
- if (fs.existsSync(llmsPath)) {
1448
- updateSection(llmsPath, HDR_S, HDR_E, headerInner(p));
1449
- updateSection(llmsPath, CORE_S, CORE_E, coreLlmsInner());
1450
- updateSection(llmsPath, SYS_S, SYS_E, systemsLlmsInner(dir));
1451
- updateSection(llmsPath, WIKI_S, WIKI_E, wikiLlmsInner(dir));
1452
- mark('update', 'llms.txt');
1453
- } else {
1454
- write(llmsPath, tplLlmsTxt(p));
1455
- mark('add', 'llms.txt');
1456
- }
1457
-
1458
- // Dynamic memory files — update managed sections only
1459
- const dynUpdates = [
1460
- [path.join(memDir, '00-project-brief.md'), () => tplProjectBrief(p), ID_S, ID_E, identityInner(p)],
1461
- [path.join(memDir, '00-agent-index.md'), () => tplAgentIndex(p), SNAP_S, SNAP_E, snapshotInner(p)],
1462
- [path.join(memDir, '02-current-project-state.md'), () => tplCurrentState(p), SNAP_S, SNAP_E, snapshotInner(p)],
1463
- ];
1464
- for (const [fp, tpl, s, e, inner] of dynUpdates) {
1465
- const rel = path.relative(dir, fp);
1466
- if (!fs.existsSync(fp)) {
1467
- write(fp, tpl());
1468
- mark('add', rel);
1469
- } else if (updateSection(fp, s, e, inner)) {
1470
- mark('update', `${rel} (managed section)`);
1471
- } else {
1472
- mark('skip', rel);
1473
- }
1474
- }
1475
-
1476
- // session-summary is agent-owned — never overwrite, only add if missing
1477
- const summaryPath = path.join(memDir, 'session-summary.md');
1478
- if (fs.existsSync(summaryPath)) {
1479
- const summarySize = Buffer.byteLength(fs.readFileSync(summaryPath, 'utf8'), 'utf8');
1480
- if (summarySize > 1000) {
1481
- console.log(` ⚠ session-summary.md is ${summarySize}B (recommended: <800B).`);
1482
- }
1483
- mark('skip', '.memoc/session-summary.md (agent-owned, not modified)');
1484
- } else {
1485
- write(summaryPath, tplSessionSummary());
1486
- mark('add', '.memoc/session-summary.md');
1487
- }
1488
-
1489
- // Static + user-owned files — only add if missing
1490
- const addIfMissing = [
1491
- [path.join(memDir, 'boot.md'), tplBoot],
1492
- [path.join(memDir, '01-agent-workflow.md'), tplWorkflow],
1493
- [path.join(memDir, '03-decisions.md'), tplDecisions],
1494
- [path.join(memDir, '04-handoff.md'), tplHandoff],
1495
- [path.join(memDir, '05-done-checklist.md'), tplDoneChecklist],
1496
- [path.join(memDir, '06-project-rules.md'), tplProjectRules],
1497
- [path.join(memDir, 'log.md'), tplLog],
1671
+ console.log(` Re-scanning project: ${p.isEmpty ? 'nothing detected' : stackStr(p.stack)}`);
1672
+ console.log();
1673
+
1674
+ // Entry files — update managed blocks, preserve user content
1675
+ mark(applyManagedBlock(path.join(dir, 'CLAUDE.md'), tplClaude), 'CLAUDE.md');
1676
+ mark(applyManagedBlock(path.join(dir, 'AGENTS.md'), tplAgents), 'AGENTS.md');
1677
+
1678
+ // Third-party agent files — update only if already added
1679
+ for (const [, agent] of Object.entries(AGENT_REGISTRY)) {
1680
+ const fp = path.join(dir, agent.file);
1681
+ if (fs.existsSync(fp)) {
1682
+ mark(applyManagedBlock(fp, () => tplAgentEntry(agent.label)), agent.file);
1683
+ }
1684
+ }
1685
+
1686
+ // llms.txt — update all managed sections
1687
+ const llmsPath = path.join(dir, 'llms.txt');
1688
+ if (fs.existsSync(llmsPath)) {
1689
+ updateSection(llmsPath, HDR_S, HDR_E, headerInner(p));
1690
+ updateSection(llmsPath, CORE_S, CORE_E, coreLlmsInner());
1691
+ updateSection(llmsPath, SYS_S, SYS_E, systemsLlmsInner(dir));
1692
+ updateSection(llmsPath, WIKI_S, WIKI_E, wikiLlmsInner(dir));
1693
+ mark('update', 'llms.txt');
1694
+ } else {
1695
+ write(llmsPath, tplLlmsTxt(p));
1696
+ mark('add', 'llms.txt');
1697
+ }
1698
+
1699
+ // Dynamic memory files — update managed sections only
1700
+ const dynUpdates = [
1701
+ [path.join(memDir, '00-project-brief.md'), () => tplProjectBrief(p), ID_S, ID_E, identityInner(p)],
1702
+ [path.join(memDir, '00-agent-index.md'), () => tplAgentIndex(p), SNAP_S, SNAP_E, snapshotInner(p)],
1703
+ [path.join(memDir, '02-current-project-state.md'), () => tplCurrentState(p), SNAP_S, SNAP_E, snapshotInner(p)],
1704
+ ];
1705
+ for (const [fp, tpl, s, e, inner] of dynUpdates) {
1706
+ const rel = path.relative(dir, fp);
1707
+ if (!fs.existsSync(fp)) {
1708
+ write(fp, tpl());
1709
+ mark('add', rel);
1710
+ } else if (updateSection(fp, s, e, inner)) {
1711
+ mark('update', `${rel} (managed section)`);
1712
+ } else {
1713
+ mark('skip', rel);
1714
+ }
1715
+ }
1716
+
1717
+ // session-summary is agent-owned — never overwrite, only add if missing
1718
+ const summaryPath = path.join(memDir, 'session-summary.md');
1719
+ if (fs.existsSync(summaryPath)) {
1720
+ const summarySize = Buffer.byteLength(fs.readFileSync(summaryPath, 'utf8'), 'utf8');
1721
+ if (summarySize > 1000) {
1722
+ console.log(` ⚠ session-summary.md is ${summarySize}B (recommended: <800B).`);
1723
+ }
1724
+ mark('skip', '.memoc/session-summary.md (agent-owned, not modified)');
1725
+ } else {
1726
+ write(summaryPath, tplSessionSummary());
1727
+ mark('add', '.memoc/session-summary.md');
1728
+ }
1729
+
1730
+ // Static + user-owned files — only add if missing
1731
+ const addIfMissing = [
1732
+ [path.join(memDir, 'boot.md'), tplBoot],
1733
+ [path.join(memDir, '01-agent-workflow.md'), tplWorkflow],
1734
+ [path.join(memDir, '03-decisions.md'), tplDecisions],
1735
+ [path.join(memDir, '04-handoff.md'), tplHandoff],
1736
+ [path.join(memDir, '05-done-checklist.md'), tplDoneChecklist],
1737
+ [path.join(memDir, '06-project-rules.md'), tplProjectRules],
1738
+ [path.join(memDir, 'log.md'), tplLog],
1498
1739
  [path.join(memDir, 'memoc-usage.md'), tplMemocUsage],
1499
- [path.join(memDir, 'systems/README.md'), tplSystemsReadme],
1500
- [path.join(memDir, 'wiki/index.md'), tplWikiIndex],
1501
- [path.join(memDir, 'wiki/sources.md'), tplWikiSources],
1502
- [path.join(memDir, 'wiki/glossary.md'), tplWikiGlossary],
1503
- [path.join(memDir, 'wiki/questions.md'), tplWikiQuestions],
1504
- [path.join(memDir, 'wiki/lint.md'), tplWikiLint],
1505
- [path.join(memDir, 'wiki/sources/README.md'), tplWikiSourcesReadme],
1506
- [path.join(memDir, 'wiki/topics/README.md'), tplWikiTopicsReadme],
1507
- [path.join(memDir, 'wiki/global/README.md'), tplWikiGlobalReadme],
1508
- [path.join(dir, 'skills/project-memory-maintainer/SKILL.md'), tplSkillMaintainer],
1509
- ];
1510
- for (const [fp, tpl] of addIfMissing) {
1511
- const rel = path.relative(dir, fp);
1512
- if (ensure(fp, tpl())) mark('add', rel);
1513
- // silently skip existing — user/agent owns them
1514
- }
1515
-
1516
- // PATH helpers — let agents run memoc even when the npm bin is not on PATH
1740
+ [path.join(memDir, 'systems/README.md'), tplSystemsReadme],
1741
+ [path.join(memDir, 'wiki/index.md'), tplWikiIndex],
1742
+ [path.join(memDir, 'wiki/sources.md'), tplWikiSources],
1743
+ [path.join(memDir, 'wiki/glossary.md'), tplWikiGlossary],
1744
+ [path.join(memDir, 'wiki/questions.md'), tplWikiQuestions],
1745
+ [path.join(memDir, 'wiki/lint.md'), tplWikiLint],
1746
+ [path.join(memDir, 'wiki/sources/README.md'), tplWikiSourcesReadme],
1747
+ [path.join(memDir, 'wiki/topics/README.md'), tplWikiTopicsReadme],
1748
+ [path.join(memDir, 'wiki/global/README.md'), tplWikiGlobalReadme],
1749
+ [path.join(dir, 'skills/project-memory-maintainer/SKILL.md'), tplSkillMaintainer],
1750
+ ];
1751
+ for (const [fp, tpl] of addIfMissing) {
1752
+ const rel = path.relative(dir, fp);
1753
+ if (ensure(fp, tpl())) mark('add', rel);
1754
+ // silently skip existing — user/agent owns them
1755
+ }
1756
+ ensureWikiScaffoldLinks(memDir, mark);
1757
+
1758
+ // PATH helpers — let agents run memoc even when the npm bin is not on PATH
1517
1759
  ensureClaudeStopHookFile(dir, mark);
1518
1760
  ensurePendingGitignore(dir, mark);
1519
1761
  ensurePathHelpers(dir, mark);
1520
1762
  ensurePathRegistration(dir, mark);
1521
1763
 
1522
1764
  // Append update record to log.md
1523
- const logPath = path.join(memDir, 'log.md');
1524
- if (fs.existsSync(logPath)) {
1525
- fs.appendFileSync(logPath,
1765
+ const logPath = path.join(memDir, 'log.md');
1766
+ if (fs.existsSync(logPath)) {
1767
+ fs.appendFileSync(logPath,
1526
1768
  `\n## [${nowISO()}] ${action} | Re-scanned: ${p.isEmpty ? 'nothing detected' : stackStr(p.stack)}\n`,
1527
- 'utf8'
1528
- );
1529
- mark('append', '.memoc/log.md');
1530
- }
1531
- }
1532
-
1769
+ 'utf8'
1770
+ );
1771
+ mark('append', '.memoc/log.md');
1772
+ }
1773
+ }
1774
+
1533
1775
  hideOnWindows(memDir);
1534
1776
  console.log(log.join('\n'));
1535
1777
  printCommandHint();
1536
1778
  console.log('\n Done.');
1537
1779
  }
1538
-
1539
- // ═══════════════════════════════════════════════════════════════════
1540
- // ADD — add entry file for a specific agent
1541
- // ═══════════════════════════════════════════════════════════════════
1542
-
1543
- function runAdd(dir) {
1544
- const agentKey = (process.argv[3] || '').toLowerCase();
1545
-
1546
- if (!agentKey) {
1547
- console.log('\n Available agents:\n');
1548
- for (const [key, agent] of Object.entries(AGENT_REGISTRY)) {
1549
- const exists = fs.existsSync(path.join(dir, agent.file)) ? ' (already added)' : '';
1550
- console.log(` ${key.padEnd(10)} → ${agent.file}${exists}`);
1551
- }
1552
- console.log('\n Usage: memoc add <agent>');
1553
- return;
1554
- }
1555
-
1556
- const agent = AGENT_REGISTRY[agentKey];
1557
- if (!agent) {
1558
- console.error(`\n Unknown agent: "${agentKey}"`);
1559
- console.error(` Available: ${Object.keys(AGENT_REGISTRY).join(', ')}`);
1560
- process.exit(1);
1561
- }
1562
-
1563
- const filePath = path.join(dir, agent.file);
1564
- const result = applyManagedBlock(filePath, () => tplAgentEntry(agent.label));
1565
- console.log(`\n ${result.padEnd(8)} ${agent.file} (${agent.label})`);
1566
- console.log('\n Done.');
1567
- }
1568
-
1569
- // ═══════════════════════════════════════════════════════════════════
1570
- // SEARCH
1571
- // ═══════════════════════════════════════════════════════════════════
1572
-
1573
- function runSearch(dir, scope = 'memory') {
1574
- const rawArgs = process.argv.slice(3);
1575
- const opts = { mode: 'files', limit: 12, all: false };
1576
- const queryParts = [];
1577
-
1578
- for (let i = 0; i < rawArgs.length; i++) {
1579
- const arg = rawArgs[i];
1580
- if (arg === '--snippets') { opts.mode = 'snippets'; continue; }
1581
- if (arg === '--files') { opts.mode = 'files'; continue; }
1582
- if (arg === '--all') { opts.all = true; continue; }
1583
- if (arg === '--limit') {
1584
- const n = Number(rawArgs[++i]);
1585
- if (Number.isFinite(n) && n > 0) opts.limit = Math.floor(n);
1586
- continue;
1587
- }
1588
- if (arg.startsWith('--limit=')) {
1589
- const n = Number(arg.slice('--limit='.length));
1590
- if (Number.isFinite(n) && n > 0) opts.limit = Math.floor(n);
1591
- continue;
1592
- }
1593
- queryParts.push(arg);
1594
- }
1595
-
1596
- const query = queryParts.join(' ').toLowerCase();
1597
-
1598
- const searchRoots = scope === 'project' ? [dir] : memorySearchRoots(dir);
1599
-
1600
- if (!query) {
1601
- // No query — list searchable files sorted by recency
1602
- const allFiles = [];
1603
- function collectFile(fp) {
1604
- if (!fs.existsSync(fp)) return;
1605
- const rel = path.relative(dir, fp);
1606
- let mtime = 0;
1607
- try { mtime = fs.statSync(fp).mtimeMs; } catch {}
1608
- allFiles.push({ file: rel, mtime });
1609
- }
1610
- function collectDir(d) {
1611
- if (!fs.existsSync(d)) return;
1612
- for (const entry of fs.readdirSync(d)) {
1613
- const fp = path.join(d, entry);
1614
- try {
1615
- const st = fs.statSync(fp);
1616
- if (st.isDirectory()) {
1780
+
1781
+ // ═══════════════════════════════════════════════════════════════════
1782
+ // ADD — add entry file for a specific agent
1783
+ // ═══════════════════════════════════════════════════════════════════
1784
+
1785
+ function runAdd(dir) {
1786
+ const agentKey = (process.argv[3] || '').toLowerCase();
1787
+
1788
+ if (!agentKey) {
1789
+ console.log('\n Available agents:\n');
1790
+ for (const [key, agent] of Object.entries(AGENT_REGISTRY)) {
1791
+ const exists = fs.existsSync(path.join(dir, agent.file)) ? ' (already added)' : '';
1792
+ console.log(` ${key.padEnd(10)} → ${agent.file}${exists}`);
1793
+ }
1794
+ console.log('\n Usage: memoc add <agent>');
1795
+ return;
1796
+ }
1797
+
1798
+ const agent = AGENT_REGISTRY[agentKey];
1799
+ if (!agent) {
1800
+ console.error(`\n Unknown agent: "${agentKey}"`);
1801
+ console.error(` Available: ${Object.keys(AGENT_REGISTRY).join(', ')}`);
1802
+ process.exit(1);
1803
+ }
1804
+
1805
+ const filePath = path.join(dir, agent.file);
1806
+ const result = applyManagedBlock(filePath, () => tplAgentEntry(agent.label));
1807
+ console.log(`\n ${result.padEnd(8)} ${agent.file} (${agent.label})`);
1808
+ console.log('\n Done.');
1809
+ }
1810
+
1811
+ // ═══════════════════════════════════════════════════════════════════
1812
+ // SEARCH
1813
+ // ═══════════════════════════════════════════════════════════════════
1814
+
1815
+ function runSearch(dir, scope = 'memory') {
1816
+ const rawArgs = process.argv.slice(3);
1817
+ const opts = { mode: 'files', limit: 12, all: false };
1818
+ const queryParts = [];
1819
+
1820
+ for (let i = 0; i < rawArgs.length; i++) {
1821
+ const arg = rawArgs[i];
1822
+ if (arg === '--snippets') { opts.mode = 'snippets'; continue; }
1823
+ if (arg === '--files') { opts.mode = 'files'; continue; }
1824
+ if (arg === '--all') { opts.all = true; continue; }
1825
+ if (arg === '--limit') {
1826
+ const n = Number(rawArgs[++i]);
1827
+ if (Number.isFinite(n) && n > 0) opts.limit = Math.floor(n);
1828
+ continue;
1829
+ }
1830
+ if (arg.startsWith('--limit=')) {
1831
+ const n = Number(arg.slice('--limit='.length));
1832
+ if (Number.isFinite(n) && n > 0) opts.limit = Math.floor(n);
1833
+ continue;
1834
+ }
1835
+ queryParts.push(arg);
1836
+ }
1837
+
1838
+ const query = queryParts.join(' ').toLowerCase();
1839
+
1840
+ const searchRoots = scope === 'project' ? [dir] : memorySearchRoots(dir);
1841
+
1842
+ if (!query) {
1843
+ // No query — list searchable files sorted by recency
1844
+ const allFiles = [];
1845
+ function collectFile(fp) {
1846
+ if (!fs.existsSync(fp)) return;
1847
+ const rel = path.relative(dir, fp);
1848
+ let mtime = 0;
1849
+ try { mtime = fs.statSync(fp).mtimeMs; } catch {}
1850
+ allFiles.push({ file: rel, mtime });
1851
+ }
1852
+ function collectDir(d) {
1853
+ if (!fs.existsSync(d)) return;
1854
+ for (const entry of fs.readdirSync(d)) {
1855
+ const fp = path.join(d, entry);
1856
+ try {
1857
+ const st = fs.statSync(fp);
1858
+ if (st.isDirectory()) {
1617
1859
  if (!shouldSkipSearchDir(entry, scope)) collectDir(fp);
1618
- } else if (isSearchableFile(fp, entry, st, scope)) collectFile(fp);
1619
- } catch {}
1620
- }
1621
- }
1622
- for (const root of searchRoots) {
1623
- try {
1624
- if (fs.statSync(root).isDirectory()) collectDir(root);
1625
- else collectFile(root);
1626
- } catch {}
1627
- }
1628
- allFiles.sort((a, b) => b.mtime - a.mtime || a.file.localeCompare(b.file));
1629
- const limited = opts.all ? allFiles : allFiles.slice(0, opts.limit);
1630
- console.log(limited.map(r => r.file).join('\n'));
1631
- if (!opts.all && allFiles.length > limited.length) {
1632
- console.log(`... ${allFiles.length - limited.length} more files. Use --all to show all.`);
1633
- }
1634
- return;
1635
- }
1636
-
1637
- const matchesByFile = new Map(); // rel -> { matches: [], mtime: number }
1638
-
1639
- function searchFile(fp) {
1640
- if (!fs.existsSync(fp)) return;
1641
- const rel = path.relative(dir, fp);
1642
- let mtime = 0;
1643
- try {
1644
- const st = fs.statSync(fp);
1645
- if (!isSearchableFile(fp, path.basename(fp), st, scope)) return;
1646
- mtime = st.mtimeMs;
1647
- } catch {}
1648
- const lines = fs.readFileSync(fp, 'utf8').split('\n');
1649
- lines.forEach((line, i) => {
1650
- if (line.toLowerCase().includes(query)) {
1651
- if (!matchesByFile.has(rel)) matchesByFile.set(rel, { matches: [], mtime });
1652
- matchesByFile.get(rel).matches.push({ line: i + 1, text: line.trim() });
1653
- }
1654
- });
1655
- }
1656
-
1657
- function walkDir(d) {
1658
- if (!fs.existsSync(d)) return;
1659
- for (const entry of fs.readdirSync(d)) {
1660
- const fp = path.join(d, entry);
1661
- try {
1662
- const st = fs.statSync(fp);
1860
+ } else if (isSearchableFile(fp, entry, st, scope)) collectFile(fp);
1861
+ } catch {}
1862
+ }
1863
+ }
1864
+ for (const root of searchRoots) {
1865
+ try {
1866
+ if (fs.statSync(root).isDirectory()) collectDir(root);
1867
+ else collectFile(root);
1868
+ } catch {}
1869
+ }
1870
+ allFiles.sort((a, b) => b.mtime - a.mtime || a.file.localeCompare(b.file));
1871
+ const limited = opts.all ? allFiles : allFiles.slice(0, opts.limit);
1872
+ console.log(limited.map(r => r.file).join('\n'));
1873
+ if (!opts.all && allFiles.length > limited.length) {
1874
+ console.log(`... ${allFiles.length - limited.length} more files. Use --all to show all.`);
1875
+ }
1876
+ return;
1877
+ }
1878
+
1879
+ const matchesByFile = new Map(); // rel -> { matches: [], mtime: number }
1880
+
1881
+ function searchFile(fp) {
1882
+ if (!fs.existsSync(fp)) return;
1883
+ const rel = path.relative(dir, fp);
1884
+ let mtime = 0;
1885
+ try {
1886
+ const st = fs.statSync(fp);
1887
+ if (!isSearchableFile(fp, path.basename(fp), st, scope)) return;
1888
+ mtime = st.mtimeMs;
1889
+ } catch {}
1890
+ const lines = fs.readFileSync(fp, 'utf8').split('\n');
1891
+ lines.forEach((line, i) => {
1892
+ if (line.toLowerCase().includes(query)) {
1893
+ if (!matchesByFile.has(rel)) matchesByFile.set(rel, { matches: [], mtime });
1894
+ matchesByFile.get(rel).matches.push({ line: i + 1, text: line.trim() });
1895
+ }
1896
+ });
1897
+ }
1898
+
1899
+ function walkDir(d) {
1900
+ if (!fs.existsSync(d)) return;
1901
+ for (const entry of fs.readdirSync(d)) {
1902
+ const fp = path.join(d, entry);
1903
+ try {
1904
+ const st = fs.statSync(fp);
1663
1905
  if (st.isDirectory()) {
1664
1906
  if (!shouldSkipSearchDir(entry, scope)) walkDir(fp);
1665
- } else if (isSearchableFile(fp, entry, st, scope)) searchFile(fp);
1666
- } catch {}
1667
- }
1668
- }
1669
-
1670
- for (const root of searchRoots) {
1671
- try {
1672
- if (fs.statSync(root).isDirectory()) walkDir(root);
1673
- else searchFile(root);
1674
- } catch {}
1675
- }
1676
-
1677
- if (!matchesByFile.size) {
1678
- console.log('No matches found.');
1679
- } else if (opts.mode === 'files') {
1907
+ } else if (isSearchableFile(fp, entry, st, scope)) searchFile(fp);
1908
+ } catch {}
1909
+ }
1910
+ }
1911
+
1912
+ for (const root of searchRoots) {
1913
+ try {
1914
+ if (fs.statSync(root).isDirectory()) walkDir(root);
1915
+ else searchFile(root);
1916
+ } catch {}
1917
+ }
1918
+
1919
+ if (!matchesByFile.size) {
1920
+ console.log('No matches found.');
1921
+ } else if (opts.mode === 'files') {
1680
1922
  const rows = [...matchesByFile.entries()]
1681
1923
  .map(([file, { matches, mtime }]) => ({ file, count: matches.length, mtime }))
1682
1924
  .sort((a, b) =>
@@ -1685,12 +1927,12 @@ function runSearch(dir, scope = 'memory') {
1685
1927
  b.mtime - a.mtime ||
1686
1928
  a.file.localeCompare(b.file)
1687
1929
  );
1688
- const limited = opts.all ? rows : rows.slice(0, opts.limit);
1689
- console.log(limited.map(r => `${r.file} ${r.count} match${r.count === 1 ? '' : 'es'}`).join('\n'));
1690
- if (!opts.all && rows.length > limited.length) {
1691
- console.log(`... ${rows.length - limited.length} more files. Use --all to show all, or --snippets for line matches.`);
1692
- }
1693
- } else {
1930
+ const limited = opts.all ? rows : rows.slice(0, opts.limit);
1931
+ console.log(limited.map(r => `${r.file} ${r.count} match${r.count === 1 ? '' : 'es'}`).join('\n'));
1932
+ if (!opts.all && rows.length > limited.length) {
1933
+ console.log(`... ${rows.length - limited.length} more files. Use --all to show all, or --snippets for line matches.`);
1934
+ }
1935
+ } else {
1694
1936
  const snippets = [];
1695
1937
  for (const [file, { matches }] of matchesByFile.entries()) {
1696
1938
  for (const m of matches) snippets.push({ file, line: m.line, text: m.text });
@@ -1702,23 +1944,23 @@ function runSearch(dir, scope = 'memory') {
1702
1944
  );
1703
1945
  const limited = opts.all ? snippets : snippets.slice(0, opts.limit);
1704
1946
  console.log(limited.map(m => `${m.file}:${m.line} ${m.text}`).join('\n'));
1705
- if (!opts.all && snippets.length > limited.length) {
1706
- console.log(`... ${snippets.length - limited.length} more matches. Use --all to show all, or --limit N.`);
1707
- }
1708
- }
1709
- }
1710
-
1711
- function memorySearchRoots(dir) {
1712
- return [
1713
- path.join(dir, '.memoc'),
1714
- path.join(dir, 'skills'),
1715
- path.join(dir, 'llms.txt'),
1716
- path.join(dir, 'AGENTS.md'),
1717
- path.join(dir, 'CLAUDE.md'),
1718
- ...Object.values(AGENT_REGISTRY).map(agent => path.join(dir, agent.file)),
1719
- ];
1720
- }
1721
-
1947
+ if (!opts.all && snippets.length > limited.length) {
1948
+ console.log(`... ${snippets.length - limited.length} more matches. Use --all to show all, or --limit N.`);
1949
+ }
1950
+ }
1951
+ }
1952
+
1953
+ function memorySearchRoots(dir) {
1954
+ return [
1955
+ path.join(dir, '.memoc'),
1956
+ path.join(dir, 'skills'),
1957
+ path.join(dir, 'llms.txt'),
1958
+ path.join(dir, 'AGENTS.md'),
1959
+ path.join(dir, 'CLAUDE.md'),
1960
+ ...Object.values(AGENT_REGISTRY).map(agent => path.join(dir, agent.file)),
1961
+ ];
1962
+ }
1963
+
1722
1964
  function shouldSkipSearchDir(name, scope = 'memory') {
1723
1965
  const skipped = new Set([
1724
1966
  '.git', 'node_modules', '.next', 'dist', 'build', 'out', 'coverage',
@@ -1739,16 +1981,16 @@ function isSearchableFile(fp, name, st, scope = 'memory') {
1739
1981
  if (scope === 'project' && isAgentMemoryFile(name)) return false;
1740
1982
  if (name === 'llms.txt' || name.endsWith('rules')) return true;
1741
1983
  const ext = path.extname(fp).toLowerCase();
1742
- if (scope === 'memory') {
1743
- return new Set(['.md', '.txt']).has(ext);
1744
- }
1745
- return new Set([
1746
- '.md', '.txt', '.json', '.jsonc', '.yaml', '.yml', '.toml', '.ini', '.env',
1747
- '.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs',
1748
- '.py', '.rs', '.go', '.java', '.cs', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.hxx',
1749
- '.html', '.css', '.scss', '.sass', '.vue', '.svelte',
1750
- '.sql', '.graphql', '.gql', '.sh', '.bash', '.zsh', '.ps1', '.bat', '.cmd',
1751
- '.xml', '.gradle', '.kts', '.cmake',
1984
+ if (scope === 'memory') {
1985
+ return new Set(['.md', '.txt']).has(ext);
1986
+ }
1987
+ return new Set([
1988
+ '.md', '.txt', '.json', '.jsonc', '.yaml', '.yml', '.toml', '.ini', '.env',
1989
+ '.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs',
1990
+ '.py', '.rs', '.go', '.java', '.cs', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.hxx',
1991
+ '.html', '.css', '.scss', '.sass', '.vue', '.svelte',
1992
+ '.sql', '.graphql', '.gql', '.sh', '.bash', '.zsh', '.ps1', '.bat', '.cmd',
1993
+ '.xml', '.gradle', '.kts', '.cmake',
1752
1994
  ]).has(ext);
1753
1995
  }
1754
1996
 
@@ -1787,206 +2029,206 @@ function searchPriority(file, scope = 'memory') {
1787
2029
  if (normalized.startsWith('skills/')) return 40;
1788
2030
  return 50;
1789
2031
  }
1790
-
1791
- // ═══════════════════════════════════════════════════════════════════
1792
- // TOKENS — estimate token cost of current memory state
1793
- // ═══════════════════════════════════════════════════════════════════
1794
-
1795
- function runTokens(dir) {
1796
- const est = text => Math.ceil(Buffer.byteLength(text, 'utf8') / 4);
1797
- const read = fp => { try { return fs.readFileSync(fp, 'utf8'); } catch { return ''; } };
1798
- const memDir = path.join(dir, '.memoc');
1799
-
1800
- const startup = [
1801
- ['CLAUDE.md', path.join(dir, 'CLAUDE.md')],
1802
- ['session-summary.md', path.join(memDir, 'session-summary.md')],
1803
- ];
1804
- const onDemand = [
1805
- ['llms.txt', path.join(dir, 'llms.txt')],
1806
- ['02-current-project-state.md', path.join(memDir, '02-current-project-state.md')],
1807
- ['03-decisions.md', path.join(memDir, '03-decisions.md')],
1808
- ['04-handoff.md', path.join(memDir, '04-handoff.md')],
1809
- ['06-project-rules.md', path.join(memDir, '06-project-rules.md')],
1810
- ['log.md', path.join(memDir, 'log.md')],
1811
- ];
1812
-
1813
- console.log('\n memoc tokens\n');
1814
- let startupTotal = 0;
1815
- console.log(' Startup (always loaded):');
1816
- for (const [name, fp] of startup) {
1817
- const content = read(fp);
1818
- const t = est(content);
1819
- const b = Buffer.byteLength(content, 'utf8');
1820
- startupTotal += t;
1821
- const warn = b > 1000 ? ' ⚠ large' : '';
1822
- console.log(` ${name.padEnd(32)} ${String(t).padStart(5)} tokens (${b}B)${warn}`);
1823
- }
1824
- console.log(` ${'── startup total'.padEnd(32)} ${String(startupTotal).padStart(5)} tokens`);
1825
-
1826
- console.log('\n On-demand (read when needed):');
1827
- let onDemandTotal = 0;
1828
- for (const [name, fp] of onDemand) {
1829
- const content = read(fp);
1830
- if (!content) continue;
1831
- const t = est(content);
1832
- const b = Buffer.byteLength(content, 'utf8');
1833
- onDemandTotal += t;
1834
- const warn = t > 500 ? ' ⚠ consider compress' : '';
1835
- console.log(` ${name.padEnd(32)} ${String(t).padStart(5)} tokens (${b}B)${warn}`);
1836
- }
1837
- console.log(` ${'── on-demand total'.padEnd(32)} ${String(onDemandTotal).padStart(5)} tokens`);
1838
- console.log(`\n If all loaded: ~${startupTotal + onDemandTotal} tokens`);
1839
-
1840
- const summaryContent = read(path.join(memDir, 'session-summary.md'));
1841
- const summaryBytes = Buffer.byteLength(summaryContent, 'utf8');
1842
- if (summaryBytes > 800) {
1843
- console.log(`\n ⚠ session-summary.md is ${summaryBytes}B — recommended <800B. Trim it manually.`);
1844
- }
1845
- console.log();
1846
- }
1847
-
1848
- // ═══════════════════════════════════════════════════════════════════
1849
- // COMPRESS — archive old log.md entries to keep file small
1850
- // ═══════════════════════════════════════════════════════════════════
1851
-
1852
- function runCompress(dir) {
1853
- const KEEP = 20;
1854
- const logPath = path.join(dir, '.memoc', 'log.md');
1855
- const archivePath = path.join(dir, '.memoc', 'log-archive.md');
1856
-
1857
- if (!fs.existsSync(logPath)) {
1858
- console.log('\n No .memoc/log.md found.\n');
1859
- return;
1860
- }
1861
-
1862
- const src = fs.readFileSync(logPath, 'utf8');
1863
- // Split on entry headers, keep header as part of each chunk
1864
- const parts = src.split(/(?=\n## \[)/);
1865
- const header = parts[0]; // everything before first entry
1866
- const entries = parts.slice(1).filter(e => e.trim());
1867
-
1868
- if (entries.length <= KEEP) {
1869
- console.log(`\n log.md has ${entries.length} entries — nothing to compress (threshold: ${KEEP}).\n`);
1870
- return;
1871
- }
1872
-
1873
- const toArchive = entries.slice(0, entries.length - KEEP);
1874
- const toKeep = entries.slice(entries.length - KEEP);
1875
-
1876
- // Append to archive
1877
- const archiveExists = fs.existsSync(archivePath);
1878
- const archiveHeader = archiveExists ? '' : '# Log Archive\n\nOlder entries moved from log.md by `memoc compress`.\n';
1879
- fs.appendFileSync(archivePath, archiveHeader + toArchive.join('') + '\n', 'utf8');
1880
-
1881
- // Rewrite log.md with only recent entries
1882
- write(logPath, header.trimEnd() + '\n' + toKeep.join('') + '\n');
1883
-
1884
- console.log(`\n memoc compress\n`);
1885
- console.log(` Archived ${toArchive.length} entries → .memoc/log-archive.md`);
1886
- console.log(` Kept ${toKeep.length} recent entries in log.md`);
1887
- const saved = Buffer.byteLength(toArchive.join(''), 'utf8');
1888
- console.log(` Freed ~${saved}B from log.md`);
1889
- console.log('\n Done.\n');
1890
- }
1891
-
1892
- // ═══════════════════════════════════════════════════════════════════
1893
- // SUMMARY
1894
- // ═══════════════════════════════════════════════════════════════════
1895
-
1896
- function runSummary(dir) {
1897
- const files = [
1898
- path.join(dir, '.memoc/session-summary.md'),
1899
- path.join(dir, '.memoc/02-current-project-state.md'),
1900
- path.join(dir, '.memoc/04-handoff.md'),
1901
- ];
1902
-
1903
- function read(fp) {
1904
- try { return fs.readFileSync(fp, 'utf8'); } catch { return ''; }
1905
- }
1906
-
1907
- function section(src, heading) {
1908
- const re = new RegExp(`^## ${heading}\\n([\\s\\S]*?)(?=\\n## |$)`, 'm');
1909
- const m = src.match(re);
1910
- return m ? m[1].trim() : '';
1911
- }
1912
-
1913
- function bullets(text, max = 3) {
1914
- return text.split('\n')
1915
- .map(line => line.trim())
1916
- .filter(line => line.startsWith('- ') && !line.includes('_None yet._'))
1917
- .slice(0, max);
1918
- }
1919
-
1920
- const summary = read(files[0]);
1921
- const state = read(files[1]);
1922
- const handoff = read(files[2]);
1923
- try {
1924
- const pkg = JSON.parse(read(path.join(dir, 'package.json')));
1925
- if (pkg.name || pkg.version) {
1926
- console.log(`Project: ${pkg.name || path.basename(dir)}${pkg.version ? `@${pkg.version}` : ''}`);
1927
- }
1928
- } catch {}
1929
- const rows = [
1930
- ['Status', bullets(section(summary, 'Status')).concat(bullets(section(state, 'Current Status'))).slice(0, 3)],
1931
- ['Open Tasks', bullets(section(summary, 'Open Tasks')).concat(bullets(section(state, 'Open Tasks'))).slice(0, 3)],
1932
- ['Resume', bullets(section(summary, 'Resume')).concat(bullets(section(handoff, 'Next Steps'))).slice(0, 3)],
1933
- ['Verified', bullets(section(handoff, 'Verified'), 2)],
1934
- ];
1935
-
1936
- let printed = false;
1937
- for (const [label, items] of rows) {
1938
- if (!items.length) continue;
1939
- printed = true;
1940
- console.log(`${label}:`);
1941
- for (const item of items) console.log(item);
1942
- }
1943
- if (!printed) console.log('No summary bullets yet. Read .memoc/session-summary.md.');
1944
- }
1945
-
1946
- // ═══════════════════════════════════════════════════════════════════
1947
- // CLI ENTRY POINT
1948
- // ═══════════════════════════════════════════════════════════════════
1949
-
1950
- const cmd = process.argv[2];
1951
- const cwd = process.cwd();
1952
-
1953
- if (cmd === '--version' || cmd === '-v') {
1954
- console.log(VERSION);
1955
- process.exit(0);
1956
- }
1957
-
1958
- if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') {
1959
- console.log('Usage: memoc <command>\n');
2032
+
2033
+ // ═══════════════════════════════════════════════════════════════════
2034
+ // TOKENS — estimate token cost of current memory state
2035
+ // ═══════════════════════════════════════════════════════════════════
2036
+
2037
+ function runTokens(dir) {
2038
+ const est = text => Math.ceil(Buffer.byteLength(text, 'utf8') / 4);
2039
+ const read = fp => { try { return fs.readFileSync(fp, 'utf8'); } catch { return ''; } };
2040
+ const memDir = path.join(dir, '.memoc');
2041
+
2042
+ const startup = [
2043
+ ['CLAUDE.md', path.join(dir, 'CLAUDE.md')],
2044
+ ['session-summary.md', path.join(memDir, 'session-summary.md')],
2045
+ ];
2046
+ const onDemand = [
2047
+ ['llms.txt', path.join(dir, 'llms.txt')],
2048
+ ['02-current-project-state.md', path.join(memDir, '02-current-project-state.md')],
2049
+ ['03-decisions.md', path.join(memDir, '03-decisions.md')],
2050
+ ['04-handoff.md', path.join(memDir, '04-handoff.md')],
2051
+ ['06-project-rules.md', path.join(memDir, '06-project-rules.md')],
2052
+ ['log.md', path.join(memDir, 'log.md')],
2053
+ ];
2054
+
2055
+ console.log('\n memoc tokens\n');
2056
+ let startupTotal = 0;
2057
+ console.log(' Startup (always loaded):');
2058
+ for (const [name, fp] of startup) {
2059
+ const content = read(fp);
2060
+ const t = est(content);
2061
+ const b = Buffer.byteLength(content, 'utf8');
2062
+ startupTotal += t;
2063
+ const warn = b > 1000 ? ' ⚠ large' : '';
2064
+ console.log(` ${name.padEnd(32)} ${String(t).padStart(5)} tokens (${b}B)${warn}`);
2065
+ }
2066
+ console.log(` ${'── startup total'.padEnd(32)} ${String(startupTotal).padStart(5)} tokens`);
2067
+
2068
+ console.log('\n On-demand (read when needed):');
2069
+ let onDemandTotal = 0;
2070
+ for (const [name, fp] of onDemand) {
2071
+ const content = read(fp);
2072
+ if (!content) continue;
2073
+ const t = est(content);
2074
+ const b = Buffer.byteLength(content, 'utf8');
2075
+ onDemandTotal += t;
2076
+ const warn = t > 500 ? ' ⚠ consider compress' : '';
2077
+ console.log(` ${name.padEnd(32)} ${String(t).padStart(5)} tokens (${b}B)${warn}`);
2078
+ }
2079
+ console.log(` ${'── on-demand total'.padEnd(32)} ${String(onDemandTotal).padStart(5)} tokens`);
2080
+ console.log(`\n If all loaded: ~${startupTotal + onDemandTotal} tokens`);
2081
+
2082
+ const summaryContent = read(path.join(memDir, 'session-summary.md'));
2083
+ const summaryBytes = Buffer.byteLength(summaryContent, 'utf8');
2084
+ if (summaryBytes > 800) {
2085
+ console.log(`\n ⚠ session-summary.md is ${summaryBytes}B — recommended <800B. Trim it manually.`);
2086
+ }
2087
+ console.log();
2088
+ }
2089
+
2090
+ // ═══════════════════════════════════════════════════════════════════
2091
+ // COMPRESS — archive old log.md entries to keep file small
2092
+ // ═══════════════════════════════════════════════════════════════════
2093
+
2094
+ function runCompress(dir) {
2095
+ const KEEP = 20;
2096
+ const logPath = path.join(dir, '.memoc', 'log.md');
2097
+ const archivePath = path.join(dir, '.memoc', 'log-archive.md');
2098
+
2099
+ if (!fs.existsSync(logPath)) {
2100
+ console.log('\n No .memoc/log.md found.\n');
2101
+ return;
2102
+ }
2103
+
2104
+ const src = fs.readFileSync(logPath, 'utf8');
2105
+ // Split on entry headers, keep header as part of each chunk
2106
+ const parts = src.split(/(?=\n## \[)/);
2107
+ const header = parts[0]; // everything before first entry
2108
+ const entries = parts.slice(1).filter(e => e.trim());
2109
+
2110
+ if (entries.length <= KEEP) {
2111
+ console.log(`\n log.md has ${entries.length} entries — nothing to compress (threshold: ${KEEP}).\n`);
2112
+ return;
2113
+ }
2114
+
2115
+ const toArchive = entries.slice(0, entries.length - KEEP);
2116
+ const toKeep = entries.slice(entries.length - KEEP);
2117
+
2118
+ // Append to archive
2119
+ const archiveExists = fs.existsSync(archivePath);
2120
+ const archiveHeader = archiveExists ? '' : '# Log Archive\n\nOlder entries moved from log.md by `memoc compress`.\n';
2121
+ fs.appendFileSync(archivePath, archiveHeader + toArchive.join('') + '\n', 'utf8');
2122
+
2123
+ // Rewrite log.md with only recent entries
2124
+ write(logPath, header.trimEnd() + '\n' + toKeep.join('') + '\n');
2125
+
2126
+ console.log(`\n memoc compress\n`);
2127
+ console.log(` Archived ${toArchive.length} entries → .memoc/log-archive.md`);
2128
+ console.log(` Kept ${toKeep.length} recent entries in log.md`);
2129
+ const saved = Buffer.byteLength(toArchive.join(''), 'utf8');
2130
+ console.log(` Freed ~${saved}B from log.md`);
2131
+ console.log('\n Done.\n');
2132
+ }
2133
+
2134
+ // ═══════════════════════════════════════════════════════════════════
2135
+ // SUMMARY
2136
+ // ═══════════════════════════════════════════════════════════════════
2137
+
2138
+ function runSummary(dir) {
2139
+ const files = [
2140
+ path.join(dir, '.memoc/session-summary.md'),
2141
+ path.join(dir, '.memoc/02-current-project-state.md'),
2142
+ path.join(dir, '.memoc/04-handoff.md'),
2143
+ ];
2144
+
2145
+ function read(fp) {
2146
+ try { return fs.readFileSync(fp, 'utf8'); } catch { return ''; }
2147
+ }
2148
+
2149
+ function section(src, heading) {
2150
+ const re = new RegExp(`^## ${heading}\\n([\\s\\S]*?)(?=\\n## |$)`, 'm');
2151
+ const m = src.match(re);
2152
+ return m ? m[1].trim() : '';
2153
+ }
2154
+
2155
+ function bullets(text, max = 3) {
2156
+ return text.split('\n')
2157
+ .map(line => line.trim())
2158
+ .filter(line => line.startsWith('- ') && !line.includes('_None yet._'))
2159
+ .slice(0, max);
2160
+ }
2161
+
2162
+ const summary = read(files[0]);
2163
+ const state = read(files[1]);
2164
+ const handoff = read(files[2]);
2165
+ try {
2166
+ const pkg = JSON.parse(read(path.join(dir, 'package.json')));
2167
+ if (pkg.name || pkg.version) {
2168
+ console.log(`Project: ${pkg.name || path.basename(dir)}${pkg.version ? `@${pkg.version}` : ''}`);
2169
+ }
2170
+ } catch {}
2171
+ const rows = [
2172
+ ['Status', bullets(section(summary, 'Status')).concat(bullets(section(state, 'Current Status'))).slice(0, 3)],
2173
+ ['Open Tasks', bullets(section(summary, 'Open Tasks')).concat(bullets(section(state, 'Open Tasks'))).slice(0, 3)],
2174
+ ['Resume', bullets(section(summary, 'Resume')).concat(bullets(section(handoff, 'Next Steps'))).slice(0, 3)],
2175
+ ['Verified', bullets(section(handoff, 'Verified'), 2)],
2176
+ ];
2177
+
2178
+ let printed = false;
2179
+ for (const [label, items] of rows) {
2180
+ if (!items.length) continue;
2181
+ printed = true;
2182
+ console.log(`${label}:`);
2183
+ for (const item of items) console.log(item);
2184
+ }
2185
+ if (!printed) console.log('No summary bullets yet. Read .memoc/session-summary.md.');
2186
+ }
2187
+
2188
+ // ═══════════════════════════════════════════════════════════════════
2189
+ // CLI ENTRY POINT
2190
+ // ═══════════════════════════════════════════════════════════════════
2191
+
2192
+ const cmd = process.argv[2];
2193
+ const cwd = process.cwd();
2194
+
2195
+ if (cmd === '--version' || cmd === '-v') {
2196
+ console.log(VERSION);
2197
+ process.exit(0);
2198
+ }
2199
+
2200
+ if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') {
2201
+ console.log('Usage: memoc <command>\n');
1960
2202
  console.log('Commands:');
1961
2203
  console.log(' init Scaffold agent memory (auto-detects project, updates if already exists)');
1962
2204
  console.log(' update Force-update managed sections based on current project state');
1963
2205
  console.log(' upgrade Refresh memoc runtime/wrappers and managed sections; preserve memory');
1964
2206
  console.log(' summary Print a tiny status/resume overview');
1965
- console.log(' tokens Estimate token cost of current memory files');
1966
- console.log(' compress Archive old log.md entries to keep file small');
1967
- console.log(' add <agent> Add entry file for a specific agent (run without args to list)');
1968
- console.log(' search "<query>" Search memory/agent docs (use --snippets for line matches)');
1969
- console.log(' grep "<query>" Search project source/text files (use --snippets for line matches)');
1970
- console.log('\nSearch flags:');
1971
- console.log(' --files Show file names and match counts, sorted by relevance + recency (default)');
1972
- console.log(' --snippets Show matching lines');
1973
- console.log(' --limit N Limit output (default 12)');
1974
- console.log(' --all Show all matches');
1975
- console.log('\nFlags:');
1976
- console.log(' --version, -v Print version');
1977
- process.exit(0);
1978
- }
1979
-
2207
+ console.log(' tokens Estimate token cost of current memory files');
2208
+ console.log(' compress Archive old log.md entries to keep file small');
2209
+ console.log(' add <agent> Add entry file for a specific agent (run without args to list)');
2210
+ console.log(' search "<query>" Search memory/agent docs (use --snippets for line matches)');
2211
+ console.log(' grep "<query>" Search project source/text files (use --snippets for line matches)');
2212
+ console.log('\nSearch flags:');
2213
+ console.log(' --files Show file names and match counts, sorted by relevance + recency (default)');
2214
+ console.log(' --snippets Show matching lines');
2215
+ console.log(' --limit N Limit output (default 12)');
2216
+ console.log(' --all Show all matches');
2217
+ console.log('\nFlags:');
2218
+ console.log(' --version, -v Print version');
2219
+ process.exit(0);
2220
+ }
2221
+
1980
2222
  if (cmd === 'init') { run(cwd, false); process.exit(0); }
1981
2223
  if (cmd === 'update') { run(cwd, true, 'update'); process.exit(0); }
1982
2224
  if (cmd === 'upgrade') { run(cwd, true, 'upgrade'); process.exit(0); }
1983
- if (cmd === 'summary') { runSummary(cwd); process.exit(0); }
1984
- if (cmd === 'tokens') { runTokens(cwd); process.exit(0); }
1985
- if (cmd === 'compress') { runCompress(cwd); process.exit(0); }
1986
- if (cmd === 'add') { runAdd(cwd); process.exit(0); }
1987
- if (cmd === 'search') { runSearch(cwd, 'memory'); process.exit(0); }
1988
- if (cmd === 'grep') { runSearch(cwd, 'project'); process.exit(0); }
1989
-
1990
- console.error(`Unknown command: ${cmd}`);
1991
- console.error('Run "memoc --help" for usage.');
1992
- process.exit(1);
2225
+ if (cmd === 'summary') { runSummary(cwd); process.exit(0); }
2226
+ if (cmd === 'tokens') { runTokens(cwd); process.exit(0); }
2227
+ if (cmd === 'compress') { runCompress(cwd); process.exit(0); }
2228
+ if (cmd === 'add') { runAdd(cwd); process.exit(0); }
2229
+ if (cmd === 'search') { runSearch(cwd, 'memory'); process.exit(0); }
2230
+ if (cmd === 'grep') { runSearch(cwd, 'project'); process.exit(0); }
2231
+
2232
+ console.error(`Unknown command: ${cmd}`);
2233
+ console.error('Run "memoc --help" for usage.');
2234
+ process.exit(1);