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 CHANGED
@@ -1,319 +1,90 @@
1
1
  # πŸ¦‹ Moth AI
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/@kishlay42/moth-ai.svg)](https://www.npmjs.com/package/@kishlay42/moth-ai)
3
+ [![npm version](https://img.shields.io/npm/v/moth-ai.svg)](https://www.npmjs.com/package/moth-ai)
4
4
  [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)
5
- [![Node.js Version](https://img.shields.io/node/v/@kishlay42/moth-ai.svg)](https://nodejs.org)
6
5
 
7
- **The Intelligent, Local-First CLI Coding Assistant**
6
+ > **The World's First Truly Open CLI Assistant for Local & Open Source Models**
8
7
 
9
- Moth AI is a powerful **terminal-native coding assistant** built for developers who value **privacy, speed, and control**. It lives inside your terminal, understands your project context, and helps you **write, debug, refactor, and reason about code** using both **local and cloud LLMs**.
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
- ## ✨ Key Features
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
- - **Cloud Providers**
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
- <img width="1093" height="241" alt="LLM Switching" src="https://github.com/user-attachments/assets/2de67c9d-f562-4ce3-8bc6-51e2066b69ae" />
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
- ### πŸ€– Agentic Capabilities
20
+ ## πŸš€ Key Features
100
21
 
101
- Moth is not just a chatbot β€” it's an **AI agent**.
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
- You stay in control β€” always.
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
- - Explicit approval before file edits or command execution
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
- ### 🎭 Moth Profiles
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
- Save and switch between different AI personalities.
124
-
125
- - **Coding Profile** – Optimized for TypeScript / Python
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
- ## οΏ½ CLI Commands
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
- ### LLM Profile Management
48
+ ### Global Installation (Recommended)
49
+ Install Moth AI globally to access the `moth` command from any directory.
147
50
 
148
51
  ```bash
149
- # Add a new LLM profile
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
- ### Setting Up Ollama (Local Models)
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
- moth llm add
217
- # Select "Anthropic"
218
- # Enter your API key from https://console.anthropic.com/
59
+ npm install moth-ai
60
+ npx moth-ai
219
61
  ```
220
62
 
221
- #### Google (Gemini)
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
- ## πŸ’‘ Usage Examples
67
+ ## ⚑ Quick Start
231
68
 
232
- ### Basic Chat
69
+ 1. **Initialize Moth:**
70
+ ```bash
71
+ moth
72
+ ```
233
73
 
234
- ```bash
235
- moth
236
- > How do I implement a binary search in TypeScript?
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
- ### Code Refactoring
240
-
241
- ```bash
242
- moth
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: "Failed to parse LLM response. Retrying...", toolOutput: `Error: ${e}` };
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
- You have access to the following tools:
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
- IMPORTANT GUIDELINES:
73
- 1. For general questions, explanations, or code snippets that don't need to be saved, use "finalAnswer".
74
- 2. Do NOT use "write_to_file" unless the user explicitly asks to save a file or implies a persistent change.
75
- 3. If the user asks for "Hello World code", just show it in the explanation (finalAnswer). Do NOT create a file for it.
76
- 4. Be concise and helpful.
77
- 5. If files are referenced above, use that context to answer questions about them.
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": "your reasoning",
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
- OR if you are done/replying:
202
+
203
+ For final answer:
85
204
  {
86
- "thought": "reasoning",
87
- "finalAnswer": "your response/code/explanation"
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
- // Clean markdown code blocks if present
135
- const jsonText = text.replace(/```json/g, '').replace(/```/g, '').trim();
136
- return JSON.parse(jsonText);
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 (autopilot) {
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
- setAutopilot(prev => !prev);
216
- setMessages(prev => [...prev, {
217
- role: 'assistant',
218
- content: `Autopilot ${!autopilot ? 'enabled' : 'disabled'}.`
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
- setAutopilot(true);
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
- setAutopilot(true);
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.0" }) }), _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: "row", marginBottom: 1, children: [_jsxs(Text, { color: m.role === 'user' ? 'blue' : 'green', bold: true, children: [m.role === 'user' ? 'You' : 'Moth', ":"] }), _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] })] })), autopilot && (_jsx(Text, { color: "magenta", children: "AUTOPILOT MODE" })), 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) => {
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) })), _jsx(Box, { flexDirection: "row", justifyContent: "flex-end", children: _jsx(Text, { color: "gray", dimColor: true, children: activeProfile?.name }) })] }))] }));
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",
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": {