@kevin0181/memoc 1.0.8 → 1.1.2
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/README.md +89 -19
- package/bin/cli.js +649 -500
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -188,338 +188,354 @@ function scriptsMd(scripts) {
|
|
|
188
188
|
|
|
189
189
|
function hideOnWindows(dirPath) {
|
|
190
190
|
if (process.platform === 'win32') {
|
|
191
|
-
try { require('child_process').
|
|
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');
|
|
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
203
|
return true;
|
|
204
204
|
}
|
|
205
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
|
-
|
|
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
|
-
return `# Source this from the project root to put the local memoc wrapper first in PATH.\nMEMOC_DIR="$
|
|
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
|
-
}
|
|
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`;
|
|
267
249
|
}
|
|
268
|
-
|
|
269
|
-
function
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
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
|
-
function
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
try {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
return
|
|
437
|
-
} catch {
|
|
438
|
-
return
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function
|
|
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 ensurePathRegistration(dir, mark) {
|
|
302
|
+
ensureCurrentPathLauncher(mark);
|
|
303
|
+
const binDir = ensureUserLauncher(mark);
|
|
304
|
+
const pathSep = path.delimiter;
|
|
305
|
+
|
|
306
|
+
if ((process.env.PATH || '').split(pathSep).some(p => samePath(p, binDir))) {
|
|
307
|
+
mark('skip', 'PATH (user memoc bin already active)');
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
process.env.PATH = `${binDir}${pathSep}${process.env.PATH || ''}`;
|
|
312
|
+
|
|
313
|
+
if (process.env.MEMOC_SKIP_PATH_REGISTER === '1') {
|
|
314
|
+
mark('skip', 'PATH registration (test mode)');
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (currentPlatform() !== 'win32') {
|
|
319
|
+
const updated = ensureUnixPathRegistration(binDir);
|
|
320
|
+
mark(updated ? 'update' : 'skip', `${currentPlatform()} PATH (${userPathShellHint(binDir)})`);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
const current = require('child_process')
|
|
326
|
+
.execFileSync('powershell.exe', [
|
|
327
|
+
'-NoProfile',
|
|
328
|
+
'-ExecutionPolicy', 'Bypass',
|
|
329
|
+
'-Command',
|
|
330
|
+
"[Environment]::GetEnvironmentVariable('Path','User')",
|
|
331
|
+
], { encoding: 'utf8' })
|
|
332
|
+
.trim();
|
|
333
|
+
const parts = current.split(pathSep).filter(Boolean);
|
|
334
|
+
if (parts.some(p => samePath(p, binDir))) {
|
|
335
|
+
mark('skip', 'User PATH (memoc bin already registered)');
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const nextPath = [binDir, ...parts].join(pathSep);
|
|
339
|
+
require('child_process').execFileSync('powershell.exe', [
|
|
340
|
+
'-NoProfile',
|
|
341
|
+
'-ExecutionPolicy', 'Bypass',
|
|
342
|
+
'-Command',
|
|
343
|
+
`[Environment]::SetEnvironmentVariable('Path', ${JSON.stringify(nextPath)}, 'User')`,
|
|
344
|
+
], { stdio: 'ignore' });
|
|
345
|
+
mark('update', 'User PATH (memoc bin added; open a new terminal if needed)');
|
|
346
|
+
} catch {
|
|
347
|
+
mark('skip', 'User PATH registration failed (use . .\\.memoc\\env.ps1)');
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function ensureCurrentPathLauncher(mark) {
|
|
352
|
+
const target = findWritablePathDir();
|
|
353
|
+
if (!target) {
|
|
354
|
+
mark('skip', 'active PATH launcher (no writable PATH directory found)');
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
writeLaunchers(target, mark, 'active PATH', ensureRuntimeInstall(mark));
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function ensureRuntimeInstall(mark) {
|
|
362
|
+
const runtimeDir = defaultRuntimeDir();
|
|
363
|
+
const sourceRoot = path.join(__dirname, '..');
|
|
364
|
+
const files = [
|
|
365
|
+
[path.join(sourceRoot, 'bin', 'cli.js'), path.join(runtimeDir, 'bin', 'cli.js')],
|
|
366
|
+
[path.join(sourceRoot, 'package.json'), path.join(runtimeDir, 'package.json')],
|
|
367
|
+
];
|
|
368
|
+
|
|
369
|
+
for (const [src, dest] of files) {
|
|
370
|
+
try {
|
|
371
|
+
const content = fs.readFileSync(src, 'utf8');
|
|
372
|
+
const changed = writeIfChanged(dest, content);
|
|
373
|
+
mark(changed, `runtime ${path.relative(runtimeDir, dest)}`);
|
|
374
|
+
} catch {
|
|
375
|
+
mark('skip', `runtime ${path.basename(dest)} unavailable`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
chmodExecutable(path.join(runtimeDir, 'bin', 'cli.js'));
|
|
380
|
+
return path.join(runtimeDir, 'bin', 'cli.js');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function findWritablePathDir() {
|
|
384
|
+
const dirs = [...new Set((process.env.PATH || '').split(path.delimiter).filter(Boolean))];
|
|
385
|
+
const npmBin = npmGlobalBinDir();
|
|
386
|
+
const ranked = dirs
|
|
387
|
+
.filter(d => !isVolatilePathDir(d))
|
|
388
|
+
.filter(d => {
|
|
389
|
+
try { return fs.existsSync(d) && fs.statSync(d).isDirectory() && canWriteDir(d); }
|
|
390
|
+
catch { return false; }
|
|
391
|
+
})
|
|
392
|
+
.sort((a, b) => pathRank(a, npmBin) - pathRank(b, npmBin));
|
|
393
|
+
return ranked[0] || null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function pathRank(dir, npmBin) {
|
|
397
|
+
if (npmBin && samePath(dir, npmBin)) return 0;
|
|
398
|
+
const lower = dir.toLowerCase();
|
|
399
|
+
for (const root of userWritableRoots()) {
|
|
400
|
+
if (root && lower.startsWith(root.toLowerCase())) return 1;
|
|
401
|
+
}
|
|
402
|
+
return 5;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function userWritableRoots() {
|
|
406
|
+
return [
|
|
407
|
+
process.env.APPDATA,
|
|
408
|
+
process.env.LOCALAPPDATA,
|
|
409
|
+
process.env.HOME,
|
|
410
|
+
process.env.USERPROFILE,
|
|
411
|
+
].filter(Boolean).map(p => path.resolve(p));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function npmGlobalBinDir() {
|
|
415
|
+
try {
|
|
416
|
+
const prefix = require('child_process').execFileSync('npm', ['config', 'get', 'prefix'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
417
|
+
if (!prefix) return null;
|
|
418
|
+
return currentPlatform() === 'win32' ? prefix : path.join(prefix, 'bin');
|
|
419
|
+
} catch {
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function isVolatilePathDir(dir) {
|
|
425
|
+
const lower = dir.toLowerCase();
|
|
426
|
+
return lower.includes(`${path.sep}_npx${path.sep}`) ||
|
|
427
|
+
lower.includes(`${path.sep}node_modules${path.sep}.bin`) ||
|
|
428
|
+
lower.includes(`${path.sep}npm-cache${path.sep}_npx${path.sep}`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function canWriteDir(dir) {
|
|
432
|
+
const probe = path.join(dir, `.memoc-write-test-${process.pid}-${Date.now()}`);
|
|
433
|
+
try {
|
|
434
|
+
fs.writeFileSync(probe, '');
|
|
435
|
+
fs.unlinkSync(probe);
|
|
436
|
+
return true;
|
|
437
|
+
} catch {
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function ensureUnixPathRegistration(binDir) {
|
|
443
|
+
if (process.env.MEMOC_SKIP_PATH_REGISTER === '1') return false;
|
|
444
|
+
|
|
445
|
+
const home = process.env.HOME;
|
|
446
|
+
if (!home) return false;
|
|
447
|
+
|
|
448
|
+
const block = [
|
|
449
|
+
'# memoc PATH',
|
|
450
|
+
`MEMOC_BIN=${shellSingleQuote(binDir)}`,
|
|
451
|
+
'case ":$PATH:" in *":$MEMOC_BIN:"*) ;; *) PATH="$MEMOC_BIN:$PATH"; export PATH ;; esac',
|
|
452
|
+
'# end memoc PATH',
|
|
453
|
+
].join('\n');
|
|
454
|
+
|
|
455
|
+
const candidates = [
|
|
456
|
+
path.join(home, '.profile'),
|
|
457
|
+
path.join(home, '.zshrc'),
|
|
458
|
+
path.join(home, '.bashrc'),
|
|
459
|
+
];
|
|
460
|
+
|
|
461
|
+
let changed = false;
|
|
462
|
+
for (const fp of candidates) {
|
|
463
|
+
try {
|
|
464
|
+
const src = fs.existsSync(fp) ? fs.readFileSync(fp, 'utf8') : '';
|
|
465
|
+
if (src.includes(binDir) || src.includes('# memoc PATH')) continue;
|
|
466
|
+
fs.appendFileSync(fp, `${src.endsWith('\n') || !src ? '' : '\n'}\n${block}\n`, 'utf8');
|
|
467
|
+
changed = true;
|
|
468
|
+
} catch {}
|
|
469
|
+
}
|
|
470
|
+
return changed;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function userPathShellHint(binDir) {
|
|
474
|
+
return `user bin ${binDir} ${process.env.MEMOC_SKIP_PATH_REGISTER === '1' ? 'test mode' : 'registered; open a new terminal if needed'}`;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function currentPlatform() {
|
|
478
|
+
return process.env.MEMOC_PLATFORM || process.platform;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function shellSingleQuote(value) {
|
|
482
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function psSingleQuote(value) {
|
|
486
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function escapeCmdPath(value) {
|
|
490
|
+
return String(value).replace(/"/g, '""');
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function samePath(a, b) {
|
|
494
|
+
if (!a || !b) return false;
|
|
495
|
+
const norm = p => path.resolve(p).toLowerCase().replace(/[\\/]+$/, '');
|
|
496
|
+
try { return norm(a) === norm(b); } catch { return false; }
|
|
497
|
+
}
|
|
498
|
+
|
|
499
499
|
function updateSection(filePath, startMark, endMark, inner) {
|
|
500
500
|
if (!fs.existsSync(filePath)) return false;
|
|
501
501
|
const src = fs.readFileSync(filePath, 'utf8');
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
}
|
|
502
|
+
const range = findMarkedRange(src, startMark, endMark);
|
|
503
|
+
if (!range) return false;
|
|
504
|
+
write(filePath,
|
|
505
|
+
src.slice(0, range.s) + startMark + '\n' + inner + '\n' + endMark + src.slice(range.e + range.endMark.length)
|
|
506
|
+
);
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
510
509
|
|
|
511
510
|
// ═══════════════════════════════════════════════════════════════════
|
|
512
511
|
// SECTION MARKERS
|
|
513
512
|
// ═══════════════════════════════════════════════════════════════════
|
|
514
513
|
|
|
515
|
-
const mk = n => [`<!--
|
|
516
|
-
const [MGMT_S, MGMT_E] = mk('managed');
|
|
517
|
-
const [ID_S, ID_E] = mk('identity');
|
|
518
|
-
const [SNAP_S, SNAP_E] = mk('snapshot');
|
|
519
|
-
const [CORE_S, CORE_E] = mk('core');
|
|
520
|
-
const [HDR_S, HDR_E] = mk('header');
|
|
521
|
-
const [SYS_S, SYS_E] = mk('systems');
|
|
522
|
-
const [WIKI_S, WIKI_E] = mk('wiki');
|
|
514
|
+
const mk = n => [`<!-- memoc:${n}:start -->`, `<!-- memoc:${n}:end -->`];
|
|
515
|
+
const [MGMT_S, MGMT_E] = mk('managed');
|
|
516
|
+
const [ID_S, ID_E] = mk('identity');
|
|
517
|
+
const [SNAP_S, SNAP_E] = mk('snapshot');
|
|
518
|
+
const [CORE_S, CORE_E] = mk('core');
|
|
519
|
+
const [HDR_S, HDR_E] = mk('header');
|
|
520
|
+
const [SYS_S, SYS_E] = mk('systems');
|
|
521
|
+
const [WIKI_S, WIKI_E] = mk('wiki');
|
|
522
|
+
|
|
523
|
+
function markerPairs(startMark, endMark) {
|
|
524
|
+
const legacyStart = startMark.replace('<!-- memoc:', '<!-- context-forge:');
|
|
525
|
+
const legacyEnd = endMark.replace('<!-- memoc:', '<!-- context-forge:');
|
|
526
|
+
return legacyStart === startMark
|
|
527
|
+
? [[startMark, endMark]]
|
|
528
|
+
: [[startMark, endMark], [legacyStart, legacyEnd]];
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function findMarkedRange(src, startMark, endMark) {
|
|
532
|
+
for (const [sMark, eMark] of markerPairs(startMark, endMark)) {
|
|
533
|
+
const s = src.indexOf(sMark);
|
|
534
|
+
const e = src.indexOf(eMark);
|
|
535
|
+
if (s !== -1 && e !== -1 && e > s) return { s, e, endMark: eMark };
|
|
536
|
+
}
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
523
539
|
|
|
524
540
|
// ═══════════════════════════════════════════════════════════════════
|
|
525
541
|
// AGENT REGISTRY — third-party agent entry files (added via `add`)
|
|
@@ -536,18 +552,18 @@ const AGENT_REGISTRY = {
|
|
|
536
552
|
// DYNAMIC CONTENT (re-generated on update)
|
|
537
553
|
// ═══════════════════════════════════════════════════════════════════
|
|
538
554
|
|
|
539
|
-
function
|
|
555
|
+
function legacyManagedBlock() {
|
|
540
556
|
return `${MGMT_S}
|
|
541
|
-
## Session Start
|
|
542
|
-
- [ ] Read \`.memoc/session-summary.md\`
|
|
543
|
-
- [ ] \`.pending\` exists? → review changed files → update memory if needed → delete it
|
|
544
|
-
- [ ] 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\`
|
|
545
|
-
|
|
546
|
-
## Before Opening More Files
|
|
547
|
-
- [ ] 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>"\`
|
|
548
|
-
- [ ] Open on demand: \`02\` status · \`04\` resume · \`06\` rules · \`llms.txt\` map
|
|
549
|
-
- [ ] If memory search is not enough, search project files with \`memoc grep "<query>" --limit 5\`
|
|
550
|
-
- [ ] Keep output small: \`summary\`, \`search --limit\`, \`grep --limit\`, \`--snippets\`
|
|
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\`
|
|
551
567
|
|
|
552
568
|
## Before Finishing _(update only applicable files; skip Q&A / throwaway exploration)_
|
|
553
569
|
- [ ] Code/config/deps changed → \`02\` (version, commands list, Last synced) + \`session-summary.md\` (status, changed, open tasks)
|
|
@@ -558,7 +574,29 @@ function managedBlock() {
|
|
|
558
574
|
${MGMT_E}`;
|
|
559
575
|
}
|
|
560
576
|
|
|
561
|
-
function
|
|
577
|
+
function managedBlock() {
|
|
578
|
+
return `${MGMT_S}
|
|
579
|
+
## Session Start
|
|
580
|
+
- [ ] Read \`.memoc/session-summary.md\`
|
|
581
|
+
- [ ] \`.pending\` exists? Review changed files, update memory if needed, then delete it.
|
|
582
|
+
- [ ] If \`memoc\` is not found, use the project-local wrapper for the rest of the session: Windows \`.\\.memoc\\bin\\memoc.cmd <command>\`; sh \`.memoc/bin/memoc <command>\`
|
|
583
|
+
|
|
584
|
+
## Before Opening More Files
|
|
585
|
+
- [ ] Search memory first: \`memoc search "<query>" --limit 5\`, or wrapper fallback above if PATH fails
|
|
586
|
+
- [ ] Open on demand: \`02\` status, \`04\` resume, \`06\` rules, \`llms.txt\` map
|
|
587
|
+
- [ ] If memory search is not enough, search project files with \`memoc grep "<query>" --limit 5\` (or wrapper fallback)
|
|
588
|
+
- [ ] Keep output small: \`summary\`, \`search --limit\`, \`grep --limit\`, \`--snippets\`
|
|
589
|
+
|
|
590
|
+
## Before Finishing _(update only applicable files; skip Q&A / throwaway exploration)_
|
|
591
|
+
- [ ] Code/config/deps changed? Update \`02\` + \`session-summary.md\`
|
|
592
|
+
- [ ] Decision made? Update \`03-decisions.md\` + \`02\`
|
|
593
|
+
- [ ] Work incomplete or risky? Update \`04-handoff.md\`
|
|
594
|
+
- [ ] Rule/preference set? Update \`06-project-rules.md\`
|
|
595
|
+
- [ ] Wiki/systems work? Read \`skills/project-memory-maintainer/SKILL.md\`
|
|
596
|
+
${MGMT_E}`;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function identityInner(p) {
|
|
562
600
|
return [
|
|
563
601
|
`- Project name: \`${p.name}\``,
|
|
564
602
|
`- Detected stack: ${stackStr(p.stack)}`,
|
|
@@ -858,14 +896,14 @@ On-demand reference only. The entry-file managed block is authoritative.
|
|
|
858
896
|
| \`.memoc/wiki/*.md\` | For synthesized project knowledge |
|
|
859
897
|
| \`llms.txt\` | For full project file map |
|
|
860
898
|
|
|
861
|
-
## Search First
|
|
862
|
-
|
|
863
|
-
\`memoc search "<query>"\` — returns file:line matches across memory and agent docs only.
|
|
864
|
-
\`memoc grep "<query>"\` — searches project source/text files when memory docs are not enough.
|
|
865
|
-
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>"\`.
|
|
866
|
-
Use it before opening any file to avoid reading more than needed.
|
|
867
|
-
`;
|
|
868
|
-
}
|
|
899
|
+
## Search First
|
|
900
|
+
|
|
901
|
+
\`memoc search "<query>"\` — returns file:line matches across memory and agent docs only.
|
|
902
|
+
\`memoc grep "<query>"\` — searches project source/text files when memory docs are not enough.
|
|
903
|
+
If \`memoc\` is not on PATH, try \`.\\.memoc\\bin\\memoc.cmd search "<query>"\` on Windows or \`.memoc/bin/memoc search "<query>"\` in sh, then \`npx @kevin0181/memoc search "<query>"\`.
|
|
904
|
+
Use it before opening any file to avoid reading more than needed.
|
|
905
|
+
`;
|
|
906
|
+
}
|
|
869
907
|
|
|
870
908
|
function tplWorkflow() {
|
|
871
909
|
return `# Agent Workflow
|
|
@@ -1020,20 +1058,23 @@ Append-only chronological log for project memory updates.
|
|
|
1020
1058
|
`;
|
|
1021
1059
|
}
|
|
1022
1060
|
|
|
1023
|
-
function
|
|
1024
|
-
return `# memoc Usage
|
|
1061
|
+
function tplMemocUsage() {
|
|
1062
|
+
return `# memoc Usage
|
|
1025
1063
|
|
|
1026
1064
|
This project uses \`memoc\` to maintain agent-readable project memory.
|
|
1027
1065
|
|
|
1028
|
-
## Commands
|
|
1029
|
-
|
|
1030
|
-
\`\`\`bash
|
|
1031
|
-
# Optional: put the project-local wrapper first in PATH for this shell
|
|
1032
|
-
# PowerShell: . .\\.memoc\\env.ps1
|
|
1033
|
-
# sh/bash: . ./.memoc/env.sh
|
|
1034
|
-
|
|
1035
|
-
# First-time setup (or re-run to update managed sections)
|
|
1036
|
-
memoc init
|
|
1066
|
+
## Commands
|
|
1067
|
+
|
|
1068
|
+
\`\`\`bash
|
|
1069
|
+
# Optional: put the project-local wrapper first in PATH for this shell
|
|
1070
|
+
# PowerShell: . .\\.memoc\\env.ps1
|
|
1071
|
+
# sh/bash: . ./.memoc/env.sh
|
|
1072
|
+
|
|
1073
|
+
# First-time setup (or re-run to update managed sections)
|
|
1074
|
+
memoc init
|
|
1075
|
+
|
|
1076
|
+
# Refresh memoc itself when run through npx @latest, preserving project memory
|
|
1077
|
+
memoc upgrade
|
|
1037
1078
|
|
|
1038
1079
|
# Explicitly update managed sections based on current project state
|
|
1039
1080
|
memoc update
|
|
@@ -1041,24 +1082,27 @@ memoc update
|
|
|
1041
1082
|
# Tiny status overview
|
|
1042
1083
|
memoc summary
|
|
1043
1084
|
|
|
1044
|
-
# Search memory first; add --snippets only when needed
|
|
1045
|
-
memoc search "<query>" --limit 12
|
|
1046
|
-
memoc search "<query>" --snippets --limit 5
|
|
1047
|
-
|
|
1048
|
-
# Search project source/text files when memory is not enough
|
|
1049
|
-
memoc grep "<query>" --limit 12
|
|
1050
|
-
memoc grep "<query>" --snippets --limit 5
|
|
1051
|
-
\`\`\`
|
|
1052
|
-
|
|
1053
|
-
If \`memoc\` is not on PATH, use \`.\\.memoc\\bin\\memoc.cmd <command>\` on Windows or \`.memoc/bin/memoc <command>\` in sh. If
|
|
1054
|
-
|
|
1085
|
+
# Search memory first; add --snippets only when needed
|
|
1086
|
+
memoc search "<query>" --limit 12
|
|
1087
|
+
memoc search "<query>" --snippets --limit 5
|
|
1088
|
+
|
|
1089
|
+
# Search project source/text files when memory is not enough
|
|
1090
|
+
memoc grep "<query>" --limit 12
|
|
1091
|
+
memoc grep "<query>" --snippets --limit 5
|
|
1092
|
+
\`\`\`
|
|
1093
|
+
|
|
1094
|
+
If \`memoc\` is not on PATH, use \`.\\.memoc\\bin\\memoc.cmd <command>\` on Windows or \`.memoc/bin/memoc <command>\` in sh for the rest of the session. If the local wrapper is missing, use \`npx @kevin0181/memoc <command>\` or re-run init.
|
|
1095
|
+
|
|
1055
1096
|
## Agent Read Order
|
|
1056
1097
|
|
|
1057
1098
|
1. Entry-file managed block.
|
|
1058
1099
|
2. \`.memoc/session-summary.md\` only.
|
|
1059
|
-
3. Search memory first: \`memoc search "<query>"\`.
|
|
1060
|
-
4.
|
|
1061
|
-
5.
|
|
1100
|
+
3. Search memory first with one or two concrete terms: \`memoc search "<query>" --limit 5\`.
|
|
1101
|
+
4. Open only the matching memory file(s) that matter.
|
|
1102
|
+
5. If memory is not enough, search project files: \`memoc grep "<query>" --limit 5\`.
|
|
1103
|
+
6. Use \`--snippets\` only when file names are not enough.
|
|
1104
|
+
|
|
1105
|
+
Use \`memoc search\` for known concepts, changed areas, decisions, tasks, or handoff notes. Skip it for brand-new questions where no prior memory can exist.
|
|
1062
1106
|
|
|
1063
1107
|
## When To Run Memory Updates
|
|
1064
1108
|
|
|
@@ -1149,11 +1193,11 @@ description: Maintain this project's LLM-wiki memory files after durable context
|
|
|
1149
1193
|
|
|
1150
1194
|
Use this local skill after meaningful project work so future agents can continue without rediscovering context.
|
|
1151
1195
|
|
|
1152
|
-
## Required Reads
|
|
1153
|
-
|
|
1154
|
-
1. \`.memoc/session-summary.md\`
|
|
1155
|
-
2. \`memoc summary\` or \`memoc search "<query>"\`; use \`memoc grep "<query>"\` only when source/text search is needed
|
|
1156
|
-
3. Open only files you will use or update.
|
|
1196
|
+
## Required Reads
|
|
1197
|
+
|
|
1198
|
+
1. \`.memoc/session-summary.md\`
|
|
1199
|
+
2. \`memoc summary\` or \`memoc search "<query>"\`; use \`memoc grep "<query>"\` only when source/text search is needed
|
|
1200
|
+
3. Open only files you will use or update.
|
|
1157
1201
|
|
|
1158
1202
|
## Maintenance Checklist
|
|
1159
1203
|
|
|
@@ -1188,9 +1232,9 @@ Usually skip for pure Q&A, tiny edits with no future impact, or throwaway explor
|
|
|
1188
1232
|
// CLAUDE CODE HOOK SETTINGS
|
|
1189
1233
|
// ═══════════════════════════════════════════════════════════════════
|
|
1190
1234
|
|
|
1191
|
-
function claudeStopHookCmd() {
|
|
1192
|
-
return `node -e "const fs=require('fs'),{
|
|
1193
|
-
}
|
|
1235
|
+
function claudeStopHookCmd() {
|
|
1236
|
+
return `node -e "const fs=require('fs'),{execFileSync}=require('child_process');try{const o=execFileSync('git',['status','--porcelain'],{encoding:'utf8',stdio:['ignore','pipe','ignore']});if(o.trim()){const files=o.trim().split(/\\r?\\n/).map(l=>l.slice(3).trim()).filter(Boolean).slice(0,8).join(', ');fs.writeFileSync('.memoc/.pending',new Date().toISOString()+'\\n'+files)}}catch{}"`;
|
|
1237
|
+
}
|
|
1194
1238
|
|
|
1195
1239
|
function tplClaudeSettings() {
|
|
1196
1240
|
return JSON.stringify({
|
|
@@ -1200,43 +1244,108 @@ function tplClaudeSettings() {
|
|
|
1200
1244
|
}, null, 2) + '\n';
|
|
1201
1245
|
}
|
|
1202
1246
|
|
|
1203
|
-
function ensureClaudeStopHook(settingsPath) {
|
|
1204
|
-
const cmd = claudeStopHookCmd();
|
|
1205
|
-
let settings;
|
|
1206
|
-
try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); }
|
|
1207
|
-
catch { settings = {}; }
|
|
1247
|
+
function ensureClaudeStopHook(settingsPath) {
|
|
1248
|
+
const cmd = claudeStopHookCmd();
|
|
1249
|
+
let settings;
|
|
1250
|
+
try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); }
|
|
1251
|
+
catch { settings = {}; }
|
|
1208
1252
|
|
|
1209
|
-
if (!settings.hooks) settings.hooks = {};
|
|
1210
|
-
if (!settings.hooks.Stop) settings.hooks.Stop = [];
|
|
1253
|
+
if (!settings.hooks || typeof settings.hooks !== 'object' || Array.isArray(settings.hooks)) settings.hooks = {};
|
|
1254
|
+
if (!Array.isArray(settings.hooks.Stop)) settings.hooks.Stop = [];
|
|
1211
1255
|
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
)
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1256
|
+
let hasCurrent = false;
|
|
1257
|
+
let changed = false;
|
|
1258
|
+
for (const entry of settings.hooks.Stop) {
|
|
1259
|
+
if (!Array.isArray(entry.hooks)) continue;
|
|
1260
|
+
const nextHooks = [];
|
|
1261
|
+
for (const hook of entry.hooks) {
|
|
1262
|
+
if (hook && hook.command === cmd) {
|
|
1263
|
+
if (hasCurrent) changed = true;
|
|
1264
|
+
else {
|
|
1265
|
+
hasCurrent = true;
|
|
1266
|
+
nextHooks.push(hook);
|
|
1267
|
+
}
|
|
1268
|
+
} else if (isMemocClaudeStopHook(hook)) {
|
|
1269
|
+
changed = true;
|
|
1270
|
+
} else {
|
|
1271
|
+
nextHooks.push(hook);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
entry.hooks = nextHooks;
|
|
1275
|
+
}
|
|
1276
|
+
settings.hooks.Stop = settings.hooks.Stop.filter(entry =>
|
|
1277
|
+
!Array.isArray(entry.hooks) || entry.hooks.length > 0
|
|
1278
|
+
);
|
|
1279
|
+
if (hasCurrent && !changed) return false; // no change needed
|
|
1280
|
+
|
|
1281
|
+
if (!hasCurrent) settings.hooks.Stop.push({ matcher: '', hooks: [{ type: 'command', command: cmd }] });
|
|
1282
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
1283
|
+
return true; // merged or migrated
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
function isMemocClaudeStopHook(hook) {
|
|
1287
|
+
if (!hook || typeof hook.command !== 'string') return false;
|
|
1288
|
+
const command = hook.command;
|
|
1289
|
+
return command.includes('.memoc/.pending') &&
|
|
1290
|
+
command.includes('git') &&
|
|
1291
|
+
command.includes('status') &&
|
|
1292
|
+
command.includes('--porcelain');
|
|
1293
|
+
}
|
|
1221
1294
|
|
|
1222
1295
|
// ═══════════════════════════════════════════════════════════════════
|
|
1223
1296
|
// MANAGED BLOCK UPDATE (CLAUDE.md / AGENTS.md)
|
|
1224
1297
|
// ═══════════════════════════════════════════════════════════════════
|
|
1225
1298
|
|
|
1226
|
-
function
|
|
1299
|
+
function ensureClaudeStopHookFile(dir, mark) {
|
|
1300
|
+
const claudeDir = path.join(dir, '.claude');
|
|
1301
|
+
const claudeSettings = path.join(claudeDir, 'settings.json');
|
|
1302
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
1303
|
+
if (!fs.existsSync(claudeSettings)) {
|
|
1304
|
+
write(claudeSettings, tplClaudeSettings());
|
|
1305
|
+
mark('add', '.claude/settings.json');
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
const merged = ensureClaudeStopHook(claudeSettings);
|
|
1309
|
+
mark(merged ? 'update' : 'skip', `.claude/settings.json (Stop hook ${merged ? 'merged' : 'already present'})`);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
function ensurePendingGitignore(dir, mark) {
|
|
1313
|
+
const gitignorePath = path.join(dir, '.gitignore');
|
|
1314
|
+
const PENDING_ENTRY = '.memoc/.pending';
|
|
1315
|
+
const gitignoreContent = fs.existsSync(gitignorePath)
|
|
1316
|
+
? fs.readFileSync(gitignorePath, 'utf8') : '';
|
|
1317
|
+
const hasPendingEntry = gitignoreContent
|
|
1318
|
+
.split(/\r?\n/)
|
|
1319
|
+
.some(line => line.trim() === PENDING_ENTRY);
|
|
1320
|
+
if (!hasPendingEntry) {
|
|
1321
|
+
fs.appendFileSync(gitignorePath, (gitignoreContent.endsWith('\n') ? '' : '\n') + PENDING_ENTRY + '\n', 'utf8');
|
|
1322
|
+
mark('update', '.gitignore (.memoc/.pending added)');
|
|
1323
|
+
} else {
|
|
1324
|
+
mark('skip', '.gitignore (.memoc/.pending already present)');
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
function printCommandHint() {
|
|
1329
|
+
console.log('\n Agent command fallback:');
|
|
1330
|
+
console.log(' memoc summary');
|
|
1331
|
+
console.log(' .\\.memoc\\bin\\memoc.cmd summary # Windows');
|
|
1332
|
+
console.log(' .memoc/bin/memoc summary # macOS/Linux sh');
|
|
1333
|
+
console.log(' If PATH fails once, use the project-local wrapper for the rest of the session.');
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
function applyManagedBlock(filePath, tplFn) {
|
|
1227
1337
|
if (!fs.existsSync(filePath)) {
|
|
1228
1338
|
write(filePath, tplFn());
|
|
1229
1339
|
return 'add';
|
|
1230
1340
|
}
|
|
1231
1341
|
const src = fs.readFileSync(filePath, 'utf8');
|
|
1232
|
-
const
|
|
1233
|
-
|
|
1234
|
-
if (s === -1 || e === -1) {
|
|
1342
|
+
const range = findMarkedRange(src, MGMT_S, MGMT_E);
|
|
1343
|
+
if (!range) {
|
|
1235
1344
|
// No managed block — inject at end, preserving all user content
|
|
1236
1345
|
write(filePath, src.trimEnd() + '\n\n' + managedBlock() + '\n');
|
|
1237
1346
|
return 'inject';
|
|
1238
1347
|
}
|
|
1239
|
-
write(filePath, src.slice(0, s) + managedBlock() + src.slice(e +
|
|
1348
|
+
write(filePath, src.slice(0, range.s) + managedBlock() + src.slice(range.e + range.endMark.length));
|
|
1240
1349
|
return 'update';
|
|
1241
1350
|
}
|
|
1242
1351
|
|
|
@@ -1244,7 +1353,7 @@ function applyManagedBlock(filePath, tplFn) {
|
|
|
1244
1353
|
// MAIN RUNNER
|
|
1245
1354
|
// ═══════════════════════════════════════════════════════════════════
|
|
1246
1355
|
|
|
1247
|
-
function run(dir, forceUpdate) {
|
|
1356
|
+
function run(dir, forceUpdate, action = 'update') {
|
|
1248
1357
|
const p = scanProject(dir);
|
|
1249
1358
|
const memDir = path.join(dir, '.memoc');
|
|
1250
1359
|
const isNew = !fs.existsSync(path.join(memDir, 'boot.md'));
|
|
@@ -1288,7 +1397,7 @@ function run(dir, forceUpdate) {
|
|
|
1288
1397
|
[path.join(memDir, '05-done-checklist.md'), tplDoneChecklist],
|
|
1289
1398
|
[path.join(memDir, '06-project-rules.md'), tplProjectRules],
|
|
1290
1399
|
[path.join(memDir, 'log.md'), tplLog],
|
|
1291
|
-
[path.join(memDir, 'memoc-usage.md'),
|
|
1400
|
+
[path.join(memDir, 'memoc-usage.md'), tplMemocUsage],
|
|
1292
1401
|
[path.join(memDir, 'systems/README.md'), tplSystemsReadme],
|
|
1293
1402
|
[path.join(memDir, 'wiki/index.md'), tplWikiIndex],
|
|
1294
1403
|
[path.join(memDir, 'wiki/sources.md'), tplWikiSources],
|
|
@@ -1306,36 +1415,18 @@ function run(dir, forceUpdate) {
|
|
|
1306
1415
|
}
|
|
1307
1416
|
|
|
1308
1417
|
// Claude Code Stop hook — writes .memoc/.pending when git detects changes
|
|
1309
|
-
|
|
1310
|
-
const claudeSettings = path.join(claudeDir, 'settings.json');
|
|
1311
|
-
fs.mkdirSync(claudeDir, { recursive: true });
|
|
1312
|
-
if (!fs.existsSync(claudeSettings)) {
|
|
1313
|
-
write(claudeSettings, tplClaudeSettings());
|
|
1314
|
-
mark('add', '.claude/settings.json');
|
|
1315
|
-
} else {
|
|
1316
|
-
const merged = ensureClaudeStopHook(claudeSettings);
|
|
1317
|
-
mark(merged ? 'update' : 'skip', `.claude/settings.json (Stop hook ${merged ? 'merged' : 'already present'})`);
|
|
1318
|
-
}
|
|
1418
|
+
ensureClaudeStopHookFile(dir, mark);
|
|
1319
1419
|
|
|
1320
1420
|
// .gitignore — add .memoc/.pending if not already present
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
? fs.readFileSync(gitignorePath, 'utf8') : '';
|
|
1325
|
-
if (!gitignoreContent.includes(PENDING_ENTRY)) {
|
|
1326
|
-
fs.appendFileSync(gitignorePath, (gitignoreContent.endsWith('\n') ? '' : '\n') + PENDING_ENTRY + '\n', 'utf8');
|
|
1327
|
-
mark('update', '.gitignore (.memoc/.pending added)');
|
|
1328
|
-
} else {
|
|
1329
|
-
mark('skip', '.gitignore (.memoc/.pending already present)');
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
// PATH helpers — let agents run memoc even when the npm bin is not on PATH
|
|
1421
|
+
ensurePendingGitignore(dir, mark);
|
|
1422
|
+
|
|
1423
|
+
// PATH helpers — let agents run memoc even when the npm bin is not on PATH
|
|
1333
1424
|
ensurePathHelpers(dir, mark);
|
|
1334
|
-
ensurePathRegistration(dir, mark);
|
|
1335
|
-
|
|
1336
|
-
} else {
|
|
1425
|
+
ensurePathRegistration(dir, mark);
|
|
1426
|
+
|
|
1427
|
+
} else {
|
|
1337
1428
|
// ── UPDATE MODE
|
|
1338
|
-
console.log(`\n memoc
|
|
1429
|
+
console.log(`\n memoc ${action} — ${path.basename(dir)}`);
|
|
1339
1430
|
console.log(` Re-scanning project: ${p.isEmpty ? 'nothing detected' : stackStr(p.stack)}`);
|
|
1340
1431
|
console.log();
|
|
1341
1432
|
|
|
@@ -1404,7 +1495,7 @@ function run(dir, forceUpdate) {
|
|
|
1404
1495
|
[path.join(memDir, '05-done-checklist.md'), tplDoneChecklist],
|
|
1405
1496
|
[path.join(memDir, '06-project-rules.md'), tplProjectRules],
|
|
1406
1497
|
[path.join(memDir, 'log.md'), tplLog],
|
|
1407
|
-
[path.join(memDir, 'memoc-usage.md'),
|
|
1498
|
+
[path.join(memDir, 'memoc-usage.md'), tplMemocUsage],
|
|
1408
1499
|
[path.join(memDir, 'systems/README.md'), tplSystemsReadme],
|
|
1409
1500
|
[path.join(memDir, 'wiki/index.md'), tplWikiIndex],
|
|
1410
1501
|
[path.join(memDir, 'wiki/sources.md'), tplWikiSources],
|
|
@@ -1416,13 +1507,15 @@ function run(dir, forceUpdate) {
|
|
|
1416
1507
|
[path.join(memDir, 'wiki/global/README.md'), tplWikiGlobalReadme],
|
|
1417
1508
|
[path.join(dir, 'skills/project-memory-maintainer/SKILL.md'), tplSkillMaintainer],
|
|
1418
1509
|
];
|
|
1419
|
-
for (const [fp, tpl] of addIfMissing) {
|
|
1420
|
-
const rel = path.relative(dir, fp);
|
|
1421
|
-
if (ensure(fp, tpl())) mark('add', rel);
|
|
1422
|
-
// silently skip existing — user/agent owns them
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
// PATH helpers — let agents run memoc even when the npm bin is not on PATH
|
|
1510
|
+
for (const [fp, tpl] of addIfMissing) {
|
|
1511
|
+
const rel = path.relative(dir, fp);
|
|
1512
|
+
if (ensure(fp, tpl())) mark('add', rel);
|
|
1513
|
+
// silently skip existing — user/agent owns them
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// PATH helpers — let agents run memoc even when the npm bin is not on PATH
|
|
1517
|
+
ensureClaudeStopHookFile(dir, mark);
|
|
1518
|
+
ensurePendingGitignore(dir, mark);
|
|
1426
1519
|
ensurePathHelpers(dir, mark);
|
|
1427
1520
|
ensurePathRegistration(dir, mark);
|
|
1428
1521
|
|
|
@@ -1430,17 +1523,18 @@ function run(dir, forceUpdate) {
|
|
|
1430
1523
|
const logPath = path.join(memDir, 'log.md');
|
|
1431
1524
|
if (fs.existsSync(logPath)) {
|
|
1432
1525
|
fs.appendFileSync(logPath,
|
|
1433
|
-
`\n## [${nowISO()}]
|
|
1526
|
+
`\n## [${nowISO()}] ${action} | Re-scanned: ${p.isEmpty ? 'nothing detected' : stackStr(p.stack)}\n`,
|
|
1434
1527
|
'utf8'
|
|
1435
1528
|
);
|
|
1436
1529
|
mark('append', '.memoc/log.md');
|
|
1437
1530
|
}
|
|
1438
1531
|
}
|
|
1439
1532
|
|
|
1440
|
-
hideOnWindows(memDir);
|
|
1441
|
-
console.log(log.join('\n'));
|
|
1442
|
-
|
|
1443
|
-
|
|
1533
|
+
hideOnWindows(memDir);
|
|
1534
|
+
console.log(log.join('\n'));
|
|
1535
|
+
printCommandHint();
|
|
1536
|
+
console.log('\n Done.');
|
|
1537
|
+
}
|
|
1444
1538
|
|
|
1445
1539
|
// ═══════════════════════════════════════════════════════════════════
|
|
1446
1540
|
// ADD — add entry file for a specific agent
|
|
@@ -1476,9 +1570,9 @@ function runAdd(dir) {
|
|
|
1476
1570
|
// SEARCH
|
|
1477
1571
|
// ═══════════════════════════════════════════════════════════════════
|
|
1478
1572
|
|
|
1479
|
-
function runSearch(dir, scope = 'memory') {
|
|
1480
|
-
const rawArgs = process.argv.slice(3);
|
|
1481
|
-
const opts = { mode: 'files', limit: 12, all: false };
|
|
1573
|
+
function runSearch(dir, scope = 'memory') {
|
|
1574
|
+
const rawArgs = process.argv.slice(3);
|
|
1575
|
+
const opts = { mode: 'files', limit: 12, all: false };
|
|
1482
1576
|
const queryParts = [];
|
|
1483
1577
|
|
|
1484
1578
|
for (let i = 0; i < rawArgs.length; i++) {
|
|
@@ -1499,12 +1593,12 @@ function runSearch(dir, scope = 'memory') {
|
|
|
1499
1593
|
queryParts.push(arg);
|
|
1500
1594
|
}
|
|
1501
1595
|
|
|
1502
|
-
const query = queryParts.join(' ').toLowerCase();
|
|
1503
|
-
|
|
1504
|
-
const searchRoots = scope === 'project' ? [dir] : memorySearchRoots(dir);
|
|
1596
|
+
const query = queryParts.join(' ').toLowerCase();
|
|
1597
|
+
|
|
1598
|
+
const searchRoots = scope === 'project' ? [dir] : memorySearchRoots(dir);
|
|
1505
1599
|
|
|
1506
1600
|
if (!query) {
|
|
1507
|
-
// No query — list searchable files sorted by recency
|
|
1601
|
+
// No query — list searchable files sorted by recency
|
|
1508
1602
|
const allFiles = [];
|
|
1509
1603
|
function collectFile(fp) {
|
|
1510
1604
|
if (!fs.existsSync(fp)) return;
|
|
@@ -1518,10 +1612,10 @@ function runSearch(dir, scope = 'memory') {
|
|
|
1518
1612
|
for (const entry of fs.readdirSync(d)) {
|
|
1519
1613
|
const fp = path.join(d, entry);
|
|
1520
1614
|
try {
|
|
1521
|
-
const st = fs.statSync(fp);
|
|
1522
|
-
if (st.isDirectory()) {
|
|
1523
|
-
if (!shouldSkipSearchDir(entry)) collectDir(fp);
|
|
1524
|
-
} else if (isSearchableFile(fp, entry, st, scope)) collectFile(fp);
|
|
1615
|
+
const st = fs.statSync(fp);
|
|
1616
|
+
if (st.isDirectory()) {
|
|
1617
|
+
if (!shouldSkipSearchDir(entry, scope)) collectDir(fp);
|
|
1618
|
+
} else if (isSearchableFile(fp, entry, st, scope)) collectFile(fp);
|
|
1525
1619
|
} catch {}
|
|
1526
1620
|
}
|
|
1527
1621
|
}
|
|
@@ -1542,15 +1636,15 @@ function runSearch(dir, scope = 'memory') {
|
|
|
1542
1636
|
|
|
1543
1637
|
const matchesByFile = new Map(); // rel -> { matches: [], mtime: number }
|
|
1544
1638
|
|
|
1545
|
-
function searchFile(fp) {
|
|
1546
|
-
if (!fs.existsSync(fp)) return;
|
|
1547
|
-
const rel = path.relative(dir, fp);
|
|
1548
|
-
let mtime = 0;
|
|
1549
|
-
try {
|
|
1550
|
-
const st = fs.statSync(fp);
|
|
1551
|
-
if (!isSearchableFile(fp, path.basename(fp), st, scope)) return;
|
|
1552
|
-
mtime = st.mtimeMs;
|
|
1553
|
-
} catch {}
|
|
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 {}
|
|
1554
1648
|
const lines = fs.readFileSync(fp, 'utf8').split('\n');
|
|
1555
1649
|
lines.forEach((line, i) => {
|
|
1556
1650
|
if (line.toLowerCase().includes(query)) {
|
|
@@ -1565,10 +1659,10 @@ function runSearch(dir, scope = 'memory') {
|
|
|
1565
1659
|
for (const entry of fs.readdirSync(d)) {
|
|
1566
1660
|
const fp = path.join(d, entry);
|
|
1567
1661
|
try {
|
|
1568
|
-
const st = fs.statSync(fp);
|
|
1662
|
+
const st = fs.statSync(fp);
|
|
1569
1663
|
if (st.isDirectory()) {
|
|
1570
|
-
if (!shouldSkipSearchDir(entry)) walkDir(fp);
|
|
1571
|
-
} else if (isSearchableFile(fp, entry, st, scope)) searchFile(fp);
|
|
1664
|
+
if (!shouldSkipSearchDir(entry, scope)) walkDir(fp);
|
|
1665
|
+
} else if (isSearchableFile(fp, entry, st, scope)) searchFile(fp);
|
|
1572
1666
|
} catch {}
|
|
1573
1667
|
}
|
|
1574
1668
|
}
|
|
@@ -1583,63 +1677,116 @@ function runSearch(dir, scope = 'memory') {
|
|
|
1583
1677
|
if (!matchesByFile.size) {
|
|
1584
1678
|
console.log('No matches found.');
|
|
1585
1679
|
} else if (opts.mode === 'files') {
|
|
1586
|
-
const rows = [...matchesByFile.entries()]
|
|
1587
|
-
.map(([file, { matches, mtime }]) => ({ file, count: matches.length, mtime }))
|
|
1588
|
-
.sort((a, b) =>
|
|
1680
|
+
const rows = [...matchesByFile.entries()]
|
|
1681
|
+
.map(([file, { matches, mtime }]) => ({ file, count: matches.length, mtime }))
|
|
1682
|
+
.sort((a, b) =>
|
|
1683
|
+
searchPriority(a.file, scope) - searchPriority(b.file, scope) ||
|
|
1684
|
+
b.count - a.count ||
|
|
1685
|
+
b.mtime - a.mtime ||
|
|
1686
|
+
a.file.localeCompare(b.file)
|
|
1687
|
+
);
|
|
1589
1688
|
const limited = opts.all ? rows : rows.slice(0, opts.limit);
|
|
1590
1689
|
console.log(limited.map(r => `${r.file} ${r.count} match${r.count === 1 ? '' : 'es'}`).join('\n'));
|
|
1591
1690
|
if (!opts.all && rows.length > limited.length) {
|
|
1592
1691
|
console.log(`... ${rows.length - limited.length} more files. Use --all to show all, or --snippets for line matches.`);
|
|
1593
1692
|
}
|
|
1594
1693
|
} else {
|
|
1595
|
-
const snippets = [];
|
|
1596
|
-
for (const [file, { matches }] of matchesByFile.entries()) {
|
|
1597
|
-
for (const m of matches) snippets.push(
|
|
1598
|
-
}
|
|
1599
|
-
|
|
1600
|
-
|
|
1694
|
+
const snippets = [];
|
|
1695
|
+
for (const [file, { matches }] of matchesByFile.entries()) {
|
|
1696
|
+
for (const m of matches) snippets.push({ file, line: m.line, text: m.text });
|
|
1697
|
+
}
|
|
1698
|
+
snippets.sort((a, b) =>
|
|
1699
|
+
searchPriority(a.file, scope) - searchPriority(b.file, scope) ||
|
|
1700
|
+
a.file.localeCompare(b.file) ||
|
|
1701
|
+
a.line - b.line
|
|
1702
|
+
);
|
|
1703
|
+
const limited = opts.all ? snippets : snippets.slice(0, opts.limit);
|
|
1704
|
+
console.log(limited.map(m => `${m.file}:${m.line} ${m.text}`).join('\n'));
|
|
1601
1705
|
if (!opts.all && snippets.length > limited.length) {
|
|
1602
1706
|
console.log(`... ${snippets.length - limited.length} more matches. Use --all to show all, or --limit N.`);
|
|
1603
1707
|
}
|
|
1604
1708
|
}
|
|
1605
|
-
}
|
|
1606
|
-
|
|
1607
|
-
function memorySearchRoots(dir) {
|
|
1608
|
-
return [
|
|
1609
|
-
path.join(dir, '.memoc'),
|
|
1610
|
-
path.join(dir, 'skills'),
|
|
1611
|
-
path.join(dir, 'llms.txt'),
|
|
1612
|
-
path.join(dir, 'AGENTS.md'),
|
|
1613
|
-
path.join(dir, 'CLAUDE.md'),
|
|
1614
|
-
...Object.values(AGENT_REGISTRY).map(agent => path.join(dir, agent.file)),
|
|
1615
|
-
];
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
|
-
function shouldSkipSearchDir(name) {
|
|
1619
|
-
|
|
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
|
+
|
|
1722
|
+
function shouldSkipSearchDir(name, scope = 'memory') {
|
|
1723
|
+
const skipped = new Set([
|
|
1620
1724
|
'.git', 'node_modules', '.next', 'dist', 'build', 'out', 'coverage',
|
|
1621
1725
|
'Saved', 'Intermediate', 'DerivedDataCache', 'Binaries',
|
|
1622
1726
|
'.venv', 'venv', '__pycache__', '.pytest_cache',
|
|
1623
|
-
])
|
|
1727
|
+
]);
|
|
1728
|
+
if (scope === 'project') {
|
|
1729
|
+
skipped.add('.memoc');
|
|
1730
|
+
skipped.add('skills');
|
|
1731
|
+
skipped.add('.claude');
|
|
1732
|
+
}
|
|
1733
|
+
return skipped.has(name);
|
|
1624
1734
|
}
|
|
1625
1735
|
|
|
1626
1736
|
function isSearchableFile(fp, name, st, scope = 'memory') {
|
|
1627
1737
|
if (!st || !st.isFile()) return false;
|
|
1628
1738
|
if (st.size > 1024 * 1024) return false;
|
|
1739
|
+
if (scope === 'project' && isAgentMemoryFile(name)) return false;
|
|
1629
1740
|
if (name === 'llms.txt' || name.endsWith('rules')) return true;
|
|
1630
1741
|
const ext = path.extname(fp).toLowerCase();
|
|
1631
|
-
if (scope === 'memory') {
|
|
1632
|
-
return new Set(['.md', '.txt']).has(ext);
|
|
1633
|
-
}
|
|
1634
|
-
return new Set([
|
|
1635
|
-
'.md', '.txt', '.json', '.jsonc', '.yaml', '.yml', '.toml', '.ini', '.env',
|
|
1636
|
-
'.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs',
|
|
1637
|
-
'.py', '.rs', '.go', '.java', '.cs', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.hxx',
|
|
1638
|
-
'.html', '.css', '.scss', '.sass', '.vue', '.svelte',
|
|
1639
|
-
'.sql', '.graphql', '.gql', '.sh', '.bash', '.zsh', '.ps1', '.bat', '.cmd',
|
|
1640
|
-
'.xml', '.gradle', '.kts', '.cmake',
|
|
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',
|
|
1641
1752
|
]).has(ext);
|
|
1642
1753
|
}
|
|
1754
|
+
|
|
1755
|
+
function isAgentMemoryFile(name) {
|
|
1756
|
+
return new Set([
|
|
1757
|
+
'AGENTS.md',
|
|
1758
|
+
'CLAUDE.md',
|
|
1759
|
+
'GEMINI.md',
|
|
1760
|
+
'llms.txt',
|
|
1761
|
+
'.cursorrules',
|
|
1762
|
+
'.windsurfrules',
|
|
1763
|
+
'copilot-instructions.md',
|
|
1764
|
+
]).has(name);
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
function searchPriority(file, scope = 'memory') {
|
|
1768
|
+
if (scope !== 'memory') return 0;
|
|
1769
|
+
const normalized = file.replace(/\\/g, '/');
|
|
1770
|
+
const order = [
|
|
1771
|
+
'.memoc/session-summary.md',
|
|
1772
|
+
'.memoc/02-current-project-state.md',
|
|
1773
|
+
'.memoc/04-handoff.md',
|
|
1774
|
+
'.memoc/06-project-rules.md',
|
|
1775
|
+
'.memoc/03-decisions.md',
|
|
1776
|
+
'.memoc/log.md',
|
|
1777
|
+
'AGENTS.md',
|
|
1778
|
+
'CLAUDE.md',
|
|
1779
|
+
'llms.txt',
|
|
1780
|
+
'.memoc/00-project-brief.md',
|
|
1781
|
+
'.memoc/00-agent-index.md',
|
|
1782
|
+
];
|
|
1783
|
+
const exact = order.indexOf(normalized);
|
|
1784
|
+
if (exact !== -1) return exact;
|
|
1785
|
+
if (normalized.startsWith('.memoc/systems/')) return 20;
|
|
1786
|
+
if (normalized.startsWith('.memoc/wiki/')) return 30;
|
|
1787
|
+
if (normalized.startsWith('skills/')) return 40;
|
|
1788
|
+
return 50;
|
|
1789
|
+
}
|
|
1643
1790
|
|
|
1644
1791
|
// ═══════════════════════════════════════════════════════════════════
|
|
1645
1792
|
// TOKENS — estimate token cost of current memory state
|
|
@@ -1810,15 +1957,16 @@ if (cmd === '--version' || cmd === '-v') {
|
|
|
1810
1957
|
|
|
1811
1958
|
if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') {
|
|
1812
1959
|
console.log('Usage: memoc <command>\n');
|
|
1813
|
-
console.log('Commands:');
|
|
1814
|
-
console.log(' init Scaffold agent memory (auto-detects project, updates if already exists)');
|
|
1815
|
-
console.log(' update Force-update managed sections based on current project state');
|
|
1816
|
-
console.log('
|
|
1960
|
+
console.log('Commands:');
|
|
1961
|
+
console.log(' init Scaffold agent memory (auto-detects project, updates if already exists)');
|
|
1962
|
+
console.log(' update Force-update managed sections based on current project state');
|
|
1963
|
+
console.log(' upgrade Refresh memoc runtime/wrappers and managed sections; preserve memory');
|
|
1964
|
+
console.log(' summary Print a tiny status/resume overview');
|
|
1817
1965
|
console.log(' tokens Estimate token cost of current memory files');
|
|
1818
1966
|
console.log(' compress Archive old log.md entries to keep file small');
|
|
1819
1967
|
console.log(' add <agent> Add entry file for a specific agent (run without args to list)');
|
|
1820
|
-
console.log(' search "<query>" Search memory/agent docs (use --snippets for line matches)');
|
|
1821
|
-
console.log(' grep "<query>" Search project source/text files (use --snippets for line matches)');
|
|
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)');
|
|
1822
1970
|
console.log('\nSearch flags:');
|
|
1823
1971
|
console.log(' --files Show file names and match counts, sorted by relevance + recency (default)');
|
|
1824
1972
|
console.log(' --snippets Show matching lines');
|
|
@@ -1829,14 +1977,15 @@ if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') {
|
|
|
1829
1977
|
process.exit(0);
|
|
1830
1978
|
}
|
|
1831
1979
|
|
|
1832
|
-
if (cmd === 'init') { run(cwd, false);
|
|
1833
|
-
if (cmd === 'update') { run(cwd, true);
|
|
1980
|
+
if (cmd === 'init') { run(cwd, false); process.exit(0); }
|
|
1981
|
+
if (cmd === 'update') { run(cwd, true, 'update'); process.exit(0); }
|
|
1982
|
+
if (cmd === 'upgrade') { run(cwd, true, 'upgrade'); process.exit(0); }
|
|
1834
1983
|
if (cmd === 'summary') { runSummary(cwd); process.exit(0); }
|
|
1835
1984
|
if (cmd === 'tokens') { runTokens(cwd); process.exit(0); }
|
|
1836
1985
|
if (cmd === 'compress') { runCompress(cwd); process.exit(0); }
|
|
1837
1986
|
if (cmd === 'add') { runAdd(cwd); process.exit(0); }
|
|
1838
|
-
if (cmd === 'search') { runSearch(cwd, 'memory'); process.exit(0); }
|
|
1839
|
-
if (cmd === 'grep') { runSearch(cwd, 'project'); process.exit(0); }
|
|
1987
|
+
if (cmd === 'search') { runSearch(cwd, 'memory'); process.exit(0); }
|
|
1988
|
+
if (cmd === 'grep') { runSearch(cwd, 'project'); process.exit(0); }
|
|
1840
1989
|
|
|
1841
1990
|
console.error(`Unknown command: ${cmd}`);
|
|
1842
1991
|
console.error('Run "memoc --help" for usage.');
|