codegrunt 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +351 -0
- package/dist/cli/at-resolver.d.ts +10 -0
- package/dist/cli/at-resolver.js +138 -0
- package/dist/cli/at-resolver.js.map +1 -0
- package/dist/cli/banner.d.ts +1 -0
- package/dist/cli/banner.js +111 -0
- package/dist/cli/banner.js.map +1 -0
- package/dist/cli/commands.d.ts +25 -0
- package/dist/cli/commands.js +799 -0
- package/dist/cli/commands.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +142 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/input.d.ts +14 -0
- package/dist/cli/input.js +742 -0
- package/dist/cli/input.js.map +1 -0
- package/dist/cli/repl.d.ts +2 -0
- package/dist/cli/repl.js +217 -0
- package/dist/cli/repl.js.map +1 -0
- package/dist/cli/setup.d.ts +7 -0
- package/dist/cli/setup.js +82 -0
- package/dist/cli/setup.js.map +1 -0
- package/dist/cli/skills.d.ts +28 -0
- package/dist/cli/skills.js +299 -0
- package/dist/cli/skills.js.map +1 -0
- package/dist/cli/update.d.ts +7 -0
- package/dist/cli/update.js +135 -0
- package/dist/cli/update.js.map +1 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +93 -0
- package/dist/config.js.map +1 -0
- package/dist/core/agent/loop.d.ts +17 -0
- package/dist/core/agent/loop.js +353 -0
- package/dist/core/agent/loop.js.map +1 -0
- package/dist/core/context/manager.d.ts +26 -0
- package/dist/core/context/manager.js +98 -0
- package/dist/core/context/manager.js.map +1 -0
- package/dist/core/context/project-guide.d.ts +1 -0
- package/dist/core/context/project-guide.js +17 -0
- package/dist/core/context/project-guide.js.map +1 -0
- package/dist/core/tools/edit_file.d.ts +2 -0
- package/dist/core/tools/edit_file.js +54 -0
- package/dist/core/tools/edit_file.js.map +1 -0
- package/dist/core/tools/execute_shell.d.ts +2 -0
- package/dist/core/tools/execute_shell.js +67 -0
- package/dist/core/tools/execute_shell.js.map +1 -0
- package/dist/core/tools/executor.d.ts +3 -0
- package/dist/core/tools/executor.js +86 -0
- package/dist/core/tools/executor.js.map +1 -0
- package/dist/core/tools/list_directory.d.ts +2 -0
- package/dist/core/tools/list_directory.js +74 -0
- package/dist/core/tools/list_directory.js.map +1 -0
- package/dist/core/tools/read_file.d.ts +2 -0
- package/dist/core/tools/read_file.js +40 -0
- package/dist/core/tools/read_file.js.map +1 -0
- package/dist/core/tools/registry.d.ts +4 -0
- package/dist/core/tools/registry.js +24 -0
- package/dist/core/tools/registry.js.map +1 -0
- package/dist/core/tools/search_files.d.ts +2 -0
- package/dist/core/tools/search_files.js +88 -0
- package/dist/core/tools/search_files.js.map +1 -0
- package/dist/core/tools/write_file.d.ts +2 -0
- package/dist/core/tools/write_file.js +43 -0
- package/dist/core/tools/write_file.js.map +1 -0
- package/dist/providers/deepseek/client.d.ts +8 -0
- package/dist/providers/deepseek/client.js +27 -0
- package/dist/providers/deepseek/client.js.map +1 -0
- package/dist/providers/deepseek/provider.d.ts +8 -0
- package/dist/providers/deepseek/provider.js +165 -0
- package/dist/providers/deepseek/provider.js.map +1 -0
- package/dist/types.d.ts +111 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/billing.d.ts +40 -0
- package/dist/utils/billing.js +165 -0
- package/dist/utils/billing.js.map +1 -0
- package/dist/utils/confirm.d.ts +3 -0
- package/dist/utils/confirm.js +242 -0
- package/dist/utils/confirm.js.map +1 -0
- package/dist/utils/display.d.ts +10 -0
- package/dist/utils/display.js +121 -0
- package/dist/utils/display.js.map +1 -0
- package/dist/utils/interrupt.d.ts +5 -0
- package/dist/utils/interrupt.js +13 -0
- package/dist/utils/interrupt.js.map +1 -0
- package/dist/utils/markdown.d.ts +18 -0
- package/dist/utils/markdown.js +223 -0
- package/dist/utils/markdown.js.map +1 -0
- package/package.json +42 -0
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
import { readdir, readFile, writeFile, mkdir } from 'fs/promises';
|
|
2
|
+
import { join, resolve, relative } from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { DEEPSEEK_MODELS } from './setup.js';
|
|
5
|
+
import { getSessionUsage } from '../core/agent/loop.js';
|
|
6
|
+
import { printBalanceAndUsage, formatDualCurrency, PRICING } from '../utils/billing.js';
|
|
7
|
+
import { getGlobalSkillsDir, createSkill } from './skills.js';
|
|
8
|
+
import { validateApiKey } from '../providers/deepseek/client.js';
|
|
9
|
+
import { MarkdownRenderer } from '../utils/markdown.js';
|
|
10
|
+
export async function handleSlashCommand(input, cwd, config, provider, context, skills = []) {
|
|
11
|
+
if (!input.startsWith('/'))
|
|
12
|
+
return { type: 'not_a_command' };
|
|
13
|
+
const [cmd, ...rest] = input.slice(1).split(' ');
|
|
14
|
+
const args = rest.join(' ').trim();
|
|
15
|
+
switch (cmd.toLowerCase()) {
|
|
16
|
+
case 'help':
|
|
17
|
+
printHelp(config, skills);
|
|
18
|
+
return { type: 'handled' };
|
|
19
|
+
case 'clear':
|
|
20
|
+
context.clear();
|
|
21
|
+
console.log(chalk.gray('Context cleared.'));
|
|
22
|
+
return { type: 'clear' };
|
|
23
|
+
case 'compact':
|
|
24
|
+
await compactContext(context, config, provider);
|
|
25
|
+
return { type: 'handled' };
|
|
26
|
+
case 'init':
|
|
27
|
+
await runInit(cwd, config, provider, args);
|
|
28
|
+
return { type: 'handled' };
|
|
29
|
+
case 'model':
|
|
30
|
+
return await switchModel(args, config);
|
|
31
|
+
case 'reasoning':
|
|
32
|
+
case 'effort':
|
|
33
|
+
return switchReasoningEffort(args, config);
|
|
34
|
+
case 'token':
|
|
35
|
+
case 'apikey':
|
|
36
|
+
return await switchToken(args, config);
|
|
37
|
+
case 'config':
|
|
38
|
+
return await handleConfig(rest, config);
|
|
39
|
+
case 'cost':
|
|
40
|
+
printSessionCost(config.model);
|
|
41
|
+
return { type: 'handled' };
|
|
42
|
+
case 'balance':
|
|
43
|
+
await printBalanceAndUsage(config.apiKey, config.baseURL, config.model);
|
|
44
|
+
return { type: 'handled' };
|
|
45
|
+
case 'exit':
|
|
46
|
+
return { type: 'exit' };
|
|
47
|
+
case 'skills':
|
|
48
|
+
return await handleSkills(rest, skills);
|
|
49
|
+
case 'review':
|
|
50
|
+
await reviewContext(context, config, provider);
|
|
51
|
+
return { type: 'handled' };
|
|
52
|
+
default: {
|
|
53
|
+
// Check if it matches a loaded skill
|
|
54
|
+
const skill = skills.find((s) => s.name.toLowerCase() === cmd.toLowerCase());
|
|
55
|
+
if (skill) {
|
|
56
|
+
const prompt = args ? `${skill.content}\n\n${args}` : skill.content;
|
|
57
|
+
return { type: 'skill_run', prompt, system: skill.system };
|
|
58
|
+
}
|
|
59
|
+
console.log(chalk.yellow(`Unknown command: /${cmd}. Type /help for available commands.`));
|
|
60
|
+
return { type: 'handled' };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// ── /help ───────────────────────────────────────────────────────────────────
|
|
65
|
+
function printHelp(config, skills = []) {
|
|
66
|
+
const skillsSection = skills.length > 0
|
|
67
|
+
? `\n${chalk.bold('Skills')}\n\n` +
|
|
68
|
+
skills.map((s) => ` ${chalk.cyan('/' + s.name)}${' '.repeat(Math.max(1, 18 - s.name.length - 1))}${s.description ? chalk.gray(` — ${s.description}`) : chalk.gray(`(${s.source})`)}`).join('\n') + '\n'
|
|
69
|
+
: '';
|
|
70
|
+
console.log(`
|
|
71
|
+
${chalk.bold('Slash Commands')}
|
|
72
|
+
|
|
73
|
+
${chalk.cyan('/init')} Analyze the codebase and generate a CODEGRUNT.md project guide
|
|
74
|
+
${chalk.cyan('/model')} Switch model interactively
|
|
75
|
+
${chalk.cyan('/model <id>')} Switch to a specific model (e.g. /model deepseek-v4-pro)
|
|
76
|
+
${chalk.cyan('/config')} Show current configuration
|
|
77
|
+
${chalk.cyan('/config <key> [val]')} Set a config value interactively or directly
|
|
78
|
+
Keys: ${chalk.gray('temperature maxtokens topp frequencypenalty presencepenalty reasoning')}
|
|
79
|
+
${chalk.cyan('/reasoning')} Set reasoning effort for R1 models (low/medium/high)
|
|
80
|
+
${chalk.cyan('/effort <level>')} Shortcut: /effort low | /effort medium | /effort high
|
|
81
|
+
${chalk.cyan('/token [key]')} Update your DeepSeek API key
|
|
82
|
+
${chalk.cyan('/cost')} Show session token usage and cost (DeepSeek pricing)
|
|
83
|
+
${chalk.cyan('/balance')} Show account balance, today's & this month's usage
|
|
84
|
+
${chalk.cyan('/skills')} List and manage skills (create, list)
|
|
85
|
+
${chalk.cyan('/review')} Review session changes for logic issues
|
|
86
|
+
${chalk.cyan('/help')} Show this help message
|
|
87
|
+
${chalk.cyan('/clear')} Clear conversation context
|
|
88
|
+
${chalk.cyan('/compact')} Summarize and compress conversation history to save tokens
|
|
89
|
+
${chalk.cyan('/exit')} Exit CodeGrunt
|
|
90
|
+
${skillsSection}
|
|
91
|
+
${chalk.bold('@ References')}
|
|
92
|
+
|
|
93
|
+
${chalk.cyan('@<file>')} Inject file contents into your message (e.g. @src/index.ts)
|
|
94
|
+
${chalk.cyan('@<directory>')} Inject directory listing (e.g. @src/)
|
|
95
|
+
${chalk.cyan('@<url>')} Fetch and inject webpage content (e.g. @https://example.com)
|
|
96
|
+
|
|
97
|
+
${chalk.bold('Current')}
|
|
98
|
+
|
|
99
|
+
temperature: ${chalk.cyan(String(config.temperature))} max_tokens: ${chalk.cyan(String(config.maxTokens))} top_p: ${chalk.cyan(String(config.topP ?? 1))}${config.reasoningEffort ? chalk.gray(` reasoning: ${config.reasoningEffort}`) : ''}
|
|
100
|
+
|
|
101
|
+
${chalk.bold('Other')}
|
|
102
|
+
|
|
103
|
+
${chalk.cyan('exit')} / ${chalk.cyan('quit')} Exit CodeGrunt
|
|
104
|
+
${chalk.cyan('Ctrl+C')} Interrupt a running task
|
|
105
|
+
`);
|
|
106
|
+
}
|
|
107
|
+
// ── /cost ───────────────────────────────────────────────────────────────────
|
|
108
|
+
function printSessionCost(model) {
|
|
109
|
+
const usage = getSessionUsage();
|
|
110
|
+
const pricing = PRICING[model] ?? PRICING['deepseek-chat'];
|
|
111
|
+
const inputCost = (usage.inputTokens / 1_000_000) * pricing.prompt;
|
|
112
|
+
const outputCost = (usage.outputTokens / 1_000_000) * pricing.completion;
|
|
113
|
+
const cacheSavings = (usage.cacheHitTokens / 1_000_000) * (pricing.prompt - pricing.cacheHit);
|
|
114
|
+
const totalCost = inputCost + outputCost - cacheSavings;
|
|
115
|
+
console.log(`
|
|
116
|
+
${chalk.bold('Session Usage')}
|
|
117
|
+
${chalk.gray('Model:')} ${chalk.cyan(model)}
|
|
118
|
+
${chalk.gray('Input tokens:')} ${usage.inputTokens.toLocaleString()}${usage.cacheHitTokens > 0 ? chalk.green(` (${usage.cacheHitTokens.toLocaleString()} cache hits)`) : ''}
|
|
119
|
+
${chalk.gray('Output tokens:')} ${usage.outputTokens.toLocaleString()}
|
|
120
|
+
${chalk.gray('Total tokens:')} ${(usage.inputTokens + usage.outputTokens).toLocaleString()}
|
|
121
|
+
${chalk.gray('─'.repeat(30))}
|
|
122
|
+
${chalk.gray('Input cost:')} ${formatDualCurrency(inputCost)}
|
|
123
|
+
${chalk.gray('Output cost:')} ${formatDualCurrency(outputCost)}${cacheSavings > 0 ? chalk.green(`\n ${chalk.gray('Cache saved:')} -${formatDualCurrency(cacheSavings)}`) : ''}
|
|
124
|
+
${chalk.bold('Session cost:')} ${formatDualCurrency(totalCost)}
|
|
125
|
+
`);
|
|
126
|
+
}
|
|
127
|
+
async function switchReasoningEffort(arg, config) {
|
|
128
|
+
const validEfforts = ['low', 'medium', 'high'];
|
|
129
|
+
if (arg && validEfforts.includes(arg)) {
|
|
130
|
+
const effort = arg;
|
|
131
|
+
console.log(chalk.green(`✓ Reasoning effort set to ${chalk.bold(effort)}`) +
|
|
132
|
+
chalk.gray(' (only applies to reasoner/R1 models)'));
|
|
133
|
+
return { type: 'config_changed', config: { ...config, reasoningEffort: effort } };
|
|
134
|
+
}
|
|
135
|
+
// Interactive picker
|
|
136
|
+
const { selectFromList } = await import('./input.js');
|
|
137
|
+
const selected = await selectFromList('Select reasoning effort (only applies to R1/reasoner models)', [
|
|
138
|
+
{ value: 'low', label: 'Low', desc: 'Faster responses, less thinking' },
|
|
139
|
+
{ value: 'medium', label: 'Medium', desc: 'Balanced (default)' },
|
|
140
|
+
{ value: 'high', label: 'High', desc: 'Most thorough, slower responses' },
|
|
141
|
+
], config.reasoningEffort ?? 'medium');
|
|
142
|
+
if (!selected || selected === config.reasoningEffort) {
|
|
143
|
+
console.log(chalk.gray('Reasoning effort unchanged.'));
|
|
144
|
+
return { type: 'handled' };
|
|
145
|
+
}
|
|
146
|
+
console.log(chalk.green(`✓ Reasoning effort set to ${chalk.bold(selected)}`));
|
|
147
|
+
return {
|
|
148
|
+
type: 'config_changed',
|
|
149
|
+
config: { ...config, reasoningEffort: selected },
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
async function switchToken(arg, config) {
|
|
153
|
+
// If an argument is provided, use it directly
|
|
154
|
+
if (arg) {
|
|
155
|
+
const trimmed = arg.trim();
|
|
156
|
+
if (trimmed.length < 10) {
|
|
157
|
+
console.log(chalk.yellow('API key seems too short. Please check and try again.'));
|
|
158
|
+
return { type: 'handled' };
|
|
159
|
+
}
|
|
160
|
+
process.stdout.write(chalk.gray('Validating API key…'));
|
|
161
|
+
const err = await validateApiKey(trimmed, config.baseURL);
|
|
162
|
+
process.stdout.write('\r' + ' '.repeat(30) + '\r');
|
|
163
|
+
if (err) {
|
|
164
|
+
console.log(chalk.red(`✗ ${err} Key not saved.`));
|
|
165
|
+
return { type: 'handled' };
|
|
166
|
+
}
|
|
167
|
+
console.log(chalk.green('✓ API key updated'));
|
|
168
|
+
console.log(chalk.gray(` Key: ${trimmed.slice(0, 4)}...${trimmed.slice(-4)}`));
|
|
169
|
+
return { type: 'config_changed', config: { ...config, apiKey: trimmed } };
|
|
170
|
+
}
|
|
171
|
+
// Interactive input (readline for direct text input)
|
|
172
|
+
// try/finally ensures rl.close() is always called even if the prompt throws.
|
|
173
|
+
const readline = await import('readline');
|
|
174
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
175
|
+
const ask = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
|
|
176
|
+
let newKey = '';
|
|
177
|
+
try {
|
|
178
|
+
console.log(chalk.gray('Enter your new DeepSeek API key (get one at https://platform.deepseek.com/api_keys):'));
|
|
179
|
+
newKey = (await ask(chalk.bold('API Key: '))).trim();
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
rl.close();
|
|
183
|
+
}
|
|
184
|
+
if (!newKey) {
|
|
185
|
+
console.log(chalk.gray('API key unchanged.'));
|
|
186
|
+
return { type: 'handled' };
|
|
187
|
+
}
|
|
188
|
+
if (newKey.length < 10) {
|
|
189
|
+
console.log(chalk.yellow('API key seems too short. Key unchanged.'));
|
|
190
|
+
return { type: 'handled' };
|
|
191
|
+
}
|
|
192
|
+
process.stdout.write(chalk.gray('Validating API key…'));
|
|
193
|
+
const err = await validateApiKey(newKey, config.baseURL);
|
|
194
|
+
process.stdout.write('\r' + ' '.repeat(30) + '\r');
|
|
195
|
+
if (err) {
|
|
196
|
+
console.log(chalk.red(`✗ ${err} Key not saved.`));
|
|
197
|
+
return { type: 'handled' };
|
|
198
|
+
}
|
|
199
|
+
console.log(chalk.green('✓ API key updated'));
|
|
200
|
+
console.log(chalk.gray(` Key: ${newKey.slice(0, 4)}...${newKey.slice(-4)}`));
|
|
201
|
+
return { type: 'config_changed', config: { ...config, apiKey: newKey } };
|
|
202
|
+
}
|
|
203
|
+
// ── /config ─────────────────────────────────────────────────────────────────
|
|
204
|
+
// /config — show current config
|
|
205
|
+
// /config temperature [val] — set temperature
|
|
206
|
+
// /config maxtokens [val] — set max tokens
|
|
207
|
+
// /config topp [val] — set top-p
|
|
208
|
+
// /config frequencypenalty [val] — set frequency penalty
|
|
209
|
+
// /config presencepenalty [val] — set presence penalty
|
|
210
|
+
// /config reasoning [level] — set reasoning effort
|
|
211
|
+
async function handleConfig(rest, config) {
|
|
212
|
+
const sub = rest[0]?.toLowerCase();
|
|
213
|
+
const val = rest.slice(1).join(' ').trim();
|
|
214
|
+
switch (sub) {
|
|
215
|
+
case 'temperature':
|
|
216
|
+
case 'temp':
|
|
217
|
+
return switchTemperature(val, config);
|
|
218
|
+
case 'maxtokens':
|
|
219
|
+
case 'max_tokens':
|
|
220
|
+
return switchMaxTokens(val, config);
|
|
221
|
+
case 'topp':
|
|
222
|
+
case 'top_p':
|
|
223
|
+
return switchTopP(val, config);
|
|
224
|
+
case 'frequencypenalty':
|
|
225
|
+
case 'frequency_penalty':
|
|
226
|
+
return switchFrequencyPenalty(val, config);
|
|
227
|
+
case 'presencepenalty':
|
|
228
|
+
case 'presence_penalty':
|
|
229
|
+
return switchPresencePenalty(val, config);
|
|
230
|
+
case 'reasoning':
|
|
231
|
+
case 'effort':
|
|
232
|
+
return switchReasoningEffort(val, config);
|
|
233
|
+
default:
|
|
234
|
+
// /config with no args — show current config overview
|
|
235
|
+
if (!sub) {
|
|
236
|
+
printConfigOverview(config);
|
|
237
|
+
return { type: 'handled' };
|
|
238
|
+
}
|
|
239
|
+
console.log(chalk.yellow(`Unknown config key: ${sub}\n`) +
|
|
240
|
+
chalk.gray('Available: temperature, maxtokens, topp, frequencypenalty, presencepenalty, reasoning'));
|
|
241
|
+
return { type: 'handled' };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function printConfigOverview(config) {
|
|
245
|
+
console.log(`
|
|
246
|
+
${chalk.bold('Current Configuration')}
|
|
247
|
+
|
|
248
|
+
${chalk.gray('temperature:')} ${chalk.cyan(String(config.temperature))}
|
|
249
|
+
${chalk.gray('max_tokens:')} ${chalk.cyan(String(config.maxTokens))}
|
|
250
|
+
${chalk.gray('top_p:')} ${chalk.cyan(String(config.topP ?? '1'))}
|
|
251
|
+
${chalk.gray('frequency_penalty:')} ${chalk.cyan(String(config.frequencyPenalty ?? '0'))}
|
|
252
|
+
${chalk.gray('presence_penalty:')} ${chalk.cyan(String(config.presencePenalty ?? '0'))}
|
|
253
|
+
${chalk.gray('reasoning_effort:')} ${chalk.cyan(config.reasoningEffort ?? 'medium')}
|
|
254
|
+
|
|
255
|
+
${chalk.gray('Use /config <key> <value> to change a setting, e.g. /config temperature 0.8')}
|
|
256
|
+
`);
|
|
257
|
+
}
|
|
258
|
+
// ── /temperature (via /config temperature) ──────────────────────────────────
|
|
259
|
+
async function switchTemperature(arg, config) {
|
|
260
|
+
if (arg) {
|
|
261
|
+
const val = parseFloat(arg);
|
|
262
|
+
if (isNaN(val) || val < 0 || val > 2) {
|
|
263
|
+
console.log(chalk.yellow('Temperature must be a number between 0 and 2.'));
|
|
264
|
+
return { type: 'handled' };
|
|
265
|
+
}
|
|
266
|
+
console.log(chalk.green(`✓ Temperature set to ${chalk.bold(String(val))}`));
|
|
267
|
+
return { type: 'config_changed', config: { ...config, temperature: val } };
|
|
268
|
+
}
|
|
269
|
+
const { selectFromList } = await import('./input.js');
|
|
270
|
+
const selected = await selectFromList('Select temperature (0 = deterministic, 1 = balanced, 2 = creative)', [
|
|
271
|
+
{ value: '0', label: '0.0', desc: 'Deterministic, consistent output' },
|
|
272
|
+
{ value: '0.2', label: '0.2', desc: 'Mostly deterministic (default)' },
|
|
273
|
+
{ value: '0.5', label: '0.5', desc: 'Balanced' },
|
|
274
|
+
{ value: '0.8', label: '0.8', desc: 'More creative' },
|
|
275
|
+
{ value: '1.0', label: '1.0', desc: 'Creative' },
|
|
276
|
+
{ value: '1.5', label: '1.5', desc: 'Very creative' },
|
|
277
|
+
{ value: '2.0', label: '2.0', desc: 'Maximum creativity' },
|
|
278
|
+
], String(config.temperature));
|
|
279
|
+
if (!selected || parseFloat(selected) === config.temperature) {
|
|
280
|
+
console.log(chalk.gray('Temperature unchanged.'));
|
|
281
|
+
return { type: 'handled' };
|
|
282
|
+
}
|
|
283
|
+
const val = parseFloat(selected);
|
|
284
|
+
console.log(chalk.green(`✓ Temperature set to ${chalk.bold(String(val))}`));
|
|
285
|
+
return { type: 'config_changed', config: { ...config, temperature: val } };
|
|
286
|
+
}
|
|
287
|
+
// ── /maxtokens ──────────────────────────────────────────────────────────────
|
|
288
|
+
async function switchMaxTokens(arg, config) {
|
|
289
|
+
if (arg) {
|
|
290
|
+
const val = parseInt(arg, 10);
|
|
291
|
+
if (isNaN(val) || val < 256 || val > 65536) {
|
|
292
|
+
console.log(chalk.yellow('Max tokens must be an integer between 256 and 65536.'));
|
|
293
|
+
return { type: 'handled' };
|
|
294
|
+
}
|
|
295
|
+
console.log(chalk.green(`✓ Max tokens set to ${chalk.bold(String(val))}`));
|
|
296
|
+
return { type: 'config_changed', config: { ...config, maxTokens: val } };
|
|
297
|
+
}
|
|
298
|
+
const { selectFromList } = await import('./input.js');
|
|
299
|
+
const selected = await selectFromList('Select max tokens per response', [
|
|
300
|
+
{ value: '1024', label: '1024', desc: 'Short responses' },
|
|
301
|
+
{ value: '2048', label: '2048', desc: 'Medium responses' },
|
|
302
|
+
{ value: '4096', label: '4096', desc: 'Standard length' },
|
|
303
|
+
{ value: '8192', label: '8192', desc: 'Long responses (default)' },
|
|
304
|
+
{ value: '16384', label: '16384', desc: 'Very long responses' },
|
|
305
|
+
{ value: '32768', label: '32768', desc: 'Maximum length responses' },
|
|
306
|
+
], String(config.maxTokens));
|
|
307
|
+
if (!selected || parseInt(selected, 10) === config.maxTokens) {
|
|
308
|
+
console.log(chalk.gray('Max tokens unchanged.'));
|
|
309
|
+
return { type: 'handled' };
|
|
310
|
+
}
|
|
311
|
+
const val = parseInt(selected, 10);
|
|
312
|
+
console.log(chalk.green(`✓ Max tokens set to ${chalk.bold(String(val))}`));
|
|
313
|
+
return { type: 'config_changed', config: { ...config, maxTokens: val } };
|
|
314
|
+
}
|
|
315
|
+
// ── /topp ───────────────────────────────────────────────────────────────────
|
|
316
|
+
async function switchTopP(arg, config) {
|
|
317
|
+
if (arg) {
|
|
318
|
+
const val = parseFloat(arg);
|
|
319
|
+
if (isNaN(val) || val < 0 || val > 1) {
|
|
320
|
+
console.log(chalk.yellow('Top-p must be a number between 0 and 1.'));
|
|
321
|
+
return { type: 'handled' };
|
|
322
|
+
}
|
|
323
|
+
console.log(chalk.green(`✓ Top-p set to ${chalk.bold(String(val))}`));
|
|
324
|
+
return { type: 'config_changed', config: { ...config, topP: val } };
|
|
325
|
+
}
|
|
326
|
+
const { selectFromList } = await import('./input.js');
|
|
327
|
+
const selected = await selectFromList('Select top-p (nucleus sampling)', [
|
|
328
|
+
{ value: '1', label: '1.0', desc: 'Consider all tokens (default)' },
|
|
329
|
+
{ value: '0.9', label: '0.9', desc: 'Top 90% probability mass' },
|
|
330
|
+
{ value: '0.8', label: '0.8', desc: 'Top 80%' },
|
|
331
|
+
{ value: '0.7', label: '0.7', desc: 'Top 70%' },
|
|
332
|
+
{ value: '0.5', label: '0.5', desc: 'Top 50% (more focused)' },
|
|
333
|
+
], config.topP !== undefined ? String(config.topP) : '1');
|
|
334
|
+
if (!selected || (config.topP !== undefined && parseFloat(selected) === config.topP)) {
|
|
335
|
+
console.log(chalk.gray('Top-p unchanged.'));
|
|
336
|
+
return { type: 'handled' };
|
|
337
|
+
}
|
|
338
|
+
const val = parseFloat(selected);
|
|
339
|
+
console.log(chalk.green(`✓ Top-p set to ${chalk.bold(String(val))}`));
|
|
340
|
+
return { type: 'config_changed', config: { ...config, topP: val } };
|
|
341
|
+
}
|
|
342
|
+
// ── /frequencypenalty ───────────────────────────────────────────────────────
|
|
343
|
+
async function switchFrequencyPenalty(arg, config) {
|
|
344
|
+
if (arg) {
|
|
345
|
+
const val = parseFloat(arg);
|
|
346
|
+
if (isNaN(val) || val < -2 || val > 2) {
|
|
347
|
+
console.log(chalk.yellow('Frequency penalty must be a number between -2 and 2.'));
|
|
348
|
+
return { type: 'handled' };
|
|
349
|
+
}
|
|
350
|
+
console.log(chalk.green(`✓ Frequency penalty set to ${chalk.bold(String(val))}`));
|
|
351
|
+
return { type: 'config_changed', config: { ...config, frequencyPenalty: val } };
|
|
352
|
+
}
|
|
353
|
+
const { selectFromList } = await import('./input.js');
|
|
354
|
+
const selected = await selectFromList('Select frequency penalty (-2 = encourage repetition, 2 = discourage)', [
|
|
355
|
+
{ value: '0', label: '0.0', desc: 'No penalty (default)' },
|
|
356
|
+
{ value: '0.3', label: '0.3', desc: 'Slight repetition reduction' },
|
|
357
|
+
{ value: '0.6', label: '0.6', desc: 'Moderate repetition reduction' },
|
|
358
|
+
{ value: '1.0', label: '1.0', desc: 'Strong repetition reduction' },
|
|
359
|
+
{ value: '1.5', label: '1.5', desc: 'Very strong reduction' },
|
|
360
|
+
{ value: '2.0', label: '2.0', desc: 'Maximum reduction' },
|
|
361
|
+
], config.frequencyPenalty !== undefined ? String(config.frequencyPenalty) : '0');
|
|
362
|
+
if (!selected || (config.frequencyPenalty !== undefined && parseFloat(selected) === config.frequencyPenalty)) {
|
|
363
|
+
console.log(chalk.gray('Frequency penalty unchanged.'));
|
|
364
|
+
return { type: 'handled' };
|
|
365
|
+
}
|
|
366
|
+
const val = parseFloat(selected);
|
|
367
|
+
console.log(chalk.green(`✓ Frequency penalty set to ${chalk.bold(String(val))}`));
|
|
368
|
+
return { type: 'config_changed', config: { ...config, frequencyPenalty: val } };
|
|
369
|
+
}
|
|
370
|
+
// ── /presencepenalty ────────────────────────────────────────────────────────
|
|
371
|
+
async function switchPresencePenalty(arg, config) {
|
|
372
|
+
if (arg) {
|
|
373
|
+
const val = parseFloat(arg);
|
|
374
|
+
if (isNaN(val) || val < -2 || val > 2) {
|
|
375
|
+
console.log(chalk.yellow('Presence penalty must be a number between -2 and 2.'));
|
|
376
|
+
return { type: 'handled' };
|
|
377
|
+
}
|
|
378
|
+
console.log(chalk.green(`✓ Presence penalty set to ${chalk.bold(String(val))}`));
|
|
379
|
+
return { type: 'config_changed', config: { ...config, presencePenalty: val } };
|
|
380
|
+
}
|
|
381
|
+
const { selectFromList } = await import('./input.js');
|
|
382
|
+
const selected = await selectFromList('Select presence penalty (-2 = encourage same topics, 2 = encourage new topics)', [
|
|
383
|
+
{ value: '0', label: '0.0', desc: 'No penalty (default)' },
|
|
384
|
+
{ value: '0.3', label: '0.3', desc: 'Slight topic diversity' },
|
|
385
|
+
{ value: '0.6', label: '0.6', desc: 'Moderate topic diversity' },
|
|
386
|
+
{ value: '1.0', label: '1.0', desc: 'Strong topic diversity' },
|
|
387
|
+
{ value: '1.5', label: '1.5', desc: 'Very strong diversity' },
|
|
388
|
+
{ value: '2.0', label: '2.0', desc: 'Maximum diversity' },
|
|
389
|
+
], config.presencePenalty !== undefined ? String(config.presencePenalty) : '0');
|
|
390
|
+
if (!selected || (config.presencePenalty !== undefined && parseFloat(selected) === config.presencePenalty)) {
|
|
391
|
+
console.log(chalk.gray('Presence penalty unchanged.'));
|
|
392
|
+
return { type: 'handled' };
|
|
393
|
+
}
|
|
394
|
+
const val = parseFloat(selected);
|
|
395
|
+
console.log(chalk.green(`✓ Presence penalty set to ${chalk.bold(String(val))}`));
|
|
396
|
+
return { type: 'config_changed', config: { ...config, presencePenalty: val } };
|
|
397
|
+
}
|
|
398
|
+
async function switchModel(arg, config) {
|
|
399
|
+
// /model deepseek-v4-pro — direct switch by ID
|
|
400
|
+
if (arg) {
|
|
401
|
+
const match = DEEPSEEK_MODELS.find((m) => m.id === arg || m.label.toLowerCase() === arg.toLowerCase());
|
|
402
|
+
if (!match) {
|
|
403
|
+
console.log(chalk.yellow(`Unknown model: ${arg}`));
|
|
404
|
+
console.log(chalk.gray('Available: ' + DEEPSEEK_MODELS.map((m) => m.id).join(', ')));
|
|
405
|
+
return { type: 'handled' };
|
|
406
|
+
}
|
|
407
|
+
console.log(chalk.green(`✓ Switched to ${chalk.bold(match.label)}`) + chalk.gray(` (${match.id})`));
|
|
408
|
+
return { type: 'model_changed', config: { ...config, model: match.id } };
|
|
409
|
+
}
|
|
410
|
+
// /model — arrow-key dropdown picker
|
|
411
|
+
const { selectFromList } = await import('./input.js');
|
|
412
|
+
const selected = await selectFromList('Select model', DEEPSEEK_MODELS.map((m) => ({ value: m.id, label: m.label, desc: m.description })), config.model);
|
|
413
|
+
if (!selected || selected === config.model) {
|
|
414
|
+
console.log(chalk.gray('Model unchanged.'));
|
|
415
|
+
return { type: 'handled' };
|
|
416
|
+
}
|
|
417
|
+
const match = DEEPSEEK_MODELS.find((m) => m.id === selected);
|
|
418
|
+
console.log(chalk.green(`✓ Switched to ${chalk.bold(match.label)}`) + chalk.gray(` (${selected})`));
|
|
419
|
+
return { type: 'model_changed', config: { ...config, model: selected } };
|
|
420
|
+
}
|
|
421
|
+
// ── /clear ──────────────────────────────────────────────────────────────────
|
|
422
|
+
// Handled inline above via context.clear()
|
|
423
|
+
// ── /compact ────────────────────────────────────────────────────────────────
|
|
424
|
+
async function compactContext(context, config, provider) {
|
|
425
|
+
const messages = context.getMessages();
|
|
426
|
+
const nonSystem = messages.filter((m) => m.role !== 'system');
|
|
427
|
+
if (nonSystem.length < 4) {
|
|
428
|
+
console.log(chalk.gray('Context is already short, nothing to compact.'));
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
process.stdout.write(chalk.gray('Compacting context…'));
|
|
432
|
+
const summaryMessages = [
|
|
433
|
+
{
|
|
434
|
+
role: 'system',
|
|
435
|
+
content: 'You are a helpful assistant. Summarize the following conversation concisely, preserving key decisions, code changes made, and any important context needed to continue the work.',
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
role: 'user',
|
|
439
|
+
content: nonSystem
|
|
440
|
+
.map((m) => {
|
|
441
|
+
const role = m.role.toUpperCase();
|
|
442
|
+
const content = 'content' in m && m.content ? String(m.content) : '[tool call]';
|
|
443
|
+
return `${role}: ${content}`;
|
|
444
|
+
})
|
|
445
|
+
.join('\n\n'),
|
|
446
|
+
},
|
|
447
|
+
];
|
|
448
|
+
let summary = '';
|
|
449
|
+
try {
|
|
450
|
+
const stream = provider.stream(summaryMessages, {
|
|
451
|
+
model: config.model,
|
|
452
|
+
maxTokens: 1024,
|
|
453
|
+
temperature: 0.2,
|
|
454
|
+
});
|
|
455
|
+
for await (const chunk of stream) {
|
|
456
|
+
if (chunk.type === 'text_delta')
|
|
457
|
+
summary += chunk.text;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
catch (err) {
|
|
461
|
+
console.log(chalk.red('\nFailed to compact: ' + (err instanceof Error ? err.message : String(err))));
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const systemMsg = messages.find((m) => m.role === 'system');
|
|
465
|
+
context.clear();
|
|
466
|
+
if (systemMsg)
|
|
467
|
+
context.push(systemMsg);
|
|
468
|
+
context.push({
|
|
469
|
+
role: 'user',
|
|
470
|
+
content: `[Previous conversation summary]\n${summary}`,
|
|
471
|
+
});
|
|
472
|
+
context.push({
|
|
473
|
+
role: 'assistant',
|
|
474
|
+
content: 'Understood. I have the context from our previous conversation and am ready to continue.',
|
|
475
|
+
});
|
|
476
|
+
process.stdout.write(chalk.green(' done\n'));
|
|
477
|
+
console.log(chalk.gray(`Reduced to ${context.getMessages().length} messages.\n`));
|
|
478
|
+
}
|
|
479
|
+
// ── /init ────────────────────────────────────────────────────────────────────
|
|
480
|
+
const INIT_SKIP = new Set(['node_modules', '.git', 'dist', '.next', '__pycache__', '.cache', 'coverage']);
|
|
481
|
+
const INIT_KEY_FILES = [
|
|
482
|
+
'package.json', 'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock',
|
|
483
|
+
'tsconfig.json', 'tsconfig.base.json',
|
|
484
|
+
'vite.config.ts', 'vite.config.js',
|
|
485
|
+
'vitest.config.ts', 'jest.config.ts', 'jest.config.js',
|
|
486
|
+
'.eslintrc', '.eslintrc.js', '.eslintrc.json', 'eslint.config.js',
|
|
487
|
+
'Makefile', 'Dockerfile', 'docker-compose.yml',
|
|
488
|
+
'pyproject.toml', 'setup.py', 'requirements.txt',
|
|
489
|
+
'Cargo.toml', 'go.mod',
|
|
490
|
+
'README.md', 'CLAUDE.md', 'CODEGRUNT.md',
|
|
491
|
+
];
|
|
492
|
+
async function runInit(cwd, config, provider, outputFile) {
|
|
493
|
+
const outPath = resolve(cwd, outputFile || 'CODEGRUNT.md');
|
|
494
|
+
console.log(chalk.gray(`Analyzing codebase at ${cwd}…\n`));
|
|
495
|
+
// 1. Collect file tree
|
|
496
|
+
const tree = await buildFileTree(cwd);
|
|
497
|
+
// 2. Read key config files
|
|
498
|
+
const keyContents = await readKeyFiles(cwd);
|
|
499
|
+
// 3. Sample a few source files for architecture hints
|
|
500
|
+
const sourceSamples = await sampleSourceFiles(cwd);
|
|
501
|
+
const prompt = buildInitPrompt(cwd, tree, keyContents, sourceSamples, outPath);
|
|
502
|
+
process.stdout.write(chalk.gray('Generating project guide'));
|
|
503
|
+
let output = '';
|
|
504
|
+
try {
|
|
505
|
+
const stream = provider.stream([{ role: 'user', content: prompt }], { model: config.model, maxTokens: 4096, temperature: 0.2 });
|
|
506
|
+
for await (const chunk of stream) {
|
|
507
|
+
if (chunk.type === 'text_delta') {
|
|
508
|
+
output += chunk.text;
|
|
509
|
+
process.stdout.write('.');
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
catch (err) {
|
|
514
|
+
console.log(chalk.red('\nFailed: ' + (err instanceof Error ? err.message : String(err))));
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
// Strip markdown code fence if model wrapped the output
|
|
518
|
+
const cleaned = output.replace(/^```markdown\n?/, '').replace(/\n?```$/, '').trim();
|
|
519
|
+
await mkdir(resolve(cwd), { recursive: true });
|
|
520
|
+
await writeFile(outPath, cleaned + '\n', 'utf-8');
|
|
521
|
+
process.stdout.write('\n');
|
|
522
|
+
console.log(chalk.green(`✓ Written to ${relative(cwd, outPath)}\n`));
|
|
523
|
+
}
|
|
524
|
+
async function buildFileTree(cwd) {
|
|
525
|
+
const lines = [];
|
|
526
|
+
await walkTree(cwd, cwd, 0, 3, lines);
|
|
527
|
+
return lines.join('\n');
|
|
528
|
+
}
|
|
529
|
+
async function walkTree(root, dir, depth, maxDepth, lines) {
|
|
530
|
+
if (depth > maxDepth || lines.length > 150)
|
|
531
|
+
return;
|
|
532
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
533
|
+
entries.sort((a, b) => {
|
|
534
|
+
if (a.isDirectory() !== b.isDirectory())
|
|
535
|
+
return a.isDirectory() ? -1 : 1;
|
|
536
|
+
return a.name.localeCompare(b.name);
|
|
537
|
+
});
|
|
538
|
+
for (const entry of entries) {
|
|
539
|
+
if (entry.name.startsWith('.') && depth > 0)
|
|
540
|
+
continue;
|
|
541
|
+
if (INIT_SKIP.has(entry.name))
|
|
542
|
+
continue;
|
|
543
|
+
const indent = ' '.repeat(depth);
|
|
544
|
+
if (entry.isDirectory()) {
|
|
545
|
+
lines.push(`${indent}${entry.name}/`);
|
|
546
|
+
await walkTree(root, join(dir, entry.name), depth + 1, maxDepth, lines);
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
lines.push(`${indent}${entry.name}`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
async function readKeyFiles(cwd) {
|
|
554
|
+
const result = {};
|
|
555
|
+
for (const name of INIT_KEY_FILES) {
|
|
556
|
+
const p = join(cwd, name);
|
|
557
|
+
try {
|
|
558
|
+
const content = await readFile(p, 'utf-8');
|
|
559
|
+
result[name] = content.length > 3000 ? content.slice(0, 3000) + '\n[truncated]' : content;
|
|
560
|
+
}
|
|
561
|
+
catch {
|
|
562
|
+
// file doesn't exist
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return result;
|
|
566
|
+
}
|
|
567
|
+
async function sampleSourceFiles(cwd) {
|
|
568
|
+
const result = {};
|
|
569
|
+
const candidates = [];
|
|
570
|
+
await collectSourceCandidates(cwd, cwd, candidates);
|
|
571
|
+
// Take up to 5 files
|
|
572
|
+
for (const p of candidates.slice(0, 5)) {
|
|
573
|
+
try {
|
|
574
|
+
const content = await readFile(p, 'utf-8');
|
|
575
|
+
const rel = relative(cwd, p);
|
|
576
|
+
result[rel] = content.length > 2000 ? content.slice(0, 2000) + '\n[truncated]' : content;
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
// skip
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return result;
|
|
583
|
+
}
|
|
584
|
+
async function collectSourceCandidates(root, dir, out) {
|
|
585
|
+
if (out.length >= 10)
|
|
586
|
+
return;
|
|
587
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
588
|
+
for (const entry of entries) {
|
|
589
|
+
if (out.length >= 10)
|
|
590
|
+
return;
|
|
591
|
+
if (entry.name.startsWith('.'))
|
|
592
|
+
continue;
|
|
593
|
+
const full = join(dir, entry.name);
|
|
594
|
+
if (entry.isDirectory()) {
|
|
595
|
+
if (INIT_SKIP.has(entry.name))
|
|
596
|
+
continue;
|
|
597
|
+
await collectSourceCandidates(root, full, out);
|
|
598
|
+
}
|
|
599
|
+
else if (/\.(ts|tsx|js|jsx|py|go|rs|java)$/.test(entry.name)) {
|
|
600
|
+
out.push(full);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
function buildInitPrompt(cwd, tree, keyFiles, sourceSamples, outPath) {
|
|
605
|
+
const keyFilesSection = Object.entries(keyFiles)
|
|
606
|
+
.map(([name, content]) => `### ${name}\n\`\`\`\n${content}\n\`\`\``)
|
|
607
|
+
.join('\n\n');
|
|
608
|
+
const sourceSamplesSection = Object.entries(sourceSamples)
|
|
609
|
+
.map(([name, content]) => `### ${name}\n\`\`\`\n${content}\n\`\`\``)
|
|
610
|
+
.join('\n\n');
|
|
611
|
+
return `Analyze this codebase and write a concise developer guide in Markdown.
|
|
612
|
+
|
|
613
|
+
The guide will be saved as ${outPath} and read by AI coding assistants to understand the project quickly.
|
|
614
|
+
|
|
615
|
+
## File tree
|
|
616
|
+
\`\`\`
|
|
617
|
+
${tree}
|
|
618
|
+
\`\`\`
|
|
619
|
+
|
|
620
|
+
## Key config files
|
|
621
|
+
${keyFilesSection || '(none found)'}
|
|
622
|
+
|
|
623
|
+
## Source file samples
|
|
624
|
+
${sourceSamplesSection || '(none found)'}
|
|
625
|
+
|
|
626
|
+
## Instructions
|
|
627
|
+
Write a Markdown document with these sections (only include sections that are relevant):
|
|
628
|
+
|
|
629
|
+
1. **Build & Dev Commands** — exact commands to build, run, test, lint. Include how to run a single test.
|
|
630
|
+
2. **Architecture** — high-level structure: what each major directory/module does, key data flows. Focus on non-obvious things that require reading multiple files to understand.
|
|
631
|
+
3. **Key Patterns & Conventions** — coding patterns, naming conventions, or architectural decisions that are specific to this project.
|
|
632
|
+
4. **Configuration** — environment variables, config files, and their effects.
|
|
633
|
+
|
|
634
|
+
Rules:
|
|
635
|
+
- Be concise. No generic advice. No obvious instructions.
|
|
636
|
+
- Do not list every file — only what's architecturally significant.
|
|
637
|
+
- Do not add sections that have no content.
|
|
638
|
+
- Output raw Markdown only, no code fences wrapping the whole document.`;
|
|
639
|
+
}
|
|
640
|
+
// ── /skills ─────────────────────────────────────────────────────────────────
|
|
641
|
+
// /skills — list all loaded skills
|
|
642
|
+
// /skills create <name> — interactively create a new skill in ~/.codegrunt/skills/
|
|
643
|
+
async function handleSkills(rest, skills) {
|
|
644
|
+
const sub = rest[0]?.toLowerCase();
|
|
645
|
+
const name = rest.slice(1).join(' ').trim();
|
|
646
|
+
if (sub === 'create') {
|
|
647
|
+
if (!name) {
|
|
648
|
+
console.log(chalk.yellow('Usage: /skills create <name>'));
|
|
649
|
+
console.log(chalk.gray('Example: /skills create my-skill'));
|
|
650
|
+
return { type: 'handled' };
|
|
651
|
+
}
|
|
652
|
+
const readline = await import('readline');
|
|
653
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
654
|
+
const ask = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
|
|
655
|
+
console.log(chalk.gray(`\nCreating skill "${chalk.cyan(name)}" in ${chalk.gray(getGlobalSkillsDir())}\n`));
|
|
656
|
+
// try/finally ensures rl.close() is always called even if a prompt throws.
|
|
657
|
+
let desc = '';
|
|
658
|
+
let content = '';
|
|
659
|
+
try {
|
|
660
|
+
console.log(chalk.gray('Enter a short description (optional, press Enter to skip):'));
|
|
661
|
+
desc = (await ask(chalk.bold('Description: '))).trim();
|
|
662
|
+
console.log(chalk.gray('\nEnter the skill content (instructions/prompt that will be sent to the model):'));
|
|
663
|
+
console.log(chalk.gray('Type your content and press Enter. Multi-line is supported —'));
|
|
664
|
+
console.log(chalk.gray('just keep typing and press Enter on an empty line to finish.\n'));
|
|
665
|
+
const lines = [];
|
|
666
|
+
while (true) {
|
|
667
|
+
const line = await ask('');
|
|
668
|
+
if (line === '')
|
|
669
|
+
break;
|
|
670
|
+
lines.push(line);
|
|
671
|
+
}
|
|
672
|
+
content = lines.join('\n').trim();
|
|
673
|
+
}
|
|
674
|
+
finally {
|
|
675
|
+
rl.close();
|
|
676
|
+
}
|
|
677
|
+
if (!content) {
|
|
678
|
+
console.log(chalk.yellow('Skill content cannot be empty. Aborted.'));
|
|
679
|
+
return { type: 'handled' };
|
|
680
|
+
}
|
|
681
|
+
try {
|
|
682
|
+
const fileName = await createSkill(name, desc || '', content);
|
|
683
|
+
console.log(chalk.green(`\n✓ Skill "${name}" created: ${fileName}`));
|
|
684
|
+
console.log(chalk.gray(` Directory: ${getGlobalSkillsDir()}`));
|
|
685
|
+
console.log(chalk.gray(` Use as /${name} immediately.`));
|
|
686
|
+
return { type: 'skills_reload' };
|
|
687
|
+
}
|
|
688
|
+
catch (err) {
|
|
689
|
+
console.log(chalk.red(`\nFailed to create skill: ${err instanceof Error ? err.message : String(err)}`));
|
|
690
|
+
}
|
|
691
|
+
return { type: 'handled' };
|
|
692
|
+
}
|
|
693
|
+
// /skills — list all skills
|
|
694
|
+
if (skills.length === 0) {
|
|
695
|
+
console.log(`\n${chalk.gray('No skills loaded.')}`);
|
|
696
|
+
console.log(chalk.gray(`Create one with ${chalk.cyan('/skills create <name>')}`));
|
|
697
|
+
console.log(chalk.gray(`Or add .md files to ${chalk.gray(getGlobalSkillsDir())}`));
|
|
698
|
+
console.log(chalk.gray(`Project skills: ${chalk.gray('.codegrunt/skills/')}`));
|
|
699
|
+
return { type: 'handled' };
|
|
700
|
+
}
|
|
701
|
+
console.log(`\n${chalk.bold('Skills')}\n`);
|
|
702
|
+
const maxNameLen = Math.max(...skills.map((s) => s.name.length));
|
|
703
|
+
for (const skill of skills) {
|
|
704
|
+
const sourceLabel = skill.source === 'project' ? chalk.blue('[project]') : chalk.gray('[global]');
|
|
705
|
+
const desc = skill.description ? chalk.gray(` — ${skill.description}`) : '';
|
|
706
|
+
const namePadded = chalk.cyan('/' + skill.name.padEnd(maxNameLen));
|
|
707
|
+
console.log(` ${namePadded} ${sourceLabel}${desc}`);
|
|
708
|
+
}
|
|
709
|
+
console.log(`\n${chalk.gray('Use /<skill-name> to run a skill')}`);
|
|
710
|
+
console.log(chalk.gray(`Create: ${chalk.cyan('/skills create <name>')}`));
|
|
711
|
+
console.log(chalk.gray(`Global dir: ${chalk.gray(getGlobalSkillsDir())}`));
|
|
712
|
+
console.log(chalk.gray(`Project dir: ${chalk.gray('.codegrunt/skills/')}`));
|
|
713
|
+
return { type: 'handled' };
|
|
714
|
+
}
|
|
715
|
+
// ── /review ──────────────────────────────────────────────────────────────────
|
|
716
|
+
async function reviewContext(context, config, provider) {
|
|
717
|
+
const messages = context.getMessages();
|
|
718
|
+
const nonSystem = messages.filter((m) => m.role !== 'system');
|
|
719
|
+
if (nonSystem.length < 2) {
|
|
720
|
+
console.log(chalk.gray('No conversation to review yet.'));
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
console.log(chalk.bold('\n🔍 Reviewing session changes for logic issues…\n'));
|
|
724
|
+
const reviewPrompt = messages
|
|
725
|
+
.map((m) => {
|
|
726
|
+
const role = m.role.toUpperCase();
|
|
727
|
+
if ('tool_calls' in m && m.tool_calls) {
|
|
728
|
+
const calls = m.tool_calls.map(tc => ` → ${tc.function.name}(${tc.function.arguments})`).join('\n');
|
|
729
|
+
return `${role}: [tool calls]\n${calls}`;
|
|
730
|
+
}
|
|
731
|
+
const content = 'content' in m && m.content ? String(m.content) : '';
|
|
732
|
+
return `${role}: ${content}`;
|
|
733
|
+
})
|
|
734
|
+
.join('\n\n');
|
|
735
|
+
const reviewMessages = [
|
|
736
|
+
{
|
|
737
|
+
role: 'system',
|
|
738
|
+
content: `You are an expert code reviewer. Analyze the following conversation log containing code changes (write_file, edit_file tool calls). Focus on:
|
|
739
|
+
- Logical errors or inconsistencies in the code changes
|
|
740
|
+
- Potential bugs, edge cases, or race conditions
|
|
741
|
+
- Missing error handling
|
|
742
|
+
- Type safety issues
|
|
743
|
+
- Breaking changes to existing APIs or interfaces
|
|
744
|
+
- Performance concerns
|
|
745
|
+
|
|
746
|
+
Provide a structured review:
|
|
747
|
+
1. **Critical Issues** — bugs that would cause runtime errors or data loss
|
|
748
|
+
2. **Logic Issues** — flaws in reasoning, incorrect assumptions, edge cases missed
|
|
749
|
+
3. **Style / Best Practices** — deviations from conventions, minor improvements
|
|
750
|
+
4. **Summary** — overall assessment
|
|
751
|
+
|
|
752
|
+
If no issues are found, clearly state that the changes look correct. Be specific — reference exact file paths and line content from the conversation.`,
|
|
753
|
+
},
|
|
754
|
+
{
|
|
755
|
+
role: 'user',
|
|
756
|
+
content: `Review this conversation session for logic issues:\n\n${reviewPrompt}`,
|
|
757
|
+
},
|
|
758
|
+
];
|
|
759
|
+
// Spinner animation while waiting for the first token
|
|
760
|
+
const spinnerChars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
761
|
+
let spinnerIdx = 0;
|
|
762
|
+
const spinnerInterval = setInterval(() => {
|
|
763
|
+
process.stdout.write('\r' + chalk.gray(`${spinnerChars[spinnerIdx]} Analyzing…`));
|
|
764
|
+
spinnerIdx = (spinnerIdx + 1) % spinnerChars.length;
|
|
765
|
+
}, 80);
|
|
766
|
+
let review = '';
|
|
767
|
+
const md = new MarkdownRenderer();
|
|
768
|
+
try {
|
|
769
|
+
const stream = provider.stream(reviewMessages, {
|
|
770
|
+
model: config.model,
|
|
771
|
+
maxTokens: 4096,
|
|
772
|
+
temperature: 0.2,
|
|
773
|
+
});
|
|
774
|
+
for await (const chunk of stream) {
|
|
775
|
+
if (chunk.type === 'text_delta') {
|
|
776
|
+
if (!review) {
|
|
777
|
+
clearInterval(spinnerInterval);
|
|
778
|
+
process.stdout.write('\r' + ' '.repeat(20) + '\r');
|
|
779
|
+
}
|
|
780
|
+
review += chunk.text;
|
|
781
|
+
const formatted = md.feed(chunk.text);
|
|
782
|
+
if (formatted)
|
|
783
|
+
process.stdout.write(formatted);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
// Flush any remaining markdown buffer (e.g. pending table)
|
|
787
|
+
const flushOut = md.flush();
|
|
788
|
+
if (flushOut)
|
|
789
|
+
process.stdout.write(flushOut);
|
|
790
|
+
}
|
|
791
|
+
catch (err) {
|
|
792
|
+
clearInterval(spinnerInterval);
|
|
793
|
+
process.stdout.write('\r' + ' '.repeat(20) + '\r');
|
|
794
|
+
console.log(chalk.red('\nFailed to review: ' + (err instanceof Error ? err.message : String(err))));
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
console.log('\n');
|
|
798
|
+
}
|
|
799
|
+
//# sourceMappingURL=commands.js.map
|