@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.
- package/README.md +0 -0
- package/dist/agent/orchestrator.js +97 -0
- package/dist/agent/types.js +1 -0
- package/dist/config/configManager.js +62 -0
- package/dist/config/keychain.js +20 -0
- package/dist/context/ignore.js +27 -0
- package/dist/context/manager.js +62 -0
- package/dist/context/scanner.js +41 -0
- package/dist/context/types.js +1 -0
- package/dist/editing/patcher.js +37 -0
- package/dist/index.js +390 -0
- package/dist/llm/claudeAdapter.js +47 -0
- package/dist/llm/cohereAdapter.js +42 -0
- package/dist/llm/factory.js +30 -0
- package/dist/llm/geminiAdapter.js +55 -0
- package/dist/llm/openAIAdapter.js +45 -0
- package/dist/llm/types.js +1 -0
- package/dist/planning/todoManager.js +23 -0
- package/dist/tools/definitions.js +187 -0
- package/dist/tools/factory.js +196 -0
- package/dist/tools/registry.js +21 -0
- package/dist/tools/types.js +1 -0
- package/dist/ui/App.js +182 -0
- package/dist/ui/ProfileManager.js +51 -0
- package/dist/ui/components/FlameLogo.js +40 -0
- package/dist/ui/components/WordFlame.js +10 -0
- package/dist/ui/components/WordMoth.js +10 -0
- package/dist/ui/wizards/LLMRemover.js +68 -0
- package/dist/ui/wizards/LLMWizard.js +149 -0
- package/dist/utils/paths.js +22 -0
- package/dist/utils/text.js +49 -0
- package/docs/architecture.md +63 -0
- package/docs/core_logic.md +53 -0
- package/docs/index.md +30 -0
- package/docs/llm_integration.md +49 -0
- package/docs/ui_components.md +44 -0
- package/package.json +70 -0
|
@@ -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
|
+
};
|