squidclaw 2.7.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/engine.js +16 -0
- package/lib/features/canvas.js +228 -0
- package/lib/features/nodes.js +315 -0
- package/lib/middleware/commands.js +95 -0
- package/lib/tools/router.js +136 -0
- package/package.json +2 -1
package/lib/engine.js
CHANGED
|
@@ -228,6 +228,22 @@ export class SquidclawEngine {
|
|
|
228
228
|
if (pending.c > 0) console.log(` ⏰ Reminders: ${pending.c} pending`);
|
|
229
229
|
} catch {}
|
|
230
230
|
|
|
231
|
+
// Canvas (Visual Rendering)
|
|
232
|
+
try {
|
|
233
|
+
const { Canvas } = await import('./features/canvas.js');
|
|
234
|
+
this.canvas = new Canvas(this);
|
|
235
|
+
} catch (err) { logger.error('engine', 'Canvas init failed: ' + err.message); }
|
|
236
|
+
|
|
237
|
+
// Nodes (Paired Devices)
|
|
238
|
+
try {
|
|
239
|
+
const { NodeManager } = await import('./features/nodes.js');
|
|
240
|
+
this.nodeManager = new NodeManager(this);
|
|
241
|
+
await this.nodeManager.startServer(9501);
|
|
242
|
+
const nodeCount = this.nodeManager.list('*').length;
|
|
243
|
+
if (nodeCount > 0) console.log(' 📡 Nodes: ' + nodeCount + ' registered');
|
|
244
|
+
else console.log(' 📡 Nodes: ws://0.0.0.0:9501');
|
|
245
|
+
} catch (err) { logger.error('engine', 'Nodes init failed: ' + err.message); }
|
|
246
|
+
|
|
231
247
|
// Sandbox
|
|
232
248
|
try {
|
|
233
249
|
const { Sandbox } = await import('./features/sandbox.js');
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🦑 Canvas — Render HTML/charts as images in chat
|
|
3
|
+
* Uses Puppeteer to render HTML → screenshot → send as image
|
|
4
|
+
* Mini-apps, dashboards, visual reports
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { logger } from '../core/logger.js';
|
|
8
|
+
import { writeFileSync, mkdirSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
|
|
11
|
+
export class Canvas {
|
|
12
|
+
constructor(engine) {
|
|
13
|
+
this.engine = engine;
|
|
14
|
+
this.outputDir = '/tmp/squidclaw-canvas';
|
|
15
|
+
mkdirSync(this.outputDir, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ── Render HTML to image ──
|
|
19
|
+
|
|
20
|
+
async renderHtml(html, options = {}) {
|
|
21
|
+
const puppeteer = await import('puppeteer-core');
|
|
22
|
+
const { existsSync } = await import('fs');
|
|
23
|
+
|
|
24
|
+
const paths = ['/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/chromium-browser', '/usr/bin/chromium', '/snap/bin/chromium'];
|
|
25
|
+
let execPath = null;
|
|
26
|
+
for (const p of paths) { if (existsSync(p)) { execPath = p; break; } }
|
|
27
|
+
if (!execPath) throw new Error('No Chrome/Chromium found');
|
|
28
|
+
|
|
29
|
+
const browser = await puppeteer.default.launch({
|
|
30
|
+
executablePath: execPath,
|
|
31
|
+
headless: 'new',
|
|
32
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu'],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const page = await browser.newPage();
|
|
37
|
+
const width = options.width || 800;
|
|
38
|
+
const height = options.height || 600;
|
|
39
|
+
await page.setViewport({ width, height });
|
|
40
|
+
|
|
41
|
+
// Full HTML page
|
|
42
|
+
const fullHtml = html.includes('<html') ? html : `
|
|
43
|
+
<!DOCTYPE html>
|
|
44
|
+
<html>
|
|
45
|
+
<head>
|
|
46
|
+
<meta charset="UTF-8">
|
|
47
|
+
<style>
|
|
48
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
49
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0d1117; color: #c9d1d9; padding: 24px; }
|
|
50
|
+
h1 { color: #58a6ff; font-size: 24px; margin-bottom: 16px; }
|
|
51
|
+
h2 { color: #58a6ff; font-size: 18px; margin: 20px 0 8px; }
|
|
52
|
+
.card { background: #161b22; border: 1px solid #30363d; border-radius: 12px; padding: 20px; margin: 12px 0; }
|
|
53
|
+
.stat { display: inline-block; text-align: center; padding: 16px 24px; }
|
|
54
|
+
.stat-value { font-size: 36px; font-weight: bold; color: #58a6ff; }
|
|
55
|
+
.stat-label { font-size: 12px; color: #8b949e; margin-top: 4px; }
|
|
56
|
+
.grid { display: grid; gap: 12px; }
|
|
57
|
+
.grid-2 { grid-template-columns: 1fr 1fr; }
|
|
58
|
+
.grid-3 { grid-template-columns: 1fr 1fr 1fr; }
|
|
59
|
+
.grid-4 { grid-template-columns: 1fr 1fr 1fr 1fr; }
|
|
60
|
+
.bar { height: 24px; border-radius: 4px; margin: 4px 0; }
|
|
61
|
+
.bar-fill { height: 100%; border-radius: 4px; background: linear-gradient(90deg, #58a6ff, #3fb950); }
|
|
62
|
+
.progress { background: #21262d; border-radius: 8px; overflow: hidden; height: 12px; margin: 6px 0; }
|
|
63
|
+
.progress-fill { height: 100%; background: linear-gradient(90deg, #58a6ff, #3fb950); border-radius: 8px; }
|
|
64
|
+
table { width: 100%; border-collapse: collapse; }
|
|
65
|
+
th { background: #161b22; color: #58a6ff; padding: 10px; text-align: left; border-bottom: 2px solid #30363d; }
|
|
66
|
+
td { padding: 10px; border-bottom: 1px solid #21262d; }
|
|
67
|
+
tr:nth-child(even) { background: #161b22; }
|
|
68
|
+
.tag { display: inline-block; background: #1f6feb33; color: #58a6ff; padding: 2px 10px; border-radius: 12px; font-size: 12px; margin: 2px; }
|
|
69
|
+
.green { color: #3fb950; } .red { color: #f85149; } .yellow { color: #d29922; }
|
|
70
|
+
.accent { color: #58a6ff; }
|
|
71
|
+
</style>
|
|
72
|
+
</head>
|
|
73
|
+
<body>${html}</body>
|
|
74
|
+
</html>`;
|
|
75
|
+
|
|
76
|
+
await page.setContent(fullHtml, { waitUntil: 'networkidle0', timeout: 10000 });
|
|
77
|
+
|
|
78
|
+
// Auto-height if not specified
|
|
79
|
+
if (!options.height) {
|
|
80
|
+
const bodyHeight = await page.evaluate(() => document.body.scrollHeight);
|
|
81
|
+
await page.setViewport({ width, height: Math.min(bodyHeight + 48, 2000) });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const screenshot = await page.screenshot({
|
|
85
|
+
type: 'png',
|
|
86
|
+
fullPage: !options.height,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const filename = (options.name || 'canvas_' + Date.now()) + '.png';
|
|
90
|
+
const filepath = join(this.outputDir, filename);
|
|
91
|
+
writeFileSync(filepath, screenshot);
|
|
92
|
+
|
|
93
|
+
logger.info('canvas', `Rendered: ${filepath} (${width}x${options.height || 'auto'})`);
|
|
94
|
+
return { buffer: screenshot, filepath, filename };
|
|
95
|
+
} finally {
|
|
96
|
+
await browser.close();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Pre-built templates ──
|
|
101
|
+
|
|
102
|
+
async renderDashboard(data) {
|
|
103
|
+
const { title, stats, chart, table } = data;
|
|
104
|
+
|
|
105
|
+
let html = `<h1>${title || 'Dashboard'}</h1>`;
|
|
106
|
+
|
|
107
|
+
// Stats cards
|
|
108
|
+
if (stats && stats.length > 0) {
|
|
109
|
+
html += `<div class="card"><div class="grid grid-${Math.min(stats.length, 4)}">`;
|
|
110
|
+
for (const s of stats) {
|
|
111
|
+
html += `<div class="stat">
|
|
112
|
+
${s.icon ? `<div style="font-size:32px;margin-bottom:4px">${s.icon}</div>` : ''}
|
|
113
|
+
<div class="stat-value" style="color:${s.color || '#58a6ff'}">${s.value}</div>
|
|
114
|
+
<div class="stat-label">${s.label}</div>
|
|
115
|
+
</div>`;
|
|
116
|
+
}
|
|
117
|
+
html += `</div></div>`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Bar chart
|
|
121
|
+
if (chart && chart.length > 0) {
|
|
122
|
+
const max = Math.max(...chart.map(c => c.value));
|
|
123
|
+
html += `<div class="card"><h2>${data.chartTitle || 'Chart'}</h2>`;
|
|
124
|
+
for (const item of chart) {
|
|
125
|
+
const pct = (item.value / max * 100).toFixed(0);
|
|
126
|
+
html += `<div style="display:flex;align-items:center;margin:8px 0">
|
|
127
|
+
<div style="width:100px;font-size:13px">${item.label}</div>
|
|
128
|
+
<div style="flex:1;background:#21262d;border-radius:4px;height:24px;overflow:hidden">
|
|
129
|
+
<div style="width:${pct}%;height:100%;background:linear-gradient(90deg,#58a6ff,#3fb950);border-radius:4px;display:flex;align-items:center;padding-left:8px">
|
|
130
|
+
<span style="font-size:11px;color:#fff;font-weight:bold">${item.value}</span>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</div>`;
|
|
134
|
+
}
|
|
135
|
+
html += `</div>`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Table
|
|
139
|
+
if (table && table.length > 0) {
|
|
140
|
+
html += `<div class="card"><h2>${data.tableTitle || 'Data'}</h2><table>`;
|
|
141
|
+
html += `<tr>${table[0].map(h => `<th>${h}</th>`).join('')}</tr>`;
|
|
142
|
+
for (let i = 1; i < table.length; i++) {
|
|
143
|
+
html += `<tr>${table[i].map(c => `<td>${c}</td>`).join('')}</tr>`;
|
|
144
|
+
}
|
|
145
|
+
html += `</table></div>`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
html += `<div style="text-align:center;margin-top:16px;font-size:11px;color:#484f58">Squidclaw AI 🦑 · ${new Date().toLocaleString()}</div>`;
|
|
149
|
+
|
|
150
|
+
return this.renderHtml(html, { name: 'dashboard' });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async renderChart(data) {
|
|
154
|
+
const { title, type, items } = data;
|
|
155
|
+
const max = Math.max(...items.map(i => i.value));
|
|
156
|
+
const colors = ['#58a6ff', '#3fb950', '#d29922', '#f85149', '#bc8cff', '#f778ba', '#a5d6ff', '#7ee787'];
|
|
157
|
+
|
|
158
|
+
let html = `<h1>${title || 'Chart'}</h1><div class="card" style="padding:24px">`;
|
|
159
|
+
|
|
160
|
+
if (type === 'pie' || type === 'doughnut') {
|
|
161
|
+
const total = items.reduce((s, i) => s + i.value, 0);
|
|
162
|
+
let rotation = 0;
|
|
163
|
+
const gradientParts = [];
|
|
164
|
+
|
|
165
|
+
items.forEach((item, idx) => {
|
|
166
|
+
const pct = (item.value / total * 100);
|
|
167
|
+
gradientParts.push(`${colors[idx % colors.length]} ${rotation}deg ${rotation + pct * 3.6}deg`);
|
|
168
|
+
rotation += pct * 3.6;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const hole = type === 'doughnut' ? 'radial-gradient(circle, #0d1117 40%, transparent 40%),' : '';
|
|
172
|
+
html += `<div style="display:flex;align-items:center;gap:32px">
|
|
173
|
+
<div style="width:200px;height:200px;border-radius:50%;background:${hole}conic-gradient(${gradientParts.join(',')})"></div>
|
|
174
|
+
<div>`;
|
|
175
|
+
items.forEach((item, idx) => {
|
|
176
|
+
html += `<div style="display:flex;align-items:center;gap:8px;margin:6px 0">
|
|
177
|
+
<div style="width:12px;height:12px;border-radius:2px;background:${colors[idx % colors.length]}"></div>
|
|
178
|
+
<span style="font-size:13px">${item.label}: <strong>${item.value}</strong> (${(item.value / total * 100).toFixed(0)}%)</span>
|
|
179
|
+
</div>`;
|
|
180
|
+
});
|
|
181
|
+
html += `</div></div>`;
|
|
182
|
+
} else {
|
|
183
|
+
// Bar chart
|
|
184
|
+
for (const item of items) {
|
|
185
|
+
const pct = (item.value / max * 100).toFixed(0);
|
|
186
|
+
const color = item.color || colors[items.indexOf(item) % colors.length];
|
|
187
|
+
html += `<div style="display:flex;align-items:center;margin:10px 0">
|
|
188
|
+
<div style="width:120px;font-size:13px;color:#8b949e">${item.label}</div>
|
|
189
|
+
<div style="flex:1;background:#21262d;border-radius:6px;height:28px;overflow:hidden">
|
|
190
|
+
<div style="width:${pct}%;height:100%;background:${color};border-radius:6px;display:flex;align-items:center;padding-left:10px">
|
|
191
|
+
<span style="font-size:12px;color:#fff;font-weight:600">${item.value}</span>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
</div>`;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
html += `</div>`;
|
|
199
|
+
return this.renderHtml(html, { name: 'chart' });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async renderReport(data) {
|
|
203
|
+
const { title, sections } = data;
|
|
204
|
+
let html = `<h1>${title || 'Report'}</h1>`;
|
|
205
|
+
|
|
206
|
+
for (const section of (sections || [])) {
|
|
207
|
+
html += `<div class="card"><h2>${section.title || ''}</h2>`;
|
|
208
|
+
if (section.text) html += `<p style="line-height:1.7;color:#8b949e">${section.text}</p>`;
|
|
209
|
+
if (section.bullets) {
|
|
210
|
+
html += `<ul style="list-style:none;padding:0">`;
|
|
211
|
+
section.bullets.forEach(b => {
|
|
212
|
+
html += `<li style="padding:4px 0;color:#c9d1d9">● ${b}</li>`;
|
|
213
|
+
});
|
|
214
|
+
html += `</ul>`;
|
|
215
|
+
}
|
|
216
|
+
if (section.stats) {
|
|
217
|
+
html += `<div class="grid grid-${Math.min(section.stats.length, 3)}" style="margin-top:12px">`;
|
|
218
|
+
section.stats.forEach(s => {
|
|
219
|
+
html += `<div class="stat"><div class="stat-value" style="font-size:28px">${s.value}</div><div class="stat-label">${s.label}</div></div>`;
|
|
220
|
+
});
|
|
221
|
+
html += `</div>`;
|
|
222
|
+
}
|
|
223
|
+
html += `</div>`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return this.renderHtml(html, { name: 'report' });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🦑 Nodes — Paired Devices
|
|
3
|
+
* Connect phones, PCs, servers, IoT devices
|
|
4
|
+
* Real-time control via WebSocket
|
|
5
|
+
*
|
|
6
|
+
* Pairing: token-based (generate token → enter on device)
|
|
7
|
+
* Communication: WebSocket (ws://host:9501)
|
|
8
|
+
* Commands: camera, screen, location, run, notify, status
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { logger } from '../core/logger.js';
|
|
12
|
+
import crypto from 'crypto';
|
|
13
|
+
import { getHome } from '../core/config.js';
|
|
14
|
+
|
|
15
|
+
export class NodeManager {
|
|
16
|
+
constructor(engine) {
|
|
17
|
+
this.engine = engine;
|
|
18
|
+
this.nodes = new Map(); // nodeId -> { info, ws, status, lastSeen }
|
|
19
|
+
this.pendingPairs = new Map(); // token -> { agentId, createdAt, name }
|
|
20
|
+
this.wsServer = null;
|
|
21
|
+
this._initDb();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
_initDb() {
|
|
25
|
+
this.engine.storage.db.exec(`
|
|
26
|
+
CREATE TABLE IF NOT EXISTS nodes (
|
|
27
|
+
id TEXT PRIMARY KEY,
|
|
28
|
+
agent_id TEXT NOT NULL,
|
|
29
|
+
name TEXT NOT NULL,
|
|
30
|
+
type TEXT DEFAULT 'generic',
|
|
31
|
+
platform TEXT DEFAULT 'unknown',
|
|
32
|
+
capabilities TEXT DEFAULT '[]',
|
|
33
|
+
paired_at TEXT DEFAULT (datetime('now')),
|
|
34
|
+
last_seen TEXT,
|
|
35
|
+
status TEXT DEFAULT 'offline',
|
|
36
|
+
metadata TEXT DEFAULT '{}'
|
|
37
|
+
)
|
|
38
|
+
`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── WebSocket Server ──
|
|
42
|
+
|
|
43
|
+
async startServer(port = 9501) {
|
|
44
|
+
try {
|
|
45
|
+
const { WebSocketServer } = await import('ws');
|
|
46
|
+
|
|
47
|
+
this.wsServer = new WebSocketServer({ port, path: '/nodes' });
|
|
48
|
+
|
|
49
|
+
this.wsServer.on('connection', (ws, req) => {
|
|
50
|
+
let nodeId = null;
|
|
51
|
+
|
|
52
|
+
ws.on('message', (data) => {
|
|
53
|
+
try {
|
|
54
|
+
const msg = JSON.parse(data.toString());
|
|
55
|
+
this._handleMessage(ws, msg).then(id => {
|
|
56
|
+
if (id) nodeId = id;
|
|
57
|
+
});
|
|
58
|
+
} catch (err) {
|
|
59
|
+
ws.send(JSON.stringify({ type: 'error', message: 'Invalid message' }));
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
ws.on('close', () => {
|
|
64
|
+
if (nodeId) {
|
|
65
|
+
const node = this.nodes.get(nodeId);
|
|
66
|
+
if (node) {
|
|
67
|
+
node.status = 'offline';
|
|
68
|
+
node.ws = null;
|
|
69
|
+
this.engine.storage.db.prepare("UPDATE nodes SET status = 'offline' WHERE id = ?").run(nodeId);
|
|
70
|
+
logger.info('nodes', `Node disconnected: ${node.info?.name || nodeId}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
ws.on('error', () => {});
|
|
76
|
+
|
|
77
|
+
// Send hello
|
|
78
|
+
ws.send(JSON.stringify({ type: 'hello', message: 'Squidclaw Node Server 🦑' }));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Load existing nodes from DB
|
|
82
|
+
const dbNodes = this.engine.storage.db.prepare('SELECT * FROM nodes').all();
|
|
83
|
+
for (const n of dbNodes) {
|
|
84
|
+
this.nodes.set(n.id, {
|
|
85
|
+
info: n,
|
|
86
|
+
ws: null,
|
|
87
|
+
status: 'offline',
|
|
88
|
+
lastSeen: n.last_seen,
|
|
89
|
+
pendingCommands: new Map(),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
logger.info('nodes', `WebSocket server on port ${port} (${dbNodes.length} registered nodes)`);
|
|
94
|
+
return true;
|
|
95
|
+
} catch (err) {
|
|
96
|
+
logger.error('nodes', `Server failed: ${err.message}`);
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Message Handler ──
|
|
102
|
+
|
|
103
|
+
async _handleMessage(ws, msg) {
|
|
104
|
+
const { type } = msg;
|
|
105
|
+
|
|
106
|
+
// Pairing
|
|
107
|
+
if (type === 'pair') {
|
|
108
|
+
return this._handlePair(ws, msg);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Auth (reconnect with nodeId)
|
|
112
|
+
if (type === 'auth') {
|
|
113
|
+
const node = this.nodes.get(msg.nodeId);
|
|
114
|
+
if (!node) {
|
|
115
|
+
ws.send(JSON.stringify({ type: 'error', message: 'Unknown node' }));
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
node.ws = ws;
|
|
119
|
+
node.status = 'online';
|
|
120
|
+
node.lastSeen = new Date().toISOString();
|
|
121
|
+
this.engine.storage.db.prepare("UPDATE nodes SET status = 'online', last_seen = datetime('now') WHERE id = ?").run(msg.nodeId);
|
|
122
|
+
ws.send(JSON.stringify({ type: 'auth_ok', nodeId: msg.nodeId }));
|
|
123
|
+
logger.info('nodes', `Node reconnected: ${node.info.name}`);
|
|
124
|
+
return msg.nodeId;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Command response
|
|
128
|
+
if (type === 'response') {
|
|
129
|
+
const node = this.nodes.get(msg.nodeId);
|
|
130
|
+
if (node) {
|
|
131
|
+
const pending = node.pendingCommands?.get(msg.commandId);
|
|
132
|
+
if (pending) {
|
|
133
|
+
pending.resolve(msg.data);
|
|
134
|
+
node.pendingCommands.delete(msg.commandId);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return msg.nodeId;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Status update
|
|
141
|
+
if (type === 'status') {
|
|
142
|
+
const node = this.nodes.get(msg.nodeId);
|
|
143
|
+
if (node) {
|
|
144
|
+
node.lastSeen = new Date().toISOString();
|
|
145
|
+
if (msg.data) {
|
|
146
|
+
node.info = { ...node.info, ...msg.data };
|
|
147
|
+
}
|
|
148
|
+
this.engine.storage.db.prepare("UPDATE nodes SET last_seen = datetime('now') WHERE id = ?").run(msg.nodeId);
|
|
149
|
+
}
|
|
150
|
+
return msg.nodeId;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Pairing ──
|
|
157
|
+
|
|
158
|
+
_handlePair(ws, msg) {
|
|
159
|
+
const token = msg.token;
|
|
160
|
+
const pending = this.pendingPairs.get(token);
|
|
161
|
+
|
|
162
|
+
if (!pending) {
|
|
163
|
+
ws.send(JSON.stringify({ type: 'pair_error', message: 'Invalid or expired token' }));
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const nodeId = 'node_' + crypto.randomBytes(4).toString('hex');
|
|
168
|
+
|
|
169
|
+
// Register node
|
|
170
|
+
const info = {
|
|
171
|
+
id: nodeId,
|
|
172
|
+
agent_id: pending.agentId,
|
|
173
|
+
name: pending.name || msg.name || 'Device',
|
|
174
|
+
type: msg.deviceType || 'generic',
|
|
175
|
+
platform: msg.platform || 'unknown',
|
|
176
|
+
capabilities: JSON.stringify(msg.capabilities || []),
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
this.engine.storage.db.prepare(
|
|
180
|
+
'INSERT INTO nodes (id, agent_id, name, type, platform, capabilities, status) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
|
181
|
+
).run(info.id, info.agent_id, info.name, info.type, info.platform, info.capabilities, 'online');
|
|
182
|
+
|
|
183
|
+
this.nodes.set(nodeId, {
|
|
184
|
+
info,
|
|
185
|
+
ws,
|
|
186
|
+
status: 'online',
|
|
187
|
+
lastSeen: new Date().toISOString(),
|
|
188
|
+
pendingCommands: new Map(),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
this.pendingPairs.delete(token);
|
|
192
|
+
|
|
193
|
+
ws.send(JSON.stringify({ type: 'paired', nodeId, name: info.name }));
|
|
194
|
+
logger.info('nodes', `New node paired: ${info.name} (${nodeId}) type=${info.type}`);
|
|
195
|
+
|
|
196
|
+
return nodeId;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── Generate pairing token ──
|
|
200
|
+
|
|
201
|
+
generatePairToken(agentId, name = 'Device') {
|
|
202
|
+
const token = crypto.randomBytes(3).toString('hex').toUpperCase(); // 6-char code
|
|
203
|
+
this.pendingPairs.set(token, {
|
|
204
|
+
agentId,
|
|
205
|
+
name,
|
|
206
|
+
createdAt: Date.now(),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Expire after 5 minutes
|
|
210
|
+
setTimeout(() => this.pendingPairs.delete(token), 300000);
|
|
211
|
+
|
|
212
|
+
return token;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── Send command to node ──
|
|
216
|
+
|
|
217
|
+
async sendCommand(nodeId, command, data = {}, timeout = 30000) {
|
|
218
|
+
const node = this.nodes.get(nodeId);
|
|
219
|
+
if (!node) throw new Error('Node not found: ' + nodeId);
|
|
220
|
+
if (!node.ws || node.status !== 'online') throw new Error('Node offline: ' + (node.info?.name || nodeId));
|
|
221
|
+
|
|
222
|
+
const commandId = 'cmd_' + Date.now().toString(36);
|
|
223
|
+
|
|
224
|
+
return new Promise((resolve, reject) => {
|
|
225
|
+
const timer = setTimeout(() => {
|
|
226
|
+
node.pendingCommands?.delete(commandId);
|
|
227
|
+
reject(new Error('Command timeout'));
|
|
228
|
+
}, timeout);
|
|
229
|
+
|
|
230
|
+
node.pendingCommands = node.pendingCommands || new Map();
|
|
231
|
+
node.pendingCommands.set(commandId, {
|
|
232
|
+
resolve: (data) => { clearTimeout(timer); resolve(data); },
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
node.ws.send(JSON.stringify({
|
|
236
|
+
type: 'command',
|
|
237
|
+
commandId,
|
|
238
|
+
command,
|
|
239
|
+
data,
|
|
240
|
+
}));
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ── High-level commands ──
|
|
245
|
+
|
|
246
|
+
async camera(nodeId, options = {}) {
|
|
247
|
+
return this.sendCommand(nodeId, 'camera', {
|
|
248
|
+
facing: options.facing || 'back',
|
|
249
|
+
...options,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async screenshot(nodeId) {
|
|
254
|
+
return this.sendCommand(nodeId, 'screenshot');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async location(nodeId) {
|
|
258
|
+
return this.sendCommand(nodeId, 'location');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async notify(nodeId, title, body) {
|
|
262
|
+
return this.sendCommand(nodeId, 'notify', { title, body });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async run(nodeId, command) {
|
|
266
|
+
return this.sendCommand(nodeId, 'run', { command }, 60000);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async screenRecord(nodeId, durationMs = 10000) {
|
|
270
|
+
return this.sendCommand(nodeId, 'screen_record', { durationMs }, durationMs + 10000);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ── List/Status ──
|
|
274
|
+
|
|
275
|
+
list(agentId) {
|
|
276
|
+
return this.engine.storage.db.prepare(
|
|
277
|
+
'SELECT * FROM nodes WHERE agent_id = ? ORDER BY name'
|
|
278
|
+
).all(agentId).map(n => {
|
|
279
|
+
const live = this.nodes.get(n.id);
|
|
280
|
+
return {
|
|
281
|
+
...n,
|
|
282
|
+
capabilities: JSON.parse(n.capabilities || '[]'),
|
|
283
|
+
status: live?.status || 'offline',
|
|
284
|
+
lastSeen: live?.lastSeen || n.last_seen,
|
|
285
|
+
};
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
getNode(nodeId) {
|
|
290
|
+
const db = this.engine.storage.db.prepare('SELECT * FROM nodes WHERE id = ?').get(nodeId);
|
|
291
|
+
if (!db) return null;
|
|
292
|
+
const live = this.nodes.get(nodeId);
|
|
293
|
+
return { ...db, status: live?.status || 'offline', capabilities: JSON.parse(db.capabilities || '[]') };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
removeNode(nodeId) {
|
|
297
|
+
const node = this.nodes.get(nodeId);
|
|
298
|
+
if (node?.ws) {
|
|
299
|
+
node.ws.send(JSON.stringify({ type: 'unpaired' }));
|
|
300
|
+
node.ws.close();
|
|
301
|
+
}
|
|
302
|
+
this.nodes.delete(nodeId);
|
|
303
|
+
this.engine.storage.db.prepare('DELETE FROM nodes WHERE id = ?').run(nodeId);
|
|
304
|
+
logger.info('nodes', `Removed node: ${nodeId}`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ── Cleanup ──
|
|
308
|
+
|
|
309
|
+
stop() {
|
|
310
|
+
if (this.wsServer) {
|
|
311
|
+
this.wsServer.close();
|
|
312
|
+
logger.info('nodes', 'WebSocket server stopped');
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
@@ -145,6 +145,101 @@ export async function commandsMiddleware(ctx, next) {
|
|
|
145
145
|
return;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
if (cmd === '/canvas') {
|
|
149
|
+
if (!ctx.engine.canvas) { await ctx.reply('❌ Canvas not available'); return; }
|
|
150
|
+
const args = msg.slice(8).trim();
|
|
151
|
+
|
|
152
|
+
if (!args || args === 'help') {
|
|
153
|
+
await ctx.reply('🎨 *Canvas*\n\nRender visual content as images.\n\n/canvas demo — see a sample dashboard\n\nOr ask me naturally: "Show me a dashboard of our stats" or "Make a pie chart of market share"');
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (args === 'demo') {
|
|
158
|
+
try {
|
|
159
|
+
const result = await ctx.engine.canvas.renderDashboard({
|
|
160
|
+
title: 'Squidclaw Dashboard',
|
|
161
|
+
stats: [
|
|
162
|
+
{ icon: '🦑', value: 'v2.8', label: 'Version', color: '#58a6ff' },
|
|
163
|
+
{ icon: '⚡', value: '40+', label: 'Skills', color: '#3fb950' },
|
|
164
|
+
{ icon: '🔌', value: '3', label: 'Plugins', color: '#d29922' },
|
|
165
|
+
{ icon: '💬', value: '15', label: 'Middleware', color: '#bc8cff' },
|
|
166
|
+
],
|
|
167
|
+
chartTitle: 'Feature Growth by Version',
|
|
168
|
+
chart: [
|
|
169
|
+
{ label: 'v1.0', value: 20 },
|
|
170
|
+
{ label: 'v1.5', value: 28 },
|
|
171
|
+
{ label: 'v2.0', value: 40 },
|
|
172
|
+
{ label: 'v2.5', value: 48 },
|
|
173
|
+
{ label: 'v2.8', value: 55 },
|
|
174
|
+
],
|
|
175
|
+
tableTitle: 'System Status',
|
|
176
|
+
table: [
|
|
177
|
+
['Component', 'Status', 'Uptime'],
|
|
178
|
+
['Telegram', '🟢 Connected', '99.9%'],
|
|
179
|
+
['WhatsApp', '🔴 Off', '—'],
|
|
180
|
+
['Plugins', '🟢 3 Active', '100%'],
|
|
181
|
+
['Sandbox', '🟢 Ready', '100%'],
|
|
182
|
+
['Nodes', '🟢 Listening', '100%'],
|
|
183
|
+
],
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Send as image
|
|
187
|
+
if (ctx.platform === 'telegram' && ctx.engine.telegramManager) {
|
|
188
|
+
await ctx.engine.telegramManager.sendPhoto(ctx.agentId, ctx.contactId, { base64: result.buffer.toString('base64') }, '🎨 Squidclaw Dashboard Demo', ctx.metadata);
|
|
189
|
+
}
|
|
190
|
+
} catch (err) { await ctx.reply('❌ ' + err.message); }
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (cmd === '/nodes' || cmd === '/devices') {
|
|
196
|
+
if (!ctx.engine.nodeManager) { await ctx.reply('❌ Nodes not available'); return; }
|
|
197
|
+
const args = msg.split(/\s+/).slice(1);
|
|
198
|
+
const sub = args[0];
|
|
199
|
+
|
|
200
|
+
if (!sub || sub === 'list') {
|
|
201
|
+
const nodes = ctx.engine.nodeManager.list(ctx.agentId);
|
|
202
|
+
if (nodes.length === 0) {
|
|
203
|
+
await ctx.reply('📡 No paired devices\n\nUse /nodes pair <name> to add one');
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const lines = nodes.map(n =>
|
|
207
|
+
(n.status === 'online' ? '🟢' : '🔴') + ' *' + n.name + '* (' + n.type + ')\n ' + n.platform + ' · ' + n.id
|
|
208
|
+
);
|
|
209
|
+
await ctx.reply('📡 *Paired Devices*\n\n' + lines.join('\n\n'));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (sub === 'pair') {
|
|
214
|
+
const name = args.slice(1).join(' ') || 'My Device';
|
|
215
|
+
const token = ctx.engine.nodeManager.generatePairToken(ctx.agentId, name);
|
|
216
|
+
await ctx.reply('📡 *Pair: ' + name + '*\n\nCode: `' + token + '`\n\nEnter this on the device.\nExpires in 5 minutes.\n\nWebSocket: ws://' + (ctx.engine.config.api?.host || '0.0.0.0') + ':9501/nodes');
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (sub === 'remove') {
|
|
221
|
+
const id = args[1];
|
|
222
|
+
if (!id) { await ctx.reply('Usage: /nodes remove <id>'); return; }
|
|
223
|
+
ctx.engine.nodeManager.removeNode(id);
|
|
224
|
+
await ctx.reply('✅ Device removed');
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (sub === 'notify') {
|
|
229
|
+
const id = args[1];
|
|
230
|
+
const message = args.slice(2).join(' ');
|
|
231
|
+
if (!id || !message) { await ctx.reply('Usage: /nodes notify <id> <message>'); return; }
|
|
232
|
+
try {
|
|
233
|
+
await ctx.engine.nodeManager.notify(id, 'Squidclaw', message);
|
|
234
|
+
await ctx.reply('📨 Notification sent');
|
|
235
|
+
} catch (err) { await ctx.reply('❌ ' + err.message); }
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
await ctx.reply('📡 *Node Commands*\n\n/nodes — list devices\n/nodes pair <name> — generate pairing code\n/nodes remove <id> — unpair\n/nodes notify <id> <msg> — send notification');
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
148
243
|
if (cmd === '/sandbox') {
|
|
149
244
|
if (!ctx.engine.sandbox) { await ctx.reply('❌ Sandbox not available'); return; }
|
|
150
245
|
const args = msg.slice(9).trim();
|
package/lib/tools/router.js
CHANGED
|
@@ -169,6 +169,34 @@ export class ToolRouter {
|
|
|
169
169
|
'---TOOL:handoff:reason---',
|
|
170
170
|
'Transfer the conversation to a human agent. Use when you cannot help further.');
|
|
171
171
|
|
|
172
|
+
tools.push('', '### Render Dashboard (sends as image!)',
|
|
173
|
+
'---TOOL:canvas_dashboard:title|stat_json|chart_json---',
|
|
174
|
+
'Render a visual dashboard and send as image. Use JSON for data.',
|
|
175
|
+
'Simple format: ---TOOL:canvas_dashboard:Title|icon:value:label,icon:value:label|label:value,label:value---',
|
|
176
|
+
'', '### Render Chart (sends as image!)',
|
|
177
|
+
'---TOOL:canvas_chart:title|type|label:value,label:value---',
|
|
178
|
+
'Render a chart (bar, pie, doughnut) as image. Separate items with commas.',
|
|
179
|
+
'', '### Render Custom HTML (sends as image!)',
|
|
180
|
+
'---TOOL:canvas_html:html content---',
|
|
181
|
+
'Render any HTML as an image. Built-in dark theme CSS with cards, grids, stats, tables.');
|
|
182
|
+
|
|
183
|
+
tools.push('', '### Pair Device',
|
|
184
|
+
'---TOOL:node_pair:device name---',
|
|
185
|
+
'Generate a pairing code for a new device (phone, PC, server).',
|
|
186
|
+
'', '### List Devices',
|
|
187
|
+
'---TOOL:node_list:all---',
|
|
188
|
+
'Show all paired devices and their status.',
|
|
189
|
+
'', '### Send to Device',
|
|
190
|
+
'---TOOL:node_cmd:node_id|command|data---',
|
|
191
|
+
'Send a command to a paired device. Commands: camera, screenshot, location, notify, run.',
|
|
192
|
+
'Example: ---TOOL:node_cmd:node_abc123|notify|Meeting in 5 minutes!---',
|
|
193
|
+
'', '### Device Camera',
|
|
194
|
+
'---TOOL:node_camera:node_id---',
|
|
195
|
+
'Take a photo from a paired device camera.',
|
|
196
|
+
'', '### Device Location',
|
|
197
|
+
'---TOOL:node_location:node_id---',
|
|
198
|
+
'Get GPS location of a paired device.');
|
|
199
|
+
|
|
172
200
|
tools.push('', '### Run JavaScript (Sandboxed)',
|
|
173
201
|
'---TOOL:js:console.log(2 + 2)---',
|
|
174
202
|
'Execute JavaScript in a secure VM sandbox. No network, no filesystem, no require. Safe for math, logic, data processing.',
|
|
@@ -680,6 +708,114 @@ export class ToolRouter {
|
|
|
680
708
|
}
|
|
681
709
|
break;
|
|
682
710
|
}
|
|
711
|
+
case 'canvas_dashboard': {
|
|
712
|
+
if (!this._engine?.canvas) { toolResult = 'Canvas not available'; break; }
|
|
713
|
+
try {
|
|
714
|
+
const parts = toolArg.split('|');
|
|
715
|
+
const title = parts[0]?.trim() || 'Dashboard';
|
|
716
|
+
|
|
717
|
+
// Parse stats: icon:value:label,icon:value:label
|
|
718
|
+
const stats = [];
|
|
719
|
+
if (parts[1]) {
|
|
720
|
+
parts[1].split(',').forEach(s => {
|
|
721
|
+
const [icon, value, label] = s.trim().split(':');
|
|
722
|
+
if (value) stats.push({ icon: icon || '', value: value || '', label: label || '' });
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Parse chart: label:value,label:value
|
|
727
|
+
const chart = [];
|
|
728
|
+
if (parts[2]) {
|
|
729
|
+
parts[2].split(',').forEach(c => {
|
|
730
|
+
const [label, value] = c.trim().split(':');
|
|
731
|
+
if (value) chart.push({ label: label || '', value: parseInt(value) || 0 });
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const result = await this._engine.canvas.renderDashboard({ title, stats, chart });
|
|
736
|
+
return { toolUsed: true, toolName: 'canvas', toolResult: 'Dashboard rendered', imageBase64: result.buffer.toString('base64'), mimeType: 'image/png', cleanResponse };
|
|
737
|
+
} catch (err) { toolResult = 'Canvas failed: ' + err.message; }
|
|
738
|
+
break;
|
|
739
|
+
}
|
|
740
|
+
case 'canvas_chart': {
|
|
741
|
+
if (!this._engine?.canvas) { toolResult = 'Canvas not available'; break; }
|
|
742
|
+
try {
|
|
743
|
+
const parts = toolArg.split('|');
|
|
744
|
+
const title = parts[0]?.trim() || 'Chart';
|
|
745
|
+
const type = parts[1]?.trim() || 'bar';
|
|
746
|
+
const items = [];
|
|
747
|
+
if (parts[2]) {
|
|
748
|
+
parts[2].split(',').forEach(c => {
|
|
749
|
+
const [label, value] = c.trim().split(':');
|
|
750
|
+
if (value) items.push({ label: label || '', value: parseInt(value) || 0 });
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
const result = await this._engine.canvas.renderChart({ title, type, items });
|
|
754
|
+
return { toolUsed: true, toolName: 'canvas', toolResult: 'Chart rendered', imageBase64: result.buffer.toString('base64'), mimeType: 'image/png', cleanResponse };
|
|
755
|
+
} catch (err) { toolResult = 'Canvas failed: ' + err.message; }
|
|
756
|
+
break;
|
|
757
|
+
}
|
|
758
|
+
case 'canvas_html':
|
|
759
|
+
case 'canvas_render':
|
|
760
|
+
case 'canvas': {
|
|
761
|
+
if (!this._engine?.canvas) { toolResult = 'Canvas not available'; break; }
|
|
762
|
+
try {
|
|
763
|
+
const result = await this._engine.canvas.renderHtml(toolArg);
|
|
764
|
+
return { toolUsed: true, toolName: 'canvas', toolResult: 'Rendered', imageBase64: result.buffer.toString('base64'), mimeType: 'image/png', cleanResponse };
|
|
765
|
+
} catch (err) { toolResult = 'Canvas failed: ' + err.message; }
|
|
766
|
+
break;
|
|
767
|
+
}
|
|
768
|
+
case 'node_pair': {
|
|
769
|
+
if (!this._engine?.nodeManager) { toolResult = 'Nodes not available'; break; }
|
|
770
|
+
const token = this._engine.nodeManager.generatePairToken(agentId, toolArg || 'Device');
|
|
771
|
+
toolResult = '📡 Pairing code: *' + token + '*\n\nEnter this code on the device to pair.\nExpires in 5 minutes.\n\nConnect to: ws://<server>:9501/nodes';
|
|
772
|
+
break;
|
|
773
|
+
}
|
|
774
|
+
case 'node_list': {
|
|
775
|
+
if (!this._engine?.nodeManager) { toolResult = 'Nodes not available'; break; }
|
|
776
|
+
const nodes = this._engine.nodeManager.list(agentId);
|
|
777
|
+
if (nodes.length === 0) { toolResult = 'No paired devices. Use node_pair to add one.'; break; }
|
|
778
|
+
toolResult = nodes.map(n =>
|
|
779
|
+
(n.status === 'online' ? '🟢' : '🔴') + ' ' + n.name + ' (' + n.type + ')\n ID: ' + n.id + '\n Platform: ' + n.platform + '\n Last seen: ' + (n.lastSeen || 'never')
|
|
780
|
+
).join('\n\n');
|
|
781
|
+
break;
|
|
782
|
+
}
|
|
783
|
+
case 'node_cmd': {
|
|
784
|
+
if (!this._engine?.nodeManager) { toolResult = 'Nodes not available'; break; }
|
|
785
|
+
try {
|
|
786
|
+
const parts = toolArg.split('|').map(p => p.trim());
|
|
787
|
+
const [nodeId, command, ...dataParts] = parts;
|
|
788
|
+
const data = dataParts.join('|');
|
|
789
|
+
|
|
790
|
+
if (command === 'notify') {
|
|
791
|
+
await this._engine.nodeManager.notify(nodeId, 'Squidclaw', data);
|
|
792
|
+
toolResult = '📨 Notification sent to device';
|
|
793
|
+
} else if (command === 'run') {
|
|
794
|
+
const result = await this._engine.nodeManager.run(nodeId, data);
|
|
795
|
+
toolResult = 'Output: ' + JSON.stringify(result);
|
|
796
|
+
} else {
|
|
797
|
+
const result = await this._engine.nodeManager.sendCommand(nodeId, command, { data });
|
|
798
|
+
toolResult = JSON.stringify(result);
|
|
799
|
+
}
|
|
800
|
+
} catch (err) { toolResult = 'Failed: ' + err.message; }
|
|
801
|
+
break;
|
|
802
|
+
}
|
|
803
|
+
case 'node_camera': {
|
|
804
|
+
if (!this._engine?.nodeManager) { toolResult = 'Nodes not available'; break; }
|
|
805
|
+
try {
|
|
806
|
+
const result = await this._engine.nodeManager.camera(toolArg.trim());
|
|
807
|
+
toolResult = 'Camera captured: ' + JSON.stringify(result);
|
|
808
|
+
} catch (err) { toolResult = 'Failed: ' + err.message; }
|
|
809
|
+
break;
|
|
810
|
+
}
|
|
811
|
+
case 'node_location': {
|
|
812
|
+
if (!this._engine?.nodeManager) { toolResult = 'Nodes not available'; break; }
|
|
813
|
+
try {
|
|
814
|
+
const result = await this._engine.nodeManager.location(toolArg.trim());
|
|
815
|
+
toolResult = 'Location: ' + JSON.stringify(result);
|
|
816
|
+
} catch (err) { toolResult = 'Failed: ' + err.message; }
|
|
817
|
+
break;
|
|
818
|
+
}
|
|
683
819
|
case 'sandbox_info': {
|
|
684
820
|
if (this._engine?.sandbox) {
|
|
685
821
|
const stats = this._engine.sandbox.getStats();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squidclaw",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "🦑 AI agent platform — human-like agents for WhatsApp, Telegram & more",
|
|
5
5
|
"main": "lib/engine.js",
|
|
6
6
|
"bin": {
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"qrcode-terminal": "^0.12.0",
|
|
57
57
|
"sharp": "^0.34.5",
|
|
58
58
|
"undici": "^7.22.0",
|
|
59
|
+
"ws": "^8.19.0",
|
|
59
60
|
"yaml": "^2.8.2",
|
|
60
61
|
"zod": "^4.3.6"
|
|
61
62
|
}
|