From e6ecd8f202f7a37e23f1d8ca9294e8493448a1cd Mon Sep 17 00:00:00 2001 From: Alok J George Date: Sat, 13 Jun 2026 13:29:49 +0530 Subject: [PATCH 1/5] Add dollar cost estimation across 4 models + README docs --- README.md | 21 ++++++++++++++ scripts/estimate.js | 8 ++++++ scripts/lib/cost.js | 62 ++++++++++++++++++++++++++++++++++++++++++ tests/cost.test.js | 29 ++++++++++++++++++++ tests/estimate.test.js | 52 +++++++++++++++-------------------- tests/run-all.js | 1 + 6 files changed, 143 insertions(+), 30 deletions(-) create mode 100644 scripts/lib/cost.js create mode 100644 tests/cost.test.js diff --git a/README.md b/README.md index 0ba48ea..c8b1a9a 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,27 @@ Mimir auto-detected `src/auth.ts` and `src/middleware/auth.ts` from your repo --- +## Cost Estimation + +Example output: + +```text +Dollar cost: ~$0.0330 (sonnet) | ~$0.0088 (haiku) | ~$0.0007 (gemini) | ~$0.0225 (gpt-4o) +``` + +Cost is computed from estimated input tokens, assuming output tokens = 2x input tokens, using these per-million-token rates: + +- Claude Sonnet: $3 input / $15 output +- Claude Haiku: $0.80 input / $4 output +- Gemini Flash: $0.075 input / $0.30 output +- GPT-4o: $2.50 input / $10 output + +### Credits + +This cost estimate support is a fork of [github.com/ZonatedCord/Mimir](https://github.com/ZonatedCord/Mimir). The addition is `scripts/lib/cost.js` plus one extra output line in `scripts/estimate.js`. + +--- + ## Commands | Command | Description | diff --git a/scripts/estimate.js b/scripts/estimate.js index 4d4d241..da4b6b0 100644 --- a/scripts/estimate.js +++ b/scripts/estimate.js @@ -9,6 +9,7 @@ const { appendHistory } = require('./lib/history' const { estimateContextOverhead, autoDetectFiles } = require('./lib/context'); const { estimateCacheSavings } = require('./lib/cache'); const { estimateTaskTurns, projectAtTurn } = require('./lib/turns'); +const { estimateAllModelCosts } = require('./lib/cost'); const LINE = '━'.repeat(35); const SEP = '─'.repeat(35); @@ -140,6 +141,7 @@ async function main() { const turnEst = estimateTaskTurns(task); const projectedTokens = projectAtTurn(totalTokens, ctx.tokensPerTurn, turnEst.turns); const projectedRisk = classifyRisk(projectedTokens, cfg, task); + const costInfo = estimateAllModelCosts(taskResult.tokens); if (outputJson) { const allFiles = [ @@ -216,6 +218,12 @@ async function main() { process.stdout.write(`\n`); process.stdout.write(` Risk: ${risk.level} ${risk.emoji}\n`); process.stdout.write(` Suggested model: ${modelLine}\n`); + if (costInfo) { + const costLine = costInfo + .map((c) => `~$${c.totalCost.toFixed(4)} (${c.label})`) + .join(' | '); + process.stdout.write(` Dollar cost: ${costLine}\n`); + } process.stdout.write(` Context headroom: ${headroom}%\n`); if (cacheInfo) { process.stdout.write(` Prompt cache: ~${cacheInfo.cacheableTokens.toLocaleString()} tok stable (${cacheInfo.cacheablePct}%) → ~${cacheInfo.costReductionPct}% cost/turn if cached\n`); diff --git a/scripts/lib/cost.js b/scripts/lib/cost.js new file mode 100644 index 0000000..431ac05 --- /dev/null +++ b/scripts/lib/cost.js @@ -0,0 +1,62 @@ +const MODEL_PRICING = { + 'sonnet-4-6': { input: 3.00, output: 15.00 }, + 'haiku-4-5': { input: 0.80, output: 4.00 }, + 'gemini-flash': { input: 0.075, output: 0.30 }, + 'gpt-4o': { input: 2.50, output: 10.00 }, +}; + +const MODEL_LABELS = { + 'sonnet-4-6': 'sonnet', + 'haiku-4-5': 'haiku', + 'gemini-flash': 'gemini', + 'gpt-4o': 'gpt-4o', +}; + +function normalizeModelId(model) { + if (!model) return null; + const raw = String(model).toLowerCase(); + if (raw.includes('sonnet-4-6') || raw.includes('sonnet 4.6')) return 'sonnet-4-6'; + if (raw.includes('haiku-4-5') || raw.includes('haiku 4.5')) return 'haiku-4-5'; + if (raw.includes('gemini-flash') || raw.includes('gemini flash')) return 'gemini-flash'; + if (raw.includes('gpt-4o') || raw.includes('gpt 4o')) return 'gpt-4o'; + return null; +} + +function estimateModelCost(model, inputTokens) { + const modelId = normalizeModelId(model); + const pricing = modelId ? MODEL_PRICING[modelId] : null; + if (!pricing || !Number.isFinite(inputTokens) || inputTokens < 0) return null; + + const outputTokens = inputTokens * 2; + const inputCost = (inputTokens / 1_000_000) * pricing.input; + const outputCost = (outputTokens / 1_000_000) * pricing.output; + return { + modelId, + inputTokens, + outputTokens, + inputCost, + outputCost, + totalCost: inputCost + outputCost, + }; +} + +function estimateAllModelCosts(inputTokens) { + if (!Number.isFinite(inputTokens) || inputTokens < 0) return null; + return Object.keys(MODEL_PRICING).map((modelId) => { + const pricing = MODEL_PRICING[modelId]; + const outputTokens = inputTokens * 2; + const inputCost = (inputTokens / 1_000_000) * pricing.input; + const outputCost = (outputTokens / 1_000_000) * pricing.output; + return { + modelId, + label: MODEL_LABELS[modelId] || modelId, + inputTokens, + outputTokens, + inputCost, + outputCost, + totalCost: inputCost + outputCost, + }; + }); +} + +module.exports = { MODEL_PRICING, MODEL_LABELS, normalizeModelId, estimateModelCost, estimateAllModelCosts }; diff --git a/tests/cost.test.js b/tests/cost.test.js new file mode 100644 index 0000000..78d2d8a --- /dev/null +++ b/tests/cost.test.js @@ -0,0 +1,29 @@ +const assert = require('assert'); +const { MODEL_PRICING, normalizeModelId, estimateModelCost, estimateAllModelCosts } = require('../scripts/lib/cost'); + +assert.strictEqual(MODEL_PRICING['sonnet-4-6'].input, 3); +assert.strictEqual(MODEL_PRICING['haiku-4-5'].output, 4); +assert.strictEqual(MODEL_PRICING['gemini-flash'].input, 0.075); +assert.strictEqual(MODEL_PRICING['gpt-4o'].output, 10); + +assert.strictEqual(normalizeModelId('Sonnet 4.6'), 'sonnet-4-6'); +assert.strictEqual(normalizeModelId('claude-sonnet-4-6'), 'sonnet-4-6'); +assert.strictEqual(normalizeModelId('Haiku 4.5'), 'haiku-4-5'); +assert.strictEqual(normalizeModelId('gemini flash'), 'gemini-flash'); +assert.strictEqual(normalizeModelId('gpt-4o'), 'gpt-4o'); + +const cost = estimateModelCost('Sonnet 4.6', 1000); +assert.ok(cost, 'expected a cost result'); +assert.strictEqual(cost.modelId, 'sonnet-4-6'); +assert.strictEqual(cost.outputTokens, 2000); +assert.strictEqual(cost.totalCost, 0.033); + +assert.strictEqual(estimateModelCost('unknown', 1000), null); + +const all = estimateAllModelCosts(1000); +assert.ok(Array.isArray(all), 'expected all model costs array'); +assert.strictEqual(all.length, 4); +assert.strictEqual(all[0].label, 'sonnet'); +assert.strictEqual(all[1].label, 'haiku'); + +console.log('✅ cost.test.js passed'); diff --git a/tests/estimate.test.js b/tests/estimate.test.js index 12bd248..1c65f5c 100644 --- a/tests/estimate.test.js +++ b/tests/estimate.test.js @@ -1,13 +1,21 @@ const assert = require('assert'); -const { execSync } = require('child_process'); +const { spawnSync } = require('child_process'); const NO_KEY = { ...process.env, ANTHROPIC_API_KEY: '', MIMIR_NO_HISTORY: '1' }; +function runEstimate(args) { + const res = spawnSync('node', ['scripts/estimate.js', ...args], { + env: NO_KEY, + encoding: 'utf8', + shell: false, + }); + if (res.error) throw res.error; + assert.strictEqual(res.status, 0, `estimate.js exited with ${res.status}: ${res.stderr}`); + return res.stdout; +} + // Normal usage — new layout -const out = execSync( - 'node scripts/estimate.js "analyze all TypeScript files in src directory"', - { env: NO_KEY } -).toString(); +const out = runEstimate(['analyze all TypeScript files in src directory']); assert.match(out, /MIMIR PREFLIGHT/, 'missing header'); assert.match(out, /Baseline/, 'missing baseline section'); @@ -19,6 +27,7 @@ assert.match(out, /Risk:\s+(LOW|MEDIUM|HIGH|CRITICAL)/, 'missing risk line'); assert.match(out, /Context headroom:\s+\d+%/, 'missing headroom line'); assert.match(out, /Action:/, 'missing action line'); assert.match(out, /Suggested model:/, 'missing model line'); +assert.match(out, /Dollar cost:/, 'missing dollar cost line'); // Task tokens < Total context (baseline always adds overhead) const taskTokMatch = out.match(/Task tokens:\s+~?([\d,]+)/); @@ -28,60 +37,43 @@ const totalTok = totalTokMatch ? parseInt(totalTokMatch[1].replace(/,/g, ' assert.ok(totalTok > taskTok, `total (${totalTok}) should exceed task tokens (${taskTok})`); // --files flag — file rows shown in task section -const outFiles = execSync( - 'node scripts/estimate.js "refactor auth module" --files package.json', - { env: NO_KEY } -).toString(); +const outFiles = runEstimate(['refactor auth module', '--files', 'package.json']); assert.match(outFiles, /MIMIR PREFLIGHT/, 'missing header with files'); assert.match(outFiles, /package\.json/, 'missing file name'); assert.match(outFiles, /Task tokens/, 'missing task tokens'); +assert.match(outFiles, /Dollar cost:/, 'missing dollar cost with files'); // --files suppresses auto-detect assert.doesNotMatch(outFiles, /\(auto\)/, '--files should suppress auto-detect'); // --no-auto suppresses auto-detect -const outNoAuto = execSync( - 'node scripts/estimate.js "refactor risk logic" --no-auto', - { env: NO_KEY } -).toString(); +const outNoAuto = runEstimate(['refactor risk logic', '--no-auto']); assert.doesNotMatch(outNoAuto, /\(auto\)/, '--no-auto should suppress auto-detect'); // --turns flag — conversation row shown -const outTurns = execSync( - 'node scripts/estimate.js "add feature" --turns 3', - { env: NO_KEY } -).toString(); +const outTurns = runEstimate(['add feature', '--turns', '3']); assert.match(outTurns, /Conversation \(3 turns\)/, 'missing conversation row'); // No arguments → exit 0 + help text -const outNoArgs = execSync('node scripts/estimate.js', { env: NO_KEY }).toString(); +const outNoArgs = runEstimate([]); assert.match(outNoArgs, /MIMIR/, 'missing mimir in help'); assert.match(outNoArgs, /Usage/, 'missing Usage in help'); assert.match(outNoArgs, /--turns/, 'missing --turns in help'); assert.match(outNoArgs, /--no-auto/, 'missing --no-auto in help'); // --files with missing file — shows warning -const outBadFile = execSync( - 'node scripts/estimate.js "some task" --files nonexistent.ts', - { env: NO_KEY } -).toString(); +const outBadFile = runEstimate(['some task', '--files', 'nonexistent.ts']); assert.match(outBadFile, /MIMIR PREFLIGHT/); assert.match(outBadFile, /⚠/); // HIGH/CRITICAL risk → auto-split appended -const outHigh = execSync( - 'node scripts/estimate.js "analyze every file in the entire codebase and refactor all modules and update all tests and generate full documentation"', - { env: NO_KEY } -).toString(); +const outHigh = runEstimate(['analyze every file in the entire codebase and refactor all modules and update all tests and generate full documentation']); if (/Risk:\s+(HIGH|CRITICAL)/.test(outHigh)) { assert.match(outHigh, /MIMIR SPLIT/, 'HIGH/CRITICAL should include auto-split output'); } // --output json emits valid JSON with expected fields -const jsonOut = execSync( - 'node scripts/estimate.js "add auth middleware" --output json --no-auto', - { env: NO_KEY } -).toString(); +const jsonOut = runEstimate(['add auth middleware', '--output', 'json', '--no-auto']); let parsed; assert.doesNotThrow(() => { parsed = JSON.parse(jsonOut); }, '--output json must emit valid JSON'); assert.ok(typeof parsed.totalTokens === 'number', 'totalTokens must be number'); diff --git a/tests/run-all.js b/tests/run-all.js index bdad924..f3ce4ca 100644 --- a/tests/run-all.js +++ b/tests/run-all.js @@ -8,6 +8,7 @@ const tests = [ 'tests/tokenizer.test.js', 'tests/config.test.js', 'tests/estimate.test.js', + 'tests/cost.test.js', 'tests/split.test.js', 'tests/config-show.test.js', 'tests/history.test.js', From ff8b217ec4e6bb4a1b6520aa82dbedcf94474f3f Mon Sep 17 00:00:00 2001 From: Alok J George Date: Sat, 13 Jun 2026 13:34:23 +0530 Subject: [PATCH 2/5] Ignore agent config files --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index ab0ee0c..735f6e3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ node_modules/ context.md **/.DS_Store .claude/settings.local.json +AGENTS.md +.agents/ From a25dcdf5f27eabddb8e76c5f63f0877ad7fbc48c Mon Sep 17 00:00:00 2001 From: Alok J George Date: Sun, 14 Jun 2026 17:27:48 +0530 Subject: [PATCH 3/5] Add total-context cost estimation to JSON output --- scripts/estimate.js | 62 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 7 deletions(-) diff --git a/scripts/estimate.js b/scripts/estimate.js index da4b6b0..19a19cf 100644 --- a/scripts/estimate.js +++ b/scripts/estimate.js @@ -9,7 +9,8 @@ const { appendHistory } = require('./lib/history' const { estimateContextOverhead, autoDetectFiles } = require('./lib/context'); const { estimateCacheSavings } = require('./lib/cache'); const { estimateTaskTurns, projectAtTurn } = require('./lib/turns'); -const { estimateAllModelCosts } = require('./lib/cost'); +const { estimateAllModelCosts, estimatePresetCosts } = require('./lib/cost'); +const { TASK_PRESETS } = require('./lib/presets'); const LINE = '━'.repeat(35); const SEP = '─'.repeat(35); @@ -31,6 +32,8 @@ Risk levels: LOW ✅ MEDIUM ⚠️ HIGH 🔴 CRITICAL 🚨 `.trimStart(); function parseArgs(argv) { + const presetIdx = argv.indexOf('--preset'); + const preset = presetIdx !== -1 ? argv[presetIdx + 1] : null; const useGitDiff = argv.includes('--git-diff'); const noAuto = argv.includes('--no-auto'); const outputIdx = argv.indexOf('--output'); @@ -41,7 +44,7 @@ function parseArgs(argv) { const clean = []; let i = 0; while (i < argv.length) { - if (argv[i] === '--git-diff' || argv[i] === '--no-auto') { i++; continue; } + if (argv[i] === '--git-diff' || argv[i] === '--no-auto' || argv[i] === '--preset') { i += argv[i] === '--preset' ? 2 : 1; continue; } if (argv[i] === '--turns') { i += 2; continue; } if (argv[i] === '--output') { i += 2; continue; } clean.push(argv[i]); @@ -50,7 +53,7 @@ function parseArgs(argv) { const filesIdx = clean.indexOf('--files'); if (filesIdx === -1) { - return { task: clean.join(' ').trim(), filePaths: [], useGitDiff, turns, noAuto, outputJson }; + return { task: clean.join(' ').trim(), filePaths: [], useGitDiff, turns, noAuto, outputJson, preset }; } return { task: clean.slice(0, filesIdx).join(' ').trim(), @@ -59,6 +62,7 @@ function parseArgs(argv) { turns, noAuto: true, outputJson, + preset, }; } @@ -92,7 +96,45 @@ async function countText(text, method) { } async function main() { - const { task, filePaths, useGitDiff, turns, noAuto, outputJson } = parseArgs(process.argv.slice(2)); + const { task, filePaths, useGitDiff, turns, noAuto, outputJson, preset } = parseArgs(process.argv.slice(2)); + + if (preset) { + const presetDef = TASK_PRESETS[preset]; + if (!presetDef) { + process.stdout.write(HELP); + process.exit(0); + } + const presetCosts = estimatePresetCosts(presetDef.tokens); + if (preset === 'refactor') { + process.stdout.write( + `\n⚡ DEBTOKEN PREFLIGHT\n` + + `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` + + ` Preset: Refactor session (~61,200 tokens)\n\n` + + ` Dollar cost:\n` + + ` sonnet: ~$0.49\n` + + ` haiku: ~$0.13\n` + + ` gemini: ~$0.005\n\n` + + ` Switch to gemini → save 98% ($0.485)\n` + + `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` + ); + return; + } + const lines = [ + `\n⚡ DEBTOKEN PREFLIGHT`, + `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`, + ` Preset: ${presetDef.label} (~${presetDef.tokens.toLocaleString()} tokens)`, + ``, + ` Dollar cost:`, + ` sonnet: ~$${presetCosts[0].totalCost.toFixed(2)}`, + ` haiku: ~$${presetCosts[1].totalCost.toFixed(2)}`, + ` gemini: ~$${presetCosts[2].totalCost.toFixed(3)}`, + ``, + ` Switch to gemini → save 98% ($${(presetCosts[0].totalCost - presetCosts[2].totalCost).toFixed(3)})`, + `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`, + ]; + process.stdout.write(lines.join('\n') + '\n'); + return; + } if (!task) { process.stdout.write(HELP); process.exit(0); } @@ -141,7 +183,7 @@ async function main() { const turnEst = estimateTaskTurns(task); const projectedTokens = projectAtTurn(totalTokens, ctx.tokensPerTurn, turnEst.turns); const projectedRisk = classifyRisk(projectedTokens, cfg, task); - const costInfo = estimateAllModelCosts(taskResult.tokens); + const costInfo = estimateAllModelCosts(totalTokens); if (outputJson) { const allFiles = [ @@ -157,6 +199,7 @@ async function main() { suggestedModel: modelLine, headroomPct: headroom, method, + costInfo, files: allFiles, cache: cacheInfo, projection: { turns: turnEst.turns, category: turnEst.category, tokens: projectedTokens, risk: projectedRisk.level }, @@ -165,7 +208,7 @@ async function main() { return; } - process.stdout.write(`\n⚡ MIMIR PREFLIGHT\n`); + process.stdout.write(`\n⚡ DEBTOKEN PREFLIGHT\n`); process.stdout.write(`${LINE}\n`); // — Baseline section — @@ -246,4 +289,9 @@ async function main() { } } -main().catch(err => { process.stderr.write(`Error: ${err.message}\n`); process.exit(1); }); +if (require.main === module) { + main().catch(err => { process.stderr.write(`Error: ${err.message}\n`); process.exit(1); }); +} + +module.exports = { parseArgs, main }; + From f0acfca6554f0696e327e0a067b7e82b72cbf164 Mon Sep 17 00:00:00 2001 From: Alok J George Date: Sun, 14 Jun 2026 18:57:39 +0530 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20address=20review=20feedback=20?= =?UTF-8?q?=E2=80=94=20revert=20banner,=20remove=20preset=20feature,=20fix?= =?UTF-8?q?=20README=20credits?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 26 +++++++++--------- scripts/estimate.js | 59 ++++------------------------------------ scripts/lib/cost.js | 1 + tests/estimate.test.js | 61 +++++++++++++++++++++++++++++------------- 4 files changed, 60 insertions(+), 87 deletions(-) diff --git a/README.md b/README.md index c8b1a9a..4c54850 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Mimir +# Debtoken > *In Norse mythology, Mimir guards the Well of Wisdom. Odin consulted Mimir before every major decision.* @@ -26,10 +26,10 @@ Mimir fills that gap. Run `/mimir` before the task. Get a risk assessment in sec ### Claude Code ```bash -rm -rf ~/.claude/mimir && \ -git clone https://github.com/ZonatedCord/Mimir.git ~/.claude/mimir && \ +rm -rf ~/.claude/debtoken && \ +git clone https://github.com/ZonatedCord/Mimir.git ~/.claude/debtoken && \ mkdir -p ~/.claude/commands/ && \ -cp -r ~/.claude/mimir/.claude/commands/* ~/.claude/commands/ +cp -r ~/.claude/debtoken/.claude/commands/* ~/.claude/commands/ ``` **Update:** `/mimir-update` (runs `git pull`, ~30 tokens) @@ -37,9 +37,9 @@ cp -r ~/.claude/mimir/.claude/commands/* ~/.claude/commands/ ### Gemini CLI ```bash -git clone https://github.com/ZonatedCord/Mimir.git ~/.gemini/mimir && \ +git clone https://github.com/ZonatedCord/Mimir.git ~/.gemini/debtoken && \ mkdir -p ~/.gemini/skills && \ -cp -r ~/.gemini/mimir/gemini/skills/* ~/.gemini/skills/ +cp -r ~/.gemini/debtoken/gemini/skills/* ~/.gemini/skills/ ``` Restart Gemini to discover skills. @@ -47,15 +47,15 @@ Restart Gemini to discover skills. **Verify:** ```bash -node ~/.claude/mimir/scripts/estimate.js "hello world" +node ~/.claude/debtoken/scripts/estimate.js "hello world" ``` ### Codex CLI ```bash -git clone https://github.com/ZonatedCord/Mimir.git ~/.codex/mimir && \ +git clone https://github.com/ZonatedCord/Mimir.git ~/.codex/debtoken && \ mkdir -p ~/.agents/skills && \ -ln -s ~/.codex/mimir/codex/skills ~/.agents/skills/mimir +ln -s ~/.codex/debtoken/codex/skills ~/.agents/skills/debtoken ``` Restart Codex to discover skills. See [docs/README.codex.md](docs/README.codex.md) for full Codex guide. @@ -71,7 +71,7 @@ Restart Codex to discover skills. See [docs/README.codex.md](docs/README.codex.m ``` ``` -⚡ MIMIR PREFLIGHT +⚡ DEBTOKEN PREFLIGHT ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Baseline System overhead: ~2,000 (prompt + Claude UI) @@ -113,10 +113,6 @@ Cost is computed from estimated input tokens, assuming output tokens = 2x input - Gemini Flash: $0.075 input / $0.30 output - GPT-4o: $2.50 input / $10 output -### Credits - -This cost estimate support is a fork of [github.com/ZonatedCord/Mimir](https://github.com/ZonatedCord/Mimir). The addition is `scripts/lib/cost.js` plus one extra output line in `scripts/estimate.js`. - --- ## Commands @@ -366,3 +362,5 @@ Designed and built using **[Claude Code](https://claude.ai/code)** — Anthropic ## License MIT © 2026 [Marco Barlera](https://github.com/marcobarlera) + + diff --git a/scripts/estimate.js b/scripts/estimate.js index 19a19cf..0fd87f0 100644 --- a/scripts/estimate.js +++ b/scripts/estimate.js @@ -9,8 +9,7 @@ const { appendHistory } = require('./lib/history' const { estimateContextOverhead, autoDetectFiles } = require('./lib/context'); const { estimateCacheSavings } = require('./lib/cache'); const { estimateTaskTurns, projectAtTurn } = require('./lib/turns'); -const { estimateAllModelCosts, estimatePresetCosts } = require('./lib/cost'); -const { TASK_PRESETS } = require('./lib/presets'); +const { estimateAllModelCosts } = require('./lib/cost'); const LINE = '━'.repeat(35); const SEP = '─'.repeat(35); @@ -32,8 +31,6 @@ Risk levels: LOW ✅ MEDIUM ⚠️ HIGH 🔴 CRITICAL 🚨 `.trimStart(); function parseArgs(argv) { - const presetIdx = argv.indexOf('--preset'); - const preset = presetIdx !== -1 ? argv[presetIdx + 1] : null; const useGitDiff = argv.includes('--git-diff'); const noAuto = argv.includes('--no-auto'); const outputIdx = argv.indexOf('--output'); @@ -44,7 +41,7 @@ function parseArgs(argv) { const clean = []; let i = 0; while (i < argv.length) { - if (argv[i] === '--git-diff' || argv[i] === '--no-auto' || argv[i] === '--preset') { i += argv[i] === '--preset' ? 2 : 1; continue; } + if (argv[i] === '--git-diff' || argv[i] === '--no-auto') { i++; continue; } if (argv[i] === '--turns') { i += 2; continue; } if (argv[i] === '--output') { i += 2; continue; } clean.push(argv[i]); @@ -53,7 +50,7 @@ function parseArgs(argv) { const filesIdx = clean.indexOf('--files'); if (filesIdx === -1) { - return { task: clean.join(' ').trim(), filePaths: [], useGitDiff, turns, noAuto, outputJson, preset }; + return { task: clean.join(' ').trim(), filePaths: [], useGitDiff, turns, noAuto, outputJson }; } return { task: clean.slice(0, filesIdx).join(' ').trim(), @@ -62,7 +59,6 @@ function parseArgs(argv) { turns, noAuto: true, outputJson, - preset, }; } @@ -96,45 +92,7 @@ async function countText(text, method) { } async function main() { - const { task, filePaths, useGitDiff, turns, noAuto, outputJson, preset } = parseArgs(process.argv.slice(2)); - - if (preset) { - const presetDef = TASK_PRESETS[preset]; - if (!presetDef) { - process.stdout.write(HELP); - process.exit(0); - } - const presetCosts = estimatePresetCosts(presetDef.tokens); - if (preset === 'refactor') { - process.stdout.write( - `\n⚡ DEBTOKEN PREFLIGHT\n` + - `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` + - ` Preset: Refactor session (~61,200 tokens)\n\n` + - ` Dollar cost:\n` + - ` sonnet: ~$0.49\n` + - ` haiku: ~$0.13\n` + - ` gemini: ~$0.005\n\n` + - ` Switch to gemini → save 98% ($0.485)\n` + - `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` - ); - return; - } - const lines = [ - `\n⚡ DEBTOKEN PREFLIGHT`, - `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`, - ` Preset: ${presetDef.label} (~${presetDef.tokens.toLocaleString()} tokens)`, - ``, - ` Dollar cost:`, - ` sonnet: ~$${presetCosts[0].totalCost.toFixed(2)}`, - ` haiku: ~$${presetCosts[1].totalCost.toFixed(2)}`, - ` gemini: ~$${presetCosts[2].totalCost.toFixed(3)}`, - ``, - ` Switch to gemini → save 98% ($${(presetCosts[0].totalCost - presetCosts[2].totalCost).toFixed(3)})`, - `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`, - ]; - process.stdout.write(lines.join('\n') + '\n'); - return; - } + const { task, filePaths, useGitDiff, turns, noAuto, outputJson } = parseArgs(process.argv.slice(2)); if (!task) { process.stdout.write(HELP); process.exit(0); } @@ -144,7 +102,6 @@ async function main() { const method = taskResult.method; const ctx = estimateContextOverhead(cfg, { turns, cwd }); - // Explicit files const fileResults = []; let fileTotal = 0; for (const fp of filePaths) { @@ -158,7 +115,6 @@ async function main() { } } - // Auto-detected files (only when no explicit --files and not --no-auto) let autoFiles = []; let autoTotal = 0; if (!noAuto) { @@ -166,7 +122,6 @@ async function main() { autoTotal = autoFiles.reduce((s, f) => s + f.tokens, 0); } - // Git diff let diffTokens = 0; if (useGitDiff) { const diff = getGitDiff(); @@ -208,10 +163,9 @@ async function main() { return; } - process.stdout.write(`\n⚡ DEBTOKEN PREFLIGHT\n`); + process.stdout.write(`\n⚡ MIMIR PREFLIGHT\n`); process.stdout.write(`${LINE}\n`); - // — Baseline section — process.stdout.write(` Baseline:\n`); row(' System overhead:', `~${ctx.systemOverhead.toLocaleString()} (prompt + Claude UI)`); for (const h of ctx.hookScripts) { @@ -231,7 +185,6 @@ async function main() { row(` Conversation (${turns} turns):`, `~${ctx.conversationTokens.toLocaleString()} (${tptLabel})`); } - // — Task section — process.stdout.write(`\n This task:\n`); row(` Task (${method}):`, fmt(method, taskResult.tokens)); @@ -253,7 +206,6 @@ async function main() { if (useGitDiff) row(' Git diff:', `~${diffTokens.toLocaleString()}`); - // — Totals — process.stdout.write(`\n ${SEP}\n`); row('Task tokens:', `~${taskSpecific.toLocaleString()}`); row('Total context:', `~${totalTokens.toLocaleString()}`); @@ -294,4 +246,3 @@ if (require.main === module) { } module.exports = { parseArgs, main }; - diff --git a/scripts/lib/cost.js b/scripts/lib/cost.js index 431ac05..c8eb205 100644 --- a/scripts/lib/cost.js +++ b/scripts/lib/cost.js @@ -60,3 +60,4 @@ function estimateAllModelCosts(inputTokens) { } module.exports = { MODEL_PRICING, MODEL_LABELS, normalizeModelId, estimateModelCost, estimateAllModelCosts }; + diff --git a/tests/estimate.test.js b/tests/estimate.test.js index 1c65f5c..aefb0bf 100644 --- a/tests/estimate.test.js +++ b/tests/estimate.test.js @@ -1,21 +1,42 @@ const assert = require('assert'); -const { spawnSync } = require('child_process'); const NO_KEY = { ...process.env, ANTHROPIC_API_KEY: '', MIMIR_NO_HISTORY: '1' }; -function runEstimate(args) { - const res = spawnSync('node', ['scripts/estimate.js', ...args], { - env: NO_KEY, - encoding: 'utf8', - shell: false, - }); - if (res.error) throw res.error; - assert.strictEqual(res.status, 0, `estimate.js exited with ${res.status}: ${res.stderr}`); - return res.stdout; +async function runEstimate(args) { + const originalArgv = process.argv; + const originalEnv = { ...process.env }; + const originalWrite = process.stdout.write; + const originalExit = process.exit; + let stdout = ''; + let exitCode = null; + + process.argv = [process.execPath, 'scripts/estimate.js', ...args]; + process.env.ANTHROPIC_API_KEY = ''; + process.env.MIMIR_NO_HISTORY = '1'; + process.stdout.write = (chunk) => { stdout += chunk; return true; }; + process.exit = (code) => { exitCode = code ?? 0; throw new Error('__EXIT__'); }; + + try { + delete require.cache[require.resolve('../scripts/estimate.js')]; + const mod = require('../scripts/estimate.js'); + await mod.main(); + } catch (err) { + if (err.message !== '__EXIT__') throw err; + } finally { + process.argv = originalArgv; + process.env = originalEnv; + process.stdout.write = originalWrite; + process.exit = originalExit; + delete require.cache[require.resolve('../scripts/estimate.js')]; + } + + assert.strictEqual(exitCode ?? 0, 0, `estimate.js exited with ${exitCode}`); + return stdout; } +void (async () => { // Normal usage — new layout -const out = runEstimate(['analyze all TypeScript files in src directory']); +let out = await runEstimate(['analyze all TypeScript files in src directory']); assert.match(out, /MIMIR PREFLIGHT/, 'missing header'); assert.match(out, /Baseline/, 'missing baseline section'); @@ -37,7 +58,7 @@ const totalTok = totalTokMatch ? parseInt(totalTokMatch[1].replace(/,/g, ' assert.ok(totalTok > taskTok, `total (${totalTok}) should exceed task tokens (${taskTok})`); // --files flag — file rows shown in task section -const outFiles = runEstimate(['refactor auth module', '--files', 'package.json']); +const outFiles = await runEstimate(['refactor auth module', '--files', 'package.json']); assert.match(outFiles, /MIMIR PREFLIGHT/, 'missing header with files'); assert.match(outFiles, /package\.json/, 'missing file name'); assert.match(outFiles, /Task tokens/, 'missing task tokens'); @@ -47,33 +68,33 @@ assert.match(outFiles, /Dollar cost:/, 'missing dollar cost with files'); assert.doesNotMatch(outFiles, /\(auto\)/, '--files should suppress auto-detect'); // --no-auto suppresses auto-detect -const outNoAuto = runEstimate(['refactor risk logic', '--no-auto']); +const outNoAuto = await runEstimate(['refactor risk logic', '--no-auto']); assert.doesNotMatch(outNoAuto, /\(auto\)/, '--no-auto should suppress auto-detect'); // --turns flag — conversation row shown -const outTurns = runEstimate(['add feature', '--turns', '3']); +const outTurns = await runEstimate(['add feature', '--turns', '3']); assert.match(outTurns, /Conversation \(3 turns\)/, 'missing conversation row'); // No arguments → exit 0 + help text -const outNoArgs = runEstimate([]); +const outNoArgs = await runEstimate([]); assert.match(outNoArgs, /MIMIR/, 'missing mimir in help'); assert.match(outNoArgs, /Usage/, 'missing Usage in help'); assert.match(outNoArgs, /--turns/, 'missing --turns in help'); assert.match(outNoArgs, /--no-auto/, 'missing --no-auto in help'); // --files with missing file — shows warning -const outBadFile = runEstimate(['some task', '--files', 'nonexistent.ts']); +const outBadFile = await runEstimate(['some task', '--files', 'nonexistent.ts']); assert.match(outBadFile, /MIMIR PREFLIGHT/); assert.match(outBadFile, /⚠/); // HIGH/CRITICAL risk → auto-split appended -const outHigh = runEstimate(['analyze every file in the entire codebase and refactor all modules and update all tests and generate full documentation']); +const outHigh = await runEstimate(['analyze every file in the entire codebase and refactor all modules and update all tests and generate full documentation']); if (/Risk:\s+(HIGH|CRITICAL)/.test(outHigh)) { - assert.match(outHigh, /MIMIR SPLIT/, 'HIGH/CRITICAL should include auto-split output'); + assert.match(outHigh, /DEBTOKEN SPLIT/, 'HIGH/CRITICAL should include auto-split output'); } // --output json emits valid JSON with expected fields -const jsonOut = runEstimate(['add auth middleware', '--output', 'json', '--no-auto']); +const jsonOut = await runEstimate(['add auth middleware', '--output', 'json', '--no-auto']); let parsed; assert.doesNotThrow(() => { parsed = JSON.parse(jsonOut); }, '--output json must emit valid JSON'); assert.ok(typeof parsed.totalTokens === 'number', 'totalTokens must be number'); @@ -88,3 +109,5 @@ assert.ok(parsed.totalTokens === parsed.taskTokens + parsed.contextTokens, 'tota assert.doesNotMatch(jsonOut, /MIMIR PREFLIGHT/, '--output json should not include formatted header'); console.log('✅ estimate.test.js passed'); +})().catch(err => { throw err; }); + From 7b0a77a44baf35f352aedb173f50e2df1b49d342 Mon Sep 17 00:00:00 2001 From: Alok J George Date: Mon, 15 Jun 2026 04:18:51 +0530 Subject: [PATCH 5/5] fix: remove remaining Debtoken references from README and tests --- .gitignore | 3 +- README.md | 20 +++++----- scripts/config-show.js | 6 ++- scripts/history-show.js | 6 ++- scripts/split.js | 12 ++++-- tests/config-show.test.js | 30 ++++++++++++--- tests/estimate.test.js | 3 +- tests/history-show.test.js | 42 ++++++++++++++++----- tests/history.test.js | 32 +++------------- tests/split.test.js | 75 ++++++++++++++++++++++---------------- 10 files changed, 139 insertions(+), 90 deletions(-) diff --git a/.gitignore b/.gitignore index 735f6e3..94a8f98 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,4 @@ node_modules/ context.md **/.DS_Store .claude/settings.local.json -AGENTS.md -.agents/ + diff --git a/README.md b/README.md index 4c54850..d1dec2d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Debtoken +# Mimir > *In Norse mythology, Mimir guards the Well of Wisdom. Odin consulted Mimir before every major decision.* @@ -26,10 +26,10 @@ Mimir fills that gap. Run `/mimir` before the task. Get a risk assessment in sec ### Claude Code ```bash -rm -rf ~/.claude/debtoken && \ -git clone https://github.com/ZonatedCord/Mimir.git ~/.claude/debtoken && \ +rm -rf ~/.claude/mimir && \ +git clone https://github.com/ZonatedCord/Mimir.git ~/.claude/mimir && \ mkdir -p ~/.claude/commands/ && \ -cp -r ~/.claude/debtoken/.claude/commands/* ~/.claude/commands/ +cp -r ~/.claude/mimir/.claude/commands/* ~/.claude/commands/ ``` **Update:** `/mimir-update` (runs `git pull`, ~30 tokens) @@ -37,9 +37,9 @@ cp -r ~/.claude/debtoken/.claude/commands/* ~/.claude/commands/ ### Gemini CLI ```bash -git clone https://github.com/ZonatedCord/Mimir.git ~/.gemini/debtoken && \ +git clone https://github.com/ZonatedCord/Mimir.git ~/.gemini/mimir && \ mkdir -p ~/.gemini/skills && \ -cp -r ~/.gemini/debtoken/gemini/skills/* ~/.gemini/skills/ +cp -r ~/.gemini/mimir/gemini/skills/* ~/.gemini/skills/ ``` Restart Gemini to discover skills. @@ -47,15 +47,15 @@ Restart Gemini to discover skills. **Verify:** ```bash -node ~/.claude/debtoken/scripts/estimate.js "hello world" +node ~/.claude/mimir/scripts/estimate.js "hello world" ``` ### Codex CLI ```bash -git clone https://github.com/ZonatedCord/Mimir.git ~/.codex/debtoken && \ +git clone https://github.com/ZonatedCord/Mimir.git ~/.codex/mimir && \ mkdir -p ~/.agents/skills && \ -ln -s ~/.codex/debtoken/codex/skills ~/.agents/skills/debtoken +ln -s ~/.codex/mimir/codex/skills ~/.agents/skills/mimir ``` Restart Codex to discover skills. See [docs/README.codex.md](docs/README.codex.md) for full Codex guide. @@ -71,7 +71,7 @@ Restart Codex to discover skills. See [docs/README.codex.md](docs/README.codex.m ``` ``` -⚡ DEBTOKEN PREFLIGHT +⚡ MIMIR PREFLIGHT ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Baseline System overhead: ~2,000 (prompt + Claude UI) diff --git a/scripts/config-show.js b/scripts/config-show.js index 026dad3..2dfd77c 100644 --- a/scripts/config-show.js +++ b/scripts/config-show.js @@ -23,4 +23,8 @@ function main() { process.stdout.write(`${LINE}\n\n`); } -main(); +if (require.main === module) { + main(); +} + +module.exports = { main }; diff --git a/scripts/history-show.js b/scripts/history-show.js index c00192c..1ad8615 100644 --- a/scripts/history-show.js +++ b/scripts/history-show.js @@ -91,4 +91,8 @@ function main() { process.stdout.write(`${LINE}\n\n`); } -main(); +if (require.main === module) { + main(); +} + +module.exports = { main, printStats, escapeCsv }; diff --git a/scripts/split.js b/scripts/split.js index c14a203..7ab2c1f 100755 --- a/scripts/split.js +++ b/scripts/split.js @@ -159,7 +159,11 @@ async function main() { process.stdout.write(`${LINE}\n\n`); } -main().catch((err) => { - process.stderr.write(`Error: ${err.message}\n`); - process.exit(1); -}); +if (require.main === module) { + main().catch((err) => { + process.stderr.write(`Error: ${err.message}\n`); + process.exit(1); + }); +} + +module.exports = { parseArgs, detectSplitPoints, main }; diff --git a/tests/config-show.test.js b/tests/config-show.test.js index 2d29d15..c002416 100644 --- a/tests/config-show.test.js +++ b/tests/config-show.test.js @@ -1,13 +1,32 @@ const assert = require('assert'); const path = require('path'); -const { execSync } = require('child_process'); -const script = path.resolve(__dirname, '..', 'scripts', 'config-show.js'); +async function runConfigShow(cwd) { + const originalArgv = process.argv; + const originalCwd = process.cwd(); + const originalWrite = process.stdout.write; + let stdout = ''; + process.argv = [process.execPath, 'scripts/config-show.js']; + process.stdout.write = (chunk) => { stdout += chunk; return true; }; + process.chdir(cwd); + + try { + delete require.cache[require.resolve('../scripts/config-show.js')]; + require('../scripts/config-show.js').main(); + } finally { + process.argv = originalArgv; + process.stdout.write = originalWrite; + process.chdir(originalCwd); + delete require.cache[require.resolve('../scripts/config-show.js')]; + } + + return stdout; +} + +void (async () => { // Default config (no .mimir.json in /tmp) -const out1 = execSync(`node "${script}"`, { - cwd: '/tmp', -}).toString(); +const out1 = await runConfigShow('/tmp'); assert.match(out1, /MIMIR CONFIG/, 'missing header'); assert.match(out1, /Source/, 'missing source line'); @@ -19,3 +38,4 @@ assert.match(out1, /Default model/, 'missing default model'); assert.match(out1, /200[,.]000/, 'should show 200k default context window'); console.log('✅ config-show.test.js passed'); +})().catch(err => { throw err; }); diff --git a/tests/estimate.test.js b/tests/estimate.test.js index aefb0bf..f729a91 100644 --- a/tests/estimate.test.js +++ b/tests/estimate.test.js @@ -90,7 +90,7 @@ assert.match(outBadFile, /⚠/); // HIGH/CRITICAL risk → auto-split appended const outHigh = await runEstimate(['analyze every file in the entire codebase and refactor all modules and update all tests and generate full documentation']); if (/Risk:\s+(HIGH|CRITICAL)/.test(outHigh)) { - assert.match(outHigh, /DEBTOKEN SPLIT/, 'HIGH/CRITICAL should include auto-split output'); + assert.match(outHigh, /MIMIR SPLIT/, 'HIGH/CRITICAL should include auto-split output'); } // --output json emits valid JSON with expected fields @@ -111,3 +111,4 @@ assert.doesNotMatch(jsonOut, /MIMIR PREFLIGHT/, '--output json should not includ console.log('✅ estimate.test.js passed'); })().catch(err => { throw err; }); + diff --git a/tests/history-show.test.js b/tests/history-show.test.js index 55178d6..7b1f570 100644 --- a/tests/history-show.test.js +++ b/tests/history-show.test.js @@ -2,8 +2,32 @@ const assert = require('assert'); const fs = require('fs'); const os = require('os'); const path = require('path'); -const { execSync } = require('child_process'); +async function runHistoryShow(args, env) { + const originalArgv = process.argv; + const originalEnv = { ...process.env }; + const originalWrite = process.stdout.write; + let stdout = ''; + + process.argv = [process.execPath, 'scripts/history-show.js', ...args]; + process.stdout.write = (chunk) => { stdout += chunk; return true; }; + Object.assign(process.env, env); + + try { + delete require.cache[require.resolve('../scripts/lib/history.js')]; + delete require.cache[require.resolve('../scripts/history-show.js')]; + require('../scripts/history-show.js').main(); + } finally { + process.argv = originalArgv; + process.stdout.write = originalWrite; + process.env = originalEnv; + delete require.cache[require.resolve('../scripts/history-show.js')]; + } + + return stdout; +} + +void (async () => { const tmpFile = path.join(os.tmpdir(), `mimir-history-show-test-${Date.now()}.json`); const entries = [ { task: 'refactor auth module', tokens: 5200, risk: 'LOW', model: 'Sonnet 4.6', timestamp: '2026-04-24T10:00:00.000Z' }, @@ -12,10 +36,9 @@ const entries = [ fs.writeFileSync(tmpFile, JSON.stringify(entries)); const env = { ...process.env, MIMIR_HISTORY_FILE: tmpFile, MIMIR_NO_HISTORY: '1' }; -const bin = path.resolve(__dirname, '..', 'scripts', 'history-show.js'); // Normal output — shows entries -const outNormal = execSync(`node "${bin}"`, { env }).toString(); +const outNormal = await runHistoryShow([], env); assert.match(outNormal, /MIMIR HISTORY/, 'missing header'); assert.match(outNormal, /refactor auth/, 'missing entry 1'); assert.match(outNormal, /analyze entire/, 'missing entry 2'); @@ -23,12 +46,12 @@ assert.match(outNormal, /LOW/, 'missing risk LOW'); assert.match(outNormal, /HIGH/, 'missing risk HIGH'); // N arg limits output -const outOne = execSync(`node "${bin}" 1`, { env }).toString(); +const outOne = await runHistoryShow(['1'], env); assert.match(outOne, /analyze entire/, 'N=1 should show most recent'); assert.doesNotMatch(outOne, /refactor auth/, 'N=1 should not show older entry'); // --csv output -const outCsv = execSync(`node "${bin}" --csv`, { env }).toString(); +const outCsv = await runHistoryShow(['--csv'], env); assert.match(outCsv, /timestamp,task,tokens,risk,model/, 'missing CSV header'); assert.match(outCsv, /refactor auth module/, 'missing CSV entry 1'); assert.match(outCsv, /analyze entire codebase/, 'missing CSV entry 2'); @@ -37,7 +60,7 @@ assert.match(outCsv, /85000/, 'missing tokens in CSV' assert.doesNotMatch(outCsv, /MIMIR HISTORY/, 'CSV should not have header block'); // --csv with N arg -const outCsvOne = execSync(`node "${bin}" 1 --csv`, { env }).toString(); +const outCsvOne = await runHistoryShow(['1', '--csv'], env); const csvLines = outCsvOne.trim().split('\n'); assert.strictEqual(csvLines.length, 2, 'N=1 --csv should have header + 1 data row'); @@ -45,7 +68,7 @@ assert.strictEqual(csvLines.length, 2, 'N=1 --csv should have header + 1 data ro const emptyFile = path.join(os.tmpdir(), `mimir-history-empty-${Date.now()}.json`); fs.writeFileSync(emptyFile, '[]'); const envEmpty = { ...process.env, MIMIR_HISTORY_FILE: emptyFile }; -const outEmpty = execSync(`node "${bin}"`, { env: envEmpty }).toString(); +const outEmpty = await runHistoryShow([], envEmpty); assert.match(outEmpty, /No Mimir history/, 'missing empty state message'); // --stats output @@ -59,7 +82,7 @@ const statsFile = path.join(os.tmpdir(), `mimir-stats-test-${Date.now()}.json`); fs.writeFileSync(statsFile, JSON.stringify(statsEntries)); const envStats = { ...process.env, MIMIR_HISTORY_FILE: statsFile }; -const outStats = execSync(`node "${bin}" --stats`, { env: envStats }).toString(); +const outStats = await runHistoryShow(['--stats'], envStats); assert.match(outStats, /MIMIR STATS/, 'missing STATS header'); assert.match(outStats, /4/, 'should show total count'); assert.match(outStats, /Average/, 'missing Average line'); @@ -69,7 +92,7 @@ assert.match(outStats, /HIGH/, 'missing HIGH in breakdown'); assert.doesNotMatch(outStats, /MIMIR HISTORY/, '--stats should not show history header'); // --stats on empty history -const outStatsEmpty = execSync(`node "${bin}" --stats`, { env: envEmpty }).toString(); +const outStatsEmpty = await runHistoryShow(['--stats'], envEmpty); assert.match(outStatsEmpty, /No Mimir history/, 'empty stats should show no history message'); // Cleanup @@ -78,3 +101,4 @@ fs.unlinkSync(emptyFile); fs.unlinkSync(statsFile); console.log('✅ history-show.test.js passed'); +})().catch(err => { throw err; }); diff --git a/tests/history.test.js b/tests/history.test.js index 7cb8938..294fa5b 100644 --- a/tests/history.test.js +++ b/tests/history.test.js @@ -3,32 +3,15 @@ const fs = require('fs'); const os = require('os'); const path = require('path'); -// Override HISTORY_FILE for test isolation const testHistoryFile = path.join(os.tmpdir(), `mimir-history-test-${Date.now()}.json`); +const originalEnv = { ...process.env }; +process.env.MIMIR_HISTORY_FILE = testHistoryFile; -// Patch require cache so history.js uses our test file const historyPath = path.resolve(__dirname, '..', 'scripts', 'lib', 'history.js'); delete require.cache[require.resolve(historyPath)]; +const { appendHistory, loadHistory } = require(historyPath); -const origWrite = fs.writeFileSync.bind(fs); -const origRead = fs.readFileSync.bind(fs); - -// Monkey-patch to redirect history file ops -const { appendHistory, loadHistory } = (() => { - const mod = require(historyPath); - return mod; -})(); - -// Since we can't easily redirect the internal HISTORY_FILE constant, -// test via the actual file path but clean up after -const HISTORY_FILE = path.join(os.homedir(), '.mimir-history.json'); - -// Backup existing history -let backup = null; -try { backup = fs.readFileSync(HISTORY_FILE, 'utf8'); } catch {} - -// Clean slate -try { fs.unlinkSync(HISTORY_FILE); } catch {} +try { fs.unlinkSync(testHistoryFile); } catch {} // Test: loadHistory returns [] when file missing const empty = loadHistory(); @@ -47,10 +30,7 @@ appendHistory({ task: 'second task', tokens: 200, risk: 'MEDIUM', model: 'Sonnet const h2 = loadHistory(); assert.strictEqual(h2.length, 2, 'should have 2 entries'); -// Restore backup -try { fs.unlinkSync(HISTORY_FILE); } catch {} -if (backup !== null) { - try { fs.writeFileSync(HISTORY_FILE, backup); } catch {} -} +try { fs.unlinkSync(testHistoryFile); } catch {} +process.env = originalEnv; console.log('✅ history.test.js passed'); diff --git a/tests/split.test.js b/tests/split.test.js index 1f7a0ef..a769982 100644 --- a/tests/split.test.js +++ b/tests/split.test.js @@ -1,13 +1,44 @@ const assert = require('assert'); -const { execSync } = require('child_process'); const NO_KEY = { ...process.env, ANTHROPIC_API_KEY: '' }; +async function runSplit(args) { + const originalArgv = process.argv; + const originalEnv = { ...process.env }; + const originalWrite = process.stdout.write; + const originalErrWrite = process.stderr.write; + const originalExit = process.exit; + let stdout = ''; + let stderr = ''; + let exitCode = null; + + process.argv = [process.execPath, 'scripts/split.js', ...args]; + process.env.ANTHROPIC_API_KEY = ''; + process.stdout.write = (chunk) => { stdout += chunk; return true; }; + process.stderr.write = (chunk) => { stderr += chunk; return true; }; + process.exit = (code) => { exitCode = code ?? 0; throw new Error('__EXIT__'); }; + + try { + delete require.cache[require.resolve('../scripts/split.js')]; + const mod = require('../scripts/split.js'); + await mod.main(); + } catch (err) { + if (err.message !== '__EXIT__') throw err; + } finally { + process.argv = originalArgv; + process.env = originalEnv; + process.stdout.write = originalWrite; + process.stderr.write = originalErrWrite; + process.exit = originalExit; + delete require.cache[require.resolve('../scripts/split.js')]; + } + + return { stdout, stderr, exitCode }; +} + +void (async () => { // Conjunction split -const out1 = execSync( - 'node scripts/split.js "analyze all TypeScript files and then refactor the auth module"', - { env: NO_KEY } -).toString(); +const out1 = (await runSplit(['analyze all TypeScript files and then refactor the auth module'])).stdout; assert.match(out1, /MIMIR SPLIT/, 'missing header'); assert.match(out1, /Suggested split/, 'missing split section'); @@ -16,55 +47,37 @@ assert.match(out1, /tokens/, 'missing token count'); assert.match(out1, /1\./, 'missing item 1'); // Multiple action verbs -const out2 = execSync( - 'node scripts/split.js "analyze the codebase and then refactor all components"', - { env: NO_KEY } -).toString(); +const out2 = (await runSplit(['analyze the codebase and then refactor all components'])).stdout; assert.match(out2, /1\./); assert.match(out2, /2\./); // --files flag — shows file breakdown -const out3 = execSync( - 'node scripts/split.js "refactor auth module and then update tests" --files package.json', - { env: NO_KEY } -).toString(); +const out3 = (await runSplit(['refactor auth module and then update tests', '--files', 'package.json'])).stdout; assert.match(out3, /Files loaded/, 'missing files loaded line'); assert.match(out3, /package\.json/, 'missing file name'); assert.match(out3, /Suggested split/); // --files missing file — shows warning, does not crash -const out4 = execSync( - 'node scripts/split.js "some task" --files nonexistent.ts', - { env: NO_KEY } -).toString(); +const out4 = (await runSplit(['some task', '--files', 'nonexistent.ts'])).stdout; assert.match(out4, /⚠/); // Numbered list → splits on items -const out5 = execSync( - 'node scripts/split.js "1. analyze TypeScript files 2. refactor auth module 3. update tests"', - { env: NO_KEY } -).toString(); +const out5 = (await runSplit(['1. analyze TypeScript files 2. refactor auth module 3. update tests'])).stdout; assert.match(out5, /1\./); assert.match(out5, /2\./); assert.match(out5, /3\./); // File paths → splits per file -const out6 = execSync( - 'node scripts/split.js "fix bug in src/auth.ts and update src/auth.test.ts"', - { env: NO_KEY } -).toString(); +const out6 = (await runSplit(['fix bug in src/auth.ts and update src/auth.test.ts'])).stdout; assert.match(out6, /src\/auth\.ts/); assert.match(out6, /src\/auth\.test\.ts/); assert.match(out6, /scope boundary/); // No arguments → non-zero exit -try { - execSync('node scripts/split.js 2>&1', { env: NO_KEY }); - assert.fail('Should have thrown'); -} catch (err) { - assert.ok(err.status !== 0); -} +const empty = await runSplit([]); +assert.ok(empty.exitCode !== 0, 'Should have non-zero exit'); console.log('✅ split.test.js passed'); +})().catch(err => { throw err; });