pnpfucius 2.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/README.md +396 -0
- package/bin/claude-predict.js +5 -0
- package/bin/pnpfucius.js +8 -0
- package/package.json +71 -0
- package/src/agent.js +1037 -0
- package/src/ai/index.js +6 -0
- package/src/ai/market-generator.js +186 -0
- package/src/ai/resolver.js +172 -0
- package/src/ai/scorer.js +184 -0
- package/src/analytics/aggregator.js +198 -0
- package/src/cli.js +948 -0
- package/src/collateral/privacy-tokens.js +183 -0
- package/src/config.js +128 -0
- package/src/daemon/index.js +321 -0
- package/src/daemon/lifecycle.js +168 -0
- package/src/daemon/scheduler.js +252 -0
- package/src/events/emitter.js +147 -0
- package/src/helius/client.js +221 -0
- package/src/helius/transaction-tracker.js +192 -0
- package/src/helius/webhooks.js +233 -0
- package/src/index.js +139 -0
- package/src/monitoring/news-monitor.js +262 -0
- package/src/monitoring/news-scorer.js +236 -0
- package/src/predict/agent.js +291 -0
- package/src/predict/prompts.js +69 -0
- package/src/predict/slash-commands.js +361 -0
- package/src/predict/tools/analytics-tools.js +83 -0
- package/src/predict/tools/bash-tool.js +87 -0
- package/src/predict/tools/file-tools.js +140 -0
- package/src/predict/tools/index.js +120 -0
- package/src/predict/tools/market-tools.js +851 -0
- package/src/predict/tools/news-tools.js +130 -0
- package/src/predict/ui/renderer.js +215 -0
- package/src/predict/ui/welcome.js +146 -0
- package/src/privacy-markets.js +194 -0
- package/src/storage/market-store.js +418 -0
- package/src/utils/spinner.js +172 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// Graceful shutdown and signal handling for daemon mode
|
|
2
|
+
// Ensures state is saved and resources are cleaned up
|
|
3
|
+
|
|
4
|
+
import { agentEvents, AgentEvents } from '../events/emitter.js';
|
|
5
|
+
|
|
6
|
+
export function setupGracefulShutdown(daemon, options = {}) {
|
|
7
|
+
const signals = ['SIGINT', 'SIGTERM', 'SIGHUP'];
|
|
8
|
+
let shuttingDown = false;
|
|
9
|
+
const timeout = options.timeout || 30000;
|
|
10
|
+
|
|
11
|
+
const shutdown = async (signal) => {
|
|
12
|
+
if (shuttingDown) {
|
|
13
|
+
console.log('Shutdown already in progress...');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
shuttingDown = true;
|
|
18
|
+
console.log(`\nReceived ${signal}, shutting down gracefully...`);
|
|
19
|
+
|
|
20
|
+
// Set a hard timeout
|
|
21
|
+
const hardTimeout = setTimeout(() => {
|
|
22
|
+
console.error('Shutdown timeout exceeded, forcing exit');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}, timeout);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
agentEvents.emitTyped(AgentEvents.DAEMON_STOPPED, {
|
|
28
|
+
signal,
|
|
29
|
+
reason: 'graceful_shutdown'
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
await daemon.stop();
|
|
33
|
+
console.log('Daemon stopped successfully');
|
|
34
|
+
|
|
35
|
+
clearTimeout(hardTimeout);
|
|
36
|
+
process.exit(0);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('Error during shutdown:', error.message);
|
|
39
|
+
clearTimeout(hardTimeout);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Register signal handlers
|
|
45
|
+
for (const signal of signals) {
|
|
46
|
+
process.on(signal, () => shutdown(signal));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Handle uncaught exceptions
|
|
50
|
+
process.on('uncaughtException', async (error) => {
|
|
51
|
+
console.error('Uncaught exception:', error);
|
|
52
|
+
|
|
53
|
+
agentEvents.emitTyped(AgentEvents.DAEMON_ERROR, {
|
|
54
|
+
type: 'uncaughtException',
|
|
55
|
+
error: error.message,
|
|
56
|
+
stack: error.stack
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
if (daemon.saveState) {
|
|
61
|
+
await daemon.saveState();
|
|
62
|
+
}
|
|
63
|
+
} catch (saveError) {
|
|
64
|
+
console.error('Failed to save state:', saveError.message);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
process.exit(1);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Handle unhandled promise rejections
|
|
71
|
+
process.on('unhandledRejection', async (reason, promise) => {
|
|
72
|
+
console.error('Unhandled rejection at:', promise, 'reason:', reason);
|
|
73
|
+
|
|
74
|
+
agentEvents.emitTyped(AgentEvents.DAEMON_ERROR, {
|
|
75
|
+
type: 'unhandledRejection',
|
|
76
|
+
error: String(reason)
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
if (daemon.saveState) {
|
|
81
|
+
await daemon.saveState();
|
|
82
|
+
}
|
|
83
|
+
} catch (saveError) {
|
|
84
|
+
console.error('Failed to save state:', saveError.message);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
process.exit(1);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
shutdown,
|
|
92
|
+
isShuttingDown: () => shuttingDown
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Health check helper for long-running processes
|
|
97
|
+
export class HealthMonitor {
|
|
98
|
+
constructor(options = {}) {
|
|
99
|
+
this.checkInterval = options.checkInterval || 60000;
|
|
100
|
+
this.maxMemoryMB = options.maxMemoryMB || 512;
|
|
101
|
+
this.onUnhealthy = options.onUnhealthy || null;
|
|
102
|
+
this.timer = null;
|
|
103
|
+
this.startTime = Date.now();
|
|
104
|
+
this.lastCheck = null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
start() {
|
|
108
|
+
this.timer = setInterval(() => this.check(), this.checkInterval);
|
|
109
|
+
return this;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
stop() {
|
|
113
|
+
if (this.timer) {
|
|
114
|
+
clearInterval(this.timer);
|
|
115
|
+
this.timer = null;
|
|
116
|
+
}
|
|
117
|
+
return this;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
check() {
|
|
121
|
+
const health = this.getHealth();
|
|
122
|
+
this.lastCheck = Date.now();
|
|
123
|
+
|
|
124
|
+
if (!health.healthy && this.onUnhealthy) {
|
|
125
|
+
this.onUnhealthy(health);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return health;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
getHealth() {
|
|
132
|
+
const memUsage = process.memoryUsage();
|
|
133
|
+
const heapUsedMB = Math.round(memUsage.heapUsed / 1024 / 1024);
|
|
134
|
+
const rssMB = Math.round(memUsage.rss / 1024 / 1024);
|
|
135
|
+
|
|
136
|
+
const uptime = Date.now() - this.startTime;
|
|
137
|
+
const memoryOk = heapUsedMB < this.maxMemoryMB;
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
healthy: memoryOk,
|
|
141
|
+
uptime,
|
|
142
|
+
uptimeHuman: this.formatUptime(uptime),
|
|
143
|
+
memory: {
|
|
144
|
+
heapUsedMB,
|
|
145
|
+
rssMB,
|
|
146
|
+
maxMB: this.maxMemoryMB
|
|
147
|
+
},
|
|
148
|
+
lastCheck: this.lastCheck,
|
|
149
|
+
issues: memoryOk ? [] : ['Memory usage exceeded threshold']
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
formatUptime(ms) {
|
|
154
|
+
const seconds = Math.floor(ms / 1000);
|
|
155
|
+
const minutes = Math.floor(seconds / 60);
|
|
156
|
+
const hours = Math.floor(minutes / 60);
|
|
157
|
+
const days = Math.floor(hours / 24);
|
|
158
|
+
|
|
159
|
+
if (days > 0) return `${days}d ${hours % 24}h`;
|
|
160
|
+
if (hours > 0) return `${hours}h ${minutes % 60}m`;
|
|
161
|
+
if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
|
|
162
|
+
return `${seconds}s`;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function createHealthMonitor(options) {
|
|
167
|
+
return new HealthMonitor(options);
|
|
168
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// Cron-style scheduling engine for autonomous agent operation
|
|
2
|
+
// Supports both cron expressions and interval strings
|
|
3
|
+
|
|
4
|
+
import cronParser from 'cron-parser';
|
|
5
|
+
|
|
6
|
+
export class Scheduler {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.tasks = new Map();
|
|
9
|
+
this.timers = new Map();
|
|
10
|
+
this.running = false;
|
|
11
|
+
this.taskRuns = new Map(); // Track execution counts
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
addTask(config) {
|
|
15
|
+
const { name, schedule, task, runImmediately = false, enabled = true } = config;
|
|
16
|
+
|
|
17
|
+
if (!name || !schedule || !task) {
|
|
18
|
+
throw new Error('Task requires name, schedule, and task function');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const parsed = this.parseSchedule(schedule);
|
|
22
|
+
|
|
23
|
+
this.tasks.set(name, {
|
|
24
|
+
schedule: parsed,
|
|
25
|
+
originalSchedule: schedule,
|
|
26
|
+
task,
|
|
27
|
+
runImmediately,
|
|
28
|
+
enabled,
|
|
29
|
+
lastRun: null,
|
|
30
|
+
nextRun: null,
|
|
31
|
+
runCount: 0,
|
|
32
|
+
errors: []
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
removeTask(name) {
|
|
39
|
+
const timer = this.timers.get(name);
|
|
40
|
+
if (timer) {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
this.timers.delete(name);
|
|
43
|
+
}
|
|
44
|
+
this.tasks.delete(name);
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
enableTask(name) {
|
|
49
|
+
const task = this.tasks.get(name);
|
|
50
|
+
if (task) {
|
|
51
|
+
task.enabled = true;
|
|
52
|
+
if (this.running) {
|
|
53
|
+
this.scheduleNext(name, task);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
disableTask(name) {
|
|
60
|
+
const task = this.tasks.get(name);
|
|
61
|
+
if (task) {
|
|
62
|
+
task.enabled = false;
|
|
63
|
+
const timer = this.timers.get(name);
|
|
64
|
+
if (timer) {
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
this.timers.delete(name);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
parseSchedule(schedule) {
|
|
73
|
+
// Support cron expressions (5 or 6 fields)
|
|
74
|
+
if (schedule.includes('*') || schedule.split(' ').length >= 5) {
|
|
75
|
+
return { type: 'cron', expression: schedule };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Support interval strings like "30s", "5m", "1h", "2d"
|
|
79
|
+
const match = schedule.match(/^(\d+)(s|m|h|d)$/i);
|
|
80
|
+
if (match) {
|
|
81
|
+
const multipliers = {
|
|
82
|
+
s: 1000,
|
|
83
|
+
m: 60 * 1000,
|
|
84
|
+
h: 60 * 60 * 1000,
|
|
85
|
+
d: 24 * 60 * 60 * 1000
|
|
86
|
+
};
|
|
87
|
+
const value = parseInt(match[1], 10);
|
|
88
|
+
const unit = match[2].toLowerCase();
|
|
89
|
+
return {
|
|
90
|
+
type: 'interval',
|
|
91
|
+
ms: value * multipliers[unit],
|
|
92
|
+
original: schedule
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Support plain milliseconds
|
|
97
|
+
const ms = parseInt(schedule, 10);
|
|
98
|
+
if (!isNaN(ms)) {
|
|
99
|
+
return { type: 'interval', ms };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
throw new Error(`Invalid schedule format: ${schedule}. Use cron expression or interval (e.g., "5m", "1h")`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
start() {
|
|
106
|
+
if (this.running) return this;
|
|
107
|
+
|
|
108
|
+
this.running = true;
|
|
109
|
+
|
|
110
|
+
for (const [name, config] of this.tasks) {
|
|
111
|
+
if (!config.enabled) continue;
|
|
112
|
+
|
|
113
|
+
if (config.runImmediately) {
|
|
114
|
+
this.executeTask(name, config);
|
|
115
|
+
}
|
|
116
|
+
this.scheduleNext(name, config);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return this;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
stop() {
|
|
123
|
+
this.running = false;
|
|
124
|
+
|
|
125
|
+
for (const timer of this.timers.values()) {
|
|
126
|
+
clearTimeout(timer);
|
|
127
|
+
}
|
|
128
|
+
this.timers.clear();
|
|
129
|
+
|
|
130
|
+
return this;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
scheduleNext(name, config) {
|
|
134
|
+
if (!this.running || !config.enabled) return;
|
|
135
|
+
|
|
136
|
+
let delay;
|
|
137
|
+
|
|
138
|
+
if (config.schedule.type === 'interval') {
|
|
139
|
+
delay = config.schedule.ms;
|
|
140
|
+
} else {
|
|
141
|
+
try {
|
|
142
|
+
const interval = cronParser.parseExpression(config.schedule.expression);
|
|
143
|
+
const nextDate = interval.next().toDate();
|
|
144
|
+
delay = nextDate.getTime() - Date.now();
|
|
145
|
+
|
|
146
|
+
// Ensure minimum delay of 1 second
|
|
147
|
+
if (delay < 1000) {
|
|
148
|
+
delay = 1000;
|
|
149
|
+
}
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.error(`Invalid cron expression for task ${name}:`, error.message);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
config.nextRun = Date.now() + delay;
|
|
157
|
+
|
|
158
|
+
const timer = setTimeout(async () => {
|
|
159
|
+
if (!this.running || !config.enabled) return;
|
|
160
|
+
|
|
161
|
+
await this.executeTask(name, config);
|
|
162
|
+
this.scheduleNext(name, config);
|
|
163
|
+
}, delay);
|
|
164
|
+
|
|
165
|
+
this.timers.set(name, timer);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async executeTask(name, config) {
|
|
169
|
+
config.lastRun = Date.now();
|
|
170
|
+
config.runCount++;
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
await config.task();
|
|
174
|
+
} catch (error) {
|
|
175
|
+
config.errors.push({
|
|
176
|
+
time: Date.now(),
|
|
177
|
+
message: error.message
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Keep only last 10 errors
|
|
181
|
+
if (config.errors.length > 10) {
|
|
182
|
+
config.errors.shift();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
console.error(`Task "${name}" failed:`, error.message);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Trigger immediate execution of a task
|
|
190
|
+
async runNow(name) {
|
|
191
|
+
const config = this.tasks.get(name);
|
|
192
|
+
if (!config) {
|
|
193
|
+
throw new Error(`Task not found: ${name}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
await this.executeTask(name, config);
|
|
197
|
+
return this;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
getNextRun(taskName) {
|
|
201
|
+
const config = this.tasks.get(taskName);
|
|
202
|
+
if (!config) return null;
|
|
203
|
+
|
|
204
|
+
return config.nextRun;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
getTaskInfo(taskName) {
|
|
208
|
+
const config = this.tasks.get(taskName);
|
|
209
|
+
if (!config) return null;
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
name: taskName,
|
|
213
|
+
schedule: config.originalSchedule,
|
|
214
|
+
enabled: config.enabled,
|
|
215
|
+
lastRun: config.lastRun,
|
|
216
|
+
nextRun: config.nextRun,
|
|
217
|
+
runCount: config.runCount,
|
|
218
|
+
recentErrors: config.errors.slice(-5)
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
getAllTasks() {
|
|
223
|
+
const tasks = [];
|
|
224
|
+
for (const [name, config] of this.tasks) {
|
|
225
|
+
tasks.push(this.getTaskInfo(name));
|
|
226
|
+
}
|
|
227
|
+
return tasks;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
isRunning() {
|
|
231
|
+
return this.running;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function createScheduler() {
|
|
236
|
+
return new Scheduler();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Helper to format next run time
|
|
240
|
+
export function formatNextRun(timestamp) {
|
|
241
|
+
if (!timestamp) return 'Not scheduled';
|
|
242
|
+
|
|
243
|
+
const now = Date.now();
|
|
244
|
+
const diff = timestamp - now;
|
|
245
|
+
|
|
246
|
+
if (diff < 0) return 'Overdue';
|
|
247
|
+
if (diff < 60000) return `${Math.round(diff / 1000)}s`;
|
|
248
|
+
if (diff < 3600000) return `${Math.round(diff / 60000)}m`;
|
|
249
|
+
if (diff < 86400000) return `${Math.round(diff / 3600000)}h`;
|
|
250
|
+
|
|
251
|
+
return new Date(timestamp).toLocaleString();
|
|
252
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Central event bus for agent status updates and coordination
|
|
2
|
+
// Uses Node.js EventEmitter with typed events
|
|
3
|
+
|
|
4
|
+
import { EventEmitter } from 'events';
|
|
5
|
+
|
|
6
|
+
// Event type constants
|
|
7
|
+
export const AgentEvents = {
|
|
8
|
+
// Daemon lifecycle
|
|
9
|
+
DAEMON_STARTED: 'daemon:started',
|
|
10
|
+
DAEMON_STOPPED: 'daemon:stopped',
|
|
11
|
+
DAEMON_ERROR: 'daemon:error',
|
|
12
|
+
|
|
13
|
+
// Cycle events
|
|
14
|
+
CYCLE_START: 'cycle:start',
|
|
15
|
+
CYCLE_COMPLETE: 'cycle:complete',
|
|
16
|
+
CYCLE_ERROR: 'cycle:error',
|
|
17
|
+
|
|
18
|
+
// Market events
|
|
19
|
+
MARKET_CREATED: 'market:created',
|
|
20
|
+
MARKET_FAILED: 'market:failed',
|
|
21
|
+
MARKET_RESOLVED: 'market:resolved',
|
|
22
|
+
MARKET_UPDATED: 'market:updated',
|
|
23
|
+
|
|
24
|
+
// Transaction events
|
|
25
|
+
TX_SENT: 'tx:sent',
|
|
26
|
+
TX_CONFIRMED: 'tx:confirmed',
|
|
27
|
+
TX_FAILED: 'tx:failed',
|
|
28
|
+
|
|
29
|
+
// News events
|
|
30
|
+
NEWS_EVENT: 'news:event',
|
|
31
|
+
NEWS_CHECK_COMPLETE: 'news:check:complete',
|
|
32
|
+
|
|
33
|
+
// Webhook events
|
|
34
|
+
WEBHOOK_RECEIVED: 'webhook:received',
|
|
35
|
+
WEBHOOK_SERVER_STARTED: 'webhook:server:started',
|
|
36
|
+
WEBHOOK_SERVER_STOPPED: 'webhook:server:stopped',
|
|
37
|
+
HELIUS_EVENT: 'helius:event',
|
|
38
|
+
|
|
39
|
+
// State events
|
|
40
|
+
STATE_SAVED: 'state:saved',
|
|
41
|
+
STATE_RESTORED: 'state:restored',
|
|
42
|
+
|
|
43
|
+
// Analytics events
|
|
44
|
+
STATS_UPDATED: 'stats:updated'
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
class AgentEventEmitter extends EventEmitter {
|
|
48
|
+
constructor() {
|
|
49
|
+
super();
|
|
50
|
+
this.setMaxListeners(50);
|
|
51
|
+
this.eventHistory = [];
|
|
52
|
+
this.maxHistorySize = 100;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Emit with timestamp and optional metadata
|
|
56
|
+
emitTyped(event, payload = {}) {
|
|
57
|
+
const eventData = {
|
|
58
|
+
type: event,
|
|
59
|
+
timestamp: Date.now(),
|
|
60
|
+
...payload
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Store in history
|
|
64
|
+
this.eventHistory.unshift(eventData);
|
|
65
|
+
if (this.eventHistory.length > this.maxHistorySize) {
|
|
66
|
+
this.eventHistory.pop();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.emit(event, eventData);
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Get recent events of a specific type
|
|
74
|
+
getRecentEvents(type = null, limit = 10) {
|
|
75
|
+
let events = this.eventHistory;
|
|
76
|
+
|
|
77
|
+
if (type) {
|
|
78
|
+
events = events.filter(e => e.type === type);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return events.slice(0, limit);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Clear event history
|
|
85
|
+
clearHistory() {
|
|
86
|
+
this.eventHistory = [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Subscribe to multiple events
|
|
90
|
+
onMany(events, handler) {
|
|
91
|
+
for (const event of events) {
|
|
92
|
+
this.on(event, handler);
|
|
93
|
+
}
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// One-time subscription to multiple events
|
|
98
|
+
onceAny(events, handler) {
|
|
99
|
+
const wrappedHandler = (data) => {
|
|
100
|
+
// Remove all listeners after first event
|
|
101
|
+
for (const event of events) {
|
|
102
|
+
this.off(event, wrappedHandler);
|
|
103
|
+
}
|
|
104
|
+
handler(data);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
for (const event of events) {
|
|
108
|
+
this.on(event, wrappedHandler);
|
|
109
|
+
}
|
|
110
|
+
return this;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Wait for a specific event with timeout
|
|
114
|
+
waitFor(event, timeout = 30000) {
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
const timer = setTimeout(() => {
|
|
117
|
+
this.off(event, handler);
|
|
118
|
+
reject(new Error(`Timeout waiting for event: ${event}`));
|
|
119
|
+
}, timeout);
|
|
120
|
+
|
|
121
|
+
const handler = (data) => {
|
|
122
|
+
clearTimeout(timer);
|
|
123
|
+
resolve(data);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
this.once(event, handler);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Singleton instance
|
|
132
|
+
export const agentEvents = new AgentEventEmitter();
|
|
133
|
+
|
|
134
|
+
// Helper to create scoped event emitters
|
|
135
|
+
export function createScopedEmitter(scope) {
|
|
136
|
+
return {
|
|
137
|
+
emit: (event, payload) => {
|
|
138
|
+
agentEvents.emitTyped(`${scope}:${event}`, payload);
|
|
139
|
+
},
|
|
140
|
+
on: (event, handler) => {
|
|
141
|
+
agentEvents.on(`${scope}:${event}`, handler);
|
|
142
|
+
},
|
|
143
|
+
off: (event, handler) => {
|
|
144
|
+
agentEvents.off(`${scope}:${event}`, handler);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|