@indiccoder/mentis-cli 1.1.3 → 1.1.5
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/.claude/settings.local.json +8 -0
- package/.mentis/session.json +15 -0
- package/.mentis/sessions/1769189035730.json +23 -0
- package/.mentis/sessions/1769189569160.json +23 -0
- package/.mentis/sessions/1769767538672.json +23 -0
- package/.mentis/sessions/1769767785155.json +23 -0
- package/.mentis/sessions/1769768745802.json +23 -0
- package/.mentis/sessions/1769769600884.json +31 -0
- package/.mentis/sessions/1769770030160.json +31 -0
- package/.mentis/sessions/1769770606004.json +78 -0
- package/.mentis/sessions/1769771084515.json +141 -0
- package/.mentis/sessions/1769881926630.json +57 -0
- package/ARCHITECTURE.md +267 -0
- package/CONTRIBUTING.md +209 -0
- package/README.md +17 -0
- package/dist/checkpoint/CheckpointManager.js +92 -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/debug_google.js +61 -0
- package/dist/debug_lite.js +49 -0
- package/dist/debug_lite_headers.js +57 -0
- package/dist/debug_search.js +16 -0
- package/dist/index.js +33 -0
- package/dist/mcp/JsonRpcClient.js +16 -0
- package/dist/mcp/McpConfig.js +132 -0
- package/dist/mcp/McpManager.js +189 -0
- package/dist/repl/PersistentShell.js +20 -1
- package/dist/repl/ReplManager.js +410 -138
- 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/tools/AskQuestionTool.js +172 -0
- package/dist/tools/EditFileTool.js +141 -0
- package/dist/tools/FileTools.js +7 -1
- package/dist/tools/PlanModeTool.js +53 -0
- package/dist/tools/WebSearchTool.js +190 -27
- package/dist/ui/DiffViewer.js +110 -0
- package/dist/ui/InputBox.js +16 -2
- package/dist/ui/MultiFileSelector.js +123 -0
- package/dist/ui/PlanModeUI.js +105 -0
- package/dist/ui/ToolExecutor.js +154 -0
- package/dist/ui/UIManager.js +12 -2
- package/dist/utils/__mocks__/chalk.js +20 -0
- package/dist/utils/__tests__/ContextVisualizer.test.js +95 -0
- package/docs/MCP_INTEGRATION.md +290 -0
- package/google_dump.html +18 -0
- package/lite_dump.html +176 -0
- package/lite_headers_dump.html +176 -0
- package/package.json +34 -2
- package/scripts/test_exa_mcp.ts +90 -0
- package/src/checkpoint/CheckpointManager.ts +102 -0
- 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/debug_google.ts +30 -0
- package/src/debug_lite.ts +18 -0
- package/src/debug_lite_headers.ts +25 -0
- package/src/debug_search.ts +18 -0
- package/src/index.ts +45 -1
- package/src/mcp/JsonRpcClient.ts +19 -0
- package/src/mcp/McpConfig.ts +153 -0
- package/src/mcp/McpManager.ts +224 -0
- package/src/repl/PersistentShell.ts +24 -1
- package/src/repl/ReplManager.ts +1521 -1204
- 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/tools/AskQuestionTool.ts +197 -0
- package/src/tools/EditFileTool.ts +172 -0
- package/src/tools/FileTools.ts +3 -0
- package/src/tools/PlanModeTool.ts +50 -0
- package/src/tools/WebSearchTool.ts +235 -63
- package/src/ui/DiffViewer.ts +117 -0
- package/src/ui/InputBox.ts +17 -2
- package/src/ui/MultiFileSelector.ts +135 -0
- package/src/ui/PlanModeUI.ts +121 -0
- package/src/ui/ToolExecutor.ts +182 -0
- package/src/ui/UIManager.ts +15 -2
- package/src/utils/__mocks__/chalk.ts +19 -0
- package/src/utils/__tests__/ContextVisualizer.test.ts +118 -0
- package/console.log(tick) +0 -0
package/src/skills/Skill.ts
CHANGED
|
@@ -1,26 +1,96 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Agent Skills System
|
|
3
|
+
*
|
|
4
|
+
* Based on Claude Code's Agent Skills format. Skills are reusable AI agent
|
|
5
|
+
* configurations stored as SKILL.md files in dedicated directories.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```markdown
|
|
11
|
+
* ---
|
|
12
|
+
* name: code-reviewer
|
|
13
|
+
* description: Use when the user asks for a code review. Examines code for bugs, style issues, and improvements.
|
|
14
|
+
* allowed-tools: [Read, Grep, Glob]
|
|
15
|
+
* ---
|
|
16
|
+
*
|
|
17
|
+
* Review the code for...
|
|
18
|
+
* ```
|
|
4
19
|
*/
|
|
5
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Core metadata for an Agent Skill
|
|
23
|
+
*
|
|
24
|
+
* @remarks
|
|
25
|
+
* Skills define specialized AI behaviors with optional tool restrictions.
|
|
26
|
+
* The description should include when to use the skill (e.g., "Use when...").
|
|
27
|
+
*/
|
|
6
28
|
export interface SkillMetadata {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Lowercase skill name with numbers and hyphens only (max 64 chars)
|
|
31
|
+
*
|
|
32
|
+
* @pattern ^[a-z0-9-]+$
|
|
33
|
+
* @maxLength 64
|
|
34
|
+
*/
|
|
35
|
+
name: string;
|
|
36
|
+
/**
|
|
37
|
+
* What the skill does and when to use it (max 1024 chars)
|
|
38
|
+
*
|
|
39
|
+
* @remarks
|
|
40
|
+
* Should include phrases like "Use when..." or "Use for..."
|
|
41
|
+
* to help the AI understand when to invoke this skill.
|
|
42
|
+
*
|
|
43
|
+
* @maxLength 1024
|
|
44
|
+
*/
|
|
45
|
+
description: string;
|
|
46
|
+
/**
|
|
47
|
+
* Optional list of tools the skill is allowed to use
|
|
48
|
+
*
|
|
49
|
+
* @remarks
|
|
50
|
+
* If specified, the AI will be restricted to only these tools
|
|
51
|
+
* when executing this skill.
|
|
52
|
+
*/
|
|
53
|
+
allowedTools?: string[];
|
|
10
54
|
}
|
|
11
55
|
|
|
56
|
+
/**
|
|
57
|
+
* A fully loaded Agent Skill
|
|
58
|
+
*/
|
|
12
59
|
export interface Skill extends SkillMetadata {
|
|
13
|
-
|
|
60
|
+
/** Absolute path to the SKILL.md file */
|
|
61
|
+
path: string;
|
|
62
|
+
/** Whether this is personal, project, or plugin skill */
|
|
14
63
|
type: 'personal' | 'project' | 'plugin';
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Full skill content (loaded on demand)
|
|
66
|
+
*
|
|
67
|
+
* @remarks
|
|
68
|
+
* For progressive disclosure, only metadata is loaded initially.
|
|
69
|
+
* The full content is loaded only when the skill is invoked.
|
|
70
|
+
*/
|
|
71
|
+
content?: string;
|
|
72
|
+
/** Directory containing the skill (for resolving supporting files) */
|
|
73
|
+
directory: string;
|
|
74
|
+
/** Whether the skill passes validation */
|
|
75
|
+
isValid: boolean;
|
|
76
|
+
/** Validation errors if any */
|
|
77
|
+
errors?: string[];
|
|
19
78
|
}
|
|
20
79
|
|
|
80
|
+
/**
|
|
81
|
+
* YAML frontmatter parsed from SKILL.md
|
|
82
|
+
*/
|
|
21
83
|
export interface SkillFrontmatter {
|
|
84
|
+
/** Skill name */
|
|
22
85
|
name: string;
|
|
86
|
+
/** Skill description */
|
|
23
87
|
description: string;
|
|
88
|
+
/**
|
|
89
|
+
* Optional tool restrictions
|
|
90
|
+
*
|
|
91
|
+
* @remarks
|
|
92
|
+
* YAML key uses kebab-case: allowed-tools
|
|
93
|
+
*/
|
|
24
94
|
'allowed-tools'?: string[];
|
|
25
95
|
}
|
|
26
96
|
|
|
@@ -28,13 +98,20 @@ export interface SkillFrontmatter {
|
|
|
28
98
|
* Validation result for a skill
|
|
29
99
|
*/
|
|
30
100
|
export interface SkillValidationResult {
|
|
101
|
+
/** Whether the skill is valid */
|
|
31
102
|
isValid: boolean;
|
|
103
|
+
/** Validation errors that must be fixed */
|
|
32
104
|
errors: string[];
|
|
105
|
+
/** Warnings that don't block usage */
|
|
33
106
|
warnings: string[];
|
|
34
107
|
}
|
|
35
108
|
|
|
36
109
|
/**
|
|
37
|
-
*
|
|
110
|
+
* Minimal skill context for model injection
|
|
111
|
+
*
|
|
112
|
+
* @remarks
|
|
113
|
+
* This is the format used when injecting skills into the system prompt.
|
|
114
|
+
* Only name and description are included (progressive disclosure).
|
|
38
115
|
*/
|
|
39
116
|
export interface SkillContext {
|
|
40
117
|
name: string;
|
|
@@ -45,7 +122,10 @@ export interface SkillContext {
|
|
|
45
122
|
* Options for skill discovery
|
|
46
123
|
*/
|
|
47
124
|
export interface SkillDiscoveryOptions {
|
|
125
|
+
/** Include personal skills from ~/.mentis/skills */
|
|
48
126
|
includePersonal?: boolean;
|
|
127
|
+
/** Include project skills from .mentis/skills */
|
|
49
128
|
includeProject?: boolean;
|
|
129
|
+
/** Include plugin skills (not yet implemented) */
|
|
50
130
|
includePlugin?: boolean;
|
|
51
131
|
}
|
|
@@ -43,11 +43,19 @@ export class SkillsManager {
|
|
|
43
43
|
const discovered: Skill[] = [];
|
|
44
44
|
|
|
45
45
|
if (includePersonal) {
|
|
46
|
-
|
|
46
|
+
try {
|
|
47
|
+
discovered.push(...await this.discoverSkillsInDirectory(this.personalSkillsDir, 'personal'));
|
|
48
|
+
} catch (error: any) {
|
|
49
|
+
console.warn(`Warning: Failed to load personal skills from ${this.personalSkillsDir}: ${error.message}`);
|
|
50
|
+
}
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
if (includeProject) {
|
|
50
|
-
|
|
54
|
+
try {
|
|
55
|
+
discovered.push(...await this.discoverSkillsInDirectory(this.projectSkillsDir, 'project'));
|
|
56
|
+
} catch (error: any) {
|
|
57
|
+
console.warn(`Warning: Failed to load project skills from ${this.projectSkillsDir}: ${error.message}`);
|
|
58
|
+
}
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
// Store skills in map for quick lookup
|
|
@@ -102,6 +110,7 @@ export class SkillsManager {
|
|
|
102
110
|
const frontmatter = this.extractFrontmatter(content);
|
|
103
111
|
|
|
104
112
|
if (!frontmatter) {
|
|
113
|
+
console.warn(`Warning: Invalid or missing frontmatter in ${skillPath} (skipping)`);
|
|
105
114
|
return null;
|
|
106
115
|
}
|
|
107
116
|
|
|
@@ -124,9 +133,22 @@ export class SkillsManager {
|
|
|
124
133
|
errors: validation.errors.length > 0 ? validation.errors : undefined
|
|
125
134
|
};
|
|
126
135
|
|
|
136
|
+
// Log validation warnings
|
|
137
|
+
if (validation.warnings.length > 0) {
|
|
138
|
+
for (const warning of validation.warnings) {
|
|
139
|
+
console.warn(`Warning (${skill.name}): ${warning}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
127
143
|
return skill;
|
|
128
144
|
} catch (error: any) {
|
|
129
|
-
|
|
145
|
+
if (error.code === 'ENOENT') {
|
|
146
|
+
console.warn(`Warning: Skill file not found: ${skillPath}`);
|
|
147
|
+
} else if (error.code === 'EACCES') {
|
|
148
|
+
console.warn(`Warning: Permission denied reading skill: ${skillPath}`);
|
|
149
|
+
} else {
|
|
150
|
+
console.error(`Error loading skill metadata from ${skillPath}: ${error.message}`);
|
|
151
|
+
}
|
|
130
152
|
return null;
|
|
131
153
|
}
|
|
132
154
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for SkillsManager
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { SkillsManager } from '../SkillsManager';
|
|
6
|
+
import { Skill } from '../Skill';
|
|
7
|
+
|
|
8
|
+
describe('SkillsManager', () => {
|
|
9
|
+
let manager: SkillsManager;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
manager = new SkillsManager();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('getAllSkills', () => {
|
|
16
|
+
it('should return empty array initially', () => {
|
|
17
|
+
const skills = manager.getAllSkills();
|
|
18
|
+
expect(skills).toEqual([]);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('getSkill', () => {
|
|
23
|
+
it('should return undefined for non-existent skill', () => {
|
|
24
|
+
const skill = manager.getSkill('non-existent');
|
|
25
|
+
expect(skill).toBeUndefined();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('getSkillsContext', () => {
|
|
30
|
+
it('should return empty string when no skills', () => {
|
|
31
|
+
const context = manager.getSkillsContext();
|
|
32
|
+
expect(context).toBe('');
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('addSkill', () => {
|
|
37
|
+
it('should add skill to manager', () => {
|
|
38
|
+
const skill: Skill = {
|
|
39
|
+
name: 'test-skill',
|
|
40
|
+
description: 'Test skill',
|
|
41
|
+
path: '/skills/test.md',
|
|
42
|
+
type: 'personal',
|
|
43
|
+
directory: '/skills',
|
|
44
|
+
isValid: true,
|
|
45
|
+
content: 'Test content'
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
manager['skills'].set('test-skill', skill);
|
|
49
|
+
const retrieved = manager.getSkill('test-skill');
|
|
50
|
+
|
|
51
|
+
expect(retrieved).toBeDefined();
|
|
52
|
+
expect(retrieved?.name).toBe('test-skill');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should return skills context with skills', () => {
|
|
56
|
+
const skill: Skill = {
|
|
57
|
+
name: 'reviewer',
|
|
58
|
+
description: 'Code reviewer skill',
|
|
59
|
+
path: '/skills/reviewer.md',
|
|
60
|
+
type: 'personal',
|
|
61
|
+
directory: '/skills',
|
|
62
|
+
isValid: true,
|
|
63
|
+
content: 'Review code'
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
manager['skills'].set('reviewer', skill);
|
|
67
|
+
const context = manager.getSkillsContext();
|
|
68
|
+
|
|
69
|
+
expect(context).toContain('Available Skills');
|
|
70
|
+
expect(context).toContain('reviewer');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { Tool } from './Tool';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Question types supported by AskQuestionTool
|
|
7
|
+
*/
|
|
8
|
+
export type QuestionType = 'text' | 'confirm' | 'list' | 'checkbox';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Question definition for the AI to create
|
|
12
|
+
*/
|
|
13
|
+
interface QuestionDef {
|
|
14
|
+
type: QuestionType;
|
|
15
|
+
question: string;
|
|
16
|
+
options?: Array<{ label: string; description?: string }>;
|
|
17
|
+
default?: any;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* AskQuestionTool - Allows the AI to ask clarifying questions
|
|
22
|
+
* This enables interactive plan mode where AI can gather requirements
|
|
23
|
+
*/
|
|
24
|
+
export class AskQuestionTool implements Tool {
|
|
25
|
+
name = 'ask_question';
|
|
26
|
+
description = 'Ask the user a clarifying question. Use this in plan mode to gather requirements before implementation. Supports: text, confirm (yes/no), list (single choice), checkbox (multi-select).';
|
|
27
|
+
parameters = {
|
|
28
|
+
type: 'object',
|
|
29
|
+
properties: {
|
|
30
|
+
question: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
description: 'The question to ask the user'
|
|
33
|
+
},
|
|
34
|
+
type: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
enum: ['text', 'confirm', 'list', 'checkbox'],
|
|
37
|
+
description: 'Type of question: text (free input), confirm (yes/no), list (single choice), checkbox (multi-select)'
|
|
38
|
+
},
|
|
39
|
+
options: {
|
|
40
|
+
type: 'array',
|
|
41
|
+
items: {
|
|
42
|
+
type: 'object',
|
|
43
|
+
properties: {
|
|
44
|
+
label: { type: 'string', description: 'Display text for the option' },
|
|
45
|
+
description: { type: 'string', description: 'Additional context (optional)' }
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
description: 'Options for list/checkbox questions. Required for list/checkbox types.'
|
|
49
|
+
},
|
|
50
|
+
default: {
|
|
51
|
+
oneOf: [{ type: 'string' }, { type: 'boolean' }, { type: 'array' }],
|
|
52
|
+
description: 'Default value (optional)'
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
required: ['question', 'type']
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Execute the question and return the user's answer
|
|
60
|
+
*/
|
|
61
|
+
async execute(args: QuestionDef & { question: string }): Promise<string> {
|
|
62
|
+
const questionType = args.type || 'text';
|
|
63
|
+
|
|
64
|
+
// Show question header
|
|
65
|
+
console.log('');
|
|
66
|
+
console.log(chalk.cyan('🤔 Question from AI:'));
|
|
67
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
let result: any;
|
|
71
|
+
|
|
72
|
+
switch (questionType) {
|
|
73
|
+
case 'text':
|
|
74
|
+
result = await this.askText(args.question, args.default);
|
|
75
|
+
break;
|
|
76
|
+
|
|
77
|
+
case 'confirm':
|
|
78
|
+
result = await this.askConfirm(args.question, args.default);
|
|
79
|
+
break;
|
|
80
|
+
|
|
81
|
+
case 'list':
|
|
82
|
+
if (!args.options || args.options.length === 0) {
|
|
83
|
+
return 'Error: list questions require options';
|
|
84
|
+
}
|
|
85
|
+
result = await this.askList(args.question, args.options, args.default);
|
|
86
|
+
break;
|
|
87
|
+
|
|
88
|
+
case 'checkbox':
|
|
89
|
+
if (!args.options || args.options.length === 0) {
|
|
90
|
+
return 'Error: checkbox questions require options';
|
|
91
|
+
}
|
|
92
|
+
result = await this.askCheckbox(args.question, args.options, args.default);
|
|
93
|
+
break;
|
|
94
|
+
|
|
95
|
+
default:
|
|
96
|
+
return `Error: Unknown question type: ${questionType}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
100
|
+
|
|
101
|
+
// Format result as string for return to LLM
|
|
102
|
+
return this.formatResult(result, questionType);
|
|
103
|
+
|
|
104
|
+
} catch (error: any) {
|
|
105
|
+
return `Error asking question: ${error.message}`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Ask a free-text question
|
|
111
|
+
*/
|
|
112
|
+
private async askText(question: string, defaultValue?: string): Promise<string> {
|
|
113
|
+
const { answer } = await inquirer.prompt([
|
|
114
|
+
{
|
|
115
|
+
type: 'input',
|
|
116
|
+
name: 'answer',
|
|
117
|
+
message: question,
|
|
118
|
+
default: defaultValue
|
|
119
|
+
}
|
|
120
|
+
]);
|
|
121
|
+
return answer;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Ask a yes/no confirmation
|
|
126
|
+
*/
|
|
127
|
+
private async askConfirm(question: string, defaultValue?: boolean): Promise<boolean> {
|
|
128
|
+
const { answer } = await inquirer.prompt([
|
|
129
|
+
{
|
|
130
|
+
type: 'confirm',
|
|
131
|
+
name: 'answer',
|
|
132
|
+
message: question,
|
|
133
|
+
default: defaultValue ?? true
|
|
134
|
+
}
|
|
135
|
+
]);
|
|
136
|
+
return answer;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Ask a single-choice list question
|
|
141
|
+
*/
|
|
142
|
+
private async askList(question: string, options: Array<{ label: string; description?: string }>, defaultValue?: string): Promise<string> {
|
|
143
|
+
const { answer } = await inquirer.prompt([
|
|
144
|
+
{
|
|
145
|
+
type: 'list',
|
|
146
|
+
name: 'answer',
|
|
147
|
+
message: question,
|
|
148
|
+
choices: options.map((opt, i) => ({
|
|
149
|
+
name: opt.label,
|
|
150
|
+
value: opt.label,
|
|
151
|
+
short: opt.label
|
|
152
|
+
})),
|
|
153
|
+
default: defaultValue
|
|
154
|
+
}
|
|
155
|
+
]);
|
|
156
|
+
return answer;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Ask a multi-select checkbox question
|
|
161
|
+
*/
|
|
162
|
+
private async askCheckbox(question: string, options: Array<{ label: string; description?: string }>, defaultValue?: string[]): Promise<string[]> {
|
|
163
|
+
const { answer } = await inquirer.prompt([
|
|
164
|
+
{
|
|
165
|
+
type: 'checkbox',
|
|
166
|
+
name: 'answer',
|
|
167
|
+
message: question,
|
|
168
|
+
choices: options.map((opt, i) => ({
|
|
169
|
+
name: opt.label,
|
|
170
|
+
value: opt.label,
|
|
171
|
+
checked: defaultValue?.includes(opt.label),
|
|
172
|
+
short: opt.label
|
|
173
|
+
}))
|
|
174
|
+
}
|
|
175
|
+
]);
|
|
176
|
+
return answer;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Format the result for return to the LLM
|
|
181
|
+
*/
|
|
182
|
+
private formatResult(result: any, questionType: QuestionType): string {
|
|
183
|
+
switch (questionType) {
|
|
184
|
+
case 'confirm':
|
|
185
|
+
return result === true ? 'Yes' : 'No';
|
|
186
|
+
|
|
187
|
+
case 'checkbox':
|
|
188
|
+
if (Array.isArray(result) && result.length > 0) {
|
|
189
|
+
return `Selected: ${result.join(', ')}`;
|
|
190
|
+
}
|
|
191
|
+
return 'None selected';
|
|
192
|
+
|
|
193
|
+
default:
|
|
194
|
+
return String(result);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { Tool } from './Tool';
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
3
|
+
import { resolve, dirname } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* EditFileTool - Performs string replacement in files (like Claude's Edit tool)
|
|
8
|
+
* Returns a unified diff preview instead of writing immediately
|
|
9
|
+
*/
|
|
10
|
+
export class EditFileTool implements Tool {
|
|
11
|
+
name = 'edit_file';
|
|
12
|
+
description = 'Make targeted edits to files using string replacement. Returns diff preview. Requires approval before writing.';
|
|
13
|
+
parameters = {
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: {
|
|
16
|
+
file_path: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
description: 'The path to the file to edit'
|
|
19
|
+
},
|
|
20
|
+
old_string: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
description: 'The exact string to replace. Must match exactly (including whitespace).'
|
|
23
|
+
},
|
|
24
|
+
new_string: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
description: 'The new string to replace old_string with.'
|
|
27
|
+
},
|
|
28
|
+
auto_format: {
|
|
29
|
+
type: 'boolean',
|
|
30
|
+
description: 'Auto-format code after edit (default: false)'
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
required: ['file_path', 'old_string', 'new_string']
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Execute the edit and return a diff preview
|
|
38
|
+
* Note: This does NOT write the file - it returns what WOULD change
|
|
39
|
+
* The caller (ReplManager) should handle approval before calling applyEdit()
|
|
40
|
+
*/
|
|
41
|
+
async execute(args: {
|
|
42
|
+
file_path: string;
|
|
43
|
+
old_string: string;
|
|
44
|
+
new_string: string;
|
|
45
|
+
auto_format?: boolean;
|
|
46
|
+
}): Promise<string> {
|
|
47
|
+
const filePath = resolve(process.cwd(), args.file_path);
|
|
48
|
+
|
|
49
|
+
if (!existsSync(filePath)) {
|
|
50
|
+
return `Error: File not found: ${args.file_path}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const originalContent = readFileSync(filePath, 'utf-8');
|
|
54
|
+
|
|
55
|
+
if (!originalContent.includes(args.old_string)) {
|
|
56
|
+
return `Error: old_string not found in file. The string must match exactly (including whitespace and indentation).`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Count occurrences
|
|
60
|
+
const occurrences = (originalContent.match(new RegExp(this.escapeRegex(args.old_string), 'g')) || []).length;
|
|
61
|
+
|
|
62
|
+
if (occurrences > 1) {
|
|
63
|
+
return `Warning: old_string found ${occurrences} times. All occurrences will be replaced.\n\n${this.generateDiff(originalContent, args.old_string, args.new_string, args.file_path)}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Generate and return diff
|
|
67
|
+
return this.generateDiff(originalContent, args.old_string, args.new_string, args.file_path);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Apply the edit after approval
|
|
72
|
+
* This should be called after user approves the diff
|
|
73
|
+
*/
|
|
74
|
+
applyEdit(args: {
|
|
75
|
+
file_path: string;
|
|
76
|
+
old_string: string;
|
|
77
|
+
new_string: string;
|
|
78
|
+
}): { success: boolean; message: string } {
|
|
79
|
+
const filePath = resolve(process.cwd(), args.file_path);
|
|
80
|
+
|
|
81
|
+
if (!existsSync(filePath)) {
|
|
82
|
+
return { success: false, message: `File not found: ${args.file_path}` };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const originalContent = readFileSync(filePath, 'utf-8');
|
|
86
|
+
|
|
87
|
+
if (!originalContent.includes(args.old_string)) {
|
|
88
|
+
return { success: false, message: 'old_string not found in file' };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const newContent = originalContent.replace(args.old_string, args.new_string);
|
|
92
|
+
|
|
93
|
+
writeFileSync(filePath, newContent, 'utf-8');
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
success: true,
|
|
97
|
+
message: `Successfully edited ${args.file_path}`
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Generate a unified diff preview
|
|
103
|
+
*/
|
|
104
|
+
private generateDiff(content: string, oldString: string, newString: string, filePath: string): string {
|
|
105
|
+
const lines = content.split('\n');
|
|
106
|
+
const oldLines = oldString.split('\n');
|
|
107
|
+
const newLines = newString.split('\n');
|
|
108
|
+
|
|
109
|
+
// Find the line number where old_string starts
|
|
110
|
+
let startLine = -1;
|
|
111
|
+
for (let i = 0; i <= lines.length - oldLines.length; i++) {
|
|
112
|
+
let match = true;
|
|
113
|
+
for (let j = 0; j < oldLines.length; j++) {
|
|
114
|
+
if (lines[i + j] !== oldLines[j]) {
|
|
115
|
+
match = false;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (match) {
|
|
120
|
+
startLine = i;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (startLine === -1) {
|
|
126
|
+
return 'Error: Could not locate old_string in file';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Build unified diff
|
|
130
|
+
let diff = `\n${'─'.repeat(60)}\n`;
|
|
131
|
+
diff += `📝 Edit Preview: ${filePath}\n`;
|
|
132
|
+
diff += `${'─'.repeat(60)}\n`;
|
|
133
|
+
diff += `Line ${startLine + 1}:\n\n`;
|
|
134
|
+
|
|
135
|
+
// Show context (2 lines before)
|
|
136
|
+
const contextStart = Math.max(0, startLine - 2);
|
|
137
|
+
if (contextStart < startLine) {
|
|
138
|
+
for (let i = contextStart; i < startLine; i++) {
|
|
139
|
+
diff += ` ${lines[i]}\n`;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Show removed lines (red)
|
|
144
|
+
for (const line of oldLines) {
|
|
145
|
+
diff += `\x1b[31m- ${line}\x1b[0m\n`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Show added lines (green)
|
|
149
|
+
for (const line of newLines) {
|
|
150
|
+
diff += `\x1b[32m+ ${line}\x1b[0m\n`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Show context (2 lines after)
|
|
154
|
+
const contextEnd = Math.min(lines.length, startLine + oldLines.length + 2);
|
|
155
|
+
if (startLine + oldLines.length < contextEnd) {
|
|
156
|
+
for (let i = startLine + oldLines.length; i < contextEnd; i++) {
|
|
157
|
+
diff += ` ${lines[i]}\n`;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
diff += `${'─'.repeat(60)}\n`;
|
|
162
|
+
|
|
163
|
+
return diff;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Escape special regex characters
|
|
168
|
+
*/
|
|
169
|
+
private escapeRegex(str: string): string {
|
|
170
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
171
|
+
}
|
|
172
|
+
}
|
package/src/tools/FileTools.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { Tool } from './Tool';
|
|
4
|
+
export { EditFileTool } from './EditFileTool';
|
|
5
|
+
export { AskQuestionTool } from './AskQuestionTool';
|
|
6
|
+
export { PlanModeTool } from './PlanModeTool';
|
|
4
7
|
|
|
5
8
|
export class WriteFileTool implements Tool {
|
|
6
9
|
name = 'write_file';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { Tool } from './Tool';
|
|
4
|
+
import { PlanModeUI } from '../ui/PlanModeUI';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* PlanModeTool - Allows AI to suggest switching to plan mode
|
|
8
|
+
* Use this when the task is complex, requires architecture design, or needs requirements gathering
|
|
9
|
+
*/
|
|
10
|
+
export class PlanModeTool implements Tool {
|
|
11
|
+
name = 'enter_plan_mode';
|
|
12
|
+
description = 'Suggest switching to plan mode for complex tasks. Call this when you need to gather requirements, design architecture, or break down a complex implementation before coding.';
|
|
13
|
+
parameters = {
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: {
|
|
16
|
+
reason: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
description: 'Explain why plan mode is recommended for this task'
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
required: ['reason']
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Execute - Ask user if they want to switch to plan mode
|
|
26
|
+
*/
|
|
27
|
+
async execute(args: { reason: string }): Promise<string> {
|
|
28
|
+
console.log('');
|
|
29
|
+
console.log(chalk.cyan('🎯 AI suggests entering PLAN MODE'));
|
|
30
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
31
|
+
console.log(chalk.dim(`Reason: ${args.reason}`));
|
|
32
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
33
|
+
|
|
34
|
+
const { confirm } = await inquirer.prompt([
|
|
35
|
+
{
|
|
36
|
+
type: 'confirm',
|
|
37
|
+
name: 'confirm',
|
|
38
|
+
message: 'Switch to plan mode?',
|
|
39
|
+
default: true
|
|
40
|
+
}
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
if (confirm) {
|
|
44
|
+
PlanModeUI.showPlanHeader();
|
|
45
|
+
return 'User approved. Switching to plan mode for requirements gathering and planning.';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return 'User declined. Continuing in current mode.';
|
|
49
|
+
}
|
|
50
|
+
}
|