@kishlay42/moth-ai 1.0.2 β†’ 1.0.3

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,17 +1,85 @@
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)
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
+
3
7
  **The Intelligent, Local-First CLI Coding Assistant**
4
8
 
5
- <img width="1095" height="504" alt="Screenshot 2025-12-27 015039" src="https://github.com/user-attachments/assets/23b83a6b-2b63-45af-b9ec-a6dcb0a89b2f" />
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
6
32
 
7
- Moth AI is a powerful **terminal-native coding assistant** built for developers who value **privacy, speed, and control**.
8
- 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**.
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
9
76
 
10
77
  ---
11
78
 
12
79
  ## ✨ Key Features
13
80
 
14
81
  ### 🧠 LLM-Agnostic & Local-First
82
+
15
83
  Use **any LLM**, local or cloud β€” switch instantly without changing workflows.
16
84
 
17
85
  - **Local (via Ollama)**
@@ -24,32 +92,34 @@ Use **any LLM**, local or cloud β€” switch instantly without changing workflows.
24
92
  - Anthropic (Claude 3.5 Sonnet)
25
93
  - Google (Gemini)
26
94
 
27
- <img width="1093" height="241" alt="Screenshot 2025-12-27 014805" src="https://github.com/user-attachments/assets/2de67c9d-f562-4ce3-8bc6-51e2066b69ae" />
28
-
29
- _Easily switch between local and cloud models_
95
+ <img width="1093" height="241" alt="LLM Switching" src="https://github.com/user-attachments/assets/2de67c9d-f562-4ce3-8bc6-51e2066b69ae" />
30
96
 
31
97
  ---
32
98
 
33
99
  ### πŸ€– Agentic Capabilities
34
- Moth is not just a chatbot β€” it’s an **AI agent**.
100
+
101
+ Moth is not just a chatbot β€” it's an **AI agent**.
35
102
 
36
103
  - **Task Planning** – Break complex goals into executable steps
37
104
  - **File Editing** – Precise diffs, patches, and refactors
38
105
  - **Terminal Control** – Run builds, tests, and Git commands from chat
106
+ - **Context-Aware** – Understands your project structure and codebase
39
107
 
40
108
  ---
41
109
 
42
110
  ### πŸ›‘οΈ Permission-First by Design
111
+
43
112
  You stay in control β€” always.
44
113
 
45
114
  - Explicit approval before file edits or command execution
46
115
  - Granular permissions per action
47
- - Autopilot mode for trusted workflows
116
+ - **Autopilot mode** for trusted workflows
48
117
  - Feedback loop to guide the agent instead of blind execution
49
118
 
50
119
  ---
51
120
 
52
121
  ### 🎭 Moth Profiles
122
+
53
123
  Save and switch between different AI personalities.
54
124
 
55
125
  - **Coding Profile** – Optimized for TypeScript / Python
@@ -57,17 +127,193 @@ Save and switch between different AI personalities.
57
127
  - **Fast Profile** – Lightweight local model for quick answers
58
128
 
59
129
  ---
60
- ## πŸš€ Installation
61
130
 
62
- ### Global (recommended)
131
+ ## οΏ½ CLI Commands
132
+
133
+ ### Main Commands
134
+
63
135
  ```bash
64
- npm install -g @kishlay42/moth-ai
136
+ # Start interactive chat
137
+ moth
138
+
139
+ # Show help
140
+ moth --help
141
+
142
+ # Display version
143
+ moth --version
65
144
  ```
66
145
 
67
- ### Project-local
146
+ ### LLM Profile Management
147
+
148
+ ```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
68
160
  ```
69
- npm install @kishlay42/moth-ai
70
- npx moth
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"
190
+ ```
191
+
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
+ ```
213
+
214
+ #### Anthropic (Claude)
215
+ ```bash
216
+ moth llm add
217
+ # Select "Anthropic"
218
+ # Enter your API key from https://console.anthropic.com/
219
+ ```
220
+
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
+ ```
227
+
228
+ ---
229
+
230
+ ## πŸ’‘ Usage Examples
231
+
232
+ ### Basic Chat
233
+
234
+ ```bash
235
+ moth
236
+ > How do I implement a binary search in TypeScript?
237
+ ```
238
+
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
71
258
  ```
72
259
 
260
+ ---
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
+ ```
294
+
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
+ ---
73
318
 
319
+ **Made with ❀️ for developers who code in the terminal**
@@ -1,10 +1,15 @@
1
+ import { formatFileForContext, truncateFileContent } from '../utils/fileUtils.js';
2
+ import * as fs from 'fs/promises';
3
+ import * as path from 'path';
1
4
  export class AgentOrchestrator {
2
5
  config;
3
6
  state;
4
7
  tools;
5
- constructor(config, registry) {
8
+ root;
9
+ constructor(config, registry, root = process.cwd()) {
6
10
  this.config = config;
7
11
  this.tools = registry;
12
+ this.root = root;
8
13
  this.state = {
9
14
  history: [],
10
15
  maxSteps: config.maxSteps || 10
@@ -13,8 +18,10 @@ export class AgentOrchestrator {
13
18
  async *run(prompt, history = []) {
14
19
  let currentPrompt = prompt;
15
20
  for (let i = 0; i < this.state.maxSteps; i++) {
16
- // Construct system prompt with tools
17
- const systemPrompt = this.buildSystemPrompt();
21
+ // Collect all attached files from history
22
+ const allAttachedFiles = this.collectAttachedFiles(history);
23
+ // Construct system prompt with tools and file context
24
+ const systemPrompt = await this.buildSystemPrompt(allAttachedFiles);
18
25
  // Full context: System -> History -> Current State
19
26
  const messages = [
20
27
  { role: 'user', content: systemPrompt }, // In many APIs system prompt is special, here we use user role as generic fallback or modify Client to handle system
@@ -50,17 +57,24 @@ export class AgentOrchestrator {
50
57
  }
51
58
  return "Max steps reached.";
52
59
  }
53
- buildSystemPrompt() {
60
+ async buildSystemPrompt(attachedFiles = []) {
54
61
  const toolDefs = this.tools.getDefinitions().map(t => `${t.name}: ${t.description} Params: ${JSON.stringify(t.parameters)}`).join('\n');
62
+ let fileContext = '';
63
+ if (attachedFiles.length > 0) {
64
+ fileContext = await this.buildFileContext(attachedFiles);
65
+ }
55
66
  return `You are Moth, an intelligent CLI coding assistant.
56
67
  You have access to the following tools:
57
68
  ${toolDefs}
58
69
 
70
+ ${fileContext}
71
+
59
72
  IMPORTANT GUIDELINES:
60
73
  1. For general questions, explanations, or code snippets that don't need to be saved, use "finalAnswer".
61
74
  2. Do NOT use "write_to_file" unless the user explicitly asks to save a file or implies a persistent change.
62
75
  3. If the user asks for "Hello World code", just show it in the explanation (finalAnswer). Do NOT create a file for it.
63
76
  4. Be concise and helpful.
77
+ 5. If files are referenced above, use that context to answer questions about them.
64
78
 
65
79
  Format your response exactly as a JSON object:
66
80
  {
@@ -74,6 +88,36 @@ OR if you are done/replying:
74
88
  }
75
89
  `;
76
90
  }
91
+ collectAttachedFiles(history) {
92
+ const files = new Set();
93
+ for (const msg of history) {
94
+ if (msg.attachedFiles) {
95
+ msg.attachedFiles.forEach(f => files.add(f));
96
+ }
97
+ }
98
+ return Array.from(files);
99
+ }
100
+ async buildFileContext(filePaths) {
101
+ const fileContents = ['=== Referenced Files ===\n'];
102
+ for (const filePath of filePaths) {
103
+ try {
104
+ const fullPath = path.join(this.root, filePath);
105
+ // Security: Prevent breaking out of root
106
+ if (!fullPath.startsWith(this.root)) {
107
+ fileContents.push(`File: ${filePath}\nError: Access denied (outside project root)\n`);
108
+ continue;
109
+ }
110
+ const content = await fs.readFile(fullPath, 'utf-8');
111
+ const truncated = truncateFileContent(content, 500);
112
+ const formatted = formatFileForContext(filePath, truncated);
113
+ fileContents.push(formatted);
114
+ }
115
+ catch (e) {
116
+ fileContents.push(`File: ${filePath}\nError: ${e.message}\n`);
117
+ }
118
+ }
119
+ return fileContents.join('\n');
120
+ }
77
121
  async callLLM(messages) {
78
122
  // Direct integration with LLM Client
79
123
  // This assumes Client has a simple chat interface returning string
package/dist/index.js CHANGED
@@ -114,11 +114,12 @@ program.command('use')
114
114
  let config = loadConfig();
115
115
  if (!name) {
116
116
  console.log("No active profile selected. Please select one:");
117
- render(React.createElement(ProfileManager, {
117
+ const { unmount } = render(React.createElement(ProfileManager, {
118
118
  config,
119
119
  onSelect: (selected) => {
120
120
  // onSelect saves internally
121
- process.exit(0);
121
+ unmount();
122
+ setTimeout(() => startChatSession(), 500);
122
123
  }
123
124
  }));
124
125
  return;
@@ -139,9 +140,12 @@ program.command('remove')
139
140
  .action(async (name) => {
140
141
  let config = loadConfig();
141
142
  if (!name) {
142
- render(React.createElement(LLMRemover, {
143
+ const { unmount } = render(React.createElement(LLMRemover, {
143
144
  config,
144
- onExit: () => process.exit(0)
145
+ onExit: () => {
146
+ unmount();
147
+ setTimeout(() => startChatSession(), 500);
148
+ }
145
149
  }));
146
150
  return;
147
151
  }
@@ -160,7 +164,7 @@ program.command('add')
160
164
  .action(async (options) => {
161
165
  // Re-use exactly the same logic as llm add
162
166
  if (!options.name && !options.provider && !options.model) {
163
- render(React.createElement(LLMWizard, {
167
+ const { unmount } = render(React.createElement(LLMWizard, {
164
168
  onComplete: async (resultConfig) => {
165
169
  let config = loadConfig();
166
170
  config = addProfile(config, resultConfig);
@@ -170,12 +174,14 @@ program.command('add')
170
174
  config = setActiveProfile(config, resultConfig.name);
171
175
  saveConfig(config);
172
176
  console.log(`Profile '${resultConfig.name}' created.`);
173
- // Chain to main chat
174
- process.exit(0); // For now, just exit as per current wizard
177
+ // Redirect to chat
178
+ unmount();
179
+ setTimeout(() => startChatSession(), 500);
175
180
  },
176
181
  onCancel: () => {
177
182
  console.log('Setup cancelled.');
178
- process.exit(0);
183
+ unmount();
184
+ setTimeout(() => startChatSession(), 500);
179
185
  }
180
186
  }));
181
187
  return;
@@ -228,7 +234,7 @@ llm.command('add')
228
234
  .action(async (options) => {
229
235
  // If no options provided, launch wizard
230
236
  if (!options.name && !options.provider && !options.model) {
231
- render(React.createElement(LLMWizard, {
237
+ const { unmount } = render(React.createElement(LLMWizard, {
232
238
  onComplete: async (resultConfig) => {
233
239
  let config = loadConfig();
234
240
  config = addProfile(config, resultConfig);
@@ -240,11 +246,13 @@ llm.command('add')
240
246
  }
241
247
  saveConfig(config);
242
248
  console.log(`Profile '${resultConfig.name}' added successfully!`);
243
- process.exit(0);
249
+ unmount();
250
+ setTimeout(() => startChatSession(), 500);
244
251
  },
245
252
  onCancel: () => {
246
253
  console.log('Setup cancelled.');
247
- process.exit(0);
254
+ unmount();
255
+ setTimeout(() => startChatSession(), 500);
248
256
  }
249
257
  }));
250
258
  return;
@@ -298,9 +306,12 @@ llm.command('remove')
298
306
  .action(async (name) => {
299
307
  let config = loadConfig();
300
308
  if (!name) {
301
- render(React.createElement(LLMRemover, {
309
+ const { unmount } = render(React.createElement(LLMRemover, {
302
310
  config,
303
- onExit: () => process.exit(0)
311
+ onExit: () => {
312
+ unmount();
313
+ setTimeout(() => startChatSession(), 500);
314
+ }
304
315
  }));
305
316
  return;
306
317
  }
package/dist/ui/App.js CHANGED
@@ -2,12 +2,20 @@ 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
6
  import { TodoManager } from '../planning/todoManager.js';
6
7
  import { AgentOrchestrator } from '../agent/orchestrator.js';
7
8
  import { createToolRegistry } from '../tools/factory.js';
8
9
  import { findProjectRoot } from '../utils/paths.js';
9
- import TextInput from 'ink-text-input';
10
+ import { ProjectScanner } from '../context/scanner.js';
11
+ import { CustomTextInput } from './components/CustomTextInput.js';
10
12
  import { WordMoth } from './components/WordMoth.js';
13
+ import { FileChip } from './components/FileChip.js';
14
+ import { FileAutocomplete } from './components/FileAutocomplete.js';
15
+ import { CommandPalette } from './components/CommandPalette.js';
16
+ import { ProfileManager } from './ProfileManager.js';
17
+ import { LLMWizard } from './wizards/LLMWizard.js';
18
+ import { LLMRemover } from './wizards/LLMRemover.js';
11
19
  export const App = ({ command, args, todoManager: propTodoManager, username }) => {
12
20
  const [messages, setMessages] = useState([]);
13
21
  const [inputVal, setInputVal] = useState('');
@@ -18,6 +26,17 @@ export const App = ({ command, args, todoManager: propTodoManager, username }) =
18
26
  const [autopilot, setAutopilot] = useState(false);
19
27
  const [isProcessing, setIsProcessing] = useState(false);
20
28
  const [thinkingText, setThinkingText] = useState('Sauting...');
29
+ // File Reference State
30
+ const [selectedFiles, setSelectedFiles] = useState([]);
31
+ const [showAutocomplete, setShowAutocomplete] = useState(false);
32
+ const [autocompleteQuery, setAutocompleteQuery] = useState('');
33
+ const [autocompleteIndex, setAutocompleteIndex] = useState(0);
34
+ const [availableFiles, setAvailableFiles] = useState([]);
35
+ const [showFileChips, setShowFileChips] = useState(false); // Collapsed by default
36
+ // Command Palette State
37
+ const [showCommandPalette, setShowCommandPalette] = useState(false);
38
+ // Active Wizard State (for command palette execution)
39
+ const [activeWizard, setActiveWizard] = useState(null);
21
40
  // Effect for cycling thinking text
22
41
  useEffect(() => {
23
42
  if (isProcessing) {
@@ -28,6 +47,21 @@ export const App = ({ command, args, todoManager: propTodoManager, username }) =
28
47
  return () => clearInterval(interval);
29
48
  }
30
49
  }, [isProcessing]);
50
+ // Load available files on mount
51
+ useEffect(() => {
52
+ const loadFiles = async () => {
53
+ try {
54
+ const root = findProjectRoot() || process.cwd();
55
+ const scanner = new ProjectScanner(root);
56
+ const files = await scanner.scan();
57
+ setAvailableFiles(files);
58
+ }
59
+ catch (e) {
60
+ console.error('Failed to load files:', e);
61
+ }
62
+ };
63
+ loadFiles();
64
+ }, []);
31
65
  // Permission State
32
66
  const [pendingPermission, setPendingPermission] = useState(null);
33
67
  const [feedbackMode, setFeedbackMode] = useState(false);
@@ -45,12 +79,16 @@ export const App = ({ command, args, todoManager: propTodoManager, username }) =
45
79
  runAgent(initialPrompt);
46
80
  }
47
81
  }, [initialPrompt]);
48
- const runAgent = async (userPrompt) => {
82
+ const runAgent = async (userPrompt, attachedFiles) => {
49
83
  if (showWelcome)
50
84
  setShowWelcome(false);
51
85
  setIsProcessing(true);
52
- // Create new user message
53
- const userMsg = { role: 'user', content: userPrompt };
86
+ // Create new user message with attached files
87
+ const userMsg = {
88
+ role: 'user',
89
+ content: userPrompt,
90
+ attachedFiles: attachedFiles && attachedFiles.length > 0 ? attachedFiles : undefined
91
+ };
54
92
  setMessages(prev => [...prev, userMsg]);
55
93
  try {
56
94
  const root = findProjectRoot() || process.cwd();
@@ -76,7 +114,7 @@ export const App = ({ command, args, todoManager: propTodoManager, username }) =
76
114
  model: client,
77
115
  tools: registry.getDefinitions(),
78
116
  maxSteps: 10
79
- }, registry);
117
+ }, registry, root);
80
118
  let finalAnswer = "";
81
119
  // Pass accumulated history to the agent
82
120
  // 'messages' state contains history prior to this turn.
@@ -111,9 +149,129 @@ export const App = ({ command, args, todoManager: propTodoManager, username }) =
111
149
  setIsProcessing(false);
112
150
  }
113
151
  };
152
+ // Handle input changes to detect @ for autocomplete
153
+ const handleInputChange = (value) => {
154
+ setInputVal(value);
155
+ // Extract and track file references from @ mentions
156
+ const referencedFiles = extractFileReferences(value);
157
+ setSelectedFiles(referencedFiles);
158
+ // Detect @ symbol for file autocomplete
159
+ const atIndex = value.lastIndexOf('@');
160
+ if (atIndex !== -1) {
161
+ const afterAt = value.slice(atIndex + 1);
162
+ // Close autocomplete if there's a space after @ (end of filename)
163
+ if (afterAt.includes(' ')) {
164
+ setShowAutocomplete(false);
165
+ return;
166
+ }
167
+ const query = afterAt;
168
+ // Only show autocomplete if @ is at start or after a space
169
+ if (atIndex === 0 || value[atIndex - 1] === ' ') {
170
+ setAutocompleteQuery(query);
171
+ setShowAutocomplete(true);
172
+ setAutocompleteIndex(0);
173
+ }
174
+ }
175
+ else {
176
+ setShowAutocomplete(false);
177
+ }
178
+ };
179
+ // Get filtered files for autocomplete
180
+ const getFilteredFiles = () => {
181
+ return availableFiles
182
+ .filter(file => {
183
+ const lowerFile = file.toLowerCase();
184
+ const lowerQuery = autocompleteQuery.toLowerCase();
185
+ return lowerFile.includes(lowerQuery);
186
+ })
187
+ .filter(file => !selectedFiles.includes(file)) // Exclude already selected
188
+ .slice(0, 10);
189
+ };
190
+ const handleFileSelect = (file) => {
191
+ // Replace the entire input value to ensure cursor ends up at the end
192
+ const atIndex = inputVal.lastIndexOf('@');
193
+ if (atIndex !== -1) {
194
+ const beforeAt = inputVal.slice(0, atIndex);
195
+ // Set the complete new value - this places cursor at the end automatically
196
+ const newInput = beforeAt + '@' + file + ' ';
197
+ setInputVal(newInput);
198
+ }
199
+ setShowAutocomplete(false);
200
+ setAutocompleteQuery('');
201
+ };
202
+ const handleFileRemove = (file) => {
203
+ setSelectedFiles(prev => prev.filter(f => f !== file));
204
+ };
205
+ // Extract file references from input text
206
+ const extractFileReferences = (text) => {
207
+ const atMatches = text.match(/@[\w\/.\-]+/g) || [];
208
+ return atMatches.map(match => match.slice(1)); // Remove @ prefix
209
+ };
210
+ // Execute command from command palette
211
+ const executeCommand = (action) => {
212
+ setShowCommandPalette(false);
213
+ switch (action) {
214
+ case 'autopilot':
215
+ setAutopilot(prev => !prev);
216
+ setMessages(prev => [...prev, {
217
+ role: 'assistant',
218
+ content: `Autopilot ${!autopilot ? 'enabled' : 'disabled'}.`
219
+ }]);
220
+ break;
221
+ case 'llm-list':
222
+ setActiveWizard('llm-list');
223
+ break;
224
+ case 'llm-add':
225
+ setActiveWizard('llm-add');
226
+ break;
227
+ case 'llm-use':
228
+ setActiveWizard('llm-use');
229
+ break;
230
+ case 'llm-remove':
231
+ setActiveWizard('llm-remove');
232
+ break;
233
+ case 'help':
234
+ setMessages(prev => [...prev, {
235
+ role: 'assistant',
236
+ content: 'Moth AI - Your terminal coding assistant.\n\nKeyboard Shortcuts:\n- Ctrl+U: Open command palette\n- Ctrl+O: Toggle file references\n- Esc: Pause/Resume\n\nFor full help, run: moth --help'
237
+ }]);
238
+ break;
239
+ }
240
+ };
114
241
  useInput((input, key) => {
242
+ // Autocomplete Navigation
243
+ if (showAutocomplete && !pendingPermission) {
244
+ const filteredFiles = getFilteredFiles();
245
+ if (key.escape) {
246
+ setShowAutocomplete(false);
247
+ return;
248
+ }
249
+ if (key.upArrow) {
250
+ setAutocompleteIndex(prev => (prev - 1 + filteredFiles.length) % filteredFiles.length);
251
+ return;
252
+ }
253
+ if (key.downArrow) {
254
+ setAutocompleteIndex(prev => (prev + 1) % filteredFiles.length);
255
+ return;
256
+ }
257
+ // Use Enter to select file from autocomplete
258
+ if (key.return && filteredFiles.length > 0) {
259
+ handleFileSelect(filteredFiles[autocompleteIndex]);
260
+ return;
261
+ }
262
+ }
263
+ // Ctrl+O to toggle file chips visibility
264
+ if (input === 'o' && key.ctrl) {
265
+ setShowFileChips(prev => !prev);
266
+ return;
267
+ }
268
+ // Ctrl+U to toggle command palette
269
+ if (input === 'u' && key.ctrl) {
270
+ setShowCommandPalette(prev => !prev);
271
+ return;
272
+ }
115
273
  // ESC Pause
116
- if (key.escape) {
274
+ if (key.escape && !showAutocomplete) {
117
275
  setIsPaused(prev => !prev);
118
276
  setStatus(prev => prev === 'Paused' ? 'Resumed' : 'Paused');
119
277
  return;
@@ -173,10 +331,57 @@ export const App = ({ command, args, todoManager: propTodoManager, username }) =
173
331
  }
174
332
  });
175
333
  // --- RENDER ---
176
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [(command === 'run' && showWelcome && messages.length === 0) && (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingBottom: 1, paddingTop: 0, borderStyle: "round", borderColor: "#0192e5", alignItems: "flex-start", children: [_jsx(WordMoth, { text: "MOTH", big: true }), _jsxs(Box, { flexDirection: "column", alignItems: "flex-start", marginTop: -1, children: [_jsx(Text, { dimColor: true, children: "v1.0.0" }), _jsxs(Text, { color: "#3EA0C3", bold: true, children: ["Welcome back, ", username || os.userInfo().username, "."] }), _jsxs(Text, { color: "green", children: ["Active AI: ", activeProfile?.name || 'None'] }), _jsxs(Text, { dimColor: true, children: ["Path: ", process.cwd()] }), _jsx(Text, { dimColor: true, children: "Run \"moth --help\" to view all 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" })), _jsxs(Box, { borderStyle: "round", borderColor: isProcessing ? "gray" : "blue", paddingX: 1, children: [_jsx(Text, { color: isProcessing ? "gray" : "cyan", children: '> ' }), _jsx(TextInput, { value: inputVal, onChange: setInputVal, onSubmit: (val) => {
334
+ // Render wizards if active
335
+ if (activeWizard === 'llm-list' || activeWizard === 'llm-use') {
336
+ const config = loadConfig();
337
+ return (_jsx(ProfileManager, { config: config, onSelect: (profile) => {
338
+ setActiveWizard(null);
339
+ setMessages(prev => [...prev, {
340
+ role: 'assistant',
341
+ content: `Switched to profile '${profile.name}'.`
342
+ }]);
343
+ } }));
344
+ }
345
+ if (activeWizard === 'llm-add') {
346
+ return (_jsx(LLMWizard, { onComplete: (newProfile) => {
347
+ const config = loadConfig();
348
+ const updatedConfig = addProfile(config, newProfile);
349
+ saveConfig(updatedConfig);
350
+ setActiveWizard(null);
351
+ setMessages(prev => [...prev, {
352
+ role: 'assistant',
353
+ content: `Profile '${newProfile.name}' added successfully!`
354
+ }]);
355
+ }, onCancel: () => {
356
+ setActiveWizard(null);
357
+ setMessages(prev => [...prev, {
358
+ role: 'assistant',
359
+ content: 'Cancelled.'
360
+ }]);
361
+ } }));
362
+ }
363
+ if (activeWizard === 'llm-remove') {
364
+ const config = loadConfig();
365
+ return (_jsx(LLMRemover, { config: config, onExit: () => {
366
+ setActiveWizard(null);
367
+ setMessages(prev => [...prev, {
368
+ role: 'assistant',
369
+ content: 'Returned to chat.'
370
+ }]);
371
+ } }));
372
+ }
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) => {
374
+ // Don't submit if autocomplete is open
375
+ if (showAutocomplete) {
376
+ return;
377
+ }
177
378
  if (val.trim() && !isProcessing) {
178
- runAgent(val);
379
+ // Extract file references from @ mentions in text
380
+ const referencedFiles = extractFileReferences(val);
381
+ const allFiles = [...new Set([...selectedFiles, ...referencedFiles])];
382
+ runAgent(val, allFiles.length > 0 ? allFiles : undefined);
179
383
  setInputVal('');
384
+ setSelectedFiles([]);
180
385
  }
181
- }, focus: !isProcessing && !pendingPermission })] }), _jsx(Box, { flexDirection: "row", justifyContent: "flex-end", children: _jsx(Text, { color: "gray", dimColor: true, children: activeProfile?.name }) })] }))] }));
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 }) })] }))] }));
182
387
  };
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ const commands = [
5
+ { name: 'LLM: List Profiles', description: 'Show all configured LLM profiles', action: 'llm-list' },
6
+ { name: 'LLM: Add Profile', description: 'Add a new LLM profile', action: 'llm-add' },
7
+ { name: 'LLM: Switch Profile', description: 'Switch to a different LLM profile', action: 'llm-use' },
8
+ { name: 'LLM: Remove Profile', description: 'Remove an LLM profile', action: 'llm-remove' },
9
+ { name: 'Toggle Autopilot', description: 'Enable/disable autopilot mode', action: 'autopilot' },
10
+ { name: 'Show Help', description: 'Display help information', action: 'help' },
11
+ ];
12
+ export const CommandPalette = ({ onExecute, onClose }) => {
13
+ const [selectedIndex, setSelectedIndex] = useState(0);
14
+ useInput((input, key) => {
15
+ if (key.upArrow) {
16
+ setSelectedIndex(prev => Math.max(0, prev - 1));
17
+ }
18
+ else if (key.downArrow) {
19
+ setSelectedIndex(prev => Math.min(commands.length - 1, prev + 1));
20
+ }
21
+ else if (key.return) {
22
+ onExecute(commands[selectedIndex].action);
23
+ }
24
+ else if (key.escape || (input === 'u' && key.ctrl)) {
25
+ onClose();
26
+ }
27
+ });
28
+ return (_jsx(Box, { flexDirection: "column", children: _jsxs(Box, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Command Palette (Ctrl+U to close)" }), _jsx(Text, { dimColor: true, children: "Select a command and press Enter" }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: commands.map((cmd, index) => (_jsxs(Box, { marginY: 0, children: [_jsxs(Text, { color: index === selectedIndex ? 'green' : undefined, bold: index === selectedIndex, children: [index === selectedIndex ? '> ' : ' ', cmd.name] }), _jsxs(Text, { dimColor: true, children: [" - ", cmd.description] })] }, cmd.action))) })] }) }));
29
+ };
@@ -0,0 +1,75 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ export const CustomTextInput = ({ value: externalValue, onChange, onSubmit, focus = true, placeholder = '' }) => {
5
+ const [internalValue, setInternalValue] = useState(externalValue);
6
+ const [cursorPos, setCursorPos] = useState(externalValue.length);
7
+ // Sync with external value changes (e.g., from file path insertion)
8
+ useEffect(() => {
9
+ setInternalValue(externalValue);
10
+ // Place cursor at end when value changes externally
11
+ setCursorPos(externalValue.length);
12
+ }, [externalValue]);
13
+ useInput((input, key) => {
14
+ if (!focus)
15
+ return;
16
+ // Submit on Enter
17
+ if (key.return) {
18
+ onSubmit(internalValue);
19
+ return;
20
+ }
21
+ // Backspace - delete character before cursor
22
+ if (key.backspace) {
23
+ if (cursorPos > 0) {
24
+ const newValue = internalValue.slice(0, cursorPos - 1) + internalValue.slice(cursorPos);
25
+ setInternalValue(newValue);
26
+ setCursorPos(cursorPos - 1);
27
+ onChange(newValue);
28
+ }
29
+ return;
30
+ }
31
+ // Delete - delete character after cursor
32
+ if (key.delete) {
33
+ if (cursorPos < internalValue.length) {
34
+ const newValue = internalValue.slice(0, cursorPos) + internalValue.slice(cursorPos + 1);
35
+ setInternalValue(newValue);
36
+ onChange(newValue);
37
+ }
38
+ return;
39
+ }
40
+ // Left arrow - move cursor left
41
+ if (key.leftArrow) {
42
+ setCursorPos(Math.max(0, cursorPos - 1));
43
+ return;
44
+ }
45
+ // Right arrow - move cursor right
46
+ if (key.rightArrow) {
47
+ setCursorPos(Math.min(internalValue.length, cursorPos + 1));
48
+ return;
49
+ }
50
+ // Home - move to start
51
+ if (key.home) {
52
+ setCursorPos(0);
53
+ return;
54
+ }
55
+ // End - move to end
56
+ if (key.end) {
57
+ setCursorPos(internalValue.length);
58
+ return;
59
+ }
60
+ // Regular character input (ignore ctrl/meta combinations)
61
+ if (input && !key.ctrl && !key.meta && input.length === 1) {
62
+ const newValue = internalValue.slice(0, cursorPos) + input + internalValue.slice(cursorPos);
63
+ setInternalValue(newValue);
64
+ setCursorPos(cursorPos + 1);
65
+ onChange(newValue);
66
+ return;
67
+ }
68
+ }, { isActive: focus });
69
+ // Render the input with cursor
70
+ const displayValue = internalValue;
71
+ const beforeCursor = displayValue.slice(0, cursorPos);
72
+ const cursorChar = displayValue[cursorPos] || ' '; // Always show space if nothing there
73
+ const afterCursor = displayValue.slice(cursorPos + 1);
74
+ return (_jsxs(Box, { children: [_jsx(Text, { children: beforeCursor }), _jsx(Text, { inverse: true, children: cursorChar }), _jsx(Text, { children: afterCursor })] }));
75
+ };
@@ -0,0 +1,16 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ export const FileAutocomplete = ({ files, query, selectedIndex, onSelect }) => {
4
+ // Filter files based on query
5
+ const filteredFiles = files
6
+ .filter(file => {
7
+ const lowerFile = file.toLowerCase();
8
+ const lowerQuery = query.toLowerCase();
9
+ return lowerFile.includes(lowerQuery);
10
+ })
11
+ .slice(0, 10); // Limit to 10 results
12
+ if (filteredFiles.length === 0) {
13
+ return (_jsx(Box, { borderStyle: "round", borderColor: "yellow", flexDirection: "column", paddingX: 1, children: _jsxs(Text, { dimColor: true, children: ["No files found matching \"", query, "\""] }) }));
14
+ }
15
+ return (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "yellow", children: "\u2191\u2193 navigate, Enter inserts path, Esc cancels" }), filteredFiles.map((file, index) => (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: index === selectedIndex ? 'green' : undefined, bold: index === selectedIndex, children: [index === selectedIndex ? '> ' : ' ', file] }) }, file)))] }));
16
+ };
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import * as path from 'path';
4
+ export const FileChip = ({ filePath, onRemove }) => {
5
+ const basename = path.basename(filePath);
6
+ const dirname = path.dirname(filePath);
7
+ return (_jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, marginRight: 1, children: [_jsx(Text, { color: "cyan", bold: true, children: basename }), _jsxs(Text, { dimColor: true, children: [" ", dirname !== '.' ? `(${dirname})` : ''] }), _jsx(Text, { color: "red", bold: true, children: " [x]" })] }));
8
+ };
@@ -0,0 +1,67 @@
1
+ import * as path from 'path';
2
+ /**
3
+ * Filter files by query using simple substring matching
4
+ */
5
+ export function fuzzyMatchFiles(query, files) {
6
+ const lowerQuery = query.toLowerCase();
7
+ return files.filter(file => file.toLowerCase().includes(lowerQuery));
8
+ }
9
+ /**
10
+ * Truncate file content to a maximum number of lines
11
+ */
12
+ export function truncateFileContent(content, maxLines = 500) {
13
+ const lines = content.split('\n');
14
+ if (lines.length <= maxLines) {
15
+ return content;
16
+ }
17
+ const truncated = lines.slice(0, maxLines).join('\n');
18
+ return `${truncated}\n\n... (truncated ${lines.length - maxLines} lines)`;
19
+ }
20
+ /**
21
+ * Format file for LLM context with proper delimiters
22
+ */
23
+ export function formatFileForContext(filePath, content) {
24
+ const ext = getFileExtension(filePath);
25
+ const language = getLanguageFromExtension(ext);
26
+ return `File: ${filePath}\n\`\`\`${language}\n${content}\n\`\`\`\n`;
27
+ }
28
+ /**
29
+ * Get file extension from path
30
+ */
31
+ export function getFileExtension(filePath) {
32
+ return path.extname(filePath).slice(1);
33
+ }
34
+ /**
35
+ * Map file extension to language identifier for syntax highlighting
36
+ */
37
+ function getLanguageFromExtension(ext) {
38
+ const languageMap = {
39
+ 'ts': 'typescript',
40
+ 'tsx': 'typescript',
41
+ 'js': 'javascript',
42
+ 'jsx': 'javascript',
43
+ 'py': 'python',
44
+ 'java': 'java',
45
+ 'cpp': 'cpp',
46
+ 'c': 'c',
47
+ 'cs': 'csharp',
48
+ 'go': 'go',
49
+ 'rs': 'rust',
50
+ 'rb': 'ruby',
51
+ 'php': 'php',
52
+ 'swift': 'swift',
53
+ 'kt': 'kotlin',
54
+ 'md': 'markdown',
55
+ 'json': 'json',
56
+ 'yaml': 'yaml',
57
+ 'yml': 'yaml',
58
+ 'xml': 'xml',
59
+ 'html': 'html',
60
+ 'css': 'css',
61
+ 'scss': 'scss',
62
+ 'sql': 'sql',
63
+ 'sh': 'bash',
64
+ 'bash': 'bash',
65
+ };
66
+ return languageMap[ext] || ext;
67
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kishlay42/moth-ai",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Local, LLM-agnostic code intelligence CLI",
5
5
  "main": "dist/index.js",
6
6
  "bin": {