coderev-cli 1.0.22 → 1.0.24
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/package.json +1 -1
- package/src/cli.js +230 -0
- package/src/models.js +160 -0
- package/src/models.test.js +80 -0
- package/src/reviewer.js +68 -15
- package/src/rules-market.js +257 -0
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -848,6 +848,236 @@ program
|
|
|
848
848
|
}
|
|
849
849
|
});
|
|
850
850
|
|
|
851
|
+
// ── Models ────────────────────────────────────────────────────────
|
|
852
|
+
program
|
|
853
|
+
.command('models')
|
|
854
|
+
.description('List available AI model templates')
|
|
855
|
+
.option('--json', 'Output as JSON')
|
|
856
|
+
.action((options) => {
|
|
857
|
+
const { listTemplates } = require('./models');
|
|
858
|
+
const templates = listTemplates();
|
|
859
|
+
|
|
860
|
+
if (options.json) {
|
|
861
|
+
console.log(JSON.stringify(templates, null, 2));
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const tierIcons = { recommended: '⭐', reasoning: '🧠', standard: ' ' };
|
|
866
|
+
console.log(chalk.bold('\n📋 Available Model Templates\n'));
|
|
867
|
+
console.log(chalk.gray(' Usage: coderev setup --model <name> (set primary model)'));
|
|
868
|
+
console.log(chalk.gray(' coderev setup --model <name> --fallback <name> (set primary + fallback)'));
|
|
869
|
+
console.log(chalk.gray(' coderev setup --agent-security <name> --agent-quality <name>'));
|
|
870
|
+
console.log('');
|
|
871
|
+
|
|
872
|
+
for (const t of templates) {
|
|
873
|
+
const icon = tierIcons[t.tier] || ' ';
|
|
874
|
+
const providerTag = chalk.gray(`[${t.provider}]`);
|
|
875
|
+
console.log(` ${icon} ${chalk.green(t.name)} ${providerTag}`);
|
|
876
|
+
console.log(` ${t.desc}`);
|
|
877
|
+
console.log(chalk.gray(` API Key: ${t.apiKeyEnv}`));
|
|
878
|
+
console.log('');
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
// ── Config ────────────────────────────────────────────────────────
|
|
883
|
+
program
|
|
884
|
+
.command('setup')
|
|
885
|
+
.description('Manage coderev model configuration')
|
|
886
|
+
.option('--model <name>', 'Set primary model template')
|
|
887
|
+
.option('--fallback <name>', 'Set fallback model template')
|
|
888
|
+
.option('--agent-security <name>', 'Model for security agent')
|
|
889
|
+
.option('--agent-bugs <name>', 'Model for bug detection agent')
|
|
890
|
+
.option('--agent-quality <name>', 'Model for quality agent')
|
|
891
|
+
.action((options) => {
|
|
892
|
+
const fs = require('fs');
|
|
893
|
+
const { resolveTemplate } = require('./models');
|
|
894
|
+
const configPath = path.join(process.cwd(), '.coderevrc.json');
|
|
895
|
+
|
|
896
|
+
let config = {};
|
|
897
|
+
if (fs.existsSync(configPath)) {
|
|
898
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
if (!config.ai) config.ai = {};
|
|
902
|
+
|
|
903
|
+
// Set primary model
|
|
904
|
+
if (options.model) {
|
|
905
|
+
try {
|
|
906
|
+
const resolved = resolveTemplate(options.model);
|
|
907
|
+
Object.assign(config.ai, resolved);
|
|
908
|
+
console.log(chalk.green(`✔ Primary model set to "${options.model}" (${resolved.model})`));
|
|
909
|
+
} catch (err) {
|
|
910
|
+
console.error(chalk.red(`✖ ${err.message}`));
|
|
911
|
+
process.exit(1);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// Set fallback model
|
|
916
|
+
if (options.fallback) {
|
|
917
|
+
try {
|
|
918
|
+
const resolved = resolveTemplate(options.fallback);
|
|
919
|
+
config.ai.fallback = {
|
|
920
|
+
enabled: true,
|
|
921
|
+
provider: resolved.provider,
|
|
922
|
+
baseURL: resolved.baseURL,
|
|
923
|
+
model: resolved.model,
|
|
924
|
+
temperature: resolved.temperature,
|
|
925
|
+
maxTokens: resolved.maxTokens,
|
|
926
|
+
};
|
|
927
|
+
console.log(chalk.green(`✔ Fallback model set to "${options.fallback}" (${resolved.model})`));
|
|
928
|
+
} catch (err) {
|
|
929
|
+
console.error(chalk.red(`✖ ${err.message}`));
|
|
930
|
+
process.exit(1);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Set per-agent models
|
|
935
|
+
if (!config.ai.agents) config.ai.agents = {};
|
|
936
|
+
const agentOpts = [
|
|
937
|
+
['security', options.agentSecurity],
|
|
938
|
+
['bugs', options.agentBugs],
|
|
939
|
+
['quality', options.agentQuality],
|
|
940
|
+
];
|
|
941
|
+
for (const [agent, templateName] of agentOpts) {
|
|
942
|
+
if (templateName) {
|
|
943
|
+
try {
|
|
944
|
+
const resolved = resolveTemplate(templateName);
|
|
945
|
+
config.ai.agents[agent] = {
|
|
946
|
+
provider: resolved.provider,
|
|
947
|
+
baseURL: resolved.baseURL,
|
|
948
|
+
model: resolved.model,
|
|
949
|
+
temperature: resolved.temperature,
|
|
950
|
+
};
|
|
951
|
+
console.log(chalk.green(`✔ ${agent} agent model set to "${templateName}" (${resolved.model})`));
|
|
952
|
+
} catch (err) {
|
|
953
|
+
console.error(chalk.red(`✖ ${err.message}`));
|
|
954
|
+
process.exit(1);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
960
|
+
console.log(chalk.blue(` Config saved to ${configPath}`));
|
|
961
|
+
|
|
962
|
+
if (!options.model && !options.fallback && !options.agentSecurity && !options.agentBugs && !options.agentQuality) {
|
|
963
|
+
// No options: show current config
|
|
964
|
+
console.log(chalk.bold('\n📋 Current coderev config:\n'));
|
|
965
|
+
console.log(JSON.stringify(config, null, 2));
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
// ── Rules Marketplace ─────────────────────────────────────────────
|
|
970
|
+
program
|
|
971
|
+
.command('rules <action>')
|
|
972
|
+
.description('Manage rule packs from the coderev marketplace')
|
|
973
|
+
.option('-q, --query <text>', 'Search query')
|
|
974
|
+
.option('-n, --name <name>', 'Rule pack name')
|
|
975
|
+
.option('--version <ver>', 'Version for publish', '1.0.0')
|
|
976
|
+
.option('--desc <text>', 'Description for publish')
|
|
977
|
+
.option('--api-url <url>', 'Marketplace API URL')
|
|
978
|
+
.action(async (action, options) => {
|
|
979
|
+
try {
|
|
980
|
+
const { searchRules, installRule, publishRules, listInstalled, uninstallRule, DEFAULT_API_URL } = require('./rules-market');
|
|
981
|
+
const apiUrl = options.apiUrl || process.env.CODEREV_MARKETPLACE_URL || DEFAULT_API_URL;
|
|
982
|
+
|
|
983
|
+
switch (action) {
|
|
984
|
+
case 'search': {
|
|
985
|
+
console.log(chalk.blue(`🔍 Searching marketplace for "${options.query || ''}"...`));
|
|
986
|
+
const results = await searchRules(options.query || '', apiUrl);
|
|
987
|
+
if (!Array.isArray(results) || results.length === 0) {
|
|
988
|
+
console.log(chalk.yellow('No rule packs found.'));
|
|
989
|
+
} else {
|
|
990
|
+
console.log(chalk.bold(`\nFound ${results.length} rule pack(s):\n`));
|
|
991
|
+
for (const pack of results) {
|
|
992
|
+
console.log(chalk.green(` 📦 ${pack.name}`) + chalk.gray(` v${pack.version}`));
|
|
993
|
+
console.log(` ${pack.description || '(no description)'}`);
|
|
994
|
+
console.log(chalk.gray(` ${pack.rules || 0} rules • ${pack.downloads || 0} downloads • by ${pack.author || 'unknown'}`));
|
|
995
|
+
console.log('');
|
|
996
|
+
}
|
|
997
|
+
console.log(chalk.blue(`Install: coderev rules install <name>`));
|
|
998
|
+
}
|
|
999
|
+
break;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
case 'install': {
|
|
1003
|
+
const name = options.name || options.query;
|
|
1004
|
+
if (!name) {
|
|
1005
|
+
console.error(chalk.red('✖ Please specify a rule pack name: coderev rules install <name>'));
|
|
1006
|
+
process.exit(1);
|
|
1007
|
+
}
|
|
1008
|
+
console.log(chalk.blue(`📥 Installing "${name}"...`));
|
|
1009
|
+
const result = await installRule(name, apiUrl);
|
|
1010
|
+
console.log(chalk.green(`✔ Installed ${result.name} v${result.version}`));
|
|
1011
|
+
console.log(chalk.gray(` ${result.added}/${result.total} rules added to .coderevrc.json`));
|
|
1012
|
+
break;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
case 'publish': {
|
|
1016
|
+
console.log(chalk.blue('📤 Publishing rules to marketplace...'));
|
|
1017
|
+
const result = await publishRules(apiUrl, {
|
|
1018
|
+
name: options.name,
|
|
1019
|
+
version: options.version,
|
|
1020
|
+
description: options.desc,
|
|
1021
|
+
});
|
|
1022
|
+
console.log(chalk.green(`✔ Published "${result.name}" v${options.version} (${result.rules} rules)`));
|
|
1023
|
+
break;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
case 'list': {
|
|
1027
|
+
const result = listInstalled();
|
|
1028
|
+
if (result.packs.length === 0) {
|
|
1029
|
+
console.log(chalk.yellow(result.message));
|
|
1030
|
+
} else {
|
|
1031
|
+
console.log(chalk.bold(`\n📦 Installed rule packs (${result.packs.length}):\n`));
|
|
1032
|
+
for (const pack of result.packs) {
|
|
1033
|
+
console.log(chalk.green(` 📦 ${pack.name}`) + chalk.gray(` v${pack.version}`));
|
|
1034
|
+
console.log(chalk.gray(` ${pack.rules} rules • installed ${new Date(pack.installedAt).toLocaleDateString()}`));
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
break;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
case 'uninstall': {
|
|
1041
|
+
const name = options.name || options.query;
|
|
1042
|
+
if (!name) {
|
|
1043
|
+
console.error(chalk.red('✖ Please specify a rule pack name: coderev rules uninstall <name>'));
|
|
1044
|
+
process.exit(1);
|
|
1045
|
+
}
|
|
1046
|
+
const result = uninstallRule(name);
|
|
1047
|
+
console.log(chalk.green(`✔ Uninstalled "${result.name}"`));
|
|
1048
|
+
break;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
case 'info': {
|
|
1052
|
+
const name = options.name || options.query;
|
|
1053
|
+
if (!name) {
|
|
1054
|
+
console.error(chalk.red('✖ Please specify a rule pack name: coderev rules info <name>'));
|
|
1055
|
+
process.exit(1);
|
|
1056
|
+
}
|
|
1057
|
+
const pack = await apiRequest(apiUrl, `/rules/${encodeURIComponent(name)}`);
|
|
1058
|
+
console.log(chalk.bold(`\n📦 ${pack.name}`) + chalk.gray(` v${pack.version}`));
|
|
1059
|
+
console.log(` ${pack.description || '(no description)'}`);
|
|
1060
|
+
console.log(chalk.gray(` ${pack.rules?.length || 0} rules • by ${pack.author || 'unknown'}`));
|
|
1061
|
+
if (pack.rules && pack.rules.length > 0) {
|
|
1062
|
+
console.log(chalk.bold('\n Rules:'));
|
|
1063
|
+
for (const r of pack.rules) {
|
|
1064
|
+
const sev = r.severity === 'error' ? chalk.red(r.severity) : r.severity === 'warning' ? chalk.yellow(r.severity) : chalk.gray(r.severity);
|
|
1065
|
+
console.log(` - ${r.name} [${sev}] ${r.message || ''}`);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
break;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
default:
|
|
1072
|
+
console.error(chalk.red(`✖ Unknown action "${action}". Use: search | install | publish | list | uninstall | info`));
|
|
1073
|
+
process.exit(1);
|
|
1074
|
+
}
|
|
1075
|
+
} catch (err) {
|
|
1076
|
+
console.error(chalk.red(`✖ ${err.message}`));
|
|
1077
|
+
process.exit(1);
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
1080
|
+
|
|
851
1081
|
program.parse(process.argv);
|
|
852
1082
|
|
|
853
1083
|
// ── Helpers ───────────────────────────────────────────────────
|
package/src/models.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model Templates — built-in hot model presets for coderev.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* coderev config --model deepseek # switch to DeepSeek template
|
|
6
|
+
* coderev config --model openai --fallback qwen # primary + fallback
|
|
7
|
+
* coderev config --agent-security deepseek --agent-quality qwen
|
|
8
|
+
* coderev models # list all templates
|
|
9
|
+
*
|
|
10
|
+
* Each template provides provider + baseURL + default model,
|
|
11
|
+
* user only needs to set the corresponding API key env var.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const BUILTIN_TEMPLATES = {
|
|
15
|
+
deepseek: {
|
|
16
|
+
provider: 'deepseek',
|
|
17
|
+
baseURL: 'https://api.deepseek.com',
|
|
18
|
+
model: 'deepseek-chat',
|
|
19
|
+
apiKeyEnv: 'DEEPSEEK_API_KEY',
|
|
20
|
+
desc: 'DeepSeek V3 — 国产高性价比,¥1/百万token',
|
|
21
|
+
tier: 'recommended',
|
|
22
|
+
},
|
|
23
|
+
'deepseek-r1': {
|
|
24
|
+
provider: 'deepseek',
|
|
25
|
+
baseURL: 'https://api.deepseek.com',
|
|
26
|
+
model: 'deepseek-reasoner',
|
|
27
|
+
apiKeyEnv: 'DEEPSEEK_API_KEY',
|
|
28
|
+
desc: 'DeepSeek R1 — 推理增强,适合复杂漏洞分析',
|
|
29
|
+
tier: 'reasoning',
|
|
30
|
+
},
|
|
31
|
+
openai: {
|
|
32
|
+
provider: 'openai',
|
|
33
|
+
baseURL: 'https://api.openai.com/v1',
|
|
34
|
+
model: 'gpt-4o',
|
|
35
|
+
apiKeyEnv: 'OPENAI_API_KEY',
|
|
36
|
+
desc: 'GPT-4o — OpenAI 多模态旗舰',
|
|
37
|
+
tier: 'standard',
|
|
38
|
+
},
|
|
39
|
+
'openai-o3': {
|
|
40
|
+
provider: 'openai',
|
|
41
|
+
baseURL: 'https://api.openai.com/v1',
|
|
42
|
+
model: 'o3-mini',
|
|
43
|
+
apiKeyEnv: 'OPENAI_API_KEY',
|
|
44
|
+
desc: 'OpenAI o3-mini — 推理型,速度快',
|
|
45
|
+
tier: 'reasoning',
|
|
46
|
+
},
|
|
47
|
+
qwen: {
|
|
48
|
+
provider: 'openai',
|
|
49
|
+
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
|
50
|
+
model: 'qwen-plus',
|
|
51
|
+
apiKeyEnv: 'DASHSCOPE_API_KEY',
|
|
52
|
+
desc: '通义千问 Qwen-Plus — 中文能力强,¥0.8/百万token',
|
|
53
|
+
tier: 'standard',
|
|
54
|
+
},
|
|
55
|
+
'qwen-coder': {
|
|
56
|
+
provider: 'openai',
|
|
57
|
+
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
|
58
|
+
model: 'qwen-coder-plus',
|
|
59
|
+
apiKeyEnv: 'DASHSCOPE_API_KEY',
|
|
60
|
+
desc: '通义千问 Coder — 代码专精,¥2/百万token',
|
|
61
|
+
tier: 'recommended',
|
|
62
|
+
},
|
|
63
|
+
claude: {
|
|
64
|
+
provider: 'openai',
|
|
65
|
+
baseURL: 'https://api.anthropic.com/v1',
|
|
66
|
+
model: 'claude-sonnet-4-20250514',
|
|
67
|
+
apiKeyEnv: 'ANTHROPIC_API_KEY',
|
|
68
|
+
desc: 'Claude Sonnet 4 — 代码理解深度最强',
|
|
69
|
+
tier: 'standard',
|
|
70
|
+
},
|
|
71
|
+
gemini: {
|
|
72
|
+
provider: 'openai',
|
|
73
|
+
baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
|
74
|
+
model: 'gemini-2.5-pro',
|
|
75
|
+
apiKeyEnv: 'GEMINI_API_KEY',
|
|
76
|
+
desc: 'Gemini 2.5 Pro — Google,100万token上下文',
|
|
77
|
+
tier: 'standard',
|
|
78
|
+
},
|
|
79
|
+
zhipu: {
|
|
80
|
+
provider: 'openai',
|
|
81
|
+
baseURL: 'https://open.bigmodel.cn/api/paas/v4',
|
|
82
|
+
model: 'glm-4-plus',
|
|
83
|
+
apiKeyEnv: 'ZHIPU_API_KEY',
|
|
84
|
+
desc: '智谱 GLM-4-Plus — 国产强推理',
|
|
85
|
+
tier: 'standard',
|
|
86
|
+
},
|
|
87
|
+
moonshot: {
|
|
88
|
+
provider: 'openai',
|
|
89
|
+
baseURL: 'https://api.moonshot.cn/v1',
|
|
90
|
+
model: 'moonshot-v1-8k',
|
|
91
|
+
apiKeyEnv: 'MOONSHOT_API_KEY',
|
|
92
|
+
desc: '月之暗面 Kimi — 长文本处理强',
|
|
93
|
+
tier: 'standard',
|
|
94
|
+
},
|
|
95
|
+
codestral: {
|
|
96
|
+
provider: 'openai',
|
|
97
|
+
baseURL: 'https://api.mistral.ai/v1',
|
|
98
|
+
model: 'codestral-latest',
|
|
99
|
+
apiKeyEnv: 'MISTRAL_API_KEY',
|
|
100
|
+
desc: 'Mistral Codestral — 专注代码生成与审查',
|
|
101
|
+
tier: 'standard',
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Resolve a model template by name, with optional user overrides.
|
|
107
|
+
* Returns a full ai config block ready for use.
|
|
108
|
+
*
|
|
109
|
+
* @param {string} templateName - Template name (e.g. 'deepseek', 'qwen')
|
|
110
|
+
* @param {object} [overrides] - User overrides (model, baseURL, provider, etc.)
|
|
111
|
+
* @returns {object} Resolved ai config
|
|
112
|
+
*/
|
|
113
|
+
function resolveTemplate(templateName, overrides = {}) {
|
|
114
|
+
const template = BUILTIN_TEMPLATES[templateName];
|
|
115
|
+
if (!template) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Unknown model template "${templateName}". ` +
|
|
118
|
+
`Available: ${Object.keys(BUILTIN_TEMPLATES).join(', ')}`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
provider: overrides.provider || template.provider,
|
|
124
|
+
baseURL: overrides.baseURL || template.baseURL,
|
|
125
|
+
model: overrides.model || template.model,
|
|
126
|
+
apiKeyEnv: overrides.apiKeyEnv || template.apiKeyEnv,
|
|
127
|
+
temperature: overrides.temperature ?? 0.3,
|
|
128
|
+
maxTokens: overrides.maxTokens || 4096,
|
|
129
|
+
_template: templateName,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* List all built-in templates with key info.
|
|
135
|
+
* @returns {Array<{name: string, provider: string, model: string, apiKeyEnv: string, desc: string, tier: string}>}
|
|
136
|
+
*/
|
|
137
|
+
function listTemplates() {
|
|
138
|
+
return Object.entries(BUILTIN_TEMPLATES).map(([name, t]) => ({
|
|
139
|
+
name,
|
|
140
|
+
provider: t.provider,
|
|
141
|
+
model: t.model,
|
|
142
|
+
apiKeyEnv: t.apiKeyEnv,
|
|
143
|
+
desc: t.desc,
|
|
144
|
+
tier: t.tier,
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get a single template by name. Returns null if not found.
|
|
150
|
+
*/
|
|
151
|
+
function getTemplate(name) {
|
|
152
|
+
return BUILTIN_TEMPLATES[name] || null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = {
|
|
156
|
+
BUILTIN_TEMPLATES,
|
|
157
|
+
resolveTemplate,
|
|
158
|
+
listTemplates,
|
|
159
|
+
getTemplate,
|
|
160
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const { describe, it } = require('node:test');
|
|
2
|
+
const assert = require('assert');
|
|
3
|
+
const { BUILTIN_TEMPLATES, resolveTemplate, listTemplates, getTemplate } = require('./models');
|
|
4
|
+
|
|
5
|
+
describe('models.js', () => {
|
|
6
|
+
it('should have all 11 built-in templates', () => {
|
|
7
|
+
const templates = listTemplates();
|
|
8
|
+
assert.strictEqual(templates.length, 11);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should resolve deepseek template correctly', () => {
|
|
12
|
+
const result = resolveTemplate('deepseek');
|
|
13
|
+
assert.strictEqual(result.provider, 'deepseek');
|
|
14
|
+
assert.strictEqual(result.model, 'deepseek-chat');
|
|
15
|
+
assert.strictEqual(result.baseURL, 'https://api.deepseek.com');
|
|
16
|
+
assert.strictEqual(result.apiKeyEnv, 'DEEPSEEK_API_KEY');
|
|
17
|
+
assert.strictEqual(result._template, 'deepseek');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should resolve qwen template with openai-compatible provider', () => {
|
|
21
|
+
const result = resolveTemplate('qwen');
|
|
22
|
+
assert.strictEqual(result.provider, 'openai');
|
|
23
|
+
assert.strictEqual(result.model, 'qwen-plus');
|
|
24
|
+
assert.ok(result.baseURL.includes('dashscope'));
|
|
25
|
+
assert.strictEqual(result.apiKeyEnv, 'DASHSCOPE_API_KEY');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should apply user overrides on template', () => {
|
|
29
|
+
const result = resolveTemplate('openai', {
|
|
30
|
+
model: 'gpt-4o-mini',
|
|
31
|
+
temperature: 0.1,
|
|
32
|
+
});
|
|
33
|
+
assert.strictEqual(result.provider, 'openai');
|
|
34
|
+
assert.strictEqual(result.model, 'gpt-4o-mini');
|
|
35
|
+
assert.strictEqual(result.temperature, 0.1);
|
|
36
|
+
assert.strictEqual(result._template, 'openai');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should throw on unknown template', () => {
|
|
40
|
+
assert.throws(() => resolveTemplate('nonexistent'), {
|
|
41
|
+
message: /Unknown model template/,
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('getTemplate should return null for unknown', () => {
|
|
46
|
+
assert.strictEqual(getTemplate('nonexistent'), null);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('getTemplate should return object for known', () => {
|
|
50
|
+
const t = getTemplate('deepseek');
|
|
51
|
+
assert.ok(t);
|
|
52
|
+
assert.strictEqual(t.model, 'deepseek-chat');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('listTemplates should return array with required fields', () => {
|
|
56
|
+
const templates = listTemplates();
|
|
57
|
+
for (const t of templates) {
|
|
58
|
+
assert.ok(t.name, `template should have name: ${JSON.stringify(t)}`);
|
|
59
|
+
assert.ok(t.provider, `template should have provider: ${JSON.stringify(t)}`);
|
|
60
|
+
assert.ok(t.model, `template should have model: ${JSON.stringify(t)}`);
|
|
61
|
+
assert.ok(t.apiKeyEnv, `template should have apiKeyEnv: ${JSON.stringify(t)}`);
|
|
62
|
+
assert.ok(t.desc, `template should have desc: ${JSON.stringify(t)}`);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should have recommended and reasoning tiers', () => {
|
|
67
|
+
const templates = listTemplates();
|
|
68
|
+
const recommended = templates.filter(t => t.tier === 'recommended');
|
|
69
|
+
const reasoning = templates.filter(t => t.tier === 'reasoning');
|
|
70
|
+
assert.ok(recommended.length >= 2, 'should have at least 2 recommended templates');
|
|
71
|
+
assert.ok(reasoning.length >= 2, 'should have at least 2 reasoning templates');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('deepseek-r1 should be a reasoning model', () => {
|
|
75
|
+
const t = getTemplate('deepseek-r1');
|
|
76
|
+
assert.ok(t);
|
|
77
|
+
assert.strictEqual(t.tier, 'reasoning');
|
|
78
|
+
assert.strictEqual(t.model, 'deepseek-reasoner');
|
|
79
|
+
});
|
|
80
|
+
});
|
package/src/reviewer.js
CHANGED
|
@@ -186,7 +186,8 @@ async function runParallelAgents(apiKey, config, prompts) {
|
|
|
186
186
|
|
|
187
187
|
const tasks = prompts.map(async (p) => {
|
|
188
188
|
try {
|
|
189
|
-
|
|
189
|
+
// Pass agent name so callAI can select per-agent model config
|
|
190
|
+
const text = await callAI(apiKey, p.messages, config, p.role.name);
|
|
190
191
|
const parsed = parseReviewResponse(text);
|
|
191
192
|
return { name: p.role.name, success: true, ...parsed };
|
|
192
193
|
} catch (err) {
|
|
@@ -440,32 +441,84 @@ Important:
|
|
|
440
441
|
}
|
|
441
442
|
|
|
442
443
|
/**
|
|
443
|
-
* Call the AI provider
|
|
444
|
+
* Call the AI provider with support for:
|
|
445
|
+
* - Per-agent model selection (ai.agents[agentType])
|
|
446
|
+
* - Primary/fallback model switching (ai.fallback)
|
|
447
|
+
*
|
|
448
|
+
* @param {string} apiKey - API key
|
|
449
|
+
* @param {Array} messages - Chat messages
|
|
450
|
+
* @param {object} config - Full config object
|
|
451
|
+
* @param {string} [agentType] - Agent type for per-agent model selection ('security'|'bugs'|'quality')
|
|
452
|
+
* @returns {Promise<string>} AI response text
|
|
444
453
|
*/
|
|
445
|
-
async function callAI(apiKey, messages, config) {
|
|
454
|
+
async function callAI(apiKey, messages, config, agentType) {
|
|
446
455
|
const aiConfig = config.ai || {};
|
|
447
|
-
const provider = aiConfig.provider || 'openai';
|
|
448
456
|
|
|
449
|
-
|
|
457
|
+
// Resolve model config for this agent
|
|
458
|
+
let modelConfig = { ...aiConfig };
|
|
459
|
+
if (agentType && aiConfig.agents && aiConfig.agents[agentType]) {
|
|
460
|
+
const agentCfg = aiConfig.agents[agentType];
|
|
461
|
+
modelConfig = { ...aiConfig, ...agentCfg };
|
|
462
|
+
}
|
|
450
463
|
|
|
451
|
-
|
|
452
|
-
|
|
464
|
+
const provider = modelConfig.provider || 'openai';
|
|
465
|
+
|
|
466
|
+
// Determine base URL
|
|
467
|
+
let baseURL = modelConfig.baseURL;
|
|
453
468
|
if (!baseURL) {
|
|
454
469
|
if (provider === 'deepseek') baseURL = 'https://api.deepseek.com';
|
|
455
|
-
// openai and all others default to undefined (official endpoint)
|
|
456
470
|
}
|
|
457
471
|
|
|
458
|
-
const defaultModels = {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
472
|
+
const defaultModels = { openai: 'gpt-4o', deepseek: 'deepseek-chat' };
|
|
473
|
+
const model = modelConfig.model || defaultModels[provider] || provider;
|
|
474
|
+
|
|
475
|
+
// Try primary model first
|
|
476
|
+
try {
|
|
477
|
+
return await _doCallAI(apiKey, messages, {
|
|
478
|
+
model,
|
|
479
|
+
baseURL,
|
|
480
|
+
temperature: modelConfig.temperature ?? 0.3,
|
|
481
|
+
maxTokens: modelConfig.maxTokens || 4096,
|
|
482
|
+
});
|
|
483
|
+
} catch (err) {
|
|
484
|
+
const fallback = aiConfig.fallback;
|
|
485
|
+
const fallbackEnabled = fallback && fallback.enabled !== false;
|
|
486
|
+
|
|
487
|
+
if (!fallbackEnabled || !fallback.model) {
|
|
488
|
+
throw err;
|
|
489
|
+
}
|
|
462
490
|
|
|
491
|
+
// Log fallback attempt (visible in --debug mode)
|
|
492
|
+
const chalk = require('chalk');
|
|
493
|
+
console.error(chalk.yellow(`⚠️ Primary model failed: ${err.message}`));
|
|
494
|
+
console.error(chalk.blue(`↩ Falling back to ${fallback.model}...`));
|
|
495
|
+
|
|
496
|
+
const fbProvider = fallback.provider || provider;
|
|
497
|
+
let fbBaseURL = fallback.baseURL;
|
|
498
|
+
if (!fbBaseURL) {
|
|
499
|
+
if (fbProvider === 'deepseek') fbBaseURL = 'https://api.deepseek.com';
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return await _doCallAI(apiKey, messages, {
|
|
503
|
+
model: fallback.model,
|
|
504
|
+
baseURL: fbBaseURL,
|
|
505
|
+
temperature: fallback.temperature ?? 0.3,
|
|
506
|
+
maxTokens: fallback.maxTokens || 4096,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Internal: make a single AI call.
|
|
513
|
+
*/
|
|
514
|
+
async function _doCallAI(apiKey, messages, { model, baseURL, temperature, maxTokens }) {
|
|
515
|
+
const OpenAI = require('openai');
|
|
463
516
|
const client = new OpenAI({ apiKey, baseURL: baseURL || undefined });
|
|
464
517
|
|
|
465
518
|
const response = await client.chat.completions.create({
|
|
466
|
-
model
|
|
467
|
-
temperature
|
|
468
|
-
max_tokens:
|
|
519
|
+
model,
|
|
520
|
+
temperature,
|
|
521
|
+
max_tokens: maxTokens,
|
|
469
522
|
messages,
|
|
470
523
|
});
|
|
471
524
|
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rules Market — SaaS cloud rule repository for coderev.
|
|
3
|
+
*
|
|
4
|
+
* Commands:
|
|
5
|
+
* coderev rules search <query> Search the rule marketplace
|
|
6
|
+
* coderev rules install <name> Install a rule pack from the marketplace
|
|
7
|
+
* coderev rules publish Publish local rules to the marketplace
|
|
8
|
+
* coderev rules list List installed rule packs
|
|
9
|
+
*
|
|
10
|
+
* API Base: configurable via .coderevrc.json → marketplace.apiUrl
|
|
11
|
+
* Default: https://rules.coderev.dev/api
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const https = require('https');
|
|
17
|
+
const http = require('http');
|
|
18
|
+
|
|
19
|
+
const DEFAULT_API_URL = 'https://rules.coderev.dev/api';
|
|
20
|
+
const MARKETPLACE_DIR = '.coderev-marketplace';
|
|
21
|
+
const INSTALLED_MANIFEST = 'installed.json';
|
|
22
|
+
|
|
23
|
+
// ── API Client ───────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Make an HTTP request to the marketplace API.
|
|
27
|
+
*/
|
|
28
|
+
function apiRequest(apiUrl, endpoint, method = 'GET', body = null) {
|
|
29
|
+
const url = new URL(`${apiUrl}${endpoint}`);
|
|
30
|
+
const isHttps = url.protocol === 'https:';
|
|
31
|
+
const transport = isHttps ? https : http;
|
|
32
|
+
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const options = {
|
|
35
|
+
hostname: url.hostname,
|
|
36
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
37
|
+
path: url.pathname + url.search,
|
|
38
|
+
method,
|
|
39
|
+
headers: {
|
|
40
|
+
'Accept': 'application/json',
|
|
41
|
+
'User-Agent': 'coderev-cli',
|
|
42
|
+
...(body ? { 'Content-Type': 'application/json' } : {}),
|
|
43
|
+
},
|
|
44
|
+
timeout: 15000,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const req = transport.request(options, (res) => {
|
|
48
|
+
let data = '';
|
|
49
|
+
res.on('data', (chunk) => (data += chunk));
|
|
50
|
+
res.on('end', () => {
|
|
51
|
+
try {
|
|
52
|
+
const json = JSON.parse(data);
|
|
53
|
+
if (res.statusCode >= 400) {
|
|
54
|
+
reject(new Error(json.error || `API error: ${res.statusCode}`));
|
|
55
|
+
} else {
|
|
56
|
+
resolve(json);
|
|
57
|
+
}
|
|
58
|
+
} catch (e) {
|
|
59
|
+
reject(new Error(`Invalid API response: ${data.slice(0, 200)}`));
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
req.on('timeout', () => {
|
|
65
|
+
req.destroy();
|
|
66
|
+
reject(new Error('API request timed out'));
|
|
67
|
+
});
|
|
68
|
+
req.on('error', (err) => reject(new Error(`API connection failed: ${err.message}`)));
|
|
69
|
+
|
|
70
|
+
if (body) req.write(JSON.stringify(body));
|
|
71
|
+
req.end();
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Marketplace Directory ────────────────────────────────
|
|
76
|
+
|
|
77
|
+
function getMarketplaceDir() {
|
|
78
|
+
return path.join(process.cwd(), MARKETPLACE_DIR);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getInstalledManifest() {
|
|
82
|
+
const dir = getMarketplaceDir();
|
|
83
|
+
const manifestPath = path.join(dir, INSTALLED_MANIFEST);
|
|
84
|
+
if (!fs.existsSync(manifestPath)) return { packs: [] };
|
|
85
|
+
return JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function saveInstalledManifest(manifest) {
|
|
89
|
+
const dir = getMarketplaceDir();
|
|
90
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
91
|
+
fs.writeFileSync(path.join(dir, INSTALLED_MANIFEST), JSON.stringify(manifest, null, 2));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── Search ───────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
async function searchRules(query, apiUrl) {
|
|
97
|
+
const endpoint = query
|
|
98
|
+
? `/rules?q=${encodeURIComponent(query)}`
|
|
99
|
+
: '/rules';
|
|
100
|
+
const result = await apiRequest(apiUrl, endpoint);
|
|
101
|
+
return result.rules || result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── Install ──────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
async function installRule(packName, apiUrl) {
|
|
107
|
+
// Fetch rule pack from marketplace
|
|
108
|
+
const pack = await apiRequest(apiUrl, `/rules/${encodeURIComponent(packName)}`);
|
|
109
|
+
|
|
110
|
+
if (!pack || !pack.rules) {
|
|
111
|
+
throw new Error(`Rule pack "${packName}" not found or empty`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Merge into local .coderevrc.json
|
|
115
|
+
const configPath = path.join(process.cwd(), '.coderevrc.json');
|
|
116
|
+
let config = {};
|
|
117
|
+
if (fs.existsSync(configPath)) {
|
|
118
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Ensure custom rules array exists
|
|
122
|
+
if (!config.rules) config.rules = {};
|
|
123
|
+
if (!config.rules.custom) config.rules.custom = [];
|
|
124
|
+
|
|
125
|
+
// Add pack rules (skip duplicates by name)
|
|
126
|
+
let added = 0;
|
|
127
|
+
for (const rule of pack.rules) {
|
|
128
|
+
const exists = config.rules.custom.some(r => r.name === rule.name);
|
|
129
|
+
if (!exists) {
|
|
130
|
+
config.rules.custom.push({
|
|
131
|
+
...rule,
|
|
132
|
+
_source: packName,
|
|
133
|
+
_version: pack.version,
|
|
134
|
+
});
|
|
135
|
+
added++;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
140
|
+
|
|
141
|
+
// Record installation
|
|
142
|
+
const manifest = getInstalledManifest();
|
|
143
|
+
const existing = manifest.packs.findIndex(p => p.name === packName);
|
|
144
|
+
if (existing >= 0) {
|
|
145
|
+
manifest.packs[existing] = {
|
|
146
|
+
name: packName,
|
|
147
|
+
version: pack.version,
|
|
148
|
+
rules: pack.rules.length,
|
|
149
|
+
installedAt: new Date().toISOString(),
|
|
150
|
+
};
|
|
151
|
+
} else {
|
|
152
|
+
manifest.packs.push({
|
|
153
|
+
name: packName,
|
|
154
|
+
version: pack.version,
|
|
155
|
+
rules: pack.rules.length,
|
|
156
|
+
installedAt: new Date().toISOString(),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
saveInstalledManifest(manifest);
|
|
160
|
+
|
|
161
|
+
return { name: packName, version: pack.version, added, total: pack.rules.length };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── Publish ───────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
async function publishRules(apiUrl, options = {}) {
|
|
167
|
+
const configPath = path.join(process.cwd(), '.coderevrc.json');
|
|
168
|
+
if (!fs.existsSync(configPath)) {
|
|
169
|
+
throw new Error('No .coderevrc.json found. Run `coderev init` first.');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
173
|
+
const rules = config.rules?.custom || [];
|
|
174
|
+
|
|
175
|
+
if (rules.length === 0) {
|
|
176
|
+
throw new Error('No custom rules found in .coderevrc.json');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const packName = options.name || path.basename(process.cwd());
|
|
180
|
+
const payload = {
|
|
181
|
+
name: packName,
|
|
182
|
+
version: options.version || '1.0.0',
|
|
183
|
+
description: options.description || `Rules from ${packName}`,
|
|
184
|
+
rules: rules.map(r => ({
|
|
185
|
+
name: r.name,
|
|
186
|
+
pattern: r.pattern,
|
|
187
|
+
severity: r.severity || 'warning',
|
|
188
|
+
message: r.message,
|
|
189
|
+
filePattern: r.filePattern,
|
|
190
|
+
category: r.category || 'style',
|
|
191
|
+
})),
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const result = await apiRequest(apiUrl, '/rules', 'POST', payload);
|
|
195
|
+
|
|
196
|
+
return { name: packName, version: payload.version, rules: rules.length, published: true };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── List Installed ───────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
function listInstalled() {
|
|
202
|
+
const manifest = getInstalledManifest();
|
|
203
|
+
|
|
204
|
+
if (manifest.packs.length === 0) {
|
|
205
|
+
return { packs: [], message: 'No rule packs installed. Use `coderev rules search` to find rules.' };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return { packs: manifest.packs };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ── Uninstall ─────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
function uninstallRule(packName) {
|
|
214
|
+
const manifest = getInstalledManifest();
|
|
215
|
+
const idx = manifest.packs.findIndex(p => p.name === packName);
|
|
216
|
+
if (idx < 0) {
|
|
217
|
+
throw new Error(`Rule pack "${packName}" is not installed.`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
manifest.packs.splice(idx, 1);
|
|
221
|
+
saveInstalledManifest(manifest);
|
|
222
|
+
|
|
223
|
+
// Also remove from .coderevrc.json
|
|
224
|
+
const configPath = path.join(process.cwd(), '.coderevrc.json');
|
|
225
|
+
if (fs.existsSync(configPath)) {
|
|
226
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
227
|
+
if (config.rules?.custom) {
|
|
228
|
+
config.rules.custom = config.rules.custom.filter(r => r._source !== packName);
|
|
229
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return { name: packName, removed: true };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── Search Local ──────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
function searchLocalRules(query) {
|
|
239
|
+
const manifest = getInstalledManifest();
|
|
240
|
+
if (!query) return manifest.packs;
|
|
241
|
+
|
|
242
|
+
const q = query.toLowerCase();
|
|
243
|
+
return manifest.packs.filter(
|
|
244
|
+
p => p.name.toLowerCase().includes(q)
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
module.exports = {
|
|
249
|
+
searchRules,
|
|
250
|
+
installRule,
|
|
251
|
+
publishRules,
|
|
252
|
+
listInstalled,
|
|
253
|
+
uninstallRule,
|
|
254
|
+
searchLocalRules,
|
|
255
|
+
DEFAULT_API_URL,
|
|
256
|
+
getMarketplaceDir,
|
|
257
|
+
};
|