@kevin0181/memoc 1.1.10 → 1.2.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 +2 -2
  2. package/bin/cli.js +133 -29
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -144,7 +144,7 @@ Run it from the project root. It preserves existing project memory, including:
144
144
  - `.memoc/systems/`
145
145
  - `.memoc/wiki/`
146
146
 
147
- It refreshes the managed blocks, project-local wrappers, runtime copy, PATH helpers, and missing template files. If `memoc` is not on PATH after upgrading, keep using:
147
+ It refreshes the managed blocks, project-local wrappers, runtime copy, PATH helpers, and memoc-owned protocol templates. User-owned memory files such as `session-summary.md`, `03-decisions.md`, `04-handoff.md`, `06-project-rules.md`, and wiki topic/source pages are preserved. Upgrade also runs the `trim-summary` compaction pass so startup memory stays small. If `memoc` is not on PATH after upgrading, keep using:
148
148
 
149
149
  ```bash
150
150
  # Windows
@@ -277,7 +277,7 @@ Actor detection order:
277
277
 
278
278
  `activity.md`, `actors/README.md`, and `worklog/README.md` are regenerated indexes. Run `memoc activity --write` when you want to refresh them from worklog files.
279
279
 
280
- `log.md` is legacy. New installs do not create it, and shared activity should live in worklog files. Existing projects can delete `.memoc/log.md` after preserving any useful history in worklogs or archives.
280
+ `log.md` is legacy. New installs do not create it, and shared activity should live in worklog files. On upgrade, an existing `.memoc/log.md` is moved to `.memoc/raw/legacy-log.md` so old history is preserved but no longer part of the normal memory flow.
281
281
 
282
282
  ---
283
283
 
package/bin/cli.js CHANGED
@@ -209,6 +209,12 @@ function write(filePath, content) {
209
209
  fs.writeFileSync(filePath, content, 'utf8');
210
210
  }
211
211
 
212
+ function writeChanged(filePath, content) {
213
+ if (fs.existsSync(filePath) && fs.readFileSync(filePath, 'utf8') === content) return false;
214
+ write(filePath, content);
215
+ return true;
216
+ }
217
+
212
218
  function slugify(value, fallback = 'note') {
213
219
  const slug = String(value || '')
214
220
  .toLowerCase()
@@ -228,6 +234,76 @@ function uniquePath(filePath) {
228
234
  return `${base}-${i}${ext}`;
229
235
  }
230
236
 
237
+ function archiveLegacyLog(dir, mark) {
238
+ const logPath = path.join(dir, '.memoc', 'log.md');
239
+ if (!fs.existsSync(logPath)) {
240
+ mark('skip', '.memoc/log.md (legacy; no file)');
241
+ return;
242
+ }
243
+ const archivePath = uniquePath(path.join(dir, '.memoc', 'raw', 'legacy-log.md'));
244
+ fs.mkdirSync(path.dirname(archivePath), { recursive: true });
245
+ fs.renameSync(logPath, archivePath);
246
+ mark('move', `${path.relative(dir, logPath)} -> ${path.relative(dir, archivePath)}`);
247
+ }
248
+
249
+ function summarySectionBulletCounts(src) {
250
+ const counts = {};
251
+ let current = '';
252
+ for (const line of String(src || '').split(/\r?\n/)) {
253
+ const heading = line.match(/^##\s+(.+?)\s*$/);
254
+ if (heading) {
255
+ current = heading[1].trim();
256
+ counts[current] = counts[current] || 0;
257
+ continue;
258
+ }
259
+ if (current && /^-\s+/.test(line)) counts[current] = (counts[current] || 0) + 1;
260
+ }
261
+ return counts;
262
+ }
263
+
264
+ function trimSummaryFile(dir) {
265
+ const summaryPath = path.join(dir, '.memoc', 'session-summary.md');
266
+ const archivePath = path.join(dir, '.memoc', 'session-summary-archive.md');
267
+ if (!fs.existsSync(summaryPath)) {
268
+ write(summaryPath, tplSessionSummary());
269
+ return { action: 'add' };
270
+ }
271
+
272
+ const src = fs.readFileSync(summaryPath, 'utf8');
273
+ const beforeBytes = Buffer.byteLength(src, 'utf8');
274
+ const counts = summarySectionBulletCounts(src);
275
+ const tooManyBullets = Object.values(counts).some(count => count > 3);
276
+ if (beforeBytes <= 800 && !tooManyBullets) {
277
+ return { action: 'skip', beforeBytes };
278
+ }
279
+
280
+ const compact = compactSessionSummary(src);
281
+ const afterBytes = Buffer.byteLength(compact, 'utf8');
282
+ const archiveHeader = fs.existsSync(archivePath)
283
+ ? ''
284
+ : '# Session Summary Archive\n\nOlder oversized startup summaries moved by `memoc trim-summary`.\n';
285
+ fs.appendFileSync(archivePath, `${archiveHeader}\n## [${nowISO()}] archived summary (${beforeBytes}B)\n\n${src.trimEnd()}\n`, 'utf8');
286
+ write(summaryPath, compact);
287
+ return { action: 'trim', beforeBytes, afterBytes };
288
+ }
289
+
290
+ function migrateLegacyLogReferences(filePath) {
291
+ if (!fs.existsSync(filePath)) return false;
292
+ const before = fs.readFileSync(filePath, 'utf8');
293
+ let after = before
294
+ .replace(/- \[Project Log\]\(log\.md\)\n/g, '- [Activity](activity.md)\n- [Worklog](worklog/README.md)\n')
295
+ .replace(/\| `\.memoc\/log\.md` \| For append-only history \|\n/g, '| `.memoc/activity.md` | Generated worklog index |\n| `.memoc/worklog/` | Actor-scoped work history |\n')
296
+ .replace(/See `\.memoc\/log\.md` for full history\./g, 'See `.memoc/worklog/` for full shared activity history.')
297
+ .replace(/See `\.memoc\/log\.md`\./g, 'See `.memoc/worklog/` and generated `.memoc/activity.md`.')
298
+ .replace(/- \[ \] `\.memoc\/log\.md` has a new entry for meaningful work\./g, '- [ ] Meaningful shared work has a `.memoc/worklog/<actor>/YYYY-MM/*.md` entry.')
299
+ .replace(/Append `\.memoc\/log\.md` for meaningful changes, decisions, and handoffs\./g, 'Create a short actor worklog with `memoc work "<title>" --from-git` for meaningful changes, decisions, and handoffs.')
300
+ .replace(/Keep completed history in `\.memoc\/log\.md`; keep current-state files short\./g, 'Keep completed history in actor worklogs; keep current-state files short.')
301
+ .replace(/Append `\.memoc\/log\.md`\./g, 'If the change is meaningful shared work, run `memoc work "<title>" --from-git`.');
302
+ if (after === before) return false;
303
+ write(filePath, after);
304
+ return true;
305
+ }
306
+
231
307
  function markdownTitle(src, fallback) {
232
308
  const m = String(src || '').match(/^#\s+(.+)$/m);
233
309
  return m ? m[1].trim() : fallback;
@@ -2252,10 +2328,18 @@ function run(dir, forceUpdate, action = 'update') {
2252
2328
  mark('add', 'llms.txt');
2253
2329
  }
2254
2330
 
2255
- // Dynamic memory filesupdate managed sections only
2331
+ // Generated memory mapsreplace so old protocols do not linger.
2332
+ const generatedRefresh = [
2333
+ [path.join(memDir, '00-project-brief.md'), () => tplProjectBrief(p)],
2334
+ [path.join(memDir, '00-agent-index.md'), () => tplAgentIndex(p)],
2335
+ ];
2336
+ for (const [fp, tpl] of generatedRefresh) {
2337
+ const rel = path.relative(dir, fp);
2338
+ mark(writeChanged(fp, tpl()) ? 'update' : 'skip', rel);
2339
+ }
2340
+
2341
+ // Dynamic user-owned memory files — update managed sections only
2256
2342
  const dynUpdates = [
2257
- [path.join(memDir, '00-project-brief.md'), () => tplProjectBrief(p), ID_S, ID_E, identityInner(p)],
2258
- [path.join(memDir, '00-agent-index.md'), () => tplAgentIndex(p), SNAP_S, SNAP_E, snapshotInner(p)],
2259
2343
  [path.join(memDir, '02-current-project-state.md'), () => tplCurrentState(p), SNAP_S, SNAP_E, snapshotInner(p)],
2260
2344
  ];
2261
2345
  for (const [fp, tpl, s, e, inner] of dynUpdates) {
@@ -2283,18 +2367,27 @@ function run(dir, forceUpdate, action = 'update') {
2283
2367
  mark('add', '.memoc/session-summary.md');
2284
2368
  }
2285
2369
 
2286
- // Static + user-owned files — only add if missing
2287
- const addIfMissing = [
2370
+ // Protocol/template files — replace on update so old instructions are removed.
2371
+ const templateRefresh = [
2288
2372
  [path.join(memDir, 'boot.md'), tplBoot],
2289
2373
  [path.join(memDir, '01-agent-workflow.md'), tplWorkflow],
2374
+ [path.join(memDir, '05-done-checklist.md'), tplDoneChecklist],
2375
+ [path.join(memDir, 'memoc-usage.md'), tplMemocUsage],
2376
+ [path.join(dir, 'skills/project-memory-maintainer/SKILL.md'), tplSkillMaintainer],
2377
+ ];
2378
+ for (const [fp, tpl] of templateRefresh) {
2379
+ const rel = path.relative(dir, fp);
2380
+ mark(writeChanged(fp, tpl()) ? 'update' : 'skip', rel);
2381
+ }
2382
+
2383
+ // Static indexes/scaffolds — add if missing; content may be user- or command-owned.
2384
+ const addIfMissing = [
2290
2385
  [path.join(memDir, '03-decisions.md'), tplDecisions],
2291
2386
  [path.join(memDir, '04-handoff.md'), tplHandoff],
2292
- [path.join(memDir, '05-done-checklist.md'), tplDoneChecklist],
2293
2387
  [path.join(memDir, '06-project-rules.md'), tplProjectRules],
2294
2388
  [path.join(memDir, 'activity.md'), tplActivity],
2295
2389
  [path.join(memDir, 'actors/README.md'), tplActorsReadme],
2296
2390
  [path.join(memDir, 'worklog/README.md'), tplWorklogReadme],
2297
- [path.join(memDir, 'memoc-usage.md'), tplMemocUsage],
2298
2391
  [path.join(memDir, 'systems/README.md'), tplSystemsReadme],
2299
2392
  [path.join(memDir, 'raw/README.md'), tplRawReadme],
2300
2393
  [path.join(memDir, 'raw/files/README.md'), tplRawFilesReadme],
@@ -2309,7 +2402,6 @@ function run(dir, forceUpdate, action = 'update') {
2309
2402
  [path.join(memDir, 'wiki/sources/README.md'), tplWikiSourcesReadme],
2310
2403
  [path.join(memDir, 'wiki/topics/README.md'), tplWikiTopicsReadme],
2311
2404
  [path.join(memDir, 'wiki/global/README.md'), tplWikiGlobalReadme],
2312
- [path.join(dir, 'skills/project-memory-maintainer/SKILL.md'), tplSkillMaintainer],
2313
2405
  ];
2314
2406
  for (const [fp, tpl] of addIfMissing) {
2315
2407
  const rel = path.relative(dir, fp);
@@ -2318,8 +2410,20 @@ function run(dir, forceUpdate, action = 'update') {
2318
2410
  }
2319
2411
  ensureWikiScaffoldLinks(memDir, mark);
2320
2412
 
2321
- // Obsidian graph filters — add/merge memoc tags for existing installs too
2322
- ensureObsidianFrontmatter(dir, mark);
2413
+ const legacyReferenceFiles = [
2414
+ path.join(memDir, '02-current-project-state.md'),
2415
+ path.join(memDir, '04-handoff.md'),
2416
+ path.join(memDir, '06-project-rules.md'),
2417
+ path.join(memDir, 'systems/README.md'),
2418
+ path.join(memDir, 'wiki/index.md'),
2419
+ path.join(memDir, 'wiki/sources.md'),
2420
+ path.join(memDir, 'wiki/glossary.md'),
2421
+ path.join(memDir, 'wiki/questions.md'),
2422
+ path.join(memDir, 'wiki/lint.md'),
2423
+ ];
2424
+ for (const fp of legacyReferenceFiles) {
2425
+ if (migrateLegacyLogReferences(fp)) mark('update', `${path.relative(dir, fp)} (legacy refs)`);
2426
+ }
2323
2427
 
2324
2428
  // PATH helpers — let agents run memoc even when the npm bin is not on PATH
2325
2429
  ensureClaudeStopHookFile(dir, mark);
@@ -2327,7 +2431,20 @@ function run(dir, forceUpdate, action = 'update') {
2327
2431
  ensurePathHelpers(dir, mark);
2328
2432
  ensurePathRegistration(dir, mark);
2329
2433
 
2330
- mark('skip', '.memoc/log.md (legacy; shared history belongs in worklog)');
2434
+ archiveLegacyLog(dir, mark);
2435
+
2436
+ const trim = trimSummaryFile(dir);
2437
+ if (trim.action === 'trim') {
2438
+ mark('update', `.memoc/session-summary.md (trimmed ${trim.beforeBytes}B -> ${trim.afterBytes}B)`);
2439
+ mark('update', '.memoc/session-summary-archive.md');
2440
+ } else if (trim.action === 'add') {
2441
+ mark('add', '.memoc/session-summary.md');
2442
+ } else {
2443
+ mark('skip', `.memoc/session-summary.md (compact ${trim.beforeBytes || 0}B)`);
2444
+ }
2445
+
2446
+ // Obsidian graph filters — add/merge memoc tags for existing installs too
2447
+ ensureObsidianFrontmatter(dir, mark);
2331
2448
  }
2332
2449
 
2333
2450
  hideOnWindows(memDir);
@@ -3326,37 +3443,24 @@ function runCompress(dir) {
3326
3443
  // ═══════════════════════════════════════════════════════════════════
3327
3444
 
3328
3445
  function runTrimSummary(dir) {
3329
- const summaryPath = path.join(dir, '.memoc', 'session-summary.md');
3330
- const archivePath = path.join(dir, '.memoc', 'session-summary-archive.md');
3331
- if (!fs.existsSync(summaryPath)) {
3332
- write(summaryPath, tplSessionSummary());
3446
+ const result = trimSummaryFile(dir);
3447
+ if (result.action === 'add') {
3333
3448
  console.log('\n memoc trim-summary\n');
3334
3449
  console.log(' Added .memoc/session-summary.md');
3335
3450
  console.log('\n Done.\n');
3336
3451
  return;
3337
3452
  }
3338
3453
 
3339
- const src = fs.readFileSync(summaryPath, 'utf8');
3340
- const beforeBytes = Buffer.byteLength(src, 'utf8');
3341
- const compact = compactSessionSummary(src);
3342
- const afterBytes = Buffer.byteLength(compact, 'utf8');
3343
-
3344
- if (src === compact && beforeBytes <= 800) {
3454
+ if (result.action === 'skip') {
3345
3455
  console.log('\n memoc trim-summary\n');
3346
- console.log(` session-summary.md is already compact (${beforeBytes}B).`);
3456
+ console.log(` session-summary.md is already compact (${result.beforeBytes}B).`);
3347
3457
  console.log('\n Done.\n');
3348
3458
  return;
3349
3459
  }
3350
3460
 
3351
- const archiveHeader = fs.existsSync(archivePath)
3352
- ? ''
3353
- : '# Session Summary Archive\n\nOlder oversized startup summaries moved by `memoc trim-summary`.\n';
3354
- fs.appendFileSync(archivePath, `${archiveHeader}\n## [${nowISO()}] archived summary (${beforeBytes}B)\n\n${src.trimEnd()}\n`, 'utf8');
3355
- write(summaryPath, compact);
3356
-
3357
3461
  console.log('\n memoc trim-summary\n');
3358
3462
  console.log(` Archived .memoc/session-summary-archive.md`);
3359
- console.log(` Rewrote .memoc/session-summary.md (${beforeBytes}B → ${afterBytes}B)`);
3463
+ console.log(` Rewrote .memoc/session-summary.md (${result.beforeBytes}B → ${result.afterBytes}B)`);
3360
3464
  console.log(' Reminder Completed history belongs in worklog; resume details belong in 04-handoff.md.');
3361
3465
  console.log('\n Done.\n');
3362
3466
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kevin0181/memoc",
3
- "version": "1.1.10",
3
+ "version": "1.2.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",