@react-frameui/loki-ai 1.0.0
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/LICENSE +21 -0
- package/README.md +158 -0
- package/dist/agents/devAgent.js +14 -0
- package/dist/agents/index.js +50 -0
- package/dist/agents/orchestrator.js +76 -0
- package/dist/cli/chat.js +142 -0
- package/dist/cli/doctor.js +67 -0
- package/dist/cli/tui.js +97 -0
- package/dist/context/fileContext.js +77 -0
- package/dist/context/projectContext.js +79 -0
- package/dist/core/agentRunner.js +156 -0
- package/dist/core/intentRouter.js +58 -0
- package/dist/index.js +74 -0
- package/dist/interfaces/whatsapp/client.js +185 -0
- package/dist/langchain/llmAdapter.js +44 -0
- package/dist/langchain/rag/index.js +71 -0
- package/dist/langchain/tools.js +74 -0
- package/dist/langgraph/refactorGraph.js +49 -0
- package/dist/llm/groq.js +46 -0
- package/dist/llm/ollama.js +112 -0
- package/dist/llm/providerFactory.js +14 -0
- package/dist/llm/types.js +2 -0
- package/dist/memory/memoryStore.js +57 -0
- package/dist/memory/semanticStore.js +88 -0
- package/dist/rag/indexer.js +83 -0
- package/dist/refactor/refactorEngine.js +35 -0
- package/dist/tools/fsTool.js +59 -0
- package/dist/tools/gitTool.js +54 -0
- package/dist/tools/mathTool.js +70 -0
- package/dist/tools/timeTool.js +71 -0
- package/dist/tools/toolRegistry.js +80 -0
- package/dist/utils/config.js +45 -0
- package/dist/utils/spinner.js +45 -0
- package/package.json +74 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.loadFileContext = loadFileContext;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const config_1 = require("../utils/config");
|
|
10
|
+
/**
|
|
11
|
+
* allowed extensions for text context.
|
|
12
|
+
*/
|
|
13
|
+
const ALLOWED_EXTENSIONS = [
|
|
14
|
+
'.ts', '.js', '.json', '.md', '.txt', '.html', '.css', '.py', '.java', '.c', '.cpp', '.h', '.go', '.rs', '.yml', '.yaml', '.sh'
|
|
15
|
+
];
|
|
16
|
+
/**
|
|
17
|
+
* Directories to ignore.
|
|
18
|
+
*/
|
|
19
|
+
const IGNORED_DIRS = [
|
|
20
|
+
'node_modules', '.git', 'dist', 'build', 'out', 'bin', '.idea', '.vscode', 'coverage'
|
|
21
|
+
];
|
|
22
|
+
/**
|
|
23
|
+
* files to ignore.
|
|
24
|
+
*/
|
|
25
|
+
const IGNORED_FILES = [
|
|
26
|
+
'package-lock.json', 'yarn.lock', '.DS_Store'
|
|
27
|
+
];
|
|
28
|
+
function isTextFile(filePath) {
|
|
29
|
+
const ext = path_1.default.extname(filePath).toLowerCase();
|
|
30
|
+
return ALLOWED_EXTENSIONS.includes(ext);
|
|
31
|
+
}
|
|
32
|
+
function shouldIgnore(entryName) {
|
|
33
|
+
return IGNORED_DIRS.includes(entryName) || IGNORED_FILES.includes(entryName) || entryName.startsWith('.');
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Recursively reads files from a directory or reads a single file.
|
|
37
|
+
*/
|
|
38
|
+
function readPath(targetPath, gatheredContent) {
|
|
39
|
+
try {
|
|
40
|
+
const stats = fs_1.default.statSync(targetPath);
|
|
41
|
+
if (stats.isDirectory()) {
|
|
42
|
+
const entries = fs_1.default.readdirSync(targetPath);
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
if (shouldIgnore(entry))
|
|
45
|
+
continue;
|
|
46
|
+
readPath(path_1.default.join(targetPath, entry), gatheredContent);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else if (stats.isFile()) {
|
|
50
|
+
if (isTextFile(targetPath) && !shouldIgnore(path_1.default.basename(targetPath))) {
|
|
51
|
+
const content = fs_1.default.readFileSync(targetPath, 'utf-8');
|
|
52
|
+
gatheredContent.push(`\n--- FILE: ${targetPath} ---\n${content}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
// Ignore unreadable files
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Loads context from provided file or directory paths.
|
|
62
|
+
* Truncates if it exceeds the limit.
|
|
63
|
+
*/
|
|
64
|
+
function loadFileContext(paths) {
|
|
65
|
+
const contentChunks = [];
|
|
66
|
+
for (const p of paths) {
|
|
67
|
+
const absPath = path_1.default.resolve(p);
|
|
68
|
+
if (fs_1.default.existsSync(absPath)) {
|
|
69
|
+
readPath(absPath, contentChunks);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const fullContent = contentChunks.join('\n');
|
|
73
|
+
if (fullContent.length > config_1.CONFIG.MAX_CONTEXT_CHARS) {
|
|
74
|
+
return fullContent.substring(0, config_1.CONFIG.MAX_CONTEXT_CHARS) + '\n...[Context Truncated]...';
|
|
75
|
+
}
|
|
76
|
+
return fullContent;
|
|
77
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getProjectContext = getProjectContext;
|
|
7
|
+
exports.formatProjectContext = formatProjectContext;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const gitTool_1 = require("../tools/gitTool");
|
|
11
|
+
function getDirectoryStructure(dir, depth = 0, maxDepth = 2) {
|
|
12
|
+
if (depth > maxDepth)
|
|
13
|
+
return '';
|
|
14
|
+
try {
|
|
15
|
+
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
16
|
+
const lines = [];
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === 'dist')
|
|
19
|
+
continue;
|
|
20
|
+
const indent = ' '.repeat(depth);
|
|
21
|
+
if (entry.isDirectory()) {
|
|
22
|
+
lines.push(`${indent}š ${entry.name}/`);
|
|
23
|
+
lines.push(getDirectoryStructure(path_1.default.join(dir, entry.name), depth + 1, maxDepth));
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
// Only show key files at top level or if small
|
|
27
|
+
if (depth === 0 || ['.json', '.md', '.ts', '.js'].includes(path_1.default.extname(entry.name))) {
|
|
28
|
+
lines.push(`${indent}š ${entry.name}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return lines.join('\n');
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function getProjectContext(cwd = process.cwd()) {
|
|
39
|
+
const summary = {
|
|
40
|
+
name: path_1.default.basename(cwd),
|
|
41
|
+
isGit: (0, gitTool_1.isGitRepo)(cwd),
|
|
42
|
+
gitStatus: 'N/A',
|
|
43
|
+
structure: getDirectoryStructure(cwd),
|
|
44
|
+
};
|
|
45
|
+
if (summary.isGit) {
|
|
46
|
+
summary.gitStatus = (0, gitTool_1.getGitStatus)(cwd);
|
|
47
|
+
}
|
|
48
|
+
const pkgPath = path_1.default.join(cwd, 'package.json');
|
|
49
|
+
if (fs_1.default.existsSync(pkgPath)) {
|
|
50
|
+
try {
|
|
51
|
+
const pkg = JSON.parse(fs_1.default.readFileSync(pkgPath, 'utf-8'));
|
|
52
|
+
summary.packageJson = {
|
|
53
|
+
name: pkg.name,
|
|
54
|
+
version: pkg.version,
|
|
55
|
+
scripts: pkg.scripts,
|
|
56
|
+
dependencies: pkg.dependencies ? Object.keys(pkg.dependencies) : [],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
catch { }
|
|
60
|
+
}
|
|
61
|
+
const readmePath = path_1.default.join(cwd, 'README.md');
|
|
62
|
+
if (fs_1.default.existsSync(readmePath)) {
|
|
63
|
+
const content = fs_1.default.readFileSync(readmePath, 'utf-8');
|
|
64
|
+
summary.readmePreview = content.substring(0, 500).replace(/\n/g, ' ') + '...';
|
|
65
|
+
}
|
|
66
|
+
return summary;
|
|
67
|
+
}
|
|
68
|
+
function formatProjectContext(ctx) {
|
|
69
|
+
return `
|
|
70
|
+
PROJECT CONTEXT:
|
|
71
|
+
- Name: ${ctx.name}
|
|
72
|
+
- Git: ${ctx.isGit ? 'Yes' : 'No'}
|
|
73
|
+
- Git Status: ${ctx.gitStatus ? ctx.gitStatus.substring(0, 100).replace(/\n/g, ', ') : 'Clean'}
|
|
74
|
+
- Structure:
|
|
75
|
+
${ctx.structure}
|
|
76
|
+
- Package: ${ctx.packageJson ? JSON.stringify(ctx.packageJson, null, 2) : 'N/A'}
|
|
77
|
+
- Readme: ${ctx.readmePreview || 'N/A'}
|
|
78
|
+
`.trim();
|
|
79
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.agentRunner = agentRunner;
|
|
4
|
+
const providerFactory_1 = require("../llm/providerFactory");
|
|
5
|
+
const index_1 = require("../agents/index");
|
|
6
|
+
const memoryStore_1 = require("../memory/memoryStore");
|
|
7
|
+
const fileContext_1 = require("../context/fileContext");
|
|
8
|
+
const intentRouter_1 = require("./intentRouter");
|
|
9
|
+
const toolRegistry_1 = require("../tools/toolRegistry");
|
|
10
|
+
const semanticStore_1 = require("../memory/semanticStore");
|
|
11
|
+
const projectContext_1 = require("../context/projectContext");
|
|
12
|
+
/**
|
|
13
|
+
* Main orchestration layer for LOKI.
|
|
14
|
+
*/
|
|
15
|
+
async function agentRunner(userMessage, options = {}) {
|
|
16
|
+
// --- 0. FAST INTENT ROUTING ---
|
|
17
|
+
const intent = await (0, intentRouter_1.routeIntent)(userMessage);
|
|
18
|
+
if (intent.handled) {
|
|
19
|
+
return intent.result || "Done.";
|
|
20
|
+
}
|
|
21
|
+
if (options.signal?.aborted)
|
|
22
|
+
throw new Error('Aborted');
|
|
23
|
+
// --- 1. PREPARE CONTEXT ---
|
|
24
|
+
const projectCtx = (0, projectContext_1.getProjectContext)();
|
|
25
|
+
const projectBlock = `\n=== PROJECT CONTEXT ===\n${(0, projectContext_1.formatProjectContext)(projectCtx)}\n=======================\n`;
|
|
26
|
+
let semanticBlock = '';
|
|
27
|
+
if (options.useSemantic !== false) {
|
|
28
|
+
const memories = await (0, semanticStore_1.retrieveRelevantMemory)(userMessage);
|
|
29
|
+
if (memories.length > 0) {
|
|
30
|
+
semanticBlock = `\n=== RELEVANT MEMORIES ===\n${memories.join('\n')}\n=========================\n`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
let historyBlock = '';
|
|
34
|
+
if (options.useMemory !== false) {
|
|
35
|
+
const history = (0, memoryStore_1.loadMemory)();
|
|
36
|
+
if (history.length > 0) {
|
|
37
|
+
const h = history.map(getItem => `${getItem.role.toUpperCase()}: ${getItem.content}`).join('\n');
|
|
38
|
+
historyBlock = `\n=== CONVERSATION HISTORY ===\n${h}\n============================\n`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
let fileBlock = '';
|
|
42
|
+
if (options.fileContextPaths && options.fileContextPaths.length > 0) {
|
|
43
|
+
const fileContent = (0, fileContext_1.loadFileContext)(options.fileContextPaths);
|
|
44
|
+
if (fileContent) {
|
|
45
|
+
fileBlock = `\n=== FILE CONTEXT ===\n${fileContent}\n====================\n`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// --- 2. BUILD PROMPT ---
|
|
49
|
+
const systemPrompt = (0, index_1.getSystemPrompt)(options.agent);
|
|
50
|
+
let fullPrompt;
|
|
51
|
+
if (options.isChat) {
|
|
52
|
+
// Chat mode (WhatsApp) - cleaner, conversational responses
|
|
53
|
+
const chatSystemPrompt = `You are LOKI, a friendly AI assistant chatting on WhatsApp.
|
|
54
|
+
|
|
55
|
+
CRITICAL RULES:
|
|
56
|
+
1. NEVER output JSON, code blocks (\`\`\`), or any programming syntax
|
|
57
|
+
2. NEVER mention "tools", "commands", or technical implementation details
|
|
58
|
+
3. Respond in plain, natural language only
|
|
59
|
+
4. Be conversational, friendly, and concise
|
|
60
|
+
5. Keep responses under 300 characters
|
|
61
|
+
6. If asked about files/folders/system tasks, explain you can only help with those in the full CLI version
|
|
62
|
+
7. For chat, focus on conversation, advice, information, and friendly assistance
|
|
63
|
+
|
|
64
|
+
Remember: You are chatting casually. No technical output allowed.`;
|
|
65
|
+
fullPrompt = `${chatSystemPrompt}
|
|
66
|
+
|
|
67
|
+
User: ${userMessage}
|
|
68
|
+
Assistant:`;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// Full mode (CLI) - with tools and project context
|
|
72
|
+
// System Context - Real user info
|
|
73
|
+
const os = require('os');
|
|
74
|
+
const systemContext = `
|
|
75
|
+
=== SYSTEM CONTEXT ===
|
|
76
|
+
User: ${os.userInfo().username}
|
|
77
|
+
Home Directory: ${os.homedir()}
|
|
78
|
+
Desktop Path: ${os.homedir()}/Desktop
|
|
79
|
+
Current Working Directory: ${process.cwd()}
|
|
80
|
+
OS: ${os.platform()}
|
|
81
|
+
======================
|
|
82
|
+
`;
|
|
83
|
+
const toolsBlock = `
|
|
84
|
+
=== AVAILABLE TOOLS ===
|
|
85
|
+
You can use tools by outputting a JSON block:
|
|
86
|
+
\`\`\`json
|
|
87
|
+
{ "tool": "tool_name", "args": { ... } }
|
|
88
|
+
\`\`\`
|
|
89
|
+
${(0, toolRegistry_1.getToolSchemas)()}
|
|
90
|
+
=======================
|
|
91
|
+
If you use a tool, output ONLY the JSON block first. I will give you the result.
|
|
92
|
+
Then you can answer the user.
|
|
93
|
+
`;
|
|
94
|
+
fullPrompt = `
|
|
95
|
+
${systemPrompt}
|
|
96
|
+
|
|
97
|
+
${systemContext}
|
|
98
|
+
${projectBlock}
|
|
99
|
+
${semanticBlock}
|
|
100
|
+
${fileBlock}
|
|
101
|
+
${toolsBlock}
|
|
102
|
+
${historyBlock}
|
|
103
|
+
|
|
104
|
+
User: ${userMessage}
|
|
105
|
+
Assistant:`;
|
|
106
|
+
}
|
|
107
|
+
// --- 3. EXECUTE ---
|
|
108
|
+
if (options.signal?.aborted)
|
|
109
|
+
throw new Error('Aborted');
|
|
110
|
+
const provider = (0, providerFactory_1.getProvider)(options.provider);
|
|
111
|
+
// Initial pass (Non-streaming to catch JSON better, or standard)
|
|
112
|
+
// We use generated promise wrapper to allow abort
|
|
113
|
+
let initialResponse = await provider.generate(fullPrompt, undefined, options.signal);
|
|
114
|
+
// Check for JSON tool call
|
|
115
|
+
const toolRegex = /```json\s*(\{.*"tool":.*\})\s*```/s;
|
|
116
|
+
const match = initialResponse.match(toolRegex);
|
|
117
|
+
if (match) {
|
|
118
|
+
try {
|
|
119
|
+
const toolCall = JSON.parse(match[1]);
|
|
120
|
+
if (options.onToken)
|
|
121
|
+
options.onToken(`[Executing ${toolCall.tool}...] `);
|
|
122
|
+
const toolResult = await (0, toolRegistry_1.executeToolCall)(toolCall.tool, toolCall.args);
|
|
123
|
+
fullPrompt += `\n${initialResponse}\n\nTool Output: ${toolResult}\n\nAssistant (Interpreting result):`;
|
|
124
|
+
// Final Response
|
|
125
|
+
if (options.stream && provider.streamGenerate && options.onToken) {
|
|
126
|
+
const final = await provider.streamGenerate(fullPrompt, options.onToken, options.signal);
|
|
127
|
+
await saveInteraction(userMessage, final, options);
|
|
128
|
+
return final.trim();
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
const final = await provider.generate(fullPrompt, undefined, options.signal);
|
|
132
|
+
await saveInteraction(userMessage, final, options);
|
|
133
|
+
return final.trim();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
// failed tool parse
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Pass through initial response if no tool
|
|
141
|
+
if (options.stream && options.onToken) {
|
|
142
|
+
options.onToken(initialResponse);
|
|
143
|
+
}
|
|
144
|
+
await saveInteraction(userMessage, initialResponse, options);
|
|
145
|
+
return initialResponse.trim();
|
|
146
|
+
}
|
|
147
|
+
async function saveInteraction(user, assistant, options) {
|
|
148
|
+
if (options.useMemory !== false) {
|
|
149
|
+
const timestamp = new Date().toISOString();
|
|
150
|
+
(0, memoryStore_1.appendMemory)({ timestamp, role: 'user', content: user });
|
|
151
|
+
(0, memoryStore_1.appendMemory)({ timestamp, role: 'assistant', content: assistant });
|
|
152
|
+
}
|
|
153
|
+
if (options.useSemantic !== false) {
|
|
154
|
+
await (0, semanticStore_1.storeSemanticMemory)(user, 'chat');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.routeIntent = routeIntent;
|
|
4
|
+
const timeTool_1 = require("../tools/timeTool");
|
|
5
|
+
const mathTool_1 = require("../tools/mathTool");
|
|
6
|
+
const fsTool_1 = require("../tools/fsTool");
|
|
7
|
+
/**
|
|
8
|
+
* Central routing logic.
|
|
9
|
+
* Decides if a query can be answered locally by a tool or needs the LLM.
|
|
10
|
+
*/
|
|
11
|
+
async function routeIntent(message) {
|
|
12
|
+
const cleanMsg = message.trim();
|
|
13
|
+
const lowerMsg = cleanMsg.toLowerCase();
|
|
14
|
+
// --- 1. TIME & DATE ---
|
|
15
|
+
if (lowerMsg.includes('time') && (lowerMsg.includes('what') || lowerMsg.includes('current') || lowerMsg.includes('now') || lowerMsg.includes('in'))) {
|
|
16
|
+
// Simple extraction of "in <location>"
|
|
17
|
+
const match = lowerMsg.match(/in\s+([a-zA-Z]+)/);
|
|
18
|
+
const timezone = match ? match[1] : undefined;
|
|
19
|
+
return { handled: true, result: `It's ${(0, timeTool_1.getCurrentTime)(timezone)}` };
|
|
20
|
+
}
|
|
21
|
+
if (lowerMsg.includes('date') && (lowerMsg.includes('what') || lowerMsg.includes('current') || lowerMsg.includes('today'))) {
|
|
22
|
+
const match = lowerMsg.match(/in\s+([a-zA-Z]+)/);
|
|
23
|
+
const timezone = match ? match[1] : undefined;
|
|
24
|
+
return { handled: true, result: `Today is ${(0, timeTool_1.getCurrentDate)(timezone)}` };
|
|
25
|
+
}
|
|
26
|
+
// --- 2. FILE SYSTEM ---
|
|
27
|
+
if (lowerMsg.startsWith('list files') || lowerMsg.startsWith('ls ') || lowerMsg.startsWith('show files')) {
|
|
28
|
+
// extract path
|
|
29
|
+
// e.g. "list files in src" -> "src"
|
|
30
|
+
// heuristic: last word or specific "in <path>"
|
|
31
|
+
let targetPath = '.';
|
|
32
|
+
if (lowerMsg.includes(' in ')) {
|
|
33
|
+
targetPath = cleanMsg.split(' in ')[1];
|
|
34
|
+
}
|
|
35
|
+
else if (lowerMsg.startsWith('ls ')) {
|
|
36
|
+
targetPath = cleanMsg.substring(3).trim();
|
|
37
|
+
if (!targetPath)
|
|
38
|
+
targetPath = '.';
|
|
39
|
+
}
|
|
40
|
+
const files = (0, fsTool_1.listFiles)(targetPath);
|
|
41
|
+
return { handled: true, result: files.join(', ') };
|
|
42
|
+
}
|
|
43
|
+
if ((lowerMsg.startsWith('read ') || lowerMsg.startsWith('cat ')) && !lowerMsg.includes('what')) {
|
|
44
|
+
let targetFile = '';
|
|
45
|
+
if (lowerMsg.startsWith('read '))
|
|
46
|
+
targetFile = cleanMsg.substring(5).trim();
|
|
47
|
+
else if (lowerMsg.startsWith('cat '))
|
|
48
|
+
targetFile = cleanMsg.substring(4).trim();
|
|
49
|
+
return { handled: true, result: (0, fsTool_1.readFile)(targetFile) };
|
|
50
|
+
}
|
|
51
|
+
// --- 3. MATH ---
|
|
52
|
+
// If it looks like a math expression and NOT natural language
|
|
53
|
+
if ((0, mathTool_1.isMathExpression)(cleanMsg)) {
|
|
54
|
+
return { handled: true, result: (0, mathTool_1.evaluateMath)(cleanMsg) };
|
|
55
|
+
}
|
|
56
|
+
// Default: Not handled by tools
|
|
57
|
+
return { handled: false };
|
|
58
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const commander_1 = require("commander");
|
|
7
|
+
const chat_1 = require("./cli/chat");
|
|
8
|
+
const doctor_1 = require("./cli/doctor");
|
|
9
|
+
const refactorEngine_1 = require("./refactor/refactorEngine");
|
|
10
|
+
const indexer_1 = require("./rag/indexer");
|
|
11
|
+
const config_1 = require("./utils/config");
|
|
12
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
13
|
+
const program = new commander_1.Command();
|
|
14
|
+
const ASCII_ART = `
|
|
15
|
+
āāā āāāāāāā āāā āāāāāā
|
|
16
|
+
āāā āāāāāāāāāāāā āāāāāāā
|
|
17
|
+
āāā āāā āāāāāāāāāā āāā
|
|
18
|
+
āāā āāā āāāāāāāāāā āāā
|
|
19
|
+
āāāāāāāāāāāāāāāāāāāā āāāāāā
|
|
20
|
+
āāāāāāāā āāāāāāā āāā āāāāāā
|
|
21
|
+
`;
|
|
22
|
+
program
|
|
23
|
+
.name('loki')
|
|
24
|
+
.description(`${chalk_1.default.green.bold(ASCII_ART)}\nLocal-first AI developer platform.`)
|
|
25
|
+
.version(config_1.CONFIG.VERSION);
|
|
26
|
+
program
|
|
27
|
+
.command('chat [message]')
|
|
28
|
+
.description('Chat with LOKI.')
|
|
29
|
+
.option('-p, --provider <name>', 'model provider: ollama or groq', 'ollama')
|
|
30
|
+
.option('-a, --agent <name>', 'agent persona: dev, explain, refactor', 'dev')
|
|
31
|
+
.option('--no-memory', 'disable persistent memory')
|
|
32
|
+
.option('--no-stream', 'disable streaming output')
|
|
33
|
+
.option('-f, --file <path>', 'add a single file to context')
|
|
34
|
+
.option('-d, --dir <path>', 'add a directory files to context')
|
|
35
|
+
.action(async (message, options) => {
|
|
36
|
+
await (0, chat_1.chatCommand)(message, options);
|
|
37
|
+
});
|
|
38
|
+
program
|
|
39
|
+
.command('doctor')
|
|
40
|
+
.description('System health check.')
|
|
41
|
+
.action(async () => {
|
|
42
|
+
await (0, doctor_1.doctorCommand)();
|
|
43
|
+
});
|
|
44
|
+
program
|
|
45
|
+
.command('workflow <instruction>')
|
|
46
|
+
.description('Run a multi-agent workflow (e.g. refactor).')
|
|
47
|
+
.action(async (instruction) => {
|
|
48
|
+
await (0, refactorEngine_1.engineCommand)(instruction);
|
|
49
|
+
});
|
|
50
|
+
program
|
|
51
|
+
.command('index')
|
|
52
|
+
.description('Index current repository for RAG.')
|
|
53
|
+
.action(async () => {
|
|
54
|
+
await (0, indexer_1.indexRepository)();
|
|
55
|
+
});
|
|
56
|
+
program
|
|
57
|
+
.command('server')
|
|
58
|
+
.description('Start the WhatsApp webhook server.')
|
|
59
|
+
.action(async () => {
|
|
60
|
+
// Lazy load to avoid require cost if not used
|
|
61
|
+
const { startWhatsAppClient } = require('./interfaces/whatsapp/client');
|
|
62
|
+
await startWhatsAppClient();
|
|
63
|
+
});
|
|
64
|
+
// Check if any command was provided
|
|
65
|
+
if (process.argv.length <= 2) {
|
|
66
|
+
// No args passed. Launch TUI.
|
|
67
|
+
// Need to import dynamically or at top.
|
|
68
|
+
// We can just rely on the import we will add.
|
|
69
|
+
const { startLoop } = require('./cli/tui');
|
|
70
|
+
startLoop();
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
program.parse(process.argv);
|
|
74
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.startWhatsAppClient = startWhatsAppClient;
|
|
40
|
+
const baileys_1 = __importStar(require("@whiskeysockets/baileys"));
|
|
41
|
+
const qrcode_terminal_1 = __importDefault(require("qrcode-terminal"));
|
|
42
|
+
const config_1 = require("../../utils/config");
|
|
43
|
+
const agentRunner_1 = require("../../core/agentRunner");
|
|
44
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
45
|
+
const path_1 = __importDefault(require("path"));
|
|
46
|
+
const fs_1 = __importDefault(require("fs"));
|
|
47
|
+
const pino_1 = __importDefault(require("pino"));
|
|
48
|
+
const authDir = path_1.default.join(config_1.CONFIG.CONFIG_DIR, 'whatsapp_baileys_auth');
|
|
49
|
+
async function startWhatsAppClient() {
|
|
50
|
+
console.log(chalk_1.default.blue.bold('š± LOKI WhatsApp Client'));
|
|
51
|
+
console.log(chalk_1.default.gray('Connecting...\n'));
|
|
52
|
+
// Ensure auth directory exists
|
|
53
|
+
if (!fs_1.default.existsSync(authDir)) {
|
|
54
|
+
fs_1.default.mkdirSync(authDir, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
const { state, saveCreds } = await (0, baileys_1.useMultiFileAuthState)(authDir);
|
|
57
|
+
// Silent logger to reduce noise
|
|
58
|
+
const logger = (0, pino_1.default)({ level: 'silent' });
|
|
59
|
+
const sock = (0, baileys_1.default)({
|
|
60
|
+
auth: state,
|
|
61
|
+
browser: ['LOKI', 'Desktop', '1.0.0'],
|
|
62
|
+
logger: logger
|
|
63
|
+
});
|
|
64
|
+
// Save credentials on update
|
|
65
|
+
sock.ev.on('creds.update', saveCreds);
|
|
66
|
+
// Connection updates
|
|
67
|
+
sock.ev.on('connection.update', (update) => {
|
|
68
|
+
const { connection, lastDisconnect, qr } = update;
|
|
69
|
+
// Display QR code manually
|
|
70
|
+
if (qr) {
|
|
71
|
+
console.log(chalk_1.default.yellow('\nš· Scan this QR code with WhatsApp:\n'));
|
|
72
|
+
qrcode_terminal_1.default.generate(qr, { small: true });
|
|
73
|
+
}
|
|
74
|
+
if (connection === 'close') {
|
|
75
|
+
const statusCode = lastDisconnect?.error?.output?.statusCode;
|
|
76
|
+
const shouldReconnect = statusCode !== baileys_1.DisconnectReason.loggedOut;
|
|
77
|
+
console.log(chalk_1.default.red('Connection closed.'));
|
|
78
|
+
if (shouldReconnect) {
|
|
79
|
+
console.log(chalk_1.default.yellow('Reconnecting...'));
|
|
80
|
+
startWhatsAppClient();
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
console.log(chalk_1.default.red('Logged out. Delete ~/.loki/whatsapp_baileys_auth to re-link.'));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else if (connection === 'open') {
|
|
87
|
+
console.log(chalk_1.default.green.bold('\nā
LOKI WhatsApp Client Connected!'));
|
|
88
|
+
console.log(chalk_1.default.cyan('Listening for messages. Send /ping to test.\n'));
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
// Message handler
|
|
92
|
+
sock.ev.on('messages.upsert', async (m) => {
|
|
93
|
+
const msg = m.messages[0];
|
|
94
|
+
if (!msg)
|
|
95
|
+
return;
|
|
96
|
+
// Skip if no message content
|
|
97
|
+
if (!msg.message)
|
|
98
|
+
return;
|
|
99
|
+
// Skip status broadcasts
|
|
100
|
+
if (msg.key.remoteJid === 'status@broadcast')
|
|
101
|
+
return;
|
|
102
|
+
// Get message text
|
|
103
|
+
const text = msg.message.conversation ||
|
|
104
|
+
msg.message.extendedTextMessage?.text ||
|
|
105
|
+
'';
|
|
106
|
+
if (!text.trim())
|
|
107
|
+
return;
|
|
108
|
+
const fromMe = msg.key.fromMe || false;
|
|
109
|
+
const sender = msg.key.remoteJid || '';
|
|
110
|
+
// Check if this is a self-chat (messaging yourself)
|
|
111
|
+
const isSelfChat = sender.includes('@lid') || sender === msg.key.participant;
|
|
112
|
+
console.log(chalk_1.default.magenta(`[MSG] ${sender}: "${text}" (fromMe: ${fromMe}, selfChat: ${isSelfChat})`));
|
|
113
|
+
// CRITICAL: Only respond to incoming messages OR self-chat
|
|
114
|
+
if (fromMe && !isSelfChat) {
|
|
115
|
+
console.log(chalk_1.default.gray(' [Skipped - outgoing message to someone else]'));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// Skip LOKI's own responses (double safety)
|
|
119
|
+
if (text.startsWith('š§ ') || text.startsWith('š') || text.startsWith('ā ļø')) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const cmd = text.trim().toLowerCase();
|
|
123
|
+
// Command: /ping
|
|
124
|
+
if (cmd === '/ping') {
|
|
125
|
+
console.log(chalk_1.default.yellow('ā Processing /ping'));
|
|
126
|
+
await sock.sendMessage(sender, { text: 'š Pong! LOKI is online.' });
|
|
127
|
+
console.log(chalk_1.default.green('ā Sent pong'));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// Command: /help
|
|
131
|
+
if (cmd === '/help') {
|
|
132
|
+
await sock.sendMessage(sender, {
|
|
133
|
+
text: `š§ *LOKI Help*\n⢠Ask anything in natural language\n⢠/ping - Check if online\n⢠/help - Show this help`
|
|
134
|
+
});
|
|
135
|
+
console.log(chalk_1.default.green('ā Sent help'));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// AI Processing for ALL messages (including self for testing)
|
|
139
|
+
console.log(chalk_1.default.yellow('ā Processing with LOKI AI...'));
|
|
140
|
+
// Show "typing..." indicator to user
|
|
141
|
+
try {
|
|
142
|
+
await sock.presenceSubscribe(sender);
|
|
143
|
+
await sock.sendPresenceUpdate('composing', sender);
|
|
144
|
+
console.log(chalk_1.default.gray(' [typing indicator sent]'));
|
|
145
|
+
}
|
|
146
|
+
catch (e) {
|
|
147
|
+
console.log(chalk_1.default.gray(' [typing indicator failed]'));
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
// Use FULL agent mode with tools enabled
|
|
151
|
+
const rawResponse = await (0, agentRunner_1.agentRunner)(text, {
|
|
152
|
+
useMemory: true,
|
|
153
|
+
stream: false,
|
|
154
|
+
isChat: false // Enable tools!
|
|
155
|
+
});
|
|
156
|
+
// Clean up response - remove code blocks and JSON artifacts
|
|
157
|
+
let cleanResponse = rawResponse
|
|
158
|
+
.replace(/```json[\s\S]*?```/g, '') // Remove JSON blocks
|
|
159
|
+
.replace(/```[\s\S]*?```/g, '') // Remove any code blocks
|
|
160
|
+
.replace(/\{[^{}]*"tool"[^{}]*\}/g, '') // Remove inline tool JSON
|
|
161
|
+
.replace(/\n{3,}/g, '\n\n') // Collapse multiple newlines
|
|
162
|
+
.trim();
|
|
163
|
+
// If response is empty after cleanup, provide fallback
|
|
164
|
+
if (!cleanResponse) {
|
|
165
|
+
cleanResponse = "I've processed your request. Is there anything else you'd like to know?";
|
|
166
|
+
}
|
|
167
|
+
// Stop typing indicator
|
|
168
|
+
try {
|
|
169
|
+
await sock.sendPresenceUpdate('paused', sender);
|
|
170
|
+
}
|
|
171
|
+
catch (e) { }
|
|
172
|
+
await sock.sendMessage(sender, { text: `š§ ${cleanResponse}` });
|
|
173
|
+
console.log(chalk_1.default.green('ā Sent AI response'));
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
// Stop typing indicator on error too
|
|
177
|
+
try {
|
|
178
|
+
await sock.sendPresenceUpdate('paused', sender);
|
|
179
|
+
}
|
|
180
|
+
catch (err) { }
|
|
181
|
+
console.error(chalk_1.default.red('AI Error:'), e.message);
|
|
182
|
+
await sock.sendMessage(sender, { text: `ā ļø Error: ${e.message}` });
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LokiChatModel = void 0;
|
|
4
|
+
const chat_models_1 = require("@langchain/core/language_models/chat_models");
|
|
5
|
+
const providerFactory_1 = require("../llm/providerFactory");
|
|
6
|
+
const config_1 = require("../utils/config");
|
|
7
|
+
class LokiChatModel extends chat_models_1.SimpleChatModel {
|
|
8
|
+
constructor(fields) {
|
|
9
|
+
super({});
|
|
10
|
+
this.providerName = fields?.providerName || config_1.CONFIG.DEFAULT_PROVIDER;
|
|
11
|
+
this.provider = (0, providerFactory_1.getProvider)(this.providerName);
|
|
12
|
+
}
|
|
13
|
+
_llmType() {
|
|
14
|
+
return 'loki_chat_model';
|
|
15
|
+
}
|
|
16
|
+
async _call(messages, options, runManager) {
|
|
17
|
+
const prompt = this.messagesToPrompt(messages);
|
|
18
|
+
if (this.provider.streamGenerate) {
|
|
19
|
+
// If provider supports streaming, we should probably prefer _stream
|
|
20
|
+
// but SimpleChatModel logic separates them.
|
|
21
|
+
// However, if we are just calling generates, we can assume non-stream here
|
|
22
|
+
// or we can consume the stream and concatenate.
|
|
23
|
+
// Let's just use generate for _call.
|
|
24
|
+
}
|
|
25
|
+
// We pass an abort signal if runManager has one?
|
|
26
|
+
// runManager doesn't expose signal directly usually, but it's in options usually.
|
|
27
|
+
// For MVP, simple call.
|
|
28
|
+
return await this.provider.generate(prompt);
|
|
29
|
+
}
|
|
30
|
+
// Helper to convert LangChain messages to a single string prompt
|
|
31
|
+
// Since our providers currently expect a string string (Mistral style usually).
|
|
32
|
+
messagesToPrompt(messages) {
|
|
33
|
+
return messages.map(m => {
|
|
34
|
+
if (m._getType() === 'system')
|
|
35
|
+
return `System: ${m.content}\n`;
|
|
36
|
+
if (m._getType() === 'human')
|
|
37
|
+
return `User: ${m.content}\n`;
|
|
38
|
+
if (m._getType() === 'ai')
|
|
39
|
+
return `Assistant: ${m.content}\n`;
|
|
40
|
+
return `${m.content}\n`;
|
|
41
|
+
}).join('\n') + "\nAssistant:";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
exports.LokiChatModel = LokiChatModel;
|