@mmnto/cli 0.44.0 → 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.
Files changed (65) hide show
  1. package/dist/adapters/gh-utils.d.ts.map +1 -1
  2. package/dist/adapters/gh-utils.js +6 -5
  3. package/dist/adapters/gh-utils.js.map +1 -1
  4. package/dist/adapters/gh-utils.test.js +2 -0
  5. package/dist/adapters/gh-utils.test.js.map +1 -1
  6. package/dist/adapters/github-cli-pr.test.js +2 -0
  7. package/dist/adapters/github-cli-pr.test.js.map +1 -1
  8. package/dist/adapters/github-cli.test.js +4 -2
  9. package/dist/adapters/github-cli.test.js.map +1 -1
  10. package/dist/adapters/multi-repo-adapter.d.ts.map +1 -1
  11. package/dist/adapters/multi-repo-adapter.js +4 -4
  12. package/dist/adapters/multi-repo-adapter.js.map +1 -1
  13. package/dist/assets/compiled-baseline.d.ts +3 -0
  14. package/dist/assets/compiled-baseline.d.ts.map +1 -0
  15. package/dist/assets/compiled-baseline.js +243 -0
  16. package/dist/assets/compiled-baseline.js.map +1 -0
  17. package/dist/commands/audit.d.ts.map +1 -1
  18. package/dist/commands/audit.js +25 -13
  19. package/dist/commands/audit.js.map +1 -1
  20. package/dist/commands/compile.d.ts.map +1 -1
  21. package/dist/commands/compile.js +32 -0
  22. package/dist/commands/compile.js.map +1 -1
  23. package/dist/commands/docs.d.ts +1 -1
  24. package/dist/commands/docs.d.ts.map +1 -1
  25. package/dist/commands/docs.js +31 -26
  26. package/dist/commands/docs.js.map +1 -1
  27. package/dist/commands/docs.test.js +3 -3
  28. package/dist/commands/docs.test.js.map +1 -1
  29. package/dist/commands/explain.d.ts +2 -0
  30. package/dist/commands/explain.d.ts.map +1 -0
  31. package/dist/commands/explain.js +98 -0
  32. package/dist/commands/explain.js.map +1 -0
  33. package/dist/commands/extract.d.ts.map +1 -1
  34. package/dist/commands/extract.js +6 -2
  35. package/dist/commands/extract.js.map +1 -1
  36. package/dist/commands/init.d.ts +4 -2
  37. package/dist/commands/init.d.ts.map +1 -1
  38. package/dist/commands/init.js +346 -235
  39. package/dist/commands/init.js.map +1 -1
  40. package/dist/commands/init.test.js +30 -13
  41. package/dist/commands/init.test.js.map +1 -1
  42. package/dist/commands/run-compiled-rules.d.ts.map +1 -1
  43. package/dist/commands/run-compiled-rules.js +14 -6
  44. package/dist/commands/run-compiled-rules.js.map +1 -1
  45. package/dist/commands/spec.d.ts +1 -1
  46. package/dist/commands/spec.d.ts.map +1 -1
  47. package/dist/commands/spec.js +38 -8
  48. package/dist/commands/spec.js.map +1 -1
  49. package/dist/commands/triage.js +1 -1
  50. package/dist/git.js +1 -1
  51. package/dist/git.js.map +1 -1
  52. package/dist/git.test.js +2 -0
  53. package/dist/git.test.js.map +1 -1
  54. package/dist/index.js +38 -2
  55. package/dist/index.js.map +1 -1
  56. package/dist/orchestrators/conformance.test.js +1 -0
  57. package/dist/orchestrators/conformance.test.js.map +1 -1
  58. package/dist/orchestrators/ollama-orchestrator.d.ts.map +1 -1
  59. package/dist/orchestrators/ollama-orchestrator.js +3 -0
  60. package/dist/orchestrators/ollama-orchestrator.js.map +1 -1
  61. package/dist/orchestrators/ollama-orchestrator.test.js +2 -2
  62. package/dist/orchestrators/ollama-orchestrator.test.js.map +1 -1
  63. package/dist/orchestrators/shell-orchestrator.test.js +1 -0
  64. package/dist/orchestrators/shell-orchestrator.test.js.map +1 -1
  65. package/package.json +2 -2
@@ -1,3 +1,4 @@
1
+ import { execSync } from 'node:child_process';
1
2
  import * as fs from 'node:fs';
2
3
  import * as path from 'node:path';
3
4
  import { stdin as input, stdout as output } from 'node:process';
@@ -437,7 +438,94 @@ function formatTargets(targets) {
437
438
  });
438
439
  return lines.join('\n');
439
440
  }
440
- export async function generateConfig(targets, embeddingTier) {
441
+ /** Check whether a CLI command exists on PATH. */
442
+ function cliExists(name) {
443
+ try {
444
+ const cmd = IS_WIN ? `where ${name}` : `which ${name}`;
445
+ execSync(cmd, { stdio: 'ignore', timeout: 3000 });
446
+ return true;
447
+ }
448
+ catch {
449
+ return false;
450
+ }
451
+ }
452
+ /** Check whether any of the given env keys are set (in process.env or .env file content). */
453
+ function hasKey(envContent, ...keyNames) {
454
+ for (const keyName of keyNames) {
455
+ if (process.env[keyName] && /\S/.test(process.env[keyName])) {
456
+ return true;
457
+ }
458
+ }
459
+ const keyPattern = new RegExp(`^\\s*(?:${keyNames.join('|')})\\s*=\\s*\\S+`, 'm');
460
+ return keyPattern.test(envContent);
461
+ }
462
+ /**
463
+ * Auto-detect the best orchestrator from the environment.
464
+ * Priority: gemini CLI → claude CLI → API keys (GEMINI → ANTHROPIC → OPENAI) → null.
465
+ */
466
+ function detectOrchestrator(cwd) {
467
+ // Read .env file once (loadEnv may not have run yet)
468
+ const envPath = path.join(cwd, '.env');
469
+ const envContent = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf-8') : '';
470
+ // 1. Gemini CLI on PATH → shell provider
471
+ if (cliExists('gemini')) {
472
+ return {
473
+ block: ` orchestrator: {
474
+ provider: 'shell',
475
+ command: 'gemini --model {model} -o json -e none < {file}',
476
+ defaultModel: 'gemini-3-flash-preview',
477
+ overrides: {
478
+ 'spec': 'gemini-3.1-pro-preview',
479
+ 'shield': 'gemini-3.1-pro-preview',
480
+ 'triage': 'gemini-3.1-pro-preview',
481
+ },
482
+ },`,
483
+ };
484
+ }
485
+ // 2. Claude CLI on PATH → shell provider (anthropic)
486
+ if (cliExists('claude')) {
487
+ return {
488
+ block: ` orchestrator: {
489
+ provider: 'shell',
490
+ command: 'claude -p {file} --model {model} --output-format json',
491
+ defaultModel: 'sonnet',
492
+ },`,
493
+ };
494
+ }
495
+ // 3. API keys → native SDK providers
496
+ if (hasKey(envContent, 'GEMINI_API_KEY', 'GOOGLE_API_KEY')) {
497
+ return {
498
+ block: ` orchestrator: {
499
+ provider: 'gemini',
500
+ defaultModel: 'gemini-3-flash-preview',
501
+ overrides: {
502
+ 'spec': 'gemini-3.1-pro-preview',
503
+ 'shield': 'gemini-3.1-pro-preview',
504
+ 'triage': 'gemini-3.1-pro-preview',
505
+ },
506
+ },`,
507
+ };
508
+ }
509
+ if (hasKey(envContent, 'ANTHROPIC_API_KEY')) {
510
+ return {
511
+ block: ` orchestrator: {
512
+ provider: 'anthropic',
513
+ defaultModel: 'claude-sonnet-4-20250514',
514
+ },`,
515
+ };
516
+ }
517
+ if (hasKey(envContent, 'OPENAI_API_KEY')) {
518
+ return {
519
+ block: ` orchestrator: {
520
+ provider: 'openai',
521
+ defaultModel: 'gpt-4.1-mini',
522
+ },`,
523
+ };
524
+ }
525
+ // 4. Nothing found → omit orchestrator (Lite/Standard tier)
526
+ return null;
527
+ }
528
+ export async function generateConfig(targets, embeddingTier, cwd) {
441
529
  const { DEFAULT_IGNORE_PATTERNS } = await import('@mmnto/totem');
442
530
  let embeddingBlock;
443
531
  switch (embeddingTier) {
@@ -454,6 +542,10 @@ export async function generateConfig(targets, embeddingTier) {
454
542
  embeddingBlock = ` // embedding: { provider: 'openai', model: 'text-embedding-3-small' },\n // Lite tier — set OPENAI_API_KEY and re-run \`totem init\` to enable sync/search.`;
455
543
  break;
456
544
  }
545
+ const orchestrator = detectOrchestrator(cwd);
546
+ const orchestratorBlock = orchestrator
547
+ ? `\n${orchestrator.block}`
548
+ : `\n // orchestrator: no CLI or API key detected. Add one and re-run \`totem init\`.`;
457
549
  return `import type { TotemConfig } from '@mmnto/totem';
458
550
 
459
551
  const config: TotemConfig = {
@@ -466,17 +558,7 @@ ${embeddingBlock}
466
558
  ignorePatterns: [
467
559
  ${DEFAULT_IGNORE_PATTERNS.map((p) => ` '${p}',`).join('\n')}
468
560
  ],
469
-
470
- orchestrator: {
471
- provider: 'shell',
472
- command: 'gemini --model {model} -o json -e none < {file}',
473
- defaultModel: 'gemini-3-flash-preview',
474
- overrides: {
475
- 'spec': 'gemini-3.1-pro-preview',
476
- 'shield': 'gemini-3.1-pro-preview',
477
- 'triage': 'gemini-3.1-pro-preview',
478
- },
479
- },
561
+ ${orchestratorBlock}
480
562
  };
481
563
 
482
564
  export default config;
@@ -490,16 +572,11 @@ export function detectEmbeddingTier(cwd) {
490
572
  // Read .env file once (loadEnv may not have run yet)
491
573
  const envPath = path.join(cwd, '.env');
492
574
  const envContent = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf-8') : '';
493
- const hasGemini = (process.env['GEMINI_API_KEY'] && /\S/.test(process.env['GEMINI_API_KEY'])) ||
494
- (process.env['GOOGLE_API_KEY'] && /\S/.test(process.env['GOOGLE_API_KEY'])) ||
495
- /^\s*(?:GEMINI_API_KEY|GOOGLE_API_KEY)\s*=\s*\S+/m.test(envContent);
496
- const hasOpenai = (process.env['OPENAI_API_KEY'] && /\S/.test(process.env['OPENAI_API_KEY'])) ||
497
- /^\s*OPENAI_API_KEY\s*=\s*\S+/m.test(envContent);
498
575
  // Gemini first — task-type aware embeddings, best retrieval quality
499
- if (hasGemini)
576
+ if (hasKey(envContent, 'GEMINI_API_KEY', 'GOOGLE_API_KEY'))
500
577
  return 'gemini';
501
578
  // OpenAI — widely available, low friction
502
- if (hasOpenai)
579
+ if (hasKey(envContent, 'OPENAI_API_KEY'))
503
580
  return 'openai';
504
581
  return 'none';
505
582
  }
@@ -611,7 +688,7 @@ function applyReflexUpgrade(filePath) {
611
688
  fs.writeFileSync(filePath, updated, 'utf-8');
612
689
  return clean;
613
690
  }
614
- export async function initCommand() {
691
+ export async function initCommand(options) {
615
692
  const cwd = process.cwd();
616
693
  const configPath = path.join(cwd, 'totem.config.ts');
617
694
  const totemDir = path.join(cwd, '.totem');
@@ -623,70 +700,83 @@ export async function initCommand() {
623
700
  if (!configExists) {
624
701
  // --- Fresh install: generate config ---
625
702
  log.info('Totem', 'Scanning project...');
626
- const detected = detectProject(cwd);
627
- const detections = [];
628
- if (detected.hasTypeScript)
629
- detections.push('TypeScript');
630
- if (detected.hasSrc)
631
- detections.push('src/');
632
- if (detected.hasDocs)
633
- detections.push('docs/');
634
- if (detected.hasSpecs)
635
- detections.push('specs/');
636
- if (detected.hasContext)
637
- detections.push('context/');
638
- if (detected.hasSessions)
639
- detections.push('session logs');
640
- if (detections.length > 0) {
641
- log.info('Totem', `Detected: ${bold(detections.join(', '))}`);
642
- }
643
- else {
644
- log.dim('Totem', 'No specific project structure detected. Using markdown defaults.');
645
- }
646
- const targets = buildTargets(detected);
647
- // Auto-detect embedding tier from environment
703
+ let targets = [];
648
704
  let embeddingTier = detectEmbeddingTier(cwd);
649
- if (embeddingTier === 'openai') {
650
- log.info('Totem', `Detected ${bold('OPENAI_API_KEY')} in environment. Using OpenAI embeddings.`);
651
- }
652
- else if (embeddingTier === 'gemini') {
653
- log.info('Totem', `Detected ${bold('GEMINI_API_KEY')} in environment. Using Gemini embeddings (single-key DX).`);
705
+ if (options?.bare) {
706
+ log.info('Totem', `Initializing in ${bold('bare mode')} (non-code repository)`);
707
+ targets = [
708
+ { glob: '.totem/lessons/*.md', type: 'lesson', strategy: 'markdown-heading' },
709
+ { glob: '.totem/lessons.md', type: 'lesson', strategy: 'markdown-heading' },
710
+ { glob: '**/*.md', type: 'spec', strategy: 'markdown-heading' },
711
+ ];
712
+ embeddingTier = 'none'; // Force Lite tier for bare repos
654
713
  }
655
714
  else {
656
- // No key detected prompt the user
657
- const answer = await rl.question('Enter your OpenAI API key, type "ollama" for a local model, or press Enter for Lite tier: ');
658
- const input = answer.trim().replace(/[\r\n]/g, '');
659
- if (input.toLowerCase() === 'ollama') {
660
- embeddingTier = 'ollama';
661
- log.info('Totem', 'Configured for Ollama. Make sure it is running locally.');
715
+ const detected = detectProject(cwd);
716
+ const detections = [];
717
+ if (detected.hasTypeScript)
718
+ detections.push('TypeScript');
719
+ if (detected.hasSrc)
720
+ detections.push('src/');
721
+ if (detected.hasDocs)
722
+ detections.push('docs/');
723
+ if (detected.hasSpecs)
724
+ detections.push('specs/');
725
+ if (detected.hasContext)
726
+ detections.push('context/');
727
+ if (detected.hasSessions)
728
+ detections.push('session logs');
729
+ if (detections.length > 0) {
730
+ log.info('Totem', `Detected: ${bold(detections.join(', '))}`);
662
731
  }
663
- else if (input) {
664
- if (!/^sk-[a-zA-Z0-9_-]+$/.test(input)) {
665
- log.warn('Totem', 'API key does not look like a valid OpenAI key (expected sk-...). Starting in Lite tier.');
732
+ else {
733
+ log.dim('Totem', 'No specific project structure detected. Using markdown defaults.');
734
+ }
735
+ targets = buildTargets(detected);
736
+ if (embeddingTier === 'openai') {
737
+ log.info('Totem', `Detected ${bold('OPENAI_API_KEY')} in environment. Using OpenAI embeddings.`);
738
+ }
739
+ else if (embeddingTier === 'gemini') {
740
+ log.info('Totem', `Detected ${bold('GEMINI_API_KEY')} in environment. Using Gemini embeddings (single-key DX).`);
741
+ }
742
+ else {
743
+ // No key detected — prompt the user
744
+ const answer = await rl.question('Enter your OpenAI API key, type "ollama" for a local model, or press Enter for Lite tier: ');
745
+ const input = answer.trim().replace(/[\r\n]/g, '');
746
+ if (input.toLowerCase() === 'ollama') {
747
+ embeddingTier = 'ollama';
748
+ log.info('Totem', 'Configured for Ollama. Make sure it is running locally.');
666
749
  }
667
- else {
668
- const envPath = path.join(cwd, '.env');
669
- const envLine = `OPENAI_API_KEY="${input}"\n`;
670
- if (fs.existsSync(envPath)) {
671
- const existing = fs.readFileSync(envPath, 'utf-8');
672
- if (!/^\s*OPENAI_API_KEY\s*=/m.test(existing)) {
673
- const prefix = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
674
- fs.appendFileSync(envPath, prefix + envLine);
675
- }
750
+ else if (input) {
751
+ if (!/^sk-[a-zA-Z0-9_-]+$/.test(input)) {
752
+ log.warn('Totem', 'API key does not look like a valid OpenAI key (expected sk-...). Starting in Lite tier.');
676
753
  }
677
754
  else {
678
- fs.writeFileSync(envPath, envLine);
755
+ const envPath = path.join(cwd, '.env');
756
+ const envLine = `OPENAI_API_KEY="${input}"\n`;
757
+ if (fs.existsSync(envPath)) {
758
+ const existing = fs.readFileSync(envPath, 'utf-8');
759
+ if (!/^\s*OPENAI_API_KEY\s*=/m.test(existing)) {
760
+ const prefix = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
761
+ fs.appendFileSync(envPath, prefix + envLine);
762
+ }
763
+ }
764
+ else {
765
+ fs.writeFileSync(envPath, envLine);
766
+ }
767
+ embeddingTier = 'openai';
768
+ summary.push({ file: '.env', action: 'Saved OpenAI API key' });
679
769
  }
680
- embeddingTier = 'openai';
681
- summary.push({ file: '.env', action: 'Saved OpenAI API key' });
682
770
  }
683
771
  }
684
772
  }
685
773
  if (embeddingTier === 'none') {
686
774
  log.info('Totem', `Starting in ${bold('Lite')} tier (add-lesson, bridge, eject only).`);
687
- log.dim('Totem', 'Set OPENAI_API_KEY and re-run `totem init` to unlock sync/search/shield.');
775
+ if (!options?.bare) {
776
+ log.dim('Totem', 'Set OPENAI_API_KEY and re-run `totem init` to unlock sync/search/shield.');
777
+ }
688
778
  }
689
- const configContent = await generateConfig(targets, embeddingTier);
779
+ const configContent = await generateConfig(targets, embeddingTier, cwd);
690
780
  fs.writeFileSync(configPath, configContent, 'utf-8');
691
781
  const tierLabel = embeddingTier === 'none'
692
782
  ? 'Lite'
@@ -724,194 +814,215 @@ export async function initCommand() {
724
814
  action: 'Installed Universal Baseline lessons',
725
815
  });
726
816
  }
727
- // --- Unified AI tool selection ---
728
- const detectedTools = detectAiTools(cwd);
729
- if (detectedTools.length > 0) {
730
- const toolNames = detectedTools.map((t) => t.name).join(', ');
731
- log.info('Totem', `Detected AI tools: ${bold(toolNames)}`);
732
- const toolAnswer = await rl.question('Which tools should Totem configure? [all/none/select] (default: all): ');
733
- let selectedTools;
734
- const trimmed = toolAnswer.trim().toLowerCase();
735
- if (trimmed === 'none') {
736
- selectedTools = [];
817
+ // --- Pre-compiled baseline rules (zero-LLM protection from Day 1) ---
818
+ const compiledRulesPath = path.join(totemDir, 'compiled-rules.json');
819
+ if (!fs.existsSync(compiledRulesPath)) {
820
+ try {
821
+ const { COMPILED_BASELINE_RULES } = await import('../assets/compiled-baseline.js');
822
+ const payload = { version: 1, rules: COMPILED_BASELINE_RULES };
823
+ fs.writeFileSync(compiledRulesPath, JSON.stringify(payload, null, 2) + '\n');
824
+ summary.push({
825
+ file: '.totem/compiled-rules.json',
826
+ action: `Installed ${COMPILED_BASELINE_RULES.length} pre-compiled baseline rules`,
827
+ });
737
828
  }
738
- else if (trimmed === 'select') {
739
- selectedTools = [];
740
- for (const tool of detectedTools) {
741
- const pick = await rl.question(` Configure ${tool.name}? (Y/n): `);
742
- if (pick.trim().toLowerCase() !== 'n' && pick.trim().toLowerCase() !== 'no') {
743
- selectedTools.push(tool);
744
- }
745
- }
829
+ catch (err) {
830
+ log.dim('Totem', `Could not install pre-compiled rules: ${err instanceof Error ? err.message : String(err)}`);
746
831
  }
747
- else {
748
- // 'all' or Enter (default)
749
- selectedTools = detectedTools;
750
- }
751
- // --- MCP scaffolding for selected tools ---
752
- for (const tool of selectedTools) {
753
- if (!tool.mcpPath || !tool.serverEntry)
754
- continue;
755
- const filePath = path.join(cwd, tool.mcpPath);
756
- const result = scaffoldMcpConfig(filePath, tool.serverEntry);
757
- if (result.err) {
758
- log.error('Totem Error', result.err); // totem-ignore — result.err is internal scaffolding error, not LLM output
759
- console.error(`To fix this, add the following manually to your ${tool.mcpPath} under "mcpServers":\n`);
760
- console.error(` "totem": ${JSON.stringify(tool.serverEntry, null, 2)}\n`);
832
+ }
833
+ if (options?.bare) {
834
+ log.info('Totem', 'Skipping AI tool and hook installation for bare mode.');
835
+ }
836
+ else {
837
+ // --- Unified AI tool selection ---
838
+ const detectedTools = detectAiTools(cwd);
839
+ if (detectedTools.length > 0) {
840
+ const toolNames = detectedTools.map((t) => t.name).join(', ');
841
+ log.info('Totem', `Detected AI tools: ${bold(toolNames)}`);
842
+ const toolAnswer = await rl.question('Which tools should Totem configure? [all/none/select] (default: all): ');
843
+ let selectedTools;
844
+ const trimmed = toolAnswer.trim().toLowerCase();
845
+ if (trimmed === 'none') {
846
+ selectedTools = [];
761
847
  }
762
- else if (result.action === 'created') {
763
- summary.push({ file: tool.mcpPath, action: `Created with Totem MCP server` });
848
+ else if (trimmed === 'select') {
849
+ selectedTools = [];
850
+ for (const tool of detectedTools) {
851
+ const pick = await rl.question(` Configure ${tool.name}? (Y/n): `);
852
+ if (pick.trim().toLowerCase() !== 'n' && pick.trim().toLowerCase() !== 'no') {
853
+ selectedTools.push(tool);
854
+ }
855
+ }
764
856
  }
765
- else if (result.action === 'merged') {
766
- summary.push({ file: tool.mcpPath, action: `Added totem to mcpServers` });
857
+ else {
858
+ // 'all' or Enter (default)
859
+ selectedTools = detectedTools;
767
860
  }
768
- }
769
- // --- Reflex injection & upgrade for selected tools ---
770
- const outdatedFiles = [];
771
- for (const tool of selectedTools) {
772
- if (!tool.reflexFile)
773
- continue;
774
- const filePath = path.join(cwd, tool.reflexFile);
775
- try {
776
- const result = injectReflexes(filePath);
777
- if (result === 'injected') {
778
- summary.push({ file: tool.reflexFile, action: 'Injected memory reflexes (v2)' });
861
+ // --- MCP scaffolding for selected tools ---
862
+ for (const tool of selectedTools) {
863
+ if (!tool.mcpPath || !tool.serverEntry)
864
+ continue;
865
+ const filePath = path.join(cwd, tool.mcpPath);
866
+ const result = scaffoldMcpConfig(filePath, tool.serverEntry);
867
+ if (result.err) {
868
+ log.error('Totem Error', result.err); // totem-ignore — result.err is internal scaffolding error, not LLM output
869
+ console.error(`To fix this, add the following manually to your ${tool.mcpPath} under "mcpServers":\n`);
870
+ console.error(` "totem": ${JSON.stringify(tool.serverEntry, null, 2)}\n`);
871
+ }
872
+ else if (result.action === 'created') {
873
+ summary.push({ file: tool.mcpPath, action: `Created with Totem MCP server` });
779
874
  }
780
- else if (result === 'outdated') {
781
- outdatedFiles.push({ tool, filePath });
875
+ else if (result.action === 'merged') {
876
+ summary.push({ file: tool.mcpPath, action: `Added totem to mcpServers` });
782
877
  }
783
878
  }
784
- catch (err) {
785
- const message = err instanceof Error ? err.message : String(err);
786
- log.error('Totem Error', `Failed to inject reflexes into ${tool.reflexFile}: ${message}`);
787
- }
788
- }
789
- // Prompt once for all outdated reflex files
790
- if (outdatedFiles.length > 0) {
791
- const fileList = outdatedFiles.map((f) => f.tool.reflexFile).join(', ');
792
- log.warn('Totem', `Outdated reflexes found in: ${bold(fileList)}`);
793
- let shouldUpgrade = false;
794
- if (process.stdin.isTTY) {
795
- const answer = await rl.question(`Upgrade reflexes to v${REFLEX_VERSION}? (Y/n): `);
796
- shouldUpgrade =
797
- answer.trim().toLowerCase() !== 'n' && answer.trim().toLowerCase() !== 'no';
798
- }
799
- else {
800
- // Non-TTY (CI/scripted): auto-upgrade to match baseline lessons behavior
801
- shouldUpgrade = true;
802
- log.info('Totem', 'Non-interactive mode — auto-upgrading reflexes.');
879
+ // --- Reflex injection & upgrade for selected tools ---
880
+ const outdatedFiles = [];
881
+ for (const tool of selectedTools) {
882
+ if (!tool.reflexFile)
883
+ continue;
884
+ const filePath = path.join(cwd, tool.reflexFile);
885
+ try {
886
+ const result = injectReflexes(filePath);
887
+ if (result === 'injected') {
888
+ summary.push({ file: tool.reflexFile, action: 'Injected memory reflexes (v2)' });
889
+ }
890
+ else if (result === 'outdated') {
891
+ outdatedFiles.push({ tool, filePath });
892
+ }
893
+ }
894
+ catch (err) {
895
+ const message = err instanceof Error ? err.message : String(err);
896
+ log.error('Totem Error', `Failed to inject reflexes into ${tool.reflexFile}: ${message}`);
897
+ }
803
898
  }
804
- if (shouldUpgrade) {
805
- for (const { tool, filePath } of outdatedFiles) {
806
- try {
807
- const clean = applyReflexUpgrade(filePath);
808
- if (clean) {
809
- summary.push({
810
- file: tool.reflexFile,
811
- action: `Upgraded reflexes to v${REFLEX_VERSION}`,
812
- });
899
+ // Prompt once for all outdated reflex files
900
+ if (outdatedFiles.length > 0) {
901
+ const fileList = outdatedFiles.map((f) => f.tool.reflexFile).join(', ');
902
+ log.warn('Totem', `Outdated reflexes found in: ${bold(fileList)}`);
903
+ let shouldUpgrade = false;
904
+ if (process.stdin.isTTY) {
905
+ const answer = await rl.question(`Upgrade reflexes to v${REFLEX_VERSION}? (Y/n): `);
906
+ shouldUpgrade =
907
+ answer.trim().toLowerCase() !== 'n' && answer.trim().toLowerCase() !== 'no';
908
+ }
909
+ else {
910
+ // Non-TTY (CI/scripted): auto-upgrade to match baseline lessons behavior
911
+ shouldUpgrade = true;
912
+ log.info('Totem', 'Non-interactive mode — auto-upgrading reflexes.');
913
+ }
914
+ if (shouldUpgrade) {
915
+ for (const { tool, filePath } of outdatedFiles) {
916
+ try {
917
+ const clean = applyReflexUpgrade(filePath);
918
+ if (clean) {
919
+ summary.push({
920
+ file: tool.reflexFile,
921
+ action: `Upgraded reflexes to v${REFLEX_VERSION}`,
922
+ });
923
+ }
924
+ else {
925
+ summary.push({
926
+ file: tool.reflexFile,
927
+ action: `Appended v${REFLEX_VERSION} reflexes (manual cleanup needed — remove old block)`,
928
+ });
929
+ log.warn('Totem', `Could not cleanly replace old reflexes in ${tool.reflexFile}. New block appended — please remove the old one manually.`);
930
+ }
813
931
  }
814
- else {
815
- summary.push({
816
- file: tool.reflexFile,
817
- action: `Appended v${REFLEX_VERSION} reflexes (manual cleanup needed — remove old block)`,
818
- });
819
- log.warn('Totem', `Could not cleanly replace old reflexes in ${tool.reflexFile}. New block appended — please remove the old one manually.`);
932
+ catch (err) {
933
+ const message = err instanceof Error ? err.message : String(err);
934
+ log.error('Totem Error', `Failed to upgrade reflexes in ${tool.reflexFile}: ${message}`);
820
935
  }
821
936
  }
822
- catch (err) {
823
- const message = err instanceof Error ? err.message : String(err);
824
- log.error('Totem Error', `Failed to upgrade reflexes in ${tool.reflexFile}: ${message}`);
937
+ }
938
+ else {
939
+ for (const { tool } of outdatedFiles) {
940
+ summary.push({
941
+ file: tool.reflexFile,
942
+ action: 'Outdated reflexes — upgrade declined',
943
+ });
825
944
  }
826
945
  }
827
946
  }
828
- else {
829
- for (const { tool } of outdatedFiles) {
830
- summary.push({
831
- file: tool.reflexFile,
832
- action: 'Outdated reflexes upgrade declined',
833
- });
947
+ // --- Hook installation for selected tools ---
948
+ for (const tool of selectedTools) {
949
+ if (!tool.hookInstaller)
950
+ continue;
951
+ const results = await tool.hookInstaller(cwd);
952
+ for (const result of results) {
953
+ if (result.err) {
954
+ log.error('Totem Error', `Hook scaffolding failed for ${result.file}: ${result.err}`); // totem-ignore — internal hook installer error
955
+ }
956
+ else if (result.action === 'created') {
957
+ summary.push({ file: result.file, action: `Scaffolded ${tool.name} hook` });
958
+ }
959
+ else if (result.action === 'merged') {
960
+ summary.push({
961
+ file: result.file,
962
+ action: `Merged ${tool.name} hook into existing config`,
963
+ });
964
+ }
834
965
  }
835
966
  }
836
967
  }
837
- // --- Hook installation for selected tools ---
838
- for (const tool of selectedTools) {
839
- if (!tool.hookInstaller)
840
- continue;
841
- const results = await tool.hookInstaller(cwd);
842
- for (const result of results) {
843
- if (result.err) {
844
- log.error('Totem Error', `Hook scaffolding failed for ${result.file}: ${result.err}`); // totem-ignore — internal hook installer error
845
- }
846
- else if (result.action === 'created') {
847
- summary.push({ file: result.file, action: `Scaffolded ${tool.name} hook` });
848
- }
849
- else if (result.action === 'merged') {
850
- summary.push({
851
- file: result.file,
852
- action: `Merged ${tool.name} hook into existing config`,
853
- });
854
- }
855
- }
968
+ // --- Always run: enforcement hooks (pre-commit + pre-push) ---
969
+ const enforcement = await installEnforcementHooks(cwd, rl);
970
+ if (enforcement.preCommit === 'installed' || enforcement.preCommit === 'appended') {
971
+ summary.push({
972
+ file: '.git/hooks/pre-commit',
973
+ action: `${enforcement.preCommit === 'installed' ? 'Installed' : 'Appended'} main-branch protection`,
974
+ });
856
975
  }
857
- }
858
- // --- Always run: enforcement hooks (pre-commit + pre-push) ---
859
- const enforcement = await installEnforcementHooks(cwd, rl);
860
- if (enforcement.preCommit === 'installed' || enforcement.preCommit === 'appended') {
861
- summary.push({
862
- file: '.git/hooks/pre-commit',
863
- action: `${enforcement.preCommit === 'installed' ? 'Installed' : 'Appended'} main-branch protection`,
864
- });
865
- }
866
- else if (enforcement.preCommit === 'skipped-non-shell') {
867
- summary.push({
868
- file: '.git/hooks/pre-commit',
869
- action: 'Skipped — non-shell hook detected (manual integration needed)',
870
- });
871
- }
872
- if (enforcement.prePush === 'installed' || enforcement.prePush === 'appended') {
873
- summary.push({
874
- file: '.git/hooks/pre-push',
875
- action: `${enforcement.prePush === 'installed' ? 'Installed' : 'Appended'} deterministic shield gate`,
876
- });
877
- }
878
- else if (enforcement.prePush === 'skipped-non-shell') {
879
- summary.push({
880
- file: '.git/hooks/pre-push',
881
- action: 'Skipped — non-shell hook detected (manual integration needed)',
882
- });
883
- }
884
- // --- Always run: post-merge git hook ---
885
- await installPostMergeHook(cwd, rl);
886
- // --- Always run: .gitignore ---
887
- const gitignorePath = path.join(cwd, '.gitignore');
888
- if (fs.existsSync(gitignorePath)) {
889
- const gitignore = fs.readFileSync(gitignorePath, 'utf-8');
890
- if (!gitignore.includes('.lancedb')) {
891
- fs.appendFileSync(gitignorePath, '\n# Totem\n.lancedb/\n');
892
- summary.push({ file: '.gitignore', action: 'Added .lancedb/ exclusion' });
976
+ else if (enforcement.preCommit === 'skipped-non-shell') {
977
+ summary.push({
978
+ file: '.git/hooks/pre-commit',
979
+ action: 'Skipped non-shell hook detected (manual integration needed)',
980
+ });
893
981
  }
894
- }
895
- // --- Auto-ingest cursor rules (ADR-048) ---
896
- const { scanCursorInstructions } = await import('@mmnto/totem');
897
- const cursorInstructions = scanCursorInstructions(cwd);
898
- if (cursorInstructions.length > 0) {
899
- const answer = await rl.question(`\nFound ${cursorInstructions.length} existing AI rule(s) (.cursorrules / .mdc). Compile into deterministic invariants? (Y/n): `);
900
- if (answer.trim().toLowerCase() !== 'n' && answer.trim().toLowerCase() !== 'no') {
901
- try {
902
- const { compileCommand } = await import('./compile.js');
903
- await compileCommand({ fromCursor: true });
904
- summary.push({
905
- file: '.totem/compiled-rules.json',
906
- action: `Compiled ${cursorInstructions.length} cursor rule(s) into invariants`,
907
- });
982
+ if (enforcement.prePush === 'installed' || enforcement.prePush === 'appended') {
983
+ summary.push({
984
+ file: '.git/hooks/pre-push',
985
+ action: `${enforcement.prePush === 'installed' ? 'Installed' : 'Appended'} deterministic shield gate`,
986
+ });
987
+ }
988
+ else if (enforcement.prePush === 'skipped-non-shell') {
989
+ summary.push({
990
+ file: '.git/hooks/pre-push',
991
+ action: 'Skipped — non-shell hook detected (manual integration needed)',
992
+ });
993
+ }
994
+ // --- Always run: post-merge git hook ---
995
+ await installPostMergeHook(cwd, rl);
996
+ // --- Always run: .gitignore ---
997
+ const gitignorePath = path.join(cwd, '.gitignore');
998
+ if (fs.existsSync(gitignorePath)) {
999
+ const gitignore = fs.readFileSync(gitignorePath, 'utf-8');
1000
+ if (!gitignore.includes('.lancedb')) {
1001
+ fs.appendFileSync(gitignorePath, '\n# Totem\n.lancedb/\n');
1002
+ summary.push({ file: '.gitignore', action: 'Added .lancedb/ exclusion' });
908
1003
  }
909
- catch (err) {
910
- const detail = err instanceof Error ? err.message : String(err);
911
- log.warn('Totem', `Could not compile cursor rules: ${detail}`);
1004
+ }
1005
+ // --- Auto-ingest cursor rules (ADR-048) ---
1006
+ const { scanCursorInstructions } = await import('@mmnto/totem');
1007
+ const cursorInstructions = scanCursorInstructions(cwd);
1008
+ if (cursorInstructions.length > 0) {
1009
+ const answer = await rl.question(`\nFound ${cursorInstructions.length} existing AI rule(s) (.cursorrules / .mdc). Compile into deterministic invariants? (Y/n): `);
1010
+ if (answer.trim().toLowerCase() !== 'n' && answer.trim().toLowerCase() !== 'no') {
1011
+ try {
1012
+ const { compileCommand } = await import('./compile.js');
1013
+ await compileCommand({ fromCursor: true });
1014
+ summary.push({
1015
+ file: '.totem/compiled-rules.json',
1016
+ action: `Compiled ${cursorInstructions.length} cursor rule(s) into invariants`,
1017
+ });
1018
+ }
1019
+ catch (err) {
1020
+ const detail = err instanceof Error ? err.message : String(err);
1021
+ log.warn('Totem', `Could not compile cursor rules: ${detail}`);
1022
+ }
912
1023
  }
913
1024
  }
914
- }
1025
+ } // end of bare mode else block
915
1026
  // --- Print summary ---
916
1027
  if (summary.length > 0) {
917
1028
  console.error(`\n${brand('--- Totem Init Summary ---')}`);