murmur8 4.2.0 → 4.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.blueprint/agents/AGENT_SPECIFICATION_ALEX.md +33 -3
- package/.blueprint/features/feature_config-factory/FEATURE_SPEC.md +138 -0
- package/.blueprint/features/feature_config-factory/IMPLEMENTATION_PLAN.md +187 -0
- package/.blueprint/features/feature_config-factory/handoff-nigel.md +57 -0
- package/.blueprint/features/feature_cost-tracking/FEATURE_SPEC.md +216 -0
- package/.blueprint/features/feature_cost-tracking/IMPLEMENTATION_PLAN.md +50 -0
- package/.blueprint/features/feature_diff-preview/FEATURE_SPEC.md +182 -0
- package/.blueprint/features/feature_diff-preview/IMPLEMENTATION_PLAN.md +42 -0
- package/.blueprint/features/feature_extract-prompt-util/FEATURE_SPEC.md +42 -0
- package/.blueprint/features/feature_fix-status-icons/FEATURE_SPEC.md +37 -0
- package/.blueprint/features/feature_murm-subagent/FEATURE_SPEC.md +137 -0
- package/.blueprint/features/feature_murm-subagent/SKILL_CHANGES.md +345 -0
- package/.blueprint/features/feature_split-cli-commands/FEATURE_SPEC.md +125 -0
- package/.blueprint/features/feature_split-cli-commands/IMPLEMENTATION_PLAN.md +119 -0
- package/.blueprint/features/feature_split-cli-commands/handoff-nigel.md +45 -0
- package/.blueprint/features/feature_theme-adoption/FEATURE_SPEC.md +143 -0
- package/.blueprint/features/feature_theme-adoption/IMPLEMENTATION_PLAN.md +68 -0
- package/.blueprint/features/feature_theme-adoption/handoff-nigel.md +35 -0
- package/.blueprint/templates/BACKLOG_TEMPLATE.md +46 -0
- package/README.md +79 -12
- package/SKILL.md +377 -3
- package/bin/cli.js +20 -411
- package/package.json +1 -1
- package/src/commands/cost-config.js +28 -0
- package/src/commands/feedback-config.js +32 -0
- package/src/commands/help.js +86 -0
- package/src/commands/history.js +42 -0
- package/src/commands/init.js +12 -0
- package/src/commands/insights.js +23 -0
- package/src/commands/murm-config.js +52 -0
- package/src/commands/murm.js +109 -0
- package/src/commands/queue.js +19 -0
- package/src/commands/retry-config.js +28 -0
- package/src/commands/stack-config.js +32 -0
- package/src/commands/update.js +12 -0
- package/src/commands/utils.js +25 -0
- package/src/commands/validate.js +15 -0
- package/src/config-factory.js +190 -0
- package/src/cost.js +122 -0
- package/src/diff-preview.js +165 -0
- package/src/feedback.js +5 -2
- package/src/history.js +51 -3
- package/src/index.js +25 -0
- package/src/init.js +1 -15
- package/src/insights.js +19 -16
- package/src/retry.js +5 -2
- package/src/stack.js +4 -1
- package/src/theme.js +6 -5
- package/src/update.js +2 -15
- package/src/utils.js +26 -0
- package/src/validate.js +5 -12
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* murm-config command - View or modify murmuration pipeline configuration
|
|
3
|
+
*/
|
|
4
|
+
const {
|
|
5
|
+
readMurmConfig,
|
|
6
|
+
writeMurmConfig,
|
|
7
|
+
getDefaultMurmConfig
|
|
8
|
+
} = require('../murm');
|
|
9
|
+
|
|
10
|
+
const description = 'View or modify murmuration pipeline configuration';
|
|
11
|
+
const aliases = ['parallel-config'];
|
|
12
|
+
|
|
13
|
+
async function run(args) {
|
|
14
|
+
const subArg = args[1];
|
|
15
|
+
|
|
16
|
+
if (subArg === 'set') {
|
|
17
|
+
const key = args[2];
|
|
18
|
+
const value = args[3];
|
|
19
|
+
if (!key || !value) {
|
|
20
|
+
console.error('Usage: murm-config set <key> <value>');
|
|
21
|
+
console.error('Valid keys: cli, skill, skillFlags, worktreeDir, maxConcurrency, queueFile');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
const config = readMurmConfig();
|
|
25
|
+
if (key === 'maxConcurrency') {
|
|
26
|
+
config[key] = parseInt(value, 10);
|
|
27
|
+
} else {
|
|
28
|
+
config[key] = value;
|
|
29
|
+
}
|
|
30
|
+
writeMurmConfig(config);
|
|
31
|
+
console.log(`Set ${key} = ${value}`);
|
|
32
|
+
} else if (subArg === 'reset') {
|
|
33
|
+
writeMurmConfig(getDefaultMurmConfig());
|
|
34
|
+
console.log('Murmuration configuration reset to defaults.');
|
|
35
|
+
} else {
|
|
36
|
+
const config = readMurmConfig();
|
|
37
|
+
console.log('Murmuration Configuration\n');
|
|
38
|
+
console.log(` cli: ${config.cli}`);
|
|
39
|
+
console.log(` skill: ${config.skill}`);
|
|
40
|
+
console.log(` skillFlags: ${config.skillFlags}`);
|
|
41
|
+
console.log(` worktreeDir: ${config.worktreeDir}`);
|
|
42
|
+
console.log(` maxConcurrency: ${config.maxConcurrency}`);
|
|
43
|
+
console.log(` maxFeatures: ${config.maxFeatures}`);
|
|
44
|
+
console.log(` timeout: ${config.timeout} min`);
|
|
45
|
+
console.log(` minDiskSpaceMB: ${config.minDiskSpaceMB}`);
|
|
46
|
+
console.log(` queueFile: ${config.queueFile}`);
|
|
47
|
+
console.log('\nTo change: murmur8 murm-config set <key> <value>');
|
|
48
|
+
console.log('Run pipelines: murmur8 murm <slug1> <slug2> ...');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { run, description, aliases };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* murm command - Run multiple feature pipelines in parallel using git worktrees
|
|
3
|
+
*/
|
|
4
|
+
const {
|
|
5
|
+
formatStatus,
|
|
6
|
+
runMurm,
|
|
7
|
+
loadQueue,
|
|
8
|
+
cleanupWorktrees,
|
|
9
|
+
abortMurm,
|
|
10
|
+
getLockInfo,
|
|
11
|
+
getDetailedStatus,
|
|
12
|
+
formatDetailedStatus,
|
|
13
|
+
rollbackMurm
|
|
14
|
+
} = require('../murm');
|
|
15
|
+
|
|
16
|
+
const description = 'Run multiple feature pipelines in parallel using git worktrees';
|
|
17
|
+
const aliases = ['parallel', 'murmuration'];
|
|
18
|
+
|
|
19
|
+
async function run(args) {
|
|
20
|
+
const subArg = args[1];
|
|
21
|
+
|
|
22
|
+
if (subArg === 'status') {
|
|
23
|
+
const detailed = args.includes('--detailed') || args.includes('-d');
|
|
24
|
+
const lock = getLockInfo();
|
|
25
|
+
|
|
26
|
+
if (detailed) {
|
|
27
|
+
const details = getDetailedStatus();
|
|
28
|
+
console.log(formatDetailedStatus(details));
|
|
29
|
+
} else {
|
|
30
|
+
const queue = loadQueue();
|
|
31
|
+
|
|
32
|
+
if (!queue.features || queue.features.length === 0) {
|
|
33
|
+
if (lock) {
|
|
34
|
+
console.log(`Murmuration execution in progress (PID: ${lock.pid})`);
|
|
35
|
+
console.log(`Started: ${lock.startedAt}`);
|
|
36
|
+
console.log(`Features: ${lock.features.join(', ')}`);
|
|
37
|
+
} else {
|
|
38
|
+
console.log('No murmuration pipelines active.');
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log('Murmuration Pipeline Status\n');
|
|
44
|
+
console.log(formatStatus(queue.features));
|
|
45
|
+
const summary = {
|
|
46
|
+
running: queue.features.filter(f => f.status === 'murm_running').length,
|
|
47
|
+
pending: queue.features.filter(f => f.status === 'murm_queued').length,
|
|
48
|
+
completed: queue.features.filter(f => f.status === 'murm_complete').length,
|
|
49
|
+
failed: queue.features.filter(f => f.status === 'murm_failed').length,
|
|
50
|
+
conflicts: queue.features.filter(f => f.status === 'merge_conflict').length
|
|
51
|
+
};
|
|
52
|
+
console.log(`\nRunning: ${summary.running} | Pending: ${summary.pending} | Completed: ${summary.completed} | Failed: ${summary.failed} | Conflicts: ${summary.conflicts}`);
|
|
53
|
+
|
|
54
|
+
// Show log paths for running/failed
|
|
55
|
+
const withLogs = queue.features.filter(f =>
|
|
56
|
+
f.logPath && (f.status === 'murm_running' || f.status === 'murm_failed')
|
|
57
|
+
);
|
|
58
|
+
if (withLogs.length > 0) {
|
|
59
|
+
console.log('\nLog files:');
|
|
60
|
+
withLogs.forEach(f => console.log(` ${f.slug}: ${f.logPath}`));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log('\nTip: Use --detailed for progress bars');
|
|
64
|
+
}
|
|
65
|
+
} else if (subArg === 'rollback') {
|
|
66
|
+
const dryRunFlag = args.includes('--dry-run');
|
|
67
|
+
const forceFlag = args.includes('--force');
|
|
68
|
+
await rollbackMurm({ dryRun: dryRunFlag, force: forceFlag });
|
|
69
|
+
} else if (subArg === 'cleanup') {
|
|
70
|
+
const cleaned = await cleanupWorktrees();
|
|
71
|
+
console.log(`Cleaned ${cleaned} worktree(s).`);
|
|
72
|
+
} else if (subArg === 'abort') {
|
|
73
|
+
const cleanupFlag = args.includes('--cleanup');
|
|
74
|
+
await abortMurm({ cleanup: cleanupFlag });
|
|
75
|
+
} else {
|
|
76
|
+
const slugs = args.slice(1).filter(a => !a.startsWith('--') && !a.startsWith('-'));
|
|
77
|
+
if (slugs.length === 0) {
|
|
78
|
+
console.error('Usage: murmur8 murm <slug1> <slug2> ... [options]');
|
|
79
|
+
console.error('\nOptions:');
|
|
80
|
+
console.error(' --dry-run Preview execution plan without running');
|
|
81
|
+
console.error(' --yes, -y Skip confirmation prompt');
|
|
82
|
+
console.error(' --force Override existing lock');
|
|
83
|
+
console.error(' --verbose Stream output to console (not just logs)');
|
|
84
|
+
console.error(' --skip-preflight Skip feature validation checks');
|
|
85
|
+
console.error(' --max-concurrency=N Set max parallel pipelines (default: 3)');
|
|
86
|
+
console.error('\nSubcommands:');
|
|
87
|
+
console.error(' murm status Show status of all pipelines');
|
|
88
|
+
console.error(' murm abort Stop all running pipelines');
|
|
89
|
+
console.error(' murm cleanup Remove completed/aborted worktrees');
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const maxFlag = args.find(a => a.startsWith('--max-concurrency='));
|
|
94
|
+
const options = {
|
|
95
|
+
dryRun: args.includes('--dry-run'),
|
|
96
|
+
yes: args.includes('--yes') || args.includes('-y'),
|
|
97
|
+
force: args.includes('--force'),
|
|
98
|
+
verbose: args.includes('--verbose'),
|
|
99
|
+
skipPreflight: args.includes('--skip-preflight')
|
|
100
|
+
};
|
|
101
|
+
if (maxFlag) {
|
|
102
|
+
options.maxConcurrency = parseInt(maxFlag.split('=')[1], 10);
|
|
103
|
+
}
|
|
104
|
+
const result = await runMurm(slugs, options);
|
|
105
|
+
process.exit(result.success ? 0 : 1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = { run, description, aliases };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* queue command - Show queue status or reset
|
|
3
|
+
*/
|
|
4
|
+
const { displayQueue, resetQueue } = require('../orchestrator');
|
|
5
|
+
|
|
6
|
+
const description = 'Show queue status (use "reset" to clear)';
|
|
7
|
+
|
|
8
|
+
async function run(args) {
|
|
9
|
+
const subArg = args[1];
|
|
10
|
+
|
|
11
|
+
if (subArg === 'reset') {
|
|
12
|
+
resetQueue();
|
|
13
|
+
console.log('Queue has been reset.');
|
|
14
|
+
} else {
|
|
15
|
+
displayQueue();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { run, description };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* retry-config command - Manage retry configuration for adaptive retry logic
|
|
3
|
+
*/
|
|
4
|
+
const { displayConfig, setConfigValue, resetConfig } = require('../retry');
|
|
5
|
+
|
|
6
|
+
const description = 'Manage retry configuration for adaptive retry logic';
|
|
7
|
+
|
|
8
|
+
async function run(args) {
|
|
9
|
+
const subArg = args[1];
|
|
10
|
+
|
|
11
|
+
if (subArg === 'set') {
|
|
12
|
+
const key = args[2];
|
|
13
|
+
const value = args[3];
|
|
14
|
+
if (!key || !value) {
|
|
15
|
+
console.error('Usage: retry-config set <key> <value>');
|
|
16
|
+
console.error('Valid keys: maxRetries, windowSize, highFailureThreshold');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
setConfigValue(key, value);
|
|
20
|
+
} else if (subArg === 'reset') {
|
|
21
|
+
resetConfig();
|
|
22
|
+
console.log('Retry configuration reset to defaults.');
|
|
23
|
+
} else {
|
|
24
|
+
displayConfig();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = { run, description };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stack-config command - View or modify project tech stack configuration
|
|
3
|
+
*/
|
|
4
|
+
const {
|
|
5
|
+
displayStackConfig,
|
|
6
|
+
setStackConfigValue,
|
|
7
|
+
resetStackConfig
|
|
8
|
+
} = require('../stack');
|
|
9
|
+
|
|
10
|
+
const description = 'View or modify project tech stack configuration';
|
|
11
|
+
|
|
12
|
+
async function run(args) {
|
|
13
|
+
const subArg = args[1];
|
|
14
|
+
|
|
15
|
+
if (subArg === 'set') {
|
|
16
|
+
const key = args[2];
|
|
17
|
+
const value = args[3];
|
|
18
|
+
if (!key || !value) {
|
|
19
|
+
console.error('Usage: stack-config set <key> <value>');
|
|
20
|
+
console.error('Valid keys: language, runtime, packageManager, frameworks, testRunner, testCommand, linter, tools');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
setStackConfigValue(key, value);
|
|
24
|
+
} else if (subArg === 'reset') {
|
|
25
|
+
resetStackConfig();
|
|
26
|
+
console.log('Stack configuration reset to defaults.');
|
|
27
|
+
} else {
|
|
28
|
+
displayStackConfig();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { run, description };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* update command - Update agents, templates, and rituals (preserves your content)
|
|
3
|
+
*/
|
|
4
|
+
const { update } = require('../update');
|
|
5
|
+
|
|
6
|
+
const description = 'Update agents, templates, and rituals (preserves your content)';
|
|
7
|
+
|
|
8
|
+
async function run(args) {
|
|
9
|
+
await update();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
module.exports = { run, description };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for CLI commands
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parse common flags from command line arguments
|
|
7
|
+
* @param {string[]} args - Command line arguments
|
|
8
|
+
* @returns {Object} Parsed flags
|
|
9
|
+
*/
|
|
10
|
+
function parseFlags(args) {
|
|
11
|
+
const flags = {};
|
|
12
|
+
for (const arg of args) {
|
|
13
|
+
if (arg === '--all') flags.all = true;
|
|
14
|
+
if (arg === '--stats') flags.stats = true;
|
|
15
|
+
if (arg === '--force') flags.force = true;
|
|
16
|
+
if (arg === '--bottlenecks') flags.bottlenecks = true;
|
|
17
|
+
if (arg === '--failures') flags.failures = true;
|
|
18
|
+
if (arg === '--json') flags.json = true;
|
|
19
|
+
if (arg === '--feedback') flags.feedback = true;
|
|
20
|
+
if (arg === '--cost') flags.cost = true;
|
|
21
|
+
}
|
|
22
|
+
return flags;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = { parseFlags };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* validate command - Run pre-flight checks to validate project configuration
|
|
3
|
+
*/
|
|
4
|
+
const { validate, formatOutput } = require('../validate');
|
|
5
|
+
|
|
6
|
+
const description = 'Run pre-flight checks to validate project configuration';
|
|
7
|
+
|
|
8
|
+
async function run(args) {
|
|
9
|
+
const result = await validate();
|
|
10
|
+
const useColor = process.stdout.isTTY || false;
|
|
11
|
+
console.log(formatOutput(result, useColor));
|
|
12
|
+
process.exit(result.exitCode);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = { run, description };
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Factory function to create a standardized config module.
|
|
8
|
+
*
|
|
9
|
+
* @param {Object} options - Configuration options
|
|
10
|
+
* @param {string} options.name - Display name for the config (e.g., 'Retry', 'Feedback')
|
|
11
|
+
* @param {string} options.file - Path to config file (e.g., '.claude/retry-config.json')
|
|
12
|
+
* @param {Object} options.defaults - Default configuration values
|
|
13
|
+
* @param {Object} [options.validators] - Map of key -> validator function
|
|
14
|
+
* Validator returns true for valid, or error string for invalid
|
|
15
|
+
* @param {Object} [options.formatters] - Map of key -> display formatter function
|
|
16
|
+
* @param {string[]} [options.arrayKeys] - Keys that accept JSON array values
|
|
17
|
+
* @returns {Object} Config module with standard methods
|
|
18
|
+
*/
|
|
19
|
+
function createConfigModule(options) {
|
|
20
|
+
const {
|
|
21
|
+
name,
|
|
22
|
+
file,
|
|
23
|
+
defaults,
|
|
24
|
+
validators = {},
|
|
25
|
+
formatters = {},
|
|
26
|
+
arrayKeys = []
|
|
27
|
+
} = options;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Ensures the config directory exists.
|
|
31
|
+
*/
|
|
32
|
+
function ensureConfigDir() {
|
|
33
|
+
const dir = path.dirname(file);
|
|
34
|
+
if (!fs.existsSync(dir)) {
|
|
35
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns a copy of the default configuration.
|
|
41
|
+
*/
|
|
42
|
+
function getDefault() {
|
|
43
|
+
return JSON.parse(JSON.stringify(defaults));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Reads the config from file.
|
|
48
|
+
* Returns defaults if file is missing or corrupted.
|
|
49
|
+
* Merges missing keys from defaults.
|
|
50
|
+
*/
|
|
51
|
+
function read() {
|
|
52
|
+
ensureConfigDir();
|
|
53
|
+
if (!fs.existsSync(file)) {
|
|
54
|
+
return getDefault();
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
58
|
+
const parsed = JSON.parse(content);
|
|
59
|
+
// Merge missing keys from defaults
|
|
60
|
+
const merged = { ...getDefault(), ...parsed };
|
|
61
|
+
return merged;
|
|
62
|
+
} catch (err) {
|
|
63
|
+
// Graceful degradation: return defaults on parse error
|
|
64
|
+
return getDefault();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Writes the config to file.
|
|
70
|
+
* Creates directory if needed.
|
|
71
|
+
*/
|
|
72
|
+
function write(config) {
|
|
73
|
+
ensureConfigDir();
|
|
74
|
+
fs.writeFileSync(file, JSON.stringify(config, null, 2));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Resets config to defaults by writing default config to file.
|
|
79
|
+
*/
|
|
80
|
+
function reset() {
|
|
81
|
+
write(getDefault());
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Sets a config value by key.
|
|
86
|
+
* @param {string} key - Config key
|
|
87
|
+
* @param {string} value - New value (will be parsed appropriately)
|
|
88
|
+
*/
|
|
89
|
+
function setValue(key, value) {
|
|
90
|
+
const validKeys = Object.keys(defaults);
|
|
91
|
+
|
|
92
|
+
if (!validKeys.includes(key)) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Unknown config key: ${key}. Valid keys: ${validKeys.join(', ')}`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let parsed = value;
|
|
99
|
+
|
|
100
|
+
// Handle array keys
|
|
101
|
+
if (arrayKeys.includes(key)) {
|
|
102
|
+
try {
|
|
103
|
+
parsed = JSON.parse(value);
|
|
104
|
+
if (!Array.isArray(parsed)) {
|
|
105
|
+
throw new Error(`${key} must be a JSON array, e.g. '["a","b"]'`);
|
|
106
|
+
}
|
|
107
|
+
} catch (err) {
|
|
108
|
+
if (err.message.includes('must be a JSON array')) throw err;
|
|
109
|
+
throw new Error(`${key} must be a valid JSON array, e.g. '["a","b"]'`);
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
// Type coercion based on default value type
|
|
113
|
+
const defaultType = typeof defaults[key];
|
|
114
|
+
if (defaultType === 'number') {
|
|
115
|
+
parsed = parseFloat(value);
|
|
116
|
+
if (isNaN(parsed)) {
|
|
117
|
+
throw new Error(`Invalid value for ${key}: ${value}. Must be a number.`);
|
|
118
|
+
}
|
|
119
|
+
} else if (defaultType === 'boolean') {
|
|
120
|
+
if (value !== 'true' && value !== 'false') {
|
|
121
|
+
throw new Error(`Invalid value for ${key}: ${value}. Must be true or false.`);
|
|
122
|
+
}
|
|
123
|
+
parsed = value === 'true';
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Run custom validator if provided
|
|
128
|
+
if (validators[key]) {
|
|
129
|
+
const result = validators[key](parsed);
|
|
130
|
+
if (result !== true) {
|
|
131
|
+
throw new Error(`Invalid value for ${key}: ${value}. ${result}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const config = read();
|
|
136
|
+
config[key] = parsed;
|
|
137
|
+
write(config);
|
|
138
|
+
|
|
139
|
+
// Log confirmation
|
|
140
|
+
const display = Array.isArray(config[key]) ? JSON.stringify(config[key]) : config[key];
|
|
141
|
+
console.log(`Set ${key} = ${display}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Displays the current configuration.
|
|
146
|
+
*/
|
|
147
|
+
function display() {
|
|
148
|
+
const config = read();
|
|
149
|
+
console.log(`\n${name} Configuration\n`);
|
|
150
|
+
|
|
151
|
+
for (const [key, value] of Object.entries(config)) {
|
|
152
|
+
let displayValue;
|
|
153
|
+
|
|
154
|
+
// Use custom formatter if available
|
|
155
|
+
if (formatters[key]) {
|
|
156
|
+
displayValue = formatters[key](value);
|
|
157
|
+
} else if (Array.isArray(value)) {
|
|
158
|
+
displayValue = value.length > 0 ? value.join(', ') : '(not set)';
|
|
159
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
160
|
+
// For nested objects, format each entry
|
|
161
|
+
console.log(`\n ${key}:`);
|
|
162
|
+
for (const [subKey, subValue] of Object.entries(value)) {
|
|
163
|
+
const subDisplay = Array.isArray(subValue) ? subValue.join(' -> ') : subValue;
|
|
164
|
+
console.log(` ${subKey.padEnd(20)}: ${subDisplay}`);
|
|
165
|
+
}
|
|
166
|
+
continue;
|
|
167
|
+
} else if (value === '' || value === null || value === undefined) {
|
|
168
|
+
displayValue = '(not set)';
|
|
169
|
+
} else {
|
|
170
|
+
displayValue = String(value);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
console.log(` ${key.padEnd(20)}: ${displayValue}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.log('');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
CONFIG_FILE: file,
|
|
181
|
+
getDefault,
|
|
182
|
+
read,
|
|
183
|
+
write,
|
|
184
|
+
reset,
|
|
185
|
+
setValue,
|
|
186
|
+
display
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = { createConfigModule };
|
package/src/cost.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createConfigModule } = require('./config-factory');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_INPUT_PRICE = 3;
|
|
6
|
+
const DEFAULT_OUTPUT_PRICE = 15;
|
|
7
|
+
|
|
8
|
+
const costConfig = createConfigModule({
|
|
9
|
+
name: 'Cost',
|
|
10
|
+
file: '.claude/cost-config.json',
|
|
11
|
+
defaults: {
|
|
12
|
+
inputPricePerMillion: DEFAULT_INPUT_PRICE,
|
|
13
|
+
outputPricePerMillion: DEFAULT_OUTPUT_PRICE
|
|
14
|
+
},
|
|
15
|
+
validators: {
|
|
16
|
+
inputPricePerMillion: (v) => v >= 0 || 'Must be non-negative',
|
|
17
|
+
outputPricePerMillion: (v) => v >= 0 || 'Must be non-negative'
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function getDefaultPricing() {
|
|
22
|
+
return {
|
|
23
|
+
inputPricePerMillion: DEFAULT_INPUT_PRICE,
|
|
24
|
+
outputPricePerMillion: DEFAULT_OUTPUT_PRICE
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function loadPricingConfig() {
|
|
29
|
+
return costConfig.read();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function savePricingConfig(config) {
|
|
33
|
+
costConfig.write(config);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function calculateCost(inputTokens, outputTokens, pricing = getDefaultPricing()) {
|
|
37
|
+
const inputCost = (inputTokens / 1_000_000) * pricing.inputPricePerMillion;
|
|
38
|
+
const outputCost = (outputTokens / 1_000_000) * pricing.outputPricePerMillion;
|
|
39
|
+
return Math.round((inputCost + outputCost) * 1000) / 1000;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function formatCost(cost) {
|
|
43
|
+
if (cost === null || cost === undefined) return 'N/A';
|
|
44
|
+
return `$${cost.toFixed(3)}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function formatTokens(tokens) {
|
|
48
|
+
if (tokens === null || tokens === undefined) return 'N/A';
|
|
49
|
+
return tokens.toLocaleString();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getCostSummary(stages, pricing = loadPricingConfig()) {
|
|
53
|
+
const lines = [];
|
|
54
|
+
let totalInput = 0;
|
|
55
|
+
let totalOutput = 0;
|
|
56
|
+
let totalCost = 0;
|
|
57
|
+
|
|
58
|
+
for (const [name, data] of Object.entries(stages)) {
|
|
59
|
+
if (data && data.tokens) {
|
|
60
|
+
const input = data.tokens.input || 0;
|
|
61
|
+
const output = data.tokens.output || 0;
|
|
62
|
+
const cost = data.cost !== undefined ? data.cost : calculateCost(input, output, pricing);
|
|
63
|
+
|
|
64
|
+
totalInput += input;
|
|
65
|
+
totalOutput += output;
|
|
66
|
+
totalCost += cost;
|
|
67
|
+
|
|
68
|
+
lines.push({
|
|
69
|
+
stage: name,
|
|
70
|
+
input,
|
|
71
|
+
output,
|
|
72
|
+
cost
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
stages: lines,
|
|
79
|
+
totals: {
|
|
80
|
+
input: totalInput,
|
|
81
|
+
output: totalOutput,
|
|
82
|
+
cost: totalCost
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function displayCostSummary(slug, stages) {
|
|
88
|
+
const summary = getCostSummary(stages);
|
|
89
|
+
|
|
90
|
+
console.log(`\nCost Summary for feature: ${slug}\n`);
|
|
91
|
+
console.log('STAGE INPUT OUTPUT COST');
|
|
92
|
+
|
|
93
|
+
for (const s of summary.stages) {
|
|
94
|
+
const stage = s.stage.padEnd(16);
|
|
95
|
+
const input = formatTokens(s.input).padStart(9);
|
|
96
|
+
const output = formatTokens(s.output).padStart(10);
|
|
97
|
+
const cost = formatCost(s.cost).padStart(8);
|
|
98
|
+
console.log(`${stage} ${input} ${output} ${cost}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log('─'.repeat(45));
|
|
102
|
+
const totalStage = 'TOTAL'.padEnd(16);
|
|
103
|
+
const totalInput = formatTokens(summary.totals.input).padStart(9);
|
|
104
|
+
const totalOutput = formatTokens(summary.totals.output).padStart(10);
|
|
105
|
+
const totalCost = formatCost(summary.totals.cost).padStart(8);
|
|
106
|
+
console.log(`${totalStage} ${totalInput} ${totalOutput} ${totalCost}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
CONFIG_FILE: costConfig.CONFIG_FILE,
|
|
111
|
+
getDefaultPricing,
|
|
112
|
+
loadPricingConfig,
|
|
113
|
+
savePricingConfig,
|
|
114
|
+
calculateCost,
|
|
115
|
+
formatCost,
|
|
116
|
+
formatTokens,
|
|
117
|
+
getCostSummary,
|
|
118
|
+
displayCostSummary,
|
|
119
|
+
displayConfig: costConfig.display,
|
|
120
|
+
setConfigValue: costConfig.setValue,
|
|
121
|
+
resetConfig: costConfig.reset
|
|
122
|
+
};
|