@kononenko-e/email-mcp 0.2.3
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/LICENSE +155 -0
- package/README.md +820 -0
- package/dist/cli/account-commands.d.ts +10 -0
- package/dist/cli/account-commands.d.ts.map +1 -0
- package/dist/cli/account-commands.js +703 -0
- package/dist/cli/account-commands.js.map +1 -0
- package/dist/cli/config-commands.d.ts +9 -0
- package/dist/cli/config-commands.d.ts.map +1 -0
- package/dist/cli/config-commands.js +156 -0
- package/dist/cli/config-commands.js.map +1 -0
- package/dist/cli/guard.d.ts +11 -0
- package/dist/cli/guard.d.ts.map +1 -0
- package/dist/cli/guard.js +18 -0
- package/dist/cli/guard.js.map +1 -0
- package/dist/cli/install-commands.d.ts +12 -0
- package/dist/cli/install-commands.d.ts.map +1 -0
- package/dist/cli/install-commands.js +320 -0
- package/dist/cli/install-commands.js.map +1 -0
- package/dist/cli/notify.d.ts +8 -0
- package/dist/cli/notify.d.ts.map +1 -0
- package/dist/cli/notify.js +143 -0
- package/dist/cli/notify.js.map +1 -0
- package/dist/cli/providers.d.ts +13 -0
- package/dist/cli/providers.d.ts.map +1 -0
- package/dist/cli/providers.js +180 -0
- package/dist/cli/providers.js.map +1 -0
- package/dist/cli/scheduler.d.ts +8 -0
- package/dist/cli/scheduler.d.ts.map +1 -0
- package/dist/cli/scheduler.js +268 -0
- package/dist/cli/scheduler.js.map +1 -0
- package/dist/cli/setup.d.ts +12 -0
- package/dist/cli/setup.d.ts.map +1 -0
- package/dist/cli/setup.js +15 -0
- package/dist/cli/setup.js.map +1 -0
- package/dist/cli/test.d.ts +7 -0
- package/dist/cli/test.d.ts.map +1 -0
- package/dist/cli/test.js +67 -0
- package/dist/cli/test.js.map +1 -0
- package/dist/config/loader.d.ts +36 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +372 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/schema.d.ts +351 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +165 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/xdg.d.ts +27 -0
- package/dist/config/xdg.d.ts.map +1 -0
- package/dist/config/xdg.js +30 -0
- package/dist/config/xdg.js.map +1 -0
- package/dist/connections/manager.d.ts +43 -0
- package/dist/connections/manager.d.ts.map +1 -0
- package/dist/connections/manager.js +272 -0
- package/dist/connections/manager.js.map +1 -0
- package/dist/connections/types.d.ts +13 -0
- package/dist/connections/types.d.ts.map +1 -0
- package/dist/connections/types.js +2 -0
- package/dist/connections/types.js.map +1 -0
- package/dist/logging.d.ts +46 -0
- package/dist/logging.d.ts.map +1 -0
- package/dist/logging.js +63 -0
- package/dist/logging.js.map +1 -0
- package/dist/main.d.ts +14 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +381 -0
- package/dist/main.js.map +1 -0
- package/dist/prompts/actions.prompt.d.ts +9 -0
- package/dist/prompts/actions.prompt.d.ts.map +1 -0
- package/dist/prompts/actions.prompt.js +64 -0
- package/dist/prompts/actions.prompt.js.map +1 -0
- package/dist/prompts/calendar.prompt.d.ts +9 -0
- package/dist/prompts/calendar.prompt.d.ts.map +1 -0
- package/dist/prompts/calendar.prompt.js +55 -0
- package/dist/prompts/calendar.prompt.js.map +1 -0
- package/dist/prompts/cleanup.prompt.d.ts +9 -0
- package/dist/prompts/cleanup.prompt.d.ts.map +1 -0
- package/dist/prompts/cleanup.prompt.js +78 -0
- package/dist/prompts/cleanup.prompt.js.map +1 -0
- package/dist/prompts/compose.prompt.d.ts +8 -0
- package/dist/prompts/compose.prompt.d.ts.map +1 -0
- package/dist/prompts/compose.prompt.js +116 -0
- package/dist/prompts/compose.prompt.js.map +1 -0
- package/dist/prompts/register.d.ts +8 -0
- package/dist/prompts/register.d.ts.map +1 -0
- package/dist/prompts/register.js +20 -0
- package/dist/prompts/register.js.map +1 -0
- package/dist/prompts/thread.prompt.d.ts +9 -0
- package/dist/prompts/thread.prompt.d.ts.map +1 -0
- package/dist/prompts/thread.prompt.js +58 -0
- package/dist/prompts/thread.prompt.js.map +1 -0
- package/dist/prompts/triage.prompt.d.ts +9 -0
- package/dist/prompts/triage.prompt.d.ts.map +1 -0
- package/dist/prompts/triage.prompt.js +64 -0
- package/dist/prompts/triage.prompt.js.map +1 -0
- package/dist/resources/accounts.resource.d.ts +9 -0
- package/dist/resources/accounts.resource.d.ts.map +1 -0
- package/dist/resources/accounts.resource.js +26 -0
- package/dist/resources/accounts.resource.js.map +1 -0
- package/dist/resources/mailboxes.resource.d.ts +10 -0
- package/dist/resources/mailboxes.resource.d.ts.map +1 -0
- package/dist/resources/mailboxes.resource.js +33 -0
- package/dist/resources/mailboxes.resource.js.map +1 -0
- package/dist/resources/register.d.ts +12 -0
- package/dist/resources/register.d.ts.map +1 -0
- package/dist/resources/register.js +20 -0
- package/dist/resources/register.js.map +1 -0
- package/dist/resources/scheduled.resource.d.ts +9 -0
- package/dist/resources/scheduled.resource.d.ts.map +1 -0
- package/dist/resources/scheduled.resource.js +31 -0
- package/dist/resources/scheduled.resource.js.map +1 -0
- package/dist/resources/stats.resource.d.ts +10 -0
- package/dist/resources/stats.resource.d.ts.map +1 -0
- package/dist/resources/stats.resource.js +45 -0
- package/dist/resources/stats.resource.js.map +1 -0
- package/dist/resources/templates.resource.d.ts +9 -0
- package/dist/resources/templates.resource.d.ts.map +1 -0
- package/dist/resources/templates.resource.js +34 -0
- package/dist/resources/templates.resource.js.map +1 -0
- package/dist/resources/unread.resource.d.ts +11 -0
- package/dist/resources/unread.resource.d.ts.map +1 -0
- package/dist/resources/unread.resource.js +46 -0
- package/dist/resources/unread.resource.js.map +1 -0
- package/dist/safety/audit.d.ts +17 -0
- package/dist/safety/audit.d.ts.map +1 -0
- package/dist/safety/audit.js +50 -0
- package/dist/safety/audit.js.map +1 -0
- package/dist/safety/rate-limiter.d.ts +22 -0
- package/dist/safety/rate-limiter.d.ts.map +1 -0
- package/dist/safety/rate-limiter.js +52 -0
- package/dist/safety/rate-limiter.js.map +1 -0
- package/dist/safety/validation.d.ts +44 -0
- package/dist/safety/validation.d.ts.map +1 -0
- package/dist/safety/validation.js +113 -0
- package/dist/safety/validation.js.map +1 -0
- package/dist/server.d.ts +10 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +25 -0
- package/dist/server.js.map +1 -0
- package/dist/services/calendar.service.d.ts +22 -0
- package/dist/services/calendar.service.d.ts.map +1 -0
- package/dist/services/calendar.service.js +115 -0
- package/dist/services/calendar.service.js.map +1 -0
- package/dist/services/event-bus.d.ts +28 -0
- package/dist/services/event-bus.d.ts.map +1 -0
- package/dist/services/event-bus.js +16 -0
- package/dist/services/event-bus.js.map +1 -0
- package/dist/services/hooks.service.d.ts +78 -0
- package/dist/services/hooks.service.d.ts.map +1 -0
- package/dist/services/hooks.service.js +497 -0
- package/dist/services/hooks.service.js.map +1 -0
- package/dist/services/imap.service.d.ts +133 -0
- package/dist/services/imap.service.d.ts.map +1 -0
- package/dist/services/imap.service.js +1393 -0
- package/dist/services/imap.service.js.map +1 -0
- package/dist/services/label-strategy.d.ts +20 -0
- package/dist/services/label-strategy.d.ts.map +1 -0
- package/dist/services/label-strategy.js +237 -0
- package/dist/services/label-strategy.js.map +1 -0
- package/dist/services/local-calendar.service.d.ts +126 -0
- package/dist/services/local-calendar.service.d.ts.map +1 -0
- package/dist/services/local-calendar.service.js +428 -0
- package/dist/services/local-calendar.service.js.map +1 -0
- package/dist/services/notifier.service.d.ts +66 -0
- package/dist/services/notifier.service.d.ts.map +1 -0
- package/dist/services/notifier.service.js +316 -0
- package/dist/services/notifier.service.js.map +1 -0
- package/dist/services/oauth.service.d.ts +47 -0
- package/dist/services/oauth.service.d.ts.map +1 -0
- package/dist/services/oauth.service.js +140 -0
- package/dist/services/oauth.service.js.map +1 -0
- package/dist/services/presets.d.ts +36 -0
- package/dist/services/presets.d.ts.map +1 -0
- package/dist/services/presets.js +173 -0
- package/dist/services/presets.js.map +1 -0
- package/dist/services/reminders.service.d.ts +63 -0
- package/dist/services/reminders.service.d.ts.map +1 -0
- package/dist/services/reminders.service.js +281 -0
- package/dist/services/reminders.service.js.map +1 -0
- package/dist/services/scheduler.service.d.ts +42 -0
- package/dist/services/scheduler.service.d.ts.map +1 -0
- package/dist/services/scheduler.service.js +260 -0
- package/dist/services/scheduler.service.js.map +1 -0
- package/dist/services/smtp.service.d.ts +40 -0
- package/dist/services/smtp.service.d.ts.map +1 -0
- package/dist/services/smtp.service.js +151 -0
- package/dist/services/smtp.service.js.map +1 -0
- package/dist/services/template.service.d.ts +33 -0
- package/dist/services/template.service.d.ts.map +1 -0
- package/dist/services/template.service.js +123 -0
- package/dist/services/template.service.js.map +1 -0
- package/dist/services/watcher.service.d.ts +37 -0
- package/dist/services/watcher.service.d.ts.map +1 -0
- package/dist/services/watcher.service.js +244 -0
- package/dist/services/watcher.service.js.map +1 -0
- package/dist/tools/account-config.tool.d.ts +14 -0
- package/dist/tools/account-config.tool.d.ts.map +1 -0
- package/dist/tools/account-config.tool.js +245 -0
- package/dist/tools/account-config.tool.js.map +1 -0
- package/dist/tools/accounts.tool.d.ts +7 -0
- package/dist/tools/accounts.tool.d.ts.map +1 -0
- package/dist/tools/accounts.tool.js +29 -0
- package/dist/tools/accounts.tool.js.map +1 -0
- package/dist/tools/analytics.tool.d.ts +9 -0
- package/dist/tools/analytics.tool.d.ts.map +1 -0
- package/dist/tools/analytics.tool.js +27 -0
- package/dist/tools/analytics.tool.js.map +1 -0
- package/dist/tools/attachments.tool.d.ts +7 -0
- package/dist/tools/attachments.tool.d.ts.map +1 -0
- package/dist/tools/attachments.tool.js +45 -0
- package/dist/tools/attachments.tool.js.map +1 -0
- package/dist/tools/bulk.tool.d.ts +7 -0
- package/dist/tools/bulk.tool.d.ts.map +1 -0
- package/dist/tools/bulk.tool.js +75 -0
- package/dist/tools/bulk.tool.js.map +1 -0
- package/dist/tools/calendar.tool.d.ts +19 -0
- package/dist/tools/calendar.tool.d.ts.map +1 -0
- package/dist/tools/calendar.tool.js +538 -0
- package/dist/tools/calendar.tool.js.map +1 -0
- package/dist/tools/contacts.tool.d.ts +7 -0
- package/dist/tools/contacts.tool.d.ts.map +1 -0
- package/dist/tools/contacts.tool.js +44 -0
- package/dist/tools/contacts.tool.js.map +1 -0
- package/dist/tools/drafts.tool.d.ts +8 -0
- package/dist/tools/drafts.tool.d.ts.map +1 -0
- package/dist/tools/drafts.tool.js +92 -0
- package/dist/tools/drafts.tool.js.map +1 -0
- package/dist/tools/emails.tool.d.ts +7 -0
- package/dist/tools/emails.tool.d.ts.map +1 -0
- package/dist/tools/emails.tool.js +400 -0
- package/dist/tools/emails.tool.js.map +1 -0
- package/dist/tools/folders.tool.d.ts +7 -0
- package/dist/tools/folders.tool.d.ts.map +1 -0
- package/dist/tools/folders.tool.js +111 -0
- package/dist/tools/folders.tool.js.map +1 -0
- package/dist/tools/health.tool.d.ts +10 -0
- package/dist/tools/health.tool.d.ts.map +1 -0
- package/dist/tools/health.tool.js +78 -0
- package/dist/tools/health.tool.js.map +1 -0
- package/dist/tools/label.tool.d.ts +11 -0
- package/dist/tools/label.tool.d.ts.map +1 -0
- package/dist/tools/label.tool.js +165 -0
- package/dist/tools/label.tool.js.map +1 -0
- package/dist/tools/locate.tool.d.ts +11 -0
- package/dist/tools/locate.tool.d.ts.map +1 -0
- package/dist/tools/locate.tool.js +59 -0
- package/dist/tools/locate.tool.js.map +1 -0
- package/dist/tools/mailboxes.tool.d.ts +7 -0
- package/dist/tools/mailboxes.tool.d.ts.map +1 -0
- package/dist/tools/mailboxes.tool.js +38 -0
- package/dist/tools/mailboxes.tool.js.map +1 -0
- package/dist/tools/manage.tool.d.ts +7 -0
- package/dist/tools/manage.tool.d.ts.map +1 -0
- package/dist/tools/manage.tool.js +125 -0
- package/dist/tools/manage.tool.js.map +1 -0
- package/dist/tools/register.d.ts +21 -0
- package/dist/tools/register.d.ts.map +1 -0
- package/dist/tools/register.js +55 -0
- package/dist/tools/register.js.map +1 -0
- package/dist/tools/scheduler.tool.d.ts +9 -0
- package/dist/tools/scheduler.tool.d.ts.map +1 -0
- package/dist/tools/scheduler.tool.js +104 -0
- package/dist/tools/scheduler.tool.js.map +1 -0
- package/dist/tools/send.tool.d.ts +7 -0
- package/dist/tools/send.tool.d.ts.map +1 -0
- package/dist/tools/send.tool.js +123 -0
- package/dist/tools/send.tool.js.map +1 -0
- package/dist/tools/templates.tool.d.ts +12 -0
- package/dist/tools/templates.tool.d.ts.map +1 -0
- package/dist/tools/templates.tool.js +140 -0
- package/dist/tools/templates.tool.js.map +1 -0
- package/dist/tools/thread.tool.d.ts +10 -0
- package/dist/tools/thread.tool.d.ts.map +1 -0
- package/dist/tools/thread.tool.js +146 -0
- package/dist/tools/thread.tool.js.map +1 -0
- package/dist/tools/watcher.tool.d.ts +9 -0
- package/dist/tools/watcher.tool.d.ts.map +1 -0
- package/dist/tools/watcher.tool.js +282 -0
- package/dist/tools/watcher.tool.js.map +1 -0
- package/dist/types/index.d.ts +271 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/calendar-notes.d.ts +31 -0
- package/dist/utils/calendar-notes.d.ts.map +1 -0
- package/dist/utils/calendar-notes.js +99 -0
- package/dist/utils/calendar-notes.js.map +1 -0
- package/dist/utils/calendar-state.d.ts +27 -0
- package/dist/utils/calendar-state.d.ts.map +1 -0
- package/dist/utils/calendar-state.js +85 -0
- package/dist/utils/calendar-state.js.map +1 -0
- package/dist/utils/conference-details.d.ts +12 -0
- package/dist/utils/conference-details.d.ts.map +1 -0
- package/dist/utils/conference-details.js +71 -0
- package/dist/utils/conference-details.js.map +1 -0
- package/dist/utils/meeting-url.d.ts +10 -0
- package/dist/utils/meeting-url.d.ts.map +1 -0
- package/dist/utils/meeting-url.js +30 -0
- package/dist/utils/meeting-url.js.map +1 -0
- package/package.json +103 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed event bus for internal email events.
|
|
3
|
+
*
|
|
4
|
+
* Decouples the IMAP IDLE watcher from MCP notification hooks.
|
|
5
|
+
* Uses Node's built-in EventEmitter with typed event maps.
|
|
6
|
+
*/
|
|
7
|
+
import { EventEmitter } from 'node:events';
|
|
8
|
+
import type { EmailMeta } from '../types/index.js';
|
|
9
|
+
export interface NewEmailEvent {
|
|
10
|
+
account: string;
|
|
11
|
+
mailbox: string;
|
|
12
|
+
emails: EmailMeta[];
|
|
13
|
+
}
|
|
14
|
+
export interface ExpungeEvent {
|
|
15
|
+
account: string;
|
|
16
|
+
mailbox: string;
|
|
17
|
+
count: number;
|
|
18
|
+
}
|
|
19
|
+
interface EmailEventMap {
|
|
20
|
+
'email:new': [NewEmailEvent];
|
|
21
|
+
'email:expunge': [ExpungeEvent];
|
|
22
|
+
}
|
|
23
|
+
export declare class EmailEventBus extends EventEmitter<EmailEventMap> {
|
|
24
|
+
}
|
|
25
|
+
/** Singleton event bus shared across the application. */
|
|
26
|
+
declare const eventBus: EmailEventBus;
|
|
27
|
+
export default eventBus;
|
|
28
|
+
//# sourceMappingURL=event-bus.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-bus.d.ts","sourceRoot":"","sources":["../../src/services/event-bus.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAMnD,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,SAAS,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAMD,UAAU,aAAa;IACrB,WAAW,EAAE,CAAC,aAAa,CAAC,CAAC;IAC7B,eAAe,EAAE,CAAC,YAAY,CAAC,CAAC;CACjC;AAMD,qBAAa,aAAc,SAAQ,YAAY,CAAC,aAAa,CAAC;CAAG;AAEjE,yDAAyD;AACzD,QAAA,MAAM,QAAQ,eAAsB,CAAC;AACrC,eAAe,QAAQ,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed event bus for internal email events.
|
|
3
|
+
*
|
|
4
|
+
* Decouples the IMAP IDLE watcher from MCP notification hooks.
|
|
5
|
+
* Uses Node's built-in EventEmitter with typed event maps.
|
|
6
|
+
*/
|
|
7
|
+
import { EventEmitter } from 'node:events';
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Typed EventBus
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
export class EmailEventBus extends EventEmitter {
|
|
12
|
+
}
|
|
13
|
+
/** Singleton event bus shared across the application. */
|
|
14
|
+
const eventBus = new EmailEventBus();
|
|
15
|
+
export default eventBus;
|
|
16
|
+
//# sourceMappingURL=event-bus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-bus.js","sourceRoot":"","sources":["../../src/services/event-bus.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA4B3C,8EAA8E;AAC9E,iBAAiB;AACjB,8EAA8E;AAE9E,MAAM,OAAO,aAAc,SAAQ,YAA2B;CAAG;AAEjE,yDAAyD;AACzD,MAAM,QAAQ,GAAG,IAAI,aAAa,EAAE,CAAC;AACrC,eAAe,QAAQ,CAAC"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Hooks Service — intelligent email triage via MCP sampling.
|
|
3
|
+
*
|
|
4
|
+
* Listens for new email events on the event bus and:
|
|
5
|
+
* - Matches static rules FIRST (fast, free, deterministic)
|
|
6
|
+
* - Falls through to AI triage via `sampling/createMessage` if no rule matched
|
|
7
|
+
* - Uses preset system prompts + custom instructions for AI triage
|
|
8
|
+
* - Auto-applies labels and flags based on AI response
|
|
9
|
+
* - Falls back to logging if sampling is unavailable
|
|
10
|
+
*/
|
|
11
|
+
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
12
|
+
import type { EmailMeta, HookRule, HooksConfig } from '../types/index.js';
|
|
13
|
+
import type ImapService from './imap.service.js';
|
|
14
|
+
import NotifierService from './notifier.service.js';
|
|
15
|
+
interface TriageResult {
|
|
16
|
+
priority?: 'urgent' | 'high' | 'normal' | 'low';
|
|
17
|
+
labels?: string[];
|
|
18
|
+
flag?: boolean;
|
|
19
|
+
action?: string;
|
|
20
|
+
addToCalendar?: boolean;
|
|
21
|
+
}
|
|
22
|
+
interface BatchEmail {
|
|
23
|
+
account: string;
|
|
24
|
+
mailbox: string;
|
|
25
|
+
meta: EmailMeta;
|
|
26
|
+
}
|
|
27
|
+
interface RuleMatchResult {
|
|
28
|
+
matched: true;
|
|
29
|
+
rule: HookRule;
|
|
30
|
+
}
|
|
31
|
+
interface RuleNoMatch {
|
|
32
|
+
matched: false;
|
|
33
|
+
}
|
|
34
|
+
type StaticMatchOutcome = RuleMatchResult | RuleNoMatch;
|
|
35
|
+
export default class HooksService {
|
|
36
|
+
private config;
|
|
37
|
+
private imapService;
|
|
38
|
+
private lowLevelServer;
|
|
39
|
+
private samplingSupported;
|
|
40
|
+
private pendingEmails;
|
|
41
|
+
private batchTimer;
|
|
42
|
+
private rateCounter;
|
|
43
|
+
private rateResetTimer;
|
|
44
|
+
private started;
|
|
45
|
+
private readonly resolvedSystemPrompt;
|
|
46
|
+
private readonly notifier;
|
|
47
|
+
private readonly localCalendar;
|
|
48
|
+
private static readonly MAX_SAMPLING_PER_MIN;
|
|
49
|
+
constructor(config: HooksConfig, imapService: ImapService);
|
|
50
|
+
/** Returns the NotifierService instance for direct tool access. */
|
|
51
|
+
getNotifier(): NotifierService;
|
|
52
|
+
/** Returns the current hooks configuration. */
|
|
53
|
+
getHooksConfig(): HooksConfig;
|
|
54
|
+
/**
|
|
55
|
+
* Start listening for email events.
|
|
56
|
+
* Call after MCP server is connected so we can access the low-level server.
|
|
57
|
+
*/
|
|
58
|
+
start(lowLevelServer: Server, clientCapabilities: {
|
|
59
|
+
sampling?: boolean;
|
|
60
|
+
}): void;
|
|
61
|
+
stop(): void;
|
|
62
|
+
private onNewEmail;
|
|
63
|
+
private flushBatch;
|
|
64
|
+
static matchStaticRules(email: BatchEmail, rules: HookRule[]): StaticMatchOutcome;
|
|
65
|
+
private static emailMatchesRule;
|
|
66
|
+
private applyStaticRule;
|
|
67
|
+
private sendResourceUpdates;
|
|
68
|
+
private notifyBatch;
|
|
69
|
+
private triageBatch;
|
|
70
|
+
private static formatEmailSummary;
|
|
71
|
+
private applyTriageResults;
|
|
72
|
+
private applySingleTriage;
|
|
73
|
+
private applyCalendarAction;
|
|
74
|
+
static parseTriageResponse(text: string, expectedCount: number): TriageResult[];
|
|
75
|
+
static sanitizeTriageResult(raw: unknown): TriageResult;
|
|
76
|
+
}
|
|
77
|
+
export {};
|
|
78
|
+
//# sourceMappingURL=hooks.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks.service.d.ts","sourceRoot":"","sources":["../../src/services/hooks.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAExE,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAG1E,OAAO,KAAK,WAAW,MAAM,mBAAmB,CAAC;AAGjD,OAAO,eAAe,MAAM,uBAAuB,CAAC;AAQpD,UAAU,YAAY;IACpB,QAAQ,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;IAChD,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,UAAU,UAAU;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,SAAS,CAAC;CACjB;AAED,UAAU,eAAe;IACvB,OAAO,EAAE,IAAI,CAAC;IACd,IAAI,EAAE,QAAQ,CAAC;CAChB;AAED,UAAU,WAAW;IACnB,OAAO,EAAE,KAAK,CAAC;CAChB;AAED,KAAK,kBAAkB,GAAG,eAAe,GAAG,WAAW,CAAC;AAgCxD,MAAM,CAAC,OAAO,OAAO,YAAY;IAC/B,OAAO,CAAC,MAAM,CAAc;IAE5B,OAAO,CAAC,WAAW,CAAc;IAEjC,OAAO,CAAC,cAAc,CAAuB;IAE7C,OAAO,CAAC,iBAAiB,CAAS;IAElC,OAAO,CAAC,aAAa,CAAoB;IAEzC,OAAO,CAAC,UAAU,CAA8C;IAEhE,OAAO,CAAC,WAAW,CAAK;IAExB,OAAO,CAAC,cAAc,CAA+C;IAErE,OAAO,CAAC,OAAO,CAAS;IAExB,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAS;IAE9C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkB;IAE3C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAuB;IAErD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,oBAAoB,CAAM;gBAEtC,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW;IAWzD,mEAAmE;IACnE,WAAW,IAAI,eAAe;IAI9B,+CAA+C;IAC/C,cAAc,IAAI,WAAW;IAI7B;;;OAGG;IACH,KAAK,CAAC,cAAc,EAAE,MAAM,EAAE,kBAAkB,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI;IAmC/E,IAAI,IAAI,IAAI;IAmBZ,OAAO,CAAC,UAAU;YAaJ,UAAU;IAyCxB,MAAM,CAAC,gBAAgB,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,kBAAkB;IAKjF,OAAO,CAAC,MAAM,CAAC,gBAAgB;YA0BjB,eAAe;YAuEf,mBAAmB;YAiBnB,WAAW;YAiBX,WAAW;IA4CzB,OAAO,CAAC,MAAM,CAAC,kBAAkB;YAkBnB,kBAAkB;YAKlB,iBAAiB;YA+DjB,mBAAmB;IAuGjC,MAAM,CAAC,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,YAAY,EAAE;IAiB/E,MAAM,CAAC,oBAAoB,CAAC,GAAG,EAAE,OAAO,GAAG,YAAY;CAexD"}
|
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Hooks Service — intelligent email triage via MCP sampling.
|
|
3
|
+
*
|
|
4
|
+
* Listens for new email events on the event bus and:
|
|
5
|
+
* - Matches static rules FIRST (fast, free, deterministic)
|
|
6
|
+
* - Falls through to AI triage via `sampling/createMessage` if no rule matched
|
|
7
|
+
* - Uses preset system prompts + custom instructions for AI triage
|
|
8
|
+
* - Auto-applies labels and flags based on AI response
|
|
9
|
+
* - Falls back to logging if sampling is unavailable
|
|
10
|
+
*/
|
|
11
|
+
import { mcpLog } from '../logging.js';
|
|
12
|
+
import eventBus from './event-bus.js';
|
|
13
|
+
import LocalCalendarService from './local-calendar.service.js';
|
|
14
|
+
import NotifierService from './notifier.service.js';
|
|
15
|
+
import { buildSystemPrompt } from './presets.js';
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Pattern matching helpers
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
/** Convert a glob-like pattern (with `*` wildcards and `|` OR) to a RegExp. */
|
|
20
|
+
function globToRegex(pattern) {
|
|
21
|
+
const parts = pattern
|
|
22
|
+
.split('|')
|
|
23
|
+
.map((p) => p.trim())
|
|
24
|
+
.filter(Boolean);
|
|
25
|
+
const regexParts = parts.map((part) => {
|
|
26
|
+
const escaped = part.replace(/[.+?^${}()[\]\\]/g, '\\$&');
|
|
27
|
+
return escaped.replace(/\*/g, '.*');
|
|
28
|
+
});
|
|
29
|
+
return new RegExp(`^(?:${regexParts.join('|')})$`, 'i');
|
|
30
|
+
}
|
|
31
|
+
/** Test whether a value matches a glob pattern (case-insensitive). */
|
|
32
|
+
function matchesPattern(pattern, value) {
|
|
33
|
+
try {
|
|
34
|
+
return globToRegex(pattern).test(value);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// HooksService
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
export default class HooksService {
|
|
44
|
+
config;
|
|
45
|
+
imapService;
|
|
46
|
+
lowLevelServer = null;
|
|
47
|
+
samplingSupported = false;
|
|
48
|
+
pendingEmails = [];
|
|
49
|
+
batchTimer = null;
|
|
50
|
+
rateCounter = 0;
|
|
51
|
+
rateResetTimer = null;
|
|
52
|
+
started = false;
|
|
53
|
+
resolvedSystemPrompt;
|
|
54
|
+
notifier;
|
|
55
|
+
localCalendar;
|
|
56
|
+
static MAX_SAMPLING_PER_MIN = 10;
|
|
57
|
+
constructor(config, imapService) {
|
|
58
|
+
this.config = config;
|
|
59
|
+
this.imapService = imapService;
|
|
60
|
+
this.notifier = new NotifierService(config.alerts);
|
|
61
|
+
this.localCalendar = new LocalCalendarService();
|
|
62
|
+
this.resolvedSystemPrompt = buildSystemPrompt(config.preset, {
|
|
63
|
+
customInstructions: config.customInstructions,
|
|
64
|
+
systemPrompt: config.systemPrompt,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/** Returns the NotifierService instance for direct tool access. */
|
|
68
|
+
getNotifier() {
|
|
69
|
+
return this.notifier;
|
|
70
|
+
}
|
|
71
|
+
/** Returns the current hooks configuration. */
|
|
72
|
+
getHooksConfig() {
|
|
73
|
+
return this.config;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Start listening for email events.
|
|
77
|
+
* Call after MCP server is connected so we can access the low-level server.
|
|
78
|
+
*/
|
|
79
|
+
start(lowLevelServer, clientCapabilities) {
|
|
80
|
+
this.lowLevelServer = lowLevelServer;
|
|
81
|
+
this.samplingSupported = clientCapabilities.sampling === true;
|
|
82
|
+
if (this.started) {
|
|
83
|
+
// Client reconnected — server reference updated above, no need to re-register listeners.
|
|
84
|
+
mcpLog('info', 'hooks', `Hooks reconnected: sampling=${this.samplingSupported ? 'yes' : 'no'}`).catch(() => { });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
this.started = true;
|
|
88
|
+
if (this.config.onNewEmail === 'none')
|
|
89
|
+
return;
|
|
90
|
+
eventBus.on('email:new', (event) => {
|
|
91
|
+
this.onNewEmail(event);
|
|
92
|
+
});
|
|
93
|
+
// Rate limit reset every 60s
|
|
94
|
+
this.rateResetTimer = setInterval(() => {
|
|
95
|
+
this.rateCounter = 0;
|
|
96
|
+
}, 60_000);
|
|
97
|
+
const ruleCount = this.config.rules.length;
|
|
98
|
+
mcpLog('info', 'hooks', `Hooks active: mode=${this.config.onNewEmail}, preset=${this.config.preset}, ` +
|
|
99
|
+
`rules=${ruleCount}, sampling=${this.samplingSupported ? 'yes' : 'no'}`).catch(() => { });
|
|
100
|
+
}
|
|
101
|
+
stop() {
|
|
102
|
+
this.started = false;
|
|
103
|
+
if (this.batchTimer) {
|
|
104
|
+
clearTimeout(this.batchTimer);
|
|
105
|
+
this.batchTimer = null;
|
|
106
|
+
}
|
|
107
|
+
this.pendingEmails = [];
|
|
108
|
+
if (this.rateResetTimer) {
|
|
109
|
+
clearInterval(this.rateResetTimer);
|
|
110
|
+
this.rateResetTimer = null;
|
|
111
|
+
}
|
|
112
|
+
this.notifier.stop();
|
|
113
|
+
eventBus.removeAllListeners('email:new');
|
|
114
|
+
}
|
|
115
|
+
// -------------------------------------------------------------------------
|
|
116
|
+
// Event handling + batching
|
|
117
|
+
// -------------------------------------------------------------------------
|
|
118
|
+
onNewEmail(event) {
|
|
119
|
+
const items = event.emails.map((meta) => ({
|
|
120
|
+
account: event.account,
|
|
121
|
+
mailbox: event.mailbox,
|
|
122
|
+
meta,
|
|
123
|
+
}));
|
|
124
|
+
this.pendingEmails.push(...items);
|
|
125
|
+
this.batchTimer ??= setTimeout(() => {
|
|
126
|
+
this.flushBatch().catch(() => { });
|
|
127
|
+
}, this.config.batchDelay * 1000);
|
|
128
|
+
}
|
|
129
|
+
async flushBatch() {
|
|
130
|
+
this.batchTimer = null;
|
|
131
|
+
const batch = [...this.pendingEmails];
|
|
132
|
+
this.pendingEmails = [];
|
|
133
|
+
if (batch.length === 0)
|
|
134
|
+
return;
|
|
135
|
+
await this.sendResourceUpdates(batch);
|
|
136
|
+
// Partition: static-rule-matched vs needs-AI-triage
|
|
137
|
+
const ruleMatched = [];
|
|
138
|
+
const needsTriage = [];
|
|
139
|
+
batch.forEach((email) => {
|
|
140
|
+
const outcome = HooksService.matchStaticRules(email, this.config.rules);
|
|
141
|
+
if (outcome.matched) {
|
|
142
|
+
ruleMatched.push({ email, rule: outcome.rule });
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
needsTriage.push(email);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
// Apply static rule actions
|
|
149
|
+
if (ruleMatched.length > 0) {
|
|
150
|
+
const ruleOps = ruleMatched.map(async ({ email, rule }) => this.applyStaticRule(email, rule));
|
|
151
|
+
await Promise.allSettled(ruleOps);
|
|
152
|
+
}
|
|
153
|
+
// AI triage for remaining emails
|
|
154
|
+
if (needsTriage.length > 0) {
|
|
155
|
+
if (this.config.onNewEmail === 'triage' && this.samplingSupported) {
|
|
156
|
+
await this.triageBatch(needsTriage);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
await this.notifyBatch(needsTriage);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// -------------------------------------------------------------------------
|
|
164
|
+
// Static rule matching
|
|
165
|
+
// -------------------------------------------------------------------------
|
|
166
|
+
static matchStaticRules(email, rules) {
|
|
167
|
+
const matched = rules.find((rule) => HooksService.emailMatchesRule(email, rule));
|
|
168
|
+
return matched ? { matched: true, rule: matched } : { matched: false };
|
|
169
|
+
}
|
|
170
|
+
static emailMatchesRule(email, rule) {
|
|
171
|
+
const { match } = rule;
|
|
172
|
+
const fromAddr = email.meta.from.address;
|
|
173
|
+
const fromFull = email.meta.from.name ? `${email.meta.from.name} <${fromAddr}>` : fromAddr;
|
|
174
|
+
const toAddrs = email.meta.to.map((t) => t.address).join(', ');
|
|
175
|
+
const { subject } = email.meta;
|
|
176
|
+
// All specified match conditions must pass (AND logic)
|
|
177
|
+
if (match.from &&
|
|
178
|
+
!matchesPattern(match.from, fromAddr) &&
|
|
179
|
+
!matchesPattern(match.from, fromFull)) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
if (match.to && !matchesPattern(match.to, toAddrs)) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
if (match.subject && !matchesPattern(match.subject, subject)) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
// At least one match condition must be specified
|
|
189
|
+
return Boolean(match.from ?? match.to ?? match.subject);
|
|
190
|
+
}
|
|
191
|
+
async applyStaticRule(email, rule) {
|
|
192
|
+
const { actions } = rule;
|
|
193
|
+
// Apply labels
|
|
194
|
+
if (actions.labels?.length) {
|
|
195
|
+
const labelOps = actions.labels.map(async (label) => {
|
|
196
|
+
try {
|
|
197
|
+
await this.imapService.addLabel(email.account, email.meta.id, email.mailbox, label);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
await mcpLog('warning', 'hooks', `Could not add label "${label}" to email ${email.meta.id}`);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
await Promise.allSettled(labelOps);
|
|
204
|
+
}
|
|
205
|
+
// Apply flag
|
|
206
|
+
if (actions.flag) {
|
|
207
|
+
try {
|
|
208
|
+
await this.imapService.setFlags(email.account, email.mailbox, email.meta.id, 'flag');
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
await mcpLog('warning', 'hooks', `Could not flag email ${email.meta.id}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Mark read
|
|
215
|
+
if (actions.markRead) {
|
|
216
|
+
try {
|
|
217
|
+
await this.imapService.setFlags(email.account, email.mailbox, email.meta.id, 'read');
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
await mcpLog('warning', 'hooks', `Could not mark email ${email.meta.id} as read`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Send alert via notifier (rule with alert=true forces desktop notification)
|
|
224
|
+
const payload = {
|
|
225
|
+
account: email.account,
|
|
226
|
+
sender: email.meta.from,
|
|
227
|
+
subject: email.meta.subject,
|
|
228
|
+
priority: actions.flag ? 'high' : 'normal',
|
|
229
|
+
labels: actions.labels,
|
|
230
|
+
ruleName: rule.name,
|
|
231
|
+
};
|
|
232
|
+
await this.notifier.alert(payload, actions.alert === true);
|
|
233
|
+
// Add to calendar if rule requests it or global auto_calendar is on
|
|
234
|
+
if (actions.addToCalendar ?? this.config.autoCalendar) {
|
|
235
|
+
const { isCalendarProcessed, markCalendarProcessed } = await import('../utils/calendar-state.js');
|
|
236
|
+
const already = await isCalendarProcessed(email.account, email.meta.id);
|
|
237
|
+
if (!already) {
|
|
238
|
+
await this.applyCalendarAction(email);
|
|
239
|
+
await markCalendarProcessed(email.account, email.meta.id, 'event', email.meta.subject);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
await mcpLog('info', 'hooks', `Calendar: skipping auto-add for ${email.meta.id} (already processed once)`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// -------------------------------------------------------------------------
|
|
247
|
+
// Resource subscription notifications
|
|
248
|
+
// -------------------------------------------------------------------------
|
|
249
|
+
async sendResourceUpdates(emails) {
|
|
250
|
+
if (!this.lowLevelServer)
|
|
251
|
+
return;
|
|
252
|
+
const accounts = [...new Set(emails.map((e) => e.account))];
|
|
253
|
+
const srv = this.lowLevelServer;
|
|
254
|
+
const ops = accounts.flatMap((account) => [
|
|
255
|
+
srv.sendResourceUpdated({ uri: `email://${account}/unread` }).catch(() => { }),
|
|
256
|
+
srv.sendResourceUpdated({ uri: `email://${account}/mailboxes` }).catch(() => { }),
|
|
257
|
+
]);
|
|
258
|
+
await Promise.allSettled(ops);
|
|
259
|
+
}
|
|
260
|
+
// -------------------------------------------------------------------------
|
|
261
|
+
// Notify mode (alerts-aware fallback)
|
|
262
|
+
// -------------------------------------------------------------------------
|
|
263
|
+
async notifyBatch(emails) {
|
|
264
|
+
const ops = emails.map(async (e) => {
|
|
265
|
+
const payload = {
|
|
266
|
+
account: e.account,
|
|
267
|
+
sender: e.meta.from,
|
|
268
|
+
subject: e.meta.subject,
|
|
269
|
+
priority: 'normal',
|
|
270
|
+
};
|
|
271
|
+
return this.notifier.alert(payload);
|
|
272
|
+
});
|
|
273
|
+
await Promise.allSettled(ops);
|
|
274
|
+
}
|
|
275
|
+
// -------------------------------------------------------------------------
|
|
276
|
+
// Triage mode (AI sampling with preset prompts)
|
|
277
|
+
// -------------------------------------------------------------------------
|
|
278
|
+
async triageBatch(emails) {
|
|
279
|
+
if (this.rateCounter >= HooksService.MAX_SAMPLING_PER_MIN) {
|
|
280
|
+
await mcpLog('warning', 'hooks', 'Sampling rate limit reached — falling back to notify');
|
|
281
|
+
await this.notifyBatch(emails);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
// Skip AI for notification-only preset
|
|
285
|
+
if (this.config.preset === 'notification-only' || !this.resolvedSystemPrompt) {
|
|
286
|
+
await this.notifyBatch(emails);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
this.rateCounter += 1;
|
|
290
|
+
const emailSummaries = emails.map((e, i) => HooksService.formatEmailSummary(e, i)).join('\n\n');
|
|
291
|
+
const userPrompt = `Analyze these ${emails.length} new email(s):\n\n${emailSummaries}`;
|
|
292
|
+
try {
|
|
293
|
+
const srv = this.lowLevelServer;
|
|
294
|
+
if (!srv)
|
|
295
|
+
throw new Error('Server not available');
|
|
296
|
+
const result = await srv.createMessage({
|
|
297
|
+
messages: [{ role: 'user', content: { type: 'text', text: userPrompt } }],
|
|
298
|
+
systemPrompt: this.resolvedSystemPrompt,
|
|
299
|
+
maxTokens: 1000,
|
|
300
|
+
modelPreferences: {
|
|
301
|
+
hints: [{ name: 'fast' }],
|
|
302
|
+
speedPriority: 0.8,
|
|
303
|
+
intelligencePriority: 0.5,
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
const text = result.model && result.content?.type === 'text' ? result.content.text : '';
|
|
307
|
+
const triageResults = HooksService.parseTriageResponse(text, emails.length);
|
|
308
|
+
await this.applyTriageResults(emails, triageResults);
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
312
|
+
await mcpLog('warning', 'hooks', `Sampling failed: ${errMsg} — falling back to notify`);
|
|
313
|
+
await this.notifyBatch(emails);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
static formatEmailSummary(e, index) {
|
|
317
|
+
const flagIcons = [
|
|
318
|
+
e.meta.flagged ? '⭐' : '',
|
|
319
|
+
e.meta.seen ? '👁️' : '🆕',
|
|
320
|
+
e.meta.hasAttachments ? '📎' : '',
|
|
321
|
+
].join('');
|
|
322
|
+
return (`[${index + 1}] From: ${e.meta.from.name ?? e.meta.from.address}\n` +
|
|
323
|
+
` Subject: ${e.meta.subject}\n` +
|
|
324
|
+
` Date: ${e.meta.date}\n` +
|
|
325
|
+
` Flags: ${flagIcons}`);
|
|
326
|
+
}
|
|
327
|
+
// -------------------------------------------------------------------------
|
|
328
|
+
// Triage application
|
|
329
|
+
// -------------------------------------------------------------------------
|
|
330
|
+
async applyTriageResults(emails, results) {
|
|
331
|
+
const ops = emails.map(async (email, i) => this.applySingleTriage(email, results[i] ?? {}));
|
|
332
|
+
await Promise.allSettled(ops);
|
|
333
|
+
}
|
|
334
|
+
async applySingleTriage(email, triage) {
|
|
335
|
+
// Auto-label
|
|
336
|
+
if (this.config.autoLabel && triage.labels?.length) {
|
|
337
|
+
const labelOps = triage.labels.map(async (label) => {
|
|
338
|
+
try {
|
|
339
|
+
await this.imapService.addLabel(email.account, email.meta.id, email.mailbox, label);
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
await mcpLog('warning', 'hooks', `Could not add label "${label}" to email ${email.meta.id}`);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
await Promise.allSettled(labelOps);
|
|
346
|
+
}
|
|
347
|
+
// Auto-flag
|
|
348
|
+
if (this.config.autoFlag && triage.flag) {
|
|
349
|
+
try {
|
|
350
|
+
await this.imapService.setFlags(email.account, email.mailbox, email.meta.id, 'flag');
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
await mcpLog('warning', 'hooks', `Could not flag email ${email.meta.id}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// Route through notifier for urgency-based alerts
|
|
357
|
+
const priority = triage.priority ?? 'normal';
|
|
358
|
+
const payload = {
|
|
359
|
+
account: email.account,
|
|
360
|
+
sender: email.meta.from,
|
|
361
|
+
subject: email.meta.subject,
|
|
362
|
+
priority,
|
|
363
|
+
labels: triage.labels,
|
|
364
|
+
};
|
|
365
|
+
await this.notifier.alert(payload);
|
|
366
|
+
if (triage.action) {
|
|
367
|
+
await mcpLog('info', 'hooks', ` Action: ${triage.action}`);
|
|
368
|
+
}
|
|
369
|
+
// Add to calendar if AI triage requested it or global auto_calendar is on
|
|
370
|
+
if (triage.addToCalendar ?? this.config.autoCalendar) {
|
|
371
|
+
const { isCalendarProcessed, markCalendarProcessed } = await import('../utils/calendar-state.js');
|
|
372
|
+
const already = await isCalendarProcessed(email.account, email.meta.id);
|
|
373
|
+
if (!already) {
|
|
374
|
+
await this.applyCalendarAction(email);
|
|
375
|
+
await markCalendarProcessed(email.account, email.meta.id, 'event', email.meta.subject);
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
await mcpLog('info', 'hooks', `Calendar: skipping auto-add for ${email.meta.id} (already processed once — instruct AI to add again)`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// -------------------------------------------------------------------------
|
|
383
|
+
// Calendar auto-add helper
|
|
384
|
+
// -------------------------------------------------------------------------
|
|
385
|
+
async applyCalendarAction(email) {
|
|
386
|
+
const { buildCalendarNotes } = await import('../utils/calendar-notes.js');
|
|
387
|
+
const { extractConferenceDetails } = await import('../utils/conference-details.js');
|
|
388
|
+
const { extractMeetingUrl } = await import('../utils/meeting-url.js');
|
|
389
|
+
const { CALENDAR_ATTACHMENTS_DIR } = await import('../config/xdg.js');
|
|
390
|
+
const path = await import('node:path');
|
|
391
|
+
try {
|
|
392
|
+
const full = await this.imapService.getEmail(email.account, email.meta.id, email.mailbox);
|
|
393
|
+
const bodyText = full.bodyText ?? '';
|
|
394
|
+
const bodyHtml = full.bodyHtml ?? '';
|
|
395
|
+
const combined = `${bodyText}\n${bodyHtml}`;
|
|
396
|
+
// Try ICS extraction
|
|
397
|
+
let start = new Date(full.date);
|
|
398
|
+
let end = new Date(start.getTime() + 60 * 60 * 1000);
|
|
399
|
+
let location;
|
|
400
|
+
let icsUid;
|
|
401
|
+
try {
|
|
402
|
+
const { default: CalSvc } = await import('./calendar.service.js');
|
|
403
|
+
const calSvc = new CalSvc();
|
|
404
|
+
const icsContents = await this.imapService.getCalendarParts(email.account, email.mailbox, email.meta.id);
|
|
405
|
+
if (icsContents.length > 0) {
|
|
406
|
+
const events = calSvc.extractFromParts(icsContents);
|
|
407
|
+
if (events.length > 0) {
|
|
408
|
+
start = new Date(events[0].start);
|
|
409
|
+
end = new Date(events[0].end);
|
|
410
|
+
location = events[0].location;
|
|
411
|
+
icsUid = events[0].uid;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
// ICS extraction is best-effort
|
|
417
|
+
}
|
|
418
|
+
const meetingUrl = extractMeetingUrl(combined);
|
|
419
|
+
const conference = extractConferenceDetails(bodyText !== '' ? bodyText : bodyHtml);
|
|
420
|
+
// Save attachments
|
|
421
|
+
let savedAttachments = [];
|
|
422
|
+
if (full.attachments.length > 0) {
|
|
423
|
+
const destDir = path.join(CALENDAR_ATTACHMENTS_DIR, `${email.account}-${email.meta.id}`.replace(/[^a-zA-Z0-9-_]/g, '_'));
|
|
424
|
+
savedAttachments = await this.imapService.saveEmailAttachments(email.account, email.meta.id, email.mailbox, destDir);
|
|
425
|
+
}
|
|
426
|
+
const notes = buildCalendarNotes({
|
|
427
|
+
emailFrom: full.from.name ? `${full.from.name} <${full.from.address}>` : full.from.address,
|
|
428
|
+
emailSubject: full.subject,
|
|
429
|
+
emailDate: new Date(full.date).toLocaleString(),
|
|
430
|
+
meetingUrl: meetingUrl?.url,
|
|
431
|
+
meetingUrlLabel: meetingUrl?.label,
|
|
432
|
+
dialIn: conference?.dialIn,
|
|
433
|
+
meetingId: conference?.meetingId,
|
|
434
|
+
passcode: conference?.passcode,
|
|
435
|
+
conferenceProvider: conference?.provider,
|
|
436
|
+
bodyExcerpt: bodyText !== '' ? bodyText : bodyHtml,
|
|
437
|
+
savedAttachments,
|
|
438
|
+
});
|
|
439
|
+
const result = await this.localCalendar.addEvent({
|
|
440
|
+
title: full.subject,
|
|
441
|
+
start,
|
|
442
|
+
end,
|
|
443
|
+
location,
|
|
444
|
+
notes,
|
|
445
|
+
url: meetingUrl?.url,
|
|
446
|
+
urlLabel: meetingUrl?.label,
|
|
447
|
+
alarmMinutes: this.config.calendarAlarmMinutes ?? 15,
|
|
448
|
+
savedAttachments,
|
|
449
|
+
dialIn: conference?.dialIn,
|
|
450
|
+
meetingId: conference?.meetingId,
|
|
451
|
+
passcode: conference?.passcode,
|
|
452
|
+
icsUid,
|
|
453
|
+
}, this.config.calendarName !== '' ? this.config.calendarName : undefined, { confirm: this.config.calendarConfirm !== false });
|
|
454
|
+
await mcpLog('info', 'hooks', `Calendar: ${result.message}`);
|
|
455
|
+
}
|
|
456
|
+
catch (err) {
|
|
457
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
458
|
+
await mcpLog('warning', 'hooks', `Calendar auto-add failed: ${msg}`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
// -------------------------------------------------------------------------
|
|
462
|
+
// Response parsing
|
|
463
|
+
// -------------------------------------------------------------------------
|
|
464
|
+
static parseTriageResponse(text, expectedCount) {
|
|
465
|
+
try {
|
|
466
|
+
const cleaned = text.replace(/```(?:json)?\n?/g, '').trim();
|
|
467
|
+
const parsed = JSON.parse(cleaned);
|
|
468
|
+
if (Array.isArray(parsed)) {
|
|
469
|
+
return parsed.slice(0, expectedCount).map(HooksService.sanitizeTriageResult);
|
|
470
|
+
}
|
|
471
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
472
|
+
return [HooksService.sanitizeTriageResult(parsed)];
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
// Parse failure — return empty results
|
|
477
|
+
}
|
|
478
|
+
return Array.from({ length: expectedCount }, () => ({}));
|
|
479
|
+
}
|
|
480
|
+
static sanitizeTriageResult(raw) {
|
|
481
|
+
if (typeof raw !== 'object' || raw === null)
|
|
482
|
+
return {};
|
|
483
|
+
const obj = raw;
|
|
484
|
+
return {
|
|
485
|
+
priority: ['urgent', 'high', 'normal', 'low'].includes(obj.priority)
|
|
486
|
+
? obj.priority
|
|
487
|
+
: undefined,
|
|
488
|
+
labels: Array.isArray(obj.labels)
|
|
489
|
+
? obj.labels.filter((l) => typeof l === 'string').slice(0, 5)
|
|
490
|
+
: undefined,
|
|
491
|
+
flag: typeof obj.flag === 'boolean' ? obj.flag : undefined,
|
|
492
|
+
action: typeof obj.action === 'string' ? obj.action.slice(0, 200) : undefined,
|
|
493
|
+
addToCalendar: typeof obj.add_to_calendar === 'boolean' ? obj.add_to_calendar : undefined,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
//# sourceMappingURL=hooks.service.js.map
|