@indiccoder/mentis-cli 1.0.5 → 1.0.9

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.
@@ -17,6 +17,8 @@ import { Tool } from '../tools/Tool';
17
17
  import { McpClient } from '../mcp/McpClient';
18
18
 
19
19
  import { CheckpointManager } from '../checkpoint/CheckpointManager';
20
+ import { SkillsManager } from '../skills/SkillsManager';
21
+ import { LoadSkillTool, ListSkillsTool, ReadSkillFileTool } from '../skills/LoadSkillTool';
20
22
  import * as readline from 'readline';
21
23
  import * as fs from 'fs';
22
24
  import * as path from 'path';
@@ -31,18 +33,23 @@ export class ReplManager {
31
33
  private modelClient!: ModelClient;
32
34
  private contextManager: ContextManager;
33
35
  private checkpointManager: CheckpointManager;
36
+ private skillsManager: SkillsManager;
34
37
  private history: ChatMessage[] = [];
35
38
  private mode: 'PLAN' | 'BUILD' = 'BUILD';
36
39
  private tools: Tool[] = [];
37
40
  private mcpClients: McpClient[] = [];
38
41
  private shell: PersistentShell;
39
42
  private currentModelName: string = 'Unknown';
43
+ private activeSkill: string | null = null; // Track currently active skill for allowed-tools
40
44
 
41
45
  constructor() {
42
46
  this.configManager = new ConfigManager();
43
47
  this.contextManager = new ContextManager();
44
48
  this.checkpointManager = new CheckpointManager();
49
+ this.skillsManager = new SkillsManager();
45
50
  this.shell = new PersistentShell();
51
+
52
+ // Create tools array without skill tools first
46
53
  this.tools = [
47
54
  new WriteFileTool(),
48
55
  new ReadFileTool(),
@@ -64,6 +71,67 @@ export class ReplManager {
64
71
  });
65
72
  // Default to Ollama if not specified, assuming compatible endpoint
66
73
  this.initializeClient();
74
+
75
+ // Initialize skills system after client is ready
76
+ this.initializeSkills();
77
+ }
78
+
79
+ /**
80
+ * Initialize the skills system
81
+ */
82
+ private async initializeSkills() {
83
+ this.skillsManager.ensureDirectoriesExist();
84
+ await this.skillsManager.discoverSkills();
85
+
86
+ // Add skill tools to the tools list
87
+ // Pass callback to LoadSkillTool to track active skill
88
+ this.tools.push(
89
+ new LoadSkillTool(this.skillsManager, (skill) => {
90
+ this.activeSkill = skill ? skill.name : null;
91
+ }),
92
+ new ListSkillsTool(this.skillsManager),
93
+ new ReadSkillFileTool(this.skillsManager)
94
+ );
95
+ }
96
+
97
+ /**
98
+ * Check if a tool is allowed by the currently active skill
99
+ * Returns true if tool is allowed, false if it requires confirmation
100
+ */
101
+ private isToolAllowedBySkill(toolName: string): boolean {
102
+ if (!this.activeSkill) {
103
+ // No active skill, all tools require confirmation as per normal flow
104
+ return false;
105
+ }
106
+
107
+ const skill = this.skillsManager.getSkill(this.activeSkill);
108
+ if (!skill || !skill.allowedTools || skill.allowedTools.length === 0) {
109
+ // No skill or no allowed-tools restriction
110
+ return false;
111
+ }
112
+
113
+ // Map tool names to allowed tool names
114
+ const toolMapping: Record<string, string> = {
115
+ 'write_file': 'Write',
116
+ 'read_file': 'Read',
117
+ 'edit_file': 'Edit',
118
+ 'search_files': 'Grep',
119
+ 'list_dir': 'ListDir',
120
+ 'search_file': 'SearchFile',
121
+ 'run_shell': 'RunShell',
122
+ 'web_search': 'WebSearch',
123
+ 'git_status': 'GitStatus',
124
+ 'git_diff': 'GitDiff',
125
+ 'git_commit': 'GitCommit',
126
+ 'git_push': 'GitPush',
127
+ 'git_pull': 'GitPull',
128
+ 'load_skill': 'Read',
129
+ 'list_skills': 'Read',
130
+ 'read_skill_file': 'Read'
131
+ };
132
+
133
+ const mappedToolName = toolMapping[toolName] || toolName;
134
+ return skill.allowedTools.includes(mappedToolName);
67
135
  }
68
136
 
69
137
  private initializeClient() {
@@ -99,29 +167,28 @@ export class ReplManager {
99
167
  }
100
168
 
101
169
  public async start() {
102
- UIManager.displayLogo();
103
- UIManager.displayWelcome();
170
+ UIManager.renderDashboard({
171
+ model: this.currentModelName,
172
+ mode: this.mode,
173
+ cwd: process.cwd()
174
+ });
104
175
 
105
176
  // Load History
106
177
  let commandHistory: string[] = [];
107
178
  if (fs.existsSync(HISTORY_FILE)) {
108
179
  try {
109
- commandHistory = fs.readFileSync(HISTORY_FILE, 'utf-8').split('\n').filter(Boolean).reverse(); // readline expects newest first? No, newest is usually 0? Check.
110
- // readline.history is [newest, ..., oldest]
111
- // If I read from file where newest is at bottom (standard append), I need to reverse it.
112
- // Let's assume standard file: line 1 (old), line 2 (new).
113
- // So split -> reverse -> history.
180
+ commandHistory = fs.readFileSync(HISTORY_FILE, 'utf-8').split('\n').filter(Boolean).reverse();
114
181
  } catch (e) { }
115
182
  }
116
183
 
117
184
  while (true) {
118
- UIManager.printSeparator();
119
- // console.log(chalk.dim(` /help for help | Model: ${chalk.cyan(this.currentModelName)}`));
120
- // Removed redundancy to keep CLI clean, prompt has info? No, prompt is minimal.
185
+ // Minimalist Separator
186
+ console.log(chalk.gray('────────────────────────────────────────────────────────────────────────────────'));
187
+
188
+ // Hint (Claude style puts it below, we put it above for standard terminal compatibility)
189
+ console.log(chalk.dim(' ? for shortcuts'));
121
190
 
122
- const modeLabel = this.mode === 'PLAN' ? chalk.magenta('PLAN') : chalk.blue('BUILD');
123
- const modelInfo = this.currentModelName ? ` (${this.currentModelName})` : '';
124
- const promptText = `${modeLabel}${chalk.dim(modelInfo)} ${chalk.cyan('>')}`;
191
+ const promptText = `> `; // Clean prompt
125
192
 
126
193
  // Use readline for basic input to support history
127
194
  const answer = await new Promise<string>((resolve) => {
@@ -130,7 +197,7 @@ export class ReplManager {
130
197
  output: process.stdout,
131
198
  history: commandHistory,
132
199
  historySize: 1000,
133
- prompt: promptText + ' '
200
+ prompt: promptText
134
201
  });
135
202
 
136
203
  rl.prompt();
@@ -179,16 +246,16 @@ export class ReplManager {
179
246
  console.log(' /help - Show this help message');
180
247
  console.log(' /clear - Clear chat history');
181
248
  console.log(' /exit - Exit the application');
249
+ console.log(' /update - Check for and install updates');
182
250
  console.log(' /config - Configure settings');
183
251
  console.log(' /add <file> - Add file to context');
184
252
  console.log(' /drop <file> - Remove file from context');
185
253
  console.log(' /plan - Switch to PLAN mode');
186
254
  console.log(' /build - Switch to BUILD mode');
187
- console.log(' /plan - Switch to PLAN mode');
188
- console.log(' /build - Switch to BUILD mode');
189
255
  console.log(' /model - Interactively select Provider & Model');
190
256
  console.log(' /use <provider> [model] - Quick switch (legacy)');
191
257
  console.log(' /mcp <cmd> - Manage MCP servers');
258
+ console.log(' /skills <list|show|create|validate> - Manage Agent Skills');
192
259
  console.log(' /resume - Resume last session');
193
260
  console.log(' /checkpoint <save|load|list> [name] - Manage checkpoints');
194
261
  console.log(' /search <query> - Search codebase');
@@ -258,6 +325,14 @@ export class ReplManager {
258
325
  console.log(chalk.green('Session saved. Goodbye!'));
259
326
  process.exit(0);
260
327
  break;
328
+ case '/update':
329
+ const UpdateManager = require('../utils/UpdateManager').UpdateManager;
330
+ const updater = new UpdateManager();
331
+ await updater.checkAndPerformUpdate(true);
332
+ break;
333
+ case '/skills':
334
+ await this.handleSkillsCommand(args);
335
+ break;
261
336
  default:
262
337
  console.log(chalk.red(`Unknown command: ${command}`));
263
338
  }
@@ -265,6 +340,7 @@ export class ReplManager {
265
340
 
266
341
  private async handleChat(input: string) {
267
342
  const context = this.contextManager.getContextString();
343
+ const skillsContext = this.skillsManager.getSkillsContext();
268
344
  let fullInput = input;
269
345
 
270
346
  let modeInstruction = '';
@@ -276,6 +352,11 @@ export class ReplManager {
276
352
 
277
353
  fullInput = `${input}${modeInstruction}`;
278
354
 
355
+ // Add skills context if available
356
+ if (skillsContext) {
357
+ fullInput = `${skillsContext}\n\n${fullInput}`;
358
+ }
359
+
279
360
  if (context) {
280
361
  fullInput = `${context}\n\nUser Question: ${fullInput}`;
281
362
  }
@@ -338,7 +419,8 @@ export class ReplManager {
338
419
  console.log(chalk.dim(` [Action] ${toolName}(${displayArgs})`));
339
420
 
340
421
  // Safety check for write_file
341
- if (toolName === 'write_file') {
422
+ // Skip confirmation if tool is allowed by active skill
423
+ if (toolName === 'write_file' && !this.isToolAllowedBySkill('Write')) {
342
424
  // Pause cancellation listener during user interaction
343
425
  if (process.stdin.isTTY) {
344
426
  process.stdin.removeListener('keypress', keyListener);
@@ -858,6 +940,100 @@ export class ReplManager {
858
940
  }
859
941
  }
860
942
 
943
+ private async handleSkillsCommand(args: string[]) {
944
+ const { SkillCreator, validateSkills } = await import('../skills/SkillCreator');
945
+
946
+ if (args.length < 1) {
947
+ // Show skills list by default
948
+ await this.handleSkillsCommand(['list']);
949
+ return;
950
+ }
951
+
952
+ const action = args[0];
953
+
954
+ switch (action) {
955
+ case 'list':
956
+ await this.handleSkillsList();
957
+ break;
958
+ case 'show':
959
+ if (args.length < 2) {
960
+ console.log(chalk.red('Usage: /skills show <name>'));
961
+ return;
962
+ }
963
+ await this.handleSkillsShow(args[1]);
964
+ break;
965
+ case 'create':
966
+ const creator = new SkillCreator(this.skillsManager);
967
+ await creator.run(args[1]);
968
+ // Re-discover skills after creation
969
+ await this.skillsManager.discoverSkills();
970
+ break;
971
+ case 'validate':
972
+ await validateSkills(this.skillsManager);
973
+ break;
974
+ default:
975
+ console.log(chalk.red(`Unknown skills action: ${action}`));
976
+ console.log(chalk.yellow('Available actions: list, show, create, validate'));
977
+ }
978
+ }
979
+
980
+ private async handleSkillsList(): Promise<void> {
981
+ const skills = this.skillsManager.getAllSkills();
982
+
983
+ if (skills.length === 0) {
984
+ console.log(chalk.yellow('No skills available.'));
985
+ console.log(chalk.dim('Create skills with: /skills create'));
986
+ console.log(chalk.dim('Add skills to: ~/.mentis/skills/ or .mentis/skills/'));
987
+ return;
988
+ }
989
+
990
+ console.log(chalk.cyan(`\nAvailable Skills (${skills.length}):\n`));
991
+
992
+ for (const skill of skills) {
993
+ const statusIcon = skill.isValid ? '✓' : '✗';
994
+ const typeLabel = skill.type === 'personal' ? 'Personal' : 'Project';
995
+
996
+ console.log(`${statusIcon} ${chalk.bold(skill.name)} (${typeLabel})`);
997
+ console.log(` ${skill.description}`);
998
+
999
+ if (skill.allowedTools && skill.allowedTools.length > 0) {
1000
+ console.log(chalk.dim(` Allowed tools: ${skill.allowedTools.join(', ')}`));
1001
+ }
1002
+
1003
+ if (!skill.isValid && skill.errors) {
1004
+ console.log(chalk.red(` Errors: ${skill.errors.join(', ')}`));
1005
+ }
1006
+
1007
+ console.log('');
1008
+ }
1009
+ }
1010
+
1011
+ private async handleSkillsShow(name: string): Promise<void> {
1012
+ const skill = await this.skillsManager.loadFullSkill(name);
1013
+
1014
+ if (!skill) {
1015
+ console.log(chalk.red(`Skill "${name}" not found.`));
1016
+ return;
1017
+ }
1018
+
1019
+ console.log(chalk.cyan(`\n# ${skill.name}\n`));
1020
+ console.log(chalk.dim(`Type: ${skill.type}`));
1021
+ console.log(chalk.dim(`Path: ${skill.path}`));
1022
+
1023
+ if (skill.allowedTools && skill.allowedTools.length > 0) {
1024
+ console.log(chalk.dim(`Allowed tools: ${skill.allowedTools.join(', ')}`));
1025
+ }
1026
+
1027
+ console.log('');
1028
+ console.log(skill.content || 'No content available');
1029
+
1030
+ // List supporting files
1031
+ const files = this.skillsManager.listSkillFiles(name);
1032
+ if (files.length > 0) {
1033
+ console.log(chalk.dim(`\nSupporting files: ${files.join(', ')}`));
1034
+ }
1035
+ }
1036
+
861
1037
  private estimateCost(input: number, output: number): number {
862
1038
  const config = this.configManager.getConfig();
863
1039
  const provider = config.defaultProvider;
@@ -0,0 +1,168 @@
1
+ /**
2
+ * LoadSkillTool - Tool for model-invoked skill loading
3
+ * The model can call this tool when it determines a skill is relevant to the current task
4
+ */
5
+
6
+ import { Tool } from '../tools/Tool';
7
+ import { SkillsManager, Skill } from './SkillsManager';
8
+
9
+ interface LoadSkillArgs {
10
+ name: string;
11
+ }
12
+
13
+ type SkillLoadedCallback = (skill: Skill | null) => void;
14
+
15
+ export class LoadSkillTool implements Tool {
16
+ name = 'load_skill';
17
+ description = 'Load the full content of a skill by name. Use this when you need detailed instructions from a skill. Available skills can be seen in the system prompt. Example: load_skill({ name: "commit-helper" })';
18
+ parameters = {
19
+ type: 'object',
20
+ properties: {
21
+ name: {
22
+ type: 'string',
23
+ description: 'The name of the skill to load (e.g., "commit-helper", "pdf-processing")'
24
+ }
25
+ },
26
+ required: ['name']
27
+ };
28
+
29
+ private skillsManager: SkillsManager;
30
+ private onSkillLoaded?: SkillLoadedCallback;
31
+
32
+ constructor(skillsManager: SkillsManager, onSkillLoaded?: SkillLoadedCallback) {
33
+ this.skillsManager = skillsManager;
34
+ this.onSkillLoaded = onSkillLoaded;
35
+ }
36
+
37
+ async execute(args: LoadSkillArgs): Promise<string> {
38
+ const { name } = args;
39
+
40
+ if (!name) {
41
+ return 'Error: Skill name is required';
42
+ }
43
+
44
+ const skill = this.skillsManager.getSkill(name);
45
+ if (!skill) {
46
+ const availableSkills = this.skillsManager.getAllSkills().map(s => s.name).join(', ');
47
+ return `Error: Skill "${name}" not found. Available skills: ${availableSkills || 'none'}`;
48
+ }
49
+
50
+ // Load full skill content
51
+ const fullSkill = await this.skillsManager.loadFullSkill(name);
52
+ if (!fullSkill || !fullSkill.content) {
53
+ return `Error: Failed to load content for skill "${name}"`;
54
+ }
55
+
56
+ // Notify callback that skill was loaded
57
+ if (this.onSkillLoaded) {
58
+ this.onSkillLoaded(fullSkill);
59
+ }
60
+
61
+ // Format response with skill content
62
+ let response = `# Loaded Skill: ${skill.name}\n\n`;
63
+ response += `**Type**: ${skill.type}\n`;
64
+ response += `**Description**: ${skill.description}\n`;
65
+ if (skill.allowedTools && skill.allowedTools.length > 0) {
66
+ response += `**Allowed Tools**: ${skill.allowedTools.join(', ')}\n`;
67
+ }
68
+ response += `\n---\n\n`;
69
+ response += fullSkill.content;
70
+
71
+ return response;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * ListSkillsTool - Tool for listing available skills
77
+ */
78
+ export class ListSkillsTool implements Tool {
79
+ name = 'list_skills';
80
+ description = 'List all available skills with their descriptions. Use this to see what skills are available.';
81
+ parameters = {
82
+ type: 'object',
83
+ properties: {},
84
+ required: []
85
+ };
86
+
87
+ private skillsManager: SkillsManager;
88
+
89
+ constructor(skillsManager: SkillsManager) {
90
+ this.skillsManager = skillsManager;
91
+ }
92
+
93
+ async execute(): Promise<string> {
94
+ const skills = this.skillsManager.getAllSkills();
95
+
96
+ if (skills.length === 0) {
97
+ return 'No skills available. Add skills to ~/.mentis/skills/ or .mentis/skills/';
98
+ }
99
+
100
+ let response = `# Available Skills (${skills.length})\n\n`;
101
+
102
+ for (const skill of skills) {
103
+ const statusIcon = skill.isValid ? '✓' : '✗';
104
+ response += `**${statusIcon} ${skill.name}** (${skill.type})\n`;
105
+ response += ` ${skill.description}\n`;
106
+
107
+ if (skill.allowedTools && skill.allowedTools.length > 0) {
108
+ response += ` Allowed tools: ${skill.allowedTools.join(', ')}\n`;
109
+ }
110
+
111
+ if (!skill.isValid && skill.errors) {
112
+ response += ` Errors: ${skill.errors.join(', ')}\n`;
113
+ }
114
+
115
+ response += '\n';
116
+ }
117
+
118
+ return response;
119
+ }
120
+ }
121
+
122
+ /**
123
+ * ReadSkillFileTool - Tool for reading supporting files within a skill
124
+ * Used for progressive disclosure of skill resources
125
+ */
126
+ export class ReadSkillFileTool implements Tool {
127
+ name = 'read_skill_file';
128
+ description = 'Read a supporting file from within a skill directory. Use this when a skill references additional files like [reference.md](reference.md). Example: read_skill_file({ skill: "pdf-processing", file: "reference.md" })';
129
+ parameters = {
130
+ type: 'object',
131
+ properties: {
132
+ skill: {
133
+ type: 'string',
134
+ description: 'The name of the skill'
135
+ },
136
+ file: {
137
+ type: 'string',
138
+ description: 'The filename within the skill directory (e.g., "reference.md", "examples.md")'
139
+ }
140
+ },
141
+ required: ['skill', 'file']
142
+ };
143
+
144
+ private skillsManager: SkillsManager;
145
+
146
+ constructor(skillsManager: SkillsManager) {
147
+ this.skillsManager = skillsManager;
148
+ }
149
+
150
+ async execute(args: { skill: string; file: string }): Promise<string> {
151
+ const { skill, file } = args;
152
+
153
+ if (!skill || !file) {
154
+ return 'Error: Both skill and file parameters are required';
155
+ }
156
+
157
+ const content = this.skillsManager.readSkillFile(skill, file);
158
+ if (content === null) {
159
+ const availableFiles = this.skillsManager.listSkillFiles(skill);
160
+ if (availableFiles.length === 0) {
161
+ return `Error: Skill "${skill}" has no supporting files`;
162
+ }
163
+ return `Error: File "${file}" not found in skill "${skill}". Available files: ${availableFiles.join(', ')}`;
164
+ }
165
+
166
+ return content;
167
+ }
168
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Skill data structure for Agent Skills system
3
+ * Based on Claude Code's Agent Skills format
4
+ */
5
+
6
+ export interface SkillMetadata {
7
+ name: string; // Lowercase, numbers, hyphens only (max 64 chars)
8
+ description: string; // What it does + when to use it (max 1024 chars)
9
+ allowedTools?: string[]; // Optional tool restrictions
10
+ }
11
+
12
+ export interface Skill extends SkillMetadata {
13
+ path: string; // Path to SKILL.md
14
+ type: 'personal' | 'project' | 'plugin';
15
+ content?: string; // Loaded on demand (progressive disclosure)
16
+ directory: string; // Path to skill directory (for resolving supporting files)
17
+ isValid: boolean; // Whether skill passes validation
18
+ errors?: string[]; // Validation errors if any
19
+ }
20
+
21
+ export interface SkillFrontmatter {
22
+ name: string;
23
+ description: string;
24
+ 'allowed-tools'?: string[];
25
+ }
26
+
27
+ /**
28
+ * Validation result for a skill
29
+ */
30
+ export interface SkillValidationResult {
31
+ isValid: boolean;
32
+ errors: string[];
33
+ warnings: string[];
34
+ }
35
+
36
+ /**
37
+ * Skill context format for model injection
38
+ */
39
+ export interface SkillContext {
40
+ name: string;
41
+ description: string;
42
+ }
43
+
44
+ /**
45
+ * Options for skill discovery
46
+ */
47
+ export interface SkillDiscoveryOptions {
48
+ includePersonal?: boolean;
49
+ includeProject?: boolean;
50
+ includePlugin?: boolean;
51
+ }