linguclaw 0.4.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 +161 -0
- package/dist/agent-system.d.ts +196 -0
- package/dist/agent-system.d.ts.map +1 -0
- package/dist/agent-system.js +738 -0
- package/dist/agent-system.js.map +1 -0
- package/dist/alphabeta.d.ts +54 -0
- package/dist/alphabeta.d.ts.map +1 -0
- package/dist/alphabeta.js +193 -0
- package/dist/alphabeta.js.map +1 -0
- package/dist/browser.d.ts +62 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +224 -0
- package/dist/browser.js.map +1 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +565 -0
- package/dist/cli.js.map +1 -0
- package/dist/code-parser.d.ts +39 -0
- package/dist/code-parser.d.ts.map +1 -0
- package/dist/code-parser.js +385 -0
- package/dist/code-parser.js.map +1 -0
- package/dist/config.d.ts +66 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +232 -0
- package/dist/config.js.map +1 -0
- package/dist/core/engine.d.ts +359 -0
- package/dist/core/engine.d.ts.map +1 -0
- package/dist/core/engine.js +127 -0
- package/dist/core/engine.js.map +1 -0
- package/dist/daemon.d.ts +29 -0
- package/dist/daemon.d.ts.map +1 -0
- package/dist/daemon.js +212 -0
- package/dist/daemon.js.map +1 -0
- package/dist/email-receiver.d.ts +63 -0
- package/dist/email-receiver.d.ts.map +1 -0
- package/dist/email-receiver.js +553 -0
- package/dist/email-receiver.js.map +1 -0
- package/dist/git-integration.d.ts +180 -0
- package/dist/git-integration.d.ts.map +1 -0
- package/dist/git-integration.js +850 -0
- package/dist/git-integration.js.map +1 -0
- package/dist/inbox.d.ts +84 -0
- package/dist/inbox.d.ts.map +1 -0
- package/dist/inbox.js +198 -0
- package/dist/inbox.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/languages/cpp.d.ts +51 -0
- package/dist/languages/cpp.d.ts.map +1 -0
- package/dist/languages/cpp.js +930 -0
- package/dist/languages/cpp.js.map +1 -0
- package/dist/languages/csharp.d.ts +79 -0
- package/dist/languages/csharp.d.ts.map +1 -0
- package/dist/languages/csharp.js +1776 -0
- package/dist/languages/csharp.js.map +1 -0
- package/dist/languages/go.d.ts +50 -0
- package/dist/languages/go.d.ts.map +1 -0
- package/dist/languages/go.js +882 -0
- package/dist/languages/go.js.map +1 -0
- package/dist/languages/java.d.ts +47 -0
- package/dist/languages/java.d.ts.map +1 -0
- package/dist/languages/java.js +649 -0
- package/dist/languages/java.js.map +1 -0
- package/dist/languages/python.d.ts +47 -0
- package/dist/languages/python.d.ts.map +1 -0
- package/dist/languages/python.js +655 -0
- package/dist/languages/python.js.map +1 -0
- package/dist/languages/rust.d.ts +61 -0
- package/dist/languages/rust.d.ts.map +1 -0
- package/dist/languages/rust.js +1064 -0
- package/dist/languages/rust.js.map +1 -0
- package/dist/logger.d.ts +20 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +133 -0
- package/dist/logger.js.map +1 -0
- package/dist/longterm-memory.d.ts +47 -0
- package/dist/longterm-memory.d.ts.map +1 -0
- package/dist/longterm-memory.js +300 -0
- package/dist/longterm-memory.js.map +1 -0
- package/dist/memory.d.ts +42 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +274 -0
- package/dist/memory.js.map +1 -0
- package/dist/messaging.d.ts +103 -0
- package/dist/messaging.d.ts.map +1 -0
- package/dist/messaging.js +645 -0
- package/dist/messaging.js.map +1 -0
- package/dist/multi-provider.d.ts +69 -0
- package/dist/multi-provider.d.ts.map +1 -0
- package/dist/multi-provider.js +484 -0
- package/dist/multi-provider.js.map +1 -0
- package/dist/orchestrator.d.ts +65 -0
- package/dist/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator.js +441 -0
- package/dist/orchestrator.js.map +1 -0
- package/dist/plugins.d.ts +52 -0
- package/dist/plugins.d.ts.map +1 -0
- package/dist/plugins.js +215 -0
- package/dist/plugins.js.map +1 -0
- package/dist/prism-orchestrator.d.ts +26 -0
- package/dist/prism-orchestrator.d.ts.map +1 -0
- package/dist/prism-orchestrator.js +191 -0
- package/dist/prism-orchestrator.js.map +1 -0
- package/dist/prism.d.ts +46 -0
- package/dist/prism.d.ts.map +1 -0
- package/dist/prism.js +188 -0
- package/dist/prism.js.map +1 -0
- package/dist/privacy.d.ts +23 -0
- package/dist/privacy.d.ts.map +1 -0
- package/dist/privacy.js +220 -0
- package/dist/privacy.js.map +1 -0
- package/dist/proactive.d.ts +30 -0
- package/dist/proactive.d.ts.map +1 -0
- package/dist/proactive.js +260 -0
- package/dist/proactive.js.map +1 -0
- package/dist/refactoring-engine.d.ts +100 -0
- package/dist/refactoring-engine.d.ts.map +1 -0
- package/dist/refactoring-engine.js +717 -0
- package/dist/refactoring-engine.js.map +1 -0
- package/dist/resilience.d.ts +43 -0
- package/dist/resilience.d.ts.map +1 -0
- package/dist/resilience.js +200 -0
- package/dist/resilience.js.map +1 -0
- package/dist/safety.d.ts +40 -0
- package/dist/safety.d.ts.map +1 -0
- package/dist/safety.js +133 -0
- package/dist/safety.js.map +1 -0
- package/dist/sandbox.d.ts +33 -0
- package/dist/sandbox.d.ts.map +1 -0
- package/dist/sandbox.js +173 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/scheduler.d.ts +72 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +374 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/semantic-memory.d.ts +70 -0
- package/dist/semantic-memory.d.ts.map +1 -0
- package/dist/semantic-memory.js +430 -0
- package/dist/semantic-memory.js.map +1 -0
- package/dist/skills.d.ts +97 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +575 -0
- package/dist/skills.js.map +1 -0
- package/dist/static/dashboard.html +853 -0
- package/dist/static/hub.html +772 -0
- package/dist/static/index.html +818 -0
- package/dist/static/logo.svg +24 -0
- package/dist/static/workflow-editor.html +913 -0
- package/dist/tools.d.ts +67 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +303 -0
- package/dist/tools.js.map +1 -0
- package/dist/types.d.ts +295 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +90 -0
- package/dist/types.js.map +1 -0
- package/dist/web.d.ts +76 -0
- package/dist/web.d.ts.map +1 -0
- package/dist/web.js +2139 -0
- package/dist/web.js.map +1 -0
- package/dist/workflow-engine.d.ts +114 -0
- package/dist/workflow-engine.d.ts.map +1 -0
- package/dist/workflow-engine.js +855 -0
- package/dist/workflow-engine.js.map +1 -0
- package/package.json +77 -0
package/dist/web.js
ADDED
|
@@ -0,0 +1,2139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Web UI server using Express
|
|
4
|
+
* TypeScript equivalent of Python web.py
|
|
5
|
+
*/
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
+
};
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.WebUIManager = void 0;
|
|
11
|
+
exports.runWebUI = runWebUI;
|
|
12
|
+
const express_1 = __importDefault(require("express"));
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const http_1 = require("http");
|
|
15
|
+
const ws_1 = require("ws");
|
|
16
|
+
const orchestrator_1 = require("./orchestrator");
|
|
17
|
+
const memory_1 = require("./memory");
|
|
18
|
+
const longterm_memory_1 = require("./longterm-memory");
|
|
19
|
+
const tools_1 = require("./tools");
|
|
20
|
+
const multi_provider_1 = require("./multi-provider");
|
|
21
|
+
const browser_1 = require("./browser");
|
|
22
|
+
const scheduler_1 = require("./scheduler");
|
|
23
|
+
const config_1 = require("./config");
|
|
24
|
+
const logger_1 = require("./logger");
|
|
25
|
+
const inbox_1 = require("./inbox");
|
|
26
|
+
const email_receiver_1 = require("./email-receiver");
|
|
27
|
+
const messaging_1 = require("./messaging");
|
|
28
|
+
const semantic_memory_1 = require("./semantic-memory");
|
|
29
|
+
const workflow_engine_1 = require("./workflow-engine");
|
|
30
|
+
const logger = (0, logger_1.getLogger)();
|
|
31
|
+
// Load config
|
|
32
|
+
(0, config_1.loadEnvConfig)();
|
|
33
|
+
class WebUIManager {
|
|
34
|
+
projectRoot;
|
|
35
|
+
host;
|
|
36
|
+
port;
|
|
37
|
+
app;
|
|
38
|
+
server;
|
|
39
|
+
wss;
|
|
40
|
+
orchestrator;
|
|
41
|
+
connections;
|
|
42
|
+
memory;
|
|
43
|
+
scheduler;
|
|
44
|
+
browser;
|
|
45
|
+
chatHistory;
|
|
46
|
+
providerManager;
|
|
47
|
+
inbox;
|
|
48
|
+
emailReceiver;
|
|
49
|
+
messagingHub;
|
|
50
|
+
semanticMemory;
|
|
51
|
+
constructor(projectRoot, host = '0.0.0.0', port = 8080) {
|
|
52
|
+
this.projectRoot = projectRoot;
|
|
53
|
+
this.host = host;
|
|
54
|
+
this.port = port;
|
|
55
|
+
this.app = (0, express_1.default)();
|
|
56
|
+
this.server = (0, http_1.createServer)(this.app);
|
|
57
|
+
this.wss = new ws_1.WebSocketServer({ server: this.server });
|
|
58
|
+
this.orchestrator = null;
|
|
59
|
+
this.connections = new Set();
|
|
60
|
+
this.memory = new longterm_memory_1.LongTermMemory();
|
|
61
|
+
this.scheduler = new scheduler_1.TaskScheduler();
|
|
62
|
+
this.browser = new browser_1.BrowserAutomation();
|
|
63
|
+
this.chatHistory = [];
|
|
64
|
+
this.providerManager = new multi_provider_1.ProviderManager();
|
|
65
|
+
this.inbox = new inbox_1.InboxManager(this.memory);
|
|
66
|
+
this.emailReceiver = new email_receiver_1.EmailReceiver(this.inbox);
|
|
67
|
+
this.messagingHub = new messaging_1.MessagingHub();
|
|
68
|
+
this.semanticMemory = (0, semantic_memory_1.getSemanticMemory)();
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Initialize and start the web server
|
|
72
|
+
*/
|
|
73
|
+
async start() {
|
|
74
|
+
// Setup middleware
|
|
75
|
+
this.app.use(express_1.default.json());
|
|
76
|
+
this.app.use(express_1.default.static(path_1.default.join(__dirname, 'static')));
|
|
77
|
+
// Setup routes
|
|
78
|
+
this.setupRoutes();
|
|
79
|
+
this.setupWebSocket();
|
|
80
|
+
// Start email receivers if configured
|
|
81
|
+
this.startEmailReceivers();
|
|
82
|
+
// Start server
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
this.server.listen(this.port, this.host, () => {
|
|
85
|
+
logger.info(`Web UI server started at http://${this.host}:${this.port}`);
|
|
86
|
+
resolve();
|
|
87
|
+
});
|
|
88
|
+
this.server.on('error', reject);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Setup HTTP routes
|
|
93
|
+
*/
|
|
94
|
+
setupRoutes() {
|
|
95
|
+
// Health check
|
|
96
|
+
this.app.get('/api/health', (_req, res) => {
|
|
97
|
+
res.json({ status: 'ok', version: '0.3.0' });
|
|
98
|
+
});
|
|
99
|
+
// Get current state
|
|
100
|
+
this.app.get('/api/state', (req, res) => {
|
|
101
|
+
if (!this.orchestrator) {
|
|
102
|
+
res.json({ running: false });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
res.json({
|
|
106
|
+
running: true,
|
|
107
|
+
state: this.orchestrator.state,
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
// Start new task
|
|
111
|
+
this.app.post('/api/task', async (req, res) => {
|
|
112
|
+
const body = req.body;
|
|
113
|
+
if (!body.task) {
|
|
114
|
+
res.status(400).json({ error: 'Task required' });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
// Initialize provider
|
|
119
|
+
const manager = new multi_provider_1.ProviderManager();
|
|
120
|
+
const provider = manager.createFromEnv();
|
|
121
|
+
if (!provider) {
|
|
122
|
+
res.status(500).json({ error: 'No LLM provider available' });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
// Initialize tools
|
|
126
|
+
const shell = new tools_1.ShellTool(this.projectRoot);
|
|
127
|
+
await shell.init();
|
|
128
|
+
const fs = new tools_1.FileSystemTool(this.projectRoot);
|
|
129
|
+
const memory = new memory_1.RAGMemory(this.projectRoot);
|
|
130
|
+
await memory.init();
|
|
131
|
+
// Create orchestrator
|
|
132
|
+
this.orchestrator = new orchestrator_1.Orchestrator(provider, shell, fs, body.max_steps || 15);
|
|
133
|
+
// Run task asynchronously
|
|
134
|
+
this.runTask(body.task);
|
|
135
|
+
res.json({ task_id: Date.now().toString(), status: 'started' });
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
res.status(500).json({ error: error.message });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
// Settings endpoints
|
|
142
|
+
this.app.get('/api/settings', (_req, res) => {
|
|
143
|
+
const config = (0, config_1.getConfig)();
|
|
144
|
+
const cfg = config.get();
|
|
145
|
+
const safeConfig = { ...cfg, llm: { ...cfg.llm, apiKey: cfg.llm.apiKey ? '***' : '' } };
|
|
146
|
+
res.json(safeConfig);
|
|
147
|
+
});
|
|
148
|
+
this.app.post('/api/settings', (req, res) => {
|
|
149
|
+
const config = (0, config_1.getConfig)();
|
|
150
|
+
const settings = req.body;
|
|
151
|
+
try {
|
|
152
|
+
if (settings.llm) {
|
|
153
|
+
if (!settings.llm.apiKey || settings.llm.apiKey === '***' || settings.llm.apiKey.trim() === '') {
|
|
154
|
+
delete settings.llm.apiKey;
|
|
155
|
+
}
|
|
156
|
+
config.updateLLM(settings.llm);
|
|
157
|
+
}
|
|
158
|
+
if (settings.system)
|
|
159
|
+
config.updateSystem(settings.system);
|
|
160
|
+
if (settings.webui)
|
|
161
|
+
config.updateWebUI(settings.webui);
|
|
162
|
+
if (settings.user)
|
|
163
|
+
config.updateUser(settings.user);
|
|
164
|
+
res.json({ success: true });
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
res.status(400).json({ error: error.message });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
// ============ CHAT API ============
|
|
171
|
+
// Helper: execute a messaging action from parsed JSON
|
|
172
|
+
const execMessagingAction = async (action) => {
|
|
173
|
+
const platform = (action.platform || '').toLowerCase();
|
|
174
|
+
try {
|
|
175
|
+
if (platform === 'email') {
|
|
176
|
+
const ints = loadIntegrations();
|
|
177
|
+
const cfg = ints['email'] || {};
|
|
178
|
+
const host = cfg.host || process.env.EMAIL_HOST || 'smtp.gmail.com';
|
|
179
|
+
const port = parseInt(cfg.port || process.env.EMAIL_PORT || '587', 10);
|
|
180
|
+
const username = cfg.username || process.env.EMAIL_USERNAME;
|
|
181
|
+
const password = cfg.password || process.env.EMAIL_PASSWORD;
|
|
182
|
+
if (!username || !password)
|
|
183
|
+
return { success: false, message: 'Email not configured. Go to Skills → Email → Configure.' };
|
|
184
|
+
const nodemailer = require('nodemailer');
|
|
185
|
+
const transporter = nodemailer.createTransport({ host, port, secure: port === 465, auth: { user: username, pass: password } });
|
|
186
|
+
await transporter.sendMail({ from: `LinguClaw <${username}>`, to: action.to, subject: action.subject || 'Message from LinguClaw', text: action.body || '', html: action.body ? `<div style="font-family:sans-serif">${(action.body || '').replace(/\n/g, '<br>')}</div>` : '' });
|
|
187
|
+
return { success: true, message: `✅ Email sent to ${action.to}` };
|
|
188
|
+
}
|
|
189
|
+
else if (platform === 'telegram') {
|
|
190
|
+
const ints = loadIntegrations();
|
|
191
|
+
const cfg = ints['telegram'] || {};
|
|
192
|
+
const token = cfg.botToken || process.env.TELEGRAM_BOT_TOKEN;
|
|
193
|
+
const chatId = action.chatId || cfg.chatId || process.env.TELEGRAM_CHAT_ID;
|
|
194
|
+
if (!token)
|
|
195
|
+
return { success: false, message: 'Telegram not configured.' };
|
|
196
|
+
if (!chatId)
|
|
197
|
+
return { success: false, message: 'Telegram Chat ID not set.' };
|
|
198
|
+
const https = require('https');
|
|
199
|
+
const pd = JSON.stringify({ chat_id: chatId, text: action.message || action.body, parse_mode: 'HTML' });
|
|
200
|
+
const result = await new Promise((resolve, reject) => { const r = https.request({ hostname: 'api.telegram.org', path: `/bot${token}/sendMessage`, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(pd) } }, (resp) => { let d = ''; resp.on('data', (c) => d += c); resp.on('end', () => { try {
|
|
201
|
+
resolve(JSON.parse(d));
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
resolve({ ok: false });
|
|
205
|
+
} }); }); r.on('error', reject); r.write(pd); r.end(); });
|
|
206
|
+
return result.ok ? { success: true, message: `✅ Telegram message sent` } : { success: false, message: 'Telegram error: ' + (result.description || 'unknown') };
|
|
207
|
+
}
|
|
208
|
+
else if (platform === 'discord') {
|
|
209
|
+
const ints = loadIntegrations();
|
|
210
|
+
const cfg = ints['discord'] || {};
|
|
211
|
+
const token = cfg.botToken || process.env.DISCORD_BOT_TOKEN;
|
|
212
|
+
if (!token)
|
|
213
|
+
return { success: false, message: 'Discord not configured.' };
|
|
214
|
+
if (!action.channelId)
|
|
215
|
+
return { success: false, message: 'Discord channelId required.' };
|
|
216
|
+
const https = require('https');
|
|
217
|
+
const pd = JSON.stringify({ content: action.message || action.body });
|
|
218
|
+
const result = await new Promise((resolve, reject) => { const r = https.request({ hostname: 'discord.com', path: `/api/v10/channels/${action.channelId}/messages`, method: 'POST', headers: { 'Authorization': `Bot ${token}`, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(pd) } }, (resp) => { let d = ''; resp.on('data', (c) => d += c); resp.on('end', () => { try {
|
|
219
|
+
resolve(JSON.parse(d));
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
resolve({});
|
|
223
|
+
} }); }); r.on('error', reject); r.write(pd); r.end(); });
|
|
224
|
+
return result.id ? { success: true, message: `✅ Discord message sent` } : { success: false, message: 'Discord error: ' + (result.message || 'unknown') };
|
|
225
|
+
}
|
|
226
|
+
else if (platform === 'slack') {
|
|
227
|
+
const ints = loadIntegrations();
|
|
228
|
+
const cfg = ints['slack'] || {};
|
|
229
|
+
const token = cfg.botToken || process.env.SLACK_BOT_TOKEN;
|
|
230
|
+
const channel = action.channel || cfg.channel || '#general';
|
|
231
|
+
if (!token)
|
|
232
|
+
return { success: false, message: 'Slack not configured.' };
|
|
233
|
+
const https = require('https');
|
|
234
|
+
const pd = JSON.stringify({ channel, text: action.message || action.body });
|
|
235
|
+
const result = await new Promise((resolve, reject) => { const r = https.request({ hostname: 'slack.com', path: '/api/chat.postMessage', method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(pd) } }, (resp) => { let d = ''; resp.on('data', (c) => d += c); resp.on('end', () => { try {
|
|
236
|
+
resolve(JSON.parse(d));
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
resolve({ ok: false });
|
|
240
|
+
} }); }); r.on('error', reject); r.write(pd); r.end(); });
|
|
241
|
+
return result.ok ? { success: true, message: `✅ Slack message sent to ${channel}` } : { success: false, message: 'Slack error: ' + (result.error || 'unknown') };
|
|
242
|
+
}
|
|
243
|
+
else if (platform === 'whatsapp') {
|
|
244
|
+
const ints = loadIntegrations();
|
|
245
|
+
const cfg = ints['whatsapp'] || {};
|
|
246
|
+
const sid = cfg.accountSid || process.env.TWILIO_ACCOUNT_SID;
|
|
247
|
+
const authToken = cfg.authToken || process.env.TWILIO_AUTH_TOKEN;
|
|
248
|
+
const from = cfg.phoneNumber || process.env.TWILIO_PHONE_NUMBER;
|
|
249
|
+
if (!sid || !authToken || !from)
|
|
250
|
+
return { success: false, message: 'WhatsApp not configured.' };
|
|
251
|
+
if (!action.to)
|
|
252
|
+
return { success: false, message: 'WhatsApp "to" phone number required.' };
|
|
253
|
+
const https = require('https');
|
|
254
|
+
const pd = `To=whatsapp:${action.to}&From=whatsapp:${from}&Body=${encodeURIComponent(action.message || action.body || '')}`;
|
|
255
|
+
const auth = Buffer.from(`${sid}:${authToken}`).toString('base64');
|
|
256
|
+
const result = await new Promise((resolve, reject) => { const r = https.request({ hostname: 'api.twilio.com', path: `/2010-04-01/Accounts/${sid}/Messages.json`, method: 'POST', headers: { 'Authorization': `Basic ${auth}`, 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': Buffer.byteLength(pd) } }, (resp) => { let d = ''; resp.on('data', (c) => d += c); resp.on('end', () => { try {
|
|
257
|
+
resolve(JSON.parse(d));
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
resolve({});
|
|
261
|
+
} }); }); r.on('error', reject); r.write(pd); r.end(); });
|
|
262
|
+
return result.sid ? { success: true, message: `✅ WhatsApp message sent to ${action.to}` } : { success: false, message: 'WhatsApp error: ' + (result.message || 'unknown') };
|
|
263
|
+
}
|
|
264
|
+
return { success: false, message: 'Unknown platform: ' + platform };
|
|
265
|
+
}
|
|
266
|
+
catch (e) {
|
|
267
|
+
return { success: false, message: e.message };
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
this.app.post('/api/chat', async (req, res) => {
|
|
271
|
+
const { message } = req.body;
|
|
272
|
+
if (!message) {
|
|
273
|
+
res.status(400).json({ error: 'message required' });
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
const provider = this.providerManager.createFromEnv();
|
|
278
|
+
if (!provider) {
|
|
279
|
+
res.status(500).json({ error: 'No LLM provider configured. Go to Settings to add your API key.' });
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
this.chatHistory.push({ role: 'user', content: message, timestamp: new Date().toISOString() });
|
|
283
|
+
// Build context from both memory systems
|
|
284
|
+
const recentMemories = this.memory.search(message, undefined, 5);
|
|
285
|
+
const memCtx = recentMemories.length > 0 ? '\nRelevant memories: ' + recentMemories.map((m) => m.value).join('; ') : '';
|
|
286
|
+
// Semantic memory: find related past conversations and knowledge
|
|
287
|
+
const semanticResults = this.semanticMemory.search(message, 5, undefined, 0.1);
|
|
288
|
+
const semanticCtx = semanticResults.length > 0
|
|
289
|
+
? '\n\nRelated knowledge from memory:\n' + semanticResults.map((r, i) => `${i + 1}. [${r.category}] ${r.content.substring(0, 200)}`).join('\n')
|
|
290
|
+
: '';
|
|
291
|
+
// Inbox context - show unread messages to AI
|
|
292
|
+
const unreadMessages = this.inbox.getUnread();
|
|
293
|
+
let inboxCtx = '';
|
|
294
|
+
if (unreadMessages.length > 0) {
|
|
295
|
+
inboxCtx = `\n\n📬 INBOX: You have ${unreadMessages.length} unread message(s).\nRecent unread:\n` +
|
|
296
|
+
unreadMessages.slice(0, 3).map((m, i) => `${i + 1}. [${m.platform}] From: ${m.from} | Subject: ${m.subject || '(no subject)'} | Body: ${(m.body || '').substring(0, 100)}...`).join('\n');
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
inboxCtx = '\n\n📬 INBOX: No unread messages.';
|
|
300
|
+
}
|
|
301
|
+
// Check which integrations are configured
|
|
302
|
+
const ints = loadIntegrations();
|
|
303
|
+
const available = [];
|
|
304
|
+
if (ints['email'] && ints['email'].username)
|
|
305
|
+
available.push('email');
|
|
306
|
+
if (ints['telegram'] && ints['telegram'].botToken)
|
|
307
|
+
available.push('telegram');
|
|
308
|
+
if (ints['discord'] && ints['discord'].botToken)
|
|
309
|
+
available.push('discord');
|
|
310
|
+
if (ints['slack'] && ints['slack'].botToken)
|
|
311
|
+
available.push('slack');
|
|
312
|
+
if (ints['whatsapp'] && ints['whatsapp'].accountSid)
|
|
313
|
+
available.push('whatsapp');
|
|
314
|
+
const toolsInfo = available.length > 0 ? `\n\nYou have these messaging integrations configured: ${available.join(', ')}.\n\nMESSAGING RULES — follow these strictly:\n1. When the user asks to send a message, FIRST check what information is missing (to, subject, body for email; message for telegram, etc).\n2. If the user provides all details including body/content, use what they provided.\n3. If the user says things like "send an email to John about the meeting", generate a suitable email body content intelligently.\n4. Once you have ALL required info, show a PREVIEW of what will be sent. Format it clearly like:\n 📧 Email Preview:\n To: user@example.com\n Subject: Hello\n Body: Your message text here\n\n Then ask: "Should I send this?"\n5. When the user CONFIRMS (says yes, send it, go, gönder, tamam, etc), THEN respond with ONLY the JSON action object:\n {"action":"send","platform":"email","to":"user@example.com","subject":"Hello","body":"Message text"}\n {"action":"send","platform":"telegram","message":"Hello!"}\n {"action":"send","platform":"discord","channelId":"123","message":"Hello!"}\n {"action":"send","platform":"slack","channel":"#general","message":"Hello!"}\n {"action":"send","platform":"whatsapp","to":"+1234567890","message":"Hello!"}\n6. The JSON must be the ONLY content in your response — no extra text.\n7. For everything else (questions, conversation), respond normally in natural language.` : '\n\nNo messaging integrations are configured yet. If the user asks to send messages, tell them to configure integrations in the Skills panel first.';
|
|
315
|
+
const sysPrompt = 'You are LinguClaw 🦀, a powerful personal AI assistant. You can send emails, Telegram/Discord/Slack/WhatsApp messages, browse the web, manage files, schedule jobs, and more. Be helpful and conversational. When a user wants to send a message, always ask for missing details, show a preview, and wait for confirmation before sending.' + memCtx + semanticCtx + inboxCtx + toolsInfo;
|
|
316
|
+
const messages = [
|
|
317
|
+
{ role: 'system', content: sysPrompt },
|
|
318
|
+
...this.chatHistory.slice(-20).map(m => ({ role: m.role, content: m.content })),
|
|
319
|
+
];
|
|
320
|
+
const response = await provider.complete(messages, 0.7, 2048);
|
|
321
|
+
if (response.error) {
|
|
322
|
+
res.json({ reply: 'Error: ' + response.error });
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
let reply = response.content || 'No response';
|
|
326
|
+
// Check if the LLM returned a JSON action
|
|
327
|
+
let actionExecuted = false;
|
|
328
|
+
try {
|
|
329
|
+
const trimmed = reply.trim();
|
|
330
|
+
if (trimmed.startsWith('{') && trimmed.includes('"action"')) {
|
|
331
|
+
const action = JSON.parse(trimmed);
|
|
332
|
+
if (action.action === 'send' && action.platform) {
|
|
333
|
+
const result = await execMessagingAction(action);
|
|
334
|
+
// Build a detailed summary of what was sent
|
|
335
|
+
let details = '';
|
|
336
|
+
if (action.platform === 'email') {
|
|
337
|
+
details = `\n\n📧 Email Details:\nTo: ${action.to}\nSubject: ${action.subject || 'Message from LinguClaw'}\nBody: ${action.body || '(empty)'}`;
|
|
338
|
+
}
|
|
339
|
+
else if (action.platform === 'telegram') {
|
|
340
|
+
details = `\n\n📨 Telegram Message:\n${action.message || action.body}`;
|
|
341
|
+
}
|
|
342
|
+
else if (action.platform === 'discord') {
|
|
343
|
+
details = `\n\n💬 Discord Message (Channel: ${action.channelId}):\n${action.message || action.body}`;
|
|
344
|
+
}
|
|
345
|
+
else if (action.platform === 'slack') {
|
|
346
|
+
details = `\n\n💬 Slack Message (${action.channel || '#general'}):\n${action.message || action.body}`;
|
|
347
|
+
}
|
|
348
|
+
else if (action.platform === 'whatsapp') {
|
|
349
|
+
details = `\n\n📱 WhatsApp Message (To: ${action.to}):\n${action.message || action.body}`;
|
|
350
|
+
}
|
|
351
|
+
reply = result.message + details;
|
|
352
|
+
actionExecuted = true;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
catch (parseErr) {
|
|
357
|
+
logger.debug(`Chat reply JSON parse attempt: ${parseErr.message}`);
|
|
358
|
+
}
|
|
359
|
+
this.chatHistory.push({ role: 'assistant', content: reply, timestamp: new Date().toISOString() });
|
|
360
|
+
// Auto-save important info to memory
|
|
361
|
+
if (message.toLowerCase().includes('remember') || message.toLowerCase().includes('hatırla') || message.toLowerCase().includes('hatirla')) {
|
|
362
|
+
this.memory.store('chat_' + Date.now(), message, 'chat');
|
|
363
|
+
}
|
|
364
|
+
// Save conversation turn to semantic memory for cross-session recall
|
|
365
|
+
const turnId = `chat_${Date.now()}`;
|
|
366
|
+
this.semanticMemory.store(turnId, `User: ${message}\nAssistant: ${reply.substring(0, 500)}`, 'conversation', {
|
|
367
|
+
timestamp: new Date().toISOString(),
|
|
368
|
+
actionExecuted,
|
|
369
|
+
});
|
|
370
|
+
res.json({ reply, model: response.model, actionExecuted });
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
res.json({ reply: 'Error: ' + error.message });
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
// ============ STREAMING CHAT (SSE) ============
|
|
377
|
+
this.app.post('/api/chat/stream', async (req, res) => {
|
|
378
|
+
const { message } = req.body;
|
|
379
|
+
if (!message) {
|
|
380
|
+
res.status(400).json({ error: 'message required' });
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
const provider = this.providerManager.createFromEnv();
|
|
385
|
+
if (!provider) {
|
|
386
|
+
res.status(500).json({ error: 'No LLM provider configured.' });
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
this.chatHistory.push({ role: 'user', content: message, timestamp: new Date().toISOString() });
|
|
390
|
+
// Build context (same as non-streaming)
|
|
391
|
+
const recentMemories = this.memory.search(message, undefined, 5);
|
|
392
|
+
const memCtx = recentMemories.length > 0 ? '\nRelevant memories: ' + recentMemories.map((m) => m.value).join('; ') : '';
|
|
393
|
+
const semanticResults = this.semanticMemory.search(message, 5, undefined, 0.1);
|
|
394
|
+
const semanticCtx = semanticResults.length > 0
|
|
395
|
+
? '\n\nRelated knowledge from memory:\n' + semanticResults.map((r, i) => `${i + 1}. [${r.category}] ${r.content.substring(0, 200)}`).join('\n')
|
|
396
|
+
: '';
|
|
397
|
+
const unreadMessages = this.inbox.getUnread();
|
|
398
|
+
const inboxCtx = unreadMessages.length > 0
|
|
399
|
+
? `\n\n📬 INBOX: ${unreadMessages.length} unread.`
|
|
400
|
+
: '\n\n📬 INBOX: No unread messages.';
|
|
401
|
+
const sysPrompt = 'You are LinguClaw 🦀, a powerful personal AI assistant. Be helpful and conversational.' + memCtx + semanticCtx + inboxCtx;
|
|
402
|
+
const messages = [
|
|
403
|
+
{ role: 'system', content: sysPrompt },
|
|
404
|
+
...this.chatHistory.slice(-20).map(m => ({ role: m.role, content: m.content })),
|
|
405
|
+
];
|
|
406
|
+
// Set SSE headers
|
|
407
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
408
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
409
|
+
res.setHeader('Connection', 'keep-alive');
|
|
410
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
411
|
+
res.flushHeaders();
|
|
412
|
+
let fullReply = '';
|
|
413
|
+
try {
|
|
414
|
+
for await (const chunk of provider.stream(messages, 0.7, 2048)) {
|
|
415
|
+
fullReply += chunk;
|
|
416
|
+
res.write(`data: ${JSON.stringify({ chunk })}\n\n`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
catch (streamErr) {
|
|
420
|
+
// If streaming fails, fall back to complete
|
|
421
|
+
logger.warn(`Streaming failed, falling back: ${streamErr.message}`);
|
|
422
|
+
const response = await provider.complete(messages, 0.7, 2048);
|
|
423
|
+
fullReply = response.content || '';
|
|
424
|
+
res.write(`data: ${JSON.stringify({ chunk: fullReply })}\n\n`);
|
|
425
|
+
}
|
|
426
|
+
// Send done event
|
|
427
|
+
res.write(`data: ${JSON.stringify({ done: true, model: provider.model })}\n\n`);
|
|
428
|
+
res.end();
|
|
429
|
+
// Save to history and semantic memory
|
|
430
|
+
this.chatHistory.push({ role: 'assistant', content: fullReply, timestamp: new Date().toISOString() });
|
|
431
|
+
this.semanticMemory.store(`chat_${Date.now()}`, `User: ${message}\nAssistant: ${fullReply.substring(0, 500)}`, 'conversation', {
|
|
432
|
+
timestamp: new Date().toISOString(),
|
|
433
|
+
});
|
|
434
|
+
if (message.toLowerCase().includes('remember') || message.toLowerCase().includes('hatırla') || message.toLowerCase().includes('hatirla')) {
|
|
435
|
+
this.memory.store('chat_' + Date.now(), message, 'chat');
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
if (!res.headersSent) {
|
|
440
|
+
res.status(500).json({ error: error.message });
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
|
|
444
|
+
res.end();
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
this.app.get('/api/chat/history', (_req, res) => {
|
|
449
|
+
res.json(this.chatHistory.slice(-100));
|
|
450
|
+
});
|
|
451
|
+
this.app.delete('/api/chat/history', (_req, res) => {
|
|
452
|
+
this.chatHistory = [];
|
|
453
|
+
res.json({ success: true });
|
|
454
|
+
});
|
|
455
|
+
// ============ MEMORY API ============
|
|
456
|
+
this.app.get('/api/memory', (_req, res) => {
|
|
457
|
+
try {
|
|
458
|
+
const stats = this.memory.getStats();
|
|
459
|
+
const all = this.memory.search('', undefined, 100);
|
|
460
|
+
res.json({ entries: all, stats });
|
|
461
|
+
}
|
|
462
|
+
catch (e) {
|
|
463
|
+
res.json([]);
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
this.app.post('/api/memory', (req, res) => {
|
|
467
|
+
const { key, value, category, tags } = req.body;
|
|
468
|
+
if (!key || !value) {
|
|
469
|
+
res.status(400).json({ error: 'key and value required' });
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
try {
|
|
473
|
+
this.memory.store(key, value, category || 'general', tags || []);
|
|
474
|
+
res.json({ success: true });
|
|
475
|
+
}
|
|
476
|
+
catch (e) {
|
|
477
|
+
res.status(500).json({ error: e.message });
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
this.app.delete('/api/memory/:key', (req, res) => {
|
|
481
|
+
try {
|
|
482
|
+
this.memory.delete(req.params.key);
|
|
483
|
+
res.json({ success: true });
|
|
484
|
+
}
|
|
485
|
+
catch (e) {
|
|
486
|
+
res.status(500).json({ error: e.message });
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
// ============ SCHEDULER API ============
|
|
490
|
+
this.app.get('/api/scheduler/jobs', (_req, res) => {
|
|
491
|
+
res.json(this.scheduler.getJobs());
|
|
492
|
+
});
|
|
493
|
+
this.app.post('/api/scheduler/jobs', (req, res) => {
|
|
494
|
+
const { name, type, schedule, command, tags } = req.body;
|
|
495
|
+
if (!name || !type || !schedule || !command) {
|
|
496
|
+
res.status(400).json({ error: 'name, type, schedule, command required' });
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
const job = this.scheduler.addJob({ name, type, schedule, command, enabled: true, tags: tags || [] });
|
|
500
|
+
res.json(job);
|
|
501
|
+
});
|
|
502
|
+
this.app.delete('/api/scheduler/jobs/:id', (req, res) => {
|
|
503
|
+
const ok = this.scheduler.removeJob(req.params.id);
|
|
504
|
+
res.json({ success: ok });
|
|
505
|
+
});
|
|
506
|
+
this.app.post('/api/scheduler/jobs/:id/toggle', (req, res) => {
|
|
507
|
+
const job = this.scheduler.toggleJob(req.params.id);
|
|
508
|
+
res.json(job || { error: 'Job not found' });
|
|
509
|
+
});
|
|
510
|
+
this.app.get('/api/scheduler/results', (_req, res) => {
|
|
511
|
+
res.json(this.scheduler.getResults());
|
|
512
|
+
});
|
|
513
|
+
// ============ BROWSER API ============
|
|
514
|
+
this.app.post('/api/browser/browse', async (req, res) => {
|
|
515
|
+
const { url } = req.body;
|
|
516
|
+
if (!url) {
|
|
517
|
+
res.status(400).json({ error: 'url required' });
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (!this.browser.isAvailable)
|
|
521
|
+
await this.browser.init();
|
|
522
|
+
const result = await this.browser.browse(url);
|
|
523
|
+
res.json(result);
|
|
524
|
+
});
|
|
525
|
+
this.app.post('/api/browser/screenshot', async (req, res) => {
|
|
526
|
+
const { url } = req.body;
|
|
527
|
+
if (!this.browser.isAvailable)
|
|
528
|
+
await this.browser.init();
|
|
529
|
+
const result = await this.browser.screenshot(url);
|
|
530
|
+
res.json(result);
|
|
531
|
+
});
|
|
532
|
+
this.app.post('/api/browser/extract', async (req, res) => {
|
|
533
|
+
const { selector } = req.body;
|
|
534
|
+
if (!selector) {
|
|
535
|
+
res.status(400).json({ error: 'selector required' });
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const result = await this.browser.extract(selector);
|
|
539
|
+
res.json(result);
|
|
540
|
+
});
|
|
541
|
+
// AI-powered browser helpers
|
|
542
|
+
const getBrowserPageContent = async () => {
|
|
543
|
+
if (!this.browser.isAvailable)
|
|
544
|
+
return null;
|
|
545
|
+
try {
|
|
546
|
+
const result = await this.browser.evaluate('(() => { const b = document.body; if (!b) return ""; const c = b.cloneNode(true); c.querySelectorAll("script,style,noscript,svg,img").forEach(e => e.remove()); return c.innerText.substring(0, 8000); })()');
|
|
547
|
+
const title = await this.browser.evaluate('document.title');
|
|
548
|
+
const url = await this.browser.evaluate('window.location.href');
|
|
549
|
+
return { content: result.data || '', title: title.data || '', url: url.data || '' };
|
|
550
|
+
}
|
|
551
|
+
catch (err) {
|
|
552
|
+
logger.warn(`getBrowserPageContent error: ${err.message}`);
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
this.app.post('/api/browser/summarize', async (req, res) => {
|
|
557
|
+
try {
|
|
558
|
+
const provider = this.providerManager.createFromEnv();
|
|
559
|
+
if (!provider) {
|
|
560
|
+
res.status(500).json({ error: 'No LLM provider configured' });
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const page = await getBrowserPageContent();
|
|
564
|
+
if (!page || !page.content) {
|
|
565
|
+
res.status(400).json({ error: 'No page loaded. Browse a URL first.' });
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
const lang = req.body.language || 'English';
|
|
569
|
+
const messages = [
|
|
570
|
+
{ role: 'system', content: 'You are a helpful assistant that summarizes web pages clearly and concisely.' },
|
|
571
|
+
{ role: 'user', content: `Summarize the following web page in ${lang}. Include key points, main topic, and any important details.\n\nPage title: ${page.title}\nURL: ${page.url}\n\nContent:\n${page.content}` }
|
|
572
|
+
];
|
|
573
|
+
const response = await provider.complete(messages, 0.3, 1500);
|
|
574
|
+
res.json({ success: true, summary: response.content || 'No summary generated', title: page.title, url: page.url, model: response.model });
|
|
575
|
+
}
|
|
576
|
+
catch (e) {
|
|
577
|
+
res.status(500).json({ error: e.message });
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
this.app.post('/api/browser/ask', async (req, res) => {
|
|
581
|
+
const { question } = req.body;
|
|
582
|
+
if (!question) {
|
|
583
|
+
res.status(400).json({ error: 'question required' });
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
try {
|
|
587
|
+
const provider = this.providerManager.createFromEnv();
|
|
588
|
+
if (!provider) {
|
|
589
|
+
res.status(500).json({ error: 'No LLM provider configured' });
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const page = await getBrowserPageContent();
|
|
593
|
+
if (!page || !page.content) {
|
|
594
|
+
res.status(400).json({ error: 'No page loaded. Browse a URL first.' });
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
const messages = [
|
|
598
|
+
{ role: 'system', content: 'You are a helpful assistant. Answer questions about the given web page content accurately and concisely. If the answer is not in the content, say so.' },
|
|
599
|
+
{ role: 'user', content: `Page: ${page.title} (${page.url})\n\nContent:\n${page.content}\n\n---\nQuestion: ${question}` }
|
|
600
|
+
];
|
|
601
|
+
const response = await provider.complete(messages, 0.3, 1500);
|
|
602
|
+
res.json({ success: true, answer: response.content || 'No answer generated', question, title: page.title, model: response.model });
|
|
603
|
+
}
|
|
604
|
+
catch (e) {
|
|
605
|
+
res.status(500).json({ error: e.message });
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
this.app.post('/api/browser/smart-extract', async (req, res) => {
|
|
609
|
+
const { prompt } = req.body;
|
|
610
|
+
if (!prompt) {
|
|
611
|
+
res.status(400).json({ error: 'prompt required (e.g. "extract all prices", "find email addresses")' });
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
try {
|
|
615
|
+
const provider = this.providerManager.createFromEnv();
|
|
616
|
+
if (!provider) {
|
|
617
|
+
res.status(500).json({ error: 'No LLM provider configured' });
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
const page = await getBrowserPageContent();
|
|
621
|
+
if (!page || !page.content) {
|
|
622
|
+
res.status(400).json({ error: 'No page loaded. Browse a URL first.' });
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
const messages = [
|
|
626
|
+
{ role: 'system', content: 'You are a data extraction assistant. Extract the requested data from the web page content. Return the data in a clean, structured format. Use JSON when appropriate.' },
|
|
627
|
+
{ role: 'user', content: `Extract from this page: ${prompt}\n\nPage: ${page.title} (${page.url})\n\nContent:\n${page.content}` }
|
|
628
|
+
];
|
|
629
|
+
const response = await provider.complete(messages, 0.2, 2000);
|
|
630
|
+
res.json({ success: true, extracted: response.content || 'Nothing extracted', prompt, title: page.title, model: response.model });
|
|
631
|
+
}
|
|
632
|
+
catch (e) {
|
|
633
|
+
res.status(500).json({ error: e.message });
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
this.app.post('/api/browser/search', async (req, res) => {
|
|
637
|
+
const { query } = req.body;
|
|
638
|
+
if (!query) {
|
|
639
|
+
res.status(400).json({ error: 'query required' });
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
try {
|
|
643
|
+
if (!this.browser.isAvailable)
|
|
644
|
+
await this.browser.init();
|
|
645
|
+
const searchUrl = 'https://html.duckduckgo.com/html/?q=' + encodeURIComponent(query);
|
|
646
|
+
const result = await this.browser.browse(searchUrl);
|
|
647
|
+
if (!result.success) {
|
|
648
|
+
res.json(result);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
const provider = this.providerManager.createFromEnv();
|
|
652
|
+
if (provider && result.content) {
|
|
653
|
+
const messages = [
|
|
654
|
+
{ role: 'system', content: 'You are a helpful search assistant. Based on the search results below, provide a clear, informative answer to the user\'s query. Cite relevant sources when possible.' },
|
|
655
|
+
{ role: 'user', content: `Search query: ${query}\n\nSearch results:\n${result.content}` }
|
|
656
|
+
];
|
|
657
|
+
const response = await provider.complete(messages, 0.4, 1500);
|
|
658
|
+
res.json({ success: true, query, answer: response.content, rawContent: result.content, links: result.links, model: response.model });
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
res.json({ success: true, query, rawContent: result.content, links: result.links });
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
catch (e) {
|
|
665
|
+
res.status(500).json({ error: e.message });
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
// ============ SYSTEM STATUS API ============
|
|
669
|
+
this.app.get('/api/system/status', (_req, res) => {
|
|
670
|
+
const config = (0, config_1.getConfig)();
|
|
671
|
+
const cfg = config.get();
|
|
672
|
+
res.json({
|
|
673
|
+
version: cfg.version || '0.3.0',
|
|
674
|
+
provider: cfg.llm.provider,
|
|
675
|
+
model: cfg.llm.model,
|
|
676
|
+
hasApiKey: !!(cfg.llm.apiKey && cfg.llm.apiKey.length > 0),
|
|
677
|
+
scheduler: { jobs: this.scheduler.getJobs().length, running: true },
|
|
678
|
+
browser: { available: this.browser.isAvailable },
|
|
679
|
+
memory: { entries: this.memory.getStats().total_entries },
|
|
680
|
+
chat: { messages: this.chatHistory.length },
|
|
681
|
+
connections: this.connections.size,
|
|
682
|
+
uptime: process.uptime(),
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
// ============ SKILLS API ============
|
|
686
|
+
const integrationsPath = path_1.default.join(require('os').homedir(), '.linguclaw', 'integrations.json');
|
|
687
|
+
const loadIntegrations = () => {
|
|
688
|
+
try {
|
|
689
|
+
return JSON.parse(require('fs').readFileSync(integrationsPath, 'utf8'));
|
|
690
|
+
}
|
|
691
|
+
catch (e) {
|
|
692
|
+
logger.debug(`loadIntegrations: ${e.message}`);
|
|
693
|
+
return {};
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
const saveIntegrations = (data) => {
|
|
697
|
+
const dir = path_1.default.dirname(integrationsPath);
|
|
698
|
+
if (!require('fs').existsSync(dir))
|
|
699
|
+
require('fs').mkdirSync(dir, { recursive: true });
|
|
700
|
+
require('fs').writeFileSync(integrationsPath, JSON.stringify(data, null, 2));
|
|
701
|
+
};
|
|
702
|
+
const isIntEnabled = (name, envKey) => {
|
|
703
|
+
const ints = loadIntegrations();
|
|
704
|
+
return !!(process.env[envKey] || (ints[name] && Object.values(ints[name]).some(v => v && v.length > 0)));
|
|
705
|
+
};
|
|
706
|
+
this.app.get('/api/skills', (_req, res) => {
|
|
707
|
+
res.json([
|
|
708
|
+
// Built-in skills
|
|
709
|
+
{ name: 'shell', description: 'Execute shell commands', type: 'builtin', enabled: true, category: 'system' },
|
|
710
|
+
{ name: 'filesystem', description: 'Read/write files', type: 'builtin', enabled: true, category: 'system' },
|
|
711
|
+
{ name: 'browser', description: 'Browse websites & extract data', type: 'builtin', enabled: this.browser.isAvailable, category: 'system' },
|
|
712
|
+
{ name: 'scheduler', description: 'Schedule background tasks', type: 'builtin', enabled: true, category: 'system' },
|
|
713
|
+
{ name: 'memory', description: 'Persistent memory storage', type: 'builtin', enabled: true, category: 'system' },
|
|
714
|
+
{ name: 'inbox', description: 'Track and reply to messages', type: 'builtin', enabled: true, category: 'system' },
|
|
715
|
+
// Messaging - Mainstream
|
|
716
|
+
{ name: 'email', description: 'Send/receive emails via IMAP', type: 'integration', enabled: isIntEnabled('email', 'EMAIL_USERNAME'), category: 'messaging',
|
|
717
|
+
configFields: [
|
|
718
|
+
{ key: 'host', label: 'IMAP Host', placeholder: 'imap.gmail.com' },
|
|
719
|
+
{ key: 'port', label: 'IMAP Port', placeholder: '993' },
|
|
720
|
+
{ key: 'username', label: 'Email', placeholder: 'you@gmail.com' },
|
|
721
|
+
{ key: 'password', label: 'App Password', placeholder: '••••••••', secret: true },
|
|
722
|
+
{ key: 'folders', label: 'Folders to Monitor', placeholder: 'INBOX, Sent, Drafts, Trash' },
|
|
723
|
+
{ key: 'useIdle', label: 'Real-time IDLE', placeholder: 'true' },
|
|
724
|
+
{ key: 'pollInterval', label: 'Poll Interval (minutes)', placeholder: '1' },
|
|
725
|
+
] },
|
|
726
|
+
{ name: 'telegram', description: 'Telegram bot integration', type: 'integration', enabled: isIntEnabled('telegram', 'TELEGRAM_BOT_TOKEN'), category: 'messaging',
|
|
727
|
+
configFields: [
|
|
728
|
+
{ key: 'botToken', label: 'Bot Token', placeholder: '123456:ABC-DEF...', secret: true },
|
|
729
|
+
{ key: 'chatId', label: 'Chat ID (optional)', placeholder: '123456789' },
|
|
730
|
+
] },
|
|
731
|
+
{ name: 'discord', description: 'Discord bot integration', type: 'integration', enabled: isIntEnabled('discord', 'DISCORD_BOT_TOKEN'), category: 'messaging',
|
|
732
|
+
configFields: [
|
|
733
|
+
{ key: 'botToken', label: 'Bot Token', placeholder: 'MTIz...', secret: true },
|
|
734
|
+
{ key: 'guildId', label: 'Server ID (optional)', placeholder: '123456789' },
|
|
735
|
+
] },
|
|
736
|
+
{ name: 'slack', description: 'Slack bot integration', type: 'integration', enabled: isIntEnabled('slack', 'SLACK_BOT_TOKEN'), category: 'messaging',
|
|
737
|
+
configFields: [
|
|
738
|
+
{ key: 'botToken', label: 'Bot Token', placeholder: 'xoxb-...', secret: true },
|
|
739
|
+
{ key: 'channel', label: 'Channel', placeholder: '#general' },
|
|
740
|
+
] },
|
|
741
|
+
{ name: 'whatsapp', description: 'WhatsApp via Twilio', type: 'integration', enabled: isIntEnabled('whatsapp', 'TWILIO_ACCOUNT_SID'), category: 'messaging',
|
|
742
|
+
configFields: [
|
|
743
|
+
{ key: 'accountSid', label: 'Account SID', placeholder: 'AC...', secret: true },
|
|
744
|
+
{ key: 'authToken', label: 'Auth Token', placeholder: '••••••••', secret: true },
|
|
745
|
+
{ key: 'phoneNumber', label: 'Twilio Phone', placeholder: '+1234567890' },
|
|
746
|
+
] },
|
|
747
|
+
// Messaging - Privacy & Enterprise
|
|
748
|
+
{ name: 'signal', description: 'Signal via signal-cli', type: 'integration', enabled: isIntEnabled('signal', 'SIGNAL_CLI_PATH'), category: 'messaging',
|
|
749
|
+
configFields: [
|
|
750
|
+
{ key: 'cliPath', label: 'signal-cli Path', placeholder: '/usr/bin/signal-cli' },
|
|
751
|
+
{ key: 'phoneNumber', label: 'Your Phone Number', placeholder: '+1234567890' },
|
|
752
|
+
] },
|
|
753
|
+
{ name: 'imessage', description: 'iMessage via BlueBubbles', type: 'integration', enabled: isIntEnabled('imessage', 'BLUEBUBBLES_URL'), category: 'messaging',
|
|
754
|
+
configFields: [
|
|
755
|
+
{ key: 'url', label: 'BlueBubbles URL', placeholder: 'http://localhost:1234' },
|
|
756
|
+
{ key: 'password', label: 'Password', placeholder: '••••••••', secret: true },
|
|
757
|
+
] },
|
|
758
|
+
{ name: 'teams', description: 'Microsoft Teams integration', type: 'integration', enabled: isIntEnabled('teams', 'MS_TEAMS_WEBHOOK'), category: 'messaging',
|
|
759
|
+
configFields: [
|
|
760
|
+
{ key: 'webhookUrl', label: 'Webhook URL', placeholder: 'https://outlook.office.com/webhook/...', secret: true },
|
|
761
|
+
{ key: 'tenantId', label: 'Tenant ID (optional)', placeholder: '...' },
|
|
762
|
+
] },
|
|
763
|
+
{ name: 'matrix', description: 'Matrix protocol integration', type: 'integration', enabled: isIntEnabled('matrix', 'MATRIX_HOMESERVER'), category: 'messaging',
|
|
764
|
+
configFields: [
|
|
765
|
+
{ key: 'homeserver', label: 'Homeserver URL', placeholder: 'https://matrix.org' },
|
|
766
|
+
{ key: 'userId', label: 'User ID', placeholder: '@user:matrix.org' },
|
|
767
|
+
{ key: 'accessToken', label: 'Access Token', placeholder: '••••••••', secret: true },
|
|
768
|
+
] },
|
|
769
|
+
{ name: 'nostr', description: 'Nostr NIP-04 messages', type: 'integration', enabled: isIntEnabled('nostr', 'NOSTR_PRIVATE_KEY'), category: 'messaging',
|
|
770
|
+
configFields: [
|
|
771
|
+
{ key: 'privateKey', label: 'Private Key (nsec)', placeholder: 'nsec1...', secret: true },
|
|
772
|
+
{ key: 'relay', label: 'Relay URL', placeholder: 'wss://relay.damus.io' },
|
|
773
|
+
] },
|
|
774
|
+
{ name: 'zalo', description: 'Zalo bot integration', type: 'integration', enabled: isIntEnabled('zalo', 'ZALO_APP_ID'), category: 'messaging',
|
|
775
|
+
configFields: [
|
|
776
|
+
{ key: 'appId', label: 'App ID', placeholder: '123456' },
|
|
777
|
+
{ key: 'appSecret', label: 'App Secret', placeholder: '••••••••', secret: true },
|
|
778
|
+
{ key: 'accessToken', label: 'Access Token', placeholder: '••••••••', secret: true },
|
|
779
|
+
] },
|
|
780
|
+
// AI Models - Cloud
|
|
781
|
+
{ name: 'openai', description: 'OpenAI GPT-4, o1, GPT-5', type: 'integration', enabled: isIntEnabled('openai', 'OPENAI_API_KEY'), category: 'ai',
|
|
782
|
+
configFields: [
|
|
783
|
+
{ key: 'apiKey', label: 'API Key', placeholder: 'sk-...', secret: true },
|
|
784
|
+
{ key: 'model', label: 'Model', placeholder: 'gpt-4o' },
|
|
785
|
+
] },
|
|
786
|
+
{ name: 'anthropic', description: 'Anthropic Claude 3.5/4', type: 'integration', enabled: isIntEnabled('anthropic', 'ANTHROPIC_API_KEY'), category: 'ai',
|
|
787
|
+
configFields: [
|
|
788
|
+
{ key: 'apiKey', label: 'API Key', placeholder: 'sk-ant-...', secret: true },
|
|
789
|
+
{ key: 'model', label: 'Model', placeholder: 'claude-3-5-sonnet' },
|
|
790
|
+
] },
|
|
791
|
+
{ name: 'google', description: 'Google Gemini 1.5/2.5', type: 'integration', enabled: isIntEnabled('google', 'GOOGLE_API_KEY'), category: 'ai',
|
|
792
|
+
configFields: [
|
|
793
|
+
{ key: 'apiKey', label: 'API Key', placeholder: 'AIza...', secret: true },
|
|
794
|
+
{ key: 'model', label: 'Model', placeholder: 'gemini-1.5-pro' },
|
|
795
|
+
] },
|
|
796
|
+
{ name: 'xai', description: 'xAI Grok', type: 'integration', enabled: isIntEnabled('xai', 'XAI_API_KEY'), category: 'ai',
|
|
797
|
+
configFields: [
|
|
798
|
+
{ key: 'apiKey', label: 'API Key', placeholder: 'xai-...', secret: true },
|
|
799
|
+
{ key: 'model', label: 'Model', placeholder: 'grok-2' },
|
|
800
|
+
] },
|
|
801
|
+
{ name: 'mistral', description: 'Mistral AI models', type: 'integration', enabled: isIntEnabled('mistral', 'MISTRAL_API_KEY'), category: 'ai',
|
|
802
|
+
configFields: [
|
|
803
|
+
{ key: 'apiKey', label: 'API Key', placeholder: '••••••••', secret: true },
|
|
804
|
+
{ key: 'model', label: 'Model', placeholder: 'mistral-large' },
|
|
805
|
+
] },
|
|
806
|
+
{ name: 'deepseek', description: 'DeepSeek V3 & R1', type: 'integration', enabled: isIntEnabled('deepseek', 'DEEPSEEK_API_KEY'), category: 'ai',
|
|
807
|
+
configFields: [
|
|
808
|
+
{ key: 'apiKey', label: 'API Key', placeholder: 'sk-...', secret: true },
|
|
809
|
+
{ key: 'model', label: 'Model', placeholder: 'deepseek-chat' },
|
|
810
|
+
] },
|
|
811
|
+
{ name: 'perplexity', description: 'Perplexity Search-augmented', type: 'integration', enabled: isIntEnabled('perplexity', 'PERPLEXITY_API_KEY'), category: 'ai',
|
|
812
|
+
configFields: [
|
|
813
|
+
{ key: 'apiKey', label: 'API Key', placeholder: 'pplx-...', secret: true },
|
|
814
|
+
{ key: 'model', label: 'Model', placeholder: 'sonar' },
|
|
815
|
+
] },
|
|
816
|
+
// AI Local
|
|
817
|
+
{ name: 'ollama', description: 'Ollama local models', type: 'integration', enabled: isIntEnabled('ollama', 'OLLAMA_URL'), category: 'ai',
|
|
818
|
+
configFields: [
|
|
819
|
+
{ key: 'url', label: 'Ollama URL', placeholder: 'http://localhost:11434' },
|
|
820
|
+
{ key: 'model', label: 'Model', placeholder: 'llama3.1' },
|
|
821
|
+
] },
|
|
822
|
+
{ name: 'lmstudio', description: 'LM Studio local models', type: 'integration', enabled: isIntEnabled('lmstudio', 'LMSTUDIO_URL'), category: 'ai',
|
|
823
|
+
configFields: [
|
|
824
|
+
{ key: 'url', label: 'LM Studio URL', placeholder: 'http://localhost:1234' },
|
|
825
|
+
{ key: 'model', label: 'Model', placeholder: 'local-model' },
|
|
826
|
+
] },
|
|
827
|
+
// Productivity & Workspace
|
|
828
|
+
{ name: 'notion', description: 'Notion pages & databases', type: 'integration', enabled: isIntEnabled('notion', 'NOTION_TOKEN'), category: 'productivity',
|
|
829
|
+
configFields: [
|
|
830
|
+
{ key: 'token', label: 'Integration Token', placeholder: 'secret_...', secret: true },
|
|
831
|
+
{ key: 'databaseId', label: 'Database ID (optional)', placeholder: '...' },
|
|
832
|
+
] },
|
|
833
|
+
{ name: 'trello', description: 'Trello boards & cards', type: 'integration', enabled: isIntEnabled('trello', 'TRELLO_API_KEY'), category: 'productivity',
|
|
834
|
+
configFields: [
|
|
835
|
+
{ key: 'apiKey', label: 'API Key', placeholder: '••••••••', secret: true },
|
|
836
|
+
{ key: 'token', label: 'Token', placeholder: '••••••••', secret: true },
|
|
837
|
+
{ key: 'boardId', label: 'Board ID', placeholder: '...' },
|
|
838
|
+
] },
|
|
839
|
+
{ name: 'github', description: 'GitHub repos, issues, PRs', type: 'integration', enabled: isIntEnabled('github', 'GITHUB_TOKEN'), category: 'productivity',
|
|
840
|
+
configFields: [
|
|
841
|
+
{ key: 'token', label: 'Personal Access Token', placeholder: 'ghp_...', secret: true },
|
|
842
|
+
{ key: 'owner', label: 'Default Owner', placeholder: 'username' },
|
|
843
|
+
{ key: 'repo', label: 'Default Repo', placeholder: 'repo-name' },
|
|
844
|
+
] },
|
|
845
|
+
{ name: 'obsidian', description: 'Obsidian vault access', type: 'integration', enabled: isIntEnabled('obsidian', 'OBSIDIAN_VAULT_PATH'), category: 'productivity',
|
|
846
|
+
configFields: [
|
|
847
|
+
{ key: 'vaultPath', label: 'Vault Path', placeholder: '/home/user/Obsidian' },
|
|
848
|
+
] },
|
|
849
|
+
{ name: 'apple-notes', description: 'Apple Notes (macOS)', type: 'integration', enabled: isIntEnabled('apple-notes', 'APPLE_NOTES_ENABLED'), category: 'productivity',
|
|
850
|
+
configFields: [
|
|
851
|
+
{ key: 'enabled', label: 'Enabled', placeholder: 'true' },
|
|
852
|
+
] },
|
|
853
|
+
// Smart Home & Media
|
|
854
|
+
{ name: 'home-assistant', description: 'Home Assistant hub', type: 'integration', enabled: isIntEnabled('home-assistant', 'HASS_URL'), category: 'smart-home',
|
|
855
|
+
configFields: [
|
|
856
|
+
{ key: 'url', label: 'Home Assistant URL', placeholder: 'http://homeassistant:8123' },
|
|
857
|
+
{ key: 'token', label: 'Long-Lived Access Token', placeholder: '••••••••', secret: true },
|
|
858
|
+
] },
|
|
859
|
+
{ name: 'hue', description: 'Philips Hue lighting', type: 'integration', enabled: isIntEnabled('hue', 'HUE_BRIDGE_IP'), category: 'smart-home',
|
|
860
|
+
configFields: [
|
|
861
|
+
{ key: 'bridgeIp', label: 'Bridge IP', placeholder: '192.168.1.100' },
|
|
862
|
+
{ key: 'username', label: 'API Key', placeholder: '••••••••', secret: true },
|
|
863
|
+
] },
|
|
864
|
+
{ name: 'spotify', description: 'Spotify control', type: 'integration', enabled: isIntEnabled('spotify', 'SPOTIFY_CLIENT_ID'), category: 'smart-home',
|
|
865
|
+
configFields: [
|
|
866
|
+
{ key: 'clientId', label: 'Client ID', placeholder: '...' },
|
|
867
|
+
{ key: 'clientSecret', label: 'Client Secret', placeholder: '••••••••', secret: true },
|
|
868
|
+
{ key: 'refreshToken', label: 'Refresh Token', placeholder: '••••••••', secret: true },
|
|
869
|
+
] },
|
|
870
|
+
{ name: 'sonos', description: 'Sonos multi-room audio', type: 'integration', enabled: isIntEnabled('sonos', 'SONOS_IP'), category: 'smart-home',
|
|
871
|
+
configFields: [
|
|
872
|
+
{ key: 'ip', label: 'Speaker IP', placeholder: '192.168.1.101' },
|
|
873
|
+
] },
|
|
874
|
+
// Utilities
|
|
875
|
+
{ name: 'weather', description: 'Weather forecasts', type: 'integration', enabled: isIntEnabled('weather', 'OPENWEATHER_API_KEY'), category: 'utility',
|
|
876
|
+
configFields: [
|
|
877
|
+
{ key: 'apiKey', label: 'OpenWeather API Key', placeholder: '••••••••', secret: true },
|
|
878
|
+
{ key: 'city', label: 'Default City', placeholder: 'London' },
|
|
879
|
+
] },
|
|
880
|
+
{ name: 'webhooks', description: 'HTTP webhook triggers', type: 'integration', enabled: isIntEnabled('webhooks', 'WEBHOOK_SECRET'), category: 'utility',
|
|
881
|
+
configFields: [
|
|
882
|
+
{ key: 'secret', label: 'Webhook Secret', placeholder: '••••••••', secret: true },
|
|
883
|
+
{ key: 'port', label: 'Port', placeholder: '8081' },
|
|
884
|
+
] },
|
|
885
|
+
{ name: 'image-gen', description: 'AI Image Generation', type: 'integration', enabled: isIntEnabled('image-gen', 'IMAGE_API_KEY'), category: 'utility',
|
|
886
|
+
configFields: [
|
|
887
|
+
{ key: 'provider', label: 'Provider', placeholder: 'openai, stability, replicate' },
|
|
888
|
+
{ key: 'apiKey', label: 'API Key', placeholder: '••••••••', secret: true },
|
|
889
|
+
] },
|
|
890
|
+
{ name: '1password', description: '1Password secrets', type: 'integration', enabled: isIntEnabled('1password', 'OP_SERVICE_ACCOUNT_TOKEN'), category: 'utility',
|
|
891
|
+
configFields: [
|
|
892
|
+
{ key: 'serviceAccountToken', label: 'Service Account Token', placeholder: 'ops_...', secret: true },
|
|
893
|
+
{ key: 'vault', label: 'Vault Name', placeholder: 'Private' },
|
|
894
|
+
] },
|
|
895
|
+
{ name: 'gmail', description: 'Gmail Pub/Sub triggers', type: 'integration', enabled: isIntEnabled('gmail', 'GMAIL_REFRESH_TOKEN'), category: 'utility',
|
|
896
|
+
configFields: [
|
|
897
|
+
{ key: 'clientId', label: 'Client ID', placeholder: '...' },
|
|
898
|
+
{ key: 'clientSecret', label: 'Client Secret', placeholder: '••••••••', secret: true },
|
|
899
|
+
{ key: 'refreshToken', label: 'Refresh Token', placeholder: '••••••••', secret: true },
|
|
900
|
+
] },
|
|
901
|
+
]);
|
|
902
|
+
});
|
|
903
|
+
this.app.get('/api/skills/config/:name', (req, res) => {
|
|
904
|
+
const ints = loadIntegrations();
|
|
905
|
+
const cfg = ints[req.params.name] || {};
|
|
906
|
+
// Mask secret values
|
|
907
|
+
const masked = {};
|
|
908
|
+
for (const [k, v] of Object.entries(cfg)) {
|
|
909
|
+
masked[k] = v && v.length > 4 ? v.substring(0, 3) + '•'.repeat(v.length - 3) : v ? '••••' : '';
|
|
910
|
+
}
|
|
911
|
+
res.json({ name: req.params.name, config: masked, hasConfig: Object.keys(cfg).length > 0 });
|
|
912
|
+
});
|
|
913
|
+
this.app.post('/api/skills/config/:name', (req, res) => {
|
|
914
|
+
try {
|
|
915
|
+
const ints = loadIntegrations();
|
|
916
|
+
const existing = ints[req.params.name] || {};
|
|
917
|
+
const incoming = req.body.config || {};
|
|
918
|
+
// Only overwrite non-empty values (preserve existing if field left empty)
|
|
919
|
+
for (const [k, v] of Object.entries(incoming)) {
|
|
920
|
+
if (v && v.length > 0 && !v.includes('•'))
|
|
921
|
+
existing[k] = v;
|
|
922
|
+
}
|
|
923
|
+
ints[req.params.name] = existing;
|
|
924
|
+
saveIntegrations(ints);
|
|
925
|
+
res.json({ success: true });
|
|
926
|
+
}
|
|
927
|
+
catch (e) {
|
|
928
|
+
res.status(500).json({ error: e.message });
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
this.app.delete('/api/skills/config/:name', (req, res) => {
|
|
932
|
+
try {
|
|
933
|
+
const ints = loadIntegrations();
|
|
934
|
+
delete ints[req.params.name];
|
|
935
|
+
saveIntegrations(ints);
|
|
936
|
+
res.json({ success: true });
|
|
937
|
+
}
|
|
938
|
+
catch (e) {
|
|
939
|
+
res.status(500).json({ error: e.message });
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
// ============ DIRECT EMAIL API ============
|
|
943
|
+
this.app.post('/api/email/send', async (req, res) => {
|
|
944
|
+
try {
|
|
945
|
+
const { to, subject, body } = req.body;
|
|
946
|
+
if (!to || !subject) {
|
|
947
|
+
res.status(400).json({ error: 'to and subject are required' });
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
const ints = loadIntegrations();
|
|
951
|
+
const cfg = ints['email'] || {};
|
|
952
|
+
const host = cfg.host || process.env.EMAIL_HOST || 'smtp.gmail.com';
|
|
953
|
+
const port = parseInt(cfg.port || process.env.EMAIL_PORT || '587', 10);
|
|
954
|
+
const username = cfg.username || process.env.EMAIL_USERNAME;
|
|
955
|
+
const password = cfg.password || process.env.EMAIL_PASSWORD;
|
|
956
|
+
if (!username || !password) {
|
|
957
|
+
res.status(400).json({ error: 'Email not configured. Go to Skills → Email → Configure first.' });
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
const nodemailer = require('nodemailer');
|
|
961
|
+
const transporter = nodemailer.createTransport({ host, port, secure: port === 465, auth: { user: username, pass: password } });
|
|
962
|
+
await transporter.sendMail({
|
|
963
|
+
from: `LinguClaw <${username}>`,
|
|
964
|
+
to,
|
|
965
|
+
subject,
|
|
966
|
+
text: body || '',
|
|
967
|
+
html: body ? `<div style="font-family:sans-serif;line-height:1.6">${body.replace(/\n/g, '<br>')}</div>` : ''
|
|
968
|
+
});
|
|
969
|
+
res.json({ success: true, message: `Email sent to ${to}` });
|
|
970
|
+
}
|
|
971
|
+
catch (e) {
|
|
972
|
+
const msg = e.message || '';
|
|
973
|
+
let hint = '';
|
|
974
|
+
if (msg.includes('EAUTH') || msg.includes('Invalid login'))
|
|
975
|
+
hint = ' — Check credentials. For Gmail use App Password.';
|
|
976
|
+
else if (msg.includes('ECONNREFUSED'))
|
|
977
|
+
hint = ' — Cannot connect to SMTP server.';
|
|
978
|
+
res.status(500).json({ error: 'Send failed: ' + msg + hint });
|
|
979
|
+
}
|
|
980
|
+
});
|
|
981
|
+
// ============ TELEGRAM API ============
|
|
982
|
+
this.app.post('/api/telegram/send', async (req, res) => {
|
|
983
|
+
try {
|
|
984
|
+
const { chatId, message } = req.body;
|
|
985
|
+
if (!message) {
|
|
986
|
+
res.status(400).json({ error: 'message required' });
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
const ints = loadIntegrations();
|
|
990
|
+
const cfg = ints['telegram'] || {};
|
|
991
|
+
const token = cfg.botToken || process.env.TELEGRAM_BOT_TOKEN;
|
|
992
|
+
const target = chatId || cfg.chatId || process.env.TELEGRAM_CHAT_ID;
|
|
993
|
+
if (!token) {
|
|
994
|
+
res.status(400).json({ error: 'Telegram not configured. Go to Skills → Telegram → Configure.' });
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
if (!target) {
|
|
998
|
+
res.status(400).json({ error: 'Chat ID required. Set it in Skills → Telegram → Configure or provide chatId.' });
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
const https = require('https');
|
|
1002
|
+
const postData = JSON.stringify({ chat_id: target, text: message, parse_mode: 'HTML' });
|
|
1003
|
+
const result = await new Promise((resolve, reject) => {
|
|
1004
|
+
const r = https.request({ hostname: 'api.telegram.org', path: `/bot${token}/sendMessage`, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData) } }, (resp) => {
|
|
1005
|
+
let d = '';
|
|
1006
|
+
resp.on('data', (c) => d += c);
|
|
1007
|
+
resp.on('end', () => { try {
|
|
1008
|
+
resolve(JSON.parse(d));
|
|
1009
|
+
}
|
|
1010
|
+
catch {
|
|
1011
|
+
resolve({ ok: false, description: d });
|
|
1012
|
+
} });
|
|
1013
|
+
});
|
|
1014
|
+
r.on('error', reject);
|
|
1015
|
+
r.write(postData);
|
|
1016
|
+
r.end();
|
|
1017
|
+
});
|
|
1018
|
+
if (result.ok)
|
|
1019
|
+
res.json({ success: true, message: `Telegram message sent to ${target}` });
|
|
1020
|
+
else
|
|
1021
|
+
res.status(500).json({ error: 'Telegram error: ' + (result.description || JSON.stringify(result)) });
|
|
1022
|
+
}
|
|
1023
|
+
catch (e) {
|
|
1024
|
+
res.status(500).json({ error: e.message });
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
// ============ DISCORD API ============
|
|
1028
|
+
this.app.post('/api/discord/send', async (req, res) => {
|
|
1029
|
+
try {
|
|
1030
|
+
const { channelId, message } = req.body;
|
|
1031
|
+
if (!message) {
|
|
1032
|
+
res.status(400).json({ error: 'message required' });
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
const ints = loadIntegrations();
|
|
1036
|
+
const cfg = ints['discord'] || {};
|
|
1037
|
+
const token = cfg.botToken || process.env.DISCORD_BOT_TOKEN;
|
|
1038
|
+
if (!token) {
|
|
1039
|
+
res.status(400).json({ error: 'Discord not configured. Go to Skills → Discord → Configure.' });
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
if (!channelId) {
|
|
1043
|
+
res.status(400).json({ error: 'channelId required.' });
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
const https = require('https');
|
|
1047
|
+
const postData = JSON.stringify({ content: message });
|
|
1048
|
+
const result = await new Promise((resolve, reject) => {
|
|
1049
|
+
const r = https.request({ hostname: 'discord.com', path: `/api/v10/channels/${channelId}/messages`, method: 'POST', headers: { 'Authorization': `Bot ${token}`, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData) } }, (resp) => {
|
|
1050
|
+
let d = '';
|
|
1051
|
+
resp.on('data', (c) => d += c);
|
|
1052
|
+
resp.on('end', () => { try {
|
|
1053
|
+
resolve(JSON.parse(d));
|
|
1054
|
+
}
|
|
1055
|
+
catch {
|
|
1056
|
+
resolve({ error: d });
|
|
1057
|
+
} });
|
|
1058
|
+
});
|
|
1059
|
+
r.on('error', reject);
|
|
1060
|
+
r.write(postData);
|
|
1061
|
+
r.end();
|
|
1062
|
+
});
|
|
1063
|
+
if (result.id)
|
|
1064
|
+
res.json({ success: true, message: `Discord message sent to channel ${channelId}` });
|
|
1065
|
+
else
|
|
1066
|
+
res.status(500).json({ error: 'Discord error: ' + (result.message || JSON.stringify(result)) });
|
|
1067
|
+
}
|
|
1068
|
+
catch (e) {
|
|
1069
|
+
res.status(500).json({ error: e.message });
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
// ============ SLACK API ============
|
|
1073
|
+
this.app.post('/api/slack/send', async (req, res) => {
|
|
1074
|
+
try {
|
|
1075
|
+
const { channel, message } = req.body;
|
|
1076
|
+
if (!message) {
|
|
1077
|
+
res.status(400).json({ error: 'message required' });
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
const ints = loadIntegrations();
|
|
1081
|
+
const cfg = ints['slack'] || {};
|
|
1082
|
+
const token = cfg.botToken || process.env.SLACK_BOT_TOKEN;
|
|
1083
|
+
const target = channel || cfg.channel || '#general';
|
|
1084
|
+
if (!token) {
|
|
1085
|
+
res.status(400).json({ error: 'Slack not configured. Go to Skills → Slack → Configure.' });
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
const https = require('https');
|
|
1089
|
+
const postData = JSON.stringify({ channel: target, text: message });
|
|
1090
|
+
const result = await new Promise((resolve, reject) => {
|
|
1091
|
+
const r = https.request({ hostname: 'slack.com', path: '/api/chat.postMessage', method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData) } }, (resp) => {
|
|
1092
|
+
let d = '';
|
|
1093
|
+
resp.on('data', (c) => d += c);
|
|
1094
|
+
resp.on('end', () => { try {
|
|
1095
|
+
resolve(JSON.parse(d));
|
|
1096
|
+
}
|
|
1097
|
+
catch {
|
|
1098
|
+
resolve({ ok: false, error: d });
|
|
1099
|
+
} });
|
|
1100
|
+
});
|
|
1101
|
+
r.on('error', reject);
|
|
1102
|
+
r.write(postData);
|
|
1103
|
+
r.end();
|
|
1104
|
+
});
|
|
1105
|
+
if (result.ok)
|
|
1106
|
+
res.json({ success: true, message: `Slack message sent to ${target}` });
|
|
1107
|
+
else
|
|
1108
|
+
res.status(500).json({ error: 'Slack error: ' + (result.error || JSON.stringify(result)) });
|
|
1109
|
+
}
|
|
1110
|
+
catch (e) {
|
|
1111
|
+
res.status(500).json({ error: e.message });
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
// ============ WHATSAPP API (Twilio) ============
|
|
1115
|
+
this.app.post('/api/whatsapp/send', async (req, res) => {
|
|
1116
|
+
try {
|
|
1117
|
+
const { to, message } = req.body;
|
|
1118
|
+
if (!to || !message) {
|
|
1119
|
+
res.status(400).json({ error: 'to and message required' });
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
const ints = loadIntegrations();
|
|
1123
|
+
const cfg = ints['whatsapp'] || {};
|
|
1124
|
+
const sid = cfg.accountSid || process.env.TWILIO_ACCOUNT_SID;
|
|
1125
|
+
const authToken = cfg.authToken || process.env.TWILIO_AUTH_TOKEN;
|
|
1126
|
+
const from = cfg.phoneNumber || process.env.TWILIO_PHONE_NUMBER;
|
|
1127
|
+
if (!sid || !authToken || !from) {
|
|
1128
|
+
res.status(400).json({ error: 'WhatsApp not configured. Go to Skills → WhatsApp → Configure.' });
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
const https = require('https');
|
|
1132
|
+
const postData = `To=whatsapp:${to}&From=whatsapp:${from}&Body=${encodeURIComponent(message)}`;
|
|
1133
|
+
const auth = Buffer.from(`${sid}:${authToken}`).toString('base64');
|
|
1134
|
+
const result = await new Promise((resolve, reject) => {
|
|
1135
|
+
const r = https.request({ hostname: 'api.twilio.com', path: `/2010-04-01/Accounts/${sid}/Messages.json`, method: 'POST', headers: { 'Authorization': `Basic ${auth}`, 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': Buffer.byteLength(postData) } }, (resp) => {
|
|
1136
|
+
let d = '';
|
|
1137
|
+
resp.on('data', (c) => d += c);
|
|
1138
|
+
resp.on('end', () => { try {
|
|
1139
|
+
resolve(JSON.parse(d));
|
|
1140
|
+
}
|
|
1141
|
+
catch {
|
|
1142
|
+
resolve({ error: d });
|
|
1143
|
+
} });
|
|
1144
|
+
});
|
|
1145
|
+
r.on('error', reject);
|
|
1146
|
+
r.write(postData);
|
|
1147
|
+
r.end();
|
|
1148
|
+
});
|
|
1149
|
+
if (result.sid)
|
|
1150
|
+
res.json({ success: true, message: `WhatsApp message sent to ${to}` });
|
|
1151
|
+
else
|
|
1152
|
+
res.status(500).json({ error: 'WhatsApp error: ' + (result.message || JSON.stringify(result)) });
|
|
1153
|
+
}
|
|
1154
|
+
catch (e) {
|
|
1155
|
+
res.status(500).json({ error: e.message });
|
|
1156
|
+
}
|
|
1157
|
+
});
|
|
1158
|
+
// ============ INBOX API ============
|
|
1159
|
+
// Use the class inbox instance (this.inbox) that was created in constructor
|
|
1160
|
+
// Get unread messages
|
|
1161
|
+
this.app.get('/api/inbox/unread', (_req, res) => {
|
|
1162
|
+
res.json(this.inbox.getUnread());
|
|
1163
|
+
});
|
|
1164
|
+
// Get all messages (paginated)
|
|
1165
|
+
this.app.get('/api/inbox/messages', (req, res) => {
|
|
1166
|
+
const limit = parseInt(req.query.limit) || 50;
|
|
1167
|
+
const offset = parseInt(req.query.offset) || 0;
|
|
1168
|
+
res.json({
|
|
1169
|
+
messages: this.inbox.getMessages(limit, offset),
|
|
1170
|
+
unread: this.inbox.getUnreadCount()
|
|
1171
|
+
});
|
|
1172
|
+
});
|
|
1173
|
+
// Get message threads
|
|
1174
|
+
this.app.get('/api/inbox/threads', (_req, res) => {
|
|
1175
|
+
res.json(this.inbox.getThreads());
|
|
1176
|
+
});
|
|
1177
|
+
// Get single thread
|
|
1178
|
+
this.app.get('/api/inbox/threads/:id', (req, res) => {
|
|
1179
|
+
const thread = this.inbox.getThread(req.params.id);
|
|
1180
|
+
if (!thread) {
|
|
1181
|
+
res.status(404).json({ error: 'Thread not found' });
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
res.json(thread);
|
|
1185
|
+
});
|
|
1186
|
+
// Mark message as read
|
|
1187
|
+
this.app.post('/api/inbox/messages/:id/read', (req, res) => {
|
|
1188
|
+
this.inbox.markAsRead(req.params.id);
|
|
1189
|
+
res.json({ success: true });
|
|
1190
|
+
});
|
|
1191
|
+
// Mark thread as read
|
|
1192
|
+
this.app.post('/api/inbox/threads/:id/read', (req, res) => {
|
|
1193
|
+
this.inbox.markThreadAsRead(req.params.id);
|
|
1194
|
+
res.json({ success: true });
|
|
1195
|
+
});
|
|
1196
|
+
// Get unread counts
|
|
1197
|
+
this.app.get('/api/inbox/counts', (_req, res) => {
|
|
1198
|
+
res.json({
|
|
1199
|
+
total: this.inbox.getUnreadCount(),
|
|
1200
|
+
byPlatform: this.inbox.getUnreadByPlatform()
|
|
1201
|
+
});
|
|
1202
|
+
});
|
|
1203
|
+
// Receive incoming message (webhook endpoint)
|
|
1204
|
+
this.app.post('/api/inbox/receive', async (req, res) => {
|
|
1205
|
+
try {
|
|
1206
|
+
const { platform, from, to, subject, body, threadId, inReplyTo } = req.body;
|
|
1207
|
+
if (!platform || !from || !to || !body) {
|
|
1208
|
+
res.status(400).json({ error: 'platform, from, to, body required' });
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
// Store message
|
|
1212
|
+
const msg = this.inbox.addMessage({
|
|
1213
|
+
platform,
|
|
1214
|
+
from,
|
|
1215
|
+
to,
|
|
1216
|
+
subject,
|
|
1217
|
+
body,
|
|
1218
|
+
threadId,
|
|
1219
|
+
inReplyTo
|
|
1220
|
+
});
|
|
1221
|
+
if (!msg) {
|
|
1222
|
+
res.status(409).json({ error: 'Duplicate message' });
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
// AI Analysis of incoming message
|
|
1226
|
+
try {
|
|
1227
|
+
const provider = this.providerManager.createFromEnv();
|
|
1228
|
+
if (provider) {
|
|
1229
|
+
const analysisMessages = [
|
|
1230
|
+
{ role: 'system', content: 'You are an assistant that analyzes incoming messages and provides a brief summary and suggested reply. Respond in JSON format: { "summary": "...", "suggestedReply": "..." }' },
|
|
1231
|
+
{ role: 'user', content: `Analyze this ${platform} message from ${from}${subject ? ` about "${subject}"` : ''}:
|
|
1232
|
+
|
|
1233
|
+
${body}
|
|
1234
|
+
|
|
1235
|
+
Provide a brief summary and suggested reply.` }
|
|
1236
|
+
];
|
|
1237
|
+
const response = await provider.complete(analysisMessages, 0.3, 500);
|
|
1238
|
+
if (response.content) {
|
|
1239
|
+
try {
|
|
1240
|
+
const analysis = JSON.parse(response.content);
|
|
1241
|
+
this.inbox.setAnalysis(msg.id, analysis.summary, analysis.suggestedReply);
|
|
1242
|
+
}
|
|
1243
|
+
catch (parseErr) {
|
|
1244
|
+
logger.debug(`Inbox analysis JSON parse failed: ${parseErr.message}`);
|
|
1245
|
+
this.inbox.setAnalysis(msg.id, response.content.substring(0, 200), undefined);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
catch (e) {
|
|
1251
|
+
// AI analysis failed, but message is still stored
|
|
1252
|
+
logger.warn('AI analysis failed for incoming message:', e);
|
|
1253
|
+
}
|
|
1254
|
+
// Broadcast to WebSocket clients
|
|
1255
|
+
this.broadcast({
|
|
1256
|
+
type: 'inbox_new',
|
|
1257
|
+
payload: { message: msg, unreadCount: this.inbox.getUnreadCount() }
|
|
1258
|
+
});
|
|
1259
|
+
res.json({ success: true, id: msg.id });
|
|
1260
|
+
}
|
|
1261
|
+
catch (e) {
|
|
1262
|
+
res.status(500).json({ error: e.message });
|
|
1263
|
+
}
|
|
1264
|
+
});
|
|
1265
|
+
// Poll for new messages (for platforms that support polling)
|
|
1266
|
+
this.app.post('/api/inbox/poll/:platform', async (req, res) => {
|
|
1267
|
+
const platform = req.params.platform;
|
|
1268
|
+
try {
|
|
1269
|
+
// This would integrate with platform-specific APIs to poll for new messages
|
|
1270
|
+
// For now, return success - actual implementation would vary by platform
|
|
1271
|
+
res.json({ success: true, polled: platform, newMessages: 0 });
|
|
1272
|
+
}
|
|
1273
|
+
catch (e) {
|
|
1274
|
+
res.status(500).json({ error: e.message });
|
|
1275
|
+
}
|
|
1276
|
+
});
|
|
1277
|
+
// Reply to a message (chat command integration)
|
|
1278
|
+
this.app.post('/api/inbox/reply', async (req, res) => {
|
|
1279
|
+
try {
|
|
1280
|
+
const { threadId, message, platform, to } = req.body;
|
|
1281
|
+
if (!threadId || !message) {
|
|
1282
|
+
res.status(400).json({ error: 'threadId and message required' });
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
// Get thread to find recipient
|
|
1286
|
+
const thread = this.inbox.getThread(threadId);
|
|
1287
|
+
if (!thread) {
|
|
1288
|
+
res.status(404).json({ error: 'Thread not found' });
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
// Send reply via appropriate platform
|
|
1292
|
+
const targetPlatform = platform || thread.platform;
|
|
1293
|
+
const targetTo = to || thread.participants.find((p) => p !== 'me') || thread.participants[0];
|
|
1294
|
+
// Execute messaging action
|
|
1295
|
+
const action = {
|
|
1296
|
+
action: 'send',
|
|
1297
|
+
platform: targetPlatform,
|
|
1298
|
+
to: targetTo,
|
|
1299
|
+
message,
|
|
1300
|
+
channelId: req.body.channelId,
|
|
1301
|
+
channel: req.body.channel
|
|
1302
|
+
};
|
|
1303
|
+
const result = await execMessagingAction(action);
|
|
1304
|
+
// Add sent message to thread
|
|
1305
|
+
this.inbox.addMessage({
|
|
1306
|
+
platform: targetPlatform,
|
|
1307
|
+
from: 'me',
|
|
1308
|
+
to: targetTo,
|
|
1309
|
+
body: message,
|
|
1310
|
+
threadId
|
|
1311
|
+
});
|
|
1312
|
+
res.json(result);
|
|
1313
|
+
}
|
|
1314
|
+
catch (e) {
|
|
1315
|
+
res.status(500).json({ error: e.message });
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
// Mark all messages as read
|
|
1319
|
+
this.app.post('/api/inbox/mark-all-read', (_req, res) => {
|
|
1320
|
+
const messages = this.inbox.getMessages(1000);
|
|
1321
|
+
messages.forEach(m => {
|
|
1322
|
+
if (!m.read)
|
|
1323
|
+
this.inbox.markAsRead(m.id);
|
|
1324
|
+
});
|
|
1325
|
+
res.json({ success: true, marked: messages.filter(m => !m.read).length });
|
|
1326
|
+
});
|
|
1327
|
+
// Delete message
|
|
1328
|
+
this.app.delete('/api/inbox/messages/:id', (req, res) => {
|
|
1329
|
+
// Note: InboxManager doesn't have delete method, we'll add a simple version
|
|
1330
|
+
this.inbox['messages'] = this.inbox['messages'].filter((m) => m.id !== req.params.id);
|
|
1331
|
+
this.inbox['saveToMemory']();
|
|
1332
|
+
res.json({ success: true });
|
|
1333
|
+
});
|
|
1334
|
+
// Manual check for new emails
|
|
1335
|
+
this.app.post('/api/inbox/check', async (_req, res) => {
|
|
1336
|
+
try {
|
|
1337
|
+
// Trigger IMAP poll if configured
|
|
1338
|
+
const integrationsPath = path_1.default.join(require('os').homedir(), '.linguclaw', 'integrations.json');
|
|
1339
|
+
let ints = {};
|
|
1340
|
+
try {
|
|
1341
|
+
ints = JSON.parse(require('fs').readFileSync(integrationsPath, 'utf8'));
|
|
1342
|
+
}
|
|
1343
|
+
catch (e) {
|
|
1344
|
+
logger.debug(`No integrations file: ${e.message}`);
|
|
1345
|
+
}
|
|
1346
|
+
const emailCfg = ints['email'];
|
|
1347
|
+
if (!emailCfg || !emailCfg.host) {
|
|
1348
|
+
res.status(400).json({ error: 'Email not configured' });
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
// Trigger manual poll via email receiver
|
|
1352
|
+
const folders = emailCfg.folders ? emailCfg.folders.split(',').map((f) => f.trim()) : ['INBOX'];
|
|
1353
|
+
// Force a poll by stopping and restarting
|
|
1354
|
+
this.emailReceiver.stop();
|
|
1355
|
+
setTimeout(() => {
|
|
1356
|
+
this.emailReceiver.startIMAP({
|
|
1357
|
+
host: emailCfg.host,
|
|
1358
|
+
port: parseInt(emailCfg.port || '993', 10),
|
|
1359
|
+
username: emailCfg.username,
|
|
1360
|
+
password: emailCfg.password,
|
|
1361
|
+
tls: true,
|
|
1362
|
+
pollInterval: 1,
|
|
1363
|
+
folders: folders,
|
|
1364
|
+
useIdle: false // Use polling for manual check
|
|
1365
|
+
});
|
|
1366
|
+
}, 100);
|
|
1367
|
+
res.json({ success: true, checked: folders.length, folders });
|
|
1368
|
+
}
|
|
1369
|
+
catch (e) {
|
|
1370
|
+
res.status(500).json({ error: e.message });
|
|
1371
|
+
}
|
|
1372
|
+
});
|
|
1373
|
+
// Serve main HTML - use static dashboard or fallback to inline
|
|
1374
|
+
// ============ WORKFLOW API ============
|
|
1375
|
+
const workflowEngine = (0, workflow_engine_1.getWorkflowEngine)();
|
|
1376
|
+
this.app.get('/api/workflows/node-definitions', (_req, res) => {
|
|
1377
|
+
res.json(workflowEngine.getNodeDefinitions());
|
|
1378
|
+
});
|
|
1379
|
+
this.app.get('/api/workflows', (_req, res) => {
|
|
1380
|
+
res.json(workflowEngine.listWorkflows());
|
|
1381
|
+
});
|
|
1382
|
+
this.app.post('/api/workflows', (req, res) => {
|
|
1383
|
+
const { name, description } = req.body;
|
|
1384
|
+
if (!name) {
|
|
1385
|
+
res.status(400).json({ error: 'name required' });
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
const wf = workflowEngine.createWorkflow(name, description || '');
|
|
1389
|
+
res.json(wf);
|
|
1390
|
+
});
|
|
1391
|
+
this.app.get('/api/workflows/:id', (req, res) => {
|
|
1392
|
+
const wf = workflowEngine.getWorkflow(req.params.id);
|
|
1393
|
+
if (!wf) {
|
|
1394
|
+
res.status(404).json({ error: 'Workflow not found' });
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
res.json(wf);
|
|
1398
|
+
});
|
|
1399
|
+
this.app.put('/api/workflows/:id', (req, res) => {
|
|
1400
|
+
const wf = workflowEngine.updateWorkflow(req.params.id, req.body);
|
|
1401
|
+
if (!wf) {
|
|
1402
|
+
res.status(404).json({ error: 'Workflow not found' });
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
res.json(wf);
|
|
1406
|
+
});
|
|
1407
|
+
this.app.delete('/api/workflows/:id', (req, res) => {
|
|
1408
|
+
const ok = workflowEngine.deleteWorkflow(req.params.id);
|
|
1409
|
+
res.json({ success: ok });
|
|
1410
|
+
});
|
|
1411
|
+
this.app.post('/api/workflows/:id/duplicate', (req, res) => {
|
|
1412
|
+
const wf = workflowEngine.duplicateWorkflow(req.params.id);
|
|
1413
|
+
if (!wf) {
|
|
1414
|
+
res.status(404).json({ error: 'Workflow not found' });
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
res.json(wf);
|
|
1418
|
+
});
|
|
1419
|
+
this.app.post('/api/workflows/:id/execute', async (req, res) => {
|
|
1420
|
+
try {
|
|
1421
|
+
const provider = this.providerManager.createFromEnv();
|
|
1422
|
+
const result = await workflowEngine.executeWorkflow(req.params.id, req.body.triggerData, {
|
|
1423
|
+
provider,
|
|
1424
|
+
memory: this.memory,
|
|
1425
|
+
});
|
|
1426
|
+
res.json(result);
|
|
1427
|
+
}
|
|
1428
|
+
catch (e) {
|
|
1429
|
+
res.status(500).json({ error: e.message });
|
|
1430
|
+
}
|
|
1431
|
+
});
|
|
1432
|
+
this.app.post('/api/workflows/:id/toggle', (req, res) => {
|
|
1433
|
+
const wf = workflowEngine.getWorkflow(req.params.id);
|
|
1434
|
+
if (!wf) {
|
|
1435
|
+
res.status(404).json({ error: 'Workflow not found' });
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
const updated = workflowEngine.updateWorkflow(req.params.id, { active: !wf.active });
|
|
1439
|
+
res.json(updated);
|
|
1440
|
+
});
|
|
1441
|
+
this.app.get('/', (_req, res) => {
|
|
1442
|
+
const landingPath = path_1.default.join(__dirname, 'static', 'index.html');
|
|
1443
|
+
if (require('fs').existsSync(landingPath)) {
|
|
1444
|
+
res.sendFile(landingPath);
|
|
1445
|
+
}
|
|
1446
|
+
else {
|
|
1447
|
+
res.send(this.generateHTML());
|
|
1448
|
+
}
|
|
1449
|
+
});
|
|
1450
|
+
this.app.get('/dashboard', (_req, res) => {
|
|
1451
|
+
res.sendFile(path_1.default.join(__dirname, 'static', 'dashboard.html'));
|
|
1452
|
+
});
|
|
1453
|
+
this.app.get('/hub', (_req, res) => {
|
|
1454
|
+
res.sendFile(path_1.default.join(__dirname, 'static', 'hub.html'));
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
/**
|
|
1458
|
+
* Setup WebSocket for real-time updates
|
|
1459
|
+
*/
|
|
1460
|
+
setupWebSocket() {
|
|
1461
|
+
this.wss.on('connection', (ws) => {
|
|
1462
|
+
logger.info('WebSocket client connected');
|
|
1463
|
+
this.connections.add(ws);
|
|
1464
|
+
ws.on('close', () => {
|
|
1465
|
+
this.connections.delete(ws);
|
|
1466
|
+
logger.info('WebSocket client disconnected');
|
|
1467
|
+
});
|
|
1468
|
+
ws.on('message', (data) => {
|
|
1469
|
+
try {
|
|
1470
|
+
const message = JSON.parse(data);
|
|
1471
|
+
this.handleWebSocketMessage(ws, message);
|
|
1472
|
+
}
|
|
1473
|
+
catch (error) {
|
|
1474
|
+
logger.error('Invalid WebSocket message');
|
|
1475
|
+
}
|
|
1476
|
+
});
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* Handle WebSocket messages
|
|
1481
|
+
*/
|
|
1482
|
+
handleWebSocketMessage(ws, message) {
|
|
1483
|
+
if (message.type === 'ping') {
|
|
1484
|
+
ws.send(JSON.stringify({ type: 'pong' }));
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
/**
|
|
1488
|
+
* Run task and broadcast updates
|
|
1489
|
+
*/
|
|
1490
|
+
async runTask(task) {
|
|
1491
|
+
if (!this.orchestrator)
|
|
1492
|
+
return;
|
|
1493
|
+
try {
|
|
1494
|
+
this.broadcast({
|
|
1495
|
+
type: 'state_update',
|
|
1496
|
+
payload: { task, running: true },
|
|
1497
|
+
});
|
|
1498
|
+
const result = await this.orchestrator.run(task);
|
|
1499
|
+
this.broadcast({
|
|
1500
|
+
type: 'complete',
|
|
1501
|
+
payload: { result },
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
catch (error) {
|
|
1505
|
+
this.broadcast({
|
|
1506
|
+
type: 'error',
|
|
1507
|
+
payload: { error: error.message },
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* Broadcast message to all connected clients
|
|
1513
|
+
*/
|
|
1514
|
+
broadcast(message) {
|
|
1515
|
+
const data = JSON.stringify(message);
|
|
1516
|
+
for (const ws of this.connections) {
|
|
1517
|
+
if (ws.readyState === ws_1.WebSocket.OPEN) {
|
|
1518
|
+
ws.send(data);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
/**
|
|
1523
|
+
* Start email and messaging receivers - all messages go to inbox
|
|
1524
|
+
*/
|
|
1525
|
+
startEmailReceivers() {
|
|
1526
|
+
const integrationsPath = path_1.default.join(require('os').homedir(), '.linguclaw', 'integrations.json');
|
|
1527
|
+
let ints = {};
|
|
1528
|
+
try {
|
|
1529
|
+
ints = JSON.parse(require('fs').readFileSync(integrationsPath, 'utf8'));
|
|
1530
|
+
}
|
|
1531
|
+
catch (e) {
|
|
1532
|
+
logger.debug(`No integrations file yet: ${e.message}`);
|
|
1533
|
+
}
|
|
1534
|
+
// Check for IMAP email configuration
|
|
1535
|
+
const emailCfg = ints['email'];
|
|
1536
|
+
if (emailCfg && emailCfg.host && emailCfg.username && emailCfg.password) {
|
|
1537
|
+
logger.info('Starting IMAP email receiver for ' + emailCfg.username);
|
|
1538
|
+
// Parse folders (comma-separated list)
|
|
1539
|
+
const folders = emailCfg.folders ? emailCfg.folders.split(',').map((f) => f.trim()) : ['INBOX'];
|
|
1540
|
+
this.emailReceiver.startIMAP({
|
|
1541
|
+
host: emailCfg.host,
|
|
1542
|
+
port: parseInt(emailCfg.port || '993', 10),
|
|
1543
|
+
username: emailCfg.username,
|
|
1544
|
+
password: emailCfg.password,
|
|
1545
|
+
tls: true,
|
|
1546
|
+
pollInterval: parseFloat(emailCfg.pollInterval || '1'),
|
|
1547
|
+
folders: folders,
|
|
1548
|
+
useIdle: emailCfg.useIdle !== 'false' // Default true for real-time
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
// Check for Gmail API configuration
|
|
1552
|
+
const gmailCfg = ints['gmail'];
|
|
1553
|
+
if (gmailCfg && gmailCfg.clientId && gmailCfg.clientSecret && gmailCfg.refreshToken) {
|
|
1554
|
+
logger.info('Starting Gmail API receiver');
|
|
1555
|
+
this.emailReceiver.startGmail({
|
|
1556
|
+
clientId: gmailCfg.clientId,
|
|
1557
|
+
clientSecret: gmailCfg.clientSecret,
|
|
1558
|
+
refreshToken: gmailCfg.refreshToken
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
// ========== TELEGRAM ==========
|
|
1562
|
+
const telegramCfg = ints['telegram'];
|
|
1563
|
+
if (telegramCfg && telegramCfg.botToken) {
|
|
1564
|
+
logger.info('Starting Telegram bot');
|
|
1565
|
+
const telegramBot = new messaging_1.TelegramBot(telegramCfg.botToken);
|
|
1566
|
+
if (telegramCfg.chatId) {
|
|
1567
|
+
telegramBot.allowChats([parseInt(telegramCfg.chatId)]);
|
|
1568
|
+
}
|
|
1569
|
+
// Route incoming messages to inbox
|
|
1570
|
+
telegramBot.onMessage((msg) => {
|
|
1571
|
+
const inboxMsg = this.inbox.addMessage({
|
|
1572
|
+
platform: 'telegram',
|
|
1573
|
+
from: msg.from || 'unknown',
|
|
1574
|
+
to: 'me',
|
|
1575
|
+
subject: msg.subject || `Telegram from ${msg.from}`,
|
|
1576
|
+
body: msg.body || msg.message || '',
|
|
1577
|
+
threadId: msg.chatId || msg.from
|
|
1578
|
+
});
|
|
1579
|
+
// Broadcast to WebSocket clients
|
|
1580
|
+
this.broadcast({
|
|
1581
|
+
type: 'inbox_new',
|
|
1582
|
+
payload: { message: inboxMsg, unreadCount: this.inbox.getUnreadCount() }
|
|
1583
|
+
});
|
|
1584
|
+
return null;
|
|
1585
|
+
});
|
|
1586
|
+
this.messagingHub.addPlatform(telegramBot);
|
|
1587
|
+
telegramBot.start();
|
|
1588
|
+
}
|
|
1589
|
+
// ========== DISCORD ==========
|
|
1590
|
+
const discordCfg = ints['discord'];
|
|
1591
|
+
if (discordCfg && discordCfg.botToken) {
|
|
1592
|
+
logger.info('Starting Discord bot');
|
|
1593
|
+
const discordBot = new messaging_1.DiscordBot(discordCfg.botToken);
|
|
1594
|
+
discordBot.onMessage((msg) => {
|
|
1595
|
+
const inboxMsg = this.inbox.addMessage({
|
|
1596
|
+
platform: 'discord',
|
|
1597
|
+
from: msg.from || 'unknown',
|
|
1598
|
+
to: 'me',
|
|
1599
|
+
subject: msg.subject || `Discord from ${msg.from}`,
|
|
1600
|
+
body: msg.body || msg.message || '',
|
|
1601
|
+
threadId: msg.channelId || msg.from
|
|
1602
|
+
});
|
|
1603
|
+
this.broadcast({
|
|
1604
|
+
type: 'inbox_new',
|
|
1605
|
+
payload: { message: inboxMsg, unreadCount: this.inbox.getUnreadCount() }
|
|
1606
|
+
});
|
|
1607
|
+
return null;
|
|
1608
|
+
});
|
|
1609
|
+
this.messagingHub.addPlatform(discordBot);
|
|
1610
|
+
discordBot.start();
|
|
1611
|
+
}
|
|
1612
|
+
// ========== SLACK ==========
|
|
1613
|
+
const slackCfg = ints['slack'];
|
|
1614
|
+
if (slackCfg && slackCfg.botToken) {
|
|
1615
|
+
logger.info('Starting Slack bot');
|
|
1616
|
+
const slackBot = new messaging_1.SlackBot(slackCfg.botToken);
|
|
1617
|
+
slackBot.onMessage((msg) => {
|
|
1618
|
+
const inboxMsg = this.inbox.addMessage({
|
|
1619
|
+
platform: 'slack',
|
|
1620
|
+
from: msg.from || 'unknown',
|
|
1621
|
+
to: 'me',
|
|
1622
|
+
subject: msg.subject || `Slack from ${msg.from}`,
|
|
1623
|
+
body: msg.body || msg.message || '',
|
|
1624
|
+
threadId: msg.channel || msg.from
|
|
1625
|
+
});
|
|
1626
|
+
this.broadcast({
|
|
1627
|
+
type: 'inbox_new',
|
|
1628
|
+
payload: { message: inboxMsg, unreadCount: this.inbox.getUnreadCount() }
|
|
1629
|
+
});
|
|
1630
|
+
return null;
|
|
1631
|
+
});
|
|
1632
|
+
this.messagingHub.addPlatform(slackBot);
|
|
1633
|
+
slackBot.start();
|
|
1634
|
+
}
|
|
1635
|
+
// ========== WHATSAPP ==========
|
|
1636
|
+
const whatsappCfg = ints['whatsapp'];
|
|
1637
|
+
if (whatsappCfg && whatsappCfg.accountSid && whatsappCfg.authToken) {
|
|
1638
|
+
logger.info('Starting WhatsApp (Twilio)');
|
|
1639
|
+
const whatsappBot = new messaging_1.WhatsAppBot('twilio');
|
|
1640
|
+
whatsappBot.onMessage((msg) => {
|
|
1641
|
+
const inboxMsg = this.inbox.addMessage({
|
|
1642
|
+
platform: 'whatsapp',
|
|
1643
|
+
from: msg.from || 'unknown',
|
|
1644
|
+
to: 'me',
|
|
1645
|
+
subject: msg.subject || `WhatsApp from ${msg.from}`,
|
|
1646
|
+
body: msg.body || msg.message || '',
|
|
1647
|
+
threadId: msg.from
|
|
1648
|
+
});
|
|
1649
|
+
this.broadcast({
|
|
1650
|
+
type: 'inbox_new',
|
|
1651
|
+
payload: { message: inboxMsg, unreadCount: this.inbox.getUnreadCount() }
|
|
1652
|
+
});
|
|
1653
|
+
return null;
|
|
1654
|
+
});
|
|
1655
|
+
this.messagingHub.addPlatform(whatsappBot);
|
|
1656
|
+
whatsappBot.start();
|
|
1657
|
+
}
|
|
1658
|
+
logger.info('All messaging receivers started - messages will appear in inbox');
|
|
1659
|
+
}
|
|
1660
|
+
/**
|
|
1661
|
+
* Generate main HTML page
|
|
1662
|
+
*/
|
|
1663
|
+
generateHTML() {
|
|
1664
|
+
return `<!DOCTYPE html>
|
|
1665
|
+
<html lang="en">
|
|
1666
|
+
<head>
|
|
1667
|
+
<meta charset="UTF-8">
|
|
1668
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1669
|
+
<title>LinguClaw Dashboard</title>
|
|
1670
|
+
<style>
|
|
1671
|
+
:root {
|
|
1672
|
+
--bg-primary: #0f0f1a;
|
|
1673
|
+
--bg-secondary: #1a1a2e;
|
|
1674
|
+
--bg-card: #16213e;
|
|
1675
|
+
--bg-input: #0d0d1a;
|
|
1676
|
+
--border: #2a2a4a;
|
|
1677
|
+
--accent: #7c3aed;
|
|
1678
|
+
--accent-hover: #6d28d9;
|
|
1679
|
+
--accent-glow: rgba(124,58,237,0.3);
|
|
1680
|
+
--green: #10b981;
|
|
1681
|
+
--red: #ef4444;
|
|
1682
|
+
--yellow: #f59e0b;
|
|
1683
|
+
--blue: #3b82f6;
|
|
1684
|
+
--text: #e2e8f0;
|
|
1685
|
+
--text-dim: #94a3b8;
|
|
1686
|
+
--text-muted: #64748b;
|
|
1687
|
+
--radius: 12px;
|
|
1688
|
+
}
|
|
1689
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1690
|
+
body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg-primary); color: var(--text); min-height: 100vh; }
|
|
1691
|
+
|
|
1692
|
+
/* Top Bar */
|
|
1693
|
+
.topbar { display: flex; align-items: center; justify-content: space-between; padding: 0 1.5rem; height: 56px; background: var(--bg-secondary); border-bottom: 1px solid var(--border); }
|
|
1694
|
+
.topbar-left { display: flex; align-items: center; gap: 0.75rem; }
|
|
1695
|
+
.logo { width: 32px; height: 32px; background: var(--accent); border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 18px; }
|
|
1696
|
+
.topbar-title { font-size: 1.1rem; font-weight: 700; color: var(--text); }
|
|
1697
|
+
.topbar-title span { color: var(--accent); }
|
|
1698
|
+
.topbar-right { display: flex; align-items: center; gap: 1rem; }
|
|
1699
|
+
.status-badge { display: flex; align-items: center; gap: 0.4rem; padding: 0.3rem 0.75rem; background: var(--bg-card); border: 1px solid var(--border); border-radius: 20px; font-size: 0.75rem; color: var(--text-dim); }
|
|
1700
|
+
.status-dot { width: 8px; height: 8px; border-radius: 50%; }
|
|
1701
|
+
.status-dot.online { background: var(--green); box-shadow: 0 0 6px var(--green); }
|
|
1702
|
+
.status-dot.offline { background: var(--red); }
|
|
1703
|
+
|
|
1704
|
+
/* Layout */
|
|
1705
|
+
.layout { display: flex; height: calc(100vh - 56px); }
|
|
1706
|
+
|
|
1707
|
+
/* Sidebar */
|
|
1708
|
+
.sidebar { width: 260px; background: var(--bg-secondary); border-right: 1px solid var(--border); display: flex; flex-direction: column; flex-shrink: 0; }
|
|
1709
|
+
.nav { padding: 1rem 0.75rem; flex-shrink: 0; }
|
|
1710
|
+
.nav-item { display: flex; align-items: center; gap: 0.6rem; padding: 0.6rem 0.75rem; border-radius: 8px; cursor: pointer; font-size: 0.875rem; color: var(--text-dim); transition: all 0.15s; margin-bottom: 2px; }
|
|
1711
|
+
.nav-item:hover { background: var(--bg-card); color: var(--text); }
|
|
1712
|
+
.nav-item.active { background: var(--accent); color: white; }
|
|
1713
|
+
.nav-icon { font-size: 1rem; width: 20px; text-align: center; }
|
|
1714
|
+
.sidebar-section { padding: 0.5rem 1rem; margin-top: 0.5rem; }
|
|
1715
|
+
.sidebar-section-title { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); margin-bottom: 0.5rem; font-weight: 600; }
|
|
1716
|
+
.step-list { list-style: none; padding: 0 0.75rem; flex: 1; overflow-y: auto; }
|
|
1717
|
+
.step-item { padding: 0.5rem 0.6rem; margin-bottom: 3px; border-radius: 6px; font-size: 0.8rem; color: var(--text-dim); background: var(--bg-card); border-left: 3px solid var(--border); display: flex; align-items: center; gap: 0.4rem; }
|
|
1718
|
+
.step-item.completed { border-left-color: var(--green); color: var(--green); }
|
|
1719
|
+
.step-item.failed { border-left-color: var(--red); color: var(--red); }
|
|
1720
|
+
.step-item.running { border-left-color: var(--yellow); color: var(--yellow); animation: pulse 1.5s infinite; }
|
|
1721
|
+
@keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:0.6; } }
|
|
1722
|
+
.step-empty { color: var(--text-muted); font-size: 0.8rem; padding: 0.5rem 0.6rem; }
|
|
1723
|
+
|
|
1724
|
+
/* Main Content */
|
|
1725
|
+
.main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
|
1726
|
+
|
|
1727
|
+
/* Task View */
|
|
1728
|
+
.task-view { flex: 1; display: flex; flex-direction: column; padding: 1.25rem; gap: 1rem; }
|
|
1729
|
+
.task-header { display: flex; gap: 0.5rem; }
|
|
1730
|
+
.task-input { flex: 1; padding: 0.8rem 1rem; background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); font-size: 0.95rem; outline: none; transition: border 0.2s; }
|
|
1731
|
+
.task-input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); }
|
|
1732
|
+
.task-btn { padding: 0.8rem 1.5rem; background: var(--accent); color: white; border: none; border-radius: var(--radius); cursor: pointer; font-weight: 600; font-size: 0.9rem; transition: all 0.2s; display: flex; align-items: center; gap: 0.4rem; }
|
|
1733
|
+
.task-btn:hover { background: var(--accent-hover); transform: translateY(-1px); box-shadow: 0 4px 12px var(--accent-glow); }
|
|
1734
|
+
.task-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; box-shadow: none; }
|
|
1735
|
+
|
|
1736
|
+
/* Output Area */
|
|
1737
|
+
.output-area { flex: 1; display: flex; flex-direction: column; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
|
|
1738
|
+
.output-tabs { display: flex; border-bottom: 1px solid var(--border); background: var(--bg-card); }
|
|
1739
|
+
.output-tab { padding: 0.6rem 1rem; font-size: 0.8rem; color: var(--text-muted); cursor: pointer; border-bottom: 2px solid transparent; transition: all 0.15s; }
|
|
1740
|
+
.output-tab:hover { color: var(--text-dim); }
|
|
1741
|
+
.output-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
1742
|
+
.output-content { flex: 1; overflow-y: auto; padding: 1rem; font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace; font-size: 0.825rem; line-height: 1.6; white-space: pre-wrap; word-break: break-word; color: var(--text-dim); }
|
|
1743
|
+
.output-content .msg-task { color: var(--blue); }
|
|
1744
|
+
.output-content .msg-ok { color: var(--green); }
|
|
1745
|
+
.output-content .msg-err { color: var(--red); }
|
|
1746
|
+
.output-content .msg-step { color: var(--yellow); }
|
|
1747
|
+
.output-content .msg-info { color: var(--text-dim); }
|
|
1748
|
+
.task-running { display: none; align-items: center; gap: 0.5rem; padding: 0.75rem 1rem; background: rgba(124,58,237,0.1); border: 1px solid var(--accent); border-radius: var(--radius); font-size: 0.85rem; color: var(--accent); }
|
|
1749
|
+
.task-running.show { display: flex; }
|
|
1750
|
+
.spinner { width: 16px; height: 16px; border: 2px solid var(--accent-glow); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; }
|
|
1751
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
1752
|
+
|
|
1753
|
+
/* Settings View */
|
|
1754
|
+
.settings-view { display: none; flex: 1; padding: 1.25rem; overflow-y: auto; }
|
|
1755
|
+
.settings-card { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); padding: 1.5rem; margin-bottom: 1rem; max-width: 700px; }
|
|
1756
|
+
.settings-card-title { font-size: 1rem; font-weight: 600; margin-bottom: 1rem; color: var(--text); display: flex; align-items: center; gap: 0.5rem; }
|
|
1757
|
+
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1rem; }
|
|
1758
|
+
.form-group { margin-bottom: 0.75rem; }
|
|
1759
|
+
.form-label { display: block; font-size: 0.8rem; color: var(--text-dim); margin-bottom: 0.3rem; font-weight: 500; }
|
|
1760
|
+
.form-input, .form-select { width: 100%; padding: 0.6rem 0.75rem; background: var(--bg-input); border: 1px solid var(--border); border-radius: 8px; color: var(--text); font-size: 0.875rem; outline: none; transition: border 0.2s; }
|
|
1761
|
+
.form-input:focus, .form-select:focus { border-color: var(--accent); }
|
|
1762
|
+
.form-hint { font-size: 0.7rem; color: var(--text-muted); margin-top: 0.2rem; }
|
|
1763
|
+
.form-actions { display: flex; gap: 0.75rem; margin-top: 1.25rem; }
|
|
1764
|
+
.btn { padding: 0.6rem 1.25rem; border-radius: 8px; font-size: 0.85rem; font-weight: 500; cursor: pointer; border: none; transition: all 0.15s; }
|
|
1765
|
+
.btn-accent { background: var(--accent); color: white; }
|
|
1766
|
+
.btn-accent:hover { background: var(--accent-hover); }
|
|
1767
|
+
.btn-ghost { background: transparent; color: var(--text-dim); border: 1px solid var(--border); }
|
|
1768
|
+
.btn-ghost:hover { background: var(--bg-card); color: var(--text); }
|
|
1769
|
+
.toast { display: none; padding: 0.6rem 1rem; border-radius: 8px; font-size: 0.825rem; margin-top: 0.75rem; }
|
|
1770
|
+
.toast.show { display: block; }
|
|
1771
|
+
.toast.ok { background: rgba(16,185,129,0.15); color: var(--green); border: 1px solid rgba(16,185,129,0.3); }
|
|
1772
|
+
.toast.err { background: rgba(239,68,68,0.15); color: var(--red); border: 1px solid rgba(239,68,68,0.3); }
|
|
1773
|
+
|
|
1774
|
+
/* History View */
|
|
1775
|
+
.history-view { display: none; flex: 1; padding: 1.25rem; overflow-y: auto; }
|
|
1776
|
+
.history-empty { color: var(--text-muted); text-align: center; padding: 3rem; font-size: 0.9rem; }
|
|
1777
|
+
.history-item { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem; margin-bottom: 0.75rem; cursor: pointer; transition: border 0.15s; }
|
|
1778
|
+
.history-item:hover { border-color: var(--accent); }
|
|
1779
|
+
.history-item-task { font-weight: 500; margin-bottom: 0.3rem; }
|
|
1780
|
+
.history-item-meta { font-size: 0.75rem; color: var(--text-muted); display: flex; gap: 1rem; }
|
|
1781
|
+
</style>
|
|
1782
|
+
</head>
|
|
1783
|
+
<body>
|
|
1784
|
+
|
|
1785
|
+
<div class="topbar">
|
|
1786
|
+
<div class="topbar-left">
|
|
1787
|
+
<div class="logo">C</div>
|
|
1788
|
+
<div class="topbar-title">Lingu<span>Claw</span></div>
|
|
1789
|
+
</div>
|
|
1790
|
+
<div class="topbar-right">
|
|
1791
|
+
<div class="status-badge" id="modelBadge">openai/gpt-3.5-turbo</div>
|
|
1792
|
+
<div class="status-badge"><div class="status-dot online" id="wsDot"></div><span id="wsStatus">Connected</span></div>
|
|
1793
|
+
</div>
|
|
1794
|
+
</div>
|
|
1795
|
+
|
|
1796
|
+
<div class="layout">
|
|
1797
|
+
<div class="sidebar">
|
|
1798
|
+
<div class="nav">
|
|
1799
|
+
<div class="nav-item active" onclick="showView('task', this)"><span class="nav-icon">⚡</span> Tasks</div>
|
|
1800
|
+
<div class="nav-item" onclick="showView('history', this)"><span class="nav-icon">📋</span> History</div>
|
|
1801
|
+
<div class="nav-item" onclick="showView('settings', this)"><span class="nav-icon">⚙</span> Settings</div>
|
|
1802
|
+
</div>
|
|
1803
|
+
<div class="sidebar-section">
|
|
1804
|
+
<div class="sidebar-section-title">Execution Steps</div>
|
|
1805
|
+
</div>
|
|
1806
|
+
<ul class="step-list" id="steps">
|
|
1807
|
+
<li class="step-empty">No active task</li>
|
|
1808
|
+
</ul>
|
|
1809
|
+
</div>
|
|
1810
|
+
|
|
1811
|
+
<div class="main">
|
|
1812
|
+
<!-- TASK VIEW -->
|
|
1813
|
+
<div class="task-view" id="taskView">
|
|
1814
|
+
<div class="task-header">
|
|
1815
|
+
<input class="task-input" id="taskInput" placeholder="Describe your task... (e.g. List all TypeScript files)" onkeydown="if(event.key==='Enter')startTask()" />
|
|
1816
|
+
<button class="task-btn" id="taskBtn" onclick="startTask()">▶ Run</button>
|
|
1817
|
+
</div>
|
|
1818
|
+
<div class="task-running" id="taskRunning"><div class="spinner"></div> Running task...</div>
|
|
1819
|
+
<div class="output-area">
|
|
1820
|
+
<div class="output-tabs">
|
|
1821
|
+
<div class="output-tab active" onclick="switchTab('output', this)">Output</div>
|
|
1822
|
+
<div class="output-tab" onclick="switchTab('raw', this)">Raw</div>
|
|
1823
|
+
</div>
|
|
1824
|
+
<div class="output-content" id="outputArea">
|
|
1825
|
+
<span class="msg-info">Welcome to LinguClaw Dashboard.
|
|
1826
|
+
Type a task above and click Run to get started.
|
|
1827
|
+
|
|
1828
|
+
Examples:
|
|
1829
|
+
- List all TypeScript files in this project
|
|
1830
|
+
- Create a hello.ts file
|
|
1831
|
+
- Analyze the project structure</span></div>
|
|
1832
|
+
<div class="output-content" id="rawArea" style="display:none;"></div>
|
|
1833
|
+
</div>
|
|
1834
|
+
</div>
|
|
1835
|
+
|
|
1836
|
+
<!-- HISTORY VIEW -->
|
|
1837
|
+
<div class="history-view" id="historyView">
|
|
1838
|
+
<h2 style="margin-bottom:1rem;font-size:1.1rem;">Task History</h2>
|
|
1839
|
+
<div id="historyList"><div class="history-empty">No tasks run yet this session.</div></div>
|
|
1840
|
+
</div>
|
|
1841
|
+
|
|
1842
|
+
<!-- SETTINGS VIEW -->
|
|
1843
|
+
<div class="settings-view" id="settingsView">
|
|
1844
|
+
<div class="settings-card">
|
|
1845
|
+
<div class="settings-card-title">🤖 LLM Provider</div>
|
|
1846
|
+
<div class="form-row">
|
|
1847
|
+
<div class="form-group">
|
|
1848
|
+
<label class="form-label">Provider</label>
|
|
1849
|
+
<select class="form-select" id="settingProvider">
|
|
1850
|
+
<option value="openrouter">OpenRouter</option>
|
|
1851
|
+
<option value="openai">OpenAI</option>
|
|
1852
|
+
<option value="anthropic">Anthropic</option>
|
|
1853
|
+
<option value="ollama">Ollama (Local)</option>
|
|
1854
|
+
<option value="lmstudio">LM Studio (Local)</option>
|
|
1855
|
+
</select>
|
|
1856
|
+
</div>
|
|
1857
|
+
<div class="form-group">
|
|
1858
|
+
<label class="form-label">Model</label>
|
|
1859
|
+
<input class="form-input" id="settingModel" placeholder="openai/gpt-3.5-turbo" />
|
|
1860
|
+
</div>
|
|
1861
|
+
</div>
|
|
1862
|
+
<div class="form-group">
|
|
1863
|
+
<label class="form-label">API Key</label>
|
|
1864
|
+
<input class="form-input" type="password" id="settingApiKey" placeholder="Enter new API key..." />
|
|
1865
|
+
<div class="form-hint">Leave empty to keep current key.</div>
|
|
1866
|
+
</div>
|
|
1867
|
+
<div class="form-row">
|
|
1868
|
+
<div class="form-group">
|
|
1869
|
+
<label class="form-label">Max Tokens</label>
|
|
1870
|
+
<input class="form-input" type="number" id="settingMaxTokens" min="100" max="8000" />
|
|
1871
|
+
</div>
|
|
1872
|
+
<div class="form-group">
|
|
1873
|
+
<label class="form-label">Temperature</label>
|
|
1874
|
+
<input class="form-input" type="number" id="settingTemperature" min="0" max="2" step="0.1" />
|
|
1875
|
+
</div>
|
|
1876
|
+
</div>
|
|
1877
|
+
</div>
|
|
1878
|
+
<div class="settings-card">
|
|
1879
|
+
<div class="settings-card-title">🛠 System</div>
|
|
1880
|
+
<div class="form-row">
|
|
1881
|
+
<div class="form-group">
|
|
1882
|
+
<label class="form-label">Max Steps</label>
|
|
1883
|
+
<input class="form-input" type="number" id="settingMaxSteps" min="1" max="50" />
|
|
1884
|
+
</div>
|
|
1885
|
+
<div class="form-group">
|
|
1886
|
+
<label class="form-label">Log Level</label>
|
|
1887
|
+
<select class="form-select" id="settingLogLevel">
|
|
1888
|
+
<option value="debug">Debug</option>
|
|
1889
|
+
<option value="info">Info</option>
|
|
1890
|
+
<option value="warn">Warning</option>
|
|
1891
|
+
<option value="error">Error</option>
|
|
1892
|
+
</select>
|
|
1893
|
+
</div>
|
|
1894
|
+
</div>
|
|
1895
|
+
<div class="form-group">
|
|
1896
|
+
<label class="form-label">Safety Mode</label>
|
|
1897
|
+
<select class="form-select" id="settingSafetyMode">
|
|
1898
|
+
<option value="strict">Strict</option>
|
|
1899
|
+
<option value="balanced">Balanced</option>
|
|
1900
|
+
<option value="permissive">Permissive</option>
|
|
1901
|
+
</select>
|
|
1902
|
+
</div>
|
|
1903
|
+
<div class="form-actions">
|
|
1904
|
+
<button class="btn btn-accent" onclick="saveSettings()">Save Settings</button>
|
|
1905
|
+
<button class="btn btn-ghost" onclick="resetSettings()">Reset Defaults</button>
|
|
1906
|
+
</div>
|
|
1907
|
+
<div class="toast" id="settingsToast"></div>
|
|
1908
|
+
</div>
|
|
1909
|
+
</div>
|
|
1910
|
+
</div>
|
|
1911
|
+
</div>
|
|
1912
|
+
|
|
1913
|
+
<script>
|
|
1914
|
+
var outputEl = document.getElementById('outputArea');
|
|
1915
|
+
var rawEl = document.getElementById('rawArea');
|
|
1916
|
+
var stepsEl = document.getElementById('steps');
|
|
1917
|
+
var taskBtn = document.getElementById('taskBtn');
|
|
1918
|
+
var runningEl = document.getElementById('taskRunning');
|
|
1919
|
+
var ws = null;
|
|
1920
|
+
var taskHistory = [];
|
|
1921
|
+
var isRunning = false;
|
|
1922
|
+
var rawLog = '';
|
|
1923
|
+
|
|
1924
|
+
// --- Views ---
|
|
1925
|
+
function showView(view, el) {
|
|
1926
|
+
var items = document.querySelectorAll('.nav-item');
|
|
1927
|
+
for (var i = 0; i < items.length; i++) items[i].classList.remove('active');
|
|
1928
|
+
el.classList.add('active');
|
|
1929
|
+
document.getElementById('taskView').style.display = view === 'task' ? 'flex' : 'none';
|
|
1930
|
+
document.getElementById('settingsView').style.display = view === 'settings' ? 'block' : 'none';
|
|
1931
|
+
document.getElementById('historyView').style.display = view === 'history' ? 'block' : 'none';
|
|
1932
|
+
if (view === 'settings') loadSettings();
|
|
1933
|
+
if (view === 'history') renderHistory();
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
// --- Output Tabs ---
|
|
1937
|
+
function switchTab(tab, el) {
|
|
1938
|
+
var tabs = document.querySelectorAll('.output-tab');
|
|
1939
|
+
for (var i = 0; i < tabs.length; i++) tabs[i].classList.remove('active');
|
|
1940
|
+
el.classList.add('active');
|
|
1941
|
+
document.getElementById('outputArea').style.display = tab === 'output' ? 'block' : 'none';
|
|
1942
|
+
document.getElementById('rawArea').style.display = tab === 'raw' ? 'block' : 'none';
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
// --- Output helpers ---
|
|
1946
|
+
function appendOutput(html) { outputEl.innerHTML += html; outputEl.scrollTop = outputEl.scrollHeight; }
|
|
1947
|
+
function setOutput(html) { outputEl.innerHTML = html; }
|
|
1948
|
+
function appendRaw(text) { rawEl.textContent += text; rawLog += text; }
|
|
1949
|
+
|
|
1950
|
+
// --- Task ---
|
|
1951
|
+
function startTask() {
|
|
1952
|
+
var task = document.getElementById('taskInput').value.trim();
|
|
1953
|
+
if (!task || isRunning) return;
|
|
1954
|
+
isRunning = true;
|
|
1955
|
+
taskBtn.disabled = true;
|
|
1956
|
+
runningEl.classList.add('show');
|
|
1957
|
+
setOutput('<span class="msg-task">Task: ' + esc(task) + '</span>\\n');
|
|
1958
|
+
rawEl.textContent = '';
|
|
1959
|
+
rawLog = '';
|
|
1960
|
+
stepsEl.innerHTML = '<li class="step-item running">Planning...</li>';
|
|
1961
|
+
|
|
1962
|
+
fetch('/api/task', {
|
|
1963
|
+
method: 'POST',
|
|
1964
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1965
|
+
body: JSON.stringify({ task: task, max_steps: 15 })
|
|
1966
|
+
}).then(function(r) { return r.json(); }).then(function(result) {
|
|
1967
|
+
if (result.error) {
|
|
1968
|
+
appendOutput('<span class="msg-err">Error: ' + esc(result.error) + '</span>\\n');
|
|
1969
|
+
taskDone(task, 'error');
|
|
1970
|
+
} else {
|
|
1971
|
+
appendOutput('<span class="msg-info">Task queued (ID: ' + result.task_id + ')</span>\\n');
|
|
1972
|
+
}
|
|
1973
|
+
}).catch(function(e) {
|
|
1974
|
+
appendOutput('<span class="msg-err">Network error: ' + esc(e.message) + '</span>\\n');
|
|
1975
|
+
taskDone(task, 'error');
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
function taskDone(task, status) {
|
|
1980
|
+
isRunning = false;
|
|
1981
|
+
taskBtn.disabled = false;
|
|
1982
|
+
runningEl.classList.remove('show');
|
|
1983
|
+
taskHistory.unshift({ task: task, time: new Date().toLocaleTimeString(), status: status, log: rawLog });
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
function esc(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
|
1987
|
+
|
|
1988
|
+
// --- History ---
|
|
1989
|
+
function renderHistory() {
|
|
1990
|
+
var el = document.getElementById('historyList');
|
|
1991
|
+
if (taskHistory.length === 0) { el.innerHTML = '<div class="history-empty">No tasks run yet this session.</div>'; return; }
|
|
1992
|
+
var html = '';
|
|
1993
|
+
for (var i = 0; i < taskHistory.length; i++) {
|
|
1994
|
+
var h = taskHistory[i];
|
|
1995
|
+
var icon = h.status === 'done' ? '<span style="color:var(--green)">✓</span>' : '<span style="color:var(--red)">✗</span>';
|
|
1996
|
+
html += '<div class="history-item" onclick="showHistoryDetail(' + i + ')">';
|
|
1997
|
+
html += '<div class="history-item-task">' + icon + ' ' + esc(h.task) + '</div>';
|
|
1998
|
+
html += '<div class="history-item-meta"><span>' + h.time + '</span><span>' + h.status + '</span></div>';
|
|
1999
|
+
html += '</div>';
|
|
2000
|
+
}
|
|
2001
|
+
el.innerHTML = html;
|
|
2002
|
+
}
|
|
2003
|
+
function showHistoryDetail(idx) {
|
|
2004
|
+
var h = taskHistory[idx];
|
|
2005
|
+
showView('task', document.querySelector('.nav-item'));
|
|
2006
|
+
setOutput('<span class="msg-task">[History] ' + esc(h.task) + '</span>\\n' + esc(h.log));
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
// --- Settings ---
|
|
2010
|
+
function loadSettings() {
|
|
2011
|
+
fetch('/api/settings').then(function(r) { return r.json(); }).then(function(s) {
|
|
2012
|
+
document.getElementById('settingProvider').value = s.llm.provider || '';
|
|
2013
|
+
document.getElementById('settingModel').value = s.llm.model || '';
|
|
2014
|
+
document.getElementById('settingMaxTokens').value = s.llm.maxTokens || 1000;
|
|
2015
|
+
document.getElementById('settingTemperature').value = s.llm.temperature || 0.7;
|
|
2016
|
+
document.getElementById('settingMaxSteps').value = s.system.maxSteps || 15;
|
|
2017
|
+
document.getElementById('settingLogLevel').value = s.system.logLevel || 'info';
|
|
2018
|
+
document.getElementById('settingSafetyMode').value = s.system.safetyMode || 'balanced';
|
|
2019
|
+
document.getElementById('modelBadge').textContent = (s.llm.provider || '') + '/' + (s.llm.model || '');
|
|
2020
|
+
}).catch(function(e) { console.error('Settings load error:', e); });
|
|
2021
|
+
}
|
|
2022
|
+
function saveSettings() {
|
|
2023
|
+
var toast = document.getElementById('settingsToast');
|
|
2024
|
+
toast.className = 'toast';
|
|
2025
|
+
var data = {
|
|
2026
|
+
llm: {
|
|
2027
|
+
provider: document.getElementById('settingProvider').value,
|
|
2028
|
+
model: document.getElementById('settingModel').value,
|
|
2029
|
+
maxTokens: parseInt(document.getElementById('settingMaxTokens').value),
|
|
2030
|
+
temperature: parseFloat(document.getElementById('settingTemperature').value)
|
|
2031
|
+
},
|
|
2032
|
+
system: {
|
|
2033
|
+
maxSteps: parseInt(document.getElementById('settingMaxSteps').value),
|
|
2034
|
+
logLevel: document.getElementById('settingLogLevel').value,f
|
|
2035
|
+
safetyMode: document.getElementById('settingSafetyMode').value
|
|
2036
|
+
}
|
|
2037
|
+
};
|
|
2038
|
+
var apiKey = document.getElementById('settingApiKey').value;
|
|
2039
|
+
if (apiKey) data.llm.apiKey = apiKey;
|
|
2040
|
+
fetch('/api/settings', {
|
|
2041
|
+
method: 'POST',
|
|
2042
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2043
|
+
body: JSON.stringify(data)
|
|
2044
|
+
}).then(function(r) {
|
|
2045
|
+
if (r.ok) {
|
|
2046
|
+
toast.textContent = 'Settings saved successfully';
|
|
2047
|
+
toast.className = 'toast show ok';
|
|
2048
|
+
document.getElementById('settingApiKey').value = '';
|
|
2049
|
+
document.getElementById('modelBadge').textContent = data.llm.provider + '/' + data.llm.model;
|
|
2050
|
+
} else {
|
|
2051
|
+
toast.textContent = 'Failed to save settings';
|
|
2052
|
+
toast.className = 'toast show err';
|
|
2053
|
+
}
|
|
2054
|
+
}).catch(function(e) {
|
|
2055
|
+
toast.textContent = 'Error: ' + e.message;
|
|
2056
|
+
toast.className = 'toast show err';
|
|
2057
|
+
});
|
|
2058
|
+
}
|
|
2059
|
+
function resetSettings() {
|
|
2060
|
+
if (!confirm('Reset all settings to defaults?')) return;
|
|
2061
|
+
fetch('/api/settings', {
|
|
2062
|
+
method: 'POST',
|
|
2063
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2064
|
+
body: JSON.stringify({ llm: { provider: 'openrouter', model: 'openai/gpt-3.5-turbo', maxTokens: 1000, temperature: 0.7 }, system: { maxSteps: 15, logLevel: 'info', safetyMode: 'balanced' } })
|
|
2065
|
+
}).then(function() { loadSettings(); var t = document.getElementById('settingsToast'); t.textContent = 'Reset to defaults'; t.className = 'toast show ok'; });
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
// --- WebSocket ---
|
|
2069
|
+
try {
|
|
2070
|
+
ws = new WebSocket('ws://' + window.location.host);
|
|
2071
|
+
ws.onopen = function() {
|
|
2072
|
+
document.getElementById('wsDot').className = 'status-dot online';
|
|
2073
|
+
document.getElementById('wsStatus').textContent = 'Connected';
|
|
2074
|
+
};
|
|
2075
|
+
ws.onclose = function() {
|
|
2076
|
+
document.getElementById('wsDot').className = 'status-dot offline';
|
|
2077
|
+
document.getElementById('wsStatus').textContent = 'Disconnected';
|
|
2078
|
+
};
|
|
2079
|
+
ws.onerror = function() {
|
|
2080
|
+
document.getElementById('wsDot').className = 'status-dot offline';
|
|
2081
|
+
document.getElementById('wsStatus').textContent = 'Error';
|
|
2082
|
+
};
|
|
2083
|
+
ws.onmessage = function(ev) {
|
|
2084
|
+
var msg = JSON.parse(ev.data);
|
|
2085
|
+
if (msg.type === 'state_update') {
|
|
2086
|
+
appendOutput('<span class="msg-step">Running: ' + esc(msg.payload.task) + '</span>\\n');
|
|
2087
|
+
appendRaw('Task: ' + msg.payload.task + '\\n');
|
|
2088
|
+
} else if (msg.type === 'step_update') {
|
|
2089
|
+
var s = msg.payload;
|
|
2090
|
+
appendOutput('<span class="msg-step">[' + esc(s.id) + '] ' + esc(s.description) + '</span>\\n');
|
|
2091
|
+
appendRaw('[Step] ' + s.id + ': ' + s.description + '\\n');
|
|
2092
|
+
updateSteps(msg.payload.steps || []);
|
|
2093
|
+
} else if (msg.type === 'log') {
|
|
2094
|
+
appendOutput('<span class="msg-info">' + esc(msg.payload) + '</span>\\n');
|
|
2095
|
+
appendRaw(msg.payload + '\\n');
|
|
2096
|
+
} else if (msg.type === 'complete') {
|
|
2097
|
+
var res = msg.payload.result || '';
|
|
2098
|
+
appendOutput('\\n<span class="msg-ok">--- Task Complete ---</span>\\n' + esc(res) + '\\n');
|
|
2099
|
+
appendRaw('\\nComplete:\\n' + res + '\\n');
|
|
2100
|
+
taskDone(document.getElementById('taskInput').value, 'done');
|
|
2101
|
+
stepsEl.innerHTML = '<li class="step-item completed">All steps completed</li>';
|
|
2102
|
+
} else if (msg.type === 'error') {
|
|
2103
|
+
appendOutput('<span class="msg-err">Error: ' + esc(msg.payload.error) + '</span>\\n');
|
|
2104
|
+
appendRaw('Error: ' + msg.payload.error + '\\n');
|
|
2105
|
+
taskDone(document.getElementById('taskInput').value, 'error');
|
|
2106
|
+
}
|
|
2107
|
+
};
|
|
2108
|
+
setInterval(function() {
|
|
2109
|
+
if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'ping' }));
|
|
2110
|
+
}, 30000);
|
|
2111
|
+
} catch(e) { console.log('WS init failed:', e); }
|
|
2112
|
+
|
|
2113
|
+
function updateSteps(stepList) {
|
|
2114
|
+
if (!stepList || stepList.length === 0) return;
|
|
2115
|
+
var html = '';
|
|
2116
|
+
for (var i = 0; i < stepList.length; i++) {
|
|
2117
|
+
var s = stepList[i];
|
|
2118
|
+
var cls = s.status === 'completed' ? 'completed' : s.status === 'failed' ? 'failed' : s.status === 'in_progress' ? 'running' : '';
|
|
2119
|
+
html += '<li class="step-item ' + cls + '">' + esc(s.id) + ': ' + esc(s.description || '').substring(0, 40) + '</li>';
|
|
2120
|
+
}
|
|
2121
|
+
stepsEl.innerHTML = html;
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
// Load model badge on start
|
|
2125
|
+
loadSettings();
|
|
2126
|
+
</script>
|
|
2127
|
+
</body>
|
|
2128
|
+
</html>`;
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
exports.WebUIManager = WebUIManager;
|
|
2132
|
+
/**
|
|
2133
|
+
* Run web UI server
|
|
2134
|
+
*/
|
|
2135
|
+
async function runWebUI(projectRoot, host, port) {
|
|
2136
|
+
const manager = new WebUIManager(projectRoot, host, port);
|
|
2137
|
+
await manager.start();
|
|
2138
|
+
}
|
|
2139
|
+
//# sourceMappingURL=web.js.map
|