@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.
- package/dist/adapters/gh-utils.d.ts.map +1 -1
- package/dist/adapters/gh-utils.js +6 -5
- package/dist/adapters/gh-utils.js.map +1 -1
- package/dist/adapters/gh-utils.test.js +2 -0
- package/dist/adapters/gh-utils.test.js.map +1 -1
- package/dist/adapters/github-cli-pr.test.js +2 -0
- package/dist/adapters/github-cli-pr.test.js.map +1 -1
- package/dist/adapters/github-cli.test.js +4 -2
- package/dist/adapters/github-cli.test.js.map +1 -1
- package/dist/adapters/multi-repo-adapter.d.ts.map +1 -1
- package/dist/adapters/multi-repo-adapter.js +4 -4
- package/dist/adapters/multi-repo-adapter.js.map +1 -1
- package/dist/assets/compiled-baseline.d.ts +3 -0
- package/dist/assets/compiled-baseline.d.ts.map +1 -0
- package/dist/assets/compiled-baseline.js +243 -0
- package/dist/assets/compiled-baseline.js.map +1 -0
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/audit.js +25 -13
- package/dist/commands/audit.js.map +1 -1
- package/dist/commands/compile.d.ts.map +1 -1
- package/dist/commands/compile.js +32 -0
- package/dist/commands/compile.js.map +1 -1
- package/dist/commands/docs.d.ts +1 -1
- package/dist/commands/docs.d.ts.map +1 -1
- package/dist/commands/docs.js +31 -26
- package/dist/commands/docs.js.map +1 -1
- package/dist/commands/docs.test.js +3 -3
- package/dist/commands/docs.test.js.map +1 -1
- package/dist/commands/explain.d.ts +2 -0
- package/dist/commands/explain.d.ts.map +1 -0
- package/dist/commands/explain.js +98 -0
- package/dist/commands/explain.js.map +1 -0
- package/dist/commands/extract.d.ts.map +1 -1
- package/dist/commands/extract.js +6 -2
- package/dist/commands/extract.js.map +1 -1
- package/dist/commands/init.d.ts +4 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +346 -235
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/init.test.js +30 -13
- package/dist/commands/init.test.js.map +1 -1
- package/dist/commands/run-compiled-rules.d.ts.map +1 -1
- package/dist/commands/run-compiled-rules.js +14 -6
- package/dist/commands/run-compiled-rules.js.map +1 -1
- package/dist/commands/spec.d.ts +1 -1
- package/dist/commands/spec.d.ts.map +1 -1
- package/dist/commands/spec.js +38 -8
- package/dist/commands/spec.js.map +1 -1
- package/dist/commands/triage.js +1 -1
- package/dist/git.js +1 -1
- package/dist/git.js.map +1 -1
- package/dist/git.test.js +2 -0
- package/dist/git.test.js.map +1 -1
- package/dist/index.js +38 -2
- package/dist/index.js.map +1 -1
- package/dist/orchestrators/conformance.test.js +1 -0
- package/dist/orchestrators/conformance.test.js.map +1 -1
- package/dist/orchestrators/ollama-orchestrator.d.ts.map +1 -1
- package/dist/orchestrators/ollama-orchestrator.js +3 -0
- package/dist/orchestrators/ollama-orchestrator.js.map +1 -1
- package/dist/orchestrators/ollama-orchestrator.test.js +2 -2
- package/dist/orchestrators/ollama-orchestrator.test.js.map +1 -1
- package/dist/orchestrators/shell-orchestrator.test.js +1 -0
- package/dist/orchestrators/shell-orchestrator.test.js.map +1 -1
- package/package.json +2 -2
package/dist/commands/init.js
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
576
|
+
if (hasKey(envContent, 'GEMINI_API_KEY', 'GOOGLE_API_KEY'))
|
|
500
577
|
return 'gemini';
|
|
501
578
|
// OpenAI — widely available, low friction
|
|
502
|
-
if (
|
|
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
|
-
|
|
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 (
|
|
650
|
-
log.info('Totem', `
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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
|
-
|
|
657
|
-
const
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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
|
|
664
|
-
|
|
665
|
-
|
|
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
|
-
|
|
669
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
// ---
|
|
728
|
-
const
|
|
729
|
-
if (
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
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
|
-
|
|
739
|
-
|
|
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
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
const
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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 (
|
|
763
|
-
|
|
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
|
|
766
|
-
|
|
857
|
+
else {
|
|
858
|
+
// 'all' or Enter (default)
|
|
859
|
+
selectedTools = detectedTools;
|
|
767
860
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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 === '
|
|
781
|
-
|
|
875
|
+
else if (result.action === 'merged') {
|
|
876
|
+
summary.push({ file: tool.mcpPath, action: `Added totem to mcpServers` });
|
|
782
877
|
}
|
|
783
878
|
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
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
|
-
// ---
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
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
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
if (
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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
|
-
|
|
910
|
-
|
|
911
|
-
|
|
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 ---')}`);
|