@kishlay42/moth-ai 1.0.1

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.
@@ -0,0 +1,187 @@
1
+ // --- 5. Planning (Keep Existing) ---
2
+ export const TodoWriteTool = {
3
+ name: 'todo_write',
4
+ description: 'Add a new task to the plan or update an existing task status.',
5
+ parameters: {
6
+ type: 'object',
7
+ properties: {
8
+ action: { type: 'string', enum: ['add', 'update'], description: 'Action to perform.' },
9
+ text: { type: 'string', description: 'Text content of the task (required for add).' },
10
+ id: { type: 'string', description: 'ID of the task to update (required for update).' },
11
+ status: { type: 'string', enum: ['pending', 'in-progress', 'completed', 'failed'], description: 'New status.' }
12
+ },
13
+ required: ['action']
14
+ }
15
+ };
16
+ export const TodoReadTool = {
17
+ name: 'todo_read',
18
+ description: 'Read the current plan and task statuses.',
19
+ parameters: { type: 'object', properties: {} }
20
+ };
21
+ // --- 1. Filesystem Tools ---
22
+ export const ReadFileTool = {
23
+ name: 'read_file',
24
+ description: 'Read the contents of a file.',
25
+ parameters: {
26
+ type: 'object',
27
+ properties: {
28
+ path: { type: 'string', description: 'Path to the file.' }
29
+ },
30
+ required: ['path']
31
+ }
32
+ };
33
+ export const ListDirTool = {
34
+ name: 'list_dir',
35
+ description: 'List contents of a directory.',
36
+ parameters: {
37
+ type: 'object',
38
+ properties: {
39
+ path: { type: 'string', description: 'Directory path.' }
40
+ },
41
+ required: ['path']
42
+ }
43
+ };
44
+ export const CreateFileTool = {
45
+ name: 'create_file',
46
+ description: 'Create a new file (fails if exists).',
47
+ parameters: {
48
+ type: 'object',
49
+ properties: {
50
+ path: { type: 'string', description: 'Path for the new file.' },
51
+ content: { type: 'string', description: 'Initial content (optional).' }
52
+ },
53
+ required: ['path']
54
+ }
55
+ };
56
+ export const WriteFileTool = {
57
+ name: 'write_file',
58
+ description: 'Overwrite an entire file.',
59
+ parameters: {
60
+ type: 'object',
61
+ properties: {
62
+ path: { type: 'string', description: 'Path to the file.' },
63
+ content: { type: 'string', description: 'New content.' }
64
+ },
65
+ required: ['path', 'content']
66
+ }
67
+ };
68
+ export const EditFileTool = {
69
+ name: 'edit_file',
70
+ description: 'Modify a file using a Unified Diff.',
71
+ parameters: {
72
+ type: 'object',
73
+ properties: {
74
+ path: { type: 'string', description: 'Path to the file.' },
75
+ diff: { type: 'string', description: 'Unified Diff string to apply.' }
76
+ },
77
+ required: ['path', 'diff']
78
+ }
79
+ };
80
+ export const CreateDirTool = {
81
+ name: 'create_dir',
82
+ description: 'Create a new directory.',
83
+ parameters: {
84
+ type: 'object',
85
+ properties: {
86
+ path: { type: 'string', description: 'Directory path.' }
87
+ },
88
+ required: ['path']
89
+ }
90
+ };
91
+ // --- 2. Search & Discovery ---
92
+ export const SearchTextTool = {
93
+ name: 'search_text',
94
+ description: 'Search for text across project files.',
95
+ parameters: {
96
+ type: 'object',
97
+ properties: {
98
+ query: { type: 'string', description: 'Text or regex to search for.' },
99
+ path: { type: 'string', description: 'Specific path to limit search (optional).' }
100
+ },
101
+ required: ['query']
102
+ }
103
+ };
104
+ export const SearchFilesTool = {
105
+ name: 'search_files',
106
+ description: 'Find files by name or pattern.',
107
+ parameters: {
108
+ type: 'object',
109
+ properties: {
110
+ pattern: { type: 'string', description: 'Glob pattern or filename.' }
111
+ },
112
+ required: ['pattern']
113
+ }
114
+ };
115
+ // --- 3. Command Execution ---
116
+ export const RunCommandTool = {
117
+ name: 'run_command',
118
+ description: 'Execute a shell command.',
119
+ parameters: {
120
+ type: 'object',
121
+ properties: {
122
+ command: { type: 'string', description: 'Command to execute.' },
123
+ cwd: { type: 'string', description: 'Working directory (optional).' }
124
+ },
125
+ required: ['command']
126
+ }
127
+ };
128
+ // --- 4. Context Tools ---
129
+ export const ScanContextTool = {
130
+ name: 'scan_context',
131
+ description: 'Scan project structure and understand file hierarchy.',
132
+ parameters: {
133
+ type: 'object',
134
+ properties: {
135
+ root: { type: 'string', description: 'Root directory to scan (optional).' }
136
+ }
137
+ }
138
+ };
139
+ export const SummarizeFileTool = {
140
+ name: 'summarize_file',
141
+ description: 'Get a summary of a file (first 100 lines).',
142
+ parameters: {
143
+ type: 'object',
144
+ properties: {
145
+ path: { type: 'string', description: 'File path.' }
146
+ },
147
+ required: ['path']
148
+ }
149
+ };
150
+ // --- Extras (Git/Test/Format) ---
151
+ export const GitDiffTool = {
152
+ name: 'git_diff',
153
+ description: 'Show unstaged changes (git diff).',
154
+ parameters: { type: 'object', properties: {} }
155
+ };
156
+ export const GitCommitTool = {
157
+ name: 'git_commit',
158
+ description: 'Commit staged changes.',
159
+ parameters: {
160
+ type: 'object',
161
+ properties: {
162
+ message: { type: 'string', description: 'Commit message.' }
163
+ },
164
+ required: ['message']
165
+ }
166
+ };
167
+ export const TestRunTool = {
168
+ name: 'test_run',
169
+ description: 'Run project tests.',
170
+ parameters: {
171
+ type: 'object',
172
+ properties: {
173
+ command: { type: 'string', description: 'Specific test command (optional).' }
174
+ }
175
+ }
176
+ };
177
+ export const FormatFileTool = {
178
+ name: 'format_file',
179
+ description: 'Format a file (using prettier if available).',
180
+ parameters: {
181
+ type: 'object',
182
+ properties: {
183
+ path: { type: 'string', description: 'File path.' }
184
+ },
185
+ required: ['path']
186
+ }
187
+ };
@@ -0,0 +1,196 @@
1
+ import { ToolRegistry } from './registry.js';
2
+ import * as Defs from './definitions.js'; // Import all as Defs
3
+ import { Patcher } from '../editing/patcher.js';
4
+ import { ProjectScanner } from '../context/scanner.js';
5
+ import * as fs from 'fs/promises';
6
+ import * as path from 'path';
7
+ import { exec } from 'child_process';
8
+ import { promisify } from 'util';
9
+ const execAsync = promisify(exec);
10
+ export function createToolRegistry(root, todoManager, checkPermission) {
11
+ const registry = new ToolRegistry();
12
+ const patcher = new Patcher(root);
13
+ const scanner = new ProjectScanner(root);
14
+ // --- Middleware ---
15
+ const withPermission = (name, executor) => {
16
+ return async (args) => {
17
+ if (checkPermission) {
18
+ const response = await checkPermission(name, args);
19
+ if (!response.allowed) {
20
+ return response.feedback
21
+ ? `User denied permission with feedback: ${response.feedback}`
22
+ : "User denied permission.";
23
+ }
24
+ }
25
+ return executor(args);
26
+ };
27
+ };
28
+ // --- Implementations ---
29
+ // 5. Planning
30
+ registry.register(Defs.TodoWriteTool, async (args) => {
31
+ if (args.action === 'add')
32
+ todoManager.add(args.text);
33
+ if (args.action === 'update')
34
+ todoManager.updateStatus(args.id, args.status);
35
+ return "Todo updated.";
36
+ });
37
+ registry.register(Defs.TodoReadTool, async () => {
38
+ return JSON.stringify(todoManager.list(), null, 2);
39
+ });
40
+ // 1. Filesystem
41
+ registry.register(Defs.ReadFileTool, async (args) => {
42
+ const fullPath = path.join(root, args.path);
43
+ // Security: Prevent breaking out of root
44
+ if (!fullPath.startsWith(root))
45
+ return "Error: Access denied (outside root).";
46
+ try {
47
+ return await fs.readFile(fullPath, 'utf-8');
48
+ }
49
+ catch (e) {
50
+ return `Error reading file: ${e.message}`;
51
+ }
52
+ });
53
+ registry.register(Defs.ListDirTool, async (args) => {
54
+ const fullPath = path.join(root, args.path);
55
+ if (!fullPath.startsWith(root))
56
+ return "Error: Access denied.";
57
+ try {
58
+ const files = await fs.readdir(fullPath);
59
+ return files.join('\n');
60
+ }
61
+ catch (e) {
62
+ return `Error listing dir: ${e.message}`;
63
+ }
64
+ });
65
+ // GATED: Create File
66
+ registry.register(Defs.CreateFileTool, withPermission('create_file', async (args) => {
67
+ const fullPath = path.join(root, args.path);
68
+ if (!fullPath.startsWith(root))
69
+ return "Error: Access denied.";
70
+ try {
71
+ await fs.access(fullPath);
72
+ return "Error: File already exists. Use write_file to overwrite.";
73
+ }
74
+ catch {
75
+ // File doesn't exist, proceed
76
+ await fs.writeFile(fullPath, args.content || '');
77
+ return `File created at ${args.path}`;
78
+ }
79
+ }));
80
+ // GATED: Write File
81
+ registry.register(Defs.WriteFileTool, withPermission('write_file', async (args) => {
82
+ const fullPath = path.join(root, args.path);
83
+ if (!fullPath.startsWith(root))
84
+ return "Error: Access denied.";
85
+ await fs.writeFile(fullPath, args.content);
86
+ return `File written to ${args.path}`;
87
+ }));
88
+ // GATED: Edit File
89
+ registry.register(Defs.EditFileTool, withPermission('edit_file', async (args) => {
90
+ // Patcher handles safety/backups internally
91
+ const success = await patcher.applyPatch(args.path, args.diff);
92
+ return success ? "Patch applied successfully." : "Patch application failed (check context/backups).";
93
+ }));
94
+ // GATED: Create Dir
95
+ registry.register(Defs.CreateDirTool, withPermission('create_dir', async (args) => {
96
+ const fullPath = path.join(root, args.path);
97
+ if (!fullPath.startsWith(root))
98
+ return "Error: Access denied.";
99
+ await fs.mkdir(fullPath, { recursive: true });
100
+ return `Directory created: ${args.path}`;
101
+ }));
102
+ // 2. Search & Discovery
103
+ registry.register(Defs.SearchTextTool, async (args) => {
104
+ // Using grep for speed (assuming unix/git bash context or simple grep availability)
105
+ // Fallback: This is a hacky implementation, properly we should traverse.
106
+ // For now, let's use Scanner to get files and basic JS search if grep fails?
107
+ // Actually, let's trust child_process 'grep' or 'findstr' based on OS, OR implement JS search.
108
+ // JS Search is safer and cross-platform.
109
+ try {
110
+ const files = await scanner.scan(); // Get all files
111
+ const regex = new RegExp(args.query);
112
+ const results = [];
113
+ for (const file of files.slice(0, 50)) { // Limit to 50 for perf
114
+ const content = await fs.readFile(path.join(root, file), 'utf-8');
115
+ if (regex.test(content))
116
+ results.push(file);
117
+ }
118
+ return `Found in:\n${results.join('\n')}`;
119
+ }
120
+ catch (e) {
121
+ return `Search error: ${e.message}`;
122
+ }
123
+ });
124
+ registry.register(Defs.SearchFilesTool, async (args) => {
125
+ const files = await scanner.scan();
126
+ // Simple includes check
127
+ return files.filter(f => f.includes(args.pattern)).join('\n');
128
+ });
129
+ // 3. Command Execution - GATED
130
+ registry.register(Defs.RunCommandTool, withPermission('run_command', async (args) => {
131
+ try {
132
+ const { stdout, stderr } = await execAsync(args.command, { cwd: args.cwd || root });
133
+ return `STDOUT:\n${stdout}\nSTDERR:\n${stderr}`;
134
+ }
135
+ catch (e) {
136
+ return `Command failed: ${e.message}\nSTDOUT:\n${e.stdout}\nSTDERR:\n${e.stderr}`;
137
+ }
138
+ }));
139
+ // 4. Context
140
+ registry.register(Defs.ScanContextTool, async () => {
141
+ const files = await scanner.scan();
142
+ return `Project Files (${files.length}):\n${files.join('\n')}`;
143
+ });
144
+ registry.register(Defs.SummarizeFileTool, async (args) => {
145
+ const fullPath = path.join(root, args.path);
146
+ try {
147
+ const content = await fs.readFile(fullPath, 'utf-8');
148
+ const lines = content.split('\n').slice(0, 100);
149
+ return `Summary (first 100 lines):\n${lines.join('\n')}`;
150
+ }
151
+ catch (e) {
152
+ return `Error: ${e.message}`;
153
+ }
154
+ });
155
+ // 5. Extras
156
+ registry.register(Defs.GitDiffTool, async () => {
157
+ try {
158
+ const { stdout } = await execAsync('git diff', { cwd: root });
159
+ return stdout || "No changes.";
160
+ }
161
+ catch (e) {
162
+ return `Git error: ${e.message}`;
163
+ }
164
+ });
165
+ registry.register(Defs.GitCommitTool, withPermission('git_commit', async (args) => {
166
+ try {
167
+ // Add all? Or just commit? Let's assume user staged manually or we add all.
168
+ // Safety: Let's just commit what is staged.
169
+ const { stdout } = await execAsync(`git commit -m "${args.message}"`, { cwd: root });
170
+ return stdout;
171
+ }
172
+ catch (e) {
173
+ return `Commit error: ${e.message}`;
174
+ }
175
+ }));
176
+ registry.register(Defs.TestRunTool, withPermission('test_run', async (args) => {
177
+ const cmd = args.command || 'npm test';
178
+ try {
179
+ const { stdout, stderr } = await execAsync(cmd, { cwd: root });
180
+ return `Test Results:\n${stdout}\n${stderr}`;
181
+ }
182
+ catch (e) {
183
+ return `Tests failed: ${e.message}`;
184
+ }
185
+ }));
186
+ registry.register(Defs.FormatFileTool, withPermission('format_file', async (args) => {
187
+ try {
188
+ await execAsync(`npx prettier --write "${args.path}"`, { cwd: root });
189
+ return `Formatted ${args.path}`;
190
+ }
191
+ catch (e) {
192
+ return `Format error: ${e.message}`;
193
+ }
194
+ }));
195
+ return registry;
196
+ }
@@ -0,0 +1,21 @@
1
+ export class ToolRegistry {
2
+ tools = new Map();
3
+ register(definition, executor) {
4
+ this.tools.set(definition.name, { definition, executor });
5
+ }
6
+ getDefinitions() {
7
+ return Array.from(this.tools.values()).map(t => t.definition);
8
+ }
9
+ async execute(name, args) {
10
+ const tool = this.tools.get(name);
11
+ if (!tool) {
12
+ throw new Error(`Tool '${name}' not found.`);
13
+ }
14
+ try {
15
+ return await tool.executor(args);
16
+ }
17
+ catch (error) {
18
+ return `Error executing tool '${name}': ${error.message}`;
19
+ }
20
+ }
21
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/ui/App.js ADDED
@@ -0,0 +1,182 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from 'react';
3
+ import { Box, Text, useInput, Newline } from 'ink';
4
+ import * as os from 'os';
5
+ import { TodoManager } from '../planning/todoManager.js';
6
+ import { AgentOrchestrator } from '../agent/orchestrator.js';
7
+ import { createToolRegistry } from '../tools/factory.js';
8
+ import { findProjectRoot } from '../utils/paths.js';
9
+ import TextInput from 'ink-text-input';
10
+ import { WordMoth } from './components/WordMoth.js';
11
+ export const App = ({ command, args, todoManager: propTodoManager, username }) => {
12
+ const [messages, setMessages] = useState([]);
13
+ const [inputVal, setInputVal] = useState('');
14
+ const [status, setStatus] = useState('Ready');
15
+ // UX State
16
+ const [showWelcome, setShowWelcome] = useState(true);
17
+ const [isPaused, setIsPaused] = useState(false);
18
+ const [autopilot, setAutopilot] = useState(false);
19
+ const [isProcessing, setIsProcessing] = useState(false);
20
+ const [thinkingText, setThinkingText] = useState('Sauting...');
21
+ // Effect for cycling thinking text
22
+ useEffect(() => {
23
+ if (isProcessing) {
24
+ const words = ['Sauting...', 'Bubbling...', 'Cooking...', 'Chopping...', 'Simmering...', 'Whisking...', 'Seasoning...'];
25
+ const interval = setInterval(() => {
26
+ setThinkingText(words[Math.floor(Math.random() * words.length)]);
27
+ }, 800);
28
+ return () => clearInterval(interval);
29
+ }
30
+ }, [isProcessing]);
31
+ // Permission State
32
+ const [pendingPermission, setPendingPermission] = useState(null);
33
+ const [feedbackMode, setFeedbackMode] = useState(false);
34
+ const [permissionSelection, setPermissionSelection] = useState(0);
35
+ // Initialize TodoManager
36
+ const [todoManager] = useState(() => propTodoManager || new TodoManager());
37
+ // --- Run Command Logic ---
38
+ const client = args.client;
39
+ const initialPrompt = args.prompt;
40
+ const activeProfile = args.profile;
41
+ useEffect(() => {
42
+ if (initialPrompt && messages.length === 0) {
43
+ if (showWelcome)
44
+ setShowWelcome(false);
45
+ runAgent(initialPrompt);
46
+ }
47
+ }, [initialPrompt]);
48
+ const runAgent = async (userPrompt) => {
49
+ if (showWelcome)
50
+ setShowWelcome(false);
51
+ setIsProcessing(true);
52
+ // Create new user message
53
+ const userMsg = { role: 'user', content: userPrompt };
54
+ setMessages(prev => [...prev, userMsg]);
55
+ try {
56
+ const root = findProjectRoot() || process.cwd();
57
+ // Permission Callback
58
+ const checkPermission = async (toolName, args) => {
59
+ if (autopilot) {
60
+ return { allowed: true };
61
+ }
62
+ return new Promise((resolve) => {
63
+ setPendingPermission({
64
+ id: Date.now().toString(),
65
+ toolName,
66
+ args,
67
+ resolve: (response) => {
68
+ setPendingPermission(null);
69
+ resolve(response);
70
+ }
71
+ });
72
+ });
73
+ };
74
+ const registry = createToolRegistry(root, todoManager, checkPermission);
75
+ const orchestrator = new AgentOrchestrator({
76
+ model: client,
77
+ tools: registry.getDefinitions(),
78
+ maxSteps: 10
79
+ }, registry);
80
+ let finalAnswer = "";
81
+ // Pass accumulated history to the agent
82
+ // 'messages' state contains history prior to this turn.
83
+ // We do NOT include the pending userMsg in the 'history' arg because orchestrator.run
84
+ // treats the prompt separately. Or we can include it in history and pass empty prompt?
85
+ // Orchestrator.run logic: [System, ...history, CurrentPrompt]
86
+ // So we pass 'messages' (which excludes current UserPrompt yet in this render cycle,
87
+ // but wait, we called setMessages above).
88
+ // Actually, setMessages update is invisible in this render closure.
89
+ // So 'messages' here IS the history before this turn. Perfect.
90
+ for await (const step of orchestrator.run(userPrompt, messages)) {
91
+ if (isPaused)
92
+ break; // Basic pause handling (not perfect resonance)
93
+ if (step.thought) {
94
+ // Ambient status update
95
+ setStatus(step.thought);
96
+ }
97
+ if (step.toolCall) {
98
+ setStatus(`Act: ${step.toolCall.name}`);
99
+ }
100
+ if (step.finalAnswer)
101
+ finalAnswer = step.finalAnswer;
102
+ }
103
+ const assistantMsg = { role: 'assistant', content: finalAnswer || "Done." };
104
+ setMessages(prev => [...prev, assistantMsg]);
105
+ }
106
+ catch (e) {
107
+ setMessages(prev => [...prev, { role: 'assistant', content: `Error: ${e.message}` }]);
108
+ }
109
+ finally {
110
+ setStatus('Ready');
111
+ setIsProcessing(false);
112
+ }
113
+ };
114
+ useInput((input, key) => {
115
+ // ESC Pause
116
+ if (key.escape) {
117
+ setIsPaused(prev => !prev);
118
+ setStatus(prev => prev === 'Paused' ? 'Resumed' : 'Paused');
119
+ return;
120
+ }
121
+ // Permission Handling
122
+ if (pendingPermission) {
123
+ if (feedbackMode) {
124
+ if (key.return) {
125
+ pendingPermission.resolve({ allowed: false, feedback: inputVal });
126
+ setInputVal('');
127
+ setFeedbackMode(false);
128
+ }
129
+ else if (key.backspace || key.delete) {
130
+ setInputVal(prev => prev.slice(0, -1));
131
+ }
132
+ else {
133
+ setInputVal(prev => prev + input);
134
+ }
135
+ return;
136
+ }
137
+ // Arrow Key Navigation
138
+ if (key.upArrow) {
139
+ setPermissionSelection(prev => (prev - 1 + 3) % 3);
140
+ return;
141
+ }
142
+ if (key.downArrow) {
143
+ setPermissionSelection(prev => (prev + 1) % 3);
144
+ return;
145
+ }
146
+ if (key.return) {
147
+ if (permissionSelection === 0) {
148
+ pendingPermission.resolve({ allowed: true });
149
+ }
150
+ else if (permissionSelection === 1) {
151
+ setAutopilot(true);
152
+ pendingPermission.resolve({ allowed: true });
153
+ }
154
+ else if (permissionSelection === 2) {
155
+ setFeedbackMode(true);
156
+ }
157
+ return;
158
+ }
159
+ if (input === 'a' || input === 'A') {
160
+ // Yes - execute once
161
+ pendingPermission.resolve({ allowed: true });
162
+ }
163
+ else if (input === 'b' || input === 'B') {
164
+ // Yes - autopilot
165
+ setAutopilot(true);
166
+ pendingPermission.resolve({ allowed: true });
167
+ }
168
+ else if (input === 'c' || input === 'C') {
169
+ // Tell Moth what to do instead
170
+ setFeedbackMode(true);
171
+ }
172
+ return;
173
+ }
174
+ });
175
+ // --- 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) => {
177
+ if (val.trim() && !isProcessing) {
178
+ runAgent(val);
179
+ setInputVal('');
180
+ }
181
+ }, focus: !isProcessing && !pendingPermission })] }), _jsx(Box, { flexDirection: "row", justifyContent: "flex-end", children: _jsx(Text, { color: "gray", dimColor: true, children: activeProfile?.name }) })] }))] }));
182
+ };
@@ -0,0 +1,51 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Box, Text, useInput, Newline, useApp } from 'ink';
4
+ import { setActiveProfile, saveConfig } from '../config/configManager.js';
5
+ export const ProfileManager = ({ config: initialConfig, onSelect }) => {
6
+ const { exit } = useApp();
7
+ const [localConfig, setLocalConfig] = useState(initialConfig);
8
+ const [selectionIndex, setSelectionIndex] = useState(() => {
9
+ const idx = initialConfig.profiles.findIndex(p => p.name === initialConfig.activeProfile);
10
+ return idx >= 0 ? idx : 0;
11
+ });
12
+ const [message, setMessage] = useState('');
13
+ useInput((input, key) => {
14
+ // Ctrl+X to Exit
15
+ if (input === '\x18' || (key.ctrl && input === 'x')) {
16
+ exit();
17
+ // Only hard exit if we're not in a larger flow, but for now safe to exit app
18
+ if (!onSelect)
19
+ process.exit(0);
20
+ return;
21
+ }
22
+ if (key.upArrow) {
23
+ setSelectionIndex(prev => (prev - 1 + localConfig.profiles.length) % localConfig.profiles.length);
24
+ return;
25
+ }
26
+ if (key.downArrow) {
27
+ setSelectionIndex(prev => (prev + 1) % localConfig.profiles.length);
28
+ return;
29
+ }
30
+ if (key.return) {
31
+ const selectedProfile = localConfig.profiles[selectionIndex];
32
+ let newConfig = setActiveProfile(localConfig, selectedProfile.name);
33
+ saveConfig(newConfig);
34
+ setLocalConfig(newConfig);
35
+ if (onSelect) {
36
+ // Give a tiny visual feedback before unmounting?
37
+ // Or just instant. Instant is better for "flow".
38
+ onSelect(selectedProfile);
39
+ }
40
+ else {
41
+ setMessage(`Active profile set to '${selectedProfile.name}'`);
42
+ setTimeout(() => setMessage(''), 3000);
43
+ }
44
+ }
45
+ });
46
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "cyan", children: [_jsx(Text, { bold: true, children: "MOTH LLM PROFILES" }), _jsx(Text, { color: "green", children: "Hint: Press Enter to select new model or Ctrl+X to exit nav." }), _jsx(Newline, {}), localConfig.profiles.map((p, i) => {
47
+ const isSelected = i === selectionIndex;
48
+ const isActive = p.name === localConfig.activeProfile;
49
+ return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: isSelected ? "green" : undefined, bold: isSelected, children: isSelected ? "* " : " " }), _jsx(Text, { bold: isActive, color: isActive ? "green" : undefined, children: p.name }), _jsxs(Text, { children: [" (", p.provider, " / ", p.model, ")"] }), isActive && _jsx(Text, { color: "green", dimColor: true, children: " (active)" })] }, i));
50
+ }), message && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "green", children: message }) }))] }));
51
+ };