@covibes/zeroshot 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +167 -0
  2. package/LICENSE +21 -0
  3. package/README.md +364 -0
  4. package/cli/index.js +3990 -0
  5. package/cluster-templates/base-templates/debug-workflow.json +181 -0
  6. package/cluster-templates/base-templates/full-workflow.json +455 -0
  7. package/cluster-templates/base-templates/single-worker.json +48 -0
  8. package/cluster-templates/base-templates/worker-validator.json +131 -0
  9. package/cluster-templates/conductor-bootstrap.json +122 -0
  10. package/cluster-templates/conductor-junior-bootstrap.json +69 -0
  11. package/docker/zeroshot-cluster/Dockerfile +132 -0
  12. package/lib/completion.js +174 -0
  13. package/lib/id-detector.js +53 -0
  14. package/lib/settings.js +97 -0
  15. package/lib/stream-json-parser.js +236 -0
  16. package/package.json +121 -0
  17. package/src/agent/agent-config.js +121 -0
  18. package/src/agent/agent-context-builder.js +241 -0
  19. package/src/agent/agent-hook-executor.js +329 -0
  20. package/src/agent/agent-lifecycle.js +555 -0
  21. package/src/agent/agent-stuck-detector.js +256 -0
  22. package/src/agent/agent-task-executor.js +1034 -0
  23. package/src/agent/agent-trigger-evaluator.js +67 -0
  24. package/src/agent-wrapper.js +459 -0
  25. package/src/agents/git-pusher-agent.json +20 -0
  26. package/src/attach/attach-client.js +438 -0
  27. package/src/attach/attach-server.js +543 -0
  28. package/src/attach/index.js +35 -0
  29. package/src/attach/protocol.js +220 -0
  30. package/src/attach/ring-buffer.js +121 -0
  31. package/src/attach/socket-discovery.js +242 -0
  32. package/src/claude-task-runner.js +468 -0
  33. package/src/config-router.js +80 -0
  34. package/src/config-validator.js +598 -0
  35. package/src/github.js +103 -0
  36. package/src/isolation-manager.js +1042 -0
  37. package/src/ledger.js +429 -0
  38. package/src/logic-engine.js +223 -0
  39. package/src/message-bus-bridge.js +139 -0
  40. package/src/message-bus.js +202 -0
  41. package/src/name-generator.js +232 -0
  42. package/src/orchestrator.js +1938 -0
  43. package/src/schemas/sub-cluster.js +156 -0
  44. package/src/sub-cluster-wrapper.js +545 -0
  45. package/src/task-runner.js +28 -0
  46. package/src/template-resolver.js +347 -0
  47. package/src/tui/CHANGES.txt +133 -0
  48. package/src/tui/LAYOUT.md +261 -0
  49. package/src/tui/README.txt +192 -0
  50. package/src/tui/TWO-LEVEL-NAVIGATION.md +186 -0
  51. package/src/tui/data-poller.js +325 -0
  52. package/src/tui/demo.js +208 -0
  53. package/src/tui/formatters.js +123 -0
  54. package/src/tui/index.js +193 -0
  55. package/src/tui/keybindings.js +383 -0
  56. package/src/tui/layout.js +317 -0
  57. package/src/tui/renderer.js +194 -0
package/src/ledger.js ADDED
@@ -0,0 +1,429 @@
1
+ /**
2
+ * Ledger - Immutable event log for multi-agent coordination
3
+ *
4
+ * Provides:
5
+ * - SQLite-backed message storage with indexes
6
+ * - Query API for message retrieval
7
+ * - In-memory cache for recent queries
8
+ * - Subscription mechanism for real-time updates
9
+ */
10
+
11
+ const Database = require('better-sqlite3');
12
+ const EventEmitter = require('events');
13
+ const crypto = require('crypto');
14
+
15
+ class Ledger extends EventEmitter {
16
+ constructor(dbPath = ':memory:') {
17
+ super();
18
+ this.db = new Database(dbPath);
19
+ this.cache = new Map(); // LRU cache for queries
20
+ this.cacheLimit = 1000;
21
+ this._initSchema();
22
+ }
23
+
24
+ _initSchema() {
25
+ // Enable WAL mode for concurrent reads
26
+ this.db.pragma('journal_mode = WAL');
27
+ // Force synchronous writes so other processes see changes immediately
28
+ this.db.pragma('synchronous = NORMAL');
29
+ // Checkpoint WAL frequently for cross-process visibility
30
+ this.db.pragma('wal_autocheckpoint = 1');
31
+
32
+ // Create messages table
33
+ this.db.exec(`
34
+ CREATE TABLE IF NOT EXISTS messages (
35
+ id TEXT PRIMARY KEY,
36
+ timestamp INTEGER NOT NULL,
37
+ topic TEXT NOT NULL,
38
+ sender TEXT NOT NULL,
39
+ receiver TEXT NOT NULL,
40
+ content_text TEXT,
41
+ content_data TEXT,
42
+ metadata TEXT,
43
+ cluster_id TEXT NOT NULL
44
+ );
45
+
46
+ CREATE INDEX IF NOT EXISTS idx_timestamp ON messages(timestamp);
47
+ CREATE INDEX IF NOT EXISTS idx_topic ON messages(topic);
48
+ CREATE INDEX IF NOT EXISTS idx_cluster_sender ON messages(cluster_id, sender);
49
+ CREATE INDEX IF NOT EXISTS idx_cluster_topic ON messages(cluster_id, topic);
50
+ CREATE INDEX IF NOT EXISTS idx_cluster_timestamp ON messages(cluster_id, timestamp);
51
+ `);
52
+
53
+ this._prepareStatements();
54
+ }
55
+
56
+ _prepareStatements() {
57
+ this.stmts = {
58
+ insert: this.db.prepare(`
59
+ INSERT INTO messages (id, timestamp, topic, sender, receiver, content_text, content_data, metadata, cluster_id)
60
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
61
+ `),
62
+
63
+ queryBase: `SELECT * FROM messages WHERE cluster_id = ?`,
64
+
65
+ count: this.db.prepare(`SELECT COUNT(*) as count FROM messages WHERE cluster_id = ?`),
66
+
67
+ getAll: this.db.prepare(`SELECT * FROM messages WHERE cluster_id = ? ORDER BY timestamp ASC`),
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Append a message to the ledger
73
+ * @param {Object} message - Message object
74
+ * @returns {Object} The appended message with generated ID
75
+ */
76
+ append(message) {
77
+ const id = message.id || `msg_${crypto.randomBytes(16).toString('hex')}`;
78
+ const timestamp = message.timestamp || Date.now();
79
+
80
+ const record = {
81
+ id,
82
+ timestamp,
83
+ topic: message.topic,
84
+ sender: message.sender,
85
+ receiver: message.receiver || 'broadcast',
86
+ content_text: message.content?.text || null,
87
+ content_data: message.content?.data ? JSON.stringify(message.content.data) : null,
88
+ metadata: message.metadata ? JSON.stringify(message.metadata) : null,
89
+ cluster_id: message.cluster_id,
90
+ };
91
+
92
+ try {
93
+ this.stmts.insert.run(
94
+ record.id,
95
+ record.timestamp,
96
+ record.topic,
97
+ record.sender,
98
+ record.receiver,
99
+ record.content_text,
100
+ record.content_data,
101
+ record.metadata,
102
+ record.cluster_id
103
+ );
104
+
105
+ // Invalidate cache
106
+ this.cache.clear();
107
+
108
+ // Emit event for subscriptions
109
+ const fullMessage = this._deserializeMessage(record);
110
+ this.emit('message', fullMessage);
111
+ this.emit(`topic:${message.topic}`, fullMessage);
112
+
113
+ return fullMessage;
114
+ } catch (error) {
115
+ throw new Error(`Failed to append message: ${error.message}`);
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Query messages with filters
121
+ * @param {Object} criteria - Query criteria
122
+ * @returns {Array} Matching messages
123
+ */
124
+ query(criteria) {
125
+ const { cluster_id, topic, sender, receiver, since, until, limit, offset } = criteria;
126
+
127
+ if (!cluster_id) {
128
+ throw new Error('cluster_id is required for queries');
129
+ }
130
+
131
+ // Build query
132
+ const conditions = ['cluster_id = ?'];
133
+ const params = [cluster_id];
134
+
135
+ if (topic) {
136
+ conditions.push('topic = ?');
137
+ params.push(topic);
138
+ }
139
+
140
+ if (sender) {
141
+ conditions.push('sender = ?');
142
+ params.push(sender);
143
+ }
144
+
145
+ if (receiver) {
146
+ conditions.push('receiver = ?');
147
+ params.push(receiver);
148
+ }
149
+
150
+ if (since) {
151
+ conditions.push('timestamp >= ?');
152
+ params.push(typeof since === 'number' ? since : new Date(since).getTime());
153
+ }
154
+
155
+ if (until) {
156
+ conditions.push('timestamp <= ?');
157
+ params.push(typeof until === 'number' ? until : new Date(until).getTime());
158
+ }
159
+
160
+ let sql = `SELECT * FROM messages WHERE ${conditions.join(' AND ')} ORDER BY timestamp ASC`;
161
+
162
+ if (limit) {
163
+ sql += ` LIMIT ?`;
164
+ params.push(limit);
165
+ }
166
+
167
+ if (offset) {
168
+ sql += ` OFFSET ?`;
169
+ params.push(offset);
170
+ }
171
+
172
+ const stmt = this.db.prepare(sql);
173
+ const rows = stmt.all(...params);
174
+ return rows.map((row) => this._deserializeMessage(row));
175
+ }
176
+
177
+ /**
178
+ * Find the last message matching criteria
179
+ * @param {Object} criteria - Query criteria
180
+ * @returns {Object|null} Last matching message
181
+ */
182
+ findLast(criteria) {
183
+ const { cluster_id, topic, sender, receiver, since, until } = criteria;
184
+
185
+ if (!cluster_id) {
186
+ throw new Error('cluster_id is required for queries');
187
+ }
188
+
189
+ // Build query with DESC order
190
+ const conditions = ['cluster_id = ?'];
191
+ const params = [cluster_id];
192
+
193
+ if (topic) {
194
+ conditions.push('topic = ?');
195
+ params.push(topic);
196
+ }
197
+
198
+ if (sender) {
199
+ conditions.push('sender = ?');
200
+ params.push(sender);
201
+ }
202
+
203
+ if (receiver) {
204
+ conditions.push('receiver = ?');
205
+ params.push(receiver);
206
+ }
207
+
208
+ if (since) {
209
+ conditions.push('timestamp >= ?');
210
+ params.push(typeof since === 'number' ? since : new Date(since).getTime());
211
+ }
212
+
213
+ if (until) {
214
+ conditions.push('timestamp <= ?');
215
+ params.push(typeof until === 'number' ? until : new Date(until).getTime());
216
+ }
217
+
218
+ const sql = `SELECT * FROM messages WHERE ${conditions.join(' AND ')} ORDER BY timestamp DESC LIMIT 1`;
219
+
220
+ const stmt = this.db.prepare(sql);
221
+ const row = stmt.get(...params);
222
+ return row ? this._deserializeMessage(row) : null;
223
+ }
224
+
225
+ /**
226
+ * Count messages matching criteria
227
+ * @param {Object} criteria - Query criteria
228
+ * @returns {Number} Message count
229
+ */
230
+ count(criteria) {
231
+ const { cluster_id, topic } = criteria;
232
+
233
+ if (!cluster_id) {
234
+ throw new Error('cluster_id is required for count');
235
+ }
236
+
237
+ let sql = 'SELECT COUNT(*) as count FROM messages WHERE cluster_id = ?';
238
+ const params = [cluster_id];
239
+
240
+ if (topic) {
241
+ sql += ' AND topic = ?';
242
+ params.push(topic);
243
+ }
244
+
245
+ const stmt = this.db.prepare(sql);
246
+ const result = stmt.get(...params);
247
+ return result.count;
248
+ }
249
+
250
+ /**
251
+ * Get messages since a specific timestamp
252
+ * @param {Object} params - { cluster_id, timestamp }
253
+ * @returns {Array} Messages since timestamp
254
+ */
255
+ since(params) {
256
+ return this.query({
257
+ cluster_id: params.cluster_id,
258
+ since: params.timestamp,
259
+ });
260
+ }
261
+
262
+ /**
263
+ * Get all messages for a cluster
264
+ * @param {String} cluster_id - Cluster ID
265
+ * @returns {Array} All messages
266
+ */
267
+ getAll(cluster_id) {
268
+ const rows = this.stmts.getAll.all(cluster_id);
269
+ return rows.map((row) => this._deserializeMessage(row));
270
+ }
271
+
272
+ /**
273
+ * Subscribe to new messages
274
+ * @param {Function} callback - Called with each new message
275
+ * @returns {Function} Unsubscribe function
276
+ */
277
+ subscribe(callback) {
278
+ this.on('message', callback);
279
+ return () => this.off('message', callback);
280
+ }
281
+
282
+ /**
283
+ * Poll for new messages (cross-process support)
284
+ * @param {String} clusterId - Cluster ID to poll (null for all clusters)
285
+ * @param {Function} callback - Called with each new message
286
+ * @param {Number} intervalMs - Poll interval (default 500ms)
287
+ * @param {Number} initialCount - Number of messages to show initially (default 300)
288
+ * @returns {Function} Stop polling function
289
+ */
290
+ pollForMessages(clusterId, callback, intervalMs = 500, initialCount = 300) {
291
+ let lastTimestamp = 0;
292
+ let lastMessageIds = new Set();
293
+ let isFirstPoll = true;
294
+
295
+ const poll = () => {
296
+ try {
297
+ let sql, params;
298
+
299
+ if (isFirstPoll) {
300
+ // First poll: get last N messages by count
301
+ if (clusterId) {
302
+ sql =
303
+ 'SELECT * FROM (SELECT * FROM messages WHERE cluster_id = ? ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC';
304
+ params = [clusterId, initialCount];
305
+ } else {
306
+ sql =
307
+ 'SELECT * FROM (SELECT * FROM messages ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC';
308
+ params = [initialCount];
309
+ }
310
+ isFirstPoll = false;
311
+ } else {
312
+ // Subsequent polls: get messages since last timestamp
313
+ if (clusterId) {
314
+ sql =
315
+ 'SELECT * FROM messages WHERE cluster_id = ? AND timestamp >= ? ORDER BY timestamp ASC';
316
+ params = [clusterId, lastTimestamp - 1000]; // 1s buffer for race conditions
317
+ } else {
318
+ sql = 'SELECT * FROM messages WHERE timestamp >= ? ORDER BY timestamp ASC';
319
+ params = [lastTimestamp - 1000];
320
+ }
321
+ }
322
+
323
+ const stmt = this.db.prepare(sql);
324
+ const rows = stmt.all(...params);
325
+
326
+ for (const row of rows) {
327
+ // Skip already-seen messages
328
+ if (lastMessageIds.has(row.id)) continue;
329
+
330
+ lastMessageIds.add(row.id);
331
+ const message = this._deserializeMessage(row);
332
+ callback(message);
333
+
334
+ // Update timestamp high-water mark
335
+ if (row.timestamp > lastTimestamp) {
336
+ lastTimestamp = row.timestamp;
337
+ }
338
+ }
339
+
340
+ // Prune old message IDs to prevent memory leak
341
+ if (lastMessageIds.size > 10000) {
342
+ const idsArray = Array.from(lastMessageIds);
343
+ lastMessageIds = new Set(idsArray.slice(-5000));
344
+ }
345
+ } catch (error) {
346
+ // DB busy is expected during concurrent access - log but continue polling
347
+ // Other errors indicate real bugs and should be visible
348
+ console.error(`[Ledger] pollForMessages error (will retry): ${error.message}`);
349
+ }
350
+ };
351
+
352
+ // Initial poll
353
+ poll();
354
+
355
+ // Set up interval
356
+ const intervalId = setInterval(poll, intervalMs);
357
+
358
+ // Return stop function
359
+ return () => clearInterval(intervalId);
360
+ }
361
+
362
+ /**
363
+ * Subscribe to specific topic
364
+ * @param {String} topic - Topic to subscribe to
365
+ * @param {Function} callback - Called with matching messages
366
+ * @returns {Function} Unsubscribe function
367
+ */
368
+ subscribeTopic(topic, callback) {
369
+ const event = `topic:${topic}`;
370
+ this.on(event, callback);
371
+ return () => this.off(event, callback);
372
+ }
373
+
374
+ /**
375
+ * Deserialize a database row into a message object
376
+ * @private
377
+ */
378
+ _deserializeMessage(row) {
379
+ const message = {
380
+ id: row.id,
381
+ timestamp: row.timestamp,
382
+ topic: row.topic,
383
+ sender: row.sender,
384
+ receiver: row.receiver,
385
+ cluster_id: row.cluster_id,
386
+ };
387
+
388
+ if (row.content_text || row.content_data) {
389
+ message.content = {};
390
+ if (row.content_text) {
391
+ message.content.text = row.content_text;
392
+ }
393
+ if (row.content_data) {
394
+ try {
395
+ message.content.data = JSON.parse(row.content_data);
396
+ } catch {
397
+ message.content.data = null;
398
+ }
399
+ }
400
+ }
401
+
402
+ if (row.metadata) {
403
+ try {
404
+ message.metadata = JSON.parse(row.metadata);
405
+ } catch {
406
+ message.metadata = null;
407
+ }
408
+ }
409
+
410
+ return message;
411
+ }
412
+
413
+ /**
414
+ * Close the database connection
415
+ */
416
+ close() {
417
+ this.db.close();
418
+ }
419
+
420
+ /**
421
+ * Clear all messages (for testing)
422
+ */
423
+ clear() {
424
+ this.db.exec('DELETE FROM messages');
425
+ this.cache.clear();
426
+ }
427
+ }
428
+
429
+ module.exports = Ledger;
@@ -0,0 +1,223 @@
1
+ /**
2
+ * LogicEngine - JavaScript sandbox for agent decision logic
3
+ *
4
+ * Provides:
5
+ * - Isolated VM for executing agent trigger logic
6
+ * - Timeout enforcement (1 second)
7
+ * - Ledger API access for queries
8
+ * - Helper functions for common patterns
9
+ * - Sandbox security (no fs, network, child_process)
10
+ */
11
+
12
+ const vm = require('vm');
13
+
14
+ class LogicEngine {
15
+ constructor(messageBus, cluster) {
16
+ this.messageBus = messageBus;
17
+ this.cluster = cluster;
18
+ this.timeout = 1000; // 1 second
19
+ }
20
+
21
+ /**
22
+ * Evaluate a trigger logic script
23
+ * @param {String} script - JavaScript code to evaluate
24
+ * @param {Object} agent - Agent context
25
+ * @param {Object} message - Triggering message
26
+ * @returns {Boolean} Whether agent should wake up
27
+ */
28
+ evaluate(script, agent, message) {
29
+ try {
30
+ // Build sandbox context
31
+ const context = this._buildContext(agent, message);
32
+
33
+ // Create isolated context with frozen prototypes
34
+ // This prevents prototype pollution attacks
35
+ const isolatedContext = {};
36
+
37
+ // Freeze Object, Array, Function prototypes in the sandbox
38
+ isolatedContext.Object = Object.freeze({ ...Object });
39
+ isolatedContext.Array = Array;
40
+ isolatedContext.Function = Function;
41
+
42
+ // Copy safe context properties
43
+ Object.assign(isolatedContext, context);
44
+
45
+ // Wrap script to prevent prototype access
46
+ const wrappedScript = `(function() {
47
+ 'use strict';
48
+ // Prevent prototype pollution
49
+ const frozenObject = Object;
50
+ const frozenArray = Array;
51
+ Object.freeze(frozenObject.prototype);
52
+ Object.freeze(frozenArray.prototype);
53
+
54
+ ${script}
55
+ })()`;
56
+
57
+ // Create and run in context
58
+ vm.createContext(isolatedContext);
59
+ const result = vm.runInContext(wrappedScript, isolatedContext, {
60
+ timeout: this.timeout,
61
+ displayErrors: true,
62
+ });
63
+
64
+ // Coerce to boolean
65
+ return Boolean(result);
66
+ } catch (error) {
67
+ console.error(`Logic evaluation error for agent ${agent.id}:`, error.message);
68
+ return false; // Default to false on error
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Build sandbox context with APIs and helpers
74
+ * @private
75
+ */
76
+ _buildContext(agent, message) {
77
+ const clusterId = agent.cluster_id;
78
+
79
+ // Ledger API wrapper (auto-scoped to cluster)
80
+ const ledgerAPI = {
81
+ query: (criteria) => {
82
+ return this.messageBus.query({ ...criteria, cluster_id: clusterId });
83
+ },
84
+
85
+ findLast: (criteria) => {
86
+ return this.messageBus.findLast({ ...criteria, cluster_id: clusterId });
87
+ },
88
+
89
+ count: (criteria) => {
90
+ return this.messageBus.count({ ...criteria, cluster_id: clusterId });
91
+ },
92
+
93
+ since: (timestamp) => {
94
+ return this.messageBus.since({ cluster_id: clusterId, timestamp });
95
+ },
96
+ };
97
+
98
+ // Helper functions
99
+ const helpers = {
100
+ /**
101
+ * Check if all agents have responded to a topic since a timestamp
102
+ */
103
+ allResponded: (agents, topic, since) => {
104
+ const responses = ledgerAPI.query({ topic, since });
105
+ const responders = new Set(responses.map((r) => r.sender));
106
+ return agents.every((a) => responders.has(a.id || a));
107
+ },
108
+
109
+ /**
110
+ * Check if all responses have approved=true
111
+ */
112
+ hasConsensus: (topic, since) => {
113
+ const responses = ledgerAPI.query({ topic, since });
114
+ if (responses.length === 0) return false;
115
+ return responses.every((r) => r.content?.data?.approved === true);
116
+ },
117
+
118
+ /**
119
+ * Get time since last message on topic (in milliseconds)
120
+ */
121
+ timeSinceLastMessage: (topic) => {
122
+ const last = ledgerAPI.findLast({ topic });
123
+ if (!last) return Infinity;
124
+ return Date.now() - last.timestamp;
125
+ },
126
+
127
+ /**
128
+ * Check if a topic has any messages since timestamp
129
+ */
130
+ hasMessagesSince: (topic, since) => {
131
+ const count = ledgerAPI.count({ topic, since });
132
+ return count > 0;
133
+ },
134
+
135
+ /**
136
+ * Get cluster config based on complexity and task type
137
+ * Returns: { base: 'template-name', params: { ... } }
138
+ */
139
+ getConfig: require('./config-router').getConfig,
140
+ };
141
+
142
+ // Cluster API wrapper
143
+ const clusterAPI = {
144
+ id: clusterId,
145
+
146
+ getAgents: () => {
147
+ return this.cluster ? this.cluster.agents || [] : [];
148
+ },
149
+
150
+ getAgentsByRole: (role) => {
151
+ return this.cluster ? (this.cluster.agents || []).filter((a) => a.role === role) : [];
152
+ },
153
+
154
+ getAgent: (id) => {
155
+ return this.cluster ? (this.cluster.agents || []).find((a) => a.id === id) : null;
156
+ },
157
+ };
158
+
159
+ // Build context
160
+ return {
161
+ // Agent context
162
+ agent: {
163
+ id: agent.id,
164
+ role: agent.role,
165
+ iteration: agent.iteration || 0,
166
+ },
167
+
168
+ // Triggering message
169
+ message: message || null,
170
+
171
+ // APIs
172
+ ledger: ledgerAPI,
173
+ cluster: clusterAPI,
174
+ helpers,
175
+
176
+ // Safe built-ins
177
+ Set,
178
+ Map,
179
+ Array,
180
+ Object,
181
+ String,
182
+ Number,
183
+ Boolean,
184
+ Math,
185
+ Date,
186
+ JSON,
187
+
188
+ // No-op console (prevent output in production)
189
+ console: {
190
+ log: () => {},
191
+ error: () => {},
192
+ warn: () => {},
193
+ info: () => {},
194
+ },
195
+ };
196
+ }
197
+
198
+ /**
199
+ * Validate script syntax without executing
200
+ * @param {String} script - JavaScript code
201
+ * @returns {Object} { valid: Boolean, error: String }
202
+ */
203
+ validateScript(script) {
204
+ try {
205
+ // Wrap in function like evaluate() does
206
+ const wrappedScript = `(function() { ${script} })()`;
207
+ new vm.Script(wrappedScript);
208
+ return { valid: true, error: null };
209
+ } catch (error) {
210
+ return { valid: false, error: error.message };
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Set timeout for script execution
216
+ * @param {Number} ms - Timeout in milliseconds
217
+ */
218
+ setTimeout(ms) {
219
+ this.timeout = ms;
220
+ }
221
+ }
222
+
223
+ module.exports = LogicEngine;