@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
package/README.md
ADDED
|
Binary file
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export class AgentOrchestrator {
|
|
2
|
+
config;
|
|
3
|
+
state;
|
|
4
|
+
tools;
|
|
5
|
+
constructor(config, registry) {
|
|
6
|
+
this.config = config;
|
|
7
|
+
this.tools = registry;
|
|
8
|
+
this.state = {
|
|
9
|
+
history: [],
|
|
10
|
+
maxSteps: config.maxSteps || 10
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
async *run(prompt, history = []) {
|
|
14
|
+
let currentPrompt = prompt;
|
|
15
|
+
for (let i = 0; i < this.state.maxSteps; i++) {
|
|
16
|
+
// Construct system prompt with tools
|
|
17
|
+
const systemPrompt = this.buildSystemPrompt();
|
|
18
|
+
// Full context: System -> History -> Current State
|
|
19
|
+
const messages = [
|
|
20
|
+
{ 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
|
|
21
|
+
...history,
|
|
22
|
+
{ role: 'user', content: currentPrompt }
|
|
23
|
+
];
|
|
24
|
+
// This is a simplified Mock for the ReAct loop to start with
|
|
25
|
+
// In reality, we need to handle the LLM raw output, parse "Thought" and "Tool Call"
|
|
26
|
+
// For now, we will rely on a Structured Output or strict parsing if the Provider supports it.
|
|
27
|
+
// Since we are using Gemini, we can ask for JSON mode or specific formatting.
|
|
28
|
+
const responseText = await this.callLLM(messages);
|
|
29
|
+
// Parse response
|
|
30
|
+
let step;
|
|
31
|
+
try {
|
|
32
|
+
step = this.parseResponse(responseText);
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
yield { thought: "Failed to parse LLM response. Retrying...", toolOutput: `Error: ${e}` };
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
this.state.history.push(step);
|
|
39
|
+
yield step;
|
|
40
|
+
if (step.finalAnswer) {
|
|
41
|
+
return step.finalAnswer;
|
|
42
|
+
}
|
|
43
|
+
if (step.toolCall) {
|
|
44
|
+
// Execute tool
|
|
45
|
+
const result = await this.executeTool(step.toolCall.name, step.toolCall.arguments);
|
|
46
|
+
step.toolOutput = result;
|
|
47
|
+
// Re-feed result to LLM
|
|
48
|
+
currentPrompt = `Tool '${step.toolCall.name}' returned: ${result}`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return "Max steps reached.";
|
|
52
|
+
}
|
|
53
|
+
buildSystemPrompt() {
|
|
54
|
+
const toolDefs = this.tools.getDefinitions().map(t => `${t.name}: ${t.description} Params: ${JSON.stringify(t.parameters)}`).join('\n');
|
|
55
|
+
return `You are Moth, an intelligent CLI coding assistant.
|
|
56
|
+
You have access to the following tools:
|
|
57
|
+
${toolDefs}
|
|
58
|
+
|
|
59
|
+
IMPORTANT GUIDELINES:
|
|
60
|
+
1. For general questions, explanations, or code snippets that don't need to be saved, use "finalAnswer".
|
|
61
|
+
2. Do NOT use "write_to_file" unless the user explicitly asks to save a file or implies a persistent change.
|
|
62
|
+
3. If the user asks for "Hello World code", just show it in the explanation (finalAnswer). Do NOT create a file for it.
|
|
63
|
+
4. Be concise and helpful.
|
|
64
|
+
|
|
65
|
+
Format your response exactly as a JSON object:
|
|
66
|
+
{
|
|
67
|
+
"thought": "your reasoning",
|
|
68
|
+
"toolCall": { "name": "tool_name", "arguments": { ... } }
|
|
69
|
+
}
|
|
70
|
+
OR if you are done/replying:
|
|
71
|
+
{
|
|
72
|
+
"thought": "reasoning",
|
|
73
|
+
"finalAnswer": "your response/code/explanation"
|
|
74
|
+
}
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
async callLLM(messages) {
|
|
78
|
+
// Direct integration with LLM Client
|
|
79
|
+
// This assumes Client has a simple chat interface returning string
|
|
80
|
+
// We might need to adjust LLMClient interface to support non-streaming one-off calls
|
|
81
|
+
// Placeholder: We will need to implement a 'generate' method on LLMClient
|
|
82
|
+
// or collect the stream.
|
|
83
|
+
let fullText = "";
|
|
84
|
+
for await (const chunk of this.config.model.chatStream(messages)) {
|
|
85
|
+
fullText += chunk;
|
|
86
|
+
}
|
|
87
|
+
return fullText;
|
|
88
|
+
}
|
|
89
|
+
parseResponse(text) {
|
|
90
|
+
// Clean markdown code blocks if present
|
|
91
|
+
const jsonText = text.replace(/```json/g, '').replace(/```/g, '').trim();
|
|
92
|
+
return JSON.parse(jsonText);
|
|
93
|
+
}
|
|
94
|
+
async executeTool(name, args) {
|
|
95
|
+
return await this.tools.execute(name, args);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
import { CONFIG_DIR, ensureConfigDir } from '../utils/paths.js';
|
|
5
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'profiles.yaml');
|
|
6
|
+
const DEFAULT_CONFIG = {
|
|
7
|
+
profiles: [],
|
|
8
|
+
};
|
|
9
|
+
export function loadConfig() {
|
|
10
|
+
ensureConfigDir();
|
|
11
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
12
|
+
saveConfig(DEFAULT_CONFIG);
|
|
13
|
+
return DEFAULT_CONFIG;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const content = fs.readFileSync(CONFIG_FILE, 'utf8');
|
|
17
|
+
const parsed = yaml.load(content);
|
|
18
|
+
// ensure structure matches
|
|
19
|
+
return {
|
|
20
|
+
profiles: parsed?.profiles || [],
|
|
21
|
+
activeProfile: parsed?.activeProfile,
|
|
22
|
+
username: parsed?.username
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
// console.error('Failed to load config, using default', e);
|
|
27
|
+
// ^ Silence for now or use UI error in future
|
|
28
|
+
return DEFAULT_CONFIG;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function saveConfig(config) {
|
|
32
|
+
ensureConfigDir();
|
|
33
|
+
fs.writeFileSync(CONFIG_FILE, yaml.dump(config), 'utf8');
|
|
34
|
+
}
|
|
35
|
+
export function addProfile(config, profile) {
|
|
36
|
+
const existingIndex = config.profiles.findIndex(p => p.name === profile.name);
|
|
37
|
+
if (existingIndex >= 0) {
|
|
38
|
+
config.profiles[existingIndex] = profile;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
config.profiles.push(profile);
|
|
42
|
+
}
|
|
43
|
+
return config;
|
|
44
|
+
}
|
|
45
|
+
export function removeProfile(config, name) {
|
|
46
|
+
config.profiles = config.profiles.filter(p => p.name !== name);
|
|
47
|
+
if (config.activeProfile === name) {
|
|
48
|
+
config.activeProfile = undefined;
|
|
49
|
+
}
|
|
50
|
+
return config;
|
|
51
|
+
}
|
|
52
|
+
export function setActiveProfile(config, name) {
|
|
53
|
+
const exists = config.profiles.some(p => p.name === name);
|
|
54
|
+
if (exists) {
|
|
55
|
+
config.activeProfile = name;
|
|
56
|
+
}
|
|
57
|
+
return config;
|
|
58
|
+
}
|
|
59
|
+
export function setUsername(config, username) {
|
|
60
|
+
config.username = username;
|
|
61
|
+
return config;
|
|
62
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import keytar from 'keytar';
|
|
2
|
+
const SERVICE_NAME = 'moth-cli';
|
|
3
|
+
const LEGACY_SERVICE_NAME = 'saute-cli';
|
|
4
|
+
export async function setApiKey(profileName, key) {
|
|
5
|
+
await keytar.setPassword(SERVICE_NAME, profileName, key);
|
|
6
|
+
}
|
|
7
|
+
export async function getApiKey(profileName) {
|
|
8
|
+
let key = await keytar.getPassword(SERVICE_NAME, profileName);
|
|
9
|
+
if (!key) {
|
|
10
|
+
// Try legacy service and migrate if found
|
|
11
|
+
key = await keytar.getPassword(LEGACY_SERVICE_NAME, profileName);
|
|
12
|
+
if (key) {
|
|
13
|
+
await keytar.setPassword(SERVICE_NAME, profileName, key);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return key;
|
|
17
|
+
}
|
|
18
|
+
export async function deleteApiKey(profileName) {
|
|
19
|
+
return keytar.deletePassword(SERVICE_NAME, profileName);
|
|
20
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import ignore from 'ignore';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
export class IgnoreManager {
|
|
5
|
+
ig = ignore();
|
|
6
|
+
constructor(root) {
|
|
7
|
+
this.loadIgnoreFile(root);
|
|
8
|
+
// Always ignore .git and node_modules
|
|
9
|
+
this.ig.add(['.git', 'node_modules', '.moth', 'dist', 'coverage']);
|
|
10
|
+
}
|
|
11
|
+
loadIgnoreFile(root) {
|
|
12
|
+
const ignorePath = path.join(root, '.gitignore');
|
|
13
|
+
if (fs.existsSync(ignorePath)) {
|
|
14
|
+
try {
|
|
15
|
+
const content = fs.readFileSync(ignorePath, 'utf8');
|
|
16
|
+
this.ig.add(content);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
console.warn('Failed to load .gitignore:', error);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
shouldIgnore(filePath) {
|
|
24
|
+
// ignore package expects relative paths
|
|
25
|
+
return this.ig.ignores(filePath);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { ProjectScanner } from './scanner.js';
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
export class ContextManager {
|
|
5
|
+
scanner;
|
|
6
|
+
root;
|
|
7
|
+
constructor(root) {
|
|
8
|
+
this.root = root;
|
|
9
|
+
this.scanner = new ProjectScanner(root);
|
|
10
|
+
}
|
|
11
|
+
async gather(request) {
|
|
12
|
+
const filePaths = await this.scanner.scan();
|
|
13
|
+
const files = [];
|
|
14
|
+
// Simple Scoring:
|
|
15
|
+
// 1. Exact filename match (1.0)
|
|
16
|
+
// 2. Query terms in path (0.5)
|
|
17
|
+
// 3. Default (0.1)
|
|
18
|
+
// Normalize query terms
|
|
19
|
+
const terms = request.query.toLowerCase().split(/\s+/);
|
|
20
|
+
for (const filePath of filePaths) {
|
|
21
|
+
let score = 0.1;
|
|
22
|
+
const lowerPath = filePath.toLowerCase();
|
|
23
|
+
const basename = path.basename(lowerPath);
|
|
24
|
+
if (terms.some(t => basename === t)) {
|
|
25
|
+
score = 1.0;
|
|
26
|
+
}
|
|
27
|
+
else if (terms.some(t => lowerPath.includes(t))) {
|
|
28
|
+
score = 0.5;
|
|
29
|
+
}
|
|
30
|
+
// Basic Tiering Logic (Placeholder)
|
|
31
|
+
// If score > 0.8 => Full
|
|
32
|
+
// If score > 0.4 => Summary (but we don't have summarizer yet, so Path)
|
|
33
|
+
// Else => Path
|
|
34
|
+
let tier = 'path';
|
|
35
|
+
let content;
|
|
36
|
+
if (score >= 0.8) {
|
|
37
|
+
tier = 'full';
|
|
38
|
+
try {
|
|
39
|
+
// Limit file read size for safety
|
|
40
|
+
content = await fs.readFile(path.join(this.root, filePath), 'utf8');
|
|
41
|
+
// Truncate if too huge? (TODO)
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
console.warn(`Failed to read ${filePath}`, e);
|
|
45
|
+
tier = 'path';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
files.push({
|
|
49
|
+
path: filePath,
|
|
50
|
+
relevance: score,
|
|
51
|
+
tier,
|
|
52
|
+
content
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
// Sort by relevance
|
|
56
|
+
files.sort((a, b) => b.relevance - a.relevance);
|
|
57
|
+
return {
|
|
58
|
+
files,
|
|
59
|
+
totalTokens: 0 // Placeholder
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { IgnoreManager } from './ignore.js';
|
|
4
|
+
export class ProjectScanner {
|
|
5
|
+
root;
|
|
6
|
+
ignoreManager;
|
|
7
|
+
constructor(root) {
|
|
8
|
+
this.root = root;
|
|
9
|
+
this.ignoreManager = new IgnoreManager(root);
|
|
10
|
+
}
|
|
11
|
+
async scan() {
|
|
12
|
+
const files = [];
|
|
13
|
+
await this.scanDir('', files);
|
|
14
|
+
return files;
|
|
15
|
+
}
|
|
16
|
+
async scanDir(relativeDir, fileList) {
|
|
17
|
+
const fullDir = path.join(this.root, relativeDir);
|
|
18
|
+
// Check if directory itself is ignored
|
|
19
|
+
if (relativeDir && this.ignoreManager.shouldIgnore(relativeDir)) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const entries = await fs.readdir(fullDir, { withFileTypes: true });
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
const relativePath = path.join(relativeDir, entry.name);
|
|
26
|
+
if (this.ignoreManager.shouldIgnore(relativePath)) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (entry.isDirectory()) {
|
|
30
|
+
await this.scanDir(relativePath, fileList);
|
|
31
|
+
}
|
|
32
|
+
else if (entry.isFile()) {
|
|
33
|
+
fileList.push(relativePath);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
console.warn(`Failed to scan directory ${fullDir}:`, error);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as Diff from 'diff';
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
export class Patcher {
|
|
5
|
+
root;
|
|
6
|
+
constructor(root) {
|
|
7
|
+
this.root = root;
|
|
8
|
+
}
|
|
9
|
+
async applyPatch(filePath, patchContent) {
|
|
10
|
+
const fullPath = path.join(this.root, filePath);
|
|
11
|
+
try {
|
|
12
|
+
const originalContent = await fs.readFile(fullPath, 'utf8');
|
|
13
|
+
// Safety: Create backup
|
|
14
|
+
await this.createBackup(filePath, originalContent);
|
|
15
|
+
// Apply patch
|
|
16
|
+
const patchedContent = Diff.applyPatch(originalContent, patchContent);
|
|
17
|
+
if (patchedContent === false) {
|
|
18
|
+
console.error(`Failed to apply patch to ${filePath}. Context mismatch.`);
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
await fs.writeFile(fullPath, patchedContent, 'utf8');
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
console.error(`Error patching ${filePath}:`, error);
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async createBackup(filePath, content) {
|
|
30
|
+
const backupDir = path.join(this.root, '.moth', 'backups');
|
|
31
|
+
await fs.mkdir(backupDir, { recursive: true });
|
|
32
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
33
|
+
const safeName = filePath.replace(/[\\/]/g, '_');
|
|
34
|
+
const backupPath = path.join(backupDir, `${safeName}_${timestamp}.bak`);
|
|
35
|
+
await fs.writeFile(backupPath, content, 'utf8');
|
|
36
|
+
}
|
|
37
|
+
}
|