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