@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 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
+ }