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