Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions crates/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ members = [
"khive-pack-gtd",
"khive-pack-memory",
"khive-pack-brain",
"khive-pack-comm",
"khive-channel",
"khive-channel-gmail",
"khive-channel-macos-mail",
"khive-channel-macos-imessage",
"khive-mcp",
"khive-vcs",
"kkernel",
Expand Down
22 changes: 22 additions & 0 deletions crates/khive-channel-gmail/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "khive-channel-gmail"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
keywords.workspace = true
categories.workspace = true
description = "Gmail channel handler — reference khive-channel implementation (OAuth + Gmail API)"

[dependencies]
khive-types = { version = "0.2.0", path = "../khive-types", features = ["serde"] }
khive-channel = { version = "0.2.0", path = "../khive-channel" }
async-trait = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
chrono = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
13 changes: 13 additions & 0 deletions crates/khive-channel-gmail/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//! khive-channel-gmail — Gmail reference implementation of [`khive_channel::ChannelHandler`].
//!
//! Demonstrates: OAuth flow, send semantics, fetch/poll/webhook, edge-creation hooks.
//! Reference implementation for community to model future channels on
//! (Slack, Discord, SMS, Telegram, ...).
//!
//! **OSS-quality reference**, NOT the gated version Cloud sells. The Cloud
//! offering will use a separate, formal-gate-wrapped implementation with
//! compliance + audit guarantees on top of the same trait surface.
//!
//! Scaffold only — auth flow, send/fetch implementation, and webhook wiring
//! are deferred (see
//! `.khive/notes/strategy_20260522_open_core_split.md`).
22 changes: 22 additions & 0 deletions crates/khive-channel-macos-imessage/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "khive-channel-macos-imessage"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
keywords.workspace = true
categories.workspace = true
description = "macOS Messages.app / iMessage channel handler — reference khive-channel implementation using AppleScript / Messages framework"

[dependencies]
khive-types = { version = "0.2.0", path = "../khive-types", features = ["serde"] }
khive-channel = { version = "0.2.0", path = "../khive-channel" }
async-trait = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
chrono = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
15 changes: 15 additions & 0 deletions crates/khive-channel-macos-imessage/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//! khive-channel-macos-imessage — macOS Messages.app / iMessage implementation of [`khive_channel::ChannelHandler`].
//!
//! Local-first messaging. Uses the user's existing Messages.app + iMessage
//! credentials, accesses the local `chat.db`, and sends via AppleScript or
//! the Messages framework.
//!
//! Demonstrates: SMS + iMessage send paths, conversation thread mapping to
//! khive entities (contacts), local-only operation (no third-party servers).
//!
//! Scaffold only — chat.db read path, AppleScript send bridge, and
//! delivery-receipt handling are deferred (see
//! `.khive/notes/strategy_20260522_open_core_split.md`).

// TODO(post-MVP): gate macOS-only build with `#[cfg(target_os = "macos")]`
// once the implementation lands, so this crate is buildable on non-Mac CI.
22 changes: 22 additions & 0 deletions crates/khive-channel-macos-mail/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "khive-channel-macos-mail"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
keywords.workspace = true
categories.workspace = true
description = "macOS Mail.app channel handler — reference khive-channel implementation using AppleScript / MailKit"

[dependencies]
khive-types = { version = "0.2.0", path = "../khive-types", features = ["serde"] }
khive-channel = { version = "0.2.0", path = "../khive-channel" }
async-trait = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
chrono = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
184 changes: 184 additions & 0 deletions crates/khive-channel-macos-mail/docs/reference/mail_bridge.applescript
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
-- mail_bridge.applescript
-- Reads INBOX messages from Apple Mail and outputs JSON to stdout.
-- Usage:
-- osascript mail_bridge.applescript
-- osascript mail_bridge.applescript "account@example.com"
-- osascript mail_bridge.applescript "" 1714240000
-- osascript mail_bridge.applescript "account@example.com" 1714240000
--
-- Parameters:
-- argv[1] : account_name (optional; sync all accounts if omitted or empty)
-- argv[2] : since_epoch (optional; Unix timestamp; only messages newer than this)

on escapeJSON(rawText)
-- Escape characters that would break JSON string values.
set t to rawText
-- Backslash must come first.
set t to my replaceText(t, "\\", "\\\\")
set t to my replaceText(t, "\"", "\\\"")
set t to my replaceText(t, "/", "\\/")
set t to my replaceText(t, return, "\\n")
set t to my replaceText(t, linefeed, "\\n")
set t to my replaceText(t, tab, "\\t")
return t
end escapeJSON

on replaceText(theText, searchStr, replacementStr)
set AppleScript's text item delimiters to searchStr
set textItems to text items of theText
set AppleScript's text item delimiters to replacementStr
set result to textItems as string
set AppleScript's text item delimiters to ""
return result
end replaceText

on truncateText(theText, maxLen)
if (length of theText) > maxLen then
return (text 1 thru maxLen of theText)
end if
return theText
end truncateText

on dateToEpoch(d)
-- Returns the Unix epoch for an AppleScript date.
-- AppleScript epoch is 2001-01-01 00:00:00 UTC; Unix epoch is 1970-01-01 00:00:00 UTC.
-- Difference: 978307200 seconds.
return (d - (date "Sunday, January 1, 2001 at 12:00:00 AM")) + 978307200
end dateToEpoch

on run argv
set filterAccount to ""
set sinceEpoch to 0

if (count of argv) >= 1 then
set filterAccount to item 1 of argv
end if
if (count of argv) >= 2 then
try
set sinceEpoch to item 2 of argv as integer
end try
end if

set jsonParts to {}
set msgCount to 0

tell application "Mail"
set allAccounts to every account

repeat with acct in allAccounts
set acctName to name of acct

-- If a specific account was requested, skip others.
if filterAccount is not "" and filterAccount is not acctName then
-- continue (AppleScript has no continue; use conditional)
else
-- Find the INBOX mailbox (case-insensitive: "INBOX" for Gmail, "Inbox" for Outlook)
set inboxMbox to missing value
try
set allMailboxes to every mailbox of acct
repeat with mbox in allMailboxes
set mboxName to name of mbox
if mboxName is "INBOX" or mboxName is "Inbox" then
set inboxMbox to mbox
exit repeat
end if
end repeat
end try

if inboxMbox is not missing value then
try
set allMessages to every message of inboxMbox

repeat with msg in allMessages
try
set msgDate to date received of msg
set epochSecs to my dateToEpoch(msgDate)

-- Apply since_epoch filter if provided.
if sinceEpoch > 0 and epochSecs <= sinceEpoch then
-- skip this message
else
-- Collect fields.
set msgId to message id of msg
if msgId is missing value then set msgId to ""

set msgSubject to subject of msg
if msgSubject is missing value then set msgSubject to ""

set msgIsRead to read status of msg

-- Sender
set senderName to ""
set senderAddr to ""
try
set senderAddr to sender of msg
end try
try
-- "Full Name <addr>" format — extract name portion if present.
if senderAddr contains "<" then
set senderName to text 1 thru ((offset of "<" in senderAddr) - 2) of senderAddr
set addrStart to (offset of "<" in senderAddr) + 1
set addrEnd to (offset of ">" in senderAddr) - 1
set senderAddr to text addrStart thru addrEnd of senderAddr
end if
end try

-- Recipients: build comma-separated list for JSON array.
set toList to {}
try
set toRecips to to recipients of msg
repeat with r in toRecips
try
set rAddr to address of r
set end of toList to "\"" & my escapeJSON(rAddr) & "\""
end try
end repeat
end try
set toJSON to "[" & my joinList(toList, ",") & "]"

-- Body (plain text, truncated).
set msgBody to ""
try
set msgBody to content of msg
set msgBody to my truncateText(msgBody, 8192)
end try

-- Build JSON object.
set isReadStr to "false"
if msgIsRead then set isReadStr to "true"

set jsonObj to "{" & ¬
"\"message_id\":\"" & my escapeJSON(msgId) & "\"," & ¬
"\"date_received\":" & epochSecs & "," & ¬
"\"from_name\":\"" & my escapeJSON(senderName) & "\"," & ¬
"\"from_address\":\"" & my escapeJSON(senderAddr) & "\"," & ¬
"\"to_addresses\":" & toJSON & "," & ¬
"\"subject\":\"" & my escapeJSON(msgSubject) & "\"," & ¬
"\"body_plain\":\"" & my escapeJSON(msgBody) & "\"," & ¬
"\"is_read\":" & isReadStr & "," & ¬
"\"account\":\"" & my escapeJSON(acctName) & "\"," & ¬
"\"mailbox\":\"INBOX\"" & ¬
"}"

set end of jsonParts to jsonObj
set msgCount to msgCount + 1
end if
end try
end repeat
end try
end if
end if
end repeat
end tell

-- Output JSON array.
set jsonOutput to "[" & my joinList(jsonParts, ",") & "]"
return jsonOutput
end run

on joinList(theList, delimiter)
set AppleScript's text item delimiters to delimiter
set result to theList as string
set AppleScript's text item delimiters to ""
return result
end joinList
Loading
Loading