@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,40 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
// Palette mapping: Character -> { color: Hex, symbol: Char }
|
|
4
|
+
const PALETTE = {
|
|
5
|
+
// Deep Blue / Background blend
|
|
6
|
+
'1': { color: '#0f20a2', char: '░' },
|
|
7
|
+
// Outer Blue
|
|
8
|
+
'2': { color: '#0020ef', char: '▒' },
|
|
9
|
+
// Mid Blue
|
|
10
|
+
'3': { color: '#002dff', char: '▓' },
|
|
11
|
+
// Bright Blue
|
|
12
|
+
'4': { color: '#0088af', char: '█' },
|
|
13
|
+
// Cyan
|
|
14
|
+
'5': { color: '#00c7f7', char: '█' },
|
|
15
|
+
// Light Cyan
|
|
16
|
+
'6': { color: '#00ffff', char: '█' },
|
|
17
|
+
// White Core
|
|
18
|
+
'7': { color: '#ffffff', char: '█' },
|
|
19
|
+
};
|
|
20
|
+
const HD_FLAME_ART = [
|
|
21
|
+
" 333 ",
|
|
22
|
+
" 2333332 ",
|
|
23
|
+
" 233444332 ",
|
|
24
|
+
" 2334555554332 ",
|
|
25
|
+
" 234556666655432 ",
|
|
26
|
+
" 34567777777776543 ",
|
|
27
|
+
" 2456777777777776542 ",
|
|
28
|
+
" 235677766666667776532 ",
|
|
29
|
+
" 345677655555556776543 ",
|
|
30
|
+
" 23456765444444456765432 ",
|
|
31
|
+
" 23455654333333345655432 ",
|
|
32
|
+
" 12344543222222234544321 ",
|
|
33
|
+
" 11233432111111123433211 "
|
|
34
|
+
];
|
|
35
|
+
export const FlameLogo = () => {
|
|
36
|
+
return (_jsx(Box, { flexDirection: "column", marginRight: 2, children: HD_FLAME_ART.map((row, rowIndex) => (_jsx(Box, { children: row.split('').map((char, colIndex) => {
|
|
37
|
+
const pixel = PALETTE[char];
|
|
38
|
+
return (_jsx(Text, { color: pixel?.color || undefined, children: pixel ? pixel.char : ' ' }, `${rowIndex}-${colIndex}`));
|
|
39
|
+
}) }, rowIndex))) }));
|
|
40
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from 'ink';
|
|
3
|
+
import BigText from 'ink-big-text';
|
|
4
|
+
export const WordFlame = ({ text, big = false }) => {
|
|
5
|
+
if (big) {
|
|
6
|
+
return (_jsx(Text, { color: "#3EA0C3", children: _jsx(BigText, { text: text, font: "block", colors: ['#0192e5', '#0192e5'] }) }) // BigText needs specific handling or Text wrapper
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
return (_jsx(Text, { color: "#0192e5", children: text }));
|
|
10
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from 'ink';
|
|
3
|
+
import BigText from 'ink-big-text';
|
|
4
|
+
export const WordMoth = ({ text, big = false }) => {
|
|
5
|
+
if (big) {
|
|
6
|
+
return (_jsx(Text, { color: "#3EA0C3", children: _jsx(BigText, { text: text, font: "block", colors: ['#0192e5', '#0192e5'] }) }) // BigText needs specific handling or Text wrapper
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
return (_jsx(Text, { color: "#0192e5", children: text }));
|
|
10
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
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 { removeProfile, saveConfig } from '../../config/configManager.js';
|
|
5
|
+
import { deleteApiKey } from '../../config/keychain.js';
|
|
6
|
+
export const LLMRemover = ({ config: initialConfig, onExit }) => {
|
|
7
|
+
const { exit } = useApp();
|
|
8
|
+
const [localConfig, setLocalConfig] = useState(initialConfig);
|
|
9
|
+
const [selectionIndex, setSelectionIndex] = useState(0);
|
|
10
|
+
const [message, setMessage] = useState('');
|
|
11
|
+
const [confirming, setConfirming] = useState(null);
|
|
12
|
+
useInput(async (input, key) => {
|
|
13
|
+
// Escape or Ctrl+C/X to exit
|
|
14
|
+
if (key.escape || (key.ctrl && (input === 'c' || input === 'x'))) {
|
|
15
|
+
onExit();
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (localConfig.profiles.length === 0) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
// Navigation
|
|
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
|
+
const selectedProfile = localConfig.profiles[selectionIndex];
|
|
31
|
+
// Confirmation Mode
|
|
32
|
+
if (confirming) {
|
|
33
|
+
if (key.return || input.toLowerCase() === 'y') {
|
|
34
|
+
// Do the delete
|
|
35
|
+
const nameToRemove = selectedProfile.name;
|
|
36
|
+
let newConfig = removeProfile(localConfig, nameToRemove);
|
|
37
|
+
await deleteApiKey(nameToRemove); // Helper needs to be imported or mocked if not in configManager? It's in keychain.ts actually.
|
|
38
|
+
// Wait, removeProfile in configManager does cleaner logic but key deletion is separate usually?
|
|
39
|
+
// Checking imports... "deleteApiKey" is in keychain.ts.
|
|
40
|
+
// I need to import it.
|
|
41
|
+
saveConfig(newConfig);
|
|
42
|
+
setLocalConfig(newConfig);
|
|
43
|
+
setConfirming(null);
|
|
44
|
+
setMessage(`Profile '${nameToRemove}' removed.`);
|
|
45
|
+
setSelectionIndex(0);
|
|
46
|
+
setTimeout(() => setMessage(''), 3000);
|
|
47
|
+
}
|
|
48
|
+
else if (input.toLowerCase() === 'n' || key.escape) {
|
|
49
|
+
setConfirming(null);
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// Trigger Delete
|
|
54
|
+
if (key.return || key.delete || key.backspace) {
|
|
55
|
+
setConfirming(selectedProfile.name);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
// Need to fix imports for deleteApiKey which is in keychain, not configManager.
|
|
59
|
+
// I will assume I can import it from '../../config/keychain.js'.
|
|
60
|
+
if (localConfig.profiles.length === 0) {
|
|
61
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "red", children: [_jsx(Text, { color: "red", children: "No profiles found." }), _jsx(Text, { children: "Use \"moth llm add\" to create one." })] }));
|
|
62
|
+
}
|
|
63
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "red", children: [_jsx(Text, { bold: true, color: "red", children: "MOTH LLM REMOVER" }), _jsx(Text, { color: "yellow", children: "Hint: Select profile and press Enter to delete." }), _jsx(Newline, {}), confirming ? (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "red", padding: 1, children: [_jsx(Text, { bold: true, color: "white", backgroundColor: "red", children: " WARNING " }), _jsx(Text, { children: "Are you sure you want to permanently delete profile:" }), _jsxs(Text, { bold: true, children: ["'", confirming, "'?"] }), _jsx(Newline, {}), _jsx(Text, { bold: true, children: "[Y] Yes, Delete [N] No, Cancel" })] })) : (localConfig.profiles.map((p, i) => {
|
|
64
|
+
const isSelected = i === selectionIndex;
|
|
65
|
+
const isActive = p.name === localConfig.activeProfile;
|
|
66
|
+
return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: isSelected ? "red" : undefined, bold: isSelected, children: isSelected ? "❯ " : " " }), _jsx(Text, { bold: isActive, color: isActive ? "green" : (isSelected ? "red" : undefined), children: p.name }), _jsxs(Text, { children: [" (", p.provider, " / ", p.model, ")"] }), isActive && _jsx(Text, { color: "green", dimColor: true, children: " (active)" })] }, i));
|
|
67
|
+
})), message && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "red", children: message }) }))] }));
|
|
68
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
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
|
+
import SelectInput from 'ink-select-input';
|
|
5
|
+
import TextInput from 'ink-text-input';
|
|
6
|
+
// 1. Model Families
|
|
7
|
+
const FAMILIES = [
|
|
8
|
+
{ label: 'GPT (OpenAI)', value: 'gpt' },
|
|
9
|
+
{ label: 'Claude (Anthropic)', value: 'claude' },
|
|
10
|
+
{ label: 'Gemini (Google)', value: 'gemini' },
|
|
11
|
+
{ label: 'LLaMA (Meta)', value: 'llama' },
|
|
12
|
+
{ label: 'Mistral', value: 'mistral' },
|
|
13
|
+
{ label: 'Qwen', value: 'qwen' },
|
|
14
|
+
{ label: 'Phi', value: 'phi' },
|
|
15
|
+
{ label: 'DeepSeek', value: 'deepseek' },
|
|
16
|
+
{ label: 'Other / Custom', value: 'custom' },
|
|
17
|
+
];
|
|
18
|
+
// 2. Deployment Types
|
|
19
|
+
const DEPLOYMENTS = [
|
|
20
|
+
{ label: 'Local (On my machine)', value: 'local' },
|
|
21
|
+
{ label: 'Cloud (Hosted API)', value: 'cloud' },
|
|
22
|
+
];
|
|
23
|
+
// Suggested models per family
|
|
24
|
+
const FAMILY_MODELS = {
|
|
25
|
+
'gpt': ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-3.5-turbo'],
|
|
26
|
+
'claude': ['claude-3-5-sonnet-20240620', 'claude-3-opus-20240229', 'claude-3-haiku-20240307'],
|
|
27
|
+
'gemini': ['gemini-1.5-pro', 'gemini-1.5-flash', 'gemini-1.0-pro'],
|
|
28
|
+
'llama': ['llama3.1', 'llama3', 'llama2'],
|
|
29
|
+
'mistral': ['mistral', 'mixtral', 'codestral'],
|
|
30
|
+
'qwen': ['qwen2.5', 'qwen2'],
|
|
31
|
+
'phi': ['phi3.5', 'phi3'],
|
|
32
|
+
'deepseek': ['deepseek-coder-v2', 'deepseek-chat'],
|
|
33
|
+
};
|
|
34
|
+
export const LLMWizard = ({ onComplete, onCancel }) => {
|
|
35
|
+
const [step, setStep] = useState(1);
|
|
36
|
+
const [family, setFamily] = useState('');
|
|
37
|
+
const [deployment, setDeployment] = useState('');
|
|
38
|
+
const [model, setModel] = useState('');
|
|
39
|
+
const [isCustomModel, setIsCustomModel] = useState(false);
|
|
40
|
+
const [apiKey, setApiKey] = useState('');
|
|
41
|
+
const [baseUrl, setBaseUrl] = useState('');
|
|
42
|
+
// Resolution State
|
|
43
|
+
const [resolvedRuntime, setResolvedRuntime] = useState({ provider: 'openai-compatible', requiresKey: true, requiresUrl: false });
|
|
44
|
+
// --- STEP 1: FAMILY ---
|
|
45
|
+
const handleFamilySelect = (item) => {
|
|
46
|
+
setFamily(item.value);
|
|
47
|
+
setStep(2);
|
|
48
|
+
};
|
|
49
|
+
// --- STEP 2: DEPLOYMENT ---
|
|
50
|
+
const handleDeploymentSelect = (item) => {
|
|
51
|
+
const deployType = item.value;
|
|
52
|
+
setDeployment(deployType);
|
|
53
|
+
resolveRuntime(family, deployType);
|
|
54
|
+
};
|
|
55
|
+
const resolveRuntime = (fam, deploy) => {
|
|
56
|
+
// LOGIC RESOLUTION
|
|
57
|
+
if (deploy === 'local') {
|
|
58
|
+
setResolvedRuntime({
|
|
59
|
+
provider: 'ollama',
|
|
60
|
+
defaultBaseUrl: 'http://localhost:11434/v1',
|
|
61
|
+
requiresKey: false,
|
|
62
|
+
requiresUrl: false // Implicitly handles URL usually, or we can confirm it
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
// CLOUD
|
|
67
|
+
if (fam === 'gemini') {
|
|
68
|
+
setResolvedRuntime({ provider: 'gemini-native', requiresKey: true, requiresUrl: false });
|
|
69
|
+
}
|
|
70
|
+
else if (fam === 'claude') {
|
|
71
|
+
setResolvedRuntime({ provider: 'claude-native', requiresKey: true, requiresUrl: false });
|
|
72
|
+
}
|
|
73
|
+
else if (fam === 'gpt') {
|
|
74
|
+
// OpenAI standard
|
|
75
|
+
setResolvedRuntime({ provider: 'openai-compatible', requiresKey: true, requiresUrl: false });
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
// LLaMA, Mistral, etc on Cloud -> Generic OpenAI Compatible (Groq, etc)
|
|
79
|
+
setResolvedRuntime({ provider: 'openai-compatible', requiresKey: true, requiresUrl: true });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
setStep(3);
|
|
83
|
+
};
|
|
84
|
+
// --- STEP 3: MODEL VARIANT ---
|
|
85
|
+
const handleModelSelect = (item) => {
|
|
86
|
+
if (item.value === 'custom') {
|
|
87
|
+
setIsCustomModel(true);
|
|
88
|
+
setModel('');
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
setModel(item.value);
|
|
92
|
+
setStep(4);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const handleCustomModelSubmit = (val) => {
|
|
96
|
+
setModel(val);
|
|
97
|
+
setStep(4);
|
|
98
|
+
};
|
|
99
|
+
// --- STEP 4: AUTH / CREDENTIALS ---
|
|
100
|
+
// This step might be split if we need both Key and URL.
|
|
101
|
+
// Let's do Key first, then URL if needed.
|
|
102
|
+
const [subStep, setSubStep] = useState('key');
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (step === 4) {
|
|
105
|
+
if (resolvedRuntime.requiresKey) {
|
|
106
|
+
setSubStep('key');
|
|
107
|
+
}
|
|
108
|
+
else if (resolvedRuntime.requiresUrl) {
|
|
109
|
+
setSubStep('url');
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
// Nothing needed
|
|
113
|
+
setStep(5);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}, [step, resolvedRuntime]);
|
|
117
|
+
const handleKeySubmit = (val) => {
|
|
118
|
+
setApiKey(val);
|
|
119
|
+
if (resolvedRuntime.requiresUrl) {
|
|
120
|
+
setSubStep('url');
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
setStep(5);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
const handleUrlSubmit = (val) => {
|
|
127
|
+
setBaseUrl(val);
|
|
128
|
+
setStep(5);
|
|
129
|
+
};
|
|
130
|
+
// --- STEP 5: CONFIRM ---
|
|
131
|
+
useInput((input, key) => {
|
|
132
|
+
if (step === 5 && key.return) {
|
|
133
|
+
onComplete({
|
|
134
|
+
name: `${family}-${model}`.replace(/[^a-zA-Z0-9-_]/g, '-').toLowerCase(),
|
|
135
|
+
provider: resolvedRuntime.provider,
|
|
136
|
+
model: model,
|
|
137
|
+
apiKey: apiKey || undefined,
|
|
138
|
+
baseUrl: baseUrl || resolvedRuntime.defaultBaseUrl || undefined
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (input === 'c' && key.ctrl) {
|
|
142
|
+
onCancel();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
// RENDER HELPERS
|
|
146
|
+
const modelItems = (FAMILY_MODELS[family] || []).map(m => ({ label: m, value: m }));
|
|
147
|
+
modelItems.push({ label: 'Type Custom Model Name...', value: 'custom' });
|
|
148
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "magenta", width: 65, children: [_jsx(Text, { bold: true, color: "magenta", children: "\uD83D\uDD25 MOTH MODEL WIZARD" }), _jsx(Text, { color: "gray", children: "--------------------------------------------------" }), step === 1 && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: "cyan", children: "STEP 1: SELECT MODEL FAMILY" }), _jsx(Box, { marginTop: 1, children: _jsx(SelectInput, { items: FAMILIES, onSelect: handleFamilySelect }) })] })), step === 2 && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: "cyan", children: "STEP 2: SELECT DEPLOYMENT" }), _jsxs(Text, { color: "gray", children: ["Where will ", family, " run?"] }), _jsx(Box, { marginTop: 1, children: _jsx(SelectInput, { items: DEPLOYMENTS, onSelect: handleDeploymentSelect }) })] })), step === 3 && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: "cyan", children: "STEP 3: SELECT MODEL VARIANT" }), _jsxs(Text, { color: "gray", children: ["Runtime: ", resolvedRuntime.provider, " (", deployment, ")"] }), isCustomModel ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { children: "Enter specific model name:" }), _jsx(TextInput, { value: model, onChange: setModel, onSubmit: handleCustomModelSubmit })] })) : (_jsx(Box, { marginTop: 1, children: _jsx(SelectInput, { items: modelItems, onSelect: handleModelSelect }) }))] })), step === 4 && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: "cyan", children: "STEP 4: CREDENTIALS" }), subStep === 'key' && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: ["Enter API Key for ", family, " (", deployment, "):"] }), _jsx(TextInput, { value: apiKey, onChange: setApiKey, onSubmit: handleKeySubmit, mask: "*" })] })), subStep === 'url' && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { children: "Enter Base URL endpoint:" }), _jsx(TextInput, { value: baseUrl, onChange: setBaseUrl, onSubmit: handleUrlSubmit, placeholder: "https://api.groq.com/openai/v1" })] }))] })), step === 5 && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: "green", children: "STEP 5: CONFIRM RESOLUTION" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", borderStyle: "single", padding: 1, borderColor: "white", children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "Family:" }), " ", family] }), _jsxs(Text, { children: [_jsx(Text, { bold: true, children: "Deployment:" }), " ", deployment] }), _jsxs(Text, { children: [_jsx(Text, { bold: true, children: "Runtime:" }), " ", resolvedRuntime.provider] }), _jsxs(Text, { children: [_jsx(Text, { bold: true, children: "Model:" }), " ", model] }), _jsxs(Text, { children: [_jsx(Text, { bold: true, children: "Auth:" }), " ", apiKey ? '(Provided)' : '(None)'] }), _jsxs(Text, { children: [_jsx(Text, { bold: true, children: "Endpoint:" }), " ", baseUrl || resolvedRuntime.defaultBaseUrl || '(Default)'] })] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "green", bold: true, children: "Press [ENTER] to save configuration." }), _jsx(Text, { color: "red", children: "Press [Ctrl+C] to cancel." })] })] }))] }));
|
|
149
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
export const CONFIG_DIR = path.join(os.homedir(), '.moth');
|
|
5
|
+
export function ensureConfigDir() {
|
|
6
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
7
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function findProjectRoot(cwd = process.cwd()) {
|
|
11
|
+
let currentDir = cwd;
|
|
12
|
+
while (true) {
|
|
13
|
+
if (fs.existsSync(path.join(currentDir, 'package.json')) || fs.existsSync(path.join(currentDir, '.git'))) {
|
|
14
|
+
return currentDir;
|
|
15
|
+
}
|
|
16
|
+
const parentDir = path.dirname(currentDir);
|
|
17
|
+
if (parentDir === currentDir) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
currentDir = parentDir;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export function levenshtein(a, b) {
|
|
2
|
+
if (a.length === 0)
|
|
3
|
+
return b.length;
|
|
4
|
+
if (b.length === 0)
|
|
5
|
+
return a.length;
|
|
6
|
+
const matrix = [];
|
|
7
|
+
// increment along the first column of each row
|
|
8
|
+
let i;
|
|
9
|
+
for (i = 0; i <= b.length; i++) {
|
|
10
|
+
matrix[i] = [i];
|
|
11
|
+
}
|
|
12
|
+
// increment each column in the first row
|
|
13
|
+
let j;
|
|
14
|
+
for (j = 0; j <= a.length; j++) {
|
|
15
|
+
matrix[0][j] = j;
|
|
16
|
+
}
|
|
17
|
+
// Fill in the rest of the matrix
|
|
18
|
+
for (i = 1; i <= b.length; i++) {
|
|
19
|
+
for (j = 1; j <= a.length; j++) {
|
|
20
|
+
if (b.charAt(i - 1) == a.charAt(j - 1)) {
|
|
21
|
+
matrix[i][j] = matrix[i - 1][j - 1];
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // substitution
|
|
25
|
+
Math.min(matrix[i][j - 1] + 1, // insertion
|
|
26
|
+
matrix[i - 1][j] + 1 // deletion
|
|
27
|
+
));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return matrix[b.length][a.length];
|
|
32
|
+
}
|
|
33
|
+
export function findClosestCommand(input, commands) {
|
|
34
|
+
let closest = null;
|
|
35
|
+
let minDistance = Infinity;
|
|
36
|
+
for (const cmd of commands) {
|
|
37
|
+
const distance = levenshtein(input, cmd);
|
|
38
|
+
if (distance < minDistance) {
|
|
39
|
+
minDistance = distance;
|
|
40
|
+
closest = cmd;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Threshold: allow max distance of 2 for words len > 3, else 1
|
|
44
|
+
const threshold = input.length > 3 ? 2 : 1;
|
|
45
|
+
if (minDistance <= threshold) {
|
|
46
|
+
return closest;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# System Architecture
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
Moth is a local, LLM-agnostic code intelligence CLI. It acts as an orchestrator between the user, the filesystem, and various LLM providers.
|
|
5
|
+
|
|
6
|
+
## High-Level Architecture
|
|
7
|
+
|
|
8
|
+
```mermaid
|
|
9
|
+
graph TD
|
|
10
|
+
User[User] -->|Commands/Prompts| CLI[CLI Entry Point (index.ts)]
|
|
11
|
+
CLI --> UI[UI Layer (Ink)]
|
|
12
|
+
CLI --> Config[Config Manager]
|
|
13
|
+
UI --> App[App Component]
|
|
14
|
+
App --> Orch[Agent Orchestrator]
|
|
15
|
+
Orch --> Tools[Tool Registry]
|
|
16
|
+
Orch --> LLM[LLM Client]
|
|
17
|
+
|
|
18
|
+
subgraph Core Logic
|
|
19
|
+
Orch
|
|
20
|
+
Tools
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
subgraph Infrastructure
|
|
24
|
+
Config
|
|
25
|
+
LLM
|
|
26
|
+
FS[File System]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
Tools --> FS
|
|
30
|
+
LLM -->|API Calls| Cloud[Cloud Providers (OpenAI/Gemini/Claude)]
|
|
31
|
+
LLM -->|Local| Local[Ollama/LocalHost]
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Component Interaction
|
|
35
|
+
|
|
36
|
+
### Chat Session Flow
|
|
37
|
+
1. **Initialization**: `startChatSession()` in `index.ts` loads configuration and mounts the `App` component.
|
|
38
|
+
2. **Input**: User types a prompt.
|
|
39
|
+
3. **App State**: `App.tsx` captures the input and appends it to the `messages` history.
|
|
40
|
+
4. **Orchestration**: `App.tsx` calls `orchestrator.run(prompt, history)`.
|
|
41
|
+
5. **Agent Loop**:
|
|
42
|
+
* **System Prompt**: Constructed with available tools.
|
|
43
|
+
* **LLM Call**: History + System Prompt + User Prompt sent to LLM.
|
|
44
|
+
* **Reasoning**: LLM returns a JSON `AgentStep` (Thought + ToolCall/FinalAnswer).
|
|
45
|
+
* **Execution**: If ToolCall, `ToolRegistry` executes the tool (e.g., `write_to_file`).
|
|
46
|
+
* **Recursion**: Output is fed back to LLM until `finalAnswer` is reached.
|
|
47
|
+
6. **Render**: `App.tsx` updates the UI with the stream or final result.
|
|
48
|
+
|
|
49
|
+
### LLM Provider Resolution
|
|
50
|
+
The system uses a **Model-First** pattern to resolve providers:
|
|
51
|
+
|
|
52
|
+
```mermaid
|
|
53
|
+
sequenceDiagram
|
|
54
|
+
participant User
|
|
55
|
+
participant Wizard
|
|
56
|
+
participant Config
|
|
57
|
+
|
|
58
|
+
User->>Wizard: Select Family (e.g. LLaMA)
|
|
59
|
+
User->>Wizard: Select Deployment (Local)
|
|
60
|
+
Wizard->>Config: Resolve to 'ollama' provider
|
|
61
|
+
Wizard->>Config: Set URL to 'http://127.0.0.1:11434'
|
|
62
|
+
Wizard->>User: Confirm "Runtime: Ollama"
|
|
63
|
+
```
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Core Logic Modules
|
|
2
|
+
|
|
3
|
+
## Agent Orchestrator (`src/agent/orchestrator.ts`)
|
|
4
|
+
|
|
5
|
+
The `AgentOrchestrator` is the brain of Moth. It implements a ReAct-style loop to solve user tasks.
|
|
6
|
+
|
|
7
|
+
### Key Responsibilities
|
|
8
|
+
- **Conversation History**: It maintains `messages` state (User/Assistant), providing short-term memory.
|
|
9
|
+
- **Tool Execution**: It parses LLM responses and executes tools via `ToolRegistry`.
|
|
10
|
+
- **Loop Management**: It manages the `maxSteps` loop to prevent infinite recursion.
|
|
11
|
+
|
|
12
|
+
### Code Snippet: Memory Injection
|
|
13
|
+
```typescript
|
|
14
|
+
// src/agent/orchestrator.ts
|
|
15
|
+
async *run(prompt: string, history: LLMMessage[] = []): AsyncGenerator<AgentStep, string, unknown> {
|
|
16
|
+
// ...
|
|
17
|
+
const messages: LLMMessage[] = [
|
|
18
|
+
{ role: 'user', content: systemPrompt },
|
|
19
|
+
...history, // <--- Injection of past context
|
|
20
|
+
{ role: 'user', content: currentPrompt }
|
|
21
|
+
];
|
|
22
|
+
// ...
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Context Management (`src/context`)
|
|
27
|
+
|
|
28
|
+
Moth uses a proactive context system to understand the user's project.
|
|
29
|
+
|
|
30
|
+
### Project Scanner (`src/context/scanner.ts`)
|
|
31
|
+
- **Function**: Scans the current directory to identify project types (Node.js, Python, C++, etc.).
|
|
32
|
+
- **Heuristics**: Checks for marker files (`package.json`, `requirements.txt`, `CMakeLists.txt`).
|
|
33
|
+
|
|
34
|
+
## Configuration Manager (`src/config/configManager.ts`)
|
|
35
|
+
|
|
36
|
+
The config manager handles persistent state, including LLM profiles.
|
|
37
|
+
|
|
38
|
+
### Data Structure
|
|
39
|
+
```typescript
|
|
40
|
+
interface ActiveConfig {
|
|
41
|
+
activeProfile: string;
|
|
42
|
+
profiles: Record<string, LLMProfile>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface LLMProfile {
|
|
46
|
+
name: string;
|
|
47
|
+
provider: 'ollama' | 'openai-compatible' | 'gemini-native' | ...;
|
|
48
|
+
model: string;
|
|
49
|
+
apiBase?: string;
|
|
50
|
+
apiKeyLoc?: string; // Pointer to keychain, not the key itself
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
- **Security**: API keys are NEVER stored in `config.json`. They are managed via the system keychain (or mock implementation).
|
package/docs/index.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Moth Documentation 📜
|
|
2
|
+
|
|
3
|
+
Welcome to the comprehensive documentation for Moth, the Model-First Code Intelligence CLI.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
### 1. [System Architecture](./architecture.md)
|
|
8
|
+
* High-Level Overview
|
|
9
|
+
* Mermaid Diagrams (System & Workflows)
|
|
10
|
+
* Interaction Flows
|
|
11
|
+
|
|
12
|
+
### 2. [Core Logic](./core_logic.md)
|
|
13
|
+
* **Orchestrator**: The ReAct Agent Brain.
|
|
14
|
+
* **Context**: How Moth analyzes your project.
|
|
15
|
+
* **Config**: State management and security.
|
|
16
|
+
|
|
17
|
+
### 3. [UI Components](./ui_components.md)
|
|
18
|
+
* **Ink Architecture**: React-based TUI.
|
|
19
|
+
* **Wizards**: Interactive setup flows.
|
|
20
|
+
* **Components**: Reusable UI elements.
|
|
21
|
+
|
|
22
|
+
### 4. [LLM Integration](./llm_integration.md)
|
|
23
|
+
* **Factory Pattern**: Dynamic client creation.
|
|
24
|
+
* **Adapters**: OpenAI, Gemini, Claude, Ollama.
|
|
25
|
+
* **Model-First Design**: Resolution logic.
|
|
26
|
+
|
|
27
|
+
## Quick Links
|
|
28
|
+
|
|
29
|
+
* [Implementation Plan](../.gemini/antigravity/brain/0989b9c8-1bda-4a68-b9de-08e5d9c71f0f/implementation_plan.md) (Internal Planning)
|
|
30
|
+
* [Walkthrough](../.gemini/antigravity/brain/0989b9c8-1bda-4a68-b9de-08e5d9c71f0f/walkthrough.md) (User Guide)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# LLM Integration (`src/llm`)
|
|
2
|
+
|
|
3
|
+
Moth routes all model interactions through a unified adapter layer.
|
|
4
|
+
|
|
5
|
+
## Factory Pattern (`src/llm/factory.ts`)
|
|
6
|
+
|
|
7
|
+
The `createLLMClient` function instantiates the correct adapter based on the configuration.
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
export const createLLMClient = (config: ActiveConfig): LLMClient => {
|
|
11
|
+
switch (config.provider) {
|
|
12
|
+
case 'ollama':
|
|
13
|
+
case 'openai-compatible':
|
|
14
|
+
return new OpenAIAdapter(...);
|
|
15
|
+
case 'gemini-native':
|
|
16
|
+
return new GeminiAdapter(...);
|
|
17
|
+
case 'claude-native':
|
|
18
|
+
return new ClaudeAdapter(...);
|
|
19
|
+
// ...
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Adapter Interface (`src/llm/types.ts`)
|
|
25
|
+
|
|
26
|
+
All adapters must implement the `LLMClient` interface.
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
export interface LLMClient {
|
|
30
|
+
chat(messages: LLMMessage[]): Promise<string>;
|
|
31
|
+
chatStream(messages: LLMMessage[]): AsyncGenerator<string, void, unknown>;
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Supported Providers
|
|
36
|
+
|
|
37
|
+
### OpenAI Compatible (`src/llm/openAIAdapter.ts`)
|
|
38
|
+
- **Usage**: Used for OpenAI, Ollama, vLLM, DeepSeek, generic endpoints.
|
|
39
|
+
- **Configuration**: Requires `apiBase` (URL) and optionally `apiKey`.
|
|
40
|
+
- **Ollama Special Case**: Uses `http://127.0.0.1:11434/v1` as default base.
|
|
41
|
+
|
|
42
|
+
### Gemini Native (`src/llm/geminiAdapter.ts`)
|
|
43
|
+
- **Usage**: Google Gemini API.
|
|
44
|
+
- **Library**: Uses `@google/generative-ai` SDK.
|
|
45
|
+
- **Safety**: Configures safety settings to BLOCK_NONE for coding tasks.
|
|
46
|
+
|
|
47
|
+
### Claude Native (`src/llm/claudeAdapter.ts`)
|
|
48
|
+
- **Usage**: Anthropic Claude API.
|
|
49
|
+
- **Library**: Uses `@anthropic-ai/sdk`.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# UI Components (`src/ui`)
|
|
2
|
+
|
|
3
|
+
Moth uses [React Ink](https://github.com/vadimdemedes/ink) to render a Terminal User Interface (TUI).
|
|
4
|
+
|
|
5
|
+
## Main Application (`src/ui/App.tsx`)
|
|
6
|
+
|
|
7
|
+
The `App` component acts as the main view controller.
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
- **Chat Interface**: Renders a scrollable list of messages using `flexDirection="column"`.
|
|
11
|
+
- **Input Handling**: Captures user keystrokes for chat and control signals (Pause/Resume).
|
|
12
|
+
- **Permission Queue**: Intercepts tool execution requests and displays a "Permission Required" overlay.
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
// Structure of the App component
|
|
16
|
+
<Box flexDirection="column">
|
|
17
|
+
<Header />
|
|
18
|
+
<MessageList messages={messages} />
|
|
19
|
+
{pendingPermission && <PermissionOverlay request={pendingPermission} />}
|
|
20
|
+
<InputArea isProcessing={isProcessing} />
|
|
21
|
+
</Box>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Wizards
|
|
25
|
+
|
|
26
|
+
Wizards are self-contained interactive components used for configuration.
|
|
27
|
+
|
|
28
|
+
### LLM Add Wizard (`src/ui/wizards/LLMWizard.tsx`)
|
|
29
|
+
Implements the **Model-First** setup flow.
|
|
30
|
+
1. **Family Selection**: Selects model family (LLaMA, GPT, etc.).
|
|
31
|
+
2. **Deployment Selection**: Local vs Cloud.
|
|
32
|
+
3. **Credential Entry**: Conditional rendering of input fields.
|
|
33
|
+
4. **Confirmation**: Final summary view.
|
|
34
|
+
|
|
35
|
+
### LLM Remove Wizard (`src/ui/wizards/LLMRemover.tsx`)
|
|
36
|
+
- Displays a list of profiles.
|
|
37
|
+
- Uses styled selection (Green for active, Red for deletion target).
|
|
38
|
+
- Requires explicit 'y' confirmation to prevent accidents.
|
|
39
|
+
|
|
40
|
+
## Utilities
|
|
41
|
+
|
|
42
|
+
### WordMoth (`src/ui/components/WordMoth.tsx`)
|
|
43
|
+
- Renders the "MOTH" ascii art header.
|
|
44
|
+
- Supports gradient coloring (Blue/Cyan theme).
|