@kevin0181/memoc 1.0.0 → 1.0.3

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