@kevin0181/memoc 1.0.6 → 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 +90 -13
  2. package/bin/cli.js +652 -457
  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,17 +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
- - [ ] Keep output small: \`summary\`, \`search --limit\`, \`search --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\`
550
567
 
551
568
  ## Before Finishing _(update only applicable files; skip Q&A / throwaway exploration)_
552
569
  - [ ] Code/config/deps changed → \`02\` (version, commands list, Last synced) + \`session-summary.md\` (status, changed, open tasks)
@@ -557,7 +574,29 @@ function managedBlock() {
557
574
  ${MGMT_E}`;
558
575
  }
559
576
 
560
- 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) {
561
600
  return [
562
601
  `- Project name: \`${p.name}\``,
563
602
  `- Detected stack: ${stackStr(p.stack)}`,
@@ -857,13 +896,14 @@ On-demand reference only. The entry-file managed block is authoritative.
857
896
  | \`.memoc/wiki/*.md\` | For synthesized project knowledge |
858
897
  | \`llms.txt\` | For full project file map |
859
898
 
860
- ## Search First
861
-
862
- \`memoc search "<query>"\` — returns file:line matches across all memory files.
863
- 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>"\`.
864
- Use it before opening any file to avoid reading more than needed.
865
- `;
866
- }
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
+ }
867
907
 
868
908
  function tplWorkflow() {
869
909
  return `# Agent Workflow
@@ -1018,20 +1058,23 @@ Append-only chronological log for project memory updates.
1018
1058
  `;
1019
1059
  }
1020
1060
 
1021
- function tplContextForgeUsage() {
1022
- return `# memoc Usage
1061
+ function tplMemocUsage() {
1062
+ return `# memoc Usage
1023
1063
 
1024
1064
  This project uses \`memoc\` to maintain agent-readable project memory.
1025
1065
 
1026
- ## Commands
1027
-
1028
- \`\`\`bash
1029
- # Optional: put the project-local wrapper first in PATH for this shell
1030
- # PowerShell: . .\\.memoc\\env.ps1
1031
- # sh/bash: . ./.memoc/env.sh
1032
-
1033
- # First-time setup (or re-run to update managed sections)
1034
- 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
1035
1078
 
1036
1079
  # Explicitly update managed sections based on current project state
1037
1080
  memoc update
@@ -1039,20 +1082,27 @@ memoc update
1039
1082
  # Tiny status overview
1040
1083
  memoc summary
1041
1084
 
1042
- # Find files first; add --snippets only when needed
1085
+ # Search memory first; add --snippets only when needed
1043
1086
  memoc search "<query>" --limit 12
1044
- memoc search "<query>" --snippets --limit 5
1045
- \`\`\`
1046
-
1047
- 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>\`.
1048
-
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
+
1049
1096
  ## Agent Read Order
1050
1097
 
1051
1098
  1. Entry-file managed block.
1052
1099
  2. \`.memoc/session-summary.md\` only.
1053
- 3. Search in this order: \`memoc search "<query>"\`, \`.\\.memoc\\bin\\memoc.cmd search "<query>"\` or \`.memoc/bin/memoc search "<query>"\`, \`npx @kevin0181/memoc search "<query>"\`.
1054
- 4. Use \`--snippets\` only when file names are not enough.
1055
- 5. Open only task-relevant memory files.
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.
1056
1106
 
1057
1107
  ## When To Run Memory Updates
1058
1108
 
@@ -1143,11 +1193,11 @@ description: Maintain this project's LLM-wiki memory files after durable context
1143
1193
 
1144
1194
  Use this local skill after meaningful project work so future agents can continue without rediscovering context.
1145
1195
 
1146
- ## Required Reads
1147
-
1148
- 1. \`.memoc/session-summary.md\`
1149
- 2. \`memoc summary\` or \`memoc search "<query>"\`; if unavailable, use \`.\\.memoc\\bin\\memoc.cmd <command>\` or \`.memoc/bin/memoc <command>\`, then \`npx @kevin0181/memoc <command>\`
1150
- 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.
1151
1201
 
1152
1202
  ## Maintenance Checklist
1153
1203
 
@@ -1182,9 +1232,9 @@ Usually skip for pure Q&A, tiny edits with no future impact, or throwaway explor
1182
1232
  // CLAUDE CODE HOOK SETTINGS
1183
1233
  // ═══════════════════════════════════════════════════════════════════
1184
1234
 
1185
- function claudeStopHookCmd() {
1186
- 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`;
1187
- }
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
+ }
1188
1238
 
1189
1239
  function tplClaudeSettings() {
1190
1240
  return JSON.stringify({
@@ -1194,43 +1244,108 @@ function tplClaudeSettings() {
1194
1244
  }, null, 2) + '\n';
1195
1245
  }
1196
1246
 
1197
- function ensureClaudeStopHook(settingsPath) {
1198
- const cmd = claudeStopHookCmd();
1199
- let settings;
1200
- try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); }
1201
- catch { settings = {}; }
1202
-
1203
- if (!settings.hooks) settings.hooks = {};
1204
- if (!settings.hooks.Stop) settings.hooks.Stop = [];
1247
+ function ensureClaudeStopHook(settingsPath) {
1248
+ const cmd = claudeStopHookCmd();
1249
+ let settings;
1250
+ try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); }
1251
+ catch { settings = {}; }
1205
1252
 
1206
- const alreadyPresent = settings.hooks.Stop.some(entry =>
1207
- Array.isArray(entry.hooks) && entry.hooks.some(h => h.command === cmd)
1208
- );
1209
- if (alreadyPresent) return false; // no change needed
1253
+ if (!settings.hooks || typeof settings.hooks !== 'object' || Array.isArray(settings.hooks)) settings.hooks = {};
1254
+ if (!Array.isArray(settings.hooks.Stop)) settings.hooks.Stop = [];
1210
1255
 
1211
- settings.hooks.Stop.push({ matcher: '', hooks: [{ type: 'command', command: cmd }] });
1212
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
1213
- return true; // merged
1214
- }
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
+ }
1215
1294
 
1216
1295
  // ═══════════════════════════════════════════════════════════════════
1217
1296
  // MANAGED BLOCK UPDATE (CLAUDE.md / AGENTS.md)
1218
1297
  // ═══════════════════════════════════════════════════════════════════
1219
1298
 
1220
- 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) {
1221
1337
  if (!fs.existsSync(filePath)) {
1222
1338
  write(filePath, tplFn());
1223
1339
  return 'add';
1224
1340
  }
1225
1341
  const src = fs.readFileSync(filePath, 'utf8');
1226
- const s = src.indexOf(MGMT_S);
1227
- const e = src.indexOf(MGMT_E);
1228
- if (s === -1 || e === -1) {
1342
+ const range = findMarkedRange(src, MGMT_S, MGMT_E);
1343
+ if (!range) {
1229
1344
  // No managed block — inject at end, preserving all user content
1230
1345
  write(filePath, src.trimEnd() + '\n\n' + managedBlock() + '\n');
1231
1346
  return 'inject';
1232
1347
  }
1233
- 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));
1234
1349
  return 'update';
1235
1350
  }
1236
1351
 
@@ -1238,7 +1353,7 @@ function applyManagedBlock(filePath, tplFn) {
1238
1353
  // MAIN RUNNER
1239
1354
  // ═══════════════════════════════════════════════════════════════════
1240
1355
 
1241
- function run(dir, forceUpdate) {
1356
+ function run(dir, forceUpdate, action = 'update') {
1242
1357
  const p = scanProject(dir);
1243
1358
  const memDir = path.join(dir, '.memoc');
1244
1359
  const isNew = !fs.existsSync(path.join(memDir, 'boot.md'));
@@ -1282,7 +1397,7 @@ function run(dir, forceUpdate) {
1282
1397
  [path.join(memDir, '05-done-checklist.md'), tplDoneChecklist],
1283
1398
  [path.join(memDir, '06-project-rules.md'), tplProjectRules],
1284
1399
  [path.join(memDir, 'log.md'), tplLog],
1285
- [path.join(memDir, 'memoc-usage.md'), tplContextForgeUsage],
1400
+ [path.join(memDir, 'memoc-usage.md'), tplMemocUsage],
1286
1401
  [path.join(memDir, 'systems/README.md'), tplSystemsReadme],
1287
1402
  [path.join(memDir, 'wiki/index.md'), tplWikiIndex],
1288
1403
  [path.join(memDir, 'wiki/sources.md'), tplWikiSources],
@@ -1300,36 +1415,18 @@ function run(dir, forceUpdate) {
1300
1415
  }
1301
1416
 
1302
1417
  // Claude Code Stop hook — writes .memoc/.pending when git detects changes
1303
- const claudeDir = path.join(dir, '.claude');
1304
- const claudeSettings = path.join(claudeDir, 'settings.json');
1305
- fs.mkdirSync(claudeDir, { recursive: true });
1306
- if (!fs.existsSync(claudeSettings)) {
1307
- write(claudeSettings, tplClaudeSettings());
1308
- mark('add', '.claude/settings.json');
1309
- } else {
1310
- const merged = ensureClaudeStopHook(claudeSettings);
1311
- mark(merged ? 'update' : 'skip', `.claude/settings.json (Stop hook ${merged ? 'merged' : 'already present'})`);
1312
- }
1418
+ ensureClaudeStopHookFile(dir, mark);
1313
1419
 
1314
1420
  // .gitignore — add .memoc/.pending if not already present
1315
- const gitignorePath = path.join(dir, '.gitignore');
1316
- const PENDING_ENTRY = '.memoc/.pending';
1317
- const gitignoreContent = fs.existsSync(gitignorePath)
1318
- ? fs.readFileSync(gitignorePath, 'utf8') : '';
1319
- if (!gitignoreContent.includes(PENDING_ENTRY)) {
1320
- fs.appendFileSync(gitignorePath, (gitignoreContent.endsWith('\n') ? '' : '\n') + PENDING_ENTRY + '\n', 'utf8');
1321
- mark('update', '.gitignore (.memoc/.pending added)');
1322
- } else {
1323
- mark('skip', '.gitignore (.memoc/.pending already present)');
1324
- }
1325
-
1326
- // 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
1327
1424
  ensurePathHelpers(dir, mark);
1328
- ensurePathRegistration(dir, mark);
1329
-
1330
- } else {
1425
+ ensurePathRegistration(dir, mark);
1426
+
1427
+ } else {
1331
1428
  // ── UPDATE MODE
1332
- console.log(`\n memoc update — ${path.basename(dir)}`);
1429
+ console.log(`\n memoc ${action} — ${path.basename(dir)}`);
1333
1430
  console.log(` Re-scanning project: ${p.isEmpty ? 'nothing detected' : stackStr(p.stack)}`);
1334
1431
  console.log();
1335
1432
 
@@ -1398,7 +1495,7 @@ function run(dir, forceUpdate) {
1398
1495
  [path.join(memDir, '05-done-checklist.md'), tplDoneChecklist],
1399
1496
  [path.join(memDir, '06-project-rules.md'), tplProjectRules],
1400
1497
  [path.join(memDir, 'log.md'), tplLog],
1401
- [path.join(memDir, 'memoc-usage.md'), tplContextForgeUsage],
1498
+ [path.join(memDir, 'memoc-usage.md'), tplMemocUsage],
1402
1499
  [path.join(memDir, 'systems/README.md'), tplSystemsReadme],
1403
1500
  [path.join(memDir, 'wiki/index.md'), tplWikiIndex],
1404
1501
  [path.join(memDir, 'wiki/sources.md'), tplWikiSources],
@@ -1410,13 +1507,15 @@ function run(dir, forceUpdate) {
1410
1507
  [path.join(memDir, 'wiki/global/README.md'), tplWikiGlobalReadme],
1411
1508
  [path.join(dir, 'skills/project-memory-maintainer/SKILL.md'), tplSkillMaintainer],
1412
1509
  ];
1413
- for (const [fp, tpl] of addIfMissing) {
1414
- const rel = path.relative(dir, fp);
1415
- if (ensure(fp, tpl())) mark('add', rel);
1416
- // silently skip existing — user/agent owns them
1417
- }
1418
-
1419
- // 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);
1420
1519
  ensurePathHelpers(dir, mark);
1421
1520
  ensurePathRegistration(dir, mark);
1422
1521
 
@@ -1424,17 +1523,18 @@ function run(dir, forceUpdate) {
1424
1523
  const logPath = path.join(memDir, 'log.md');
1425
1524
  if (fs.existsSync(logPath)) {
1426
1525
  fs.appendFileSync(logPath,
1427
- `\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`,
1428
1527
  'utf8'
1429
1528
  );
1430
1529
  mark('append', '.memoc/log.md');
1431
1530
  }
1432
1531
  }
1433
1532
 
1434
- hideOnWindows(memDir);
1435
- console.log(log.join('\n'));
1436
- console.log('\n Done.');
1437
- }
1533
+ hideOnWindows(memDir);
1534
+ console.log(log.join('\n'));
1535
+ printCommandHint();
1536
+ console.log('\n Done.');
1537
+ }
1438
1538
 
1439
1539
  // ═══════════════════════════════════════════════════════════════════
1440
1540
  // ADD — add entry file for a specific agent
@@ -1470,7 +1570,7 @@ function runAdd(dir) {
1470
1570
  // SEARCH
1471
1571
  // ═══════════════════════════════════════════════════════════════════
1472
1572
 
1473
- function runSearch(dir) {
1573
+ function runSearch(dir, scope = 'memory') {
1474
1574
  const rawArgs = process.argv.slice(3);
1475
1575
  const opts = { mode: 'files', limit: 12, all: false };
1476
1576
  const queryParts = [];
@@ -1495,17 +1595,10 @@ function runSearch(dir) {
1495
1595
 
1496
1596
  const query = queryParts.join(' ').toLowerCase();
1497
1597
 
1498
- const searchRoots = [
1499
- path.join(dir, '.memoc'),
1500
- path.join(dir, 'skills'),
1501
- path.join(dir, 'llms.txt'),
1502
- path.join(dir, 'AGENTS.md'),
1503
- path.join(dir, 'CLAUDE.md'),
1504
- ...Object.values(AGENT_REGISTRY).map(agent => path.join(dir, agent.file)),
1505
- ];
1598
+ const searchRoots = scope === 'project' ? [dir] : memorySearchRoots(dir);
1506
1599
 
1507
1600
  if (!query) {
1508
- // No query — list all memory files sorted by recency
1601
+ // No query — list searchable files sorted by recency
1509
1602
  const allFiles = [];
1510
1603
  function collectFile(fp) {
1511
1604
  if (!fs.existsSync(fp)) return;
@@ -1519,8 +1612,10 @@ function runSearch(dir) {
1519
1612
  for (const entry of fs.readdirSync(d)) {
1520
1613
  const fp = path.join(d, entry);
1521
1614
  try {
1522
- if (fs.statSync(fp).isDirectory()) collectDir(fp);
1523
- else if (entry.endsWith('.md') || entry === 'llms.txt' || entry.endsWith('rules')) 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);
1524
1619
  } catch {}
1525
1620
  }
1526
1621
  }
@@ -1545,7 +1640,11 @@ function runSearch(dir) {
1545
1640
  if (!fs.existsSync(fp)) return;
1546
1641
  const rel = path.relative(dir, fp);
1547
1642
  let mtime = 0;
1548
- try { mtime = fs.statSync(fp).mtimeMs; } catch {}
1643
+ try {
1644
+ const st = fs.statSync(fp);
1645
+ if (!isSearchableFile(fp, path.basename(fp), st, scope)) return;
1646
+ mtime = st.mtimeMs;
1647
+ } catch {}
1549
1648
  const lines = fs.readFileSync(fp, 'utf8').split('\n');
1550
1649
  lines.forEach((line, i) => {
1551
1650
  if (line.toLowerCase().includes(query)) {
@@ -1560,8 +1659,10 @@ function runSearch(dir) {
1560
1659
  for (const entry of fs.readdirSync(d)) {
1561
1660
  const fp = path.join(d, entry);
1562
1661
  try {
1563
- if (fs.statSync(fp).isDirectory()) walkDir(fp);
1564
- else if (entry.endsWith('.md') || entry === 'llms.txt' || entry.endsWith('rules')) searchFile(fp);
1662
+ const st = fs.statSync(fp);
1663
+ if (st.isDirectory()) {
1664
+ if (!shouldSkipSearchDir(entry, scope)) walkDir(fp);
1665
+ } else if (isSearchableFile(fp, entry, st, scope)) searchFile(fp);
1565
1666
  } catch {}
1566
1667
  }
1567
1668
  }
@@ -1576,27 +1677,117 @@ function runSearch(dir) {
1576
1677
  if (!matchesByFile.size) {
1577
1678
  console.log('No matches found.');
1578
1679
  } else if (opts.mode === 'files') {
1579
- const rows = [...matchesByFile.entries()]
1580
- .map(([file, { matches, mtime }]) => ({ file, count: matches.length, mtime }))
1581
- .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
+ );
1582
1688
  const limited = opts.all ? rows : rows.slice(0, opts.limit);
1583
1689
  console.log(limited.map(r => `${r.file} ${r.count} match${r.count === 1 ? '' : 'es'}`).join('\n'));
1584
1690
  if (!opts.all && rows.length > limited.length) {
1585
1691
  console.log(`... ${rows.length - limited.length} more files. Use --all to show all, or --snippets for line matches.`);
1586
1692
  }
1587
1693
  } else {
1588
- const snippets = [];
1589
- for (const [file, { matches }] of matchesByFile.entries()) {
1590
- for (const m of matches) snippets.push(`${file}:${m.line} ${m.text}`);
1591
- }
1592
- const limited = opts.all ? snippets : snippets.slice(0, opts.limit);
1593
- 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'));
1594
1705
  if (!opts.all && snippets.length > limited.length) {
1595
1706
  console.log(`... ${snippets.length - limited.length} more matches. Use --all to show all, or --limit N.`);
1596
1707
  }
1597
1708
  }
1598
1709
  }
1599
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([
1724
+ '.git', 'node_modules', '.next', 'dist', 'build', 'out', 'coverage',
1725
+ 'Saved', 'Intermediate', 'DerivedDataCache', 'Binaries',
1726
+ '.venv', 'venv', '__pycache__', '.pytest_cache',
1727
+ ]);
1728
+ if (scope === 'project') {
1729
+ skipped.add('.memoc');
1730
+ skipped.add('skills');
1731
+ skipped.add('.claude');
1732
+ }
1733
+ return skipped.has(name);
1734
+ }
1735
+
1736
+ function isSearchableFile(fp, name, st, scope = 'memory') {
1737
+ if (!st || !st.isFile()) return false;
1738
+ if (st.size > 1024 * 1024) return false;
1739
+ if (scope === 'project' && isAgentMemoryFile(name)) return false;
1740
+ if (name === 'llms.txt' || name.endsWith('rules')) return true;
1741
+ const ext = path.extname(fp).toLowerCase();
1742
+ if (scope === 'memory') {
1743
+ return new Set(['.md', '.txt']).has(ext);
1744
+ }
1745
+ return new Set([
1746
+ '.md', '.txt', '.json', '.jsonc', '.yaml', '.yml', '.toml', '.ini', '.env',
1747
+ '.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs',
1748
+ '.py', '.rs', '.go', '.java', '.cs', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.hxx',
1749
+ '.html', '.css', '.scss', '.sass', '.vue', '.svelte',
1750
+ '.sql', '.graphql', '.gql', '.sh', '.bash', '.zsh', '.ps1', '.bat', '.cmd',
1751
+ '.xml', '.gradle', '.kts', '.cmake',
1752
+ ]).has(ext);
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
+ }
1790
+
1600
1791
  // ═══════════════════════════════════════════════════════════════════
1601
1792
  // TOKENS — estimate token cost of current memory state
1602
1793
  // ═══════════════════════════════════════════════════════════════════
@@ -1766,14 +1957,16 @@ if (cmd === '--version' || cmd === '-v') {
1766
1957
 
1767
1958
  if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') {
1768
1959
  console.log('Usage: memoc <command>\n');
1769
- console.log('Commands:');
1770
- console.log(' init Scaffold agent memory (auto-detects project, updates if already exists)');
1771
- console.log(' update Force-update managed sections based on current project state');
1772
- 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');
1773
1965
  console.log(' tokens Estimate token cost of current memory files');
1774
1966
  console.log(' compress Archive old log.md entries to keep file small');
1775
1967
  console.log(' add <agent> Add entry file for a specific agent (run without args to list)');
1776
- console.log(' search "<query>" Find matching 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)');
1777
1970
  console.log('\nSearch flags:');
1778
1971
  console.log(' --files Show file names and match counts, sorted by relevance + recency (default)');
1779
1972
  console.log(' --snippets Show matching lines');
@@ -1784,13 +1977,15 @@ if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') {
1784
1977
  process.exit(0);
1785
1978
  }
1786
1979
 
1787
- if (cmd === 'init') { run(cwd, false); process.exit(0); }
1788
- 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); }
1789
1983
  if (cmd === 'summary') { runSummary(cwd); process.exit(0); }
1790
1984
  if (cmd === 'tokens') { runTokens(cwd); process.exit(0); }
1791
1985
  if (cmd === 'compress') { runCompress(cwd); process.exit(0); }
1792
1986
  if (cmd === 'add') { runAdd(cwd); process.exit(0); }
1793
- if (cmd === 'search') { runSearch(cwd); process.exit(0); }
1987
+ if (cmd === 'search') { runSearch(cwd, 'memory'); process.exit(0); }
1988
+ if (cmd === 'grep') { runSearch(cwd, 'project'); process.exit(0); }
1794
1989
 
1795
1990
  console.error(`Unknown command: ${cmd}`);
1796
1991
  console.error('Run "memoc --help" for usage.');