@kevin0181/memoc 1.1.2 → 1.1.4

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 +177 -165
  3. package/bin/cli.js +2871 -1847
  4. package/package.json +37 -37
package/bin/cli.js CHANGED
@@ -1,501 +1,545 @@
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
+ function todayISO() { return new Date().toISOString().slice(0, 10); }
176
+
177
+ function stackStr(stack) { return stack.length ? stack.join(', ') : 'Not detected'; }
178
+
179
+ function listMd(arr, empty = '_None detected._') {
180
+ return arr.length ? arr.map(x => `- \`${x}\``).join('\n') : empty;
181
+ }
182
+
183
+ function scriptsMd(scripts) {
184
+ const pairs = Object.entries(scripts);
185
+ return pairs.length
186
+ ? pairs.map(([k, v]) => `- \`${k}\`: \`${v}\``).join('\n')
187
+ : '_None detected._';
188
+ }
189
+
189
190
  function hideOnWindows(dirPath) {
190
191
  if (process.platform === 'win32') {
191
192
  try { require('child_process').execFileSync('attrib', ['+h', dirPath], { stdio: 'ignore' }); } catch {}
192
193
  }
193
194
  }
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
-
195
+
196
+ function chmodExecutable(filePath) {
197
+ try { fs.chmodSync(filePath, 0o755); } catch {}
198
+ }
199
+
200
+ function ensure(filePath, content) {
201
+ if (fs.existsSync(filePath)) return false;
202
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
203
+ fs.writeFileSync(filePath, content, 'utf8');
204
+ return true;
205
+ }
206
+
207
+ function write(filePath, content) {
208
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
209
+ fs.writeFileSync(filePath, content, 'utf8');
210
+ }
211
+
212
+ function slugify(value, fallback = 'note') {
213
+ const slug = String(value || '')
214
+ .toLowerCase()
215
+ .replace(/['"]/g, '')
216
+ .replace(/[^a-z0-9]+/g, '-')
217
+ .replace(/^-+|-+$/g, '')
218
+ .slice(0, 80);
219
+ return slug || fallback;
220
+ }
221
+
222
+ function uniquePath(filePath) {
223
+ if (!fs.existsSync(filePath)) return filePath;
224
+ const ext = path.extname(filePath);
225
+ const base = filePath.slice(0, filePath.length - ext.length);
226
+ let i = 2;
227
+ while (fs.existsSync(`${base}-${i}${ext}`)) i += 1;
228
+ return `${base}-${i}${ext}`;
229
+ }
230
+
231
+ function markdownTitle(src, fallback) {
232
+ const m = String(src || '').match(/^#\s+(.+)$/m);
233
+ return m ? m[1].trim() : fallback;
234
+ }
235
+
236
+ function tplMemocCmdWrapper(cliPath = runtimeCliPath()) {
237
+ return `@echo off\r\nnode "${escapeCmdPath(cliPath)}" %*\r\n`;
238
+ }
239
+
240
+ function tplMemocPs1Wrapper(cliPath = runtimeCliPath()) {
241
+ return `& node ${psSingleQuote(cliPath)} @args\nexit $LASTEXITCODE\n`;
242
+ }
243
+
244
+ function tplMemocShWrapper(cliPath = runtimeCliPath()) {
245
+ return `#!/bin/sh\nexec node ${shellSingleQuote(cliPath)} "$@"\n`;
246
+ }
247
+
248
+ function defaultUserBinDir() {
249
+ if (process.env.MEMOC_USER_BIN_DIR) return process.env.MEMOC_USER_BIN_DIR;
250
+ if (currentPlatform() === 'win32') {
251
+ return path.join(process.env.LOCALAPPDATA || path.join(process.env.USERPROFILE || process.cwd(), 'AppData', 'Local'), 'memoc', 'bin');
252
+ }
253
+ return path.join(process.env.HOME || process.cwd(), '.local', 'bin');
254
+ }
255
+
256
+ function defaultRuntimeDir() {
257
+ if (process.env.MEMOC_RUNTIME_DIR) return process.env.MEMOC_RUNTIME_DIR;
258
+ if (currentPlatform() === 'win32') {
259
+ return path.join(process.env.LOCALAPPDATA || path.join(process.env.USERPROFILE || process.cwd(), 'AppData', 'Local'), 'memoc', 'runtime');
260
+ }
261
+ return path.join(process.env.HOME || process.cwd(), '.local', 'share', 'memoc', 'runtime');
262
+ }
263
+
264
+ function runtimeCliPath() {
265
+ return path.join(defaultRuntimeDir(), 'bin', 'cli.js');
266
+ }
267
+
268
+ function tplEnvPs1() {
269
+ 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`;
270
+ }
271
+
247
272
  function tplEnvSh() {
248
273
  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
274
  }
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
-
275
+
276
+ function ensurePathHelpers(dir, mark) {
277
+ const cliPath = ensureRuntimeInstall(mark);
278
+ const files = [
279
+ [path.join(dir, '.memoc', 'bin', 'memoc.cmd'), () => tplMemocCmdWrapper(cliPath), false],
280
+ [path.join(dir, '.memoc', 'bin', 'memoc.ps1'), () => tplMemocPs1Wrapper(cliPath), false],
281
+ [path.join(dir, '.memoc', 'bin', 'memoc'), () => tplMemocShWrapper(cliPath), true],
282
+ [path.join(dir, '.memoc', 'env.ps1'), tplEnvPs1, false],
283
+ [path.join(dir, '.memoc', 'env.sh'), tplEnvSh, true],
284
+ ];
285
+
286
+ for (const [fp, tpl, executable] of files) {
287
+ const rel = path.relative(dir, fp);
288
+ const added = writeIfChanged(fp, tpl());
289
+ if (executable) chmodExecutable(fp);
290
+ mark(added, rel);
291
+ }
292
+ }
293
+
294
+ function ensureUserLauncher(mark) {
295
+ const userBin = defaultUserBinDir();
296
+ writeLaunchers(userBin, mark, 'user bin', ensureRuntimeInstall(mark));
297
+ return userBin;
298
+ }
299
+
300
+ function writeLaunchers(binDir, mark, label, cliPath = ensureRuntimeInstall(mark)) {
301
+ const files = [
302
+ [path.join(binDir, 'memoc.cmd'), () => tplMemocCmdWrapper(cliPath), false],
303
+ [path.join(binDir, 'memoc.ps1'), () => tplMemocPs1Wrapper(cliPath), false],
304
+ [path.join(binDir, 'memoc'), () => tplMemocShWrapper(cliPath), true],
305
+ ];
306
+
307
+ for (const [fp, tpl, executable] of files) {
308
+ const added = writeIfChanged(fp, tpl());
309
+ if (executable) chmodExecutable(fp);
310
+ mark(added, `${label} ${path.basename(fp)}`);
311
+ }
312
+ }
313
+
314
+ function writeIfChanged(filePath, content) {
315
+ if (!fs.existsSync(filePath)) {
316
+ write(filePath, content);
317
+ return 'add';
318
+ }
319
+ try {
320
+ if (fs.readFileSync(filePath, 'utf8') === content) return 'skip';
321
+ } catch {}
322
+ write(filePath, content);
323
+ return 'update';
324
+ }
325
+
326
+ function writeIfDefaultish(filePath, content, isDefaultish) {
327
+ if (!fs.existsSync(filePath)) {
328
+ write(filePath, content);
329
+ return 'add';
330
+ }
331
+ let src = '';
332
+ try { src = fs.readFileSync(filePath, 'utf8'); } catch { return 'skip'; }
333
+ if (!isDefaultish(src)) return 'skip';
334
+ if (src === content) return 'skip';
335
+ write(filePath, content);
336
+ return 'update';
337
+ }
338
+
339
+ function hasOnlyScaffold(src, required) {
340
+ if (!required.every(part => src.includes(part))) return false;
341
+ const nonEmpty = src.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
342
+ return nonEmpty.length <= 16;
343
+ }
344
+
345
+ function ensurePathRegistration(dir, mark) {
346
+ ensureCurrentPathLauncher(mark);
347
+ const binDir = ensureUserLauncher(mark);
348
+ const pathSep = path.delimiter;
349
+
350
+ if ((process.env.PATH || '').split(pathSep).some(p => samePath(p, binDir))) {
351
+ mark('skip', 'PATH (user memoc bin already active)');
352
+ return;
353
+ }
354
+
355
+ process.env.PATH = `${binDir}${pathSep}${process.env.PATH || ''}`;
356
+
357
+ if (process.env.MEMOC_SKIP_PATH_REGISTER === '1') {
358
+ mark('skip', 'PATH registration (test mode)');
359
+ return;
360
+ }
361
+
362
+ if (currentPlatform() !== 'win32') {
363
+ const updated = ensureUnixPathRegistration(binDir);
364
+ mark(updated ? 'update' : 'skip', `${currentPlatform()} PATH (${userPathShellHint(binDir)})`);
365
+ return;
366
+ }
367
+
368
+ try {
369
+ const current = require('child_process')
370
+ .execFileSync('powershell.exe', [
371
+ '-NoProfile',
372
+ '-ExecutionPolicy', 'Bypass',
373
+ '-Command',
374
+ "[Environment]::GetEnvironmentVariable('Path','User')",
375
+ ], { encoding: 'utf8' })
376
+ .trim();
377
+ const parts = current.split(pathSep).filter(Boolean);
378
+ if (parts.some(p => samePath(p, binDir))) {
379
+ mark('skip', 'User PATH (memoc bin already registered)');
380
+ return;
381
+ }
382
+ const nextPath = [binDir, ...parts].join(pathSep);
383
+ require('child_process').execFileSync('powershell.exe', [
384
+ '-NoProfile',
385
+ '-ExecutionPolicy', 'Bypass',
386
+ '-Command',
387
+ `[Environment]::SetEnvironmentVariable('Path', ${JSON.stringify(nextPath)}, 'User')`,
388
+ ], { stdio: 'ignore' });
389
+ mark('update', 'User PATH (memoc bin added; open a new terminal if needed)');
390
+ } catch {
391
+ mark('skip', 'User PATH registration failed (use . .\\.memoc\\env.ps1)');
392
+ }
393
+ }
394
+
395
+ function ensureCurrentPathLauncher(mark) {
396
+ const target = findWritablePathDir();
397
+ if (!target) {
398
+ mark('skip', 'active PATH launcher (no writable PATH directory found)');
399
+ return false;
400
+ }
401
+ writeLaunchers(target, mark, 'active PATH', ensureRuntimeInstall(mark));
402
+ return true;
403
+ }
404
+
405
+ function ensureRuntimeInstall(mark) {
406
+ const runtimeDir = defaultRuntimeDir();
407
+ const sourceRoot = path.join(__dirname, '..');
408
+ const files = [
409
+ [path.join(sourceRoot, 'bin', 'cli.js'), path.join(runtimeDir, 'bin', 'cli.js')],
410
+ [path.join(sourceRoot, 'package.json'), path.join(runtimeDir, 'package.json')],
411
+ ];
412
+
413
+ for (const [src, dest] of files) {
414
+ try {
415
+ const content = fs.readFileSync(src, 'utf8');
416
+ const changed = writeIfChanged(dest, content);
417
+ mark(changed, `runtime ${path.relative(runtimeDir, dest)}`);
418
+ } catch {
419
+ mark('skip', `runtime ${path.basename(dest)} unavailable`);
420
+ }
421
+ }
422
+
423
+ chmodExecutable(path.join(runtimeDir, 'bin', 'cli.js'));
424
+ return path.join(runtimeDir, 'bin', 'cli.js');
425
+ }
426
+
427
+ function findWritablePathDir() {
428
+ const dirs = [...new Set((process.env.PATH || '').split(path.delimiter).filter(Boolean))];
429
+ const npmBin = npmGlobalBinDir();
430
+ const ranked = dirs
431
+ .filter(d => !isVolatilePathDir(d))
432
+ .filter(d => {
433
+ try { return fs.existsSync(d) && fs.statSync(d).isDirectory() && canWriteDir(d); }
434
+ catch { return false; }
435
+ })
436
+ .sort((a, b) => pathRank(a, npmBin) - pathRank(b, npmBin));
437
+ return ranked[0] || null;
438
+ }
439
+
440
+ function pathRank(dir, npmBin) {
441
+ if (npmBin && samePath(dir, npmBin)) return 0;
442
+ const lower = dir.toLowerCase();
443
+ for (const root of userWritableRoots()) {
444
+ if (root && lower.startsWith(root.toLowerCase())) return 1;
445
+ }
446
+ return 5;
447
+ }
448
+
449
+ function userWritableRoots() {
450
+ return [
451
+ process.env.APPDATA,
452
+ process.env.LOCALAPPDATA,
453
+ process.env.HOME,
454
+ process.env.USERPROFILE,
455
+ ].filter(Boolean).map(p => path.resolve(p));
456
+ }
457
+
458
+ function npmGlobalBinDir() {
459
+ try {
460
+ const prefix = require('child_process').execFileSync('npm', ['config', 'get', 'prefix'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
461
+ if (!prefix) return null;
462
+ return currentPlatform() === 'win32' ? prefix : path.join(prefix, 'bin');
463
+ } catch {
464
+ return null;
465
+ }
466
+ }
467
+
468
+ function isVolatilePathDir(dir) {
469
+ const lower = dir.toLowerCase();
470
+ return lower.includes(`${path.sep}_npx${path.sep}`) ||
471
+ lower.includes(`${path.sep}node_modules${path.sep}.bin`) ||
472
+ lower.includes(`${path.sep}npm-cache${path.sep}_npx${path.sep}`);
473
+ }
474
+
475
+ function canWriteDir(dir) {
476
+ const probe = path.join(dir, `.memoc-write-test-${process.pid}-${Date.now()}`);
477
+ try {
478
+ fs.writeFileSync(probe, '');
479
+ fs.unlinkSync(probe);
480
+ return true;
481
+ } catch {
482
+ return false;
483
+ }
484
+ }
485
+
486
+ function ensureUnixPathRegistration(binDir) {
487
+ if (process.env.MEMOC_SKIP_PATH_REGISTER === '1') return false;
488
+
489
+ const home = process.env.HOME;
490
+ if (!home) return false;
491
+
492
+ const block = [
493
+ '# memoc PATH',
494
+ `MEMOC_BIN=${shellSingleQuote(binDir)}`,
495
+ 'case ":$PATH:" in *":$MEMOC_BIN:"*) ;; *) PATH="$MEMOC_BIN:$PATH"; export PATH ;; esac',
496
+ '# end memoc PATH',
497
+ ].join('\n');
498
+
499
+ const candidates = [
500
+ path.join(home, '.profile'),
501
+ path.join(home, '.zshrc'),
502
+ path.join(home, '.bashrc'),
503
+ ];
504
+
505
+ let changed = false;
506
+ for (const fp of candidates) {
507
+ try {
508
+ const src = fs.existsSync(fp) ? fs.readFileSync(fp, 'utf8') : '';
509
+ if (src.includes(binDir) || src.includes('# memoc PATH')) continue;
510
+ fs.appendFileSync(fp, `${src.endsWith('\n') || !src ? '' : '\n'}\n${block}\n`, 'utf8');
511
+ changed = true;
512
+ } catch {}
513
+ }
514
+ return changed;
515
+ }
516
+
517
+ function userPathShellHint(binDir) {
518
+ return `user bin ${binDir} ${process.env.MEMOC_SKIP_PATH_REGISTER === '1' ? 'test mode' : 'registered; open a new terminal if needed'}`;
519
+ }
520
+
521
+ function currentPlatform() {
522
+ return process.env.MEMOC_PLATFORM || process.platform;
523
+ }
524
+
525
+ function shellSingleQuote(value) {
526
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
527
+ }
528
+
529
+ function psSingleQuote(value) {
530
+ return `'${String(value).replace(/'/g, "''")}'`;
531
+ }
532
+
533
+ function escapeCmdPath(value) {
534
+ return String(value).replace(/"/g, '""');
535
+ }
536
+
537
+ function samePath(a, b) {
538
+ if (!a || !b) return false;
539
+ const norm = p => path.resolve(p).toLowerCase().replace(/[\\/]+$/, '');
540
+ try { return norm(a) === norm(b); } catch { return false; }
541
+ }
542
+
499
543
  function updateSection(filePath, startMark, endMark, inner) {
500
544
  if (!fs.existsSync(filePath)) return false;
501
545
  const src = fs.readFileSync(filePath, 'utf8');
@@ -506,11 +550,11 @@ function updateSection(filePath, startMark, endMark, inner) {
506
550
  );
507
551
  return true;
508
552
  }
509
-
510
- // ═══════════════════════════════════════════════════════════════════
511
- // SECTION MARKERS
512
- // ═══════════════════════════════════════════════════════════════════
513
-
553
+
554
+ // ═══════════════════════════════════════════════════════════════════
555
+ // SECTION MARKERS
556
+ // ═══════════════════════════════════════════════════════════════════
557
+
514
558
  const mk = n => [`<!-- memoc:${n}:start -->`, `<!-- memoc:${n}:end -->`];
515
559
  const [MGMT_S, MGMT_E] = mk('managed');
516
560
  const [ID_S, ID_E] = mk('identity');
@@ -528,1155 +572,2039 @@ function markerPairs(startMark, endMark) {
528
572
  : [[startMark, endMark], [legacyStart, legacyEnd]];
529
573
  }
530
574
 
531
- function findMarkedRange(src, startMark, endMark) {
532
- for (const [sMark, eMark] of markerPairs(startMark, endMark)) {
533
- const s = src.indexOf(sMark);
534
- const e = src.indexOf(eMark);
535
- if (s !== -1 && e !== -1 && e > s) return { s, e, endMark: eMark };
536
- }
537
- return null;
575
+ function findMarkedRange(src, startMark, endMark) {
576
+ for (const [sMark, eMark] of markerPairs(startMark, endMark)) {
577
+ const s = src.indexOf(sMark);
578
+ const e = src.indexOf(eMark);
579
+ if (s !== -1 && e !== -1 && e > s) return { s, e, endMark: eMark };
580
+ }
581
+ return null;
582
+ }
583
+
584
+ // ═══════════════════════════════════════════════════════════════════
585
+ // AGENT REGISTRY — third-party agent entry files (added via `add`)
586
+ // ═══════════════════════════════════════════════════════════════════
587
+
588
+ const AGENT_REGISTRY = {
589
+ cursor: { file: '.cursorrules', label: 'Cursor' },
590
+ windsurf: { file: '.windsurfrules', label: 'Windsurf' },
591
+ copilot: { file: '.github/copilot-instructions.md', label: 'GitHub Copilot' },
592
+ gemini: { file: 'GEMINI.md', label: 'Gemini CLI' },
593
+ };
594
+
595
+ // ═══════════════════════════════════════════════════════════════════
596
+ // DYNAMIC CONTENT (re-generated on update)
597
+ // ═══════════════════════════════════════════════════════════════════
598
+
599
+ function legacyManagedBlock() {
600
+ return `${MGMT_S}
601
+ ## Session Start
602
+ - [ ] Read \`.memoc/session-summary.md\`
603
+ - [ ] \`.pending\` exists? → review changed files → update memory if needed → delete it
604
+ - [ ] 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\`
605
+
606
+ ## Before Opening More Files
607
+ - [ ] 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>"\`
608
+ - [ ] Open on demand: \`02\` status · \`04\` resume · \`06\` rules · \`llms.txt\` map
609
+ - [ ] If memory search is not enough, search project files with \`memoc grep "<query>" --limit 5\`
610
+ - [ ] Keep output small: \`summary\`, \`search --limit\`, \`grep --limit\`, \`--snippets\`
611
+
612
+ ## Before Finishing _(update only applicable files; skip Q&A / throwaway exploration)_
613
+ - [ ] Code/config/deps changed → \`02\` (version, commands list, Last synced) + \`session-summary.md\` (status, changed, open tasks)
614
+ - [ ] Decision made → \`03-decisions.md\` (what & why) + \`02\`
615
+ - [ ] Work incomplete or risky → \`04-handoff.md\` (verified commands, unverified items, next steps)
616
+ - [ ] Rule/preference set → \`06-project-rules.md\`
617
+ - [ ] Wiki/systems work → read \`skills/project-memory-maintainer/SKILL.md\`
618
+ ${MGMT_E}`;
619
+ }
620
+
621
+ function managedBlock() {
622
+ return `${MGMT_S}
623
+ ## Session Start
624
+ - [ ] Read \`.memoc/session-summary.md\`
625
+ - [ ] \`.pending\` exists? Review changed files, update memory if needed, then delete it.
626
+ - [ ] If \`memoc\` is not found, use the project-local wrapper for the rest of the session: Windows \`.\\.memoc\\bin\\memoc.cmd <command>\`; sh \`.memoc/bin/memoc <command>\`
627
+
628
+ ## Before Opening More Files
629
+ - [ ] Search memory first: \`memoc search "<query>" --limit 5\`, or wrapper fallback above if PATH fails
630
+ - [ ] Open on demand: \`02\` status, \`04\` resume, \`06\` rules, \`llms.txt\` map
631
+ - [ ] If memory search is not enough, search project files with \`memoc grep "<query>" --limit 5\` (or wrapper fallback)
632
+ - [ ] If asked to refresh/update memoc project memory, run \`memoc update\` first; this refreshes managed sections, wiki links, and Obsidian tags.
633
+ - [ ] For durable source material use \`memoc ingest <path-or-url>\`; for durable analysis/query results use \`memoc note "<title>"\`; after wiki edits run \`memoc lint-wiki\`.
634
+ - [ ] Keep output small: \`summary\`, \`search --limit\`, \`grep --limit\`, \`--snippets\`
635
+
636
+ ## Before Finishing _(update only applicable files; skip Q&A / throwaway exploration)_
637
+ - [ ] Code/config/deps changed? Update \`02\` + \`session-summary.md\`
638
+ - [ ] Decision made? Update \`03-decisions.md\` + \`02\`
639
+ - [ ] Work incomplete or risky? Update \`04-handoff.md\`
640
+ - [ ] Rule/preference set? Update \`06-project-rules.md\`
641
+ - [ ] Wiki/systems work? Read \`skills/project-memory-maintainer/SKILL.md\`
642
+ - [ ] User asked to update memoc/project memory? Run \`memoc update\`, then update the smallest relevant agent-owned memory files.
643
+ - [ ] Keep \`session-summary.md\` as a replace-only snapshot under 800B; move completed history to \`log.md\` and resume details to \`04-handoff.md\`. If it grew, run \`memoc trim-summary\`.
644
+ ${MGMT_E}`;
645
+ }
646
+
647
+ function identityInner(p) {
648
+ return [
649
+ `- Project name: \`${p.name}\``,
650
+ `- Detected stack: ${stackStr(p.stack)}`,
651
+ ].join('\n');
652
+ }
653
+
654
+ function snapshotInner(p) {
655
+ const lines = [
656
+ `- Last synced: ${nowISO()}`,
657
+ `- Detected stack: ${stackStr(p.stack)}`,
658
+ ];
659
+ if (p.configFiles.length)
660
+ lines.push(`\n### Config Files\n\n${listMd(p.configFiles)}`);
661
+ if (p.srcDirs.length)
662
+ lines.push(`\n### Source Directories\n\n${listMd(p.srcDirs)}`);
663
+ const sc = scriptsMd(p.scripts);
664
+ if (sc !== '_None detected._')
665
+ lines.push(`\n### Package Scripts\n\n${sc}`);
666
+ return lines.join('\n');
667
+ }
668
+
669
+ function coreLlmsInner() {
670
+ return `- [Session Summary](.memoc/session-summary.md): only required startup read.
671
+ - [Current State](.memoc/02-current-project-state.md): status, tasks, commands.
672
+ - [Handoff](.memoc/04-handoff.md): resume context, blockers, verification.
673
+ - [Rules](.memoc/06-project-rules.md): durable preferences.
674
+ - [Agent Index](.memoc/00-agent-index.md): compact file map.
675
+ - [Project Brief](.memoc/00-project-brief.md): short identity and direction.
676
+ - [Workflow](.memoc/01-agent-workflow.md): update trigger matrix.
677
+ - [Decisions](.memoc/03-decisions.md): durable decisions.
678
+ - [Log](.memoc/log.md): append-only history.
679
+ - [Systems](.memoc/systems/README.md): subsystem docs.
680
+ - [Raw Sources](.memoc/raw/README.md): immutable source material; do not read by default.
681
+ - [Wiki](.memoc/wiki/index.md): synthesized knowledge.`;
682
+ }
683
+
684
+ function headerInner(p) {
685
+ return `# ${p.name}\n\n> LLM-facing project map for this project.`;
686
+ }
687
+
688
+ function systemsLlmsInner(dir) {
689
+ const systemsDir = path.join(dir, '.memoc', 'systems');
690
+ if (!fs.existsSync(systemsDir)) return '_None yet._';
691
+ const files = fs.readdirSync(systemsDir)
692
+ .filter(f => f.endsWith('.md') && f !== 'README.md')
693
+ .sort();
694
+ if (!files.length) return '_None yet._';
695
+ return files.map(f => `- [${f.replace('.md', '')}](.memoc/systems/${f}): subsystem context.`).join('\n');
696
+ }
697
+
698
+ function wikiLlmsInner(dir) {
699
+ const wikiDir = path.join(dir, '.memoc', 'wiki');
700
+ if (!fs.existsSync(wikiDir)) return '_None yet._';
701
+ const lines = [];
702
+ const SKIP = new Set(['index.md']);
703
+ try {
704
+ for (const f of fs.readdirSync(wikiDir).sort()) {
705
+ if (!f.endsWith('.md') || SKIP.has(f)) continue;
706
+ try { if (fs.statSync(path.join(wikiDir, f)).isDirectory()) continue; } catch { continue; }
707
+ lines.push(`- [${f.replace('.md', '')}](.memoc/wiki/${f}): wiki page.`);
708
+ }
709
+ for (const sub of ['sources', 'topics', 'global']) {
710
+ const subDir = path.join(wikiDir, sub);
711
+ if (!fs.existsSync(subDir)) continue;
712
+ for (const f of fs.readdirSync(subDir).sort()) {
713
+ if (!f.endsWith('.md')) continue;
714
+ lines.push(`- [${f.replace('.md', '')}](.memoc/wiki/${sub}/${f}): wiki page.`);
715
+ }
716
+ }
717
+ } catch {}
718
+ return lines.length ? lines.join('\n') : '_None yet._';
719
+ }
720
+
721
+ function wikiScaffoldFiles(memDir) {
722
+ return [
723
+ [
724
+ path.join(memDir, 'wiki/index.md'),
725
+ tplWikiIndex,
726
+ src => src.includes('# Wiki Index') && src.includes('Persistent LLM-maintained project wiki') &&
727
+ (src.includes('_None yet._') || !src.includes('## Graph Hubs')),
728
+ ],
729
+ [
730
+ path.join(memDir, 'wiki/sources.md'),
731
+ tplWikiSources,
732
+ src => hasOnlyScaffold(src, ['# Sources', '_No sources recorded yet._']) && !src.includes('## Related'),
733
+ ],
734
+ [
735
+ path.join(memDir, 'wiki/glossary.md'),
736
+ tplWikiGlossary,
737
+ src => hasOnlyScaffold(src, ['# Glossary', '_No terms defined yet._']) && !src.includes('## Related'),
738
+ ],
739
+ [
740
+ path.join(memDir, 'wiki/questions.md'),
741
+ tplWikiQuestions,
742
+ src => hasOnlyScaffold(src, ['# Open Questions', '_No open questions yet._']) && !src.includes('## Related'),
743
+ ],
744
+ [
745
+ path.join(memDir, 'wiki/lint.md'),
746
+ tplWikiLint,
747
+ src => src.includes('# Wiki Lint') && src.includes('_No issues found._') && !src.includes('## Graph Checks'),
748
+ ],
749
+ [
750
+ path.join(memDir, 'wiki/sources/README.md'),
751
+ tplWikiSourcesReadme,
752
+ src => hasOnlyScaffold(src, ['# Sources', 'Provenance records']) && !src.includes('## Related'),
753
+ ],
754
+ [
755
+ path.join(memDir, 'wiki/topics/README.md'),
756
+ tplWikiTopicsReadme,
757
+ src => hasOnlyScaffold(src, ['# Topics', 'Synthesized topic pages']) && !src.includes('## Related'),
758
+ ],
759
+ [
760
+ path.join(memDir, 'wiki/global/README.md'),
761
+ tplWikiGlobalReadme,
762
+ src => hasOnlyScaffold(src, ['# Global', 'Project-wide principles']) && !src.includes('## Related'),
763
+ ],
764
+ ];
765
+ }
766
+
767
+ function ensureWikiScaffoldLinks(memDir, mark) {
768
+ for (const [fp, tpl, isDefaultish] of wikiScaffoldFiles(memDir)) {
769
+ const result = writeIfDefaultish(fp, tpl(), isDefaultish);
770
+ if (result !== 'skip') mark(result, `${path.relative(path.dirname(memDir), fp)} (wiki links)`);
771
+ }
772
+ }
773
+
774
+ function ensureObsidianFrontmatter(dir, mark) {
775
+ const files = collectMemocMarkdownFiles(dir);
776
+ let changed = 0;
777
+ for (const fp of files) {
778
+ if (ensureMemocFrontmatter(fp, dir)) changed += 1;
779
+ }
780
+ mark(changed ? 'update' : 'skip', `Obsidian tags (${changed || 'already present'})`);
781
+ }
782
+
783
+ function collectMemocMarkdownFiles(dir) {
784
+ const files = [];
785
+ function walk(root) {
786
+ if (!fs.existsSync(root)) return;
787
+ try {
788
+ const st = fs.statSync(root);
789
+ if (st.isFile()) {
790
+ if (root.endsWith('.md')) files.push(root);
791
+ return;
792
+ }
793
+ if (!st.isDirectory()) return;
794
+ for (const entry of fs.readdirSync(root)) walk(path.join(root, entry));
795
+ } catch {}
796
+ }
797
+ walk(path.join(dir, '.memoc'));
798
+ walk(path.join(dir, 'skills', 'project-memory-maintainer'));
799
+ return files.sort();
800
+ }
801
+
802
+ function ensureMemocFrontmatter(filePath, dir) {
803
+ let src = '';
804
+ try { src = fs.readFileSync(filePath, 'utf8'); } catch { return false; }
805
+ const spec = obsidianFrontmatterSpec(path.relative(dir, filePath));
806
+ const next = mergeYamlFrontmatter(src, spec);
807
+ if (next === src) return false;
808
+ write(filePath, next);
809
+ return true;
810
+ }
811
+
812
+ function obsidianFrontmatterSpec(relPath) {
813
+ const rel = relPath.replace(/\\/g, '/');
814
+ let type = 'core';
815
+ const tags = ['memoc'];
816
+ const now = nowISO();
817
+ const extra = {
818
+ created: now,
819
+ updated: now,
820
+ status: 'active',
821
+ };
822
+
823
+ if (rel.startsWith('.memoc/wiki/')) {
824
+ type = 'wiki';
825
+ extra.confidence = 'medium';
826
+ tags.push('memoc/wiki');
827
+ if (rel.startsWith('.memoc/wiki/sources/')) {
828
+ tags.push('memoc/source');
829
+ extra.status = 'needs-synthesis';
830
+ } else if (rel.startsWith('.memoc/wiki/topics/')) {
831
+ tags.push('memoc/topic');
832
+ } else if (rel.startsWith('.memoc/wiki/global/')) {
833
+ tags.push('memoc/global');
834
+ } else if (rel.endsWith('/sources.md')) {
835
+ tags.push('memoc/source');
836
+ } else if (rel.endsWith('/glossary.md')) {
837
+ tags.push('memoc/glossary');
838
+ } else if (rel.endsWith('/questions.md')) {
839
+ tags.push('memoc/question');
840
+ extra.status = 'needs-review';
841
+ } else if (rel.endsWith('/lint.md')) {
842
+ tags.push('memoc/lint');
843
+ extra.status = 'generated';
844
+ }
845
+ } else if (rel.startsWith('.memoc/systems/')) {
846
+ type = 'system';
847
+ tags.push('memoc/system');
848
+ } else if (rel.startsWith('.memoc/raw/')) {
849
+ type = 'raw';
850
+ tags.push('memoc/raw');
851
+ } else if (rel.startsWith('skills/project-memory-maintainer/')) {
852
+ type = 'skill';
853
+ tags.push('memoc/skill');
854
+ } else if (/(session-summary|current-project-state|handoff|project-rules|decisions|log)\.md$/.test(rel)) {
855
+ type = 'state';
856
+ tags.push('memoc/state');
857
+ } else {
858
+ tags.push('memoc/core');
859
+ }
860
+
861
+ return { type, tags, extra };
862
+ }
863
+
864
+ function mergeYamlFrontmatter(src, spec) {
865
+ const fm = parseYamlFrontmatter(src);
866
+ if (!fm) {
867
+ return `${formatMemocFrontmatter(spec)}\n${src}`;
868
+ }
869
+
870
+ const lines = fm.body.split(/\r?\n/);
871
+ const existingTags = readYamlTags(lines);
872
+ const mergedTags = [...new Set([...existingTags, ...spec.tags])];
873
+ const nextLines = mergeYamlScalar(lines, 'memoc', 'true');
874
+ mergeYamlScalar(nextLines, 'type', spec.type);
875
+ mergeYamlScalar(nextLines, 'scope', 'project-memory');
876
+ mergeYamlScalarIfMissing(nextLines, 'updated', spec.extra.updated);
877
+ mergeYamlScalarIfMissing(nextLines, 'created', spec.extra.created);
878
+ mergeYamlScalarIfMissing(nextLines, 'status', spec.extra.status);
879
+ if (spec.extra.confidence) mergeYamlScalarIfMissing(nextLines, 'confidence', spec.extra.confidence);
880
+ mergeYamlTags(nextLines, mergedTags);
881
+
882
+ const nextFm = ['---', ...nextLines, '---'].join('\n');
883
+ return nextFm + src.slice(fm.end);
884
+ }
885
+
886
+ function parseYamlFrontmatter(src) {
887
+ if (!src.startsWith('---\n') && !src.startsWith('---\r\n')) return null;
888
+ const m = src.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
889
+ if (!m) return null;
890
+ return { body: m[1], end: m[0].length };
891
+ }
892
+
893
+ function formatMemocFrontmatter(spec) {
894
+ return [
895
+ '---',
896
+ 'memoc: true',
897
+ `type: ${spec.type}`,
898
+ 'scope: project-memory',
899
+ `created: ${spec.extra.created}`,
900
+ `updated: ${spec.extra.updated}`,
901
+ `status: ${spec.extra.status}`,
902
+ ...(spec.extra.confidence ? [`confidence: ${spec.extra.confidence}`] : []),
903
+ 'tags:',
904
+ ...spec.tags.map(tag => ` - ${tag}`),
905
+ '---',
906
+ ].join('\n');
907
+ }
908
+
909
+ function mergeYamlScalar(lines, key, value) {
910
+ const re = new RegExp(`^${escapeRegExp(key)}\\s*:`);
911
+ const idx = lines.findIndex(line => re.test(line.trim()));
912
+ if (idx === -1) lines.push(`${key}: ${value}`);
913
+ else lines[idx] = `${key}: ${value}`;
914
+ return lines;
915
+ }
916
+
917
+ function mergeYamlScalarIfMissing(lines, key, value) {
918
+ const re = new RegExp(`^${escapeRegExp(key)}\\s*:`);
919
+ if (lines.findIndex(line => re.test(line.trim())) === -1) lines.push(`${key}: ${value}`);
920
+ return lines;
921
+ }
922
+
923
+ function mergeYamlTags(lines, tags) {
924
+ const idx = lines.findIndex(line => /^tags\s*:/.test(line.trim()));
925
+ const tagLines = ['tags:', ...tags.map(tag => ` - ${tag}`)];
926
+ if (idx === -1) {
927
+ lines.push(...tagLines);
928
+ return;
929
+ }
930
+
931
+ let end = idx + 1;
932
+ while (end < lines.length && (/^\s+-\s+/.test(lines[end]) || lines[end].trim() === '')) end += 1;
933
+ lines.splice(idx, end - idx, ...tagLines);
934
+ }
935
+
936
+ function readYamlTags(lines) {
937
+ const tags = [];
938
+ const inline = lines.find(line => /^tags\s*:\s*\[/.test(line.trim()));
939
+ if (inline) {
940
+ const m = inline.match(/\[(.*)\]/);
941
+ if (m) {
942
+ for (const item of m[1].split(',')) {
943
+ const tag = item.trim().replace(/^['"]|['"]$/g, '').replace(/^#/, '');
944
+ if (tag) tags.push(tag);
945
+ }
946
+ }
947
+ }
948
+
949
+ const idx = lines.findIndex(line => /^tags\s*:/.test(line.trim()));
950
+ if (idx !== -1) {
951
+ for (let i = idx + 1; i < lines.length; i++) {
952
+ const m = lines[i].match(/^\s+-\s+(.+?)\s*$/);
953
+ if (!m) {
954
+ if (lines[i].trim() === '') continue;
955
+ break;
956
+ }
957
+ const tag = m[1].trim().replace(/^['"]|['"]$/g, '').replace(/^#/, '');
958
+ if (tag) tags.push(tag);
959
+ }
960
+ }
961
+
962
+ return [...new Set(tags)];
963
+ }
964
+
965
+ function escapeRegExp(value) {
966
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
967
+ }
968
+
969
+ // ═══════════════════════════════════════════════════════════════════
970
+ // TEMPLATES — entry files
971
+ // ═══════════════════════════════════════════════════════════════════
972
+
973
+ function tplClaude() {
974
+ return `# CLAUDE.md
975
+
976
+ This is the Claude Code entry file for the project.
977
+
978
+ ${managedBlock()}
979
+ `;
980
+ }
981
+
982
+ function tplAgents() {
983
+ return `# AGENTS.md
984
+
985
+ This is the Codex entry file for the project.
986
+
987
+ ${managedBlock()}
988
+ `;
989
+ }
990
+
991
+ function tplAgentEntry(label) {
992
+ return `# ${label}
993
+
994
+ This is the ${label} entry file for this project.
995
+
996
+ ${managedBlock()}
997
+ `;
998
+ }
999
+
1000
+ function tplLlmsTxt(p) {
1001
+ return `${HDR_S}
1002
+ # ${p.name}
1003
+
1004
+ > LLM-facing project map for this project.
1005
+ ${HDR_E}
1006
+
1007
+ This file is a map, not a startup read. Start from the entry-file protocol and open only what the task needs.
1008
+
1009
+ ## Core
1010
+
1011
+ ${CORE_S}
1012
+ ${coreLlmsInner()}
1013
+ ${CORE_E}
1014
+
1015
+ ## Systems
1016
+
1017
+ ${SYS_S}
1018
+ _None yet._
1019
+ ${SYS_E}
1020
+
1021
+ ## Wiki
1022
+
1023
+ ${WIKI_S}
1024
+ _None yet._
1025
+ ${WIKI_E}
1026
+
1027
+ ## Optional
1028
+
1029
+ - [AGENTS.md](AGENTS.md): Codex entry file.
1030
+ - [CLAUDE.md](CLAUDE.md): Claude Code entry file.
1031
+ - [Project Memory Maintainer](skills/project-memory-maintainer/SKILL.md): local maintenance skill.
1032
+ `;
1033
+ }
1034
+
1035
+ // ═══════════════════════════════════════════════════════════════════
1036
+ // TEMPLATES — dynamic .memoc files
1037
+ // ═══════════════════════════════════════════════════════════════════
1038
+
1039
+ function tplProjectBrief(p) {
1040
+ return `# Project Brief
1041
+
1042
+ This is the shortest project summary for a fresh agent. Keep it factual and easy to scan.
1043
+
1044
+ ## Identity
1045
+
1046
+ ${ID_S}
1047
+ ${identityInner(p)}
1048
+ ${ID_E}
1049
+
1050
+ ## Current Direction
1051
+
1052
+ _Not set yet._
1053
+
1054
+ ## How To Approach
1055
+
1056
+ - Start from \`session-summary.md\`; search before opening more files.
1057
+ - Open status, handoff, rules, map, systems, or wiki docs only when the task needs them.
1058
+ - After durable work, update the smallest relevant memory set.
1059
+ - Do not treat generated output folders as source unless the user explicitly asks.
1060
+
1061
+ ## Next Useful Work
1062
+
1063
+ _None yet._
1064
+
1065
+ ## Important Notes
1066
+
1067
+ _None yet._
1068
+ `;
1069
+ }
1070
+
1071
+ function tplAgentIndex(p) {
1072
+ return `# Agent Index
1073
+
1074
+ This is the fast entry map for agents. Start here, then open only the docs relevant to the task.
1075
+
1076
+ ## Read Order
1077
+
1078
+ 1. Entry file managed block.
1079
+ 2. \`.memoc/session-summary.md\`.
1080
+ 3. Search first, then open only task-relevant files.
1081
+
1082
+ ## Project Snapshot
1083
+
1084
+ ${SNAP_S}
1085
+ ${snapshotInner(p)}
1086
+ ${SNAP_E}
1087
+
1088
+ ## Core Docs
1089
+
1090
+ - [Boot](boot.md)
1091
+ - [Project Brief](00-project-brief.md)
1092
+ - [memoc Usage](memoc-usage.md)
1093
+ - [Agent Workflow](01-agent-workflow.md)
1094
+ - [Current Project State](02-current-project-state.md)
1095
+ - [Decisions](03-decisions.md)
1096
+ - [Handoff](04-handoff.md)
1097
+ - [Done Checklist](05-done-checklist.md)
1098
+ - [Project Rules](06-project-rules.md)
1099
+ - [Session Summary](session-summary.md)
1100
+ - [Project Log](log.md)
1101
+ - [Wiki Index](wiki/index.md)
1102
+ - [Raw Sources](raw/README.md)
1103
+ - [Systems Index](systems/README.md)
1104
+
1105
+ ## System Docs
1106
+
1107
+ _None yet. Add entries when subsystems are documented._
1108
+
1109
+ ## Wiki
1110
+
1111
+ - [Wiki Index](wiki/index.md) — hub for every synthesized wiki page.
1112
+ - [Sources](wiki/sources.md) — source provenance and ingest notes.
1113
+ - [Glossary](wiki/glossary.md) — project terms and aliases.
1114
+ - [Open Questions](wiki/questions.md) — unresolved knowledge gaps.
1115
+ - [Wiki Lint](wiki/lint.md) — orphan, stale, and contradiction checks.
1116
+ `;
1117
+ }
1118
+
1119
+ function tplCurrentState(p) {
1120
+ return `# Current Project State
1121
+
1122
+ Last synced: ${nowISO()}
1123
+
1124
+ ## Current Status
1125
+
1126
+ _See Project Snapshot below. Keep only current human-written status notes here._
1127
+
1128
+ ## Project Snapshot
1129
+
1130
+ ${SNAP_S}
1131
+ ${snapshotInner(p)}
1132
+ ${SNAP_E}
1133
+
1134
+ ## Open Tasks
1135
+
1136
+ _None yet._
1137
+
1138
+ ## Completed Tasks
1139
+
1140
+ See \`.memoc/log.md\` for full history.
1141
+
1142
+ ## Commands
1143
+
1144
+ _None recorded yet._
1145
+
1146
+ ## Notes
1147
+
1148
+ _None yet._
1149
+
1150
+ ## Change Log
1151
+
1152
+ See \`.memoc/log.md\`.
1153
+ `;
1154
+ }
1155
+
1156
+ function tplSessionSummary() {
1157
+ return `# Session Summary
1158
+ Last: ${nowISO()}
1159
+ Replace this file instead of appending to it. Keep total size <800B and each section ≤3 bullets.
1160
+ Completed history belongs in \`log.md\`; incomplete/risky resume detail belongs in \`04-handoff.md\`.
1161
+ Agent-owned — updated by you, not by \`memoc update\`.
1162
+
1163
+ ## Status
1164
+ _What is the current state of the project?_
1165
+
1166
+ ## Changed
1167
+ _What changed in the last session? (code, config, decisions)_
1168
+
1169
+ ## Open Tasks
1170
+ _What still needs to be done?_
1171
+
1172
+ ## Resume
1173
+ _Where should the next agent pick up?_
1174
+ `;
1175
+ }
1176
+
1177
+ // ═══════════════════════════════════════════════════════════════════
1178
+ // TEMPLATES — static .memoc files (same for every project)
1179
+ // ═══════════════════════════════════════════════════════════════════
1180
+
1181
+ function tplBoot() {
1182
+ return `# Agent Boot
1183
+
1184
+ On-demand reference only. The entry-file managed block is authoritative.
1185
+
1186
+ ## Open Only When Needed
1187
+
1188
+ | File | When to open |
1189
+ | --- | --- |
1190
+ | \`.memoc/session-summary.md\` | Every session start (only required read) |
1191
+ | \`.memoc/02-current-project-state.md\` | Before changing behavior or checking tasks |
1192
+ | \`.memoc/04-handoff.md\` | When resuming incomplete work |
1193
+ | \`.memoc/06-project-rules.md\` | When unsure about preferences or conventions |
1194
+ | \`.memoc/01-agent-workflow.md\` | When update routing is unclear |
1195
+ | \`.memoc/05-done-checklist.md\` | Before finishing substantial work |
1196
+ | \`.memoc/03-decisions.md\` | When a durable decision was made |
1197
+ | \`.memoc/log.md\` | For append-only history |
1198
+ | \`.memoc/memoc-usage.md\` | For command details |
1199
+ | \`.memoc/systems/*.md\` | Before touching a specific subsystem |
1200
+ | \`.memoc/wiki/*.md\` | For synthesized project knowledge |
1201
+ | \`llms.txt\` | For full project file map |
1202
+
1203
+ ## Search First
1204
+
1205
+ \`memoc search "<query>"\` — returns file:line matches across memory and agent docs only.
1206
+ \`memoc grep "<query>"\` — searches project source/text files when memory docs are not enough.
1207
+ 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>"\`.
1208
+ Use it before opening any file to avoid reading more than needed.
1209
+ `;
1210
+ }
1211
+
1212
+ function tplWorkflow() {
1213
+ return `# Agent Workflow
1214
+
1215
+ Shared protocol for any coding agent.
1216
+
1217
+ ## Entry Routine
1218
+
1219
+ 1. Read the entry-file managed block.
1220
+ 2. Read \`.memoc/session-summary.md\` only.
1221
+ 3. Search before opening broad docs.
1222
+ 4. Work from the smallest relevant file set.
1223
+ 5. Update memory only when durable context changed.
1224
+
1225
+ ## Memory Update Triggers
1226
+
1227
+ | Trigger | Update |
1228
+ | --- | --- |
1229
+ | User asks "update memoc", "refresh project memory", or similar | Run \`memoc update\` first, then update relevant agent-owned memory files |
1230
+ | User creates or changes a requirement | \`02-current-project-state.md\`, \`06-project-rules.md\`, \`log.md\` |
1231
+ | Code, config, data, or assets changed | \`02-current-project-state.md\`, relevant \`systems/*.md\`, \`log.md\` |
1232
+ | Architecture or system behavior changed | relevant \`systems/*.md\`, \`03-decisions.md\` |
1233
+ | A decision should affect future agents | \`03-decisions.md\`, \`02-current-project-state.md\` |
1234
+ | Work is substantial enough to resume later | \`04-handoff.md\`, \`02-current-project-state.md\`, \`log.md\` |
1235
+ | Durable knowledge was learned | \`wiki/*.md\`, \`wiki/index.md\` |
1236
+ | Source material should feed the wiki | \`memoc ingest <path-or-url>\`, then synthesize affected \`wiki/topics/*.md\` |
1237
+ | A useful query answer should persist | \`memoc note "<title>"\`, then link related sources/topics |
1238
+ | \`session-summary.md\` exceeds 800B or starts accumulating history | Run \`memoc trim-summary\`; move history to \`log.md\`, resume details to \`04-handoff.md\` |
1239
+
1240
+ ## Usually No Update Needed
1241
+
1242
+ - Pure Q&A with no durable outcome.
1243
+ - Tiny typo-only edits.
1244
+ - Temporary exploration that finds nothing actionable.
1245
+
1246
+ ## Documentation Shape
1247
+
1248
+ - Entry files: protocol only.
1249
+ - \`session-summary.md\`: replace-only latest snapshot, <800B, max 3 bullets per section; never use as history.
1250
+ - \`02-current-project-state.md\`: current status, tasks, commands, recent notes.
1251
+ - \`04-handoff.md\`: resume context, blockers, verified/unverified checks.
1252
+ - \`03-decisions.md\`: append durable decisions only.
1253
+ - \`log.md\`: full history; keep bulky completed work here.
1254
+ - \`systems/*.md\` and \`wiki/*.md\`: on-demand durable knowledge.
1255
+ `;
1256
+ }
1257
+
1258
+ function tplDecisions() {
1259
+ return `# Decisions
1260
+
1261
+ Durable project decisions live here. Keep entries short, dated, and useful to future agents.
1262
+
1263
+ ## Decision Log
1264
+
1265
+ _None yet._
1266
+ `;
1267
+ }
1268
+
1269
+ function tplHandoff() {
1270
+ return `# Agent Handoff
1271
+
1272
+ Last synced: ${nowISO()}
1273
+
1274
+ ## What Changed
1275
+
1276
+ _None yet._
1277
+
1278
+ ## Next Steps
1279
+
1280
+ _None yet._
1281
+
1282
+ ## Blockers
1283
+
1284
+ _None yet._
1285
+
1286
+ ## Do Not Touch Without Asking
1287
+
1288
+ _None yet._
1289
+
1290
+ ## Verified
1291
+
1292
+ _None yet._
1293
+
1294
+ ## Not Verified
1295
+
1296
+ _None yet._
1297
+
1298
+ ## Resume Notes
1299
+
1300
+ _None yet._
1301
+
1302
+ ## Suggested Reads
1303
+
1304
+ Search first, then open only files named above.
1305
+ `;
1306
+ }
1307
+
1308
+ function tplDoneChecklist() {
1309
+ return `# Done Checklist
1310
+
1311
+ Run through this before saying substantial work is complete.
1312
+
1313
+ ## Code
1314
+
1315
+ - [ ] Changes compile or run without errors.
1316
+ - [ ] Relevant tests pass (or new tests were added).
1317
+ - [ ] No obvious security issues introduced.
1318
+ - [ ] No hardcoded secrets or credentials.
1319
+
1320
+ ## Memory
1321
+
1322
+ - [ ] \`.memoc/02-current-project-state.md\` reflects the new status.
1323
+ - [ ] \`.memoc/03-decisions.md\` updated if a durable decision was made.
1324
+ - [ ] \`.memoc/04-handoff.md\` updated if work is incomplete or risky.
1325
+ - [ ] \`.memoc/log.md\` has a new entry for meaningful work.
1326
+ - [ ] Relevant \`.memoc/systems/*.md\` or wiki pages updated.
1327
+
1328
+ ## Communication
1329
+
1330
+ - [ ] Final answer states what was verified and what was not.
1331
+ - [ ] Unverified risks are noted in handoff.
1332
+ `;
1333
+ }
1334
+
1335
+ function tplProjectRules() {
1336
+ return `# Project Rules
1337
+
1338
+ Durable user and project preferences live here. Update when the user gives a rule that should persist across sessions.
1339
+
1340
+ ## Operating Rules
1341
+
1342
+ - Keep \`AGENTS.md\` and \`CLAUDE.md\` as short entry files; durable context belongs under \`.memoc/\`.
1343
+ - Do not track generated output folders such as \`out/\`, \`.next/\`, \`dist/\`, \`build/\` unless the user explicitly asks.
1344
+ - Update \`.memoc/04-handoff.md\` after substantial work so the next agent can resume quickly.
1345
+ - Use \`.memoc/05-done-checklist.md\` before saying substantial work is complete.
1346
+
1347
+ ## Agent Behavior Preferences
1348
+
1349
+ - Be factual and operational in memory docs.
1350
+ - Keep logs concise; do not paste temporary command output unless it changes future work.
1351
+ - Preserve user changes and avoid reverting unrelated work.
1352
+ - State unverified parts honestly in the final answer and handoff.
1353
+
1354
+ ## Project-Specific Rules
1355
+
1356
+ _None yet._
1357
+ `;
1358
+ }
1359
+
1360
+ function tplLog() {
1361
+ return `# Project Log
1362
+
1363
+ Append-only chronological log for project memory updates.
1364
+
1365
+ ## [${nowISO()}] init | Initialized memoc memory structure.
1366
+ `;
1367
+ }
1368
+
1369
+ function tplMemocUsage() {
1370
+ return `# memoc Usage
1371
+
1372
+ This project uses \`memoc\` to maintain agent-readable project memory.
1373
+
1374
+ ## Commands
1375
+
1376
+ \`\`\`bash
1377
+ # Optional: put the project-local wrapper first in PATH for this shell
1378
+ # PowerShell: . .\\.memoc\\env.ps1
1379
+ # sh/bash: . ./.memoc/env.sh
1380
+
1381
+ # First-time setup (or re-run to update managed sections)
1382
+ memoc init
1383
+
1384
+ # Refresh memoc itself when run through npx @latest, preserving project memory
1385
+ memoc upgrade
1386
+
1387
+ # Explicitly update managed sections based on current project state
1388
+ memoc update
1389
+ memoc trim-summary
1390
+
1391
+ # Tiny status overview
1392
+ memoc summary
1393
+
1394
+ # Search memory first; add --snippets only when needed
1395
+ memoc search "<query>" --limit 12
1396
+ memoc search "<query>" --snippets --limit 5
1397
+
1398
+ # Search project source/text files when memory is not enough
1399
+ memoc grep "<query>" --limit 12
1400
+ memoc grep "<query>" --snippets --limit 5
1401
+
1402
+ # Wiki operations
1403
+ memoc ingest <path-or-url>
1404
+ memoc note "Durable topic or query result"
1405
+ memoc lint-wiki
1406
+ \`\`\`
1407
+
1408
+ 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.
1409
+
1410
+ ## Agent Read Order
1411
+
1412
+ 1. Entry-file managed block.
1413
+ 2. \`.memoc/session-summary.md\` only.
1414
+ 3. Search memory first with one or two concrete terms: \`memoc search "<query>" --limit 5\`.
1415
+ 4. Open only the matching memory file(s) that matter.
1416
+ 5. If memory is not enough, search project files: \`memoc grep "<query>" --limit 5\`.
1417
+ 6. Use \`--snippets\` only when file names are not enough.
1418
+
1419
+ 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.
1420
+
1421
+ Raw files under \`.memoc/raw/\` are intentionally not part of normal memory search. Open them only through a linked source record when provenance is needed.
1422
+
1423
+ ## When To Run Memory Updates
1424
+
1425
+ Use \`memoc update\` or \`skills/project-memory-maintainer/SKILL.md\` when:
1426
+
1427
+ - The user asks to update memoc, refresh project memory, sync project memory, or "update the project in memoc".
1428
+ - Requirements, acceptance criteria, user preferences, or project rules changed.
1429
+ - Source code, config, data, content, or package scripts changed.
1430
+ - Architecture, data flow, routing, auth, or deployment behavior changed.
1431
+ - A decision was made that future agents should not revisit blindly.
1432
+ - Work is partial, multi-step, blocked, or likely to be resumed by another agent.
1433
+ - New durable knowledge belongs in \`.memoc/wiki/\` or a subsystem doc.
1434
+
1435
+ Usually skip for pure Q&A, throwaway exploration, or tiny edits with no future impact.
1436
+
1437
+ When the user asks for a general memoc/project-memory refresh, run \`memoc update\` first. It refreshes managed sections, reconnects default wiki scaffold links, and applies Obsidian frontmatter tags. Then update only the agent-owned files whose content actually changed, such as \`.memoc/session-summary.md\`, \`.memoc/02-current-project-state.md\`, \`.memoc/04-handoff.md\`, \`.memoc/wiki/index.md\`, or \`.memoc/log.md\`.
1438
+
1439
+ \`.memoc/session-summary.md\` is a startup snapshot, not a timeline. Rewrite it in place, do not append old work. If it exceeds 800B, run \`memoc trim-summary\`; it archives the previous summary and rewrites a compact version. Put completed history in \`.memoc/log.md\`, and put unfinished/risky resume detail in \`.memoc/04-handoff.md\`.
1440
+
1441
+ ## Updating The Wiki
1442
+
1443
+ Create a new Markdown file under \`.memoc/wiki/\` when synthesized knowledge should compound across sessions.
1444
+
1445
+ - \`.memoc/raw/\`: immutable source material copied or referenced by \`memoc ingest\`.
1446
+ - \`.memoc/wiki/sources/\`: provenance records.
1447
+ - \`.memoc/wiki/topics/\`: synthesized topic pages.
1448
+ - \`.memoc/wiki/global/\`: project-wide principles.
1449
+
1450
+ After creating or editing wiki pages:
1451
+ 1. Update \`.memoc/wiki/index.md\`.
1452
+ 2. Run \`memoc lint-wiki\`.
1453
+ 3. Append \`.memoc/log.md\`.
1454
+
1455
+ Useful scaffolds:
1456
+
1457
+ \`\`\`bash
1458
+ memoc ingest path/to/source.md
1459
+ memoc ingest https://example.com/spec
1460
+ memoc note "Auth flow comparison"
1461
+ memoc lint-wiki
1462
+ \`\`\`
1463
+
1464
+ ## Updating System Docs
1465
+
1466
+ Create or update \`.memoc/systems/*.md\` when a subsystem needs durable detail.
1467
+
1468
+ Examples: \`frontend.md\`, \`deployment.md\`, \`data-sources.md\`, \`auth.md\`
1469
+ `;
1470
+ }
1471
+
1472
+ function tplSystemsReadme() {
1473
+ return `# Systems
1474
+
1475
+ Subsystem documentation for agents.
1476
+
1477
+ ## How To Use
1478
+
1479
+ Create a new \`.md\` file here when a subsystem becomes important enough that future agents should not rediscover it from scratch.
1480
+
1481
+ ## Examples
1482
+
1483
+ - \`frontend.md\` — component library, routing, state management
1484
+ - \`deployment.md\` — CI/CD, environment setup, release process
1485
+ - \`data-sources.md\` — databases, APIs, file sources
1486
+ - \`auth.md\` — authentication and authorization
1487
+ - \`design-system.md\` — colors, typography, spacing
1488
+ `;
1489
+ }
1490
+
1491
+ function tplRawReadme() {
1492
+ return `# Raw Sources
1493
+
1494
+ Immutable source material for the memoc wiki.
1495
+
1496
+ ## Rules
1497
+
1498
+ - Do not edit raw files after ingest; create a new raw file or source record when material changes.
1499
+ - Do not read raw files at session start. Search or open the linked source/topic page first.
1500
+ - Source records under [wiki/sources](../wiki/sources/README.md) summarize raw material and link to affected topics.
1501
+
1502
+ ## Subdirectories
1503
+
1504
+ - [files](files/README.md) — local files copied during ingest
1505
+ - [urls](urls/README.md) — URL references and fetched/exported material
1506
+ - [conversations](conversations/README.md) — conversation excerpts worth preserving
1507
+ - [docs](docs/README.md) — external docs, specs, and long references
1508
+ `;
1509
+ }
1510
+
1511
+ function tplRawFilesReadme() {
1512
+ return `# Raw Files
1513
+
1514
+ Local files copied by \`memoc ingest <path>\`.
1515
+
1516
+ ## Related
1517
+
1518
+ - [Raw Sources](../README.md)
1519
+ - [Source Records](../../wiki/sources/README.md)
1520
+ `;
1521
+ }
1522
+
1523
+ function tplRawUrlsReadme() {
1524
+ return `# Raw URLs
1525
+
1526
+ URL references recorded by \`memoc ingest <url>\`.
1527
+
1528
+ ## Related
1529
+
1530
+ - [Raw Sources](../README.md)
1531
+ - [Source Records](../../wiki/sources/README.md)
1532
+ `;
1533
+ }
1534
+
1535
+ function tplRawConversationsReadme() {
1536
+ return `# Raw Conversations
1537
+
1538
+ Conversation excerpts that should feed durable wiki synthesis.
1539
+
1540
+ ## Related
1541
+
1542
+ - [Raw Sources](../README.md)
1543
+ - [Source Records](../../wiki/sources/README.md)
1544
+ `;
1545
+ }
1546
+
1547
+ function tplRawDocsReadme() {
1548
+ return `# Raw Docs
1549
+
1550
+ Long-form docs, specs, and references kept separate from synthesized topic pages.
1551
+
1552
+ ## Related
1553
+
1554
+ - [Raw Sources](../README.md)
1555
+ - [Source Records](../../wiki/sources/README.md)
1556
+ `;
1557
+ }
1558
+
1559
+ function tplWikiIndex() {
1560
+ return `# Wiki Index
1561
+
1562
+ Persistent LLM-maintained project wiki.
1563
+
1564
+ ## Graph Hubs
1565
+
1566
+ - [Raw Sources](../raw/README.md) — immutable source material before synthesis.
1567
+ - [Sources](sources.md) — provenance, ingests, and source-to-topic links.
1568
+ - [Topics](topics/README.md) — synthesized topic pages.
1569
+ - [Global](global/README.md) — project-wide principles and long-lived direction.
1570
+ - [Glossary](glossary.md) — terms, aliases, and canonical page names.
1571
+ - [Open Questions](questions.md) — unresolved questions and research leads.
1572
+ - [Wiki Lint](lint.md) — graph health, orphan checks, contradictions, stale claims.
1573
+
1574
+ ## Pages
1575
+
1576
+ _None yet. Add every wiki page here with a relative Markdown link and one-line summary._
1577
+
1578
+ ## Saved Queries
1579
+
1580
+ _None yet. Use \`memoc note "<title>"\` for durable analysis or query results that should become a topic._
1581
+
1582
+ ## Subdirectories
1583
+
1584
+ - [sources/](sources/README.md) — provenance records
1585
+ - [topics/](topics/README.md) — synthesized topic pages
1586
+ - [global/](global/README.md) — project-wide principles
1587
+
1588
+ ## Related Core Memory
1589
+
1590
+ - [Agent Index](../00-agent-index.md)
1591
+ - [Project Brief](../00-project-brief.md)
1592
+ - [Current Project State](../02-current-project-state.md)
1593
+ - [Project Log](../log.md)
1594
+ `;
1595
+ }
1596
+
1597
+ function tplWikiSources() {
1598
+ return `# Sources
1599
+
1600
+ Provenance index for conversations, URLs, docs, issues, and files that feed the wiki.
1601
+
1602
+ ## Source Records
1603
+
1604
+ _No sources recorded yet. Link each source record to the topic/global pages it affects._
1605
+
1606
+ Use \`memoc ingest <path-or-url>\` to create source records without loading raw material into startup context.
1607
+
1608
+ ## Related
1609
+
1610
+ - [Wiki Index](index.md)
1611
+ - [Raw Sources](../raw/README.md)
1612
+ - [Source Records Directory](sources/README.md)
1613
+ - [Topics](topics/README.md)
1614
+ - [Open Questions](questions.md)
1615
+ `;
1616
+ }
1617
+
1618
+ function tplWikiGlossary() {
1619
+ return `# Glossary
1620
+
1621
+ Canonical names, aliases, and short definitions for project terms.
1622
+
1623
+ ## Terms
1624
+
1625
+ _No terms defined yet. Link terms to their canonical topic, global, source, or system page._
1626
+
1627
+ ## Related
1628
+
1629
+ - [Wiki Index](index.md)
1630
+ - [Topics](topics/README.md)
1631
+ - [Global](global/README.md)
1632
+ - [Open Questions](questions.md)
1633
+ `;
1634
+ }
1635
+
1636
+ function tplWikiQuestions() {
1637
+ return `# Open Questions
1638
+
1639
+ Unresolved questions, data gaps, contradictions, and follow-up research leads.
1640
+
1641
+ ## Questions
1642
+
1643
+ _No open questions yet. Link each question to affected pages and sources._
1644
+
1645
+ ## Related
1646
+
1647
+ - [Wiki Index](index.md)
1648
+ - [Sources](sources.md)
1649
+ - [Topics](topics/README.md)
1650
+ - [Wiki Lint](lint.md)
1651
+ `;
1652
+ }
1653
+
1654
+ function tplWikiSourcesReadme() {
1655
+ return `# Sources
1656
+
1657
+ Provenance records for conversations, URLs, docs, and issues.
1658
+
1659
+ ## How To Link
1660
+
1661
+ - Keep source pages short: summary, raw location, affected pages, open synthesis work.
1662
+ - Link each source record back to [Sources](../sources.md).
1663
+ - Link outward to every topic, global page, system doc, or question that the source changes.
1664
+ - Prefer one source per file when the source is substantial enough to cite later.
1665
+
1666
+ ## Related
1667
+
1668
+ - [Wiki Index](../index.md)
1669
+ - [Sources](../sources.md)
1670
+ - [Topics](../topics/README.md)
1671
+ - [Open Questions](../questions.md)
1672
+ `;
1673
+ }
1674
+
1675
+ function tplWikiTopicsReadme() {
1676
+ return `# Topics
1677
+
1678
+ Synthesized topic pages that compound knowledge across sessions.
1679
+
1680
+ ## Topic Pages
1681
+
1682
+ _None yet. Add pages here when a concept deserves durable synthesis._
1683
+
1684
+ ## How To Link
1685
+
1686
+ - Each topic page should link back to [Wiki Index](../index.md) and this [Topics](README.md) page.
1687
+ - Link to related topics, source records, glossary terms, and open questions in prose or a \`## Related\` section.
1688
+ - Avoid orphan pages: every topic needs at least one inbound link from an index, source, or related topic.
1689
+
1690
+ ## Related
1691
+
1692
+ - [Wiki Index](../index.md)
1693
+ - [Sources](../sources.md)
1694
+ - [Glossary](../glossary.md)
1695
+ - [Wiki Lint](../lint.md)
1696
+ `;
1697
+ }
1698
+
1699
+ function tplWikiGlobalReadme() {
1700
+ return `# Global
1701
+
1702
+ Project-wide principles, positioning, and long-lived direction.
1703
+
1704
+ ## Global Pages
1705
+
1706
+ _None yet. Add pages here for broad context that many topic/system pages should reference._
1707
+
1708
+ ## How To Link
1709
+
1710
+ - Link global pages back to [Wiki Index](../index.md), this [Global](README.md) page, and affected topic/system docs.
1711
+ - Use global pages for durable synthesis, not temporary task notes.
1712
+
1713
+ ## Related
1714
+
1715
+ - [Wiki Index](../index.md)
1716
+ - [Project Brief](../../00-project-brief.md)
1717
+ - [Project Rules](../../06-project-rules.md)
1718
+ - [Topics](../topics/README.md)
1719
+ `;
1720
+ }
1721
+ function tplWikiLint() {
1722
+ return `# Wiki Lint
1723
+
1724
+ Last checked: ${nowISO()}
1725
+
1726
+ ## Graph Checks
1727
+
1728
+ - Every wiki page is listed from [Wiki Index](index.md) or a directory README.
1729
+ - Every wiki page links back to an index, hub, source, topic, or related page.
1730
+ - Important concepts mentioned in two or more places have their own linked page.
1731
+ - Source records link to the pages they update, and those pages link back to sources when provenance matters.
1732
+
1733
+ ## Issues
1734
+
1735
+ _No issues found._
1736
+
1737
+ ## Warnings
1738
+
1739
+ _None._
1740
+
1741
+ ## Related
1742
+
1743
+ - [Wiki Index](index.md)
1744
+ - [Sources](sources.md)
1745
+ - [Topics](topics/README.md)
1746
+ - [Open Questions](questions.md)
1747
+ `;
1748
+ }
1749
+
1750
+ function tplSkillMaintainer() {
1751
+ return `---
1752
+ name: project-memory-maintainer
1753
+ description: Maintain this project's LLM-wiki memory files after durable context changes.
1754
+ ---
1755
+
1756
+ # Project Memory Maintainer
1757
+
1758
+ Use this local skill after meaningful project work so future agents can continue without rediscovering context.
1759
+
1760
+ ## Required Reads
1761
+
1762
+ 1. \`.memoc/session-summary.md\`
1763
+ 2. \`memoc summary\` or \`memoc search "<query>"\`; use \`memoc grep "<query>"\` only when source/text search is needed
1764
+ 3. Open only files you will use or update.
1765
+
1766
+ ## Maintenance Checklist
1767
+
1768
+ - If the user asked to update/refresh memoc project memory, run \`memoc update\` first so managed sections, wiki scaffold links, and Obsidian tags are current.
1769
+ - Keep \`llms.txt\` and \`.memoc/00-agent-index.md\` as concise maps.
1770
+ - Keep \`.memoc/00-project-brief.md\` as the shortest project summary.
1771
+ - Rewrite \`.memoc/session-summary.md\` as the latest snapshot only; never append a timeline. If it is over 800B, run \`memoc trim-summary\`.
1772
+ - Update \`.memoc/02-current-project-state.md\` with new status, tasks, commands, and change log entries.
1773
+ - Update \`.memoc/03-decisions.md\` when a durable decision is made.
1774
+ - Update \`.memoc/04-handoff.md\` before ending substantial work.
1775
+ - Check \`.memoc/05-done-checklist.md\` before saying substantial work is complete.
1776
+ - Update \`.memoc/06-project-rules.md\` when the user gives durable preferences.
1777
+ - Append \`.memoc/log.md\` for meaningful changes, decisions, and handoffs.
1778
+ - Create or update \`.memoc/systems/*.md\` when a subsystem needs durable explanation.
1779
+ - Create or update \`.memoc/wiki/*.md\` when synthesized knowledge should compound over time.
1780
+ - Use \`memoc ingest <path-or-url>\` for source material and \`memoc note "<title>"\` for durable query results or analysis.
1781
+ - 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.
1782
+ - Run \`memoc lint-wiki\` after wiki/source/topic edits and address broken links before finishing.
1783
+ - Keep completed history in \`.memoc/log.md\`; keep current-state files short.
1784
+ - Move completed session details out of \`session-summary.md\` into \`log.md\`; move incomplete/risky resume details into \`04-handoff.md\`.
1785
+ - Keep tool output small; prefer \`summary\`, file-only search, \`--limit\`, and targeted reads.
1786
+
1787
+ ## Wiki Link Rules
1788
+
1789
+ - Use relative Markdown links that Obsidian can follow, for example \`[Glossary](glossary.md)\` or \`[Topics](topics/README.md)\`.
1790
+ - Every wiki page must have at least one inbound link from \`wiki/index.md\`, a directory README, a source page, or a related topic.
1791
+ - Every wiki page must link outward to its parent hub plus 1-5 genuinely related pages when they exist.
1792
+ - Prefer links in normal prose when the connection is meaningful; use \`## Related\` for compact navigation.
1793
+ - When a concept appears in multiple pages, create or update a topic/glossary page and link all mentions to it.
1794
+ - After wiki edits, check \`.memoc/wiki/lint.md\` and note orphan pages, missing backlinks, contradictions, or stale claims.
1795
+
1796
+ ## Concrete Triggers
1797
+
1798
+ Use this skill before finishing when any of these are true:
1799
+
1800
+ - The user gives a durable preference, project rule, changed requirement, or acceptance criterion.
1801
+ - The agent edits code, config, package scripts, env, data, assets, routes, or deployment files.
1802
+ - A subsystem's behavior, architecture, data flow, or API contract changes.
1803
+ - A future agent would need to know why an approach was chosen or rejected.
1804
+ - The work is partial, blocked, risky, multi-step, or likely to be resumed later.
1805
+
1806
+ Usually skip for pure Q&A, tiny edits with no future impact, or throwaway exploration.
1807
+ `;
1808
+ }
1809
+
1810
+ // ═══════════════════════════════════════════════════════════════════
1811
+ // CLAUDE CODE HOOK SETTINGS
1812
+ // ═══════════════════════════════════════════════════════════════════
1813
+
1814
+ function claudeStopHookCmd() {
1815
+ 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{}"`;
1816
+ }
1817
+
1818
+ function tplClaudeSettings() {
1819
+ return JSON.stringify({
1820
+ hooks: {
1821
+ Stop: [{ matcher: '', hooks: [{ type: 'command', command: claudeStopHookCmd() }] }],
1822
+ },
1823
+ }, null, 2) + '\n';
1824
+ }
1825
+
1826
+ function ensureClaudeStopHook(settingsPath) {
1827
+ const cmd = claudeStopHookCmd();
1828
+ let settings;
1829
+ try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); }
1830
+ catch { settings = {}; }
1831
+
1832
+ if (!settings.hooks || typeof settings.hooks !== 'object' || Array.isArray(settings.hooks)) settings.hooks = {};
1833
+ if (!Array.isArray(settings.hooks.Stop)) settings.hooks.Stop = [];
1834
+
1835
+ let hasCurrent = false;
1836
+ let changed = false;
1837
+ for (const entry of settings.hooks.Stop) {
1838
+ if (!Array.isArray(entry.hooks)) continue;
1839
+ const nextHooks = [];
1840
+ for (const hook of entry.hooks) {
1841
+ if (hook && hook.command === cmd) {
1842
+ if (hasCurrent) changed = true;
1843
+ else {
1844
+ hasCurrent = true;
1845
+ nextHooks.push(hook);
1846
+ }
1847
+ } else if (isMemocClaudeStopHook(hook)) {
1848
+ changed = true;
1849
+ } else {
1850
+ nextHooks.push(hook);
1851
+ }
1852
+ }
1853
+ entry.hooks = nextHooks;
1854
+ }
1855
+ settings.hooks.Stop = settings.hooks.Stop.filter(entry =>
1856
+ !Array.isArray(entry.hooks) || entry.hooks.length > 0
1857
+ );
1858
+ if (hasCurrent && !changed) return false; // no change needed
1859
+
1860
+ if (!hasCurrent) settings.hooks.Stop.push({ matcher: '', hooks: [{ type: 'command', command: cmd }] });
1861
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
1862
+ return true; // merged or migrated
1863
+ }
1864
+
1865
+ function isMemocClaudeStopHook(hook) {
1866
+ if (!hook || typeof hook.command !== 'string') return false;
1867
+ const command = hook.command;
1868
+ return command.includes('.memoc/.pending') &&
1869
+ command.includes('git') &&
1870
+ command.includes('status') &&
1871
+ command.includes('--porcelain');
1872
+ }
1873
+
1874
+ // ═══════════════════════════════════════════════════════════════════
1875
+ // MANAGED BLOCK UPDATE (CLAUDE.md / AGENTS.md)
1876
+ // ═══════════════════════════════════════════════════════════════════
1877
+
1878
+ function ensureClaudeStopHookFile(dir, mark) {
1879
+ const claudeDir = path.join(dir, '.claude');
1880
+ const claudeSettings = path.join(claudeDir, 'settings.json');
1881
+ fs.mkdirSync(claudeDir, { recursive: true });
1882
+ if (!fs.existsSync(claudeSettings)) {
1883
+ write(claudeSettings, tplClaudeSettings());
1884
+ mark('add', '.claude/settings.json');
1885
+ return;
1886
+ }
1887
+ const merged = ensureClaudeStopHook(claudeSettings);
1888
+ mark(merged ? 'update' : 'skip', `.claude/settings.json (Stop hook ${merged ? 'merged' : 'already present'})`);
1889
+ }
1890
+
1891
+ function ensurePendingGitignore(dir, mark) {
1892
+ const gitignorePath = path.join(dir, '.gitignore');
1893
+ const PENDING_ENTRY = '.memoc/.pending';
1894
+ const gitignoreContent = fs.existsSync(gitignorePath)
1895
+ ? fs.readFileSync(gitignorePath, 'utf8') : '';
1896
+ const hasPendingEntry = gitignoreContent
1897
+ .split(/\r?\n/)
1898
+ .some(line => line.trim() === PENDING_ENTRY);
1899
+ if (!hasPendingEntry) {
1900
+ fs.appendFileSync(gitignorePath, (gitignoreContent.endsWith('\n') ? '' : '\n') + PENDING_ENTRY + '\n', 'utf8');
1901
+ mark('update', '.gitignore (.memoc/.pending added)');
1902
+ } else {
1903
+ mark('skip', '.gitignore (.memoc/.pending already present)');
1904
+ }
1905
+ }
1906
+
1907
+ function printCommandHint() {
1908
+ console.log('\n Agent command fallback:');
1909
+ console.log(' memoc summary');
1910
+ console.log(' .\\.memoc\\bin\\memoc.cmd summary # Windows');
1911
+ console.log(' .memoc/bin/memoc summary # macOS/Linux sh');
1912
+ console.log(' If PATH fails once, use the project-local wrapper for the rest of the session.');
1913
+ }
1914
+
1915
+ function applyManagedBlock(filePath, tplFn) {
1916
+ if (!fs.existsSync(filePath)) {
1917
+ write(filePath, tplFn());
1918
+ return 'add';
1919
+ }
1920
+ const src = fs.readFileSync(filePath, 'utf8');
1921
+ const range = findMarkedRange(src, MGMT_S, MGMT_E);
1922
+ if (!range) {
1923
+ // No managed block — inject at end, preserving all user content
1924
+ write(filePath, src.trimEnd() + '\n\n' + managedBlock() + '\n');
1925
+ return 'inject';
1926
+ }
1927
+ write(filePath, src.slice(0, range.s) + managedBlock() + src.slice(range.e + range.endMark.length));
1928
+ return 'update';
1929
+ }
1930
+
1931
+ // ═══════════════════════════════════════════════════════════════════
1932
+ // MAIN RUNNER
1933
+ // ═══════════════════════════════════════════════════════════════════
1934
+
1935
+ function run(dir, forceUpdate, action = 'update') {
1936
+ const p = scanProject(dir);
1937
+ const memDir = path.join(dir, '.memoc');
1938
+ const isNew = !fs.existsSync(path.join(memDir, 'boot.md'));
1939
+ const mode = (isNew && !forceUpdate) ? 'init' : 'update';
1940
+
1941
+ const log = [];
1942
+ const mark = (label, name) => log.push(` ${label.padEnd(8)} ${name}`);
1943
+
1944
+ if (mode === 'init') {
1945
+ console.log(`\n memoc init — ${path.basename(dir)}`);
1946
+ console.log(p.isEmpty
1947
+ ? ' Empty project → using default values.'
1948
+ : ` Detected: ${stackStr(p.stack)}`
1949
+ );
1950
+ console.log();
1951
+
1952
+ // Entry files — inject/update managed block, preserve existing user content
1953
+ mark(applyManagedBlock(path.join(dir, 'CLAUDE.md'), tplClaude), 'CLAUDE.md');
1954
+ mark(applyManagedBlock(path.join(dir, 'AGENTS.md'), tplAgents), 'AGENTS.md');
1955
+ if (ensure(path.join(dir, 'llms.txt'), tplLlmsTxt(p))) mark('add', 'llms.txt');
1956
+ else mark('skip', 'llms.txt');
1957
+
1958
+ // Dynamic memory files
1959
+ const dynamicFiles = [
1960
+ [path.join(memDir, '00-project-brief.md'), () => tplProjectBrief(p)],
1961
+ [path.join(memDir, '00-agent-index.md'), () => tplAgentIndex(p)],
1962
+ [path.join(memDir, '02-current-project-state.md'), () => tplCurrentState(p)],
1963
+ [path.join(memDir, 'session-summary.md'), tplSessionSummary],
1964
+ ];
1965
+ for (const [fp, tpl] of dynamicFiles) {
1966
+ const rel = path.relative(dir, fp);
1967
+ if (ensure(fp, tpl())) mark('add', rel); else mark('skip', rel);
1968
+ }
1969
+
1970
+ // Static memory files
1971
+ const staticFiles = [
1972
+ [path.join(memDir, 'boot.md'), tplBoot],
1973
+ [path.join(memDir, '01-agent-workflow.md'), tplWorkflow],
1974
+ [path.join(memDir, '03-decisions.md'), tplDecisions],
1975
+ [path.join(memDir, '04-handoff.md'), tplHandoff],
1976
+ [path.join(memDir, '05-done-checklist.md'), tplDoneChecklist],
1977
+ [path.join(memDir, '06-project-rules.md'), tplProjectRules],
1978
+ [path.join(memDir, 'log.md'), tplLog],
1979
+ [path.join(memDir, 'memoc-usage.md'), tplMemocUsage],
1980
+ [path.join(memDir, 'systems/README.md'), tplSystemsReadme],
1981
+ [path.join(memDir, 'raw/README.md'), tplRawReadme],
1982
+ [path.join(memDir, 'raw/files/README.md'), tplRawFilesReadme],
1983
+ [path.join(memDir, 'raw/urls/README.md'), tplRawUrlsReadme],
1984
+ [path.join(memDir, 'raw/conversations/README.md'), tplRawConversationsReadme],
1985
+ [path.join(memDir, 'raw/docs/README.md'), tplRawDocsReadme],
1986
+ [path.join(memDir, 'wiki/index.md'), tplWikiIndex],
1987
+ [path.join(memDir, 'wiki/sources.md'), tplWikiSources],
1988
+ [path.join(memDir, 'wiki/glossary.md'), tplWikiGlossary],
1989
+ [path.join(memDir, 'wiki/questions.md'), tplWikiQuestions],
1990
+ [path.join(memDir, 'wiki/lint.md'), tplWikiLint],
1991
+ [path.join(memDir, 'wiki/sources/README.md'), tplWikiSourcesReadme],
1992
+ [path.join(memDir, 'wiki/topics/README.md'), tplWikiTopicsReadme],
1993
+ [path.join(memDir, 'wiki/global/README.md'), tplWikiGlobalReadme],
1994
+ [path.join(dir, 'skills/project-memory-maintainer/SKILL.md'), tplSkillMaintainer],
1995
+ ];
1996
+ for (const [fp, tpl] of staticFiles) {
1997
+ const rel = path.relative(dir, fp);
1998
+ if (ensure(fp, tpl())) mark('add', rel); else mark('skip', rel);
1999
+ }
2000
+
2001
+ // Claude Code Stop hook — writes .memoc/.pending when git detects changes
2002
+ ensureClaudeStopHookFile(dir, mark);
2003
+
2004
+ // .gitignore — add .memoc/.pending if not already present
2005
+ ensurePendingGitignore(dir, mark);
2006
+
2007
+ // Obsidian graph filters — tag memoc-owned Markdown without touching unrelated docs
2008
+ ensureObsidianFrontmatter(dir, mark);
2009
+
2010
+ // PATH helpers — let agents run memoc even when the npm bin is not on PATH
2011
+ ensurePathHelpers(dir, mark);
2012
+ ensurePathRegistration(dir, mark);
2013
+
2014
+ } else {
2015
+ // ── UPDATE MODE
2016
+ console.log(`\n memoc ${action} — ${path.basename(dir)}`);
2017
+ console.log(` Re-scanning project: ${p.isEmpty ? 'nothing detected' : stackStr(p.stack)}`);
2018
+ console.log();
2019
+
2020
+ // Entry files — update managed blocks, preserve user content
2021
+ mark(applyManagedBlock(path.join(dir, 'CLAUDE.md'), tplClaude), 'CLAUDE.md');
2022
+ mark(applyManagedBlock(path.join(dir, 'AGENTS.md'), tplAgents), 'AGENTS.md');
2023
+
2024
+ // Third-party agent files — update only if already added
2025
+ for (const [, agent] of Object.entries(AGENT_REGISTRY)) {
2026
+ const fp = path.join(dir, agent.file);
2027
+ if (fs.existsSync(fp)) {
2028
+ mark(applyManagedBlock(fp, () => tplAgentEntry(agent.label)), agent.file);
2029
+ }
2030
+ }
2031
+
2032
+ // llms.txt — update all managed sections
2033
+ const llmsPath = path.join(dir, 'llms.txt');
2034
+ if (fs.existsSync(llmsPath)) {
2035
+ updateSection(llmsPath, HDR_S, HDR_E, headerInner(p));
2036
+ updateSection(llmsPath, CORE_S, CORE_E, coreLlmsInner());
2037
+ updateSection(llmsPath, SYS_S, SYS_E, systemsLlmsInner(dir));
2038
+ updateSection(llmsPath, WIKI_S, WIKI_E, wikiLlmsInner(dir));
2039
+ mark('update', 'llms.txt');
2040
+ } else {
2041
+ write(llmsPath, tplLlmsTxt(p));
2042
+ mark('add', 'llms.txt');
2043
+ }
2044
+
2045
+ // Dynamic memory files — update managed sections only
2046
+ const dynUpdates = [
2047
+ [path.join(memDir, '00-project-brief.md'), () => tplProjectBrief(p), ID_S, ID_E, identityInner(p)],
2048
+ [path.join(memDir, '00-agent-index.md'), () => tplAgentIndex(p), SNAP_S, SNAP_E, snapshotInner(p)],
2049
+ [path.join(memDir, '02-current-project-state.md'), () => tplCurrentState(p), SNAP_S, SNAP_E, snapshotInner(p)],
2050
+ ];
2051
+ for (const [fp, tpl, s, e, inner] of dynUpdates) {
2052
+ const rel = path.relative(dir, fp);
2053
+ if (!fs.existsSync(fp)) {
2054
+ write(fp, tpl());
2055
+ mark('add', rel);
2056
+ } else if (updateSection(fp, s, e, inner)) {
2057
+ mark('update', `${rel} (managed section)`);
2058
+ } else {
2059
+ mark('skip', rel);
2060
+ }
2061
+ }
2062
+
2063
+ // session-summary is agent-owned — never overwrite, only add if missing
2064
+ const summaryPath = path.join(memDir, 'session-summary.md');
2065
+ if (fs.existsSync(summaryPath)) {
2066
+ const summarySize = Buffer.byteLength(fs.readFileSync(summaryPath, 'utf8'), 'utf8');
2067
+ if (summarySize > 1000) {
2068
+ console.log(` ⚠ session-summary.md is ${summarySize}B (recommended: <800B).`);
2069
+ }
2070
+ mark('skip', '.memoc/session-summary.md (agent-owned, not modified)');
2071
+ } else {
2072
+ write(summaryPath, tplSessionSummary());
2073
+ mark('add', '.memoc/session-summary.md');
2074
+ }
2075
+
2076
+ // Static + user-owned files — only add if missing
2077
+ const addIfMissing = [
2078
+ [path.join(memDir, 'boot.md'), tplBoot],
2079
+ [path.join(memDir, '01-agent-workflow.md'), tplWorkflow],
2080
+ [path.join(memDir, '03-decisions.md'), tplDecisions],
2081
+ [path.join(memDir, '04-handoff.md'), tplHandoff],
2082
+ [path.join(memDir, '05-done-checklist.md'), tplDoneChecklist],
2083
+ [path.join(memDir, '06-project-rules.md'), tplProjectRules],
2084
+ [path.join(memDir, 'log.md'), tplLog],
2085
+ [path.join(memDir, 'memoc-usage.md'), tplMemocUsage],
2086
+ [path.join(memDir, 'systems/README.md'), tplSystemsReadme],
2087
+ [path.join(memDir, 'raw/README.md'), tplRawReadme],
2088
+ [path.join(memDir, 'raw/files/README.md'), tplRawFilesReadme],
2089
+ [path.join(memDir, 'raw/urls/README.md'), tplRawUrlsReadme],
2090
+ [path.join(memDir, 'raw/conversations/README.md'), tplRawConversationsReadme],
2091
+ [path.join(memDir, 'raw/docs/README.md'), tplRawDocsReadme],
2092
+ [path.join(memDir, 'wiki/index.md'), tplWikiIndex],
2093
+ [path.join(memDir, 'wiki/sources.md'), tplWikiSources],
2094
+ [path.join(memDir, 'wiki/glossary.md'), tplWikiGlossary],
2095
+ [path.join(memDir, 'wiki/questions.md'), tplWikiQuestions],
2096
+ [path.join(memDir, 'wiki/lint.md'), tplWikiLint],
2097
+ [path.join(memDir, 'wiki/sources/README.md'), tplWikiSourcesReadme],
2098
+ [path.join(memDir, 'wiki/topics/README.md'), tplWikiTopicsReadme],
2099
+ [path.join(memDir, 'wiki/global/README.md'), tplWikiGlobalReadme],
2100
+ [path.join(dir, 'skills/project-memory-maintainer/SKILL.md'), tplSkillMaintainer],
2101
+ ];
2102
+ for (const [fp, tpl] of addIfMissing) {
2103
+ const rel = path.relative(dir, fp);
2104
+ if (ensure(fp, tpl())) mark('add', rel);
2105
+ // silently skip existing — user/agent owns them
2106
+ }
2107
+ ensureWikiScaffoldLinks(memDir, mark);
2108
+
2109
+ // Obsidian graph filters — add/merge memoc tags for existing installs too
2110
+ ensureObsidianFrontmatter(dir, mark);
2111
+
2112
+ // PATH helpers — let agents run memoc even when the npm bin is not on PATH
2113
+ ensureClaudeStopHookFile(dir, mark);
2114
+ ensurePendingGitignore(dir, mark);
2115
+ ensurePathHelpers(dir, mark);
2116
+ ensurePathRegistration(dir, mark);
2117
+
2118
+ // Append update record to log.md
2119
+ const logPath = path.join(memDir, 'log.md');
2120
+ if (fs.existsSync(logPath)) {
2121
+ fs.appendFileSync(logPath,
2122
+ `\n## [${nowISO()}] ${action} | Re-scanned: ${p.isEmpty ? 'nothing detected' : stackStr(p.stack)}\n`,
2123
+ 'utf8'
2124
+ );
2125
+ mark('append', '.memoc/log.md');
2126
+ }
2127
+ }
2128
+
2129
+ hideOnWindows(memDir);
2130
+ console.log(log.join('\n'));
2131
+ printCommandHint();
2132
+ console.log('\n Done.');
2133
+ }
2134
+
2135
+ // ═══════════════════════════════════════════════════════════════════
2136
+ // ADD — add entry file for a specific agent
2137
+ // ═══════════════════════════════════════════════════════════════════
2138
+
2139
+ function runAdd(dir) {
2140
+ const agentKey = (process.argv[3] || '').toLowerCase();
2141
+
2142
+ if (!agentKey) {
2143
+ console.log('\n Available agents:\n');
2144
+ for (const [key, agent] of Object.entries(AGENT_REGISTRY)) {
2145
+ const exists = fs.existsSync(path.join(dir, agent.file)) ? ' (already added)' : '';
2146
+ console.log(` ${key.padEnd(10)} → ${agent.file}${exists}`);
2147
+ }
2148
+ console.log('\n Usage: memoc add <agent>');
2149
+ return;
2150
+ }
2151
+
2152
+ const agent = AGENT_REGISTRY[agentKey];
2153
+ if (!agent) {
2154
+ console.error(`\n Unknown agent: "${agentKey}"`);
2155
+ console.error(` Available: ${Object.keys(AGENT_REGISTRY).join(', ')}`);
2156
+ process.exit(1);
2157
+ }
2158
+
2159
+ const filePath = path.join(dir, agent.file);
2160
+ const result = applyManagedBlock(filePath, () => tplAgentEntry(agent.label));
2161
+ console.log(`\n ${result.padEnd(8)} ${agent.file} (${agent.label})`);
2162
+ console.log('\n Done.');
2163
+ }
2164
+
2165
+ // ═══════════════════════════════════════════════════════════════════
2166
+ // WIKI OPERATIONS — lint, ingest, and durable topic notes
2167
+ // ═══════════════════════════════════════════════════════════════════
2168
+
2169
+ function runWikiLint(dir) {
2170
+ ensureObsidianFrontmatter(dir, () => {});
2171
+ const wikiDir = path.join(dir, '.memoc', 'wiki');
2172
+ const files = listMarkdownFiles(wikiDir);
2173
+ const issues = [];
2174
+ const warnings = [];
2175
+ const inbound = new Map(files.map(fp => [normRel(dir, fp), 0]));
2176
+
2177
+ for (const fp of files) {
2178
+ const rel = normRel(dir, fp);
2179
+ const src = safeRead(fp);
2180
+ if (!parseYamlFrontmatter(src)) warnings.push(`${rel}: missing YAML frontmatter`);
2181
+ if (!src.includes('memoc/wiki')) warnings.push(`${rel}: missing memoc/wiki tag`);
2182
+ if (!/^## Related\b/m.test(src) && !rel.endsWith('wiki/index.md')) warnings.push(`${rel}: missing ## Related section`);
2183
+
2184
+ for (const link of markdownLinks(src)) {
2185
+ if (/^[a-z][a-z0-9+.-]*:/i.test(link) || link.startsWith('#')) continue;
2186
+ const target = resolveMarkdownLink(fp, link);
2187
+ if (!target) continue;
2188
+ if (!fs.existsSync(target)) {
2189
+ issues.push(`${rel}: broken link ${link}`);
2190
+ continue;
2191
+ }
2192
+ const targetRel = normRel(dir, target);
2193
+ if (inbound.has(targetRel)) inbound.set(targetRel, inbound.get(targetRel) + 1);
2194
+ }
2195
+ }
2196
+
2197
+ for (const [rel, count] of inbound.entries()) {
2198
+ if (rel.endsWith('wiki/index.md')) continue;
2199
+ if (count === 0) warnings.push(`${rel}: no inbound wiki links`);
2200
+ }
2201
+
2202
+ const lintPath = path.join(wikiDir, 'lint.md');
2203
+ write(lintPath, wikiLintReport(issues, warnings));
2204
+ ensureMemocFrontmatter(lintPath, dir);
2205
+
2206
+ console.log('\n memoc lint-wiki\n');
2207
+ console.log(` Files ${files.length}`);
2208
+ console.log(` Issues ${issues.length}`);
2209
+ console.log(` Warnings ${warnings.length}`);
2210
+ console.log(' Report .memoc/wiki/lint.md');
2211
+ if (issues.length) {
2212
+ console.log('\n Issues:');
2213
+ for (const issue of issues.slice(0, 10)) console.log(` - ${issue}`);
2214
+ }
2215
+ if (!issues.length && !warnings.length) console.log('\n No issues found.');
2216
+ console.log();
2217
+ }
2218
+
2219
+ function wikiLintReport(issues, warnings) {
2220
+ return `# Wiki Lint
2221
+
2222
+ Last checked: ${nowISO()}
2223
+
2224
+ ## Graph Checks
2225
+
2226
+ - Every wiki page is listed from [Wiki Index](index.md) or a directory README.
2227
+ - Every wiki page links back to an index, hub, source, topic, or related page.
2228
+ - Important concepts mentioned in two or more places have their own linked page.
2229
+ - Source records link to the pages they update, and those pages link back to sources when provenance matters.
2230
+
2231
+ ## Issues
2232
+
2233
+ ${issues.length ? issues.map(x => `- ${x}`).join('\n') : '_No issues found._'}
2234
+
2235
+ ## Warnings
2236
+
2237
+ ${warnings.length ? warnings.map(x => `- ${x}`).join('\n') : '_None._'}
2238
+
2239
+ ## Related
2240
+
2241
+ - [Wiki Index](index.md)
2242
+ - [Sources](sources.md)
2243
+ - [Topics](topics/README.md)
2244
+ - [Open Questions](questions.md)
2245
+ `;
2246
+ }
2247
+
2248
+ function runIngest(dir) {
2249
+ const target = process.argv[3];
2250
+ if (!target) {
2251
+ console.error('\n Usage: memoc ingest <path-or-url>');
2252
+ process.exit(1);
2253
+ }
2254
+
2255
+ ensureMemocBase(dir);
2256
+ const isUrl = /^[a-z][a-z0-9+.-]*:\/\//i.test(target);
2257
+ const title = ingestTitle(dir, target, isUrl);
2258
+ const slug = `${todayISO()}-${slugify(title, 'source')}`;
2259
+ let rawRef;
2260
+ let rawDisplay;
2261
+
2262
+ if (isUrl) {
2263
+ const rawPath = uniquePath(path.join(dir, '.memoc', 'raw', 'urls', `${slug}.md`));
2264
+ write(rawPath, rawUrlRecord(title, target));
2265
+ ensureMemocFrontmatter(rawPath, dir);
2266
+ rawRef = pathRelativeMarkdown(path.join(dir, '.memoc', 'wiki', 'sources'), rawPath);
2267
+ rawDisplay = normRel(dir, rawPath);
2268
+ } else {
2269
+ const abs = path.resolve(dir, target);
2270
+ if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
2271
+ console.error(`\n Source file not found: ${target}`);
2272
+ process.exit(1);
2273
+ }
2274
+ const ext = path.extname(abs) || '.txt';
2275
+ const rawPath = uniquePath(path.join(dir, '.memoc', 'raw', 'files', `${slug}${ext}`));
2276
+ fs.mkdirSync(path.dirname(rawPath), { recursive: true });
2277
+ fs.copyFileSync(abs, rawPath);
2278
+ rawRef = pathRelativeMarkdown(path.join(dir, '.memoc', 'wiki', 'sources'), rawPath);
2279
+ rawDisplay = normRel(dir, rawPath);
2280
+ }
2281
+
2282
+ const sourcePath = uniquePath(path.join(dir, '.memoc', 'wiki', 'sources', `${slug}.md`));
2283
+ write(sourcePath, sourceRecord(title, rawRef, target, isUrl));
2284
+ ensureMemocFrontmatter(sourcePath, dir);
2285
+ addWikiListItem(path.join(dir, '.memoc', 'wiki', 'sources.md'), 'Source Records', pathRelativeMarkdown(path.join(dir, '.memoc', 'wiki'), sourcePath), title, 'needs synthesis');
2286
+ addWikiListItem(path.join(dir, '.memoc', 'wiki', 'sources', 'README.md'), 'Source Records', path.basename(sourcePath), title, 'source record');
2287
+ addWikiListItem(path.join(dir, '.memoc', 'wiki', 'index.md'), 'Pages', pathRelativeMarkdown(path.join(dir, '.memoc', 'wiki'), sourcePath), title, 'source record');
2288
+ appendMemocLog(dir, `ingest | Added source record ${normRel(dir, sourcePath)} from ${isUrl ? target : normRel(dir, path.resolve(dir, target))}.`);
2289
+
2290
+ console.log('\n memoc ingest\n');
2291
+ console.log(` Source record ${normRel(dir, sourcePath)}`);
2292
+ console.log(` Raw reference ${rawDisplay}`);
2293
+ console.log(' Next Synthesize affected topics, then run memoc lint-wiki.');
2294
+ console.log();
2295
+ }
2296
+
2297
+ function runNote(dir) {
2298
+ const rawArgs = process.argv.slice(3);
2299
+ const bodyIndex = rawArgs.indexOf('--body');
2300
+ let body = '';
2301
+ let titleArgs = rawArgs;
2302
+ if (bodyIndex !== -1) {
2303
+ titleArgs = rawArgs.slice(0, bodyIndex);
2304
+ body = rawArgs.slice(bodyIndex + 1).join(' ');
2305
+ }
2306
+ const title = titleArgs.join(' ').trim();
2307
+ if (!title) {
2308
+ console.error('\n Usage: memoc note "<topic title>" [--body "short note"]');
2309
+ process.exit(1);
2310
+ }
2311
+
2312
+ ensureMemocBase(dir);
2313
+ const topicPath = uniquePath(path.join(dir, '.memoc', 'wiki', 'topics', `${slugify(title, 'topic')}.md`));
2314
+ write(topicPath, topicNote(title, body));
2315
+ ensureMemocFrontmatter(topicPath, dir);
2316
+ addWikiListItem(path.join(dir, '.memoc', 'wiki', 'topics', 'README.md'), 'Topic Pages', path.basename(topicPath), title, 'topic note');
2317
+ addWikiListItem(path.join(dir, '.memoc', 'wiki', 'index.md'), 'Saved Queries', pathRelativeMarkdown(path.join(dir, '.memoc', 'wiki'), topicPath), title, 'saved query/topic note');
2318
+ appendMemocLog(dir, `note | Saved wiki topic ${normRel(dir, topicPath)}.`);
2319
+
2320
+ console.log('\n memoc note\n');
2321
+ console.log(` Topic ${normRel(dir, topicPath)}`);
2322
+ console.log(' Next Link related sources/topics, then run memoc lint-wiki.');
2323
+ console.log();
2324
+ }
2325
+
2326
+ function ensureMemocBase(dir) {
2327
+ const p = scanProject(dir);
2328
+ const memDir = path.join(dir, '.memoc');
2329
+ const files = [
2330
+ [path.join(memDir, 'wiki/index.md'), tplWikiIndex],
2331
+ [path.join(memDir, 'wiki/sources.md'), tplWikiSources],
2332
+ [path.join(memDir, 'wiki/sources/README.md'), tplWikiSourcesReadme],
2333
+ [path.join(memDir, 'wiki/topics/README.md'), tplWikiTopicsReadme],
2334
+ [path.join(memDir, 'raw/README.md'), tplRawReadme],
2335
+ [path.join(memDir, 'raw/files/README.md'), tplRawFilesReadme],
2336
+ [path.join(memDir, 'raw/urls/README.md'), tplRawUrlsReadme],
2337
+ [path.join(memDir, 'log.md'), tplLog],
2338
+ [path.join(memDir, '00-agent-index.md'), () => tplAgentIndex(p)],
2339
+ ];
2340
+ for (const [fp, tpl] of files) ensure(fp, tpl());
2341
+ ensureObsidianFrontmatter(dir, () => {});
2342
+ }
2343
+
2344
+ function ingestTitle(dir, target, isUrl) {
2345
+ if (isUrl) {
2346
+ try {
2347
+ const u = new URL(target);
2348
+ return path.basename(u.pathname) || u.hostname;
2349
+ } catch {
2350
+ return target;
2351
+ }
2352
+ }
2353
+ try {
2354
+ const src = fs.readFileSync(path.resolve(dir, target), 'utf8');
2355
+ return markdownTitle(src, path.basename(target, path.extname(target)));
2356
+ } catch {
2357
+ return path.basename(target, path.extname(target));
2358
+ }
2359
+ }
2360
+
2361
+ function rawUrlRecord(title, url) {
2362
+ return `# ${title}
2363
+
2364
+ Original URL: ${url}
2365
+
2366
+ This raw URL record stores provenance only. Summarize it in a source record before using it as durable project knowledge.
2367
+ `;
2368
+ }
2369
+
2370
+ function sourceRecord(title, rawRef, original, isUrl) {
2371
+ return `# ${title}
2372
+
2373
+ ## Source
2374
+
2375
+ - Raw: [${isUrl ? 'URL record' : 'raw file'}](${rawRef})
2376
+ - Original: ${original}
2377
+ - Ingested: ${nowISO()}
2378
+
2379
+ ## Summary
2380
+
2381
+ _Summarize only the durable facts future agents should reuse._
2382
+
2383
+ ## Affects
2384
+
2385
+ - [Sources](../sources.md)
2386
+ - [Topics](../topics/README.md)
2387
+ - [Open Questions](../questions.md)
2388
+
2389
+ ## Synthesis Tasks
2390
+
2391
+ - [ ] Create or update affected topic/global/system pages.
2392
+ - [ ] Link those pages back to this source when provenance matters.
2393
+ - [ ] Run \`memoc lint-wiki\`.
2394
+
2395
+ ## Related
2396
+
2397
+ - [Sources Index](../sources.md)
2398
+ - [Source Records](README.md)
2399
+ - [Wiki Index](../index.md)
2400
+ `;
538
2401
  }
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
-
555
- 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
-
577
- function managedBlock() {
578
- return `${MGMT_S}
579
- ## Session Start
580
- - [ ] Read \`.memoc/session-summary.md\`
581
- - [ ] \`.pending\` exists? Review changed files, update memory if needed, then delete it.
582
- - [ ] If \`memoc\` is not found, use the project-local wrapper for the rest of the session: Windows \`.\\.memoc\\bin\\memoc.cmd <command>\`; sh \`.memoc/bin/memoc <command>\`
583
2402
 
584
- ## Before Opening More Files
585
- - [ ] Search memory first: \`memoc search "<query>" --limit 5\`, or wrapper fallback above if PATH fails
586
- - [ ] Open on demand: \`02\` status, \`04\` resume, \`06\` rules, \`llms.txt\` map
587
- - [ ] If memory search is not enough, search project files with \`memoc grep "<query>" --limit 5\` (or wrapper fallback)
588
- - [ ] Keep output small: \`summary\`, \`search --limit\`, \`grep --limit\`, \`--snippets\`
2403
+ function topicNote(title, body) {
2404
+ return `# ${title}
589
2405
 
590
- ## Before Finishing _(update only applicable files; skip Q&A / throwaway exploration)_
591
- - [ ] Code/config/deps changed? Update \`02\` + \`session-summary.md\`
592
- - [ ] Decision made? Update \`03-decisions.md\` + \`02\`
593
- - [ ] Work incomplete or risky? Update \`04-handoff.md\`
594
- - [ ] Rule/preference set? Update \`06-project-rules.md\`
595
- - [ ] Wiki/systems work? Read \`skills/project-memory-maintainer/SKILL.md\`
596
- ${MGMT_E}`;
597
- }
2406
+ ## Summary
598
2407
 
599
- 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
-
1061
- function tplMemocUsage() {
1062
- 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
-
1076
- # Refresh memoc itself when run through npx @latest, preserving project memory
1077
- memoc upgrade
2408
+ ${body ? `- ${body}` : '_Capture the durable answer, analysis, or query result here._'}
1078
2409
 
1079
- # Explicitly update managed sections based on current project state
1080
- 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
-
1094
- 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
-
1096
- ## Agent Read Order
2410
+ ## Evidence
1097
2411
 
1098
- 1. Entry-file managed block.
1099
- 2. \`.memoc/session-summary.md\` only.
1100
- 3. Search memory first with one or two concrete terms: \`memoc search "<query>" --limit 5\`.
1101
- 4. Open only the matching memory file(s) that matter.
1102
- 5. If memory is not enough, search project files: \`memoc grep "<query>" --limit 5\`.
1103
- 6. Use \`--snippets\` only when file names are not enough.
2412
+ - [Sources](../sources.md)
1104
2413
 
1105
- 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
-
1235
- function claudeStopHookCmd() {
1236
- 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{}"`;
2414
+ ## Open Questions
2415
+
2416
+ _None yet._
2417
+
2418
+ ## Related
2419
+
2420
+ - [Wiki Index](../index.md)
2421
+ - [Topics](README.md)
2422
+ - [Glossary](../glossary.md)
2423
+ `;
1237
2424
  }
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
-
1247
- function ensureClaudeStopHook(settingsPath) {
1248
- const cmd = claudeStopHookCmd();
1249
- let settings;
1250
- try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); }
1251
- catch { settings = {}; }
1252
-
1253
- if (!settings.hooks || typeof settings.hooks !== 'object' || Array.isArray(settings.hooks)) settings.hooks = {};
1254
- if (!Array.isArray(settings.hooks.Stop)) settings.hooks.Stop = [];
1255
-
1256
- let hasCurrent = false;
1257
- let changed = false;
1258
- for (const entry of settings.hooks.Stop) {
1259
- if (!Array.isArray(entry.hooks)) continue;
1260
- const nextHooks = [];
1261
- for (const hook of entry.hooks) {
1262
- if (hook && hook.command === cmd) {
1263
- if (hasCurrent) changed = true;
1264
- else {
1265
- hasCurrent = true;
1266
- nextHooks.push(hook);
1267
- }
1268
- } else if (isMemocClaudeStopHook(hook)) {
1269
- changed = true;
1270
- } else {
1271
- nextHooks.push(hook);
1272
- }
2425
+
2426
+ function listMarkdownFiles(root) {
2427
+ const files = [];
2428
+ function walk(d) {
2429
+ if (!fs.existsSync(d)) return;
2430
+ for (const entry of fs.readdirSync(d)) {
2431
+ const fp = path.join(d, entry);
2432
+ try {
2433
+ const st = fs.statSync(fp);
2434
+ if (st.isDirectory()) walk(fp);
2435
+ else if (entry.endsWith('.md')) files.push(fp);
2436
+ } catch {}
1273
2437
  }
1274
- entry.hooks = nextHooks;
1275
2438
  }
1276
- settings.hooks.Stop = settings.hooks.Stop.filter(entry =>
1277
- !Array.isArray(entry.hooks) || entry.hooks.length > 0
1278
- );
1279
- if (hasCurrent && !changed) return false; // no change needed
2439
+ walk(root);
2440
+ return files.sort();
2441
+ }
1280
2442
 
1281
- if (!hasCurrent) settings.hooks.Stop.push({ matcher: '', hooks: [{ type: 'command', command: cmd }] });
1282
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
1283
- return true; // merged or migrated
2443
+ function safeRead(fp) {
2444
+ try { return fs.readFileSync(fp, 'utf8'); } catch { return ''; }
1284
2445
  }
1285
2446
 
1286
- function isMemocClaudeStopHook(hook) {
1287
- if (!hook || typeof hook.command !== 'string') return false;
1288
- const command = hook.command;
1289
- return command.includes('.memoc/.pending') &&
1290
- command.includes('git') &&
1291
- command.includes('status') &&
1292
- command.includes('--porcelain');
2447
+ function normRel(dir, fp) {
2448
+ return path.relative(dir, fp).replace(/\\/g, '/');
1293
2449
  }
1294
-
1295
- // ═══════════════════════════════════════════════════════════════════
1296
- // MANAGED BLOCK UPDATE (CLAUDE.md / AGENTS.md)
1297
- // ═══════════════════════════════════════════════════════════════════
1298
-
1299
- function ensureClaudeStopHookFile(dir, mark) {
1300
- const claudeDir = path.join(dir, '.claude');
1301
- const claudeSettings = path.join(claudeDir, 'settings.json');
1302
- fs.mkdirSync(claudeDir, { recursive: true });
1303
- if (!fs.existsSync(claudeSettings)) {
1304
- write(claudeSettings, tplClaudeSettings());
1305
- mark('add', '.claude/settings.json');
1306
- return;
1307
- }
1308
- const merged = ensureClaudeStopHook(claudeSettings);
1309
- mark(merged ? 'update' : 'skip', `.claude/settings.json (Stop hook ${merged ? 'merged' : 'already present'})`);
2450
+
2451
+ function pathRelativeMarkdown(fromDir, toFile) {
2452
+ let rel = path.relative(fromDir, toFile).replace(/\\/g, '/');
2453
+ if (!rel.startsWith('.')) rel = `./${rel}`;
2454
+ return rel;
1310
2455
  }
1311
2456
 
1312
- function ensurePendingGitignore(dir, mark) {
1313
- const gitignorePath = path.join(dir, '.gitignore');
1314
- const PENDING_ENTRY = '.memoc/.pending';
1315
- const gitignoreContent = fs.existsSync(gitignorePath)
1316
- ? fs.readFileSync(gitignorePath, 'utf8') : '';
1317
- const hasPendingEntry = gitignoreContent
1318
- .split(/\r?\n/)
1319
- .some(line => line.trim() === PENDING_ENTRY);
1320
- if (!hasPendingEntry) {
1321
- fs.appendFileSync(gitignorePath, (gitignoreContent.endsWith('\n') ? '' : '\n') + PENDING_ENTRY + '\n', 'utf8');
1322
- mark('update', '.gitignore (.memoc/.pending added)');
1323
- } else {
1324
- mark('skip', '.gitignore (.memoc/.pending already present)');
1325
- }
2457
+ function markdownLinks(src) {
2458
+ const links = [];
2459
+ const re = /!?\[[^\]]*]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
2460
+ let m;
2461
+ while ((m = re.exec(src))) links.push(m[1]);
2462
+ return links;
1326
2463
  }
1327
2464
 
1328
- function printCommandHint() {
1329
- console.log('\n Agent command fallback:');
1330
- console.log(' memoc summary');
1331
- console.log(' .\\.memoc\\bin\\memoc.cmd summary # Windows');
1332
- console.log(' .memoc/bin/memoc summary # macOS/Linux sh');
1333
- console.log(' If PATH fails once, use the project-local wrapper for the rest of the session.');
2465
+ function resolveMarkdownLink(fromFile, link) {
2466
+ const clean = decodeURIComponent(String(link).split('#')[0]);
2467
+ if (!clean) return null;
2468
+ const base = path.resolve(path.dirname(fromFile), clean);
2469
+ if (path.extname(base)) return base;
2470
+ if (fs.existsSync(`${base}.md`)) return `${base}.md`;
2471
+ return base;
1334
2472
  }
1335
2473
 
1336
- function applyManagedBlock(filePath, tplFn) {
1337
- if (!fs.existsSync(filePath)) {
1338
- write(filePath, tplFn());
1339
- return 'add';
1340
- }
1341
- const src = fs.readFileSync(filePath, 'utf8');
1342
- const range = findMarkedRange(src, MGMT_S, MGMT_E);
1343
- 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
- }
1348
- 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
-
1356
- 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],
1400
- [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
1418
- ensureClaudeStopHookFile(dir, mark);
1419
-
1420
- // .gitignore — add .memoc/.pending if not already present
1421
- ensurePendingGitignore(dir, mark);
1422
-
1423
- // PATH helpers — let agents run memoc even when the npm bin is not on PATH
1424
- ensurePathHelpers(dir, mark);
1425
- ensurePathRegistration(dir, mark);
1426
-
1427
- } else {
1428
- // ── UPDATE MODE
1429
- 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],
1498
- [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
1517
- ensureClaudeStopHookFile(dir, mark);
1518
- ensurePendingGitignore(dir, mark);
1519
- ensurePathHelpers(dir, mark);
1520
- ensurePathRegistration(dir, mark);
2474
+ function addWikiListItem(filePath, heading, link, title, note) {
2475
+ const src = safeRead(filePath);
2476
+ if (!src) return;
2477
+ if (src.includes(`](${link})`) || src.includes(`](${link.replace(/^\.\//, '')})`)) return;
2478
+ const item = `- [${title}](${link.replace(/^\.\//, '')}) — ${note}.`;
2479
+ const re = new RegExp(`(## ${escapeRegExp(heading)}\\n)([\\s\\S]*?)(?=\\n## |$)`, 'm');
2480
+ const m = src.match(re);
2481
+ if (!m) {
2482
+ write(filePath, `${src.trimEnd()}\n\n## ${heading}\n\n${item}\n`);
2483
+ return;
2484
+ }
2485
+ const replacementBody = m[2].includes('_None yet') || m[2].includes('_No sources recorded yet')
2486
+ ? `\n${item}\n`
2487
+ : `${m[2].trimEnd()}\n${item}\n`;
2488
+ write(filePath, src.replace(re, `$1${replacementBody}`));
2489
+ }
1521
2490
 
1522
- // Append update record to log.md
1523
- const logPath = path.join(memDir, 'log.md');
1524
- if (fs.existsSync(logPath)) {
1525
- fs.appendFileSync(logPath,
1526
- `\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
-
1533
- hideOnWindows(memDir);
1534
- console.log(log.join('\n'));
1535
- printCommandHint();
1536
- console.log('\n Done.');
2491
+ function appendMemocLog(dir, text) {
2492
+ const fp = path.join(dir, '.memoc', 'log.md');
2493
+ ensure(fp, tplLog());
2494
+ fs.appendFileSync(fp, `\n## [${nowISO()}] ${text}\n`, 'utf8');
1537
2495
  }
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()) {
2496
+
2497
+ // ═══════════════════════════════════════════════════════════════════
2498
+ // SEARCH
2499
+ // ═══════════════════════════════════════════════════════════════════
2500
+
2501
+ function runSearch(dir, scope = 'memory') {
2502
+ const rawArgs = process.argv.slice(3);
2503
+ const opts = { mode: 'files', limit: 12, all: false };
2504
+ const queryParts = [];
2505
+
2506
+ for (let i = 0; i < rawArgs.length; i++) {
2507
+ const arg = rawArgs[i];
2508
+ if (arg === '--snippets') { opts.mode = 'snippets'; continue; }
2509
+ if (arg === '--files') { opts.mode = 'files'; continue; }
2510
+ if (arg === '--all') { opts.all = true; continue; }
2511
+ if (arg === '--limit') {
2512
+ const n = Number(rawArgs[++i]);
2513
+ if (Number.isFinite(n) && n > 0) opts.limit = Math.floor(n);
2514
+ continue;
2515
+ }
2516
+ if (arg.startsWith('--limit=')) {
2517
+ const n = Number(arg.slice('--limit='.length));
2518
+ if (Number.isFinite(n) && n > 0) opts.limit = Math.floor(n);
2519
+ continue;
2520
+ }
2521
+ queryParts.push(arg);
2522
+ }
2523
+
2524
+ const query = queryParts.join(' ').toLowerCase();
2525
+
2526
+ const searchRoots = scope === 'project' ? [dir] : memorySearchRoots(dir);
2527
+
2528
+ if (!query) {
2529
+ // No query — list searchable files sorted by recency
2530
+ const allFiles = [];
2531
+ function collectFile(fp) {
2532
+ if (!fs.existsSync(fp)) return;
2533
+ const rel = path.relative(dir, fp);
2534
+ let mtime = 0;
2535
+ try { mtime = fs.statSync(fp).mtimeMs; } catch {}
2536
+ allFiles.push({ file: rel, mtime });
2537
+ }
2538
+ function collectDir(d) {
2539
+ if (!fs.existsSync(d)) return;
2540
+ for (const entry of fs.readdirSync(d)) {
2541
+ const fp = path.join(d, entry);
2542
+ try {
2543
+ const st = fs.statSync(fp);
2544
+ if (st.isDirectory()) {
1617
2545
  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);
2546
+ } else if (isSearchableFile(fp, entry, st, scope)) collectFile(fp);
2547
+ } catch {}
2548
+ }
2549
+ }
2550
+ for (const root of searchRoots) {
2551
+ try {
2552
+ if (fs.statSync(root).isDirectory()) collectDir(root);
2553
+ else collectFile(root);
2554
+ } catch {}
2555
+ }
2556
+ allFiles.sort((a, b) => b.mtime - a.mtime || a.file.localeCompare(b.file));
2557
+ const limited = opts.all ? allFiles : allFiles.slice(0, opts.limit);
2558
+ console.log(limited.map(r => r.file).join('\n'));
2559
+ if (!opts.all && allFiles.length > limited.length) {
2560
+ console.log(`... ${allFiles.length - limited.length} more files. Use --all to show all.`);
2561
+ }
2562
+ return;
2563
+ }
2564
+
2565
+ const matchesByFile = new Map(); // rel -> { matches: [], mtime: number }
2566
+
2567
+ function searchFile(fp) {
2568
+ if (!fs.existsSync(fp)) return;
2569
+ const rel = path.relative(dir, fp);
2570
+ let mtime = 0;
2571
+ try {
2572
+ const st = fs.statSync(fp);
2573
+ if (!isSearchableFile(fp, path.basename(fp), st, scope)) return;
2574
+ mtime = st.mtimeMs;
2575
+ } catch {}
2576
+ const lines = fs.readFileSync(fp, 'utf8').split('\n');
2577
+ lines.forEach((line, i) => {
2578
+ if (line.toLowerCase().includes(query)) {
2579
+ if (!matchesByFile.has(rel)) matchesByFile.set(rel, { matches: [], mtime });
2580
+ matchesByFile.get(rel).matches.push({ line: i + 1, text: line.trim() });
2581
+ }
2582
+ });
2583
+ }
2584
+
2585
+ function walkDir(d) {
2586
+ if (!fs.existsSync(d)) return;
2587
+ for (const entry of fs.readdirSync(d)) {
2588
+ const fp = path.join(d, entry);
2589
+ try {
2590
+ const st = fs.statSync(fp);
1663
2591
  if (st.isDirectory()) {
1664
2592
  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') {
2593
+ } else if (isSearchableFile(fp, entry, st, scope)) searchFile(fp);
2594
+ } catch {}
2595
+ }
2596
+ }
2597
+
2598
+ for (const root of searchRoots) {
2599
+ try {
2600
+ if (fs.statSync(root).isDirectory()) walkDir(root);
2601
+ else searchFile(root);
2602
+ } catch {}
2603
+ }
2604
+
2605
+ if (!matchesByFile.size) {
2606
+ console.log('No matches found.');
2607
+ } else if (opts.mode === 'files') {
1680
2608
  const rows = [...matchesByFile.entries()]
1681
2609
  .map(([file, { matches, mtime }]) => ({ file, count: matches.length, mtime }))
1682
2610
  .sort((a, b) =>
@@ -1685,12 +2613,12 @@ function runSearch(dir, scope = 'memory') {
1685
2613
  b.mtime - a.mtime ||
1686
2614
  a.file.localeCompare(b.file)
1687
2615
  );
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 {
2616
+ const limited = opts.all ? rows : rows.slice(0, opts.limit);
2617
+ console.log(limited.map(r => `${r.file} ${r.count} match${r.count === 1 ? '' : 'es'}`).join('\n'));
2618
+ if (!opts.all && rows.length > limited.length) {
2619
+ console.log(`... ${rows.length - limited.length} more files. Use --all to show all, or --snippets for line matches.`);
2620
+ }
2621
+ } else {
1694
2622
  const snippets = [];
1695
2623
  for (const [file, { matches }] of matchesByFile.entries()) {
1696
2624
  for (const m of matches) snippets.push({ file, line: m.line, text: m.text });
@@ -1702,23 +2630,23 @@ function runSearch(dir, scope = 'memory') {
1702
2630
  );
1703
2631
  const limited = opts.all ? snippets : snippets.slice(0, opts.limit);
1704
2632
  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
-
2633
+ if (!opts.all && snippets.length > limited.length) {
2634
+ console.log(`... ${snippets.length - limited.length} more matches. Use --all to show all, or --limit N.`);
2635
+ }
2636
+ }
2637
+ }
2638
+
2639
+ function memorySearchRoots(dir) {
2640
+ return [
2641
+ path.join(dir, '.memoc'),
2642
+ path.join(dir, 'skills'),
2643
+ path.join(dir, 'llms.txt'),
2644
+ path.join(dir, 'AGENTS.md'),
2645
+ path.join(dir, 'CLAUDE.md'),
2646
+ ...Object.values(AGENT_REGISTRY).map(agent => path.join(dir, agent.file)),
2647
+ ];
2648
+ }
2649
+
1722
2650
  function shouldSkipSearchDir(name, scope = 'memory') {
1723
2651
  const skipped = new Set([
1724
2652
  '.git', 'node_modules', '.next', 'dist', 'build', 'out', 'coverage',
@@ -1729,6 +2657,8 @@ function shouldSkipSearchDir(name, scope = 'memory') {
1729
2657
  skipped.add('.memoc');
1730
2658
  skipped.add('skills');
1731
2659
  skipped.add('.claude');
2660
+ } else {
2661
+ skipped.add('raw');
1732
2662
  }
1733
2663
  return skipped.has(name);
1734
2664
  }
@@ -1739,16 +2669,16 @@ function isSearchableFile(fp, name, st, scope = 'memory') {
1739
2669
  if (scope === 'project' && isAgentMemoryFile(name)) return false;
1740
2670
  if (name === 'llms.txt' || name.endsWith('rules')) return true;
1741
2671
  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',
2672
+ if (scope === 'memory') {
2673
+ return new Set(['.md', '.txt']).has(ext);
2674
+ }
2675
+ return new Set([
2676
+ '.md', '.txt', '.json', '.jsonc', '.yaml', '.yml', '.toml', '.ini', '.env',
2677
+ '.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs',
2678
+ '.py', '.rs', '.go', '.java', '.cs', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.hxx',
2679
+ '.html', '.css', '.scss', '.sass', '.vue', '.svelte',
2680
+ '.sql', '.graphql', '.gql', '.sh', '.bash', '.zsh', '.ps1', '.bat', '.cmd',
2681
+ '.xml', '.gradle', '.kts', '.cmake',
1752
2682
  ]).has(ext);
1753
2683
  }
1754
2684
 
@@ -1787,206 +2717,300 @@ function searchPriority(file, scope = 'memory') {
1787
2717
  if (normalized.startsWith('skills/')) return 40;
1788
2718
  return 50;
1789
2719
  }
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');
2720
+
2721
+ // ═══════════════════════════════════════════════════════════════════
2722
+ // TOKENS — estimate token cost of current memory state
2723
+ // ═══════════════════════════════════════════════════════════════════
2724
+
2725
+ function runTokens(dir) {
2726
+ const est = text => Math.ceil(Buffer.byteLength(text, 'utf8') / 4);
2727
+ const read = fp => { try { return fs.readFileSync(fp, 'utf8'); } catch { return ''; } };
2728
+ const memDir = path.join(dir, '.memoc');
2729
+
2730
+ const startup = [
2731
+ ['CLAUDE.md', path.join(dir, 'CLAUDE.md')],
2732
+ ['session-summary.md', path.join(memDir, 'session-summary.md')],
2733
+ ];
2734
+ const onDemand = [
2735
+ ['llms.txt', path.join(dir, 'llms.txt')],
2736
+ ['02-current-project-state.md', path.join(memDir, '02-current-project-state.md')],
2737
+ ['03-decisions.md', path.join(memDir, '03-decisions.md')],
2738
+ ['04-handoff.md', path.join(memDir, '04-handoff.md')],
2739
+ ['06-project-rules.md', path.join(memDir, '06-project-rules.md')],
2740
+ ['log.md', path.join(memDir, 'log.md')],
2741
+ ];
2742
+
2743
+ console.log('\n memoc tokens\n');
2744
+ let startupTotal = 0;
2745
+ console.log(' Startup (always loaded):');
2746
+ for (const [name, fp] of startup) {
2747
+ const content = read(fp);
2748
+ const t = est(content);
2749
+ const b = Buffer.byteLength(content, 'utf8');
2750
+ startupTotal += t;
2751
+ const warn = b > 1000 ? ' ⚠ large' : '';
2752
+ console.log(` ${name.padEnd(32)} ${String(t).padStart(5)} tokens (${b}B)${warn}`);
2753
+ }
2754
+ console.log(` ${'── startup total'.padEnd(32)} ${String(startupTotal).padStart(5)} tokens`);
2755
+
2756
+ console.log('\n On-demand (read when needed):');
2757
+ let onDemandTotal = 0;
2758
+ for (const [name, fp] of onDemand) {
2759
+ const content = read(fp);
2760
+ if (!content) continue;
2761
+ const t = est(content);
2762
+ const b = Buffer.byteLength(content, 'utf8');
2763
+ onDemandTotal += t;
2764
+ const warn = t > 500 ? ' ⚠ consider compress' : '';
2765
+ console.log(` ${name.padEnd(32)} ${String(t).padStart(5)} tokens (${b}B)${warn}`);
2766
+ }
2767
+ console.log(` ${'── on-demand total'.padEnd(32)} ${String(onDemandTotal).padStart(5)} tokens`);
2768
+ console.log(`\n If all loaded: ~${startupTotal + onDemandTotal} tokens`);
2769
+
2770
+ const summaryContent = read(path.join(memDir, 'session-summary.md'));
2771
+ const summaryBytes = Buffer.byteLength(summaryContent, 'utf8');
2772
+ if (summaryBytes > 800) {
2773
+ console.log(`\n ⚠ session-summary.md is ${summaryBytes}B — recommended <800B. Run \`memoc trim-summary\`, then move completed history to log.md and resume details to 04-handoff.md.`);
2774
+ }
2775
+ console.log();
2776
+ }
2777
+
2778
+ // ═══════════════════════════════════════════════════════════════════
2779
+ // COMPRESS — archive old log.md entries to keep file small
2780
+ // ═══════════════════════════════════════════════════════════════════
2781
+
2782
+ function runCompress(dir) {
2783
+ const KEEP = 20;
2784
+ const logPath = path.join(dir, '.memoc', 'log.md');
2785
+ const archivePath = path.join(dir, '.memoc', 'log-archive.md');
2786
+
2787
+ if (!fs.existsSync(logPath)) {
2788
+ console.log('\n No .memoc/log.md found.\n');
2789
+ return;
2790
+ }
2791
+
2792
+ const src = fs.readFileSync(logPath, 'utf8');
2793
+ // Split on entry headers, keep header as part of each chunk
2794
+ const parts = src.split(/(?=\n## \[)/);
2795
+ const header = parts[0]; // everything before first entry
2796
+ const entries = parts.slice(1).filter(e => e.trim());
2797
+
2798
+ if (entries.length <= KEEP) {
2799
+ console.log(`\n log.md has ${entries.length} entries — nothing to compress (threshold: ${KEEP}).\n`);
2800
+ return;
2801
+ }
2802
+
2803
+ const toArchive = entries.slice(0, entries.length - KEEP);
2804
+ const toKeep = entries.slice(entries.length - KEEP);
2805
+
2806
+ // Append to archive
2807
+ const archiveExists = fs.existsSync(archivePath);
2808
+ const archiveHeader = archiveExists ? '' : '# Log Archive\n\nOlder entries moved from log.md by `memoc compress`.\n';
2809
+ fs.appendFileSync(archivePath, archiveHeader + toArchive.join('') + '\n', 'utf8');
2810
+
2811
+ // Rewrite log.md with only recent entries
2812
+ write(logPath, header.trimEnd() + '\n' + toKeep.join('') + '\n');
2813
+
2814
+ console.log(`\n memoc compress\n`);
2815
+ console.log(` Archived ${toArchive.length} entries → .memoc/log-archive.md`);
2816
+ console.log(` Kept ${toKeep.length} recent entries in log.md`);
2817
+ const saved = Buffer.byteLength(toArchive.join(''), 'utf8');
2818
+ console.log(` Freed ~${saved}B from log.md`);
2819
+ console.log('\n Done.\n');
2820
+ }
2821
+
2822
+ // ═══════════════════════════════════════════════════════════════════
2823
+ // TRIM SUMMARY — keep startup memory small and move bulky text aside
2824
+ // ═══════════════════════════════════════════════════════════════════
2825
+
2826
+ function runTrimSummary(dir) {
2827
+ const summaryPath = path.join(dir, '.memoc', 'session-summary.md');
2828
+ const archivePath = path.join(dir, '.memoc', 'session-summary-archive.md');
2829
+ if (!fs.existsSync(summaryPath)) {
2830
+ write(summaryPath, tplSessionSummary());
2831
+ console.log('\n memoc trim-summary\n');
2832
+ console.log(' Added .memoc/session-summary.md');
2833
+ console.log('\n Done.\n');
2834
+ return;
2835
+ }
2836
+
2837
+ const src = fs.readFileSync(summaryPath, 'utf8');
2838
+ const beforeBytes = Buffer.byteLength(src, 'utf8');
2839
+ const compact = compactSessionSummary(src);
2840
+ const afterBytes = Buffer.byteLength(compact, 'utf8');
2841
+
2842
+ if (src === compact && beforeBytes <= 800) {
2843
+ console.log('\n memoc trim-summary\n');
2844
+ console.log(` session-summary.md is already compact (${beforeBytes}B).`);
2845
+ console.log('\n Done.\n');
2846
+ return;
2847
+ }
2848
+
2849
+ const archiveHeader = fs.existsSync(archivePath)
2850
+ ? ''
2851
+ : '# Session Summary Archive\n\nOlder oversized startup summaries moved by `memoc trim-summary`.\n';
2852
+ fs.appendFileSync(archivePath, `${archiveHeader}\n## [${nowISO()}] archived summary (${beforeBytes}B)\n\n${src.trimEnd()}\n`, 'utf8');
2853
+ write(summaryPath, compact);
2854
+ appendMemocLog(dir, `trim-summary | Archived oversized session summary (${beforeBytes}B → ${afterBytes}B).`);
2855
+
2856
+ console.log('\n memoc trim-summary\n');
2857
+ console.log(` Archived .memoc/session-summary-archive.md`);
2858
+ console.log(` Rewrote .memoc/session-summary.md (${beforeBytes}B ${afterBytes}B)`);
2859
+ console.log(' Reminder Completed history belongs in log.md; resume details belong in 04-handoff.md.');
2860
+ console.log('\n Done.\n');
2861
+ }
2862
+
2863
+ function compactSessionSummary(src) {
2864
+ const sections = ['Status', 'Changed', 'Open Tasks', 'Resume'];
2865
+ const lines = [
2866
+ '# Session Summary',
2867
+ `Last: ${nowISO()}`,
2868
+ 'Replace this file instead of appending to it. Keep total size <800B and each section ≤3 bullets.',
2869
+ 'Completed history belongs in `log.md`; incomplete/risky resume detail belongs in `04-handoff.md`.',
2870
+ '',
2871
+ ];
2872
+
2873
+ for (const heading of sections) {
2874
+ lines.push(`## ${heading}`);
2875
+ const bullets = compactSummaryBullets(sectionText(src, heading));
2876
+ if (bullets.length) lines.push(...bullets);
2877
+ else lines.push(summaryPlaceholder(heading));
2878
+ lines.push('');
2879
+ }
2880
+
2881
+ return lines.join('\n').trimEnd() + '\n';
2882
+ }
2883
+
2884
+ function sectionText(src, heading) {
2885
+ const re = new RegExp(`(?:^|\\n)## ${escapeRegExp(heading)}\\n([\\s\\S]*?)(?=\\n## |$)`);
2886
+ const m = String(src || '').match(re);
2887
+ return m ? m[1].trim() : '';
2888
+ }
2889
+
2890
+ function compactSummaryBullets(text) {
2891
+ return String(text || '')
2892
+ .split(/\r?\n/)
2893
+ .map(line => line.trim())
2894
+ .filter(line => line && !line.startsWith('#') && !/^_.*_$/.test(line))
2895
+ .map(line => line.replace(/^[-*]\s+/, '').replace(/^\d+[.)]\s+/, '').trim())
2896
+ .filter(Boolean)
2897
+ .slice(0, 3)
2898
+ .map(line => `- ${line.length > 140 ? `${line.slice(0, 137)}...` : line}`);
2899
+ }
2900
+
2901
+ function summaryPlaceholder(heading) {
2902
+ if (heading === 'Status') return '_Current state in 1-3 bullets._';
2903
+ if (heading === 'Changed') return '_Recent durable changes only._';
2904
+ if (heading === 'Open Tasks') return '_Current open tasks only._';
2905
+ return '_Where the next agent should resume._';
2906
+ }
2907
+
2908
+ // ═══════════════════════════════════════════════════════════════════
2909
+ // SUMMARY
2910
+ // ═══════════════════════════════════════════════════════════════════
2911
+
2912
+ function runSummary(dir) {
2913
+ const files = [
2914
+ path.join(dir, '.memoc/session-summary.md'),
2915
+ path.join(dir, '.memoc/02-current-project-state.md'),
2916
+ path.join(dir, '.memoc/04-handoff.md'),
2917
+ ];
2918
+
2919
+ function read(fp) {
2920
+ try { return fs.readFileSync(fp, 'utf8'); } catch { return ''; }
2921
+ }
2922
+
2923
+ function section(src, heading) {
2924
+ const re = new RegExp(`(?:^|\\n)## ${heading}\\n([\\s\\S]*?)(?=\\n## |$)`);
2925
+ const m = src.match(re);
2926
+ return m ? m[1].trim() : '';
2927
+ }
2928
+
2929
+ function bullets(text, max = 3) {
2930
+ return text.split('\n')
2931
+ .map(line => line.trim())
2932
+ .filter(line => line.startsWith('- ') && !line.includes('_None yet._'))
2933
+ .slice(0, max);
2934
+ }
2935
+
2936
+ const summary = read(files[0]);
2937
+ const state = read(files[1]);
2938
+ const handoff = read(files[2]);
2939
+ try {
2940
+ const pkg = JSON.parse(read(path.join(dir, 'package.json')));
2941
+ if (pkg.name || pkg.version) {
2942
+ console.log(`Project: ${pkg.name || path.basename(dir)}${pkg.version ? `@${pkg.version}` : ''}`);
2943
+ }
2944
+ } catch {}
2945
+ const rows = [
2946
+ ['Status', bullets(section(summary, 'Status')).concat(bullets(section(state, 'Current Status'))).slice(0, 3)],
2947
+ ['Open Tasks', bullets(section(summary, 'Open Tasks')).concat(bullets(section(state, 'Open Tasks'))).slice(0, 3)],
2948
+ ['Resume', bullets(section(summary, 'Resume')).concat(bullets(section(handoff, 'Next Steps'))).slice(0, 3)],
2949
+ ['Verified', bullets(section(handoff, 'Verified'), 2)],
2950
+ ];
2951
+
2952
+ let printed = false;
2953
+ for (const [label, items] of rows) {
2954
+ if (!items.length) continue;
2955
+ printed = true;
2956
+ console.log(`${label}:`);
2957
+ for (const item of items) console.log(item);
2958
+ }
2959
+ if (!printed) console.log('No summary bullets yet. Read .memoc/session-summary.md.');
2960
+ }
2961
+
2962
+ // ═══════════════════════════════════════════════════════════════════
2963
+ // CLI ENTRY POINT
2964
+ // ═══════════════════════════════════════════════════════════════════
2965
+
2966
+ const cmd = process.argv[2];
2967
+ const cwd = process.cwd();
2968
+
2969
+ if (cmd === '--version' || cmd === '-v') {
2970
+ console.log(VERSION);
2971
+ process.exit(0);
2972
+ }
2973
+
2974
+ if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') {
2975
+ console.log('Usage: memoc <command>\n');
1960
2976
  console.log('Commands:');
1961
2977
  console.log(' init Scaffold agent memory (auto-detects project, updates if already exists)');
1962
2978
  console.log(' update Force-update managed sections based on current project state');
1963
2979
  console.log(' upgrade Refresh memoc runtime/wrappers and managed sections; preserve memory');
1964
2980
  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
-
2981
+ console.log(' tokens Estimate token cost of current memory files');
2982
+ console.log(' trim-summary Archive and compact oversized session-summary.md');
2983
+ console.log(' compress Archive old log.md entries to keep file small');
2984
+ console.log(' add <agent> Add entry file for a specific agent (run without args to list)');
2985
+ console.log(' search "<query>" Search memory/agent docs (use --snippets for line matches)');
2986
+ console.log(' grep "<query>" Search project source/text files (use --snippets for line matches)');
2987
+ console.log(' ingest <path|url> Create a raw/source record scaffold for wiki synthesis');
2988
+ console.log(' note "<title>" Save a durable topic/query-result scaffold');
2989
+ console.log(' lint-wiki Check wiki links, tags, backlinks, and Related sections');
2990
+ console.log('\nSearch flags:');
2991
+ console.log(' --files Show file names and match counts, sorted by relevance + recency (default)');
2992
+ console.log(' --snippets Show matching lines');
2993
+ console.log(' --limit N Limit output (default 12)');
2994
+ console.log(' --all Show all matches');
2995
+ console.log('\nFlags:');
2996
+ console.log(' --version, -v Print version');
2997
+ process.exit(0);
2998
+ }
2999
+
1980
3000
  if (cmd === 'init') { run(cwd, false); process.exit(0); }
1981
3001
  if (cmd === 'update') { run(cwd, true, 'update'); process.exit(0); }
1982
3002
  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);
3003
+ if (cmd === 'summary') { runSummary(cwd); process.exit(0); }
3004
+ if (cmd === 'tokens') { runTokens(cwd); process.exit(0); }
3005
+ if (cmd === 'trim-summary') { runTrimSummary(cwd); process.exit(0); }
3006
+ if (cmd === 'compress') { runCompress(cwd); process.exit(0); }
3007
+ if (cmd === 'add') { runAdd(cwd); process.exit(0); }
3008
+ if (cmd === 'search') { runSearch(cwd, 'memory'); process.exit(0); }
3009
+ if (cmd === 'grep') { runSearch(cwd, 'project'); process.exit(0); }
3010
+ if (cmd === 'ingest') { runIngest(cwd); process.exit(0); }
3011
+ if (cmd === 'note') { runNote(cwd); process.exit(0); }
3012
+ if (cmd === 'lint-wiki') { runWikiLint(cwd); process.exit(0); }
3013
+
3014
+ console.error(`Unknown command: ${cmd}`);
3015
+ console.error('Run "memoc --help" for usage.');
3016
+ process.exit(1);