@kevin0181/memoc 1.2.1 → 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 +78 -70
  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
@@ -3294,51 +3294,29 @@ function searchPriority(file, scope = 'memory') {
3294
3294
  // ═══════════════════════════════════════════════════════════════════
3295
3295
 
3296
3296
  function runTokens(dir) {
3297
- const est = text => Math.ceil(Buffer.byteLength(text, 'utf8') / 4);
3298
- const read = fp => { try { return fs.readFileSync(fp, 'utf8'); } catch { return ''; } };
3299
- const memDir = path.join(dir, '.memoc');
3300
-
3301
- const startup = [
3302
- ['CLAUDE.md', path.join(dir, 'CLAUDE.md')],
3303
- ['session-summary.md', path.join(memDir, 'session-summary.md')],
3304
- ];
3305
- const onDemand = [
3306
- ['llms.txt', path.join(dir, 'llms.txt')],
3307
- ['02-current-project-state.md', path.join(memDir, '02-current-project-state.md')],
3308
- ['03-decisions.md', path.join(memDir, '03-decisions.md')],
3309
- ['04-handoff.md', path.join(memDir, '04-handoff.md')],
3310
- ['06-project-rules.md', path.join(memDir, '06-project-rules.md')],
3311
- ['activity.md', path.join(memDir, 'activity.md')],
3312
- ];
3297
+ const stats = memoryTokenStats(dir);
3313
3298
 
3314
3299
  console.log('\n memoc tokens\n');
3315
3300
  let startupTotal = 0;
3316
3301
  console.log(' Startup (always loaded):');
3317
- for (const [name, fp] of startup) {
3318
- const content = read(fp);
3319
- const t = est(content);
3320
- const b = Buffer.byteLength(content, 'utf8');
3321
- startupTotal += t;
3322
- const warn = b > 1000 ? ' ⚠ large' : '';
3323
- 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}`);
3324
3306
  }
3325
3307
  console.log(` ${'── startup total'.padEnd(32)} ${String(startupTotal).padStart(5)} tokens`);
3326
3308
 
3327
3309
  console.log('\n On-demand (read when needed):');
3328
3310
  let onDemandTotal = 0;
3329
- for (const [name, fp] of onDemand) {
3330
- const content = read(fp);
3331
- if (!content) continue;
3332
- const t = est(content);
3333
- const b = Buffer.byteLength(content, 'utf8');
3334
- onDemandTotal += t;
3335
- const warn = t > 500 ? ' ⚠ consider compress' : '';
3336
- 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}`);
3337
3315
  }
3338
3316
  console.log(` ${'── on-demand total'.padEnd(32)} ${String(onDemandTotal).padStart(5)} tokens`);
3339
3317
  console.log(`\n If all loaded: ~${startupTotal + onDemandTotal} tokens`);
3340
3318
 
3341
- const summaryContent = read(path.join(memDir, 'session-summary.md'));
3319
+ const summaryContent = safeRead(path.join(dir, '.memoc', 'session-summary.md'));
3342
3320
  const summaryBytes = Buffer.byteLength(summaryContent, 'utf8');
3343
3321
  if (summaryBytes > 800) {
3344
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.`);
@@ -3346,6 +3324,37 @@ function runTokens(dir) {
3346
3324
  console.log();
3347
3325
  }
3348
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
+
3349
3358
  // ═══════════════════════════════════════════════════════════════════
3350
3359
  // DOCTOR — quick health checks for shared memoc repos
3351
3360
  // ═══════════════════════════════════════════════════════════════════
@@ -3396,47 +3405,46 @@ function runDoctor(dir) {
3396
3405
  }
3397
3406
 
3398
3407
  // ═══════════════════════════════════════════════════════════════════
3399
- // COMPRESS — legacy log.md archiver
3408
+ // COMPRESS — safe whole-memory compaction
3400
3409
  // ═══════════════════════════════════════════════════════════════════
3401
3410
 
3402
3411
  function runCompress(dir) {
3403
- const KEEP = 20;
3404
- const logPath = path.join(dir, '.memoc', 'log.md');
3405
- const archivePath = path.join(dir, '.memoc', 'log-archive.md');
3406
-
3407
- if (!fs.existsSync(logPath)) {
3408
- console.log('\n No .memoc/log.md found.\n');
3409
- return;
3410
- }
3411
-
3412
- const src = fs.readFileSync(logPath, 'utf8');
3413
- // Split on entry headers, keep header as part of each chunk
3414
- const parts = src.split(/(?=\n## \[)/);
3415
- const header = parts[0]; // everything before first entry
3416
- 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}`);
3417
3416
 
3418
- if (entries.length <= KEEP) {
3419
- console.log(`\n log.md has ${entries.length} entries — nothing to compress (threshold: ${KEEP}).\n`);
3420
- 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)`);
3421
3428
  }
3422
3429
 
3423
- const toArchive = entries.slice(0, entries.length - KEEP);
3424
- const toKeep = entries.slice(entries.length - KEEP);
3425
-
3426
- // Append to archive
3427
- const archiveExists = fs.existsSync(archivePath);
3428
- const archiveHeader = archiveExists ? '' : '# Log Archive\n\nOlder entries moved from log.md by `memoc compress`.\n';
3429
- fs.appendFileSync(archivePath, archiveHeader + toArchive.join('') + '\n', 'utf8');
3430
-
3431
- // Rewrite log.md with only recent entries
3432
- write(logPath, header.trimEnd() + '\n' + toKeep.join('') + '\n');
3433
-
3434
- console.log(`\n memoc compress\n`);
3435
- console.log(' Legacy command: new activity should use .memoc/worklog/ instead of log.md.');
3436
- console.log(` Archived ${toArchive.length} entries .memoc/log-archive.md`);
3437
- console.log(` Kept ${toKeep.length} recent entries in log.md`);
3438
- const saved = Buffer.byteLength(toArchive.join(''), 'utf8');
3439
- 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.');
3440
3448
  console.log('\n Done.\n');
3441
3449
  }
3442
3450
 
@@ -3495,14 +3503,14 @@ function sectionText(src, heading) {
3495
3503
  }
3496
3504
 
3497
3505
  function compactSummaryBullets(text) {
3498
- const maxLine = 72;
3506
+ const maxLine = 48;
3499
3507
  return String(text || '')
3500
3508
  .split(/\r?\n/)
3501
3509
  .map(line => line.trim())
3502
3510
  .filter(line => line && !line.startsWith('#') && !/^_.*_$/.test(line))
3503
3511
  .map(line => line.replace(/^[-*]\s+/, '').replace(/^\d+[.)]\s+/, '').trim())
3504
3512
  .filter(Boolean)
3505
- .slice(0, 3)
3513
+ .slice(0, 2)
3506
3514
  .map(line => {
3507
3515
  const compact = line
3508
3516
  .replace(/`/g, '')
@@ -3594,7 +3602,7 @@ if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') {
3594
3602
  console.log(' summary Print a tiny status/resume overview');
3595
3603
  console.log(' tokens Estimate token cost of current memory files');
3596
3604
  console.log(' trim-summary Archive and compact oversized session-summary.md');
3597
- console.log(' compress Legacy: archive old log.md entries');
3605
+ console.log(' compress Compact memoc files and refresh generated indexes');
3598
3606
  console.log(' add <agent> Add entry file for a specific agent (run without args to list)');
3599
3607
  console.log(' actor [set <name>] Show or set the local memoc actor');
3600
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.1",
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",