@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.
- package/LICENSE +21 -21
- package/README.md +169 -155
- package/bin/cli.js +1574 -1394
- 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
|
|
208
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
|
290
|
-
return
|
|
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
|
|
304
|
-
return `# $
|
|
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
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
.
|
|
312
|
-
.
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
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
|
-
|
|
350
|
-
`;
|
|
270
|
+
return userBin;
|
|
351
271
|
}
|
|
352
272
|
|
|
353
|
-
function
|
|
354
|
-
|
|
273
|
+
function ensurePathRegistration(dir, mark) {
|
|
274
|
+
const binDir = ensureUserLauncher(mark);
|
|
275
|
+
const pathSep = path.delimiter;
|
|
355
276
|
|
|
356
|
-
|
|
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
|
-
${
|
|
359
|
-
`;
|
|
360
|
-
}
|
|
282
|
+
process.env.PATH = `${binDir}${pathSep}${process.env.PATH || ''}`;
|
|
361
283
|
|
|
362
|
-
|
|
363
|
-
|
|
284
|
+
if (process.env.MEMOC_SKIP_PATH_REGISTER === '1') {
|
|
285
|
+
mark('skip', 'PATH registration (test mode)');
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
364
288
|
|
|
365
|
-
|
|
289
|
+
if (currentPlatform() !== 'win32') {
|
|
290
|
+
const updated = ensureUnixPathRegistration(binDir);
|
|
291
|
+
mark(updated ? 'update' : 'skip', `${currentPlatform()} PATH (${userPathShellHint(binDir)})`);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
366
294
|
|
|
367
|
-
|
|
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
|
|
372
|
-
return
|
|
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
|
-
|
|
325
|
+
const home = process.env.HOME;
|
|
326
|
+
if (!home) return false;
|
|
379
327
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
$
|
|
384
|
-
|
|
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
|
-
|
|
335
|
+
const candidates = [
|
|
336
|
+
path.join(home, '.profile'),
|
|
337
|
+
path.join(home, '.zshrc'),
|
|
338
|
+
path.join(home, '.bashrc'),
|
|
339
|
+
];
|
|
399
340
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
|
|
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
|
|
443
|
-
return
|
|
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
|
|
486
|
-
return
|
|
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
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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>"\`
|
|
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
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
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
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
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
|
-
//
|
|
1116
|
-
|
|
1117
|
-
|
|
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
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
const
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
const
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
if (
|
|
1245
|
-
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
if (!
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
}
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
console.log(
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
console.log(
|
|
1360
|
-
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
const
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
const
|
|
1379
|
-
const
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
const
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
const
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
const
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
}
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
[
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
console.log('
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
if (cmd === '
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
console.
|
|
1488
|
-
console.
|
|
1489
|
-
|
|
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);
|