@indiccoder/mentis-cli 1.1.2 → 1.1.4
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/ARCHITECTURE.md +267 -0
- package/CONTRIBUTING.md +209 -0
- package/dist/commands/Command.js +15 -1
- package/dist/commands/CommandManager.js +30 -5
- package/dist/commands/__tests__/CommandManager.test.js +70 -0
- package/dist/index.js +23 -0
- package/dist/repl/ReplManager.js +19 -31
- package/dist/skills/Skill.js +17 -2
- package/dist/skills/SkillsManager.js +28 -3
- package/dist/skills/__tests__/SkillsManager.test.js +62 -0
- package/dist/ui/InputBox.js +127 -0
- package/dist/ui/UIManager.js +2 -2
- package/dist/utils/__mocks__/chalk.js +20 -0
- package/dist/utils/__tests__/ContextVisualizer.test.js +95 -0
- package/package.json +25 -2
- package/src/commands/Command.ts +64 -13
- package/src/commands/CommandManager.ts +26 -5
- package/src/commands/__tests__/CommandManager.test.ts +83 -0
- package/src/index.ts +33 -1
- package/src/repl/ReplManager.ts +19 -33
- package/src/skills/Skill.ts +91 -11
- package/src/skills/SkillsManager.ts +25 -3
- package/src/skills/__tests__/SkillsManager.test.ts +73 -0
- package/src/ui/InputBox.ts +145 -0
- package/src/ui/UIManager.ts +2 -2
- package/src/utils/__mocks__/chalk.ts +19 -0
- package/src/utils/__tests__/ContextVisualizer.test.ts +118 -0
package/dist/repl/ReplManager.js
CHANGED
|
@@ -44,6 +44,7 @@ const ConfigManager_1 = require("../config/ConfigManager");
|
|
|
44
44
|
const OpenAIClient_1 = require("../llm/OpenAIClient");
|
|
45
45
|
const ContextManager_1 = require("../context/ContextManager");
|
|
46
46
|
const UIManager_1 = require("../ui/UIManager");
|
|
47
|
+
const InputBox_1 = require("../ui/InputBox");
|
|
47
48
|
const FileTools_1 = require("../tools/FileTools");
|
|
48
49
|
const SearchTools_1 = require("../tools/SearchTools");
|
|
49
50
|
const PersistentShellTool_1 = require("../tools/PersistentShellTool");
|
|
@@ -224,46 +225,33 @@ class ReplManager {
|
|
|
224
225
|
}
|
|
225
226
|
catch (e) { }
|
|
226
227
|
}
|
|
228
|
+
// Initialize InputBox with history
|
|
229
|
+
const inputBox = new InputBox_1.InputBox(commandHistory);
|
|
227
230
|
while (true) {
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
//
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
historySize: 1000,
|
|
240
|
-
prompt: promptText
|
|
241
|
-
});
|
|
242
|
-
rl.prompt();
|
|
243
|
-
rl.on('line', (line) => {
|
|
244
|
-
rl.close();
|
|
245
|
-
resolve(line);
|
|
246
|
-
});
|
|
231
|
+
// Calculate context usage for display
|
|
232
|
+
const usage = this.contextVisualizer.calculateUsage(this.history);
|
|
233
|
+
// Display enhanced input frame
|
|
234
|
+
inputBox.displayFrame({
|
|
235
|
+
messageCount: this.history.length,
|
|
236
|
+
contextPercent: usage.percentage
|
|
237
|
+
});
|
|
238
|
+
// Get styled input
|
|
239
|
+
const answer = await inputBox.prompt({
|
|
240
|
+
showHint: this.history.length === 0,
|
|
241
|
+
hint: 'Type your message or /help for commands'
|
|
247
242
|
});
|
|
248
|
-
// Update history manually or grab from rl?
|
|
249
|
-
// rl.history gets updated when user hits enter.
|
|
250
|
-
// But we closed rl. We should manually save the input to our tracking array and file.
|
|
251
243
|
const input = answer.trim();
|
|
252
244
|
if (input) {
|
|
253
|
-
// Update
|
|
254
|
-
|
|
255
|
-
//
|
|
256
|
-
if (commandHistory[0] !== input) {
|
|
257
|
-
commandHistory.unshift(input);
|
|
258
|
-
}
|
|
259
|
-
// Append to file (as standard log, so append at end)
|
|
245
|
+
// Update history via InputBox
|
|
246
|
+
inputBox.addToHistory(input);
|
|
247
|
+
// Append to file
|
|
260
248
|
try {
|
|
261
249
|
fs.appendFileSync(HISTORY_FILE, input + '\n');
|
|
262
250
|
}
|
|
263
251
|
catch (e) { }
|
|
264
252
|
}
|
|
265
|
-
if (!
|
|
266
|
-
continue;
|
|
253
|
+
if (!input)
|
|
254
|
+
continue;
|
|
267
255
|
if (input.startsWith('/')) {
|
|
268
256
|
await this.handleCommand(input);
|
|
269
257
|
continue;
|
package/dist/skills/Skill.js
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* Agent Skills System
|
|
4
|
+
*
|
|
5
|
+
* Based on Claude Code's Agent Skills format. Skills are reusable AI agent
|
|
6
|
+
* configurations stored as SKILL.md files in dedicated directories.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```markdown
|
|
12
|
+
* ---
|
|
13
|
+
* name: code-reviewer
|
|
14
|
+
* description: Use when the user asks for a code review. Examines code for bugs, style issues, and improvements.
|
|
15
|
+
* allowed-tools: [Read, Grep, Glob]
|
|
16
|
+
* ---
|
|
17
|
+
*
|
|
18
|
+
* Review the code for...
|
|
19
|
+
* ```
|
|
5
20
|
*/
|
|
6
21
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -60,10 +60,20 @@ class SkillsManager {
|
|
|
60
60
|
} = options;
|
|
61
61
|
const discovered = [];
|
|
62
62
|
if (includePersonal) {
|
|
63
|
-
|
|
63
|
+
try {
|
|
64
|
+
discovered.push(...await this.discoverSkillsInDirectory(this.personalSkillsDir, 'personal'));
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
console.warn(`Warning: Failed to load personal skills from ${this.personalSkillsDir}: ${error.message}`);
|
|
68
|
+
}
|
|
64
69
|
}
|
|
65
70
|
if (includeProject) {
|
|
66
|
-
|
|
71
|
+
try {
|
|
72
|
+
discovered.push(...await this.discoverSkillsInDirectory(this.projectSkillsDir, 'project'));
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
console.warn(`Warning: Failed to load project skills from ${this.projectSkillsDir}: ${error.message}`);
|
|
76
|
+
}
|
|
67
77
|
}
|
|
68
78
|
// Store skills in map for quick lookup
|
|
69
79
|
for (const skill of discovered) {
|
|
@@ -109,6 +119,7 @@ class SkillsManager {
|
|
|
109
119
|
const content = fs.readFileSync(skillPath, 'utf-8');
|
|
110
120
|
const frontmatter = this.extractFrontmatter(content);
|
|
111
121
|
if (!frontmatter) {
|
|
122
|
+
console.warn(`Warning: Invalid or missing frontmatter in ${skillPath} (skipping)`);
|
|
112
123
|
return null;
|
|
113
124
|
}
|
|
114
125
|
// Convert SkillFrontmatter to SkillMetadata for validation
|
|
@@ -128,10 +139,24 @@ class SkillsManager {
|
|
|
128
139
|
isValid: validation.isValid,
|
|
129
140
|
errors: validation.errors.length > 0 ? validation.errors : undefined
|
|
130
141
|
};
|
|
142
|
+
// Log validation warnings
|
|
143
|
+
if (validation.warnings.length > 0) {
|
|
144
|
+
for (const warning of validation.warnings) {
|
|
145
|
+
console.warn(`Warning (${skill.name}): ${warning}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
131
148
|
return skill;
|
|
132
149
|
}
|
|
133
150
|
catch (error) {
|
|
134
|
-
|
|
151
|
+
if (error.code === 'ENOENT') {
|
|
152
|
+
console.warn(`Warning: Skill file not found: ${skillPath}`);
|
|
153
|
+
}
|
|
154
|
+
else if (error.code === 'EACCES') {
|
|
155
|
+
console.warn(`Warning: Permission denied reading skill: ${skillPath}`);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
console.error(`Error loading skill metadata from ${skillPath}: ${error.message}`);
|
|
159
|
+
}
|
|
135
160
|
return null;
|
|
136
161
|
}
|
|
137
162
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tests for SkillsManager
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const SkillsManager_1 = require("../SkillsManager");
|
|
7
|
+
describe('SkillsManager', () => {
|
|
8
|
+
let manager;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
manager = new SkillsManager_1.SkillsManager();
|
|
11
|
+
});
|
|
12
|
+
describe('getAllSkills', () => {
|
|
13
|
+
it('should return empty array initially', () => {
|
|
14
|
+
const skills = manager.getAllSkills();
|
|
15
|
+
expect(skills).toEqual([]);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
describe('getSkill', () => {
|
|
19
|
+
it('should return undefined for non-existent skill', () => {
|
|
20
|
+
const skill = manager.getSkill('non-existent');
|
|
21
|
+
expect(skill).toBeUndefined();
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
describe('getSkillsContext', () => {
|
|
25
|
+
it('should return empty string when no skills', () => {
|
|
26
|
+
const context = manager.getSkillsContext();
|
|
27
|
+
expect(context).toBe('');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
describe('addSkill', () => {
|
|
31
|
+
it('should add skill to manager', () => {
|
|
32
|
+
const skill = {
|
|
33
|
+
name: 'test-skill',
|
|
34
|
+
description: 'Test skill',
|
|
35
|
+
path: '/skills/test.md',
|
|
36
|
+
type: 'personal',
|
|
37
|
+
directory: '/skills',
|
|
38
|
+
isValid: true,
|
|
39
|
+
content: 'Test content'
|
|
40
|
+
};
|
|
41
|
+
manager['skills'].set('test-skill', skill);
|
|
42
|
+
const retrieved = manager.getSkill('test-skill');
|
|
43
|
+
expect(retrieved).toBeDefined();
|
|
44
|
+
expect(retrieved?.name).toBe('test-skill');
|
|
45
|
+
});
|
|
46
|
+
it('should return skills context with skills', () => {
|
|
47
|
+
const skill = {
|
|
48
|
+
name: 'reviewer',
|
|
49
|
+
description: 'Code reviewer skill',
|
|
50
|
+
path: '/skills/reviewer.md',
|
|
51
|
+
type: 'personal',
|
|
52
|
+
directory: '/skills',
|
|
53
|
+
isValid: true,
|
|
54
|
+
content: 'Review code'
|
|
55
|
+
};
|
|
56
|
+
manager['skills'].set('reviewer', skill);
|
|
57
|
+
const context = manager.getSkillsContext();
|
|
58
|
+
expect(context).toContain('Available Skills');
|
|
59
|
+
expect(context).toContain('reviewer');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* InputBox - Simple clean input with top line only
|
|
4
|
+
* Bottom line appears after submission (cross-platform compatible)
|
|
5
|
+
*/
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
+
};
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.InputBox = void 0;
|
|
11
|
+
const readline_1 = __importDefault(require("readline"));
|
|
12
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
13
|
+
class InputBox {
|
|
14
|
+
constructor(history = []) {
|
|
15
|
+
this.history = [];
|
|
16
|
+
this.historySize = 1000;
|
|
17
|
+
this.history = history;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Get terminal width
|
|
21
|
+
*/
|
|
22
|
+
getTerminalWidth() {
|
|
23
|
+
return process.stdout.columns || 80;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Create horizontal line
|
|
27
|
+
*/
|
|
28
|
+
createLine() {
|
|
29
|
+
const width = this.getTerminalWidth();
|
|
30
|
+
return chalk_1.default.gray('─'.repeat(width));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Get user input with horizontal lines around it
|
|
34
|
+
*/
|
|
35
|
+
async prompt(options = {}) {
|
|
36
|
+
const { showHint = false, hint } = options;
|
|
37
|
+
// Display top horizontal line
|
|
38
|
+
console.log(this.createLine());
|
|
39
|
+
// Display hint if provided
|
|
40
|
+
if (showHint && hint) {
|
|
41
|
+
console.log(chalk_1.default.dim(` ${hint}`));
|
|
42
|
+
}
|
|
43
|
+
return new Promise((resolve) => {
|
|
44
|
+
// Create readline with simple prompt
|
|
45
|
+
const rl = readline_1.default.createInterface({
|
|
46
|
+
input: process.stdin,
|
|
47
|
+
output: process.stdout,
|
|
48
|
+
prompt: chalk_1.default.cyan('> '),
|
|
49
|
+
history: this.history,
|
|
50
|
+
historySize: this.historySize,
|
|
51
|
+
completer: this.completer.bind(this)
|
|
52
|
+
});
|
|
53
|
+
rl.prompt();
|
|
54
|
+
rl.on('line', (line) => {
|
|
55
|
+
// Display bottom horizontal line after input
|
|
56
|
+
console.log(this.createLine());
|
|
57
|
+
rl.close();
|
|
58
|
+
resolve(line);
|
|
59
|
+
});
|
|
60
|
+
// Handle Ctrl+C
|
|
61
|
+
rl.on('SIGINT', () => {
|
|
62
|
+
console.log(this.createLine());
|
|
63
|
+
rl.close();
|
|
64
|
+
resolve('/exit');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Simple tab completer for commands
|
|
70
|
+
*/
|
|
71
|
+
completer(line) {
|
|
72
|
+
const commands = [
|
|
73
|
+
'/help', '/clear', '/exit', '/update', '/config',
|
|
74
|
+
'/init', '/resume', '/skills', '/commands', '/checkpoint',
|
|
75
|
+
'/model', '/use', '/mcp', '/search', '/run', '/commit'
|
|
76
|
+
];
|
|
77
|
+
const hits = commands.filter(c => c.startsWith(line));
|
|
78
|
+
return [hits.length ? hits : commands, line];
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Add input to history
|
|
82
|
+
*/
|
|
83
|
+
addToHistory(input) {
|
|
84
|
+
if (!input || input === this.history[0])
|
|
85
|
+
return;
|
|
86
|
+
this.history.unshift(input);
|
|
87
|
+
if (this.history.length > this.historySize) {
|
|
88
|
+
this.history = this.history.slice(0, this.historySize);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Get current history
|
|
93
|
+
*/
|
|
94
|
+
getHistory() {
|
|
95
|
+
return this.history;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Display separator and context info before input
|
|
99
|
+
*/
|
|
100
|
+
displayFrame(contextInfo) {
|
|
101
|
+
console.log('');
|
|
102
|
+
// Context bar with message count and percentage
|
|
103
|
+
if (contextInfo) {
|
|
104
|
+
const { messageCount, contextPercent } = contextInfo;
|
|
105
|
+
const color = contextPercent < 60 ? chalk_1.default.green : contextPercent < 80 ? chalk_1.default.yellow : chalk_1.default.red;
|
|
106
|
+
const bar = this.createProgressBar(contextPercent);
|
|
107
|
+
console.log(chalk_1.default.dim(` ${bar} ${messageCount} msgs ${color(contextPercent + '%')}`));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Display separator (alias for displayFrame)
|
|
112
|
+
*/
|
|
113
|
+
displaySeparator(contextInfo) {
|
|
114
|
+
this.displayFrame(contextInfo);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Create a visual progress bar
|
|
118
|
+
*/
|
|
119
|
+
createProgressBar(percentage) {
|
|
120
|
+
const width = 15;
|
|
121
|
+
const filled = Math.round(percentage / 100 * width);
|
|
122
|
+
const empty = width - filled;
|
|
123
|
+
const color = percentage < 60 ? chalk_1.default.green : percentage < 80 ? chalk_1.default.yellow : chalk_1.default.red;
|
|
124
|
+
return color('█'.repeat(filled)) + chalk_1.default.dim('░'.repeat(empty));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
exports.InputBox = InputBox;
|
package/dist/ui/UIManager.js
CHANGED
|
@@ -19,12 +19,12 @@ class UIManager {
|
|
|
19
19
|
whitespaceBreak: true,
|
|
20
20
|
});
|
|
21
21
|
console.log(gradient_string_1.default.pastel.multiline(logoText));
|
|
22
|
-
console.log(chalk_1.default.gray(' v1.1.
|
|
22
|
+
console.log(chalk_1.default.gray(' v1.1.3 - AI Coding Agent'));
|
|
23
23
|
console.log('');
|
|
24
24
|
}
|
|
25
25
|
static renderDashboard(config) {
|
|
26
26
|
const { model, cwd } = config;
|
|
27
|
-
const version = 'v1.1.
|
|
27
|
+
const version = 'v1.1.3';
|
|
28
28
|
// Layout: Left (Status/Welcome) | Right (Tips/Activity)
|
|
29
29
|
// Total width ~80 chars.
|
|
30
30
|
// Left ~45, Right ~30.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Manual mock for chalk to avoid ESM issues in Jest
|
|
3
|
+
module.exports = {
|
|
4
|
+
dim: (str) => str,
|
|
5
|
+
green: (str) => str,
|
|
6
|
+
yellow: (str) => str,
|
|
7
|
+
red: (str) => str,
|
|
8
|
+
gray: (str) => str,
|
|
9
|
+
bold: (str) => str,
|
|
10
|
+
cyan: (str) => str,
|
|
11
|
+
default: {
|
|
12
|
+
dim: (str) => str,
|
|
13
|
+
green: (str) => str,
|
|
14
|
+
yellow: (str) => str,
|
|
15
|
+
red: (str) => str,
|
|
16
|
+
gray: (str) => str,
|
|
17
|
+
bold: (str) => str,
|
|
18
|
+
cyan: (str) => str
|
|
19
|
+
}
|
|
20
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tests for ContextVisualizer
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const ContextVisualizer_1 = require("../ContextVisualizer");
|
|
7
|
+
describe('ContextVisualizer', () => {
|
|
8
|
+
let visualizer;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
visualizer = new ContextVisualizer_1.ContextVisualizer();
|
|
11
|
+
});
|
|
12
|
+
describe('calculateUsage', () => {
|
|
13
|
+
it('should handle empty history', () => {
|
|
14
|
+
const history = [];
|
|
15
|
+
const usage = visualizer.calculateUsage(history);
|
|
16
|
+
// Includes 2000 char overhead = 500 tokens
|
|
17
|
+
expect(usage.tokens).toBe(500);
|
|
18
|
+
expect(usage.maxTokens).toBe(128000);
|
|
19
|
+
expect(usage).toHaveProperty('percentage');
|
|
20
|
+
expect(usage).toHaveProperty('tokens');
|
|
21
|
+
expect(usage).toHaveProperty('maxTokens');
|
|
22
|
+
});
|
|
23
|
+
it('should calculate tokens for messages', () => {
|
|
24
|
+
const history = [
|
|
25
|
+
{ role: 'system', content: 'You are a helpful assistant.' },
|
|
26
|
+
{ role: 'user', content: 'Hello' },
|
|
27
|
+
{ role: 'assistant', content: 'Hi there!' }
|
|
28
|
+
];
|
|
29
|
+
const usage = visualizer.calculateUsage(history);
|
|
30
|
+
expect(usage.tokens).toBeGreaterThan(500);
|
|
31
|
+
expect(usage.tokens).toBeLessThan(1000);
|
|
32
|
+
expect(usage.maxTokens).toBe(128000);
|
|
33
|
+
});
|
|
34
|
+
it('should handle large messages', () => {
|
|
35
|
+
const largeContent = 'x'.repeat(10000);
|
|
36
|
+
const history = [
|
|
37
|
+
{ role: 'user', content: largeContent }
|
|
38
|
+
];
|
|
39
|
+
const usage = visualizer.calculateUsage(history);
|
|
40
|
+
expect(usage.tokens).toBeGreaterThan(1500);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
describe('formatBar', () => {
|
|
44
|
+
it('should format bar at low usage', () => {
|
|
45
|
+
const usage = { tokens: 1000, percentage: 5, maxTokens: 128000 };
|
|
46
|
+
const bar = visualizer.formatBar(usage);
|
|
47
|
+
// Check that bar contains expected data (without chalk dependency)
|
|
48
|
+
expect(bar).toContain('5');
|
|
49
|
+
expect(bar).toContain('1k');
|
|
50
|
+
expect(bar).toContain('128');
|
|
51
|
+
});
|
|
52
|
+
it('should format bar at medium usage', () => {
|
|
53
|
+
const usage = { tokens: 50000, percentage: 40, maxTokens: 128000 };
|
|
54
|
+
const bar = visualizer.formatBar(usage);
|
|
55
|
+
expect(bar).toContain('40');
|
|
56
|
+
expect(bar).toContain('50k');
|
|
57
|
+
});
|
|
58
|
+
it('should format bar at high usage', () => {
|
|
59
|
+
const usage = { tokens: 100000, percentage: 80, maxTokens: 128000 };
|
|
60
|
+
const bar = visualizer.formatBar(usage);
|
|
61
|
+
expect(bar).toContain('80');
|
|
62
|
+
expect(bar).toContain('100k');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe('shouldCompact', () => {
|
|
66
|
+
it('should return false for low percentage', () => {
|
|
67
|
+
const history = [
|
|
68
|
+
{ role: 'user', content: 'small message' }
|
|
69
|
+
];
|
|
70
|
+
const shouldCompact = visualizer.shouldCompact(history);
|
|
71
|
+
expect(shouldCompact).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
it('should return true at 80% threshold', () => {
|
|
74
|
+
// Create enough content to exceed 80%
|
|
75
|
+
// 80% of 128000 tokens = 102400 tokens = ~409600 chars
|
|
76
|
+
// Subtract 2000 overhead = ~407400 chars needed
|
|
77
|
+
const largeContent = 'x'.repeat(410000);
|
|
78
|
+
const history = [
|
|
79
|
+
{ role: 'system', content: largeContent },
|
|
80
|
+
{ role: 'user', content: largeContent }
|
|
81
|
+
];
|
|
82
|
+
const shouldCompact = visualizer.shouldCompact(history);
|
|
83
|
+
expect(shouldCompact).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
describe('setMaxTokens', () => {
|
|
87
|
+
it('should update max tokens', () => {
|
|
88
|
+
visualizer.setMaxTokens(32000);
|
|
89
|
+
const history = [];
|
|
90
|
+
const usage = visualizer.calculateUsage(history);
|
|
91
|
+
expect(usage.maxTokens).toBe(32000);
|
|
92
|
+
expect(usage.percentage).toBeGreaterThan(1); // Should be higher percentage with smaller max
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indiccoder/mentis-cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
"scripts": {
|
|
13
13
|
"build": "tsc",
|
|
14
14
|
"start": "node dist/index.js",
|
|
15
|
-
"test": "
|
|
15
|
+
"test": "jest",
|
|
16
|
+
"test:watch": "jest --watch",
|
|
17
|
+
"test:coverage": "jest --coverage"
|
|
16
18
|
},
|
|
17
19
|
"keywords": [
|
|
18
20
|
"cli",
|
|
@@ -33,6 +35,7 @@
|
|
|
33
35
|
"axios": "^1.13.2",
|
|
34
36
|
"boxen": "^8.0.1",
|
|
35
37
|
"chalk": "^5.6.2",
|
|
38
|
+
"cli-cursor": "^5.0.0",
|
|
36
39
|
"cli-highlight": "^2.1.11",
|
|
37
40
|
"commander": "^14.0.2",
|
|
38
41
|
"dotenv": "^17.2.3",
|
|
@@ -54,11 +57,31 @@
|
|
|
54
57
|
"yaml": "^2.7.0"
|
|
55
58
|
},
|
|
56
59
|
"devDependencies": {
|
|
60
|
+
"@types/cli-cursor": "^2.1.0",
|
|
57
61
|
"@types/figlet": "^1.7.0",
|
|
58
62
|
"@types/fs-extra": "^11.0.4",
|
|
59
63
|
"@types/gradient-string": "^1.1.6",
|
|
60
64
|
"@types/inquirer": "^9.0.9",
|
|
65
|
+
"@types/jest": "^30.0.0",
|
|
61
66
|
"@types/node": "^25.0.2",
|
|
67
|
+
"jest": "^30.2.0",
|
|
68
|
+
"ts-jest": "^29.4.6",
|
|
62
69
|
"typescript": "^5.9.3"
|
|
70
|
+
},
|
|
71
|
+
"jest": {
|
|
72
|
+
"preset": "ts-jest",
|
|
73
|
+
"testEnvironment": "node",
|
|
74
|
+
"roots": ["<rootDir>/src"],
|
|
75
|
+
"testMatch": ["**/__tests__/**/*.ts", "**/?(*.)+(spec|test).ts"],
|
|
76
|
+
"collectCoverageFrom": ["src/**/*.ts", "!src/**/*.d.ts"],
|
|
77
|
+
"transformIgnorePatterns": ["node_modules/(?!(chalk)/)"],
|
|
78
|
+
"coverageThreshold": {
|
|
79
|
+
"global": {
|
|
80
|
+
"branches": 20,
|
|
81
|
+
"functions": 30,
|
|
82
|
+
"lines": 30,
|
|
83
|
+
"statements": 30
|
|
84
|
+
}
|
|
85
|
+
}
|
|
63
86
|
}
|
|
64
87
|
}
|
package/src/commands/Command.ts
CHANGED
|
@@ -1,40 +1,91 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Custom Slash Commands System
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* Users can define their own slash commands as markdown files with YAML frontmatter.
|
|
5
|
+
* Commands support parameter substitution, bash execution, and file references.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```markdown
|
|
11
|
+
* ---
|
|
12
|
+
* description: Run tests and show coverage
|
|
13
|
+
* argument-hint: [test-pattern]
|
|
14
|
+
* ---
|
|
15
|
+
*
|
|
16
|
+
* Run tests with !`npm test $1`
|
|
17
|
+
* ```
|
|
4
18
|
*/
|
|
5
19
|
|
|
20
|
+
/**
|
|
21
|
+
* YAML frontmatter options for custom commands
|
|
22
|
+
*/
|
|
6
23
|
export interface CommandFrontmatter {
|
|
24
|
+
/** Human-readable description of what the command does */
|
|
7
25
|
description?: string;
|
|
26
|
+
/** Restrict which tools the AI can use when executing this command */
|
|
8
27
|
'allowed-tools'?: string[];
|
|
28
|
+
/** Hint shown to user about expected arguments (e.g., "[pattern]") */
|
|
9
29
|
'argument-hint'?: string;
|
|
30
|
+
/** Specific model to use for this command */
|
|
10
31
|
model?: string;
|
|
32
|
+
/** Disable AI model invocation (execute bash/reads only) */
|
|
11
33
|
'disable-model-invocation'?: boolean;
|
|
12
34
|
}
|
|
13
35
|
|
|
36
|
+
/**
|
|
37
|
+
* A custom slash command
|
|
38
|
+
*/
|
|
14
39
|
export interface Command {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
40
|
+
/** Command name (derived from filename, without .md extension) */
|
|
41
|
+
name: string;
|
|
42
|
+
/** Whether this is a personal or project-level command */
|
|
43
|
+
type: 'personal' | 'project';
|
|
44
|
+
/** Absolute path to the command's .md file */
|
|
45
|
+
path: string;
|
|
46
|
+
/** Directory containing the command file */
|
|
47
|
+
directory: string;
|
|
48
|
+
/** Parsed YAML frontmatter from the command file */
|
|
19
49
|
frontmatter: CommandFrontmatter;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
50
|
+
/** Raw markdown content of the command */
|
|
51
|
+
content: string;
|
|
52
|
+
/** Human-readable description for display */
|
|
53
|
+
description: string;
|
|
54
|
+
/** Whether the command uses $1, $2, or $ARGUMENTS placeholders */
|
|
55
|
+
hasParameters: boolean;
|
|
23
56
|
}
|
|
24
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Execution context for running a custom command
|
|
60
|
+
*/
|
|
25
61
|
export interface CommandExecutionContext {
|
|
62
|
+
/** The command being executed */
|
|
26
63
|
command: Command;
|
|
27
|
-
|
|
64
|
+
/** Arguments passed to the command */
|
|
65
|
+
args: string[];
|
|
66
|
+
/** Function to substitute $1, $2, $ARGUMENTS placeholders */
|
|
28
67
|
substitutePlaceholders: (content: string, args: string[]) => string;
|
|
68
|
+
/** Function to execute bash commands */
|
|
29
69
|
executeBash: (bashCommand: string) => Promise<string>;
|
|
70
|
+
/** Function to read file contents */
|
|
30
71
|
readFile: (filePath: string) => Promise<string>;
|
|
31
72
|
}
|
|
32
73
|
|
|
33
74
|
/**
|
|
34
|
-
* Parsed command with substitutions applied
|
|
75
|
+
* Parsed command with all substitutions applied
|
|
76
|
+
*
|
|
77
|
+
* @remarks
|
|
78
|
+
* After parsing, the command content will have:
|
|
79
|
+
* - $1, $2, etc. replaced with positional arguments
|
|
80
|
+
* - $ARGUMENTS replaced with all arguments joined by spaces
|
|
81
|
+
* - !`cmd` patterns extracted to bashCommands and replaced with [BASH_OUTPUT]
|
|
82
|
+
* - @file patterns extracted to fileReferences
|
|
35
83
|
*/
|
|
36
84
|
export interface ParsedCommand {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
85
|
+
/** Content with all substitutions applied */
|
|
86
|
+
content: string;
|
|
87
|
+
/** Bash commands extracted from !`cmd` patterns */
|
|
88
|
+
bashCommands: string[];
|
|
89
|
+
/** File paths extracted from @file patterns */
|
|
90
|
+
fileReferences: string[];
|
|
40
91
|
}
|