@vox-ai-app/integrations 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +125 -0
  2. package/package.json +42 -0
  3. package/src/imessage/def.js +41 -0
  4. package/src/imessage/index.js +9 -0
  5. package/src/imessage/mac/data.js +144 -0
  6. package/src/imessage/mac/reply.js +68 -0
  7. package/src/imessage/mac/service.js +141 -0
  8. package/src/imessage/tools.js +44 -0
  9. package/src/index.js +7 -0
  10. package/src/mail/def.js +317 -0
  11. package/src/mail/index.js +165 -0
  12. package/src/mail/manage/index.js +10 -0
  13. package/src/mail/manage/mac/index.js +275 -0
  14. package/src/mail/read/index.js +1 -0
  15. package/src/mail/read/mac/accounts.js +53 -0
  16. package/src/mail/read/mac/index.js +170 -0
  17. package/src/mail/read/mac/permission.js +29 -0
  18. package/src/mail/read/mac/sync.js +98 -0
  19. package/src/mail/read/mac/transform.js +55 -0
  20. package/src/mail/send/index.js +1 -0
  21. package/src/mail/send/mac/index.js +93 -0
  22. package/src/mail/shared/index.js +6 -0
  23. package/src/mail/shared/mac/index.js +48 -0
  24. package/src/mail/tools.js +41 -0
  25. package/src/screen/capture/index.js +1 -0
  26. package/src/screen/capture/mac/index.js +109 -0
  27. package/src/screen/control/index.js +15 -0
  28. package/src/screen/control/mac/accessibility.js +25 -0
  29. package/src/screen/control/mac/apps.js +62 -0
  30. package/src/screen/control/mac/exec.js +66 -0
  31. package/src/screen/control/mac/helpers.js +5 -0
  32. package/src/screen/control/mac/index.js +10 -0
  33. package/src/screen/control/mac/keyboard.js +34 -0
  34. package/src/screen/control/mac/keycodes.js +87 -0
  35. package/src/screen/control/mac/mouse.js +59 -0
  36. package/src/screen/control/mac/python-keyboard.js +66 -0
  37. package/src/screen/control/mac/python-mouse.js +66 -0
  38. package/src/screen/control/mac/python.js +2 -0
  39. package/src/screen/control/mac/ui-scan.js +45 -0
  40. package/src/screen/def.js +304 -0
  41. package/src/screen/index.js +17 -0
  42. package/src/screen/queue.js +54 -0
  43. package/src/screen/tools.js +50 -0
  44. package/src/tools.js +6 -0
package/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # @vox-ai-app/vox-integrations
2
+
3
+ macOS system integrations for Vox: Apple Mail, Screen control, and iMessage. Each integration ships with tool implementations and LLM tool definitions.
4
+
5
+ Requires macOS. Each integration needs specific system permissions granted by the user.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install @vox-ai-app/vox-integrations
11
+ ```
12
+
13
+ Peer dependency: `electron >= 28`
14
+
15
+ ## Exports
16
+
17
+ | Export | Contents |
18
+ | --------------------------------------------- | ----------------------------- |
19
+ | `@vox-ai-app/vox-integrations` | All exports |
20
+ | `@vox-ai-app/vox-integrations/defs` | All tool definitions |
21
+ | `@vox-ai-app/vox-integrations/mail` | Mail functions |
22
+ | `@vox-ai-app/vox-integrations/screen` | Screen capture + control |
23
+ | `@vox-ai-app/vox-integrations/screen/capture` | Capture only |
24
+ | `@vox-ai-app/vox-integrations/screen/control` | Control only |
25
+ | `@vox-ai-app/vox-integrations/screen/queue` | Session acquire/release |
26
+ | `@vox-ai-app/vox-integrations/imessage` | iMessage data, reply, service |
27
+
28
+ ## Mail
29
+
30
+ Requires **Automation permission** for Mail (System Settings → Privacy & Security → Automation).
31
+
32
+ ```js
33
+ import {
34
+ sendEmail,
35
+ readEmails,
36
+ searchContacts,
37
+ replyToEmail
38
+ } from '@vox-ai-app/vox-integrations/mail'
39
+
40
+ const emails = await readEmails({ account: 'Work', folder: 'INBOX', limit: 20 })
41
+ await sendEmail({ to: 'user@example.com', subject: 'Hi', body: 'Hello.' })
42
+ await replyToEmail({ messageId: '...', body: 'Thanks!' })
43
+ ```
44
+
45
+ Tool definitions:
46
+
47
+ ```js
48
+ import { MAIL_TOOL_DEFINITIONS } from '@vox-ai-app/vox-integrations/defs'
49
+ ```
50
+
51
+ ## Screen
52
+
53
+ Requires **Accessibility permission** (System Settings → Privacy & Security → Accessibility).
54
+
55
+ ```js
56
+ import {
57
+ captureFullScreen,
58
+ clickAt,
59
+ typeText,
60
+ getUiElements
61
+ } from '@vox-ai-app/vox-integrations/screen'
62
+ import { acquireScreen, releaseScreen } from '@vox-ai-app/vox-integrations/screen/queue'
63
+
64
+ const session = await acquireScreen()
65
+ try {
66
+ const img = await captureFullScreen()
67
+ await clickAt({ x: 100, y: 200 })
68
+ await typeText({ text: 'Hello' })
69
+ } finally {
70
+ await releaseScreen(session)
71
+ }
72
+ ```
73
+
74
+ Tool definitions:
75
+
76
+ ```js
77
+ import { SCREEN_TOOL_DEFINITIONS } from '@vox-ai-app/vox-integrations/defs'
78
+ ```
79
+
80
+ ## iMessage
81
+
82
+ Requires **Full Disk Access** (System Settings → Privacy & Security → Full Disk Access).
83
+
84
+ ### Tool use (read conversations, send messages)
85
+
86
+ ```js
87
+ import { listConversations, listContacts, sendReply } from '@vox-ai-app/vox-integrations/imessage'
88
+
89
+ const conversations = listConversations()
90
+ const contacts = listContacts()
91
+ await sendReply('+15551234567', 'Hello from Vox!')
92
+ ```
93
+
94
+ ### Gateway service (AI replies to incoming iMessages)
95
+
96
+ ```js
97
+ import { createIMessageService } from '@vox-ai-app/vox-integrations/imessage'
98
+
99
+ const svc = createIMessageService({
100
+ logger,
101
+ onTranscript: (text, handle) => {
102
+ /* emit to UI */
103
+ },
104
+ onOpenSettings: () => shell.openExternal('x-apple.systempreferences:...'),
105
+ onMessage: async (text, handle) => {
106
+ // call your AI here, return the reply string
107
+ return await askAI(text)
108
+ }
109
+ })
110
+
111
+ svc.start('my-passphrase')
112
+ // user sends "my-passphrase\nWhat's the weather?" → AI replies back
113
+ ```
114
+
115
+ `onMessage` must return a `Promise<string | null>`. Returning `null` skips the reply.
116
+
117
+ Tool definitions:
118
+
119
+ ```js
120
+ import { IMESSAGE_TOOL_DEFINITIONS } from '@vox-ai-app/vox-integrations/defs'
121
+ ```
122
+
123
+ ## License
124
+
125
+ MIT
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@vox-ai-app/integrations",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "description": "macOS integrations (Mail, Screen, iMessage) for Vox",
7
+ "main": "./src/index.js",
8
+ "private": false,
9
+ "exports": {
10
+ ".": "./src/index.js",
11
+ "./defs": "./src/defs/index.js",
12
+ "./defs/mail": "./src/defs/mail.js",
13
+ "./defs/screen": "./src/defs/screen.js",
14
+ "./defs/imessage": "./src/defs/imessage.js",
15
+ "./mail": "./src/mail/index.js",
16
+ "./screen": "./src/screen/index.js",
17
+ "./screen/capture": "./src/screen/capture/index.js",
18
+ "./screen/control": "./src/screen/control/index.js",
19
+ "./screen/queue": "./src/screen/queue.js",
20
+ "./imessage": "./src/imessage/index.js"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public",
24
+ "registry": "https://registry.npmjs.org"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/vox-ai-app/vox"
29
+ },
30
+ "files": [
31
+ "src",
32
+ "LICENSE",
33
+ "README.md"
34
+ ],
35
+ "peerDependencies": {
36
+ "electron": ">=28.0.0"
37
+ },
38
+ "dependencies": {
39
+ "@vox-ai-app/tools": "^1.0.0",
40
+ "better-sqlite3": "^12.0.0"
41
+ }
42
+ }
@@ -0,0 +1,41 @@
1
+ export const IMESSAGE_TOOL_DEFINITIONS = [
2
+ {
3
+ name: 'list_imessage_conversations',
4
+ description:
5
+ "List recent iMessage/SMS conversations from the user's macOS Messages app. Returns the 50 most recent threads with contact handle and a short snippet. Requires Full Disk Access in System Settings → Privacy & Security.",
6
+ parameters: {
7
+ type: 'object',
8
+ properties: {},
9
+ required: []
10
+ }
11
+ },
12
+ {
13
+ name: 'list_imessage_contacts',
14
+ description:
15
+ "List contacts from the user's macOS AddressBook with their iMessage handles (phone numbers and email addresses). Useful for looking up who to message.",
16
+ parameters: {
17
+ type: 'object',
18
+ properties: {},
19
+ required: []
20
+ }
21
+ },
22
+ {
23
+ name: 'send_imessage',
24
+ description:
25
+ 'Send an iMessage or SMS to a contact via the macOS Messages app using AppleScript. The handle must be a phone number (e.g. +14155551234) or email address registered with iMessage.',
26
+ parameters: {
27
+ type: 'object',
28
+ properties: {
29
+ handle: {
30
+ type: 'string',
31
+ description: 'Phone number or email address of the recipient.'
32
+ },
33
+ text: {
34
+ type: 'string',
35
+ description: 'Message text to send.'
36
+ }
37
+ },
38
+ required: ['handle', 'text']
39
+ }
40
+ }
41
+ ]
@@ -0,0 +1,9 @@
1
+ export {
2
+ canReadDb,
3
+ queryNewMessages,
4
+ getMaxRowId,
5
+ listConversations,
6
+ listContacts
7
+ } from './mac/data.js'
8
+ export { sendReply } from './mac/reply.js'
9
+ export { createIMessageService } from './mac/service.js'
@@ -0,0 +1,144 @@
1
+ import { existsSync, readdirSync } from 'fs'
2
+ import { join } from 'path'
3
+ import Database from 'better-sqlite3'
4
+
5
+ const HOME = process.env.HOME
6
+ const MESSAGES_DB = `${HOME}/Library/Messages/chat.db`
7
+ const AB_DIR = join(HOME, 'Library/Application Support/AddressBook')
8
+
9
+ export const canReadDb = () => {
10
+ try {
11
+ const db = new Database(MESSAGES_DB, { readonly: true, fileMustExist: true })
12
+ db.close()
13
+ return true
14
+ } catch {
15
+ return false
16
+ }
17
+ }
18
+
19
+ export const queryNewMessages = (afterRowId) => {
20
+ const db = new Database(MESSAGES_DB, { readonly: true, fileMustExist: true })
21
+ try {
22
+ return db
23
+ .prepare(
24
+ `SELECT m.ROWID, m.text,
25
+ CASE
26
+ WHEN m.is_from_me = 0 THEN h.id
27
+ ELSE COALESCE(
28
+ (
29
+ SELECT h2.id FROM chat_message_join cmj
30
+ JOIN chat_handle_join chj ON chj.chat_id = cmj.chat_id
31
+ JOIN handle h2 ON h2.ROWID = chj.handle_id
32
+ WHERE cmj.message_id = m.ROWID
33
+ LIMIT 1
34
+ ),
35
+ (
36
+ SELECT REPLACE(REPLACE(REPLACE(c.chat_identifier, 'iMessage;-;', ''), 'SMS;-;', ''), 'tel:', '')
37
+ FROM chat_message_join cmj2
38
+ JOIN chat c ON c.ROWID = cmj2.chat_id
39
+ WHERE cmj2.message_id = m.ROWID
40
+ LIMIT 1
41
+ )
42
+ )
43
+ END AS reply_handle
44
+ FROM message m
45
+ LEFT JOIN handle h ON m.handle_id = h.ROWID
46
+ WHERE m.ROWID > ?
47
+ AND m.text IS NOT NULL AND m.text != ''
48
+ ORDER BY m.ROWID ASC`
49
+ )
50
+ .all(afterRowId)
51
+ } finally {
52
+ db.close()
53
+ }
54
+ }
55
+
56
+ export const getMaxRowId = () => {
57
+ const db = new Database(MESSAGES_DB, { readonly: true, fileMustExist: true })
58
+ try {
59
+ const row = db.prepare(`SELECT MAX(ROWID) AS maxId FROM message`).get()
60
+ return row?.maxId ?? 0
61
+ } finally {
62
+ db.close()
63
+ }
64
+ }
65
+
66
+ export const listConversations = () => {
67
+ const db = new Database(MESSAGES_DB, { readonly: true, fileMustExist: true })
68
+ try {
69
+ const rows = db
70
+ .prepare(
71
+ `SELECT h.id AS handle_id,
72
+ MAX(m.date) AS last_date,
73
+ (SELECT text FROM message WHERE handle_id = h.ROWID AND text IS NOT NULL AND text != '' ORDER BY date DESC LIMIT 1) AS snippet
74
+ FROM handle h
75
+ JOIN message m ON m.handle_id = h.ROWID
76
+ WHERE m.text IS NOT NULL
77
+ GROUP BY h.id
78
+ ORDER BY last_date DESC
79
+ LIMIT 50`
80
+ )
81
+ .all()
82
+ return rows.map((r) => ({
83
+ handle: r.handle_id,
84
+ snippet: r.snippet ? String(r.snippet).slice(0, 80) : ''
85
+ }))
86
+ } finally {
87
+ db.close()
88
+ }
89
+ }
90
+
91
+ const findAddressBookDbs = () => {
92
+ const dbs = []
93
+ const main = join(AB_DIR, 'AddressBook-v22.abcddb')
94
+ if (existsSync(main)) dbs.push(main)
95
+ try {
96
+ const sources = join(AB_DIR, 'Sources')
97
+ for (const entry of readdirSync(sources)) {
98
+ const p = join(sources, entry, 'AddressBook-v22.abcddb')
99
+ if (existsSync(p)) dbs.push(p)
100
+ }
101
+ } catch {
102
+ void 0
103
+ }
104
+ return dbs
105
+ }
106
+
107
+ const CONTACTS_QUERY = `
108
+ SELECT COALESCE(r.ZNAME, TRIM(COALESCE(r.ZFIRSTNAME,'') || ' ' || COALESCE(r.ZLASTNAME,''))) AS name,
109
+ p.ZFULLNUMBER AS handle
110
+ FROM ZABCDRECORD r
111
+ JOIN ZABCDPHONENUMBER p ON p.ZOWNER = r.Z_PK
112
+ WHERE name != '' AND handle IS NOT NULL AND handle != ''
113
+ UNION ALL
114
+ SELECT COALESCE(r.ZNAME, TRIM(COALESCE(r.ZFIRSTNAME,'') || ' ' || COALESCE(r.ZLASTNAME,''))) AS name,
115
+ e.ZADDRESS AS handle
116
+ FROM ZABCDRECORD r
117
+ JOIN ZABCDEMAILADDRESS e ON e.ZOWNER = r.Z_PK
118
+ WHERE name != '' AND handle IS NOT NULL AND handle != ''
119
+ ORDER BY name ASC
120
+ LIMIT 500`
121
+
122
+ export const listContacts = (onError) => {
123
+ const seen = new Set()
124
+ const results = []
125
+ for (const dbPath of findAddressBookDbs()) {
126
+ try {
127
+ const db = new Database(dbPath, { readonly: true, fileMustExist: true })
128
+ try {
129
+ for (const row of db.prepare(CONTACTS_QUERY).all()) {
130
+ const key = `${row.name}|${row.handle}`
131
+ if (!seen.has(key)) {
132
+ seen.add(key)
133
+ results.push({ name: row.name, handle: row.handle })
134
+ }
135
+ }
136
+ } finally {
137
+ db.close()
138
+ }
139
+ } catch (err) {
140
+ onError?.(err)
141
+ }
142
+ }
143
+ return results
144
+ }
@@ -0,0 +1,68 @@
1
+ import fs from 'fs/promises'
2
+ import os from 'os'
3
+ import path from 'path'
4
+ import { promisify } from 'util'
5
+ import { exec } from 'child_process'
6
+
7
+ const execAsync = promisify(exec)
8
+
9
+ const writeTmp = async (content, ext) => {
10
+ const file = path.join(os.tmpdir(), `vox_ims_${Date.now()}.${ext}`)
11
+ await fs.writeFile(file, content, 'utf8')
12
+ return file
13
+ }
14
+
15
+ const escapeAppleScript = (s) => String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"')
16
+
17
+ const toAppleScriptString = (s) => {
18
+ const lines = escapeAppleScript(s).split('\n')
19
+ if (lines.length === 1) return `"${lines[0]}"`
20
+ return lines.map((l) => `"${l}"`).join(' & return & ')
21
+ }
22
+
23
+ export const sendReply = async (handle, text, filePaths = []) => {
24
+ const handleEsc = escapeAppleScript(handle)
25
+ const textExpr = toAppleScriptString(text)
26
+
27
+ const sendText = text
28
+ ? `
29
+ try
30
+ send ${textExpr} to buddy "${handleEsc}" of service "iMessage"
31
+ set sent to true
32
+ end try
33
+ if not sent then
34
+ try
35
+ send ${textExpr} to buddy "${handleEsc}" of (first service)
36
+ set sent to true
37
+ end try
38
+ end if`
39
+ : ''
40
+
41
+ const sendFiles = filePaths
42
+ .map((p) => {
43
+ const pEsc = escapeAppleScript(p)
44
+ return `
45
+ try
46
+ send POSIX file "${pEsc}" to buddy "${handleEsc}" of service "iMessage"
47
+ set sent to true
48
+ end try`
49
+ })
50
+ .join('')
51
+
52
+ const script = `tell application "Messages"
53
+ set sent to false
54
+ ${sendText}
55
+ ${sendFiles}
56
+ if not sent then
57
+ error "Could not send reply to ${handleEsc}"
58
+ end if
59
+ end tell`
60
+
61
+ const scriptFile = await writeTmp(script, 'scpt')
62
+ try {
63
+ await execAsync(`osascript "${scriptFile}"`, { timeout: 15_000 })
64
+ console.info('[imessage] Reply sent to', handle, 'files:', filePaths.length)
65
+ } finally {
66
+ await fs.unlink(scriptFile).catch(() => {})
67
+ }
68
+ }
@@ -0,0 +1,141 @@
1
+ import {
2
+ canReadDb,
3
+ getMaxRowId,
4
+ queryNewMessages,
5
+ listConversations as listConversationsFromDb,
6
+ listContacts as listContactsFromDb
7
+ } from './data.js'
8
+ import { sendReply } from './reply.js'
9
+
10
+ const POLL_INTERVAL_MS = 3_000
11
+ const REPLY_TIMEOUT_MS = 90_000
12
+ const FDA_ERROR_MESSAGE =
13
+ 'Vox needs Full Disk Access to read Messages. Opening System Settings → Privacy & Security → Full Disk Access — please enable it for Vox and try again.'
14
+
15
+ /**
16
+ * Creates a self-contained iMessage watcher service.
17
+ *
18
+ * @param {object} opts
19
+ * @param {(text: string, handle: string) => Promise<string|null>} opts.onMessage
20
+ * Called when a passphrase-matched message arrives. Must return the AI reply text (or null to skip reply).
21
+ * @param {(text: string, handle: string) => void} [opts.onTranscript]
22
+ * Optional: called before onMessage, useful for emitting transcript events upstream.
23
+ * @param {() => void} [opts.onOpenSettings]
24
+ * Optional: called when Full Disk Access is required. Default opens nothing.
25
+ * @param {{ info: Function, warn: Function, error: Function }} [opts.logger]
26
+ * Optional: defaults to console.
27
+ * @param {number} [opts.pollIntervalMs]
28
+ * Poll interval in ms. Default 3000.
29
+ * @returns {{ start, stop, getPassphrase, listConversations, listContacts, openSettings }}
30
+ */
31
+ export const createIMessageService = ({
32
+ onMessage,
33
+ onTranscript,
34
+ onOpenSettings,
35
+ logger,
36
+ pollIntervalMs = POLL_INTERVAL_MS
37
+ } = {}) => {
38
+ const _log = logger ?? console
39
+
40
+ let _pollTimer = null
41
+ let _passphrase = null
42
+ let _lastSeenRowId = 0
43
+
44
+ const openSettings = () => {
45
+ onOpenSettings?.()
46
+ }
47
+
48
+ const handleIncomingMessage = async (row) => {
49
+ if (!_passphrase) return
50
+ const raw = String(row.text || '')
51
+ const newline = raw.indexOf('\n')
52
+ if (newline === -1) return
53
+ const firstLine = raw.slice(0, newline).trim()
54
+ if (firstLine !== _passphrase) return
55
+ const text = raw.slice(newline + 1).trim()
56
+ if (!text) return
57
+
58
+ const sender = row.reply_handle
59
+ _log.info('[imessage] Passphrase match, reply_handle:', sender, 'text:', text.slice(0, 60))
60
+ if (!sender) return
61
+
62
+ onTranscript?.(text, sender)
63
+
64
+ let aiText = null
65
+ try {
66
+ aiText = await Promise.race([
67
+ onMessage(text, sender),
68
+ new Promise((resolve) => setTimeout(() => resolve(null), REPLY_TIMEOUT_MS))
69
+ ])
70
+ } catch (err) {
71
+ _log.error('[imessage] onMessage error:', err?.message)
72
+ return
73
+ }
74
+
75
+ if (!aiText) {
76
+ _log.warn('[imessage] No AI response received')
77
+ return
78
+ }
79
+
80
+ try {
81
+ await sendReply(sender, aiText)
82
+ } catch (err) {
83
+ _log.error('[imessage] Reply failed:', err?.message)
84
+ }
85
+ }
86
+
87
+ const poll = async () => {
88
+ if (!_passphrase || !canReadDb()) return
89
+ try {
90
+ const rows = queryNewMessages(_lastSeenRowId)
91
+ for (const row of rows) {
92
+ if (row.ROWID > _lastSeenRowId) _lastSeenRowId = row.ROWID
93
+ await handleIncomingMessage(row)
94
+ }
95
+ } catch (err) {
96
+ _log.error('[imessage] Poll error:', err?.message)
97
+ }
98
+ }
99
+
100
+ const start = (passphrase) => {
101
+ if (!canReadDb()) {
102
+ openSettings()
103
+ throw Object.assign(new Error(FDA_ERROR_MESSAGE), { code: 'IMESSAGE_FDA_REQUIRED' })
104
+ }
105
+ if (!passphrase?.trim()) throw new Error('Passphrase cannot be empty.')
106
+
107
+ stop()
108
+ _passphrase = passphrase.trim()
109
+ _lastSeenRowId = getMaxRowId()
110
+ _pollTimer = setInterval(poll, pollIntervalMs)
111
+ _log.info('[imessage] Watching all messages with passphrase')
112
+ return { passphrase: _passphrase }
113
+ }
114
+
115
+ const stop = () => {
116
+ if (_pollTimer) {
117
+ clearInterval(_pollTimer)
118
+ _pollTimer = null
119
+ }
120
+ _passphrase = null
121
+ _lastSeenRowId = 0
122
+ }
123
+
124
+ const getPassphrase = () => _passphrase
125
+
126
+ const listConversations = () => {
127
+ if (!canReadDb()) {
128
+ openSettings()
129
+ throw Object.assign(new Error(FDA_ERROR_MESSAGE), { code: 'IMESSAGE_FDA_REQUIRED' })
130
+ }
131
+ return listConversationsFromDb()
132
+ }
133
+
134
+ const listContacts = () => {
135
+ return listContactsFromDb((err) => {
136
+ _log.warn('[imessage] Skipping AddressBook DB:', err?.message)
137
+ })
138
+ }
139
+
140
+ return { start, stop, getPassphrase, listConversations, listContacts, openSettings }
141
+ }
@@ -0,0 +1,44 @@
1
+ import { IMESSAGE_TOOL_DEFINITIONS } from './def.js'
2
+ import { listConversations, listContacts } from './mac/data.js'
3
+ import { sendReply } from './mac/reply.js'
4
+
5
+ const DARWIN_ONLY = () => {
6
+ throw new Error('iMessage is only available on macOS.')
7
+ }
8
+
9
+ const listImessageConversations = async () => {
10
+ const conversations = listConversations()
11
+ return { conversations }
12
+ }
13
+
14
+ const listImessageContacts = async () => {
15
+ const contacts = listContacts()
16
+ return { contacts }
17
+ }
18
+
19
+ const sendImessage = async ({ handle, text }) => {
20
+ if (!handle) throw new Error('"handle" is required.')
21
+ if (!text) throw new Error('"text" is required.')
22
+ await sendReply(handle, String(text))
23
+ return { ok: true, handle }
24
+ }
25
+
26
+ const macExecutors = {
27
+ list_imessage_conversations: (_ctx) => listImessageConversations,
28
+ list_imessage_contacts: (_ctx) => listImessageContacts,
29
+ send_imessage: (_ctx) => sendImessage
30
+ }
31
+
32
+ const executors =
33
+ process.platform === 'darwin'
34
+ ? macExecutors
35
+ : {
36
+ list_imessage_conversations: (_ctx) => DARWIN_ONLY,
37
+ list_imessage_contacts: (_ctx) => DARWIN_ONLY,
38
+ send_imessage: (_ctx) => DARWIN_ONLY
39
+ }
40
+
41
+ export const IMESSAGE_TOOLS = IMESSAGE_TOOL_DEFINITIONS.map((def) => ({
42
+ definition: def,
43
+ execute: executors[def.name]
44
+ }))
package/src/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { MAIL_TOOL_DEFINITIONS } from './mail/def.js'
2
+ export { SCREEN_TOOL_DEFINITIONS } from './screen/def.js'
3
+ export { IMESSAGE_TOOL_DEFINITIONS } from './imessage/def.js'
4
+ export * from './mail/index.js'
5
+ export * from './screen/index.js'
6
+ export * from './imessage/index.js'
7
+ export { ALL_INTEGRATION_TOOLS, SCREEN_TOOLS, MAIL_TOOLS, IMESSAGE_TOOLS } from './tools.js'