@kevin0181/memoc 1.2.0 → 1.3.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 +3 -3
  2. package/bin/cli.js +90 -73
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -113,7 +113,7 @@ npx @kevin0181/memoc tokens
113
113
  # Archive and compact an oversized startup summary
114
114
  npx @kevin0181/memoc trim-summary
115
115
 
116
- # Legacy: archive old log.md entries before deleting/migrating log.md
116
+ # Compact oversized memoc files and refresh generated indexes
117
117
  npx @kevin0181/memoc compress
118
118
 
119
119
  # Add the same protocol to another agent's entry file
@@ -228,7 +228,7 @@ Startup cost is kept minimal by design.
228
228
 
229
229
  Everything else is on-demand. Use `memoc tokens` to see the live breakdown for your project.
230
230
 
231
- `session-summary.md` is a replace-only startup snapshot, not a timeline. If it grows beyond the warning threshold, run `memoc trim-summary`; completed history belongs in `.memoc/worklog/<actor>/YYYY-MM/`, and unfinished/risky resume detail belongs in `.memoc/04-handoff.md`.
231
+ `session-summary.md` is a replace-only startup snapshot, not a timeline. If it grows beyond the warning threshold, run `memoc compress` or `memoc trim-summary`; completed history belongs in `.memoc/worklog/<actor>/YYYY-MM/`, and unfinished/risky resume detail belongs in `.memoc/04-handoff.md`.
232
232
 
233
233
  ---
234
234
 
@@ -294,7 +294,7 @@ Node.js · Next.js · React · Vue · Svelte · Angular · Nuxt · Astro · Expr
294
294
  - **New project** — scaffolds all memory files with sensible defaults.
295
295
  - **Existing project** — detects your stack and fills in real project info (name, scripts, config files).
296
296
  - **Already initialized** — `init` injects the managed block without touching your existing content. `update` re-scans and refreshes project-specific sections.
297
- - **Long-running projects** — use actor worklogs for history; `compress` remains only for old `log.md` files.
297
+ - **Long-running projects** — use actor worklogs for history; run `compress` to trim startup memory, archive legacy logs, and refresh generated activity indexes.
298
298
 
299
299
  ---
300
300
 
package/bin/cli.js CHANGED
@@ -981,7 +981,9 @@ function collectMemocMarkdownFiles(dir) {
981
981
  }
982
982
  walk(path.join(dir, '.memoc'));
983
983
  walk(path.join(dir, 'skills', 'project-memory-maintainer'));
984
- return files.sort();
984
+ return files
985
+ .filter(fp => !/session-summary-archive(?:-\d+)?\.md$/.test(path.basename(fp)))
986
+ .sort();
985
987
  }
986
988
 
987
989
  function ensureMemocFrontmatter(filePath, dir) {
@@ -3292,51 +3294,29 @@ function searchPriority(file, scope = 'memory') {
3292
3294
  // ═══════════════════════════════════════════════════════════════════
3293
3295
 
3294
3296
  function runTokens(dir) {
3295
- const est = text => Math.ceil(Buffer.byteLength(text, 'utf8') / 4);
3296
- const read = fp => { try { return fs.readFileSync(fp, 'utf8'); } catch { return ''; } };
3297
- const memDir = path.join(dir, '.memoc');
3298
-
3299
- const startup = [
3300
- ['CLAUDE.md', path.join(dir, 'CLAUDE.md')],
3301
- ['session-summary.md', path.join(memDir, 'session-summary.md')],
3302
- ];
3303
- const onDemand = [
3304
- ['llms.txt', path.join(dir, 'llms.txt')],
3305
- ['02-current-project-state.md', path.join(memDir, '02-current-project-state.md')],
3306
- ['03-decisions.md', path.join(memDir, '03-decisions.md')],
3307
- ['04-handoff.md', path.join(memDir, '04-handoff.md')],
3308
- ['06-project-rules.md', path.join(memDir, '06-project-rules.md')],
3309
- ['activity.md', path.join(memDir, 'activity.md')],
3310
- ];
3297
+ const stats = memoryTokenStats(dir);
3311
3298
 
3312
3299
  console.log('\n memoc tokens\n');
3313
3300
  let startupTotal = 0;
3314
3301
  console.log(' Startup (always loaded):');
3315
- for (const [name, fp] of startup) {
3316
- const content = read(fp);
3317
- const t = est(content);
3318
- const b = Buffer.byteLength(content, 'utf8');
3319
- startupTotal += t;
3320
- const warn = b > 1000 ? ' ⚠ large' : '';
3321
- console.log(` ${name.padEnd(32)} ${String(t).padStart(5)} tokens (${b}B)${warn}`);
3302
+ for (const item of stats.startup) {
3303
+ startupTotal += item.tokens;
3304
+ const warn = item.bytes > 1000 ? ' ⚠ large' : '';
3305
+ console.log(` ${item.name.padEnd(32)} ${String(item.tokens).padStart(5)} tokens (${item.bytes}B)${warn}`);
3322
3306
  }
3323
3307
  console.log(` ${'── startup total'.padEnd(32)} ${String(startupTotal).padStart(5)} tokens`);
3324
3308
 
3325
3309
  console.log('\n On-demand (read when needed):');
3326
3310
  let onDemandTotal = 0;
3327
- for (const [name, fp] of onDemand) {
3328
- const content = read(fp);
3329
- if (!content) continue;
3330
- const t = est(content);
3331
- const b = Buffer.byteLength(content, 'utf8');
3332
- onDemandTotal += t;
3333
- const warn = t > 500 ? ' ⚠ consider compress' : '';
3334
- console.log(` ${name.padEnd(32)} ${String(t).padStart(5)} tokens (${b}B)${warn}`);
3311
+ for (const item of stats.onDemand) {
3312
+ onDemandTotal += item.tokens;
3313
+ const warn = item.tokens > 500 ? ' ⚠ consider compress' : '';
3314
+ console.log(` ${item.name.padEnd(32)} ${String(item.tokens).padStart(5)} tokens (${item.bytes}B)${warn}`);
3335
3315
  }
3336
3316
  console.log(` ${'── on-demand total'.padEnd(32)} ${String(onDemandTotal).padStart(5)} tokens`);
3337
3317
  console.log(`\n If all loaded: ~${startupTotal + onDemandTotal} tokens`);
3338
3318
 
3339
- const summaryContent = read(path.join(memDir, 'session-summary.md'));
3319
+ const summaryContent = safeRead(path.join(dir, '.memoc', 'session-summary.md'));
3340
3320
  const summaryBytes = Buffer.byteLength(summaryContent, 'utf8');
3341
3321
  if (summaryBytes > 800) {
3342
3322
  console.log(`\n ⚠ session-summary.md is ${summaryBytes}B — recommended <800B. Run \`memoc trim-summary\`, then move completed history to worklog and resume details to 04-handoff.md.`);
@@ -3344,6 +3324,37 @@ function runTokens(dir) {
3344
3324
  console.log();
3345
3325
  }
3346
3326
 
3327
+ function memoryTokenStats(dir) {
3328
+ const est = text => Math.ceil(Buffer.byteLength(text, 'utf8') / 4);
3329
+ const read = fp => { try { return fs.readFileSync(fp, 'utf8'); } catch { return ''; } };
3330
+ const memDir = path.join(dir, '.memoc');
3331
+ const startup = [
3332
+ ['CLAUDE.md', path.join(dir, 'CLAUDE.md'), true],
3333
+ ['session-summary.md', path.join(memDir, 'session-summary.md'), true],
3334
+ ];
3335
+ const onDemand = [
3336
+ ['llms.txt', path.join(dir, 'llms.txt')],
3337
+ ['02-current-project-state.md', path.join(memDir, '02-current-project-state.md')],
3338
+ ['03-decisions.md', path.join(memDir, '03-decisions.md')],
3339
+ ['04-handoff.md', path.join(memDir, '04-handoff.md')],
3340
+ ['06-project-rules.md', path.join(memDir, '06-project-rules.md')],
3341
+ ['activity.md', path.join(memDir, 'activity.md')],
3342
+ ];
3343
+ const existing = ([, fp, keep]) => keep || fs.existsSync(fp);
3344
+ const toItem = ([name, fp]) => {
3345
+ const content = read(fp);
3346
+ return { name, fp, bytes: Buffer.byteLength(content, 'utf8'), tokens: est(content) };
3347
+ };
3348
+ const startupItems = startup.filter(existing).map(toItem);
3349
+ const onDemandItems = onDemand.filter(existing).map(toItem);
3350
+ return {
3351
+ startup: startupItems,
3352
+ onDemand: onDemandItems,
3353
+ startupTokens: startupItems.reduce((sum, item) => sum + item.tokens, 0),
3354
+ allTokens: [...startupItems, ...onDemandItems].reduce((sum, item) => sum + item.tokens, 0),
3355
+ };
3356
+ }
3357
+
3347
3358
  // ═══════════════════════════════════════════════════════════════════
3348
3359
  // DOCTOR — quick health checks for shared memoc repos
3349
3360
  // ═══════════════════════════════════════════════════════════════════
@@ -3394,47 +3405,46 @@ function runDoctor(dir) {
3394
3405
  }
3395
3406
 
3396
3407
  // ═══════════════════════════════════════════════════════════════════
3397
- // COMPRESS — legacy log.md archiver
3408
+ // COMPRESS — safe whole-memory compaction
3398
3409
  // ═══════════════════════════════════════════════════════════════════
3399
3410
 
3400
3411
  function runCompress(dir) {
3401
- const KEEP = 20;
3402
- const logPath = path.join(dir, '.memoc', 'log.md');
3403
- const archivePath = path.join(dir, '.memoc', 'log-archive.md');
3404
-
3405
- if (!fs.existsSync(logPath)) {
3406
- console.log('\n No .memoc/log.md found.\n');
3407
- return;
3408
- }
3409
-
3410
- const src = fs.readFileSync(logPath, 'utf8');
3411
- // Split on entry headers, keep header as part of each chunk
3412
- const parts = src.split(/(?=\n## \[)/);
3413
- const header = parts[0]; // everything before first entry
3414
- const entries = parts.slice(1).filter(e => e.trim());
3412
+ ensureMemocBase(dir);
3413
+ const before = memoryTokenStats(dir);
3414
+ const actions = [];
3415
+ const mark = (label, name) => actions.push(` ${label.padEnd(8)} ${name}`);
3415
3416
 
3416
- if (entries.length <= KEEP) {
3417
- console.log(`\n log.md has ${entries.length} entries — nothing to compress (threshold: ${KEEP}).\n`);
3418
- return;
3417
+ console.log(`\n memoc compress\n`);
3418
+ archiveLegacyLog(dir, mark);
3419
+
3420
+ const trim = trimSummaryFile(dir);
3421
+ if (trim.action === 'trim') {
3422
+ mark('update', `.memoc/session-summary.md (${trim.beforeBytes}B -> ${trim.afterBytes}B)`);
3423
+ mark('update', '.memoc/session-summary-archive.md');
3424
+ } else if (trim.action === 'add') {
3425
+ mark('add', '.memoc/session-summary.md');
3426
+ } else {
3427
+ mark('skip', `.memoc/session-summary.md (compact ${trim.beforeBytes || 0}B)`);
3419
3428
  }
3420
3429
 
3421
- const toArchive = entries.slice(0, entries.length - KEEP);
3422
- const toKeep = entries.slice(entries.length - KEEP);
3423
-
3424
- // Append to archive
3425
- const archiveExists = fs.existsSync(archivePath);
3426
- const archiveHeader = archiveExists ? '' : '# Log Archive\n\nOlder entries moved from log.md by `memoc compress`.\n';
3427
- fs.appendFileSync(archivePath, archiveHeader + toArchive.join('') + '\n', 'utf8');
3428
-
3429
- // Rewrite log.md with only recent entries
3430
- write(logPath, header.trimEnd() + '\n' + toKeep.join('') + '\n');
3431
-
3432
- console.log(`\n memoc compress\n`);
3433
- console.log(' Legacy command: new activity should use .memoc/worklog/ instead of log.md.');
3434
- console.log(` Archived ${toArchive.length} entries .memoc/log-archive.md`);
3435
- console.log(` Kept ${toKeep.length} recent entries in log.md`);
3436
- const saved = Buffer.byteLength(toArchive.join(''), 'utf8');
3437
- console.log(` Freed ~${saved}B from log.md`);
3430
+ const workRoot = path.join(dir, '.memoc', 'worklog');
3431
+ const recent = listMarkdownFiles(workRoot)
3432
+ .filter(fp => path.basename(fp) !== 'README.md')
3433
+ .sort()
3434
+ .reverse()
3435
+ .slice(0, 20);
3436
+ writeActivityIndexes(dir, recent);
3437
+ mark('update', '.memoc/activity.md, .memoc/worklog/README.md, .memoc/actors/README.md');
3438
+
3439
+ ensureObsidianFrontmatter(dir, mark);
3440
+
3441
+ const after = memoryTokenStats(dir);
3442
+ const startupDelta = before.startupTokens - after.startupTokens;
3443
+ const allDelta = before.allTokens - after.allTokens;
3444
+ console.log(actions.join('\n'));
3445
+ console.log(`\n Startup ~${before.startupTokens} -> ~${after.startupTokens} tokens (${startupDelta >= 0 ? '-' : '+'}${Math.abs(startupDelta)})`);
3446
+ console.log(` All mem ~${before.allTokens} -> ~${after.allTokens} tokens (${allDelta >= 0 ? '-' : '+'}${Math.abs(allDelta)})`);
3447
+ console.log(' Note Decisions, handoff, rules, wiki topics, and source docs are preserved.');
3438
3448
  console.log('\n Done.\n');
3439
3449
  }
3440
3450
 
@@ -3470,8 +3480,8 @@ function compactSessionSummary(src) {
3470
3480
  const lines = [
3471
3481
  '# Session Summary',
3472
3482
  `Last: ${nowISO()}`,
3473
- 'Replace this file instead of appending to it. Keep total size <800B and each section ≤3 bullets.',
3474
- 'Completed history belongs in actor worklogs; incomplete/risky resume detail belongs in `04-handoff.md`.',
3483
+ 'Replace, do not append. Keep <800B.',
3484
+ 'History: worklog. Resume risks: 04-handoff.md.',
3475
3485
  '',
3476
3486
  ];
3477
3487
 
@@ -3493,14 +3503,21 @@ function sectionText(src, heading) {
3493
3503
  }
3494
3504
 
3495
3505
  function compactSummaryBullets(text) {
3506
+ const maxLine = 48;
3496
3507
  return String(text || '')
3497
3508
  .split(/\r?\n/)
3498
3509
  .map(line => line.trim())
3499
3510
  .filter(line => line && !line.startsWith('#') && !/^_.*_$/.test(line))
3500
3511
  .map(line => line.replace(/^[-*]\s+/, '').replace(/^\d+[.)]\s+/, '').trim())
3501
3512
  .filter(Boolean)
3502
- .slice(0, 3)
3503
- .map(line => `- ${line.length > 140 ? `${line.slice(0, 137)}...` : line}`);
3513
+ .slice(0, 2)
3514
+ .map(line => {
3515
+ const compact = line
3516
+ .replace(/`/g, '')
3517
+ .replace(/\s+/g, ' ')
3518
+ .trim();
3519
+ return `- ${compact.length > maxLine ? `${compact.slice(0, maxLine - 3)}...` : compact}`;
3520
+ });
3504
3521
  }
3505
3522
 
3506
3523
  function summaryPlaceholder(heading) {
@@ -3585,7 +3602,7 @@ if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') {
3585
3602
  console.log(' summary Print a tiny status/resume overview');
3586
3603
  console.log(' tokens Estimate token cost of current memory files');
3587
3604
  console.log(' trim-summary Archive and compact oversized session-summary.md');
3588
- console.log(' compress Legacy: archive old log.md entries');
3605
+ console.log(' compress Compact memoc files and refresh generated indexes');
3589
3606
  console.log(' add <agent> Add entry file for a specific agent (run without args to list)');
3590
3607
  console.log(' actor [set <name>] Show or set the local memoc actor');
3591
3608
  console.log(' work "<title>" Create a conflict-light actor worklog entry');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kevin0181/memoc",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Give AI agents a memory. Scaffolds session-to-session context for Claude Code, Codex, Cursor, and more.",
5
5
  "keywords": [
6
6
  "ai",