@indiccoder/mentis-cli 1.1.3 → 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/skills/Skill.js +17 -2
- package/dist/skills/SkillsManager.js +28 -3
- package/dist/skills/__tests__/SkillsManager.test.js +62 -0
- package/dist/utils/__mocks__/chalk.js +20 -0
- package/dist/utils/__tests__/ContextVisualizer.test.js +95 -0
- package/package.json +23 -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/skills/Skill.ts +91 -11
- package/src/skills/SkillsManager.ts +25 -3
- package/src/skills/__tests__/SkillsManager.test.ts +73 -0
- package/src/utils/__mocks__/chalk.ts +19 -0
- package/src/utils/__tests__/ContextVisualizer.test.ts +118 -0
|
@@ -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,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",
|
|
@@ -60,7 +62,26 @@
|
|
|
60
62
|
"@types/fs-extra": "^11.0.4",
|
|
61
63
|
"@types/gradient-string": "^1.1.6",
|
|
62
64
|
"@types/inquirer": "^9.0.9",
|
|
65
|
+
"@types/jest": "^30.0.0",
|
|
63
66
|
"@types/node": "^25.0.2",
|
|
67
|
+
"jest": "^30.2.0",
|
|
68
|
+
"ts-jest": "^29.4.6",
|
|
64
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
|
+
}
|
|
65
86
|
}
|
|
66
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
|
}
|
|
@@ -31,10 +31,18 @@ export class CommandManager {
|
|
|
31
31
|
const discovered: Command[] = [];
|
|
32
32
|
|
|
33
33
|
// Personal commands
|
|
34
|
-
|
|
34
|
+
try {
|
|
35
|
+
discovered.push(...await this.discoverCommandsInDirectory(this.personalCommandsDir, 'personal'));
|
|
36
|
+
} catch (error: any) {
|
|
37
|
+
console.warn(`Warning: Failed to load personal commands from ${this.personalCommandsDir}: ${error.message}`);
|
|
38
|
+
}
|
|
35
39
|
|
|
36
40
|
// Project commands
|
|
37
|
-
|
|
41
|
+
try {
|
|
42
|
+
discovered.push(...await this.discoverCommandsInDirectory(this.projectCommandsDir, 'project'));
|
|
43
|
+
} catch (error: any) {
|
|
44
|
+
console.warn(`Warning: Failed to load project commands from ${this.projectCommandsDir}: ${error.message}`);
|
|
45
|
+
}
|
|
38
46
|
|
|
39
47
|
// Store commands in map (project commands override personal)
|
|
40
48
|
for (const command of discovered) {
|
|
@@ -85,6 +93,7 @@ export class CommandManager {
|
|
|
85
93
|
const commandName = this.getCommandName(commandPath, type);
|
|
86
94
|
|
|
87
95
|
if (!commandName) {
|
|
96
|
+
console.warn(`Warning: Invalid command name in ${commandPath} (skipping)`);
|
|
88
97
|
return null;
|
|
89
98
|
}
|
|
90
99
|
|
|
@@ -107,7 +116,13 @@ export class CommandManager {
|
|
|
107
116
|
|
|
108
117
|
return command;
|
|
109
118
|
} catch (error: any) {
|
|
110
|
-
|
|
119
|
+
if (error.code === 'ENOENT') {
|
|
120
|
+
console.warn(`Warning: Command file not found: ${commandPath}`);
|
|
121
|
+
} else if (error.code === 'EACCES') {
|
|
122
|
+
console.warn(`Warning: Permission denied reading command: ${commandPath}`);
|
|
123
|
+
} else {
|
|
124
|
+
console.error(`Error parsing command ${commandPath}: ${error.message}`);
|
|
125
|
+
}
|
|
111
126
|
return null;
|
|
112
127
|
}
|
|
113
128
|
}
|
|
@@ -222,7 +237,10 @@ export class CommandManager {
|
|
|
222
237
|
const bashRegex = /!`([^`]+)`/g;
|
|
223
238
|
let bashMatch;
|
|
224
239
|
while ((bashMatch = bashRegex.exec(content)) !== null) {
|
|
225
|
-
|
|
240
|
+
const bashCommand = bashMatch[1].trim();
|
|
241
|
+
if (bashCommand) {
|
|
242
|
+
bashCommands.push(bashCommand);
|
|
243
|
+
}
|
|
226
244
|
}
|
|
227
245
|
|
|
228
246
|
// Remove bash command markers
|
|
@@ -232,7 +250,10 @@ export class CommandManager {
|
|
|
232
250
|
const fileRegex = /@([^\s]+)/g;
|
|
233
251
|
let fileMatch;
|
|
234
252
|
while ((fileMatch = fileRegex.exec(content)) !== null) {
|
|
235
|
-
|
|
253
|
+
const fileRef = fileMatch[1].trim();
|
|
254
|
+
if (fileRef && !fileReferences.includes(fileRef)) {
|
|
255
|
+
fileReferences.push(fileRef);
|
|
256
|
+
}
|
|
236
257
|
}
|
|
237
258
|
|
|
238
259
|
return { content, bashCommands, fileReferences };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for CommandManager
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { CommandManager } from '../CommandManager';
|
|
6
|
+
import { Command } from '../Command';
|
|
7
|
+
|
|
8
|
+
describe('CommandManager', () => {
|
|
9
|
+
let manager: CommandManager;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
manager = new CommandManager();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('getAllCommands', () => {
|
|
16
|
+
it('should return empty array initially', () => {
|
|
17
|
+
const commands = manager.getAllCommands();
|
|
18
|
+
expect(commands).toEqual([]);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('getCommandsContext', () => {
|
|
23
|
+
it('should return empty string when no commands', () => {
|
|
24
|
+
const context = manager.getCommandsContext();
|
|
25
|
+
expect(context).toBe('');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('parseCommand', () => {
|
|
30
|
+
it('should replace $ARGUMENTS placeholder', async () => {
|
|
31
|
+
const command: Command = {
|
|
32
|
+
name: 'echo',
|
|
33
|
+
type: 'personal',
|
|
34
|
+
path: '/echo.md',
|
|
35
|
+
directory: '/commands',
|
|
36
|
+
description: 'Echo arguments',
|
|
37
|
+
frontmatter: {},
|
|
38
|
+
content: 'You said: $ARGUMENTS',
|
|
39
|
+
hasParameters: true
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const parsed = await manager.parseCommand(command, ['hello', 'world']);
|
|
43
|
+
|
|
44
|
+
expect(parsed.content).toContain('hello world');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should replace $1, $2 placeholders', async () => {
|
|
48
|
+
const command: Command = {
|
|
49
|
+
name: 'greet',
|
|
50
|
+
type: 'personal',
|
|
51
|
+
path: '/greet.md',
|
|
52
|
+
directory: '/commands',
|
|
53
|
+
description: 'Greet user',
|
|
54
|
+
frontmatter: {},
|
|
55
|
+
content: 'Hello $1, welcome to $2',
|
|
56
|
+
hasParameters: true
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const parsed = await manager.parseCommand(command, ['Alice', 'Wonderland']);
|
|
60
|
+
|
|
61
|
+
expect(parsed.content).toContain('Hello Alice');
|
|
62
|
+
expect(parsed.content).toContain('welcome to Wonderland');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should extract bash commands from content', async () => {
|
|
66
|
+
const command: Command = {
|
|
67
|
+
name: 'run-test',
|
|
68
|
+
type: 'personal',
|
|
69
|
+
path: '/run-test.md',
|
|
70
|
+
directory: '/commands',
|
|
71
|
+
description: 'Run tests',
|
|
72
|
+
frontmatter: {},
|
|
73
|
+
content: 'Run tests with !`npm test`',
|
|
74
|
+
hasParameters: false
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const parsed = await manager.parseCommand(command, []);
|
|
78
|
+
|
|
79
|
+
expect(parsed.bashCommands).toHaveLength(1);
|
|
80
|
+
expect(parsed.bashCommands[0]).toBe('npm test');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1,13 +1,39 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mentis CLI - An Agentic, Multi-Model CLI Coding Assistant
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
|
|
2
9
|
import { ReplManager } from './repl/ReplManager';
|
|
3
10
|
|
|
11
|
+
/**
|
|
12
|
+
* CLI options for controlling Mentis behavior
|
|
13
|
+
*/
|
|
4
14
|
interface CliOptions {
|
|
15
|
+
/** Resume from the last saved checkpoint */
|
|
5
16
|
resume: boolean;
|
|
17
|
+
/** Auto-confirm all prompts (skip confirmations) */
|
|
6
18
|
yolo: boolean;
|
|
19
|
+
/** Run in headless (non-interactive) mode */
|
|
7
20
|
headless: boolean;
|
|
21
|
+
/** Prompt to execute in headless mode */
|
|
8
22
|
headlessPrompt?: string;
|
|
9
23
|
}
|
|
10
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Parse command line arguments
|
|
27
|
+
*
|
|
28
|
+
* @returns Parsed command and options
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```bash
|
|
32
|
+
* mentis --resume
|
|
33
|
+
* mentis -p "fix the bug"
|
|
34
|
+
* mentis --yolo
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
11
37
|
function parseArgs(): { command: string | null, options: CliOptions } {
|
|
12
38
|
const args = process.argv.slice(2);
|
|
13
39
|
const options: CliOptions = {
|
|
@@ -69,7 +95,12 @@ Commands (in REPL):
|
|
|
69
95
|
return { command, options };
|
|
70
96
|
}
|
|
71
97
|
|
|
72
|
-
|
|
98
|
+
/**
|
|
99
|
+
* Main entry point for Mentis CLI
|
|
100
|
+
*
|
|
101
|
+
* Parses arguments and starts the REPL or update manager
|
|
102
|
+
*/
|
|
103
|
+
async function main(): Promise<void> {
|
|
73
104
|
const { command, options } = parseArgs();
|
|
74
105
|
|
|
75
106
|
// Handle update command
|
|
@@ -85,6 +116,7 @@ async function main() {
|
|
|
85
116
|
await repl.start();
|
|
86
117
|
}
|
|
87
118
|
|
|
119
|
+
// Start the application
|
|
88
120
|
main().catch((error) => {
|
|
89
121
|
console.error('Fatal error:', error);
|
|
90
122
|
process.exit(1);
|