moth-ai 1.0.3 β 1.0.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/README.md +47 -276
- package/dist/agent/orchestrator.js +217 -16
- package/dist/config/configManager.js +6 -1
- package/dist/types/modes.js +29 -0
- package/dist/ui/App.js +40 -13
- package/dist/ui/components/MarkdownText.js +130 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -1,319 +1,90 @@
|
|
|
1
1
|
# π¦ Moth AI
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/moth-ai)
|
|
4
4
|
[](https://opensource.org/licenses/ISC)
|
|
5
|
-
[](https://nodejs.org)
|
|
6
5
|
|
|
7
|
-
**The
|
|
6
|
+
> **The World's First Truly Open CLI Assistant for Local & Open Source Models**
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
<img width="1095" height="504" alt="Moth AI Screenshot" src="https://github.com/user-attachments/assets/23b83a6b-2b63-45af-b9ec-a6dcb0a89b2f" />
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## π¦ Installation
|
|
16
|
-
|
|
17
|
-
### Global Installation (Recommended)
|
|
18
|
-
|
|
19
|
-
Install Moth AI globally to use it anywhere on your system:
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
npm install -g @kishlay42/moth-ai
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
After installation, simply run:
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
moth
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
### Local Installation
|
|
32
|
-
|
|
33
|
-
Install in a specific project:
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
npm install @kishlay42/moth-ai
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
Run using npx:
|
|
40
|
-
|
|
41
|
-
```bash
|
|
42
|
-
npx moth
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
### Requirements
|
|
46
|
-
|
|
47
|
-
- **Node.js**: >= 18.0.0
|
|
48
|
-
- **npm**: >= 8.0.0
|
|
49
|
-
|
|
50
|
-
---
|
|
51
|
-
|
|
52
|
-
## π Quick Start
|
|
53
|
-
|
|
54
|
-
1. **Install Moth AI globally:**
|
|
55
|
-
```bash
|
|
56
|
-
npm install -g @kishlay42/moth-ai
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
2. **Add your first LLM profile:**
|
|
60
|
-
```bash
|
|
61
|
-
moth llm add
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
Choose from:
|
|
65
|
-
- **Local models** (via Ollama) - Free, private, offline
|
|
66
|
-
- **Cloud providers** (OpenAI, Anthropic, Google) - Requires API key
|
|
67
|
-
|
|
68
|
-
3. **Start chatting:**
|
|
69
|
-
```bash
|
|
70
|
-
moth
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
4. **Use the command palette:**
|
|
74
|
-
- Press `Ctrl+U` to access all commands
|
|
75
|
-
- Switch profiles, toggle autopilot, and more
|
|
8
|
+
<img width="953" height="392" alt="Screenshot 2026-01-07 013431" src="https://github.com/user-attachments/assets/55e104e2-8441-4a74-b3f7-c74340d806fa" />
|
|
76
9
|
|
|
77
10
|
---
|
|
78
11
|
|
|
79
|
-
##
|
|
80
|
-
|
|
81
|
-
### π§ LLM-Agnostic & Local-First
|
|
82
|
-
|
|
83
|
-
Use **any LLM**, local or cloud β switch instantly without changing workflows.
|
|
84
|
-
|
|
85
|
-
- **Local (via Ollama)**
|
|
86
|
-
Run models like **Llama 3**, **Mistral**, **Gemma**, and **DeepSeek-Coder** locally
|
|
87
|
-
β Zero latency, full privacy, offline-friendly
|
|
12
|
+
## Overview
|
|
88
13
|
|
|
89
|
-
- **
|
|
90
|
-
Plug in your own API keys for:
|
|
91
|
-
- OpenAI (GPT-4 / GPT-4o)
|
|
92
|
-
- Anthropic (Claude 3.5 Sonnet)
|
|
93
|
-
- Google (Gemini)
|
|
14
|
+
Moth AI is the **first terminal-native coding assistant** built from the ground up to treat **local and open-source LLMs** as first-class citizens. While others lock you into proprietary models or cloud subscripts, Moth is truly open sourceβin code, philosophy, and model support.
|
|
94
15
|
|
|
95
|
-
|
|
16
|
+
It empowers you to **write, debug, refactor, and reason about code** using **your own models** on **your own hardware**. Whether you're running Llama 3 on a MacBook or GPT-4 in the cloud, Moth gives you the same powerful agentic capabilities without the compromise.
|
|
96
17
|
|
|
97
18
|
---
|
|
98
19
|
|
|
99
|
-
|
|
20
|
+
## π Key Features
|
|
100
21
|
|
|
101
|
-
|
|
22
|
+
### π Truly Open & Local-First
|
|
23
|
+
Moth is the only CLI tool designed to democratize AI access.
|
|
24
|
+
- **Local Native:** Optimized deeply for Ollama. Run **Llama 3, Mistral, Gemma, or DeepSeek** locally with zero latency, 100% privacy, and no internet connection required.
|
|
25
|
+
- **Open Source First:** We support any OpenAI-compatible endpoint, making it universally compatible with the open ecosystem of model servers (LM Studio, LocalAI, etc.).
|
|
26
|
+
- **Cloud Optional:** Seamlessly integrate OpenAI (GPT-4), Anthropic (Claude), or Google (Gemini) when you need extra horsepowerβbut only when *you* choose to.
|
|
102
27
|
|
|
103
|
-
- **Task Planning** β Break complex goals into executable steps
|
|
104
|
-
- **File Editing** β Precise diffs, patches, and refactors
|
|
105
|
-
- **Terminal Control** β Run builds, tests, and Git commands from chat
|
|
106
|
-
- **Context-Aware** β Understands your project structure and codebase
|
|
107
28
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
### π‘οΈ Permission-First by Design
|
|
29
|
+
### π€ Agentic Capabilities with Role-Based Modes
|
|
30
|
+
Moth operates in three distinct modes to match your current task intensity:
|
|
111
31
|
|
|
112
|
-
|
|
32
|
+
1. **π΅ Default Mode:** A balanced assistant that asks for permission before executing sensitive actions.
|
|
33
|
+
2. **οΏ½ Plan Mode:** Prioritizes detailed architectural planning. Moth creates comprehensive markdown plans for review before writing a single line of code.
|
|
34
|
+
3. **π Autopilot Mode:** For trusted workflows. Moth executes authorized tool calls automatically, streamlining repetitive tasks.
|
|
113
35
|
|
|
114
|
-
|
|
115
|
-
- Granular permissions per action
|
|
116
|
-
- **Autopilot mode** for trusted workflows
|
|
117
|
-
- Feedback loop to guide the agent instead of blind execution
|
|
118
|
-
|
|
119
|
-
---
|
|
36
|
+
<img width="948" height="112" alt="Screenshot 2026-01-07 003822" src="https://github.com/user-attachments/assets/ff14a463-157d-4107-8de1-519257355989" />
|
|
120
37
|
|
|
121
|
-
|
|
38
|
+
<img width="940" height="108" alt="Screenshot 2026-01-07 003744" src="https://github.com/user-attachments/assets/345509dd-a89a-4b78-83df-9c21c51f4e82" />
|
|
122
39
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
- **
|
|
126
|
-
- **Architecture Profile** β Reasoning-focused for system design
|
|
127
|
-
- **Fast Profile** β Lightweight local model for quick answers
|
|
40
|
+
### π‘οΈ Secure & Transparent
|
|
41
|
+
- **Permission-First Architecture:** You approve every significant file edit or shell command.
|
|
42
|
+
- **Context-Aware:** Moth intelligently scans your project structure to provide relevant answers, not hallucinations.
|
|
128
43
|
|
|
129
44
|
---
|
|
130
45
|
|
|
131
|
-
##
|
|
132
|
-
|
|
133
|
-
### Main Commands
|
|
134
|
-
|
|
135
|
-
```bash
|
|
136
|
-
# Start interactive chat
|
|
137
|
-
moth
|
|
138
|
-
|
|
139
|
-
# Show help
|
|
140
|
-
moth --help
|
|
141
|
-
|
|
142
|
-
# Display version
|
|
143
|
-
moth --version
|
|
144
|
-
```
|
|
46
|
+
## π¦ Installation
|
|
145
47
|
|
|
146
|
-
###
|
|
48
|
+
### Global Installation (Recommended)
|
|
49
|
+
Install Moth AI globally to access the `moth` command from any directory.
|
|
147
50
|
|
|
148
51
|
```bash
|
|
149
|
-
|
|
150
|
-
moth llm add
|
|
151
|
-
|
|
152
|
-
# List all configured profiles
|
|
153
|
-
moth llm list
|
|
154
|
-
|
|
155
|
-
# Switch to a different profile
|
|
156
|
-
moth llm use
|
|
157
|
-
|
|
158
|
-
# Remove a profile
|
|
159
|
-
moth llm remove
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
### Keyboard Shortcuts
|
|
163
|
-
|
|
164
|
-
- **Ctrl+U** - Open command palette
|
|
165
|
-
- **Ctrl+C** - Exit chat
|
|
166
|
-
- **Arrow Keys** - Navigate command palette
|
|
167
|
-
- **Enter** - Execute selected command
|
|
168
|
-
|
|
169
|
-
---
|
|
170
|
-
|
|
171
|
-
## βοΈ Configuration
|
|
172
|
-
|
|
173
|
-
Moth AI stores configuration in `~/.moth/config.yaml`
|
|
174
|
-
|
|
175
|
-
### Example Configuration
|
|
176
|
-
|
|
177
|
-
```yaml
|
|
178
|
-
profiles:
|
|
179
|
-
- name: "gpt-4"
|
|
180
|
-
provider: "openai"
|
|
181
|
-
model: "gpt-4"
|
|
182
|
-
apiKey: "sk-..."
|
|
183
|
-
|
|
184
|
-
- name: "local-llama"
|
|
185
|
-
provider: "ollama"
|
|
186
|
-
model: "llama3"
|
|
187
|
-
baseUrl: "http://localhost:11434"
|
|
188
|
-
|
|
189
|
-
activeProfile: "gpt-4"
|
|
52
|
+
npm install -g moth-ai
|
|
190
53
|
```
|
|
191
54
|
|
|
192
|
-
###
|
|
193
|
-
|
|
194
|
-
1. Install Ollama: https://ollama.ai
|
|
195
|
-
2. Pull a model:
|
|
196
|
-
```bash
|
|
197
|
-
ollama pull llama3
|
|
198
|
-
```
|
|
199
|
-
3. Add to Moth:
|
|
200
|
-
```bash
|
|
201
|
-
moth llm add
|
|
202
|
-
# Select "Ollama" and choose your model
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
### Setting Up Cloud Providers
|
|
206
|
-
|
|
207
|
-
#### OpenAI
|
|
208
|
-
```bash
|
|
209
|
-
moth llm add
|
|
210
|
-
# Select "OpenAI"
|
|
211
|
-
# Enter your API key from https://platform.openai.com/api-keys
|
|
212
|
-
```
|
|
55
|
+
### Local Installation
|
|
56
|
+
For project-specific constraints.
|
|
213
57
|
|
|
214
|
-
#### Anthropic (Claude)
|
|
215
58
|
```bash
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
# Enter your API key from https://console.anthropic.com/
|
|
59
|
+
npm install moth-ai
|
|
60
|
+
npx moth-ai
|
|
219
61
|
```
|
|
220
62
|
|
|
221
|
-
|
|
222
|
-
```bash
|
|
223
|
-
moth llm add
|
|
224
|
-
# Select "Google"
|
|
225
|
-
# Enter your API key from https://makersuite.google.com/app/apikey
|
|
226
|
-
```
|
|
63
|
+
**Requirements:** Node.js >= 18.0.0
|
|
227
64
|
|
|
228
65
|
---
|
|
229
66
|
|
|
230
|
-
##
|
|
67
|
+
## β‘ Quick Start
|
|
231
68
|
|
|
232
|
-
|
|
69
|
+
1. **Initialize Moth:**
|
|
70
|
+
```bash
|
|
71
|
+
moth
|
|
72
|
+
```
|
|
233
73
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
74
|
+
2. **Configure Your First Profile:**
|
|
75
|
+
Run the interactive setup wizard to connect your preferred model.
|
|
76
|
+
```bash
|
|
77
|
+
moth llm add
|
|
78
|
+
```
|
|
79
|
+
<img width="611" height="379" alt="image" src="https://github.com/user-attachments/assets/a90c54ee-54f7-4391-a29f-f2bdf40d5709" />
|
|
238
80
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
> Refactor src/utils.ts to use async/await instead of promises
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
### Debugging
|
|
247
|
-
|
|
248
|
-
```bash
|
|
249
|
-
moth
|
|
250
|
-
> Why is my React component re-rendering infinitely?
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
### Project Analysis
|
|
254
|
-
|
|
255
|
-
```bash
|
|
256
|
-
moth
|
|
257
|
-
> Analyze the architecture of this project and suggest improvements
|
|
258
|
-
```
|
|
81
|
+
3. **Execute Commands:**
|
|
82
|
+
- **Chat:** Simply type your query.
|
|
83
|
+
- **Command Palette:** Press `Ctrl+U` to manage profiles and settings.
|
|
84
|
+
- **Switch Modes:** Press `Ctrl+B` to cycle operational modes.
|
|
259
85
|
|
|
260
86
|
---
|
|
261
|
-
|
|
262
|
-
## π§ Troubleshooting
|
|
263
|
-
|
|
264
|
-
### Command not found: moth
|
|
265
|
-
|
|
266
|
-
Make sure the global npm bin directory is in your PATH:
|
|
267
|
-
|
|
268
|
-
```bash
|
|
269
|
-
npm config get prefix
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
Add the bin directory to your PATH in `~/.bashrc` or `~/.zshrc`:
|
|
273
|
-
|
|
274
|
-
```bash
|
|
275
|
-
export PATH="$PATH:$(npm config get prefix)/bin"
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
### Ollama connection error
|
|
279
|
-
|
|
280
|
-
Ensure Ollama is running:
|
|
281
|
-
|
|
282
|
-
```bash
|
|
283
|
-
ollama serve
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
### API key errors
|
|
287
|
-
|
|
288
|
-
Verify your API key is correctly configured:
|
|
289
|
-
|
|
290
|
-
```bash
|
|
291
|
-
moth llm list
|
|
292
|
-
# Check if your profile shows the correct provider
|
|
293
87
|
```
|
|
294
88
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
## π License
|
|
298
|
-
|
|
299
|
-
ISC License - see [LICENSE](LICENSE) file for details
|
|
300
|
-
|
|
301
|
-
---
|
|
302
|
-
|
|
303
|
-
## π€ Contributing
|
|
304
|
-
|
|
305
|
-
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
306
|
-
|
|
307
|
-
Repository: https://github.com/kishlay42/Moth-ai
|
|
308
|
-
|
|
309
|
-
---
|
|
310
|
-
|
|
311
|
-
## π Links
|
|
312
|
-
|
|
313
|
-
- **npm Package**: https://www.npmjs.com/package/@kishlay42/moth-ai
|
|
314
|
-
- **GitHub**: https://github.com/kishlay42/Moth-ai
|
|
315
|
-
- **Issues**: https://github.com/kishlay42/Moth-ai/issues
|
|
316
|
-
|
|
317
|
-
---
|
|
89
|
+
*Moth AI β Coding at the speed of thought.*
|
|
318
90
|
|
|
319
|
-
**Made with β€οΈ for developers who code in the terminal**
|
|
@@ -32,6 +32,8 @@ export class AgentOrchestrator {
|
|
|
32
32
|
// In reality, we need to handle the LLM raw output, parse "Thought" and "Tool Call"
|
|
33
33
|
// For now, we will rely on a Structured Output or strict parsing if the Provider supports it.
|
|
34
34
|
// Since we are using Gemini, we can ask for JSON mode or specific formatting.
|
|
35
|
+
// Show waiting status
|
|
36
|
+
yield { thought: "Waiting for LLM response..." };
|
|
35
37
|
const responseText = await this.callLLM(messages);
|
|
36
38
|
// Parse response
|
|
37
39
|
let step;
|
|
@@ -39,7 +41,7 @@ export class AgentOrchestrator {
|
|
|
39
41
|
step = this.parseResponse(responseText);
|
|
40
42
|
}
|
|
41
43
|
catch (e) {
|
|
42
|
-
yield { thought: "
|
|
44
|
+
yield { thought: "LLM response received, processing...", toolOutput: `Parsing error: ${e}` };
|
|
43
45
|
continue;
|
|
44
46
|
}
|
|
45
47
|
this.state.history.push(step);
|
|
@@ -59,33 +61,209 @@ export class AgentOrchestrator {
|
|
|
59
61
|
}
|
|
60
62
|
async buildSystemPrompt(attachedFiles = []) {
|
|
61
63
|
const toolDefs = this.tools.getDefinitions().map(t => `${t.name}: ${t.description} Params: ${JSON.stringify(t.parameters)}`).join('\n');
|
|
64
|
+
// Automatically scan project context
|
|
65
|
+
let projectContext = '';
|
|
66
|
+
try {
|
|
67
|
+
const contextResult = await this.tools.execute('scan_context', {});
|
|
68
|
+
const files = contextResult.split('\n').filter((f) => f.trim());
|
|
69
|
+
// Extract key information
|
|
70
|
+
const fileCount = files.length - 1; // Subtract header line
|
|
71
|
+
const extensions = new Set(files.map((f) => {
|
|
72
|
+
const match = f.match(/\.(\w+)$/);
|
|
73
|
+
return match ? match[1] : null;
|
|
74
|
+
}).filter(Boolean));
|
|
75
|
+
// Identify key files
|
|
76
|
+
const keyFiles = files.filter((f) => f.includes('package.json') ||
|
|
77
|
+
f.includes('README') ||
|
|
78
|
+
f.includes('tsconfig') ||
|
|
79
|
+
f.includes('.config')).slice(0, 10);
|
|
80
|
+
projectContext = `
|
|
81
|
+
=== PROJECT CONTEXT ===
|
|
82
|
+
|
|
83
|
+
**Current Directory:** ${this.root}
|
|
84
|
+
**Total Files:** ${fileCount}
|
|
85
|
+
**Languages/Types:** ${Array.from(extensions).join(', ')}
|
|
86
|
+
|
|
87
|
+
**Key Files:**
|
|
88
|
+
${keyFiles.slice(1, 11).join('\n')}
|
|
89
|
+
|
|
90
|
+
**Note:** You have access to all project files. Use read_file, list_dir, or search_files to explore further.
|
|
91
|
+
`;
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
// If scanning fails, continue without context
|
|
95
|
+
projectContext = `\n=== PROJECT CONTEXT ===\n**Current Directory:** ${this.root}\n`;
|
|
96
|
+
}
|
|
62
97
|
let fileContext = '';
|
|
63
98
|
if (attachedFiles.length > 0) {
|
|
64
99
|
fileContext = await this.buildFileContext(attachedFiles);
|
|
65
100
|
}
|
|
66
|
-
return `You are Moth, an intelligent CLI coding assistant.
|
|
67
|
-
|
|
101
|
+
return `You are Moth, an intelligent CLI coding assistant with full access to the project filesystem and tools.
|
|
102
|
+
|
|
103
|
+
=== YOUR ROLE ===
|
|
104
|
+
You are the DRIVER of this mecha robot (the coding assistant). You have complete control over all tools and actions.
|
|
105
|
+
The user trusts you to make smart decisions about which tools to use and when.
|
|
106
|
+
|
|
107
|
+
=== AVAILABLE TOOLS ===
|
|
68
108
|
${toolDefs}
|
|
69
109
|
|
|
110
|
+
${projectContext}
|
|
111
|
+
|
|
70
112
|
${fileContext}
|
|
71
113
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
114
|
+
=== TOOL SELECTION GUIDELINES ===
|
|
115
|
+
|
|
116
|
+
**For File Operations:**
|
|
117
|
+
|
|
118
|
+
1. **create_file** - Use when:
|
|
119
|
+
- Creating a brand new file that doesn't exist
|
|
120
|
+
- User explicitly says "create a new file"
|
|
121
|
+
- Starting a new component, module, or script
|
|
122
|
+
- Will FAIL if file already exists (use write_file instead)
|
|
123
|
+
|
|
124
|
+
2. **write_file** - Use when:
|
|
125
|
+
- Completely replacing/overwriting an existing file
|
|
126
|
+
- User asks to "rewrite", "replace", or "change the entire file"
|
|
127
|
+
- Making major changes where it's easier to rewrite than patch
|
|
128
|
+
- The file is small (< 100 lines) and changes are extensive
|
|
129
|
+
- PREFERRED for simple edits - it's more reliable than edit_file
|
|
130
|
+
|
|
131
|
+
3. **edit_file** - Use when:
|
|
132
|
+
- Making targeted changes to large files (> 100 lines)
|
|
133
|
+
- Only modifying specific sections while preserving the rest
|
|
134
|
+
- User asks to "modify", "update", or "change a specific part"
|
|
135
|
+
- You need to generate a Unified Diff (git diff format)
|
|
136
|
+
- REQUIRES: Exact context matching - if unsure, use write_file instead
|
|
137
|
+
|
|
138
|
+
4. **read_file** - ALWAYS use before editing:
|
|
139
|
+
- Read the file first to understand its current state
|
|
140
|
+
- Verify the file exists before trying to edit it
|
|
141
|
+
- Get the exact content for generating accurate diffs
|
|
142
|
+
|
|
143
|
+
**For Code Questions & Explanations:**
|
|
144
|
+
|
|
145
|
+
- If user asks "show me code" or "how do I..." β Use finalAnswer (don't create files)
|
|
146
|
+
- If user asks "create/write/save a file" β Use appropriate file tool
|
|
147
|
+
- If user asks "edit/modify @filename" β Read file first, then use write_file or edit_file
|
|
148
|
+
|
|
149
|
+
**For Project Understanding:**
|
|
150
|
+
|
|
151
|
+
- Use scan_context to understand project structure
|
|
152
|
+
- Use search_files to find specific files by name
|
|
153
|
+
- Use search_text to find code patterns or text
|
|
154
|
+
- Use read_file to examine specific files
|
|
155
|
+
|
|
156
|
+
**For Commands:**
|
|
157
|
+
|
|
158
|
+
- Use run_command for: npm install, git commands, running tests, etc.
|
|
159
|
+
- Always show the user what command you're running
|
|
160
|
+
- Explain the output in simple terms
|
|
161
|
+
|
|
162
|
+
=== DECISION MAKING PROCESS ===
|
|
163
|
+
|
|
164
|
+
When user asks to edit a file:
|
|
165
|
+
1. First, use read_file to see current content
|
|
166
|
+
2. Decide: Is this a small change or complete rewrite?
|
|
167
|
+
- Small file or major changes? β write_file (easier, more reliable)
|
|
168
|
+
- Large file with targeted changes? β edit_file (preserves rest of file)
|
|
169
|
+
3. Execute the chosen tool
|
|
170
|
+
4. Confirm what you did in finalAnswer
|
|
171
|
+
|
|
172
|
+
When user asks a question:
|
|
173
|
+
1. Is this asking for information or asking to DO something?
|
|
174
|
+
- Information β Use finalAnswer with explanation
|
|
175
|
+
- Action β Use appropriate tool
|
|
176
|
+
2. If unsure, ask the user to clarify
|
|
177
|
+
|
|
178
|
+
=== BEST PRACTICES ===
|
|
179
|
+
|
|
180
|
+
β
DO:
|
|
181
|
+
- Read files before editing them
|
|
182
|
+
- Use write_file for most edits (it's simpler and more reliable)
|
|
183
|
+
- Explain what you're doing in your "thought"
|
|
184
|
+
- Give clear, helpful finalAnswers
|
|
185
|
+
- Use tools proactively when it makes sense
|
|
186
|
+
|
|
187
|
+
β DON'T:
|
|
188
|
+
- Create files for code examples unless explicitly asked
|
|
189
|
+
- Use edit_file without reading the file first
|
|
190
|
+
- Generate invalid diffs (if unsure, use write_file)
|
|
191
|
+
- Make assumptions - ask if unclear
|
|
192
|
+
|
|
193
|
+
=== RESPONSE FORMAT ===
|
|
78
194
|
|
|
79
195
|
Format your response exactly as a JSON object:
|
|
196
|
+
|
|
197
|
+
For using a tool:
|
|
80
198
|
{
|
|
81
|
-
"thought": "
|
|
199
|
+
"thought": "I need to [action] because [reason]. I'll use [tool] to do this.",
|
|
82
200
|
"toolCall": { "name": "tool_name", "arguments": { ... } }
|
|
83
201
|
}
|
|
84
|
-
|
|
202
|
+
|
|
203
|
+
For final answer:
|
|
85
204
|
{
|
|
86
|
-
"thought": "
|
|
87
|
-
"finalAnswer": "
|
|
205
|
+
"thought": "I've completed the task. Here's what I did: [summary]",
|
|
206
|
+
"finalAnswer": "Your detailed response with code/explanation/results"
|
|
88
207
|
}
|
|
208
|
+
|
|
209
|
+
=== FINAL ANSWER GUIDELINES ===
|
|
210
|
+
|
|
211
|
+
When you complete a task, NEVER just say "Done". Always provide a comprehensive response that includes:
|
|
212
|
+
|
|
213
|
+
1. **Summary of what was accomplished:**
|
|
214
|
+
- "I've created/modified/deleted [X] file(s)"
|
|
215
|
+
- List each file with its path
|
|
216
|
+
|
|
217
|
+
2. **File references:**
|
|
218
|
+
- Use relative paths from project root
|
|
219
|
+
- Example: "Created \`src/components/Button.tsx\`"
|
|
220
|
+
- Example: "Modified \`package.json\` to add dependencies"
|
|
221
|
+
|
|
222
|
+
3. **What changed:**
|
|
223
|
+
- Briefly explain what was added/modified/removed
|
|
224
|
+
- Example: "Added a new Button component with TypeScript support"
|
|
225
|
+
- Example: "Updated the API endpoint to use the new authentication flow"
|
|
226
|
+
|
|
227
|
+
4. **Next steps or suggestions:**
|
|
228
|
+
- What the user might want to do next
|
|
229
|
+
- Example: "You can now import this component with: \`import { Button } from './components/Button'\`"
|
|
230
|
+
- Example: "Next, you might want to: run \`npm install\` to install the new dependencies"
|
|
231
|
+
|
|
232
|
+
5. **Testing/verification suggestions:**
|
|
233
|
+
- How to verify the changes work
|
|
234
|
+
- Example: "Run \`npm test\` to verify the tests pass"
|
|
235
|
+
- Example: "Try running the app with \`npm start\`"
|
|
236
|
+
|
|
237
|
+
**Example of a GOOD final answer:**
|
|
238
|
+
"I've created a new Button component for you!
|
|
239
|
+
|
|
240
|
+
**Files created:**
|
|
241
|
+
- \`src/components/Button.tsx\` - Main Button component with TypeScript
|
|
242
|
+
- \`src/components/Button.css\` - Styling for the button
|
|
243
|
+
|
|
244
|
+
**What it includes:**
|
|
245
|
+
- Customizable size (small, medium, large)
|
|
246
|
+
- Multiple variants (primary, secondary, danger)
|
|
247
|
+
- TypeScript props for type safety
|
|
248
|
+
- Accessible with proper ARIA labels
|
|
249
|
+
|
|
250
|
+
**Next steps:**
|
|
251
|
+
1. Import it in your app: \`import { Button } from './components/Button'\`
|
|
252
|
+
2. Use it: \`<Button variant="primary" size="medium">Click me</Button>\`
|
|
253
|
+
3. Customize the colors in \`Button.css\` to match your theme
|
|
254
|
+
|
|
255
|
+
**To test:**
|
|
256
|
+
Run \`npm start\` and check the component renders correctly!"
|
|
257
|
+
|
|
258
|
+
**Example of a BAD final answer:**
|
|
259
|
+
"Done"
|
|
260
|
+
"File created"
|
|
261
|
+
"Task completed"
|
|
262
|
+
|
|
263
|
+
=== REMEMBER ===
|
|
264
|
+
You are the intelligent driver with full control. Make smart decisions about tools.
|
|
265
|
+
The user trusts you to choose the right approach. When in doubt, use the simpler tool (write_file over edit_file).
|
|
266
|
+
ALWAYS provide detailed, helpful final answers with file references and next steps!
|
|
89
267
|
`;
|
|
90
268
|
}
|
|
91
269
|
collectAttachedFiles(history) {
|
|
@@ -131,9 +309,32 @@ OR if you are done/replying:
|
|
|
131
309
|
return fullText;
|
|
132
310
|
}
|
|
133
311
|
parseResponse(text) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
312
|
+
try {
|
|
313
|
+
// 1. Try to find JSON in code blocks
|
|
314
|
+
const jsonMatch = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
315
|
+
if (jsonMatch) {
|
|
316
|
+
const jsonContent = jsonMatch[1].trim();
|
|
317
|
+
return JSON.parse(jsonContent);
|
|
318
|
+
}
|
|
319
|
+
// 2. Try to find the first outer-most JSON object
|
|
320
|
+
// This regex matches from the first '{' to the last '}'
|
|
321
|
+
const objectMatch = text.match(/\{[\s\S]*\}/);
|
|
322
|
+
if (objectMatch) {
|
|
323
|
+
return JSON.parse(objectMatch[0]);
|
|
324
|
+
}
|
|
325
|
+
// 3. Fallback: Try parsing the whole text (maybe it's raw JSON)
|
|
326
|
+
return JSON.parse(text);
|
|
327
|
+
}
|
|
328
|
+
catch (e) {
|
|
329
|
+
// If all parsing fails, but the text contains "Done" or "Completed", return it as a final answer
|
|
330
|
+
if (text.toLowerCase().includes('done') || text.toLowerCase().includes('completed')) {
|
|
331
|
+
return {
|
|
332
|
+
thought: "LLM responded with plain text indicating completion.",
|
|
333
|
+
finalAnswer: text
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
throw new Error(`Could not parse JSON from response. Raw text: ${text.slice(0, 100)}...`);
|
|
337
|
+
}
|
|
137
338
|
}
|
|
138
339
|
async executeTool(name, args) {
|
|
139
340
|
return await this.tools.execute(name, args);
|
|
@@ -19,7 +19,8 @@ export function loadConfig() {
|
|
|
19
19
|
return {
|
|
20
20
|
profiles: parsed?.profiles || [],
|
|
21
21
|
activeProfile: parsed?.activeProfile,
|
|
22
|
-
username: parsed?.username
|
|
22
|
+
username: parsed?.username,
|
|
23
|
+
mode: parsed?.mode || 'default' // Default to 'default' mode
|
|
23
24
|
};
|
|
24
25
|
}
|
|
25
26
|
catch (e) {
|
|
@@ -60,3 +61,7 @@ export function setUsername(config, username) {
|
|
|
60
61
|
config.username = username;
|
|
61
62
|
return config;
|
|
62
63
|
}
|
|
64
|
+
export function setMode(config, mode) {
|
|
65
|
+
config.mode = mode;
|
|
66
|
+
return config;
|
|
67
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const MODES = {
|
|
2
|
+
default: {
|
|
3
|
+
name: 'default',
|
|
4
|
+
displayName: 'Default',
|
|
5
|
+
description: 'Balanced approach - asks for permission',
|
|
6
|
+
icon: 'π΅',
|
|
7
|
+
color: 'blue',
|
|
8
|
+
autoApprove: false,
|
|
9
|
+
emphasizePlanning: false
|
|
10
|
+
},
|
|
11
|
+
plan: {
|
|
12
|
+
name: 'plan',
|
|
13
|
+
displayName: 'Plan',
|
|
14
|
+
description: 'Planning-first - creates detailed plans before execution',
|
|
15
|
+
icon: 'π',
|
|
16
|
+
color: 'magenta',
|
|
17
|
+
autoApprove: false,
|
|
18
|
+
emphasizePlanning: true
|
|
19
|
+
},
|
|
20
|
+
autopilot: {
|
|
21
|
+
name: 'autopilot',
|
|
22
|
+
displayName: 'Autopilot',
|
|
23
|
+
description: 'Auto-execute - no permission prompts',
|
|
24
|
+
icon: 'π',
|
|
25
|
+
color: 'yellow',
|
|
26
|
+
autoApprove: true,
|
|
27
|
+
emphasizePlanning: false
|
|
28
|
+
}
|
|
29
|
+
};
|
package/dist/ui/App.js
CHANGED
|
@@ -2,7 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useState, useEffect } from 'react';
|
|
3
3
|
import { Box, Text, useInput, Newline } from 'ink';
|
|
4
4
|
import * as os from 'os';
|
|
5
|
-
import { loadConfig, saveConfig, addProfile } from '../config/configManager.js';
|
|
5
|
+
import { loadConfig, saveConfig, addProfile, setMode as saveMode } from '../config/configManager.js';
|
|
6
|
+
import { MODES } from '../types/modes.js';
|
|
6
7
|
import { TodoManager } from '../planning/todoManager.js';
|
|
7
8
|
import { AgentOrchestrator } from '../agent/orchestrator.js';
|
|
8
9
|
import { createToolRegistry } from '../tools/factory.js';
|
|
@@ -13,6 +14,7 @@ import { WordMoth } from './components/WordMoth.js';
|
|
|
13
14
|
import { FileChip } from './components/FileChip.js';
|
|
14
15
|
import { FileAutocomplete } from './components/FileAutocomplete.js';
|
|
15
16
|
import { CommandPalette } from './components/CommandPalette.js';
|
|
17
|
+
import { MarkdownText } from './components/MarkdownText.js';
|
|
16
18
|
import { ProfileManager } from './ProfileManager.js';
|
|
17
19
|
import { LLMWizard } from './wizards/LLMWizard.js';
|
|
18
20
|
import { LLMRemover } from './wizards/LLMRemover.js';
|
|
@@ -23,9 +25,13 @@ export const App = ({ command, args, todoManager: propTodoManager, username }) =
|
|
|
23
25
|
// UX State
|
|
24
26
|
const [showWelcome, setShowWelcome] = useState(true);
|
|
25
27
|
const [isPaused, setIsPaused] = useState(false);
|
|
26
|
-
const [autopilot, setAutopilot] = useState(false);
|
|
27
28
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
28
29
|
const [thinkingText, setThinkingText] = useState('Sauting...');
|
|
30
|
+
// Mode State
|
|
31
|
+
const [mode, setMode] = useState(() => {
|
|
32
|
+
const config = loadConfig();
|
|
33
|
+
return config.mode || 'default';
|
|
34
|
+
});
|
|
29
35
|
// File Reference State
|
|
30
36
|
const [selectedFiles, setSelectedFiles] = useState([]);
|
|
31
37
|
const [showAutocomplete, setShowAutocomplete] = useState(false);
|
|
@@ -94,7 +100,8 @@ export const App = ({ command, args, todoManager: propTodoManager, username }) =
|
|
|
94
100
|
const root = findProjectRoot() || process.cwd();
|
|
95
101
|
// Permission Callback
|
|
96
102
|
const checkPermission = async (toolName, args) => {
|
|
97
|
-
if
|
|
103
|
+
// Auto-approve if in autopilot mode
|
|
104
|
+
if (mode === 'autopilot') {
|
|
98
105
|
return { allowed: true };
|
|
99
106
|
}
|
|
100
107
|
return new Promise((resolve) => {
|
|
@@ -212,11 +219,11 @@ export const App = ({ command, args, todoManager: propTodoManager, username }) =
|
|
|
212
219
|
setShowCommandPalette(false);
|
|
213
220
|
switch (action) {
|
|
214
221
|
case 'autopilot':
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
222
|
+
// Set mode to autopilot (same as Ctrl+B cycling to autopilot)
|
|
223
|
+
setMode('autopilot');
|
|
224
|
+
const config = loadConfig();
|
|
225
|
+
const updatedConfig = saveMode(config, 'autopilot');
|
|
226
|
+
saveConfig(updatedConfig);
|
|
220
227
|
break;
|
|
221
228
|
case 'llm-list':
|
|
222
229
|
setActiveWizard('llm-list');
|
|
@@ -270,6 +277,19 @@ export const App = ({ command, args, todoManager: propTodoManager, username }) =
|
|
|
270
277
|
setShowCommandPalette(prev => !prev);
|
|
271
278
|
return;
|
|
272
279
|
}
|
|
280
|
+
// Ctrl+B to cycle modes
|
|
281
|
+
if (input === 'b' && key.ctrl) {
|
|
282
|
+
const modes = ['default', 'plan', 'autopilot'];
|
|
283
|
+
const currentIndex = modes.indexOf(mode);
|
|
284
|
+
const nextMode = modes[(currentIndex + 1) % modes.length];
|
|
285
|
+
setMode(nextMode);
|
|
286
|
+
// Persist mode to config
|
|
287
|
+
const config = loadConfig();
|
|
288
|
+
const updatedConfig = saveMode(config, nextMode);
|
|
289
|
+
saveConfig(updatedConfig);
|
|
290
|
+
// Mode indicator updates automatically, no need for notification
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
273
293
|
// ESC Pause
|
|
274
294
|
if (key.escape && !showAutocomplete) {
|
|
275
295
|
setIsPaused(prev => !prev);
|
|
@@ -306,7 +326,11 @@ export const App = ({ command, args, todoManager: propTodoManager, username }) =
|
|
|
306
326
|
pendingPermission.resolve({ allowed: true });
|
|
307
327
|
}
|
|
308
328
|
else if (permissionSelection === 1) {
|
|
309
|
-
|
|
329
|
+
// Enable autopilot mode
|
|
330
|
+
setMode('autopilot');
|
|
331
|
+
const config = loadConfig();
|
|
332
|
+
const updatedConfig = saveMode(config, 'autopilot');
|
|
333
|
+
saveConfig(updatedConfig);
|
|
310
334
|
pendingPermission.resolve({ allowed: true });
|
|
311
335
|
}
|
|
312
336
|
else if (permissionSelection === 2) {
|
|
@@ -319,8 +343,11 @@ export const App = ({ command, args, todoManager: propTodoManager, username }) =
|
|
|
319
343
|
pendingPermission.resolve({ allowed: true });
|
|
320
344
|
}
|
|
321
345
|
else if (input === 'b' || input === 'B') {
|
|
322
|
-
// Yes - autopilot
|
|
323
|
-
|
|
346
|
+
// Yes - enable autopilot mode
|
|
347
|
+
setMode('autopilot');
|
|
348
|
+
const config = loadConfig();
|
|
349
|
+
const updatedConfig = saveMode(config, 'autopilot');
|
|
350
|
+
saveConfig(updatedConfig);
|
|
324
351
|
pendingPermission.resolve({ allowed: true });
|
|
325
352
|
}
|
|
326
353
|
else if (input === 'c' || input === 'C') {
|
|
@@ -370,7 +397,7 @@ export const App = ({ command, args, todoManager: propTodoManager, username }) =
|
|
|
370
397
|
}]);
|
|
371
398
|
} }));
|
|
372
399
|
}
|
|
373
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [command === 'run' && (_jsxs(Box, { flexDirection: "row", paddingX: 1, paddingY: 0, borderStyle: "round", borderColor: "#0192e5", children: [_jsxs(Box, { flexDirection: "column", marginTop: -1, paddingRight: 1, children: [_jsx(WordMoth, { text: "MOTH", big: true }), _jsx(Box, { marginTop: -1, children: _jsx(Text, { dimColor: true, children: "v1.0.
|
|
400
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [command === 'run' && (_jsxs(Box, { flexDirection: "row", paddingX: 1, paddingY: 0, borderStyle: "round", borderColor: "#0192e5", children: [_jsxs(Box, { flexDirection: "column", marginTop: -1, paddingRight: 1, children: [_jsx(WordMoth, { text: "MOTH", big: true }), _jsx(Box, { marginTop: -1, children: _jsx(Text, { dimColor: true, children: "v1.0.4" }) }), _jsxs(Text, { color: "#3EA0C3", children: ["Welcome, ", username || os.userInfo().username] })] }), _jsxs(Box, { flexDirection: "column", alignItems: "flex-start", marginLeft: 2, children: [_jsxs(Text, { color: "green", children: ["Active_AI: ", activeProfile?.name || 'None'] }), _jsxs(Text, { dimColor: true, children: ["Path: ", process.cwd()] }), _jsx(Text, { dimColor: true, children: "Use Ctrl+U to view commands" })] })] })), messages.length > 0 && (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: messages.map((m, i) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: m.role === 'user' ? 'blue' : 'green', bold: true, children: [m.role === 'user' ? 'You' : 'Moth', ":"] }), m.role === 'assistant' ? (_jsx(MarkdownText, { content: m.content })) : (_jsxs(Text, { children: [" ", m.content] }))] }, i))) })), pendingPermission && (_jsxs(Box, { borderStyle: "double", borderColor: "red", flexDirection: "column", padding: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, color: "red", children: "PERMISSION REQUIRED" }), _jsxs(Text, { children: ["Tool: ", pendingPermission.toolName] }), _jsxs(Text, { children: ["Args: ", JSON.stringify(pendingPermission.args)] }), _jsx(Newline, {}), !feedbackMode ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: _jsxs(Text, { color: permissionSelection === 0 ? "green" : undefined, bold: permissionSelection === 0, children: [permissionSelection === 0 ? "> " : " ", " [a] Yes - execute this action"] }) }), _jsx(Text, { children: _jsxs(Text, { color: permissionSelection === 1 ? "green" : undefined, bold: permissionSelection === 1, children: [permissionSelection === 1 ? "> " : " ", " [b] Yes - enable autopilot (approve all)"] }) }), _jsx(Text, { children: _jsxs(Text, { color: permissionSelection === 2 ? "green" : undefined, bold: permissionSelection === 2, children: [permissionSelection === 2 ? "> " : " ", " [c] Tell Moth what to do instead"] }) })] })) : (_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Feedback: " }), _jsx(Text, { children: inputVal })] }))] })), !pendingPermission && (_jsxs(Box, { flexDirection: "column", children: [isProcessing && (_jsxs(Box, { flexDirection: "column", marginBottom: 0, children: [_jsx(Text, { color: "yellow", italic: true, children: thinkingText }), status !== 'Ready' && _jsxs(Text, { color: "gray", dimColor: true, children: [" ", status] })] })), selectedFiles.length > 0 && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { dimColor: true, children: ["Referenced Files (", selectedFiles.length, ") "] }), _jsxs(Text, { color: "gray", dimColor: true, children: ["[Ctrl+O to ", showFileChips ? 'hide' : 'show', "]"] })] }), showFileChips && (_jsx(Box, { flexDirection: "row", flexWrap: "wrap", marginTop: 1, children: selectedFiles.map((file) => (_jsx(FileChip, { filePath: file, onRemove: () => handleFileRemove(file) }, file))) }))] })), _jsxs(Box, { borderStyle: "round", borderColor: isProcessing ? "gray" : "blue", paddingX: 1, children: [_jsx(Text, { color: isProcessing ? "gray" : "cyan", children: '> ' }), _jsx(CustomTextInput, { value: inputVal, onChange: handleInputChange, onSubmit: (val) => {
|
|
374
401
|
// Don't submit if autocomplete is open
|
|
375
402
|
if (showAutocomplete) {
|
|
376
403
|
return;
|
|
@@ -383,5 +410,5 @@ export const App = ({ command, args, todoManager: propTodoManager, username }) =
|
|
|
383
410
|
setInputVal('');
|
|
384
411
|
setSelectedFiles([]);
|
|
385
412
|
}
|
|
386
|
-
}, focus: !isProcessing && !pendingPermission })] }), showAutocomplete && (_jsx(FileAutocomplete, { files: availableFiles, query: autocompleteQuery, selectedIndex: autocompleteIndex, onSelect: handleFileSelect })), showCommandPalette && (_jsx(CommandPalette, { onExecute: executeCommand, onClose: () => setShowCommandPalette(false) })),
|
|
413
|
+
}, focus: !isProcessing && !pendingPermission })] }), showAutocomplete && (_jsx(FileAutocomplete, { files: availableFiles, query: autocompleteQuery, selectedIndex: autocompleteIndex, onSelect: handleFileSelect })), showCommandPalette && (_jsx(CommandPalette, { onExecute: executeCommand, onClose: () => setShowCommandPalette(false) })), _jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: MODES[mode].color, children: [MODES[mode].icon, " ", MODES[mode].displayName.toUpperCase(), " MODE"] }), _jsx(Text, { dimColor: true, children: " | Ctrl+B to switch" })] }), _jsx(Text, { color: "gray", dimColor: true, children: activeProfile?.name })] })] }))] }));
|
|
387
414
|
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
/**
|
|
4
|
+
* Renders markdown-style text with colors in the terminal
|
|
5
|
+
* Supports:
|
|
6
|
+
* - # Headings (cyan, bold)
|
|
7
|
+
* - ## Subheadings (blue, bold)
|
|
8
|
+
* - ### Sub-subheadings (green)
|
|
9
|
+
* - **bold** text
|
|
10
|
+
* - `code` inline (yellow)
|
|
11
|
+
* - ```code blocks``` (yellow, dimmed)
|
|
12
|
+
* - Tables (formatted)
|
|
13
|
+
* - Bullet points
|
|
14
|
+
*/
|
|
15
|
+
export const MarkdownText = ({ content }) => {
|
|
16
|
+
const lines = content.split('\n');
|
|
17
|
+
const elements = [];
|
|
18
|
+
let inCodeBlock = false;
|
|
19
|
+
let codeBlockContent = [];
|
|
20
|
+
lines.forEach((line, index) => {
|
|
21
|
+
// Code block detection
|
|
22
|
+
if (line.trim().startsWith('```')) {
|
|
23
|
+
if (inCodeBlock) {
|
|
24
|
+
// End of code block - render accumulated content
|
|
25
|
+
elements.push(_jsx(Box, { flexDirection: "column", marginY: 1, paddingX: 2, borderStyle: "single", borderColor: "yellow", children: codeBlockContent.map((codeLine, i) => (_jsx(Text, { color: "yellow", dimColor: true, children: codeLine }, i))) }, `code-${index}`));
|
|
26
|
+
codeBlockContent = [];
|
|
27
|
+
inCodeBlock = false;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
// Start of code block
|
|
31
|
+
inCodeBlock = true;
|
|
32
|
+
const lang = line.trim().slice(3);
|
|
33
|
+
if (lang) {
|
|
34
|
+
elements.push(_jsxs(Text, { color: "gray", dimColor: true, children: ["[", lang, "]"] }, `lang-${index}`));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// Inside code block
|
|
40
|
+
if (inCodeBlock) {
|
|
41
|
+
codeBlockContent.push(line);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// Main heading (# )
|
|
45
|
+
if (line.startsWith('# ')) {
|
|
46
|
+
elements.push(_jsx(Text, { color: "cyan", bold: true, children: line.slice(2) }, index));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// Subheading (## )
|
|
50
|
+
if (line.startsWith('## ')) {
|
|
51
|
+
elements.push(_jsx(Text, { color: "blue", bold: true, children: line.slice(3) }, index));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// Sub-subheading (### )
|
|
55
|
+
if (line.startsWith('### ')) {
|
|
56
|
+
elements.push(_jsx(Text, { color: "green", children: line.slice(4) }, index));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Table detection (lines with | characters)
|
|
60
|
+
if (line.includes('|') && line.trim().startsWith('|')) {
|
|
61
|
+
const cells = line.split('|').filter(c => c.trim()).map(c => c.trim());
|
|
62
|
+
// Check if it's a separator line (|---|---|)
|
|
63
|
+
if (cells.every(c => /^-+$/.test(c))) {
|
|
64
|
+
elements.push(_jsx(Text, { color: "gray", dimColor: true, children: line }, index));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// Regular table row
|
|
68
|
+
elements.push(_jsx(Box, { flexDirection: "row", children: cells.map((cell, i) => (_jsx(Text, { color: i === 0 ? "cyan" : undefined, children: cell.padEnd(20, ' ') }, i))) }, index));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// Bullet points
|
|
72
|
+
if (line.trim().startsWith('* ') || line.trim().startsWith('- ')) {
|
|
73
|
+
const indent = line.search(/[*-]/);
|
|
74
|
+
const content = line.slice(indent + 2);
|
|
75
|
+
elements.push(_jsxs(Text, { children: [' '.repeat(indent), _jsx(Text, { color: "yellow", children: "\u2022 " }), renderInlineFormatting(content)] }, index));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Numbered lists
|
|
79
|
+
if (/^\s*\d+\.\s/.test(line)) {
|
|
80
|
+
const match = line.match(/^(\s*)(\d+)\.\s(.*)$/);
|
|
81
|
+
if (match) {
|
|
82
|
+
const [, indent, num, content] = match;
|
|
83
|
+
elements.push(_jsxs(Text, { children: [indent, _jsxs(Text, { color: "cyan", children: [num, ". "] }), renderInlineFormatting(content)] }, index));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Empty lines
|
|
88
|
+
if (line.trim() === '') {
|
|
89
|
+
elements.push(_jsx(Text, { children: " " }, index));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// Regular text with inline formatting
|
|
93
|
+
elements.push(_jsx(Text, { children: renderInlineFormatting(line) }, index));
|
|
94
|
+
});
|
|
95
|
+
return _jsx(Box, { flexDirection: "column", children: elements });
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Renders inline markdown formatting like **bold**, `code`, etc.
|
|
99
|
+
*/
|
|
100
|
+
function renderInlineFormatting(text) {
|
|
101
|
+
const parts = [];
|
|
102
|
+
let currentIndex = 0;
|
|
103
|
+
let key = 0;
|
|
104
|
+
// Match patterns: `code`, **bold**
|
|
105
|
+
const pattern = /(`[^`]+`)|(\*\*[^*]+\*\*)/g;
|
|
106
|
+
let match;
|
|
107
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
108
|
+
// Add text before the match
|
|
109
|
+
if (match.index > currentIndex) {
|
|
110
|
+
parts.push(_jsx(Text, { children: text.slice(currentIndex, match.index) }, key++));
|
|
111
|
+
}
|
|
112
|
+
// Add the formatted match
|
|
113
|
+
if (match[1]) {
|
|
114
|
+
// Code (backticks)
|
|
115
|
+
const code = match[1].slice(1, -1);
|
|
116
|
+
parts.push(_jsx(Text, { color: "yellow", backgroundColor: "black", children: code }, key++));
|
|
117
|
+
}
|
|
118
|
+
else if (match[2]) {
|
|
119
|
+
// Bold
|
|
120
|
+
const bold = match[2].slice(2, -2);
|
|
121
|
+
parts.push(_jsx(Text, { bold: true, children: bold }, key++));
|
|
122
|
+
}
|
|
123
|
+
currentIndex = match.index + match[0].length;
|
|
124
|
+
}
|
|
125
|
+
// Add remaining text
|
|
126
|
+
if (currentIndex < text.length) {
|
|
127
|
+
parts.push(_jsx(Text, { children: text.slice(currentIndex) }, key++));
|
|
128
|
+
}
|
|
129
|
+
return parts.length > 0 ? _jsx(_Fragment, { children: parts }) : text;
|
|
130
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moth-ai",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Local, LLM-agnostic code intelligence CLI",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"build": "tsc",
|
|
14
14
|
"prepublishOnly": "npm run build",
|
|
15
15
|
"start": "node dist/index.js",
|
|
16
|
+
"test": "node --import tsx --test tests/**/*.test.ts",
|
|
16
17
|
"lint": "eslint src/**/*.ts",
|
|
17
18
|
"format": "prettier --write \"src/**/*.ts\""
|
|
18
19
|
},
|
|
@@ -50,6 +51,7 @@
|
|
|
50
51
|
"eslint-plugin-prettier": "^5.5.4",
|
|
51
52
|
"prettier": "^3.7.4",
|
|
52
53
|
"ts-node": "^10.9.2",
|
|
54
|
+
"tsx": "^4.19.2",
|
|
53
55
|
"typescript": "^5.9.3"
|
|
54
56
|
},
|
|
55
57
|
"dependencies": {
|