chasm_cli/integrations/
hooks.rs

1// Copyright (c) 2024-2026 Nervosys LLC
2// SPDX-License-Identifier: Apache-2.0
3//! Hook System
4//!
5//! Hooks allow agents to respond to events and automate workflows.
6//! They connect triggers (events) to actions (integrations).
7
8#![allow(dead_code)]
9
10use super::{Capability, IntegrationResult};
11use chrono::{DateTime, Utc};
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14
15/// A hook connects a trigger to one or more actions
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct Hook {
18    /// Unique hook identifier
19    pub id: String,
20    /// Human-readable name
21    pub name: String,
22    /// Description of what this hook does
23    pub description: Option<String>,
24    /// Whether the hook is enabled
25    pub enabled: bool,
26    /// Trigger that activates this hook
27    pub trigger: HookTrigger,
28    /// Conditions that must be met
29    pub conditions: Vec<HookCondition>,
30    /// Actions to execute when triggered
31    pub actions: Vec<HookAction>,
32    /// Hook configuration
33    pub config: HookConfig,
34    /// Creation timestamp
35    pub created_at: DateTime<Utc>,
36    /// Last execution timestamp
37    pub last_run: Option<DateTime<Utc>>,
38    /// Run count
39    pub run_count: u64,
40}
41
42/// Trigger types that can activate a hook
43#[derive(Debug, Clone, Serialize, Deserialize)]
44#[serde(tag = "type", rename_all = "snake_case")]
45pub enum HookTrigger {
46    // =========================================================================
47    // Time-based Triggers
48    // =========================================================================
49    /// Cron schedule (e.g., "0 9 * * *" for 9 AM daily)
50    Schedule {
51        cron: String,
52        timezone: Option<String>,
53    },
54    /// Interval (e.g., every 30 minutes)
55    Interval { seconds: u64 },
56    /// Specific datetime
57    DateTime { at: DateTime<Utc> },
58    /// Recurring at specific times
59    Daily { times: Vec<String> }, // ["09:00", "17:00"]
60
61    // =========================================================================
62    // Event-based Triggers
63    // =========================================================================
64    /// Webhook received
65    Webhook {
66        path: String,
67        method: Option<String>,
68    },
69    /// File system change
70    FileChange {
71        path: String,
72        events: Vec<FileEvent>,
73        recursive: bool,
74    },
75    /// Email received
76    EmailReceived {
77        account: String,
78        filters: Option<EmailFilters>,
79    },
80    /// Calendar event
81    CalendarEvent {
82        calendar_id: String,
83        event_type: CalendarEventType,
84        minutes_before: Option<i32>,
85    },
86    /// Message received (Slack, Discord, etc.)
87    MessageReceived {
88        platform: String,
89        channel: Option<String>,
90        from: Option<String>,
91        contains: Option<String>,
92    },
93    /// Git event
94    GitEvent {
95        repo: String,
96        events: Vec<GitEventType>,
97    },
98    /// System event
99    SystemEvent { event: SystemEventType },
100    /// Smart home device event
101    DeviceEvent {
102        device_id: String,
103        state_change: Option<String>,
104    },
105    /// Location-based trigger
106    Location {
107        latitude: f64,
108        longitude: f64,
109        radius_meters: u32,
110        on_enter: bool,
111        on_exit: bool,
112    },
113    /// Manual trigger (user-initiated)
114    Manual,
115    /// Chain trigger (activated by another hook)
116    Chain { hook_id: String },
117    /// Agent request
118    AgentRequest { agent_name: Option<String> },
119}
120
121/// File system events
122#[derive(Debug, Clone, Serialize, Deserialize)]
123#[serde(rename_all = "snake_case")]
124pub enum FileEvent {
125    Created,
126    Modified,
127    Deleted,
128    Renamed,
129    Any,
130}
131
132/// Email filters for trigger
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct EmailFilters {
135    pub from: Option<String>,
136    pub subject_contains: Option<String>,
137    pub has_attachment: Option<bool>,
138    pub labels: Option<Vec<String>>,
139}
140
141/// Calendar event types
142#[derive(Debug, Clone, Serialize, Deserialize)]
143#[serde(rename_all = "snake_case")]
144pub enum CalendarEventType {
145    Created,
146    Updated,
147    Deleted,
148    Starting,
149    Ended,
150    Reminder,
151}
152
153/// Git event types
154#[derive(Debug, Clone, Serialize, Deserialize)]
155#[serde(rename_all = "snake_case")]
156pub enum GitEventType {
157    Push,
158    Pull,
159    Commit,
160    Branch,
161    Tag,
162    PullRequest,
163    Issue,
164    Release,
165}
166
167/// System event types
168#[derive(Debug, Clone, Serialize, Deserialize)]
169#[serde(rename_all = "snake_case")]
170pub enum SystemEventType {
171    Startup,
172    Shutdown,
173    Sleep,
174    Wake,
175    NetworkChange,
176    BatteryLow,
177    StorageLow,
178    AppLaunched { app: String },
179    AppClosed { app: String },
180    ClipboardChange,
181    ScreenLock,
182    ScreenUnlock,
183}
184
185/// Condition for hook execution
186#[derive(Debug, Clone, Serialize, Deserialize)]
187#[serde(tag = "type", rename_all = "snake_case")]
188pub enum HookCondition {
189    /// Time window
190    TimeWindow {
191        start: String,
192        end: String,
193        days: Option<Vec<String>>,
194    },
195    /// Expression evaluation
196    Expression { expr: String },
197    /// Previous action result
198    PreviousResult { action_index: usize, success: bool },
199    /// Environment variable
200    EnvVar {
201        name: String,
202        value: Option<String>,
203        exists: Option<bool>,
204    },
205    /// System state
206    SystemState {
207        state: String,
208        value: serde_json::Value,
209    },
210    /// Rate limit
211    RateLimit { max_runs: u32, period_seconds: u64 },
212    /// Cooldown period
213    Cooldown { seconds: u64 },
214}
215
216/// Action to execute when hook triggers
217#[derive(Debug, Clone, Serialize, Deserialize)]
218#[serde(tag = "type", rename_all = "snake_case")]
219pub enum HookAction {
220    // =========================================================================
221    // Productivity Actions
222    // =========================================================================
223    /// Create calendar event
224    CreateCalendarEvent {
225        calendar_id: String,
226        title: String,
227        start: String,
228        end: String,
229        description: Option<String>,
230        attendees: Option<Vec<String>>,
231        location: Option<String>,
232    },
233    /// Send email
234    SendEmail {
235        account: String,
236        to: Vec<String>,
237        subject: String,
238        body: String,
239        attachments: Option<Vec<String>>,
240    },
241    /// Create note
242    CreateNote {
243        app: String, // "obsidian", "notion", "apple_notes"
244        title: String,
245        content: String,
246        folder: Option<String>,
247        tags: Option<Vec<String>>,
248    },
249    /// Create task
250    CreateTask {
251        app: String, // "todoist", "things", "reminders"
252        title: String,
253        description: Option<String>,
254        due_date: Option<String>,
255        priority: Option<u8>,
256        project: Option<String>,
257    },
258
259    // =========================================================================
260    // Communication Actions
261    // =========================================================================
262    /// Send Slack message
263    SlackMessage {
264        channel: String,
265        text: String,
266        thread_ts: Option<String>,
267    },
268    /// Send Discord message
269    DiscordMessage { channel_id: String, content: String },
270    /// Send Teams message
271    TeamsMessage { channel: String, content: String },
272    /// Send Telegram message
273    TelegramMessage { chat_id: String, text: String },
274    /// Send SMS
275    SendSms { to: String, message: String },
276    /// Send notification
277    Notification {
278        title: String,
279        body: String,
280        sound: Option<String>,
281        actions: Option<Vec<String>>,
282    },
283
284    // =========================================================================
285    // Browser/Automation Actions
286    // =========================================================================
287    /// Open URL
288    OpenUrl {
289        url: String,
290        browser: Option<String>,
291    },
292    /// Run browser automation
293    BrowserAutomation { script: String, headless: bool },
294    /// Scrape webpage
295    ScrapeWebpage {
296        url: String,
297        selectors: HashMap<String, String>,
298    },
299    /// Fill form
300    FillForm {
301        url: String,
302        fields: HashMap<String, String>,
303        submit: bool,
304    },
305
306    // =========================================================================
307    // Development Actions
308    // =========================================================================
309    /// Run shell command
310    RunCommand {
311        command: String,
312        args: Vec<String>,
313        cwd: Option<String>,
314        env: Option<HashMap<String, String>>,
315    },
316    /// Git operation
317    GitOperation {
318        repo: String,
319        operation: String, // "commit", "push", "pull", "branch"
320        args: HashMap<String, String>,
321    },
322    /// Create GitHub issue
323    CreateGitHubIssue {
324        repo: String,
325        title: String,
326        body: String,
327        labels: Option<Vec<String>>,
328    },
329    /// Run Docker command
330    DockerCommand {
331        command: String,
332        container: Option<String>,
333        args: Vec<String>,
334    },
335
336    // =========================================================================
337    // Smart Home Actions
338    // =========================================================================
339    /// Control device
340    ControlDevice {
341        device_id: String,
342        action: String,
343        parameters: Option<HashMap<String, serde_json::Value>>,
344    },
345    /// Set scene
346    SetScene { scene_id: String },
347    /// Home Assistant service call
348    HomeAssistantService {
349        domain: String,
350        service: String,
351        data: Option<serde_json::Value>,
352    },
353
354    // =========================================================================
355    // File Actions
356    // =========================================================================
357    /// Copy file
358    CopyFile { source: String, destination: String },
359    /// Move file
360    MoveFile { source: String, destination: String },
361    /// Create file
362    CreateFile { path: String, content: String },
363    /// Delete file
364    DeleteFile { path: String },
365    /// Sync folder
366    SyncFolder {
367        source: String,
368        destination: String,
369        delete_extra: bool,
370    },
371
372    // =========================================================================
373    // Data Actions
374    // =========================================================================
375    /// HTTP request
376    HttpRequest {
377        method: String,
378        url: String,
379        headers: Option<HashMap<String, String>>,
380        body: Option<String>,
381    },
382    /// Store data
383    StoreData {
384        key: String,
385        value: serde_json::Value,
386        ttl_seconds: Option<u64>,
387    },
388    /// Query database
389    QueryDatabase {
390        connection: String,
391        query: String,
392        params: Option<Vec<serde_json::Value>>,
393    },
394
395    // =========================================================================
396    // AI/Agent Actions
397    // =========================================================================
398    /// Run agent
399    RunAgent {
400        agent_name: String,
401        input: String,
402        context: Option<HashMap<String, serde_json::Value>>,
403    },
404    /// Summarize content
405    Summarize {
406        content: String,
407        max_length: Option<u32>,
408    },
409    /// Classify content
410    Classify {
411        content: String,
412        categories: Vec<String>,
413    },
414    /// Extract data
415    ExtractData {
416        content: String,
417        schema: serde_json::Value,
418    },
419
420    // =========================================================================
421    // Meta Actions
422    // =========================================================================
423    /// Chain to another hook
424    ChainHook {
425        hook_id: String,
426        data: Option<serde_json::Value>,
427    },
428    /// Conditional branch
429    Conditional {
430        condition: String,
431        if_true: Box<HookAction>,
432        if_false: Option<Box<HookAction>>,
433    },
434    /// Parallel execution
435    Parallel { actions: Vec<HookAction> },
436    /// Delay
437    Delay { seconds: u64 },
438    /// Log message
439    Log { level: String, message: String },
440}
441
442/// Hook configuration
443#[derive(Debug, Clone, Serialize, Deserialize)]
444pub struct HookConfig {
445    /// Maximum retries on failure
446    pub max_retries: u32,
447    /// Retry delay in seconds
448    pub retry_delay_seconds: u64,
449    /// Timeout for each action
450    pub timeout_seconds: u64,
451    /// Whether to continue on action failure
452    pub continue_on_error: bool,
453    /// Whether to run actions in parallel
454    pub parallel: bool,
455    /// Tags for organization
456    pub tags: Vec<String>,
457    /// Priority (higher = more important)
458    pub priority: u8,
459}
460
461impl Default for HookConfig {
462    fn default() -> Self {
463        Self {
464            max_retries: 3,
465            retry_delay_seconds: 5,
466            timeout_seconds: 300,
467            continue_on_error: false,
468            parallel: false,
469            tags: vec![],
470            priority: 50,
471        }
472    }
473}
474
475/// Result of hook execution
476#[derive(Debug, Clone, Serialize, Deserialize)]
477pub struct HookResult {
478    pub hook_id: String,
479    pub success: bool,
480    pub started_at: DateTime<Utc>,
481    pub completed_at: DateTime<Utc>,
482    pub action_results: Vec<ActionResult>,
483    pub error: Option<String>,
484}
485
486/// Result of a single action
487#[derive(Debug, Clone, Serialize, Deserialize)]
488pub struct ActionResult {
489    pub action_index: usize,
490    pub action_type: String,
491    pub success: bool,
492    pub output: Option<serde_json::Value>,
493    pub error: Option<String>,
494    pub duration_ms: u64,
495}
496
497/// Hook builder for ergonomic construction
498pub struct HookBuilder {
499    hook: Hook,
500}
501
502impl HookBuilder {
503    pub fn new(name: impl Into<String>) -> Self {
504        Self {
505            hook: Hook {
506                id: uuid::Uuid::new_v4().to_string(),
507                name: name.into(),
508                description: None,
509                enabled: true,
510                trigger: HookTrigger::Manual,
511                conditions: vec![],
512                actions: vec![],
513                config: HookConfig::default(),
514                created_at: Utc::now(),
515                last_run: None,
516                run_count: 0,
517            },
518        }
519    }
520
521    pub fn description(mut self, desc: impl Into<String>) -> Self {
522        self.hook.description = Some(desc.into());
523        self
524    }
525
526    pub fn trigger(mut self, trigger: HookTrigger) -> Self {
527        self.hook.trigger = trigger;
528        self
529    }
530
531    pub fn condition(mut self, condition: HookCondition) -> Self {
532        self.hook.conditions.push(condition);
533        self
534    }
535
536    pub fn action(mut self, action: HookAction) -> Self {
537        self.hook.actions.push(action);
538        self
539    }
540
541    pub fn actions(mut self, actions: Vec<HookAction>) -> Self {
542        self.hook.actions = actions;
543        self
544    }
545
546    pub fn config(mut self, config: HookConfig) -> Self {
547        self.hook.config = config;
548        self
549    }
550
551    pub fn tags(mut self, tags: Vec<String>) -> Self {
552        self.hook.config.tags = tags;
553        self
554    }
555
556    pub fn priority(mut self, priority: u8) -> Self {
557        self.hook.config.priority = priority;
558        self
559    }
560
561    pub fn build(self) -> Hook {
562        self.hook
563    }
564}
565
566// ============================================================================
567// Preset Hooks - Common automations
568// ============================================================================
569
570pub mod presets {
571    use super::*;
572
573    /// Morning briefing hook
574    pub fn morning_briefing() -> Hook {
575        HookBuilder::new("Morning Briefing")
576            .description("Daily morning summary of calendar, emails, and tasks")
577            .trigger(HookTrigger::Daily {
578                times: vec!["07:30".to_string()],
579            })
580            .action(HookAction::RunAgent {
581                agent_name: "assistant".to_string(),
582                input:
583                    "Give me a morning briefing: today's calendar, important emails, and top tasks"
584                        .to_string(),
585                context: None,
586            })
587            .action(HookAction::Notification {
588                title: "Good Morning!".to_string(),
589                body: "Your daily briefing is ready".to_string(),
590                sound: Some("default".to_string()),
591                actions: None,
592            })
593            .tags(vec!["daily".to_string(), "productivity".to_string()])
594            .build()
595    }
596
597    /// Meeting prep hook
598    pub fn meeting_prep() -> Hook {
599        HookBuilder::new("Meeting Prep")
600            .description("Prepare summary 10 minutes before meetings")
601            .trigger(HookTrigger::CalendarEvent {
602                calendar_id: "primary".to_string(),
603                event_type: CalendarEventType::Starting,
604                minutes_before: Some(10),
605            })
606            .action(HookAction::RunAgent {
607                agent_name: "researcher".to_string(),
608                input: "Research the attendees and prepare talking points for this meeting"
609                    .to_string(),
610                context: None,
611            })
612            .action(HookAction::Notification {
613                title: "Meeting in 10 minutes".to_string(),
614                body: "Prep notes ready".to_string(),
615                sound: None,
616                actions: None,
617            })
618            .build()
619    }
620
621    /// Auto-respond to emails
622    pub fn email_auto_respond() -> Hook {
623        HookBuilder::new("Email Auto-Respond")
624            .description("Draft responses to emails matching criteria")
625            .trigger(HookTrigger::EmailReceived {
626                account: "primary".to_string(),
627                filters: Some(EmailFilters {
628                    from: None,
629                    subject_contains: None,
630                    has_attachment: None,
631                    labels: Some(vec!["needs-response".to_string()]),
632                }),
633            })
634            .action(HookAction::RunAgent {
635                agent_name: "writer".to_string(),
636                input: "Draft a professional response to this email".to_string(),
637                context: None,
638            })
639            .build()
640    }
641
642    /// Code review reminder
643    pub fn code_review_reminder() -> Hook {
644        HookBuilder::new("Code Review Reminder")
645            .description("Remind about pending code reviews")
646            .trigger(HookTrigger::Schedule {
647                cron: "0 10,15 * * 1-5".to_string(),
648                timezone: None,
649            })
650            .action(HookAction::HttpRequest {
651                method: "GET".to_string(),
652                url: "https://api.github.com/user/repos".to_string(),
653                headers: None,
654                body: None,
655            })
656            .action(HookAction::Notification {
657                title: "Code Reviews".to_string(),
658                body: "You have pending reviews".to_string(),
659                sound: None,
660                actions: Some(vec!["Open GitHub".to_string()]),
661            })
662            .build()
663    }
664
665    /// Smart home goodnight routine
666    pub fn goodnight_routine() -> Hook {
667        HookBuilder::new("Goodnight Routine")
668            .description("Turn off lights and set thermostat at bedtime")
669            .trigger(HookTrigger::Daily {
670                times: vec!["23:00".to_string()],
671            })
672            .condition(HookCondition::TimeWindow {
673                start: "22:00".to_string(),
674                end: "01:00".to_string(),
675                days: None,
676            })
677            .action(HookAction::SetScene {
678                scene_id: "goodnight".to_string(),
679            })
680            .action(HookAction::ControlDevice {
681                device_id: "thermostat".to_string(),
682                action: "set_temperature".to_string(),
683                parameters: Some(
684                    [("temperature".to_string(), serde_json::json!(68))]
685                        .into_iter()
686                        .collect(),
687                ),
688            })
689            .build()
690    }
691
692    /// Backup important files
693    pub fn daily_backup() -> Hook {
694        HookBuilder::new("Daily Backup")
695            .description("Backup important directories daily")
696            .trigger(HookTrigger::Schedule {
697                cron: "0 2 * * *".to_string(),
698                timezone: None,
699            })
700            .action(HookAction::SyncFolder {
701                source: "~/Documents".to_string(),
702                destination: "~/Backups/Documents".to_string(),
703                delete_extra: false,
704            })
705            .action(HookAction::SyncFolder {
706                source: "~/Projects".to_string(),
707                destination: "~/Backups/Projects".to_string(),
708                delete_extra: false,
709            })
710            .action(HookAction::Log {
711                level: "info".to_string(),
712                message: "Daily backup completed".to_string(),
713            })
714            .build()
715    }
716
717    /// Focus mode
718    pub fn focus_mode() -> Hook {
719        HookBuilder::new("Focus Mode")
720            .description("Block distractions and notify status")
721            .trigger(HookTrigger::Manual)
722            .action(HookAction::SlackMessage {
723                channel: "#status".to_string(),
724                text: "[Focus] In focus mode - will respond later".to_string(),
725                thread_ts: None,
726            })
727            .action(HookAction::RunCommand {
728                command: "osascript".to_string(),
729                args: vec![
730                    "-e".to_string(),
731                    "tell application \"System Events\" to set do not disturb to true".to_string(),
732                ],
733                cwd: None,
734                env: None,
735            })
736            .build()
737    }
738
739    /// Expense tracking
740    pub fn expense_tracker() -> Hook {
741        HookBuilder::new("Expense Tracker")
742            .description("Log expenses from receipts")
743            .trigger(HookTrigger::EmailReceived {
744                account: "primary".to_string(),
745                filters: Some(EmailFilters {
746                    from: None,
747                    subject_contains: Some("receipt".to_string()),
748                    has_attachment: Some(true),
749                    labels: None,
750                }),
751            })
752            .action(HookAction::ExtractData {
753                content: "${email.body}".to_string(),
754                schema: serde_json::json!({
755                    "vendor": "string",
756                    "amount": "number",
757                    "date": "date",
758                    "category": "string"
759                }),
760            })
761            .build()
762    }
763
764    /// Get all preset hooks
765    pub fn all() -> Vec<Hook> {
766        vec![
767            morning_briefing(),
768            meeting_prep(),
769            email_auto_respond(),
770            code_review_reminder(),
771            goodnight_routine(),
772            daily_backup(),
773            focus_mode(),
774            expense_tracker(),
775        ]
776    }
777}