@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.
- package/README.md +90 -13
- package/bin/cli.js +652 -457
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -188,338 +188,354 @@ function scriptsMd(scripts) {
|
|
|
188
188
|
|
|
189
189
|
function hideOnWindows(dirPath) {
|
|
190
190
|
if (process.platform === 'win32') {
|
|
191
|
-
try { require('child_process').
|
|
191
|
+
try { require('child_process').execFileSync('attrib', ['+h', dirPath], { stdio: 'ignore' }); } catch {}
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
|
-
|
|
195
|
-
function chmodExecutable(filePath) {
|
|
196
|
-
try { fs.chmodSync(filePath, 0o755); } catch {}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function ensure(filePath, content) {
|
|
200
|
-
if (fs.existsSync(filePath)) return false;
|
|
201
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
202
|
-
fs.writeFileSync(filePath, content, 'utf8');
|
|
194
|
+
|
|
195
|
+
function chmodExecutable(filePath) {
|
|
196
|
+
try { fs.chmodSync(filePath, 0o755); } catch {}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function ensure(filePath, content) {
|
|
200
|
+
if (fs.existsSync(filePath)) return false;
|
|
201
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
202
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
203
203
|
return true;
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
function write(filePath, content) {
|
|
207
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
208
|
-
fs.writeFileSync(filePath, content, 'utf8');
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function tplMemocCmdWrapper(cliPath = runtimeCliPath()) {
|
|
212
|
-
return `@echo off\r\nnode "${escapeCmdPath(cliPath)}" %*\r\n`;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function tplMemocPs1Wrapper(cliPath = runtimeCliPath()) {
|
|
216
|
-
return `& node ${psSingleQuote(cliPath)} @args\nexit $LASTEXITCODE\n`;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function tplMemocShWrapper(cliPath = runtimeCliPath()) {
|
|
220
|
-
return `#!/bin/sh\nexec node ${shellSingleQuote(cliPath)} "$@"\n`;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function defaultUserBinDir() {
|
|
224
|
-
if (process.env.MEMOC_USER_BIN_DIR) return process.env.MEMOC_USER_BIN_DIR;
|
|
225
|
-
if (currentPlatform() === 'win32') {
|
|
226
|
-
return path.join(process.env.LOCALAPPDATA || path.join(process.env.USERPROFILE || process.cwd(), 'AppData', 'Local'), 'memoc', 'bin');
|
|
227
|
-
}
|
|
228
|
-
return path.join(process.env.HOME || process.cwd(), '.local', 'bin');
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function defaultRuntimeDir() {
|
|
232
|
-
if (process.env.MEMOC_RUNTIME_DIR) return process.env.MEMOC_RUNTIME_DIR;
|
|
233
|
-
if (currentPlatform() === 'win32') {
|
|
234
|
-
return path.join(process.env.LOCALAPPDATA || path.join(process.env.USERPROFILE || process.cwd(), 'AppData', 'Local'), 'memoc', 'runtime');
|
|
235
|
-
}
|
|
236
|
-
return path.join(process.env.HOME || process.cwd(), '.local', 'share', 'memoc', 'runtime');
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function runtimeCliPath() {
|
|
240
|
-
return path.join(defaultRuntimeDir(), 'bin', 'cli.js');
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function tplEnvPs1() {
|
|
244
|
-
return `$memocBin = Join-Path $PSScriptRoot 'bin'\n$parts = $env:PATH -split [IO.Path]::PathSeparator\nif ($parts -notcontains $memocBin) {\n $env:PATH = \"$memocBin$([IO.Path]::PathSeparator)$env:PATH\"\n}\n`;
|
|
245
|
-
}
|
|
246
|
-
|
|
206
|
+
function write(filePath, content) {
|
|
207
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
208
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function tplMemocCmdWrapper(cliPath = runtimeCliPath()) {
|
|
212
|
+
return `@echo off\r\nnode "${escapeCmdPath(cliPath)}" %*\r\n`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function tplMemocPs1Wrapper(cliPath = runtimeCliPath()) {
|
|
216
|
+
return `& node ${psSingleQuote(cliPath)} @args\nexit $LASTEXITCODE\n`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function tplMemocShWrapper(cliPath = runtimeCliPath()) {
|
|
220
|
+
return `#!/bin/sh\nexec node ${shellSingleQuote(cliPath)} "$@"\n`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function defaultUserBinDir() {
|
|
224
|
+
if (process.env.MEMOC_USER_BIN_DIR) return process.env.MEMOC_USER_BIN_DIR;
|
|
225
|
+
if (currentPlatform() === 'win32') {
|
|
226
|
+
return path.join(process.env.LOCALAPPDATA || path.join(process.env.USERPROFILE || process.cwd(), 'AppData', 'Local'), 'memoc', 'bin');
|
|
227
|
+
}
|
|
228
|
+
return path.join(process.env.HOME || process.cwd(), '.local', 'bin');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function defaultRuntimeDir() {
|
|
232
|
+
if (process.env.MEMOC_RUNTIME_DIR) return process.env.MEMOC_RUNTIME_DIR;
|
|
233
|
+
if (currentPlatform() === 'win32') {
|
|
234
|
+
return path.join(process.env.LOCALAPPDATA || path.join(process.env.USERPROFILE || process.cwd(), 'AppData', 'Local'), 'memoc', 'runtime');
|
|
235
|
+
}
|
|
236
|
+
return path.join(process.env.HOME || process.cwd(), '.local', 'share', 'memoc', 'runtime');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function runtimeCliPath() {
|
|
240
|
+
return path.join(defaultRuntimeDir(), 'bin', 'cli.js');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function tplEnvPs1() {
|
|
244
|
+
return `$memocBin = Join-Path $PSScriptRoot 'bin'\n$parts = $env:PATH -split [IO.Path]::PathSeparator\nif ($parts -notcontains $memocBin) {\n $env:PATH = \"$memocBin$([IO.Path]::PathSeparator)$env:PATH\"\n}\n`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
247
|
function tplEnvSh() {
|
|
248
|
-
return `# Source this from the project root to put the local memoc wrapper first in PATH.\nMEMOC_DIR="$
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function ensurePathHelpers(dir, mark) {
|
|
252
|
-
const cliPath = ensureRuntimeInstall(mark);
|
|
253
|
-
const files = [
|
|
254
|
-
[path.join(dir, '.memoc', 'bin', 'memoc.cmd'), () => tplMemocCmdWrapper(cliPath), false],
|
|
255
|
-
[path.join(dir, '.memoc', 'bin', 'memoc.ps1'), () => tplMemocPs1Wrapper(cliPath), false],
|
|
256
|
-
[path.join(dir, '.memoc', 'bin', 'memoc'), () => tplMemocShWrapper(cliPath), true],
|
|
257
|
-
[path.join(dir, '.memoc', 'env.ps1'), tplEnvPs1, false],
|
|
258
|
-
[path.join(dir, '.memoc', 'env.sh'), tplEnvSh, true],
|
|
259
|
-
];
|
|
260
|
-
|
|
261
|
-
for (const [fp, tpl, executable] of files) {
|
|
262
|
-
const rel = path.relative(dir, fp);
|
|
263
|
-
const added = writeIfChanged(fp, tpl());
|
|
264
|
-
if (executable) chmodExecutable(fp);
|
|
265
|
-
mark(added, rel);
|
|
266
|
-
}
|
|
248
|
+
return `# Source this from the project root to put the local memoc wrapper first in PATH.\nMEMOC_DIR="$PWD/.memoc"\ncase ":$PATH:" in\n *":$MEMOC_DIR/bin:"*) ;;\n *) PATH="$MEMOC_DIR/bin:$PATH"; export PATH ;;\nesac\n`;
|
|
267
249
|
}
|
|
268
|
-
|
|
269
|
-
function
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
]
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
function
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
try {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
return
|
|
437
|
-
} catch {
|
|
438
|
-
return
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
}
|
|
498
|
-
|
|
250
|
+
|
|
251
|
+
function ensurePathHelpers(dir, mark) {
|
|
252
|
+
const cliPath = ensureRuntimeInstall(mark);
|
|
253
|
+
const files = [
|
|
254
|
+
[path.join(dir, '.memoc', 'bin', 'memoc.cmd'), () => tplMemocCmdWrapper(cliPath), false],
|
|
255
|
+
[path.join(dir, '.memoc', 'bin', 'memoc.ps1'), () => tplMemocPs1Wrapper(cliPath), false],
|
|
256
|
+
[path.join(dir, '.memoc', 'bin', 'memoc'), () => tplMemocShWrapper(cliPath), true],
|
|
257
|
+
[path.join(dir, '.memoc', 'env.ps1'), tplEnvPs1, false],
|
|
258
|
+
[path.join(dir, '.memoc', 'env.sh'), tplEnvSh, true],
|
|
259
|
+
];
|
|
260
|
+
|
|
261
|
+
for (const [fp, tpl, executable] of files) {
|
|
262
|
+
const rel = path.relative(dir, fp);
|
|
263
|
+
const added = writeIfChanged(fp, tpl());
|
|
264
|
+
if (executable) chmodExecutable(fp);
|
|
265
|
+
mark(added, rel);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function ensureUserLauncher(mark) {
|
|
270
|
+
const userBin = defaultUserBinDir();
|
|
271
|
+
writeLaunchers(userBin, mark, 'user bin', ensureRuntimeInstall(mark));
|
|
272
|
+
return userBin;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function writeLaunchers(binDir, mark, label, cliPath = ensureRuntimeInstall(mark)) {
|
|
276
|
+
const files = [
|
|
277
|
+
[path.join(binDir, 'memoc.cmd'), () => tplMemocCmdWrapper(cliPath), false],
|
|
278
|
+
[path.join(binDir, 'memoc.ps1'), () => tplMemocPs1Wrapper(cliPath), false],
|
|
279
|
+
[path.join(binDir, 'memoc'), () => tplMemocShWrapper(cliPath), true],
|
|
280
|
+
];
|
|
281
|
+
|
|
282
|
+
for (const [fp, tpl, executable] of files) {
|
|
283
|
+
const added = writeIfChanged(fp, tpl());
|
|
284
|
+
if (executable) chmodExecutable(fp);
|
|
285
|
+
mark(added, `${label} ${path.basename(fp)}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function writeIfChanged(filePath, content) {
|
|
290
|
+
if (!fs.existsSync(filePath)) {
|
|
291
|
+
write(filePath, content);
|
|
292
|
+
return 'add';
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
if (fs.readFileSync(filePath, 'utf8') === content) return 'skip';
|
|
296
|
+
} catch {}
|
|
297
|
+
write(filePath, content);
|
|
298
|
+
return 'update';
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function ensurePathRegistration(dir, mark) {
|
|
302
|
+
ensureCurrentPathLauncher(mark);
|
|
303
|
+
const binDir = ensureUserLauncher(mark);
|
|
304
|
+
const pathSep = path.delimiter;
|
|
305
|
+
|
|
306
|
+
if ((process.env.PATH || '').split(pathSep).some(p => samePath(p, binDir))) {
|
|
307
|
+
mark('skip', 'PATH (user memoc bin already active)');
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
process.env.PATH = `${binDir}${pathSep}${process.env.PATH || ''}`;
|
|
312
|
+
|
|
313
|
+
if (process.env.MEMOC_SKIP_PATH_REGISTER === '1') {
|
|
314
|
+
mark('skip', 'PATH registration (test mode)');
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (currentPlatform() !== 'win32') {
|
|
319
|
+
const updated = ensureUnixPathRegistration(binDir);
|
|
320
|
+
mark(updated ? 'update' : 'skip', `${currentPlatform()} PATH (${userPathShellHint(binDir)})`);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
const current = require('child_process')
|
|
326
|
+
.execFileSync('powershell.exe', [
|
|
327
|
+
'-NoProfile',
|
|
328
|
+
'-ExecutionPolicy', 'Bypass',
|
|
329
|
+
'-Command',
|
|
330
|
+
"[Environment]::GetEnvironmentVariable('Path','User')",
|
|
331
|
+
], { encoding: 'utf8' })
|
|
332
|
+
.trim();
|
|
333
|
+
const parts = current.split(pathSep).filter(Boolean);
|
|
334
|
+
if (parts.some(p => samePath(p, binDir))) {
|
|
335
|
+
mark('skip', 'User PATH (memoc bin already registered)');
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const nextPath = [binDir, ...parts].join(pathSep);
|
|
339
|
+
require('child_process').execFileSync('powershell.exe', [
|
|
340
|
+
'-NoProfile',
|
|
341
|
+
'-ExecutionPolicy', 'Bypass',
|
|
342
|
+
'-Command',
|
|
343
|
+
`[Environment]::SetEnvironmentVariable('Path', ${JSON.stringify(nextPath)}, 'User')`,
|
|
344
|
+
], { stdio: 'ignore' });
|
|
345
|
+
mark('update', 'User PATH (memoc bin added; open a new terminal if needed)');
|
|
346
|
+
} catch {
|
|
347
|
+
mark('skip', 'User PATH registration failed (use . .\\.memoc\\env.ps1)');
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function ensureCurrentPathLauncher(mark) {
|
|
352
|
+
const target = findWritablePathDir();
|
|
353
|
+
if (!target) {
|
|
354
|
+
mark('skip', 'active PATH launcher (no writable PATH directory found)');
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
writeLaunchers(target, mark, 'active PATH', ensureRuntimeInstall(mark));
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function ensureRuntimeInstall(mark) {
|
|
362
|
+
const runtimeDir = defaultRuntimeDir();
|
|
363
|
+
const sourceRoot = path.join(__dirname, '..');
|
|
364
|
+
const files = [
|
|
365
|
+
[path.join(sourceRoot, 'bin', 'cli.js'), path.join(runtimeDir, 'bin', 'cli.js')],
|
|
366
|
+
[path.join(sourceRoot, 'package.json'), path.join(runtimeDir, 'package.json')],
|
|
367
|
+
];
|
|
368
|
+
|
|
369
|
+
for (const [src, dest] of files) {
|
|
370
|
+
try {
|
|
371
|
+
const content = fs.readFileSync(src, 'utf8');
|
|
372
|
+
const changed = writeIfChanged(dest, content);
|
|
373
|
+
mark(changed, `runtime ${path.relative(runtimeDir, dest)}`);
|
|
374
|
+
} catch {
|
|
375
|
+
mark('skip', `runtime ${path.basename(dest)} unavailable`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
chmodExecutable(path.join(runtimeDir, 'bin', 'cli.js'));
|
|
380
|
+
return path.join(runtimeDir, 'bin', 'cli.js');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function findWritablePathDir() {
|
|
384
|
+
const dirs = [...new Set((process.env.PATH || '').split(path.delimiter).filter(Boolean))];
|
|
385
|
+
const npmBin = npmGlobalBinDir();
|
|
386
|
+
const ranked = dirs
|
|
387
|
+
.filter(d => !isVolatilePathDir(d))
|
|
388
|
+
.filter(d => {
|
|
389
|
+
try { return fs.existsSync(d) && fs.statSync(d).isDirectory() && canWriteDir(d); }
|
|
390
|
+
catch { return false; }
|
|
391
|
+
})
|
|
392
|
+
.sort((a, b) => pathRank(a, npmBin) - pathRank(b, npmBin));
|
|
393
|
+
return ranked[0] || null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function pathRank(dir, npmBin) {
|
|
397
|
+
if (npmBin && samePath(dir, npmBin)) return 0;
|
|
398
|
+
const lower = dir.toLowerCase();
|
|
399
|
+
for (const root of userWritableRoots()) {
|
|
400
|
+
if (root && lower.startsWith(root.toLowerCase())) return 1;
|
|
401
|
+
}
|
|
402
|
+
return 5;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function userWritableRoots() {
|
|
406
|
+
return [
|
|
407
|
+
process.env.APPDATA,
|
|
408
|
+
process.env.LOCALAPPDATA,
|
|
409
|
+
process.env.HOME,
|
|
410
|
+
process.env.USERPROFILE,
|
|
411
|
+
].filter(Boolean).map(p => path.resolve(p));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function npmGlobalBinDir() {
|
|
415
|
+
try {
|
|
416
|
+
const prefix = require('child_process').execFileSync('npm', ['config', 'get', 'prefix'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
417
|
+
if (!prefix) return null;
|
|
418
|
+
return currentPlatform() === 'win32' ? prefix : path.join(prefix, 'bin');
|
|
419
|
+
} catch {
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function isVolatilePathDir(dir) {
|
|
425
|
+
const lower = dir.toLowerCase();
|
|
426
|
+
return lower.includes(`${path.sep}_npx${path.sep}`) ||
|
|
427
|
+
lower.includes(`${path.sep}node_modules${path.sep}.bin`) ||
|
|
428
|
+
lower.includes(`${path.sep}npm-cache${path.sep}_npx${path.sep}`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function canWriteDir(dir) {
|
|
432
|
+
const probe = path.join(dir, `.memoc-write-test-${process.pid}-${Date.now()}`);
|
|
433
|
+
try {
|
|
434
|
+
fs.writeFileSync(probe, '');
|
|
435
|
+
fs.unlinkSync(probe);
|
|
436
|
+
return true;
|
|
437
|
+
} catch {
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function ensureUnixPathRegistration(binDir) {
|
|
443
|
+
if (process.env.MEMOC_SKIP_PATH_REGISTER === '1') return false;
|
|
444
|
+
|
|
445
|
+
const home = process.env.HOME;
|
|
446
|
+
if (!home) return false;
|
|
447
|
+
|
|
448
|
+
const block = [
|
|
449
|
+
'# memoc PATH',
|
|
450
|
+
`MEMOC_BIN=${shellSingleQuote(binDir)}`,
|
|
451
|
+
'case ":$PATH:" in *":$MEMOC_BIN:"*) ;; *) PATH="$MEMOC_BIN:$PATH"; export PATH ;; esac',
|
|
452
|
+
'# end memoc PATH',
|
|
453
|
+
].join('\n');
|
|
454
|
+
|
|
455
|
+
const candidates = [
|
|
456
|
+
path.join(home, '.profile'),
|
|
457
|
+
path.join(home, '.zshrc'),
|
|
458
|
+
path.join(home, '.bashrc'),
|
|
459
|
+
];
|
|
460
|
+
|
|
461
|
+
let changed = false;
|
|
462
|
+
for (const fp of candidates) {
|
|
463
|
+
try {
|
|
464
|
+
const src = fs.existsSync(fp) ? fs.readFileSync(fp, 'utf8') : '';
|
|
465
|
+
if (src.includes(binDir) || src.includes('# memoc PATH')) continue;
|
|
466
|
+
fs.appendFileSync(fp, `${src.endsWith('\n') || !src ? '' : '\n'}\n${block}\n`, 'utf8');
|
|
467
|
+
changed = true;
|
|
468
|
+
} catch {}
|
|
469
|
+
}
|
|
470
|
+
return changed;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function userPathShellHint(binDir) {
|
|
474
|
+
return `user bin ${binDir} ${process.env.MEMOC_SKIP_PATH_REGISTER === '1' ? 'test mode' : 'registered; open a new terminal if needed'}`;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function currentPlatform() {
|
|
478
|
+
return process.env.MEMOC_PLATFORM || process.platform;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function shellSingleQuote(value) {
|
|
482
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function psSingleQuote(value) {
|
|
486
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function escapeCmdPath(value) {
|
|
490
|
+
return String(value).replace(/"/g, '""');
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function samePath(a, b) {
|
|
494
|
+
if (!a || !b) return false;
|
|
495
|
+
const norm = p => path.resolve(p).toLowerCase().replace(/[\\/]+$/, '');
|
|
496
|
+
try { return norm(a) === norm(b); } catch { return false; }
|
|
497
|
+
}
|
|
498
|
+
|
|
499
499
|
function updateSection(filePath, startMark, endMark, inner) {
|
|
500
500
|
if (!fs.existsSync(filePath)) return false;
|
|
501
501
|
const src = fs.readFileSync(filePath, 'utf8');
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
}
|
|
502
|
+
const range = findMarkedRange(src, startMark, endMark);
|
|
503
|
+
if (!range) return false;
|
|
504
|
+
write(filePath,
|
|
505
|
+
src.slice(0, range.s) + startMark + '\n' + inner + '\n' + endMark + src.slice(range.e + range.endMark.length)
|
|
506
|
+
);
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
510
509
|
|
|
511
510
|
// ═══════════════════════════════════════════════════════════════════
|
|
512
511
|
// SECTION MARKERS
|
|
513
512
|
// ═══════════════════════════════════════════════════════════════════
|
|
514
513
|
|
|
515
|
-
const mk = n => [`<!--
|
|
516
|
-
const [MGMT_S, MGMT_E] = mk('managed');
|
|
517
|
-
const [ID_S, ID_E] = mk('identity');
|
|
518
|
-
const [SNAP_S, SNAP_E] = mk('snapshot');
|
|
519
|
-
const [CORE_S, CORE_E] = mk('core');
|
|
520
|
-
const [HDR_S, HDR_E] = mk('header');
|
|
521
|
-
const [SYS_S, SYS_E] = mk('systems');
|
|
522
|
-
const [WIKI_S, WIKI_E] = mk('wiki');
|
|
514
|
+
const mk = n => [`<!-- memoc:${n}:start -->`, `<!-- memoc:${n}:end -->`];
|
|
515
|
+
const [MGMT_S, MGMT_E] = mk('managed');
|
|
516
|
+
const [ID_S, ID_E] = mk('identity');
|
|
517
|
+
const [SNAP_S, SNAP_E] = mk('snapshot');
|
|
518
|
+
const [CORE_S, CORE_E] = mk('core');
|
|
519
|
+
const [HDR_S, HDR_E] = mk('header');
|
|
520
|
+
const [SYS_S, SYS_E] = mk('systems');
|
|
521
|
+
const [WIKI_S, WIKI_E] = mk('wiki');
|
|
522
|
+
|
|
523
|
+
function markerPairs(startMark, endMark) {
|
|
524
|
+
const legacyStart = startMark.replace('<!-- memoc:', '<!-- context-forge:');
|
|
525
|
+
const legacyEnd = endMark.replace('<!-- memoc:', '<!-- context-forge:');
|
|
526
|
+
return legacyStart === startMark
|
|
527
|
+
? [[startMark, endMark]]
|
|
528
|
+
: [[startMark, endMark], [legacyStart, legacyEnd]];
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function findMarkedRange(src, startMark, endMark) {
|
|
532
|
+
for (const [sMark, eMark] of markerPairs(startMark, endMark)) {
|
|
533
|
+
const s = src.indexOf(sMark);
|
|
534
|
+
const e = src.indexOf(eMark);
|
|
535
|
+
if (s !== -1 && e !== -1 && e > s) return { s, e, endMark: eMark };
|
|
536
|
+
}
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
523
539
|
|
|
524
540
|
// ═══════════════════════════════════════════════════════════════════
|
|
525
541
|
// AGENT REGISTRY — third-party agent entry files (added via `add`)
|
|
@@ -536,17 +552,18 @@ const AGENT_REGISTRY = {
|
|
|
536
552
|
// DYNAMIC CONTENT (re-generated on update)
|
|
537
553
|
// ═══════════════════════════════════════════════════════════════════
|
|
538
554
|
|
|
539
|
-
function
|
|
555
|
+
function legacyManagedBlock() {
|
|
540
556
|
return `${MGMT_S}
|
|
541
|
-
## Session Start
|
|
542
|
-
- [ ] Read \`.memoc/session-summary.md\`
|
|
543
|
-
- [ ] \`.pending\` exists? → review changed files → update memory if needed → delete it
|
|
544
|
-
- [ ] If \`memoc\` is not found in an existing shell, open a new terminal or load the local helper: PowerShell \`. .\\.memoc\\env.ps1\`; sh \`. ./.memoc/env.sh\`
|
|
545
|
-
|
|
546
|
-
## Before Opening More Files
|
|
547
|
-
- [ ] Run memoc commands in this order: \`memoc search "<query>"\` → \`.\\.memoc\\bin\\memoc.cmd search "<query>"\` (Windows) or \`.memoc/bin/memoc search "<query>"\` (sh) → \`npx @kevin0181/memoc search "<query>"\`
|
|
548
|
-
- [ ] Open on demand: \`02\` status · \`04\` resume · \`06\` rules · \`llms.txt\` map
|
|
549
|
-
- [ ]
|
|
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
|
|
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
|
|
863
|
-
|
|
864
|
-
|
|
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
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
|
1054
|
-
4.
|
|
1055
|
-
5.
|
|
1100
|
+
3. Search memory first with one or two concrete terms: \`memoc search "<query>" --limit 5\`.
|
|
1101
|
+
4. Open only the matching memory file(s) that matter.
|
|
1102
|
+
5. If memory is not enough, search project files: \`memoc grep "<query>" --limit 5\`.
|
|
1103
|
+
6. Use \`--snippets\` only when file names are not enough.
|
|
1104
|
+
|
|
1105
|
+
Use \`memoc search\` for known concepts, changed areas, decisions, tasks, or handoff notes. Skip it for brand-new questions where no prior memory can exist.
|
|
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>"\`;
|
|
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'),{
|
|
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
|
-
|
|
1207
|
-
|
|
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
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
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
|
|
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
|
|
1227
|
-
|
|
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 +
|
|
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'),
|
|
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
|
-
|
|
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
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
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
|
|
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'),
|
|
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()}]
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1523
|
-
|
|
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 {
|
|
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
|
-
|
|
1564
|
-
|
|
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) =>
|
|
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(
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1593
|
-
|
|
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('
|
|
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>"
|
|
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);
|
|
1788
|
-
if (cmd === 'update') { run(cwd, true);
|
|
1980
|
+
if (cmd === 'init') { run(cwd, false); process.exit(0); }
|
|
1981
|
+
if (cmd === 'update') { run(cwd, true, 'update'); process.exit(0); }
|
|
1982
|
+
if (cmd === 'upgrade') { run(cwd, true, 'upgrade'); process.exit(0); }
|
|
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);
|
|
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.');
|