@kevin0181/memoc 1.0.8 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +92 -19
  2. package/bin/cli.js +649 -500
  3. 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').execSync(`attrib +h "${dirPath}"`, { stdio: 'ignore' }); } catch {}
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="$(pwd)/.memoc"\ncase ":$PATH:" in\n *":$MEMOC_DIR/bin:"*) ;;\n *) PATH="$MEMOC_DIR/bin:$PATH"; export PATH ;;\nesac\n`;
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 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
-
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 s = src.indexOf(startMark);
503
- const e = src.indexOf(endMark);
504
- if (s === -1 || e === -1) return false;
505
- write(filePath,
506
- src.slice(0, s) + startMark + '\n' + inner + '\n' + endMark + src.slice(e + endMark.length)
507
- );
508
- return true;
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 => [`<!-- context-forge:${n}:start -->`, `<!-- context-forge:${n}:end -->`];
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 managedBlock() {
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 identityInner(p) {
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 tplContextForgeUsage() {
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 that is unavailable, use \`npx @kevin0181/memoc <command>\`.
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. If memory is not enough, search project files: \`memoc grep "<query>" --limit 5\`.
1061
- 5. Use \`--snippets\` only when file names are not enough.
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'),{execSync}=require('child_process');try{const o=execSync('git status --porcelain',{stdio:'pipe'}).toString();if(o.trim()){const files=o.trim().split('\\n').map(l=>l.slice(3).trim()).filter(Boolean).slice(0,8).join(', ');fs.writeFileSync('.memoc/.pending',new Date().toISOString()+'\\n'+files)}}catch{}" 2>/dev/null || true`;
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
- const alreadyPresent = settings.hooks.Stop.some(entry =>
1213
- Array.isArray(entry.hooks) && entry.hooks.some(h => h.command === cmd)
1214
- );
1215
- if (alreadyPresent) return false; // no change needed
1216
-
1217
- settings.hooks.Stop.push({ matcher: '', hooks: [{ type: 'command', command: cmd }] });
1218
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
1219
- return true; // merged
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 applyManagedBlock(filePath, tplFn) {
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 s = src.indexOf(MGMT_S);
1233
- const e = src.indexOf(MGMT_E);
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 + MGMT_E.length));
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'), tplContextForgeUsage],
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
- const claudeDir = path.join(dir, '.claude');
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
- const gitignorePath = path.join(dir, '.gitignore');
1322
- const PENDING_ENTRY = '.memoc/.pending';
1323
- const gitignoreContent = fs.existsSync(gitignorePath)
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 update — ${path.basename(dir)}`);
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'), tplContextForgeUsage],
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()}] update | Re-scanned: ${p.isEmpty ? 'nothing detected' : stackStr(p.stack)}\n`,
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
- console.log('\n Done.');
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) => b.count - a.count || b.mtime - a.mtime || a.file.localeCompare(b.file));
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(`${file}:${m.line} ${m.text}`);
1598
- }
1599
- const limited = opts.all ? snippets : snippets.slice(0, opts.limit);
1600
- console.log(limited.join('\n'));
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
- return new Set([
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
- ]).has(name);
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(' summary Print a tiny status/resume overview');
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); process.exit(0); }
1833
- if (cmd === 'update') { run(cwd, true); process.exit(0); }
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.');