@indiccoder/mentis-cli 1.0.9 → 1.1.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/.mentis/commands/ls.md +12 -0
- package/dist/commands/Command.js +6 -0
- package/dist/commands/CommandCreator.js +286 -0
- package/dist/commands/CommandManager.js +268 -0
- package/dist/commands/SlashCommandTool.js +160 -0
- package/dist/index.js +52 -2
- package/dist/repl/ReplManager.js +120 -4
- package/dist/ui/UIManager.js +2 -2
- package/dist/utils/ContextVisualizer.js +92 -0
- package/dist/utils/ConversationCompacter.js +101 -0
- package/dist/utils/ProjectInitializer.js +181 -0
- package/package.json +2 -2
- package/src/commands/Command.ts +40 -0
- package/src/commands/CommandCreator.ts +281 -0
- package/src/commands/CommandManager.ts +280 -0
- package/src/commands/SlashCommandTool.ts +152 -0
- package/src/index.ts +62 -2
- package/src/repl/ReplManager.ts +152 -4
- package/src/ui/UIManager.ts +2 -2
- package/src/utils/ContextVisualizer.ts +105 -0
- package/src/utils/ConversationCompacter.ts +128 -0
- package/src/utils/ProjectInitializer.ts +170 -0
package/src/repl/ReplManager.ts
CHANGED
|
@@ -19,6 +19,11 @@ import { McpClient } from '../mcp/McpClient';
|
|
|
19
19
|
import { CheckpointManager } from '../checkpoint/CheckpointManager';
|
|
20
20
|
import { SkillsManager } from '../skills/SkillsManager';
|
|
21
21
|
import { LoadSkillTool, ListSkillsTool, ReadSkillFileTool } from '../skills/LoadSkillTool';
|
|
22
|
+
import { ContextVisualizer } from '../utils/ContextVisualizer';
|
|
23
|
+
import { ProjectInitializer } from '../utils/ProjectInitializer';
|
|
24
|
+
import { ConversationCompacter } from '../utils/ConversationCompacter';
|
|
25
|
+
import { CommandManager } from '../commands/CommandManager';
|
|
26
|
+
import { SlashCommandTool, ListCommandsTool } from '../commands/SlashCommandTool';
|
|
22
27
|
import * as readline from 'readline';
|
|
23
28
|
import * as fs from 'fs';
|
|
24
29
|
import * as path from 'path';
|
|
@@ -28,12 +33,20 @@ import TerminalRenderer from 'marked-terminal';
|
|
|
28
33
|
|
|
29
34
|
const HISTORY_FILE = path.join(os.homedir(), '.mentis_history');
|
|
30
35
|
|
|
36
|
+
export interface CliOptions {
|
|
37
|
+
resume: boolean;
|
|
38
|
+
yolo: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
export class ReplManager {
|
|
32
42
|
private configManager: ConfigManager;
|
|
33
43
|
private modelClient!: ModelClient;
|
|
34
44
|
private contextManager: ContextManager;
|
|
35
45
|
private checkpointManager: CheckpointManager;
|
|
36
46
|
private skillsManager: SkillsManager;
|
|
47
|
+
private contextVisualizer: ContextVisualizer;
|
|
48
|
+
private conversationCompacter: ConversationCompacter;
|
|
49
|
+
private commandManager: CommandManager;
|
|
37
50
|
private history: ChatMessage[] = [];
|
|
38
51
|
private mode: 'PLAN' | 'BUILD' = 'BUILD';
|
|
39
52
|
private tools: Tool[] = [];
|
|
@@ -41,12 +54,17 @@ export class ReplManager {
|
|
|
41
54
|
private shell: PersistentShell;
|
|
42
55
|
private currentModelName: string = 'Unknown';
|
|
43
56
|
private activeSkill: string | null = null; // Track currently active skill for allowed-tools
|
|
57
|
+
private options: CliOptions;
|
|
44
58
|
|
|
45
|
-
constructor() {
|
|
59
|
+
constructor(options: CliOptions = { resume: false, yolo: false }) {
|
|
60
|
+
this.options = options;
|
|
46
61
|
this.configManager = new ConfigManager();
|
|
47
62
|
this.contextManager = new ContextManager();
|
|
48
63
|
this.checkpointManager = new CheckpointManager();
|
|
49
64
|
this.skillsManager = new SkillsManager();
|
|
65
|
+
this.contextVisualizer = new ContextVisualizer();
|
|
66
|
+
this.conversationCompacter = new ConversationCompacter();
|
|
67
|
+
this.commandManager = new CommandManager();
|
|
50
68
|
this.shell = new PersistentShell();
|
|
51
69
|
|
|
52
70
|
// Create tools array without skill tools first
|
|
@@ -77,12 +95,17 @@ export class ReplManager {
|
|
|
77
95
|
}
|
|
78
96
|
|
|
79
97
|
/**
|
|
80
|
-
* Initialize the skills system
|
|
98
|
+
* Initialize the skills and custom commands system
|
|
81
99
|
*/
|
|
82
100
|
private async initializeSkills() {
|
|
101
|
+
// Initialize skills
|
|
83
102
|
this.skillsManager.ensureDirectoriesExist();
|
|
84
103
|
await this.skillsManager.discoverSkills();
|
|
85
104
|
|
|
105
|
+
// Initialize custom commands
|
|
106
|
+
this.commandManager.ensureDirectoriesExist();
|
|
107
|
+
await this.commandManager.discoverCommands();
|
|
108
|
+
|
|
86
109
|
// Add skill tools to the tools list
|
|
87
110
|
// Pass callback to LoadSkillTool to track active skill
|
|
88
111
|
this.tools.push(
|
|
@@ -90,7 +113,9 @@ export class ReplManager {
|
|
|
90
113
|
this.activeSkill = skill ? skill.name : null;
|
|
91
114
|
}),
|
|
92
115
|
new ListSkillsTool(this.skillsManager),
|
|
93
|
-
new ReadSkillFileTool(this.skillsManager)
|
|
116
|
+
new ReadSkillFileTool(this.skillsManager),
|
|
117
|
+
new SlashCommandTool(this.commandManager),
|
|
118
|
+
new ListCommandsTool(this.commandManager)
|
|
94
119
|
);
|
|
95
120
|
}
|
|
96
121
|
|
|
@@ -127,7 +152,9 @@ export class ReplManager {
|
|
|
127
152
|
'git_pull': 'GitPull',
|
|
128
153
|
'load_skill': 'Read',
|
|
129
154
|
'list_skills': 'Read',
|
|
130
|
-
'read_skill_file': 'Read'
|
|
155
|
+
'read_skill_file': 'Read',
|
|
156
|
+
'slash_command': 'Read',
|
|
157
|
+
'list_commands': 'Read'
|
|
131
158
|
};
|
|
132
159
|
|
|
133
160
|
const mappedToolName = toolMapping[toolName] || toolName;
|
|
@@ -173,6 +200,18 @@ export class ReplManager {
|
|
|
173
200
|
cwd: process.cwd()
|
|
174
201
|
});
|
|
175
202
|
|
|
203
|
+
// Auto-resume if --resume flag is set
|
|
204
|
+
if (this.options.resume) {
|
|
205
|
+
const cp = this.checkpointManager.load('latest');
|
|
206
|
+
if (cp) {
|
|
207
|
+
this.history = cp.history;
|
|
208
|
+
console.log(chalk.green(`\n✓ Resumed session from ${new Date(cp.timestamp).toLocaleString()}`));
|
|
209
|
+
console.log(chalk.dim(` Messages: ${this.history.length}\n`));
|
|
210
|
+
} else {
|
|
211
|
+
console.log(chalk.yellow('\n⚠ No previous session found to resume.\n'));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
176
215
|
// Load History
|
|
177
216
|
let commandHistory: string[] = [];
|
|
178
217
|
if (fs.existsSync(HISTORY_FILE)) {
|
|
@@ -256,11 +295,13 @@ export class ReplManager {
|
|
|
256
295
|
console.log(' /use <provider> [model] - Quick switch (legacy)');
|
|
257
296
|
console.log(' /mcp <cmd> - Manage MCP servers');
|
|
258
297
|
console.log(' /skills <list|show|create|validate> - Manage Agent Skills');
|
|
298
|
+
console.log(' /commands <list|create|validate> - Manage Custom Commands');
|
|
259
299
|
console.log(' /resume - Resume last session');
|
|
260
300
|
console.log(' /checkpoint <save|load|list> [name] - Manage checkpoints');
|
|
261
301
|
console.log(' /search <query> - Search codebase');
|
|
262
302
|
console.log(' /run <cmd> - Run shell command');
|
|
263
303
|
console.log(' /commit [msg] - Git commit all changes');
|
|
304
|
+
console.log(' /init - Initialize project with .mentis.md');
|
|
264
305
|
break;
|
|
265
306
|
case '/plan':
|
|
266
307
|
this.mode = 'PLAN';
|
|
@@ -330,9 +371,15 @@ export class ReplManager {
|
|
|
330
371
|
const updater = new UpdateManager();
|
|
331
372
|
await updater.checkAndPerformUpdate(true);
|
|
332
373
|
break;
|
|
374
|
+
case '/init':
|
|
375
|
+
await this.handleInitCommand();
|
|
376
|
+
break;
|
|
333
377
|
case '/skills':
|
|
334
378
|
await this.handleSkillsCommand(args);
|
|
335
379
|
break;
|
|
380
|
+
case '/commands':
|
|
381
|
+
await this.handleCommandsCommand(args);
|
|
382
|
+
break;
|
|
336
383
|
default:
|
|
337
384
|
console.log(chalk.red(`Unknown command: ${command}`));
|
|
338
385
|
}
|
|
@@ -341,6 +388,7 @@ export class ReplManager {
|
|
|
341
388
|
private async handleChat(input: string) {
|
|
342
389
|
const context = this.contextManager.getContextString();
|
|
343
390
|
const skillsContext = this.skillsManager.getSkillsContext();
|
|
391
|
+
const commandsContext = this.commandManager.getCommandsContext();
|
|
344
392
|
let fullInput = input;
|
|
345
393
|
|
|
346
394
|
let modeInstruction = '';
|
|
@@ -357,6 +405,11 @@ export class ReplManager {
|
|
|
357
405
|
fullInput = `${skillsContext}\n\n${fullInput}`;
|
|
358
406
|
}
|
|
359
407
|
|
|
408
|
+
// Add commands context if available
|
|
409
|
+
if (commandsContext) {
|
|
410
|
+
fullInput = `${commandsContext}\n\n${fullInput}`;
|
|
411
|
+
}
|
|
412
|
+
|
|
360
413
|
if (context) {
|
|
361
414
|
fullInput = `${context}\n\nUser Question: ${fullInput}`;
|
|
362
415
|
}
|
|
@@ -518,8 +571,23 @@ export class ReplManager {
|
|
|
518
571
|
console.log(chalk.dim(`\n(Tokens: ${input_tokens} in / ${output_tokens} out | Est. Cost: $${totalCost.toFixed(5)})`));
|
|
519
572
|
}
|
|
520
573
|
|
|
574
|
+
// Display context bar
|
|
575
|
+
const contextBar = this.contextVisualizer.getContextBar(this.history);
|
|
576
|
+
console.log(chalk.dim(`\n${contextBar}`));
|
|
577
|
+
|
|
521
578
|
console.log('');
|
|
522
579
|
this.history.push({ role: 'assistant', content: response.content });
|
|
580
|
+
|
|
581
|
+
// Auto-compact prompt when context is at 80%
|
|
582
|
+
const usage = this.contextVisualizer.calculateUsage(this.history);
|
|
583
|
+
if (usage.percentage >= 80) {
|
|
584
|
+
this.history = await this.conversationCompacter.promptIfCompactNeeded(
|
|
585
|
+
usage.percentage,
|
|
586
|
+
this.history,
|
|
587
|
+
this.modelClient,
|
|
588
|
+
this.options.yolo
|
|
589
|
+
);
|
|
590
|
+
}
|
|
523
591
|
}
|
|
524
592
|
} catch (error: any) {
|
|
525
593
|
spinner.stop();
|
|
@@ -1034,6 +1102,86 @@ export class ReplManager {
|
|
|
1034
1102
|
}
|
|
1035
1103
|
}
|
|
1036
1104
|
|
|
1105
|
+
private async handleInitCommand(): Promise<void> {
|
|
1106
|
+
const initializer = new ProjectInitializer();
|
|
1107
|
+
await initializer.run();
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
private async handleCommandsCommand(args: string[]) {
|
|
1111
|
+
if (args.length < 1) {
|
|
1112
|
+
// Show commands list by default
|
|
1113
|
+
await this.handleCommandsCommand(['list']);
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
const action = args[0];
|
|
1118
|
+
|
|
1119
|
+
switch (action) {
|
|
1120
|
+
case 'list':
|
|
1121
|
+
await this.handleCommandsList();
|
|
1122
|
+
break;
|
|
1123
|
+
case 'create':
|
|
1124
|
+
await this.handleCommandsCreate(args[1]);
|
|
1125
|
+
break;
|
|
1126
|
+
case 'validate':
|
|
1127
|
+
await this.handleCommandsValidate();
|
|
1128
|
+
break;
|
|
1129
|
+
default:
|
|
1130
|
+
console.log(chalk.red(`Unknown commands action: ${action}`));
|
|
1131
|
+
console.log(chalk.yellow('Available actions: list, create, validate'));
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
private async handleCommandsList(): Promise<void> {
|
|
1136
|
+
const commands = this.commandManager.getAllCommands();
|
|
1137
|
+
|
|
1138
|
+
if (commands.length === 0) {
|
|
1139
|
+
console.log(chalk.yellow('No custom commands available.'));
|
|
1140
|
+
console.log(chalk.dim('Create commands with: /commands create'));
|
|
1141
|
+
console.log(chalk.dim('Add commands to: ~/.mentis/commands/ or .mentis/commands/'));
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
console.log(chalk.cyan(`\nCustom Commands (${commands.length}):\n`));
|
|
1146
|
+
|
|
1147
|
+
// Group by namespace
|
|
1148
|
+
const grouped = new Map<string, any[]>();
|
|
1149
|
+
for (const cmd of commands) {
|
|
1150
|
+
const ns = cmd.description.match(/\(([^)]+)\)/)?.[1] || cmd.type;
|
|
1151
|
+
if (!grouped.has(ns)) {
|
|
1152
|
+
grouped.set(ns, []);
|
|
1153
|
+
}
|
|
1154
|
+
grouped.get(ns)!.push(cmd);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
for (const [namespace, cmds] of grouped) {
|
|
1158
|
+
console.log(chalk.bold(`\n${namespace}`));
|
|
1159
|
+
for (const cmd of cmds) {
|
|
1160
|
+
const params = cmd.frontmatter['argument-hint'] ? ` ${cmd.frontmatter['argument-hint']}` : '';
|
|
1161
|
+
console.log(` /${cmd.name}${params}`);
|
|
1162
|
+
console.log(` ${cmd.description.replace(/\s*\([^)]+\)/, '')}`);
|
|
1163
|
+
|
|
1164
|
+
if (cmd.frontmatter['allowed-tools'] && cmd.frontmatter['allowed-tools'].length > 0) {
|
|
1165
|
+
console.log(chalk.dim(` Allowed tools: ${cmd.frontmatter['allowed-tools'].join(', ')}`));
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
console.log('');
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
private async handleCommandsCreate(name?: string): Promise<void> {
|
|
1173
|
+
const { CommandCreator } = await import('../commands/CommandCreator');
|
|
1174
|
+
const creator = new CommandCreator(this.commandManager);
|
|
1175
|
+
await creator.run(name);
|
|
1176
|
+
// Re-discover commands after creation
|
|
1177
|
+
await this.commandManager.discoverCommands();
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
private async handleCommandsValidate(): Promise<void> {
|
|
1181
|
+
const { validateCommands } = await import('../commands/CommandCreator');
|
|
1182
|
+
await validateCommands(this.commandManager);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1037
1185
|
private estimateCost(input: number, output: number): number {
|
|
1038
1186
|
const config = this.configManager.getConfig();
|
|
1039
1187
|
const provider = config.defaultProvider;
|
package/src/ui/UIManager.ts
CHANGED
|
@@ -14,13 +14,13 @@ export class UIManager {
|
|
|
14
14
|
whitespaceBreak: true,
|
|
15
15
|
});
|
|
16
16
|
console.log(gradient.pastel.multiline(logoText));
|
|
17
|
-
console.log(chalk.gray(' v1.
|
|
17
|
+
console.log(chalk.gray(' v1.1.1 - AI Coding Agent'));
|
|
18
18
|
console.log('');
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
public static renderDashboard(config: { model: string, mode: string, cwd: string }) {
|
|
22
22
|
const { model, cwd } = config;
|
|
23
|
-
const version = 'v1.
|
|
23
|
+
const version = 'v1.1.1';
|
|
24
24
|
|
|
25
25
|
// Layout: Left (Status/Welcome) | Right (Tips/Activity)
|
|
26
26
|
// Total width ~80 chars.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ContextVisualizer - Auto context bar display
|
|
3
|
+
* Shows token usage as a colored progress bar
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
|
|
8
|
+
export interface ContextUsage {
|
|
9
|
+
tokens: number;
|
|
10
|
+
percentage: number;
|
|
11
|
+
maxTokens: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class ContextVisualizer {
|
|
15
|
+
private maxTokens: number = 128000; // Default context window
|
|
16
|
+
|
|
17
|
+
constructor(maxTokens?: number) {
|
|
18
|
+
if (maxTokens) {
|
|
19
|
+
this.maxTokens = maxTokens;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Calculate approximate token count from text
|
|
25
|
+
* Rough estimate: ~4 characters per token
|
|
26
|
+
*/
|
|
27
|
+
private estimateTokens(text: string): number {
|
|
28
|
+
if (!text) return 0;
|
|
29
|
+
// Rough estimation: ~4 characters per token for English text
|
|
30
|
+
return Math.ceil(text.length / 4);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Calculate total tokens from message history
|
|
35
|
+
*/
|
|
36
|
+
calculateUsage(history: any[]): ContextUsage {
|
|
37
|
+
let totalChars = 0;
|
|
38
|
+
|
|
39
|
+
for (const msg of history) {
|
|
40
|
+
if (msg.content) {
|
|
41
|
+
totalChars += msg.content.length;
|
|
42
|
+
}
|
|
43
|
+
if (msg.tool_calls) {
|
|
44
|
+
totalChars += JSON.stringify(msg.tool_calls).length;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Add overhead for system prompt, skills, etc.
|
|
49
|
+
totalChars += 2000;
|
|
50
|
+
|
|
51
|
+
// Rough estimation: ~4 characters per token
|
|
52
|
+
const tokens = Math.ceil(totalChars / 4);
|
|
53
|
+
const percentage = Math.min(100, Math.round((tokens / this.maxTokens) * 100));
|
|
54
|
+
|
|
55
|
+
return { tokens, percentage, maxTokens: this.maxTokens };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Format the context bar for display
|
|
60
|
+
*/
|
|
61
|
+
formatBar(usage: ContextUsage): string {
|
|
62
|
+
const { percentage, tokens, maxTokens } = usage;
|
|
63
|
+
|
|
64
|
+
// Create progress bar (20 chars wide)
|
|
65
|
+
const filled = Math.round(percentage / 5);
|
|
66
|
+
const empty = 20 - filled;
|
|
67
|
+
|
|
68
|
+
let bar: string;
|
|
69
|
+
if (percentage < 60) {
|
|
70
|
+
bar = chalk.green('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
|
|
71
|
+
} else if (percentage < 80) {
|
|
72
|
+
bar = chalk.yellow('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
|
|
73
|
+
} else {
|
|
74
|
+
bar = chalk.red('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const tokensK = Math.round(tokens / 1000);
|
|
78
|
+
const maxTokensK = Math.round(maxTokens / 1000);
|
|
79
|
+
|
|
80
|
+
return `${bar} ${percentage}% | ${tokensK}k/${maxTokensK} tokens`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get the context bar string for current history
|
|
85
|
+
*/
|
|
86
|
+
getContextBar(history: any[]): string {
|
|
87
|
+
const usage = this.calculateUsage(history);
|
|
88
|
+
return this.formatBar(usage);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Check if context is at warning threshold (>=80%)
|
|
93
|
+
*/
|
|
94
|
+
shouldCompact(history: any[]): boolean {
|
|
95
|
+
const usage = this.calculateUsage(history);
|
|
96
|
+
return usage.percentage >= 80;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Set custom max tokens (for different models)
|
|
101
|
+
*/
|
|
102
|
+
setMaxTokens(max: number): void {
|
|
103
|
+
this.maxTokens = max;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConversationCompacter - Compact conversation to save tokens
|
|
3
|
+
* Summarizes conversation history while preserving important context
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import inquirer from 'inquirer';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import { ModelClient, ChatMessage } from '../llm/ModelInterface';
|
|
9
|
+
|
|
10
|
+
export class ConversationCompacter {
|
|
11
|
+
/**
|
|
12
|
+
* Compact conversation history using AI
|
|
13
|
+
*/
|
|
14
|
+
async compact(
|
|
15
|
+
history: ChatMessage[],
|
|
16
|
+
modelClient: ModelClient,
|
|
17
|
+
options?: {
|
|
18
|
+
keepSystemMessages?: boolean;
|
|
19
|
+
focusTopic?: string;
|
|
20
|
+
}
|
|
21
|
+
): Promise<ChatMessage[]> {
|
|
22
|
+
const { keepSystemMessages = true, focusTopic } = options || {};
|
|
23
|
+
|
|
24
|
+
// Preserve system-style messages
|
|
25
|
+
const preserved: ChatMessage[] = [];
|
|
26
|
+
const toCompact: ChatMessage[] = [];
|
|
27
|
+
|
|
28
|
+
for (const msg of history) {
|
|
29
|
+
if (msg.role === 'system' || msg.role === 'tool') {
|
|
30
|
+
preserved.push(msg);
|
|
31
|
+
} else {
|
|
32
|
+
toCompact.push(msg);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (toCompact.length === 0) {
|
|
37
|
+
return history;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Create compaction prompt
|
|
41
|
+
let compactPrompt = 'Please summarize the following conversation into a concise overview. ';
|
|
42
|
+
compactPrompt += 'Include:\n';
|
|
43
|
+
compactPrompt += '- The main topic/problem being discussed\n';
|
|
44
|
+
compactPrompt += '- Key decisions made\n';
|
|
45
|
+
compactPrompt += '- Important technical details\n';
|
|
46
|
+
compactPrompt += '- Current status/next steps\n\n';
|
|
47
|
+
|
|
48
|
+
if (focusTopic) {
|
|
49
|
+
compactPrompt += `Focus primarily on content related to: ${focusTopic}\n\n`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
compactPrompt += 'Return ONLY the summary, no other text.\n\n---\n\n';
|
|
53
|
+
|
|
54
|
+
for (const msg of toCompact.slice(-10)) { // Last 10 messages for context
|
|
55
|
+
compactPrompt += `${msg.role.toUpperCase()}: ${msg.content}\n\n`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
// Call AI to summarize
|
|
60
|
+
const summaryResponse = await modelClient.chat(
|
|
61
|
+
[{ role: 'user', content: compactPrompt }],
|
|
62
|
+
[]
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const summary = summaryResponse.content;
|
|
66
|
+
|
|
67
|
+
// Create new compacted history
|
|
68
|
+
const newHistory: ChatMessage[] = [...preserved];
|
|
69
|
+
|
|
70
|
+
// Add summary as a system message for context
|
|
71
|
+
newHistory.push({
|
|
72
|
+
role: 'system',
|
|
73
|
+
content: `[Previous Conversation Summary]\n${summary}`
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return newHistory;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error('Compaction failed:', error);
|
|
79
|
+
return history; // Return original if compaction fails
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Prompt user to compact when threshold is reached
|
|
85
|
+
*/
|
|
86
|
+
async promptIfCompactNeeded(
|
|
87
|
+
percentage: number,
|
|
88
|
+
history: ChatMessage[],
|
|
89
|
+
modelClient: ModelClient,
|
|
90
|
+
yolo: boolean = false
|
|
91
|
+
): Promise<ChatMessage[]> {
|
|
92
|
+
if (percentage < 80) {
|
|
93
|
+
return history;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log(chalk.yellow(`\n⚠️ Context is ${percentage}% full. Consider compacting to save tokens.`));
|
|
97
|
+
|
|
98
|
+
// Skip confirmation if yolo mode is enabled
|
|
99
|
+
if (!yolo) {
|
|
100
|
+
const { shouldCompact } = await inquirer.prompt([
|
|
101
|
+
{
|
|
102
|
+
type: 'confirm',
|
|
103
|
+
name: 'shouldCompact',
|
|
104
|
+
message: 'Compact conversation now?',
|
|
105
|
+
default: true
|
|
106
|
+
}
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
if (!shouldCompact) {
|
|
110
|
+
return history;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const focusTopic = yolo ? '' : await inquirer.prompt([
|
|
115
|
+
{
|
|
116
|
+
type: 'input',
|
|
117
|
+
name: 'focusTopic',
|
|
118
|
+
message: 'Focus on specific topic? (leave empty for general)',
|
|
119
|
+
default: ''
|
|
120
|
+
}
|
|
121
|
+
]).then(a => a.focusTopic);
|
|
122
|
+
|
|
123
|
+
return await this.compact(history, modelClient, {
|
|
124
|
+
keepSystemMessages: true,
|
|
125
|
+
focusTopic: focusTopic || undefined
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProjectInitializer - Initialize project with .mentis.md
|
|
3
|
+
* Interactive wizard for creating project guide files
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import inquirer from 'inquirer';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import * as os from 'os';
|
|
11
|
+
|
|
12
|
+
export class ProjectInitializer {
|
|
13
|
+
/**
|
|
14
|
+
* Run the interactive project initialization wizard
|
|
15
|
+
*/
|
|
16
|
+
async run(cwd: string = process.cwd()): Promise<boolean> {
|
|
17
|
+
console.log('\n📁 Initialize Project with .mentis.md\n');
|
|
18
|
+
|
|
19
|
+
// Step 1: Project name
|
|
20
|
+
const { projectName } = await inquirer.prompt([
|
|
21
|
+
{
|
|
22
|
+
type: 'input',
|
|
23
|
+
name: 'projectName',
|
|
24
|
+
message: 'Project name:',
|
|
25
|
+
default: path.basename(cwd)
|
|
26
|
+
}
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
// Step 2: Project description
|
|
30
|
+
const { description } = await inquirer.prompt([
|
|
31
|
+
{
|
|
32
|
+
type: 'input',
|
|
33
|
+
name: 'description',
|
|
34
|
+
message: 'Brief description:',
|
|
35
|
+
default: 'A software project'
|
|
36
|
+
}
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
// Step 3: Tech stack
|
|
40
|
+
const { techStack } = await inquirer.prompt([
|
|
41
|
+
{
|
|
42
|
+
type: 'checkbox',
|
|
43
|
+
name: 'techStack',
|
|
44
|
+
message: 'Select technologies:',
|
|
45
|
+
choices: [
|
|
46
|
+
'TypeScript',
|
|
47
|
+
'JavaScript',
|
|
48
|
+
'Python',
|
|
49
|
+
'React',
|
|
50
|
+
'Vue',
|
|
51
|
+
'Node.js',
|
|
52
|
+
'Express',
|
|
53
|
+
'PostgreSQL',
|
|
54
|
+
'MongoDB',
|
|
55
|
+
'Redis',
|
|
56
|
+
'Docker',
|
|
57
|
+
'GraphQL',
|
|
58
|
+
'REST API',
|
|
59
|
+
'Other'
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
// Step 4: Project type
|
|
65
|
+
const { projectType } = await inquirer.prompt([
|
|
66
|
+
{
|
|
67
|
+
type: 'list',
|
|
68
|
+
name: 'projectType',
|
|
69
|
+
message: 'Project type:',
|
|
70
|
+
choices: ['Web Application', 'API/Backend', 'CLI Tool', 'Library/Package', 'Mobile App', 'Desktop App', 'Other']
|
|
71
|
+
}
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
// Step 5: Conventions
|
|
75
|
+
const { useConventions } = await inquirer.prompt([
|
|
76
|
+
{
|
|
77
|
+
type: 'confirm',
|
|
78
|
+
name: 'useConventions',
|
|
79
|
+
message: 'Add coding conventions?',
|
|
80
|
+
default: true
|
|
81
|
+
}
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
let conventions = '';
|
|
85
|
+
if (useConventions) {
|
|
86
|
+
const { conventionStyle } = await inquirer.prompt([
|
|
87
|
+
{
|
|
88
|
+
type: 'list',
|
|
89
|
+
name: 'conventionStyle',
|
|
90
|
+
message: 'Style guide:',
|
|
91
|
+
choices: ['Standard', 'Airbnb', 'Google', 'Prettier', 'Custom']
|
|
92
|
+
}
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
conventions = this.getConventionText(conventionStyle);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Create .mentis.md content
|
|
99
|
+
const content = this.generateMentisMd({
|
|
100
|
+
projectName,
|
|
101
|
+
description,
|
|
102
|
+
techStack,
|
|
103
|
+
projectType,
|
|
104
|
+
conventions
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Write to file
|
|
108
|
+
const mentisMdPath = path.join(cwd, '.mentis.md');
|
|
109
|
+
fs.writeFileSync(mentisMdPath, content, 'utf-8');
|
|
110
|
+
|
|
111
|
+
console.log(chalk.green(`\n✓ Created .mentis.md`));
|
|
112
|
+
console.log(chalk.dim(`\nTip: Edit .mentis.md to add project-specific instructions for Mentis.\n`));
|
|
113
|
+
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Generate .mentis.md content
|
|
119
|
+
*/
|
|
120
|
+
private generateMentisMd(options: {
|
|
121
|
+
projectName: string;
|
|
122
|
+
description: string;
|
|
123
|
+
techStack: string[];
|
|
124
|
+
projectType: string;
|
|
125
|
+
conventions: string;
|
|
126
|
+
}): string {
|
|
127
|
+
const { projectName, description, techStack, projectType, conventions } = options;
|
|
128
|
+
|
|
129
|
+
let content = `# ${projectName}\n\n`;
|
|
130
|
+
content += `${description}\n\n`;
|
|
131
|
+
content += `**Type**: ${projectType}\n\n`;
|
|
132
|
+
content += `## Tech Stack\n\n`;
|
|
133
|
+
content += techStack.map(t => `- ${t}`).join('\n');
|
|
134
|
+
content += `\n\n`;
|
|
135
|
+
|
|
136
|
+
if (conventions) {
|
|
137
|
+
content += `## Coding Conventions\n\n`;
|
|
138
|
+
content += `${conventions}\n\n`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
content += `## Project Structure\n\n`;
|
|
142
|
+
content += `<!-- Add project structure here -->\n\n`;
|
|
143
|
+
|
|
144
|
+
content += `## Guidelines for Mentis\n\n`;
|
|
145
|
+
content += `- When writing code, follow the conventions above\n`;
|
|
146
|
+
content += `- Prefer existing patterns in the codebase\n`;
|
|
147
|
+
content += `- Add comments for complex logic\n`;
|
|
148
|
+
content += `- Run tests before suggesting changes\n\n`;
|
|
149
|
+
|
|
150
|
+
content += `## Common Commands\n\n`;
|
|
151
|
+
content += `<!-- Add common development commands here -->\n`;
|
|
152
|
+
|
|
153
|
+
return content;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get convention text based on style
|
|
158
|
+
*/
|
|
159
|
+
private getConventionText(style: string): string {
|
|
160
|
+
const conventions: Record<string, string> = {
|
|
161
|
+
'Standard': `- Use 2 spaces for indentation\n- Use camelCase for variables\n- Use PascalCase for classes\n- Prefer const over let\n- Use arrow functions`,
|
|
162
|
+
'Airbnb': `- Follow Airbnb Style Guide\n- Use 2 spaces for indentation\n- Prefer named exports\n- Use template literals`,
|
|
163
|
+
'Google': `- Follow Google JavaScript Style Guide\n- Use 2 spaces for indentation\n- JSDoc comments for functions`,
|
|
164
|
+
'Prettier': `- Use Prettier for formatting\n- 2 spaces for indentation\n- Single quotes for strings\n- No trailing commas`,
|
|
165
|
+
'Custom': `- Define your conventions below`
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
return conventions[style] || conventions['Standard'];
|
|
169
|
+
}
|
|
170
|
+
}
|