squidclaw 2.7.0 โ 2.8.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 +10 -0
- package/lib/features/nodes.js +315 -0
- package/lib/middleware/commands.js +48 -0
- package/lib/tools/router.js +68 -0
- package/package.json +2 -1
package/lib/engine.js
CHANGED
|
@@ -228,6 +228,16 @@ export class SquidclawEngine {
|
|
|
228
228
|
if (pending.c > 0) console.log(` โฐ Reminders: ${pending.c} pending`);
|
|
229
229
|
} catch {}
|
|
230
230
|
|
|
231
|
+
// Nodes (Paired Devices)
|
|
232
|
+
try {
|
|
233
|
+
const { NodeManager } = await import('./features/nodes.js');
|
|
234
|
+
this.nodeManager = new NodeManager(this);
|
|
235
|
+
await this.nodeManager.startServer(9501);
|
|
236
|
+
const nodeCount = this.nodeManager.list('*').length;
|
|
237
|
+
if (nodeCount > 0) console.log(' ๐ก Nodes: ' + nodeCount + ' registered');
|
|
238
|
+
else console.log(' ๐ก Nodes: ws://0.0.0.0:9501');
|
|
239
|
+
} catch (err) { logger.error('engine', 'Nodes init failed: ' + err.message); }
|
|
240
|
+
|
|
231
241
|
// Sandbox
|
|
232
242
|
try {
|
|
233
243
|
const { Sandbox } = await import('./features/sandbox.js');
|
|
@@ -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,54 @@ export async function commandsMiddleware(ctx, next) {
|
|
|
145
145
|
return;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
if (cmd === '/nodes' || cmd === '/devices') {
|
|
149
|
+
if (!ctx.engine.nodeManager) { await ctx.reply('โ Nodes not available'); return; }
|
|
150
|
+
const args = msg.split(/\s+/).slice(1);
|
|
151
|
+
const sub = args[0];
|
|
152
|
+
|
|
153
|
+
if (!sub || sub === 'list') {
|
|
154
|
+
const nodes = ctx.engine.nodeManager.list(ctx.agentId);
|
|
155
|
+
if (nodes.length === 0) {
|
|
156
|
+
await ctx.reply('๐ก No paired devices\n\nUse /nodes pair <name> to add one');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const lines = nodes.map(n =>
|
|
160
|
+
(n.status === 'online' ? '๐ข' : '๐ด') + ' *' + n.name + '* (' + n.type + ')\n ' + n.platform + ' ยท ' + n.id
|
|
161
|
+
);
|
|
162
|
+
await ctx.reply('๐ก *Paired Devices*\n\n' + lines.join('\n\n'));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (sub === 'pair') {
|
|
167
|
+
const name = args.slice(1).join(' ') || 'My Device';
|
|
168
|
+
const token = ctx.engine.nodeManager.generatePairToken(ctx.agentId, name);
|
|
169
|
+
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');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (sub === 'remove') {
|
|
174
|
+
const id = args[1];
|
|
175
|
+
if (!id) { await ctx.reply('Usage: /nodes remove <id>'); return; }
|
|
176
|
+
ctx.engine.nodeManager.removeNode(id);
|
|
177
|
+
await ctx.reply('โ
Device removed');
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (sub === 'notify') {
|
|
182
|
+
const id = args[1];
|
|
183
|
+
const message = args.slice(2).join(' ');
|
|
184
|
+
if (!id || !message) { await ctx.reply('Usage: /nodes notify <id> <message>'); return; }
|
|
185
|
+
try {
|
|
186
|
+
await ctx.engine.nodeManager.notify(id, 'Squidclaw', message);
|
|
187
|
+
await ctx.reply('๐จ Notification sent');
|
|
188
|
+
} catch (err) { await ctx.reply('โ ' + err.message); }
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
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');
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
148
196
|
if (cmd === '/sandbox') {
|
|
149
197
|
if (!ctx.engine.sandbox) { await ctx.reply('โ Sandbox not available'); return; }
|
|
150
198
|
const args = msg.slice(9).trim();
|
package/lib/tools/router.js
CHANGED
|
@@ -169,6 +169,23 @@ 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('', '### Pair Device',
|
|
173
|
+
'---TOOL:node_pair:device name---',
|
|
174
|
+
'Generate a pairing code for a new device (phone, PC, server).',
|
|
175
|
+
'', '### List Devices',
|
|
176
|
+
'---TOOL:node_list:all---',
|
|
177
|
+
'Show all paired devices and their status.',
|
|
178
|
+
'', '### Send to Device',
|
|
179
|
+
'---TOOL:node_cmd:node_id|command|data---',
|
|
180
|
+
'Send a command to a paired device. Commands: camera, screenshot, location, notify, run.',
|
|
181
|
+
'Example: ---TOOL:node_cmd:node_abc123|notify|Meeting in 5 minutes!---',
|
|
182
|
+
'', '### Device Camera',
|
|
183
|
+
'---TOOL:node_camera:node_id---',
|
|
184
|
+
'Take a photo from a paired device camera.',
|
|
185
|
+
'', '### Device Location',
|
|
186
|
+
'---TOOL:node_location:node_id---',
|
|
187
|
+
'Get GPS location of a paired device.');
|
|
188
|
+
|
|
172
189
|
tools.push('', '### Run JavaScript (Sandboxed)',
|
|
173
190
|
'---TOOL:js:console.log(2 + 2)---',
|
|
174
191
|
'Execute JavaScript in a secure VM sandbox. No network, no filesystem, no require. Safe for math, logic, data processing.',
|
|
@@ -680,6 +697,57 @@ export class ToolRouter {
|
|
|
680
697
|
}
|
|
681
698
|
break;
|
|
682
699
|
}
|
|
700
|
+
case 'node_pair': {
|
|
701
|
+
if (!this._engine?.nodeManager) { toolResult = 'Nodes not available'; break; }
|
|
702
|
+
const token = this._engine.nodeManager.generatePairToken(agentId, toolArg || 'Device');
|
|
703
|
+
toolResult = '๐ก Pairing code: *' + token + '*\n\nEnter this code on the device to pair.\nExpires in 5 minutes.\n\nConnect to: ws://<server>:9501/nodes';
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
case 'node_list': {
|
|
707
|
+
if (!this._engine?.nodeManager) { toolResult = 'Nodes not available'; break; }
|
|
708
|
+
const nodes = this._engine.nodeManager.list(agentId);
|
|
709
|
+
if (nodes.length === 0) { toolResult = 'No paired devices. Use node_pair to add one.'; break; }
|
|
710
|
+
toolResult = nodes.map(n =>
|
|
711
|
+
(n.status === 'online' ? '๐ข' : '๐ด') + ' ' + n.name + ' (' + n.type + ')\n ID: ' + n.id + '\n Platform: ' + n.platform + '\n Last seen: ' + (n.lastSeen || 'never')
|
|
712
|
+
).join('\n\n');
|
|
713
|
+
break;
|
|
714
|
+
}
|
|
715
|
+
case 'node_cmd': {
|
|
716
|
+
if (!this._engine?.nodeManager) { toolResult = 'Nodes not available'; break; }
|
|
717
|
+
try {
|
|
718
|
+
const parts = toolArg.split('|').map(p => p.trim());
|
|
719
|
+
const [nodeId, command, ...dataParts] = parts;
|
|
720
|
+
const data = dataParts.join('|');
|
|
721
|
+
|
|
722
|
+
if (command === 'notify') {
|
|
723
|
+
await this._engine.nodeManager.notify(nodeId, 'Squidclaw', data);
|
|
724
|
+
toolResult = '๐จ Notification sent to device';
|
|
725
|
+
} else if (command === 'run') {
|
|
726
|
+
const result = await this._engine.nodeManager.run(nodeId, data);
|
|
727
|
+
toolResult = 'Output: ' + JSON.stringify(result);
|
|
728
|
+
} else {
|
|
729
|
+
const result = await this._engine.nodeManager.sendCommand(nodeId, command, { data });
|
|
730
|
+
toolResult = JSON.stringify(result);
|
|
731
|
+
}
|
|
732
|
+
} catch (err) { toolResult = 'Failed: ' + err.message; }
|
|
733
|
+
break;
|
|
734
|
+
}
|
|
735
|
+
case 'node_camera': {
|
|
736
|
+
if (!this._engine?.nodeManager) { toolResult = 'Nodes not available'; break; }
|
|
737
|
+
try {
|
|
738
|
+
const result = await this._engine.nodeManager.camera(toolArg.trim());
|
|
739
|
+
toolResult = 'Camera captured: ' + JSON.stringify(result);
|
|
740
|
+
} catch (err) { toolResult = 'Failed: ' + err.message; }
|
|
741
|
+
break;
|
|
742
|
+
}
|
|
743
|
+
case 'node_location': {
|
|
744
|
+
if (!this._engine?.nodeManager) { toolResult = 'Nodes not available'; break; }
|
|
745
|
+
try {
|
|
746
|
+
const result = await this._engine.nodeManager.location(toolArg.trim());
|
|
747
|
+
toolResult = 'Location: ' + JSON.stringify(result);
|
|
748
|
+
} catch (err) { toolResult = 'Failed: ' + err.message; }
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
683
751
|
case 'sandbox_info': {
|
|
684
752
|
if (this._engine?.sandbox) {
|
|
685
753
|
const stats = this._engine.sandbox.getStats();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squidclaw",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.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
|
}
|