@ebowwa/osascript 1.1.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 +123 -0
- package/dist/index.js +574 -0
- package/dist/mcp/index.js +866 -0
- package/dist/mcp/stdio.js +1704 -0
- package/package.json +79 -0
- package/src/index.ts +844 -0
- package/src/mcp/events.ts +468 -0
- package/src/mcp/index.ts +565 -0
- package/src/mcp/stdio.ts +1284 -0
- package/src/types.ts +189 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Action Events System for osascript MCP
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - Completion callbacks for AppleScript commands
|
|
6
|
+
* - System event monitoring (keystrokes, apps, windows)
|
|
7
|
+
* - Webhook integration for external automation
|
|
8
|
+
* - MCP lifecycle hooks integration
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { spawn, ChildProcess } from "child_process";
|
|
12
|
+
import { EventEmitter } from "events";
|
|
13
|
+
|
|
14
|
+
// ==============
|
|
15
|
+
// Types
|
|
16
|
+
// ==============
|
|
17
|
+
|
|
18
|
+
export interface ActionEvent {
|
|
19
|
+
id: string;
|
|
20
|
+
type: EventType;
|
|
21
|
+
timestamp: number;
|
|
22
|
+
source: string;
|
|
23
|
+
data: Record<string, any>;
|
|
24
|
+
success: boolean;
|
|
25
|
+
error?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type EventType =
|
|
29
|
+
| "keystroke"
|
|
30
|
+
| "keycode"
|
|
31
|
+
| "app_launch"
|
|
32
|
+
| "app_quit"
|
|
33
|
+
| "app_activate"
|
|
34
|
+
| "window_change"
|
|
35
|
+
| "clipboard_change"
|
|
36
|
+
| "volume_change"
|
|
37
|
+
| "display_change"
|
|
38
|
+
| "script_complete"
|
|
39
|
+
| "script_error"
|
|
40
|
+
| "webhook_sent"
|
|
41
|
+
| "lifecycle_start"
|
|
42
|
+
| "lifecycle_end"
|
|
43
|
+
| "lifecycle_tool_use";
|
|
44
|
+
|
|
45
|
+
export interface EventListener {
|
|
46
|
+
id: string;
|
|
47
|
+
eventType: EventType | "*";
|
|
48
|
+
active: boolean;
|
|
49
|
+
webhookUrl?: string;
|
|
50
|
+
filter?: (event: ActionEvent) => boolean;
|
|
51
|
+
callback?: (event: ActionEvent) => void | Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface WebhookConfig {
|
|
55
|
+
url: string;
|
|
56
|
+
method?: "POST" | "PUT";
|
|
57
|
+
headers?: Record<string, string>;
|
|
58
|
+
timeout?: number;
|
|
59
|
+
retries?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface KeystrokeMonitorConfig {
|
|
63
|
+
captureKeys: boolean;
|
|
64
|
+
captureModifiers: boolean;
|
|
65
|
+
targetApp?: string;
|
|
66
|
+
excludeApps?: string[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ==============
|
|
70
|
+
// Event Bus
|
|
71
|
+
// ==============
|
|
72
|
+
|
|
73
|
+
class ActionEventBus extends EventEmitter {
|
|
74
|
+
private listeners: Map<string, EventListener> = new Map();
|
|
75
|
+
private eventHistory: ActionEvent[] = [];
|
|
76
|
+
private maxHistorySize: number = 1000;
|
|
77
|
+
private monitors: Map<string, ChildProcess> = new Map();
|
|
78
|
+
|
|
79
|
+
emit(event: ActionEvent): boolean {
|
|
80
|
+
// Store in history
|
|
81
|
+
this.eventHistory.push(event);
|
|
82
|
+
if (this.eventHistory.length > this.maxHistorySize) {
|
|
83
|
+
this.eventHistory.shift();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Notify all matching listeners
|
|
87
|
+
for (const [id, listener] of this.listeners) {
|
|
88
|
+
if (!listener.active) continue;
|
|
89
|
+
if (listener.eventType !== "*" && listener.eventType !== event.type) continue;
|
|
90
|
+
if (listener.filter && !listener.filter(event)) continue;
|
|
91
|
+
|
|
92
|
+
// Call local callback
|
|
93
|
+
if (listener.callback) {
|
|
94
|
+
Promise.resolve(listener.callback(event)).catch(console.error);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Send webhook
|
|
98
|
+
if (listener.webhookUrl) {
|
|
99
|
+
this.sendWebhook(listener.webhookUrl, event).catch(console.error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return super.emit(event.type, event);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
addListener(listener: EventListener): void {
|
|
107
|
+
this.listeners.set(listener.id, listener);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
removeListener(id: string): boolean {
|
|
111
|
+
return this.listeners.delete(id);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
getListeners(): EventListener[] {
|
|
115
|
+
return Array.from(this.listeners.values());
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
getHistory(since?: number, type?: EventType): ActionEvent[] {
|
|
119
|
+
let events = this.eventHistory;
|
|
120
|
+
if (since) {
|
|
121
|
+
events = events.filter((e) => e.timestamp >= since);
|
|
122
|
+
}
|
|
123
|
+
if (type) {
|
|
124
|
+
events = events.filter((e) => e.type === type);
|
|
125
|
+
}
|
|
126
|
+
return events;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
clearHistory(): void {
|
|
130
|
+
this.eventHistory = [];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async sendWebhook(url: string, event: ActionEvent, config?: WebhookConfig): Promise<boolean> {
|
|
134
|
+
const retries = config?.retries || 3;
|
|
135
|
+
const timeout = config?.timeout || 5000;
|
|
136
|
+
|
|
137
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
138
|
+
try {
|
|
139
|
+
const controller = new AbortController();
|
|
140
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
141
|
+
|
|
142
|
+
const response = await fetch(url, {
|
|
143
|
+
method: config?.method || "POST",
|
|
144
|
+
headers: {
|
|
145
|
+
"Content-Type": "application/json",
|
|
146
|
+
...config?.headers,
|
|
147
|
+
},
|
|
148
|
+
body: JSON.stringify(event),
|
|
149
|
+
signal: controller.signal,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
clearTimeout(timeoutId);
|
|
153
|
+
|
|
154
|
+
if (response.ok) {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (attempt === retries - 1) {
|
|
159
|
+
console.error(`Webhook failed after ${retries} attempts:`, error);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
addMonitor(id: string, process: ChildProcess): void {
|
|
167
|
+
this.monitors.set(id, process);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
removeMonitor(id: string): void {
|
|
171
|
+
const proc = this.monitors.get(id);
|
|
172
|
+
if (proc) {
|
|
173
|
+
proc.kill();
|
|
174
|
+
this.monitors.delete(id);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
stopAllMonitors(): void {
|
|
179
|
+
for (const [id, proc] of this.monitors) {
|
|
180
|
+
proc.kill();
|
|
181
|
+
}
|
|
182
|
+
this.monitors.clear();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Global event bus instance
|
|
187
|
+
export const eventBus = new ActionEventBus();
|
|
188
|
+
|
|
189
|
+
// ==============
|
|
190
|
+
// Event Generators
|
|
191
|
+
// ==============
|
|
192
|
+
|
|
193
|
+
let eventCounter = 0;
|
|
194
|
+
|
|
195
|
+
function generateEventId(): string {
|
|
196
|
+
return `evt_${Date.now()}_${++eventCounter}`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function createEvent(
|
|
200
|
+
type: EventType,
|
|
201
|
+
source: string,
|
|
202
|
+
data: Record<string, any>,
|
|
203
|
+
success: boolean = true,
|
|
204
|
+
error?: string
|
|
205
|
+
): ActionEvent {
|
|
206
|
+
return {
|
|
207
|
+
id: generateEventId(),
|
|
208
|
+
type,
|
|
209
|
+
timestamp: Date.now(),
|
|
210
|
+
source,
|
|
211
|
+
data,
|
|
212
|
+
success,
|
|
213
|
+
error,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ==============
|
|
218
|
+
// Monitoring Functions
|
|
219
|
+
// ==============
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Start monitoring keystrokes (requires accessibility permissions)
|
|
223
|
+
*/
|
|
224
|
+
export async function startKeystrokeMonitor(config: KeystrokeMonitorConfig = {
|
|
225
|
+
captureKeys: true,
|
|
226
|
+
captureModifiers: true,
|
|
227
|
+
}): Promise<string> {
|
|
228
|
+
const monitorId = `keystroke_${Date.now()}`;
|
|
229
|
+
|
|
230
|
+
// AppleScript to monitor keystrokes via System Events
|
|
231
|
+
const script = `
|
|
232
|
+
tell application "System Events"
|
|
233
|
+
set lastKey to ""
|
|
234
|
+
repeat
|
|
235
|
+
try
|
|
236
|
+
-- This is a polling approach; real keylogging requires more permissions
|
|
237
|
+
-- For MCP purposes, we track when we send keystrokes, not passive monitoring
|
|
238
|
+
delay 0.1
|
|
239
|
+
end try
|
|
240
|
+
end repeat
|
|
241
|
+
end tell
|
|
242
|
+
`;
|
|
243
|
+
|
|
244
|
+
// Note: Passive keystroke monitoring requires accessibility permissions
|
|
245
|
+
// and is restricted by macOS. We'll emit events for our own keystroke actions.
|
|
246
|
+
|
|
247
|
+
return monitorId;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Start monitoring application changes
|
|
252
|
+
*/
|
|
253
|
+
export async function startAppMonitor(): Promise<string> {
|
|
254
|
+
const monitorId = `app_${Date.now()}`;
|
|
255
|
+
|
|
256
|
+
// Create a background process to monitor app changes
|
|
257
|
+
const script = `
|
|
258
|
+
set lastApp to ""
|
|
259
|
+
repeat
|
|
260
|
+
tell application "System Events"
|
|
261
|
+
set currentApp to name of first process whose frontmost is true
|
|
262
|
+
end tell
|
|
263
|
+
if currentApp is not lastApp then
|
|
264
|
+
do shell script "echo " & quoted form of currentApp
|
|
265
|
+
set lastApp to currentApp
|
|
266
|
+
end if
|
|
267
|
+
delay 0.5
|
|
268
|
+
end repeat
|
|
269
|
+
`;
|
|
270
|
+
|
|
271
|
+
const proc = spawn("osascript", ["-e", script]);
|
|
272
|
+
eventBus.addMonitor(monitorId, proc);
|
|
273
|
+
|
|
274
|
+
proc.stdout?.on("data", (data) => {
|
|
275
|
+
const appName = data.toString().trim();
|
|
276
|
+
eventBus.emit(createEvent("app_activate", "app_monitor", { appName }));
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
proc.stderr?.on("data", (data) => {
|
|
280
|
+
console.error(`App monitor error: ${data}`);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
proc.on("close", () => {
|
|
284
|
+
eventBus.removeMonitor(monitorId);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
return monitorId;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Start monitoring clipboard changes
|
|
292
|
+
*/
|
|
293
|
+
export async function startClipboardMonitor(): Promise<string> {
|
|
294
|
+
const monitorId = `clipboard_${Date.now()}`;
|
|
295
|
+
|
|
296
|
+
const script = `
|
|
297
|
+
set lastClipboard to the clipboard as text
|
|
298
|
+
repeat
|
|
299
|
+
delay 0.5
|
|
300
|
+
try
|
|
301
|
+
set currentClipboard to the clipboard as text
|
|
302
|
+
if currentClipboard is not lastClipboard then
|
|
303
|
+
do shell script "echo CLIPBOARD_CHANGED"
|
|
304
|
+
set lastClipboard to currentClipboard
|
|
305
|
+
end if
|
|
306
|
+
end try
|
|
307
|
+
end repeat
|
|
308
|
+
`;
|
|
309
|
+
|
|
310
|
+
const proc = spawn("osascript", ["-e", script]);
|
|
311
|
+
eventBus.addMonitor(monitorId, proc);
|
|
312
|
+
|
|
313
|
+
proc.stdout?.on("data", (data) => {
|
|
314
|
+
if (data.toString().includes("CLIPBOARD_CHANGED")) {
|
|
315
|
+
eventBus.emit(createEvent("clipboard_change", "clipboard_monitor", {}));
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
proc.stderr?.on("data", (data) => {
|
|
320
|
+
console.error(`Clipboard monitor error: ${data}`);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
proc.on("close", () => {
|
|
324
|
+
eventBus.removeMonitor(monitorId);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
return monitorId;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Stop a specific monitor
|
|
332
|
+
*/
|
|
333
|
+
export function stopMonitor(monitorId: string): boolean {
|
|
334
|
+
eventBus.removeMonitor(monitorId);
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Stop all active monitors
|
|
340
|
+
*/
|
|
341
|
+
export function stopAllMonitors(): void {
|
|
342
|
+
eventBus.stopAllMonitors();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ==============
|
|
346
|
+
// Event Listener Management
|
|
347
|
+
// ==============
|
|
348
|
+
|
|
349
|
+
export function registerEventListener(
|
|
350
|
+
eventType: EventType | "*",
|
|
351
|
+
options: {
|
|
352
|
+
webhookUrl?: string;
|
|
353
|
+
callback?: (event: ActionEvent) => void | Promise<void>;
|
|
354
|
+
filter?: (event: ActionEvent) => boolean;
|
|
355
|
+
} = {}
|
|
356
|
+
): string {
|
|
357
|
+
const listenerId = `listener_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
358
|
+
|
|
359
|
+
eventBus.addListener({
|
|
360
|
+
id: listenerId,
|
|
361
|
+
eventType,
|
|
362
|
+
active: true,
|
|
363
|
+
webhookUrl: options.webhookUrl,
|
|
364
|
+
callback: options.callback,
|
|
365
|
+
filter: options.filter,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
return listenerId;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function unregisterEventListener(listenerId: string): boolean {
|
|
372
|
+
return eventBus.removeListener(listenerId);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function listEventListeners(): EventListener[] {
|
|
376
|
+
return eventBus.getListeners();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export function toggleListener(listenerId: string, active: boolean): boolean {
|
|
380
|
+
const listeners = eventBus.getListeners();
|
|
381
|
+
const listener = listeners.find((l) => l.id === listenerId);
|
|
382
|
+
if (listener) {
|
|
383
|
+
listener.active = active;
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ==============
|
|
390
|
+
// Event History
|
|
391
|
+
// ==============
|
|
392
|
+
|
|
393
|
+
export function getEventHistory(since?: number, type?: EventType): ActionEvent[] {
|
|
394
|
+
return eventBus.getHistory(since, type);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function clearEventHistory(): void {
|
|
398
|
+
eventBus.clearHistory();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ==============
|
|
402
|
+
// Webhook Integration
|
|
403
|
+
// ==============
|
|
404
|
+
|
|
405
|
+
export async function sendEventWebhook(
|
|
406
|
+
url: string,
|
|
407
|
+
event: ActionEvent,
|
|
408
|
+
config?: WebhookConfig
|
|
409
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
410
|
+
const success = await eventBus.sendWebhook(url, event, config);
|
|
411
|
+
return { success, error: success ? undefined : "Webhook delivery failed" };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ==============
|
|
415
|
+
// MCP Lifecycle Integration
|
|
416
|
+
// ==============
|
|
417
|
+
|
|
418
|
+
export interface MCPLifecycleEvent {
|
|
419
|
+
sessionId?: string;
|
|
420
|
+
toolName?: string;
|
|
421
|
+
action: "start" | "end" | "tool_use" | "tool_result";
|
|
422
|
+
metadata?: Record<string, any>;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export function emitLifecycleEvent(lifecycleEvent: MCPLifecycleEvent): ActionEvent {
|
|
426
|
+
const eventType: EventType = `lifecycle_${lifecycleEvent.action}` as EventType;
|
|
427
|
+
const event = createEvent(eventType, "mcp_lifecycle", {
|
|
428
|
+
sessionId: lifecycleEvent.sessionId,
|
|
429
|
+
toolName: lifecycleEvent.toolName,
|
|
430
|
+
metadata: lifecycleEvent.metadata,
|
|
431
|
+
});
|
|
432
|
+
eventBus.emit(event);
|
|
433
|
+
return event;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ==============
|
|
437
|
+
// Convenience: Emit event after action
|
|
438
|
+
// ==============
|
|
439
|
+
|
|
440
|
+
export function withEvent<T>(
|
|
441
|
+
type: EventType,
|
|
442
|
+
source: string,
|
|
443
|
+
action: () => T | Promise<T>
|
|
444
|
+
): Promise<{ result: T; event: ActionEvent }> {
|
|
445
|
+
return Promise.resolve(action())
|
|
446
|
+
.then((result) => {
|
|
447
|
+
const event = createEvent(type, source, { result }, true);
|
|
448
|
+
eventBus.emit(event);
|
|
449
|
+
return { result, event };
|
|
450
|
+
})
|
|
451
|
+
.catch((error) => {
|
|
452
|
+
const event = createEvent(type, source, {}, false, error.message);
|
|
453
|
+
eventBus.emit(event);
|
|
454
|
+
throw error;
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ==============
|
|
459
|
+
// Export active monitor count
|
|
460
|
+
// ==============
|
|
461
|
+
|
|
462
|
+
export function getActiveMonitorCount(): number {
|
|
463
|
+
return eventBus["monitors"].size;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export function getActiveListenerCount(): number {
|
|
467
|
+
return eventBus.getListeners().filter((l) => l.active).length;
|
|
468
|
+
}
|