create-claude-cabinet 0.27.0 → 0.27.1

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.
@@ -549,6 +549,111 @@ function buildEdgesJson(edges) {
549
549
  );
550
550
  }
551
551
 
552
+ // ---------------------------------------------------------------------------
553
+ // Merge mode — additive, never clobbers native memory
554
+ // ---------------------------------------------------------------------------
555
+
556
+ const OMEGA_SUBDIR = 'omega-migrated';
557
+
558
+ /**
559
+ * Build the MEMORY.md section that indexes omega topic files living under
560
+ * the omega-migrated/ subdir. Carries the preamble marker so a re-run
561
+ * detects already-migrated. Paths are prefixed with the subdir.
562
+ */
563
+ function buildMergeSection(topicFileMeta, summary) {
564
+ const lines = [
565
+ PREAMBLE_MARKER,
566
+ '',
567
+ `## Migrated from omega (${new Date().toISOString().slice(0, 10)})`,
568
+ '',
569
+ `_${summary.migrated} memories migrated from omega into \`${OMEGA_SUBDIR}/\`. ` +
570
+ `Native memory files above are unchanged. ${summary.edges} edges in ${OMEGA_SUBDIR}/edges.json._`,
571
+ '',
572
+ ];
573
+ for (const { topic, file, count } of topicFileMeta) {
574
+ const desc = describeTopic(topic);
575
+ lines.push(`- [${topic}](${OMEGA_SUBDIR}/${file}) (${count}) — ${desc}`);
576
+ }
577
+ return lines.join('\n') + '\n';
578
+ }
579
+
580
+ /**
581
+ * Additive merge: write omega topic files into an omega-migrated/ subdir
582
+ * (zero filename collision with native files), back up the existing dir
583
+ * first, and append/create a MEMORY.md section indexing them. Never
584
+ * overwrites a native file.
585
+ *
586
+ * @returns {{ backupDir, omegaDir, indexedInto }}
587
+ */
588
+ function mergeIntoExisting(outputDir, topicFiles, edgesJson, mergeSection, opts = {}) {
589
+ const omegaDir = path.join(outputDir, OMEGA_SUBDIR);
590
+ const memoryMdPath = path.join(outputDir, 'MEMORY.md');
591
+
592
+ // GUARD: never clobber whatever already occupies the subdir path. If it
593
+ // exists, it is either a native item the user named `omega-migrated/`, or
594
+ // residue from an interrupted prior merge. We cannot safely tell them
595
+ // apart, and the cost of guessing wrong is permanent loss of native data.
596
+ // Refuse and instruct — the caller surfaces this as a failure with the
597
+ // native dir fully intact (nothing has been written yet at this point).
598
+ if (fs.existsSync(omegaDir)) {
599
+ throw new Error(
600
+ `Refusing to merge: '${OMEGA_SUBDIR}/' already exists at ${omegaDir}. ` +
601
+ `If it is leftover from an interrupted migration, remove it and re-run. ` +
602
+ `If it is your own data, rename it first, then re-run.`
603
+ );
604
+ }
605
+
606
+ // 1. Back up the existing memory dir wholesale, to a guaranteed-fresh path,
607
+ // BEFORE any destructive step. Never proceed without a backup taken
608
+ // this invocation (a stale/reused backup wouldn't reflect pristine
609
+ // native state).
610
+ let backupDir = opts.backupDir;
611
+ if (!backupDir) {
612
+ const stamp = new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-');
613
+ const base = `${outputDir}.pre-omega-merge-${stamp}`;
614
+ backupDir = base;
615
+ for (let n = 1; fs.existsSync(backupDir); n++) backupDir = `${base}-${n}`;
616
+ } else if (fs.existsSync(backupDir)) {
617
+ throw new Error(`Refusing to merge: backup path ${backupDir} already exists.`);
618
+ }
619
+ fs.cpSync(outputDir, backupDir, { recursive: true });
620
+
621
+ // 2. Write omega topic files + edges into the subdir (atomic via staging
622
+ // rename of the subdir). omegaDir is known absent from the guard above.
623
+ const stagingDir = path.join(outputDir, `.${OMEGA_SUBDIR}-staging-${process.pid}`);
624
+ fs.rmSync(stagingDir, { recursive: true, force: true });
625
+ fs.mkdirSync(stagingDir, { recursive: true });
626
+ for (const [name, content] of Object.entries(topicFiles)) {
627
+ fs.writeFileSync(path.join(stagingDir, name), content, 'utf8');
628
+ }
629
+ if (edgesJson) fs.writeFileSync(path.join(stagingDir, 'edges.json'), edgesJson, 'utf8');
630
+ fs.renameSync(stagingDir, omegaDir);
631
+
632
+ // 3. Append/create MEMORY.md with the omega section (preamble marker
633
+ // included so re-runs detect already-migrated). Idempotent: if the
634
+ // marker is somehow already present, do not append a second section.
635
+ let indexedInto;
636
+ if (fs.existsSync(memoryMdPath)) {
637
+ const existing = fs.readFileSync(memoryMdPath, 'utf8');
638
+ if (existing.includes(PREAMBLE_MARKER)) {
639
+ indexedInto = 'already-indexed';
640
+ } else {
641
+ const sep = existing.endsWith('\n') ? '\n' : '\n\n';
642
+ const merged = existing + sep + mergeSection;
643
+ const tmp = memoryMdPath + '.tmp-' + process.pid;
644
+ fs.writeFileSync(tmp, merged, 'utf8');
645
+ fs.renameSync(tmp, memoryMdPath);
646
+ indexedInto = 'appended-to-existing';
647
+ }
648
+ } else {
649
+ const header = `# Memory Index\n\n_Native memory files in this directory predate the index; Claude reads them on demand._\n\n`;
650
+ fs.writeFileSync(memoryMdPath, header + mergeSection, 'utf8');
651
+ indexedInto = 'created-new';
652
+ }
653
+
654
+ return { backupDir, omegaDir, indexedInto };
655
+ }
656
+
552
657
  // ---------------------------------------------------------------------------
553
658
  // Main orchestrator
554
659
  // ---------------------------------------------------------------------------
@@ -564,19 +669,17 @@ async function migrateFromOmega(opts = {}) {
564
669
  const currentProject = opts.currentProject || resolveCurrentProject(cwd, homeDir);
565
670
  const outputDir = resolveOutputDir({ ...opts, cwd, homeDir });
566
671
 
672
+ // Determine write mode. Foreign content (native memory present, no
673
+ // migration preamble) triggers MERGE — additive, never clobbers — so
674
+ // omega memories land alongside native memory instead of being skipped.
675
+ let mode = 'fresh';
567
676
  if (!opts.force) {
568
677
  const existing = checkExistingMigration(outputDir);
569
678
  if (existing.state === 'migrated') {
570
679
  return { migrated: 0, reason: 'already-migrated', outputDir };
571
680
  }
572
681
  if (existing.state === 'foreign-content' || existing.state === 'partial-or-foreign') {
573
- return {
574
- migrated: 0,
575
- reason: existing.state,
576
- outputDir,
577
- details: existing.files || ['MEMORY.md without migration preamble'],
578
- hint: 'Inspect outputDir. If safe to overwrite, re-run with { force: true }.',
579
- };
682
+ mode = 'merge';
580
683
  }
581
684
  }
582
685
 
@@ -587,6 +690,10 @@ async function migrateFromOmega(opts = {}) {
587
690
  const { memories, edges } = readVault(vaultDir);
588
691
 
589
692
  if (memories.length === 0) {
693
+ // Nothing to migrate. In merge mode, leave native memory untouched.
694
+ if (mode === 'merge') {
695
+ return { migrated: 0, reason: 'empty-db', outputDir, mode: 'merge', noop: true };
696
+ }
590
697
  const minimalIndex = `${PREAMBLE_MARKER}\n# Memory Index\n\n_Source: migrated from omega on ${new Date()
591
698
  .toISOString()
592
699
  .slice(0, 10)}. No prior memories migrated — omega database was empty._\n`;
@@ -636,11 +743,7 @@ async function migrateFromOmega(opts = {}) {
636
743
  }
637
744
 
638
745
  const memoryMd = buildMemoryMd(topicFileMeta, summary);
639
- filesToWrite['MEMORY.md'] = memoryMd;
640
-
641
- if (edges.length > 0) {
642
- filesToWrite['edges.json'] = buildEdgesJson(edges);
643
- }
746
+ const edgesJson = edges.length > 0 ? buildEdgesJson(edges) : null;
644
747
 
645
748
  if (opts.dryRun) {
646
749
  return {
@@ -648,13 +751,39 @@ async function migrateFromOmega(opts = {}) {
648
751
  edges: edges.length,
649
752
  outputDir,
650
753
  dryRun: true,
754
+ mode,
651
755
  topicFiles: topicFileMeta,
652
- memoryMdPreview: memoryMd,
756
+ memoryMdPreview: mode === 'merge' ? buildMergeSection(topicFileMeta, summary) : memoryMd,
653
757
  omegaBin,
654
758
  currentProject,
655
759
  };
656
760
  }
657
761
 
762
+ if (mode === 'merge') {
763
+ const mergeSection = buildMergeSection(topicFileMeta, summary);
764
+ const { backupDir, omegaDir, indexedInto } = mergeIntoExisting(
765
+ outputDir,
766
+ filesToWrite,
767
+ edgesJson,
768
+ mergeSection,
769
+ opts
770
+ );
771
+ return {
772
+ migrated: memories.length,
773
+ edges: edges.length,
774
+ outputDir,
775
+ mode: 'merge',
776
+ backupDir,
777
+ omegaDir,
778
+ indexedInto,
779
+ topicFiles: topicFileMeta,
780
+ currentProject,
781
+ };
782
+ }
783
+
784
+ // Fresh write (empty dir, or force clobber).
785
+ filesToWrite['MEMORY.md'] = memoryMd;
786
+ if (edgesJson) filesToWrite['edges.json'] = edgesJson;
658
787
  const stagingDir = path.join(path.dirname(outputDir), STAGING_PREFIX + process.pid);
659
788
  writeStaging(stagingDir, filesToWrite);
660
789
  commitStaging(stagingDir, outputDir, { force: opts.force });
@@ -663,6 +792,7 @@ async function migrateFromOmega(opts = {}) {
663
792
  migrated: memories.length,
664
793
  edges: edges.length,
665
794
  outputDir,
795
+ mode: 'fresh',
666
796
  topicFiles: topicFileMeta,
667
797
  currentProject,
668
798
  };
@@ -221,15 +221,20 @@ async function stepMigrateMemories(ctx) {
221
221
  if (result.reason === 'already-migrated') {
222
222
  return { action: `memories already migrated; reusing existing output at ${result.outputDir}` };
223
223
  }
224
- if (result.reason === 'foreign-content' || result.reason === 'partial-or-foreign') {
225
- return {
226
- action: `migrate-from-omega refused: ${result.reason} at ${result.outputDir}. ` +
227
- `Hint: ${result.hint || 'inspect manually'}. Continuing — omega cleanup steps will still run.`,
228
- };
229
- }
230
224
  if (result.reason === 'empty-db') {
225
+ if (result.mode === 'merge') {
226
+ return { action: `omega DB was empty; native memory at ${result.outputDir} left untouched` };
227
+ }
231
228
  return { action: `omega DB was empty; minimal MEMORY.md written at ${result.outputDir}` };
232
229
  }
230
+ if (result.mode === 'merge') {
231
+ return {
232
+ action:
233
+ `merged ${result.migrated} omega memories (${result.edges || 0} edges) into existing native ` +
234
+ `memory at ${result.outputDir} (omega content under omega-migrated/; backup at ${result.backupDir}). ` +
235
+ `Native memories preserved.`,
236
+ };
237
+ }
233
238
  return {
234
239
  action: `migrated ${result.migrated} memories (${result.edges || 0} edges) → ${result.outputDir}`,
235
240
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-cabinet",
3
- "version": "0.27.0",
3
+ "version": "0.27.1",
4
4
  "description": "Claude Cabinet — opinionated process scaffolding for Claude Code projects",
5
5
  "bin": {
6
6
  "create-claude-cabinet": "bin/create-claude-cabinet.js"