agent-messenger 2.27.2 → 2.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/README.md +11 -1
- package/.claude-plugin/marketplace.json +14 -1
- package/.claude-plugin/plugin.json +4 -2
- package/AGENTS.md +4 -0
- package/README.md +40 -9
- package/dist/package.json +10 -2
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +3 -0
- package/dist/src/cli.js.map +1 -1
- package/dist/src/platforms/discordbot/cli.js +1 -1
- package/dist/src/platforms/imessage/cli.d.ts +5 -0
- package/dist/src/platforms/imessage/cli.d.ts.map +1 -0
- package/dist/src/platforms/imessage/cli.js +31 -0
- package/dist/src/platforms/imessage/cli.js.map +1 -0
- package/dist/src/platforms/imessage/client.d.ts +43 -0
- package/dist/src/platforms/imessage/client.d.ts.map +1 -0
- package/dist/src/platforms/imessage/client.js +190 -0
- package/dist/src/platforms/imessage/client.js.map +1 -0
- package/dist/src/platforms/imessage/commands/auth.d.ts +31 -0
- package/dist/src/platforms/imessage/commands/auth.d.ts.map +1 -0
- package/dist/src/platforms/imessage/commands/auth.js +100 -0
- package/dist/src/platforms/imessage/commands/auth.js.map +1 -0
- package/dist/src/platforms/imessage/commands/chat.d.ts +3 -0
- package/dist/src/platforms/imessage/commands/chat.d.ts.map +1 -0
- package/dist/src/platforms/imessage/commands/chat.js +42 -0
- package/dist/src/platforms/imessage/commands/chat.js.map +1 -0
- package/dist/src/platforms/imessage/commands/doctor.d.ts +25 -0
- package/dist/src/platforms/imessage/commands/doctor.d.ts.map +1 -0
- package/dist/src/platforms/imessage/commands/doctor.js +101 -0
- package/dist/src/platforms/imessage/commands/doctor.js.map +1 -0
- package/dist/src/platforms/imessage/commands/index.d.ts +7 -0
- package/dist/src/platforms/imessage/commands/index.d.ts.map +1 -0
- package/dist/src/platforms/imessage/commands/index.js +7 -0
- package/dist/src/platforms/imessage/commands/index.js.map +1 -0
- package/dist/src/platforms/imessage/commands/message.d.ts +5 -0
- package/dist/src/platforms/imessage/commands/message.d.ts.map +1 -0
- package/dist/src/platforms/imessage/commands/message.js +127 -0
- package/dist/src/platforms/imessage/commands/message.js.map +1 -0
- package/dist/src/platforms/imessage/commands/setup.d.ts +20 -0
- package/dist/src/platforms/imessage/commands/setup.d.ts.map +1 -0
- package/dist/src/platforms/imessage/commands/setup.js +57 -0
- package/dist/src/platforms/imessage/commands/setup.js.map +1 -0
- package/dist/src/platforms/imessage/commands/shared.d.ts +10 -0
- package/dist/src/platforms/imessage/commands/shared.d.ts.map +1 -0
- package/dist/src/platforms/imessage/commands/shared.js +53 -0
- package/dist/src/platforms/imessage/commands/shared.js.map +1 -0
- package/dist/src/platforms/imessage/commands/whoami.d.ts +3 -0
- package/dist/src/platforms/imessage/commands/whoami.d.ts.map +1 -0
- package/dist/src/platforms/imessage/commands/whoami.js +20 -0
- package/dist/src/platforms/imessage/commands/whoami.js.map +1 -0
- package/dist/src/platforms/imessage/credential-manager.d.ts +22 -0
- package/dist/src/platforms/imessage/credential-manager.d.ts.map +1 -0
- package/dist/src/platforms/imessage/credential-manager.js +111 -0
- package/dist/src/platforms/imessage/credential-manager.js.map +1 -0
- package/dist/src/platforms/imessage/ensure-auth.d.ts +2 -0
- package/dist/src/platforms/imessage/ensure-auth.d.ts.map +1 -0
- package/dist/src/platforms/imessage/ensure-auth.js +13 -0
- package/dist/src/platforms/imessage/ensure-auth.js.map +1 -0
- package/dist/src/platforms/imessage/errors.d.ts +3 -0
- package/dist/src/platforms/imessage/errors.d.ts.map +1 -0
- package/dist/src/platforms/imessage/errors.js +25 -0
- package/dist/src/platforms/imessage/errors.js.map +1 -0
- package/dist/src/platforms/imessage/index.d.ts +7 -0
- package/dist/src/platforms/imessage/index.d.ts.map +1 -0
- package/dist/src/platforms/imessage/index.js +5 -0
- package/dist/src/platforms/imessage/index.js.map +1 -0
- package/dist/src/platforms/imessage/rpc.d.ts +20 -0
- package/dist/src/platforms/imessage/rpc.d.ts.map +1 -0
- package/dist/src/platforms/imessage/rpc.js +133 -0
- package/dist/src/platforms/imessage/rpc.js.map +1 -0
- package/dist/src/platforms/imessage/types.d.ts +59 -0
- package/dist/src/platforms/imessage/types.d.ts.map +1 -0
- package/dist/src/platforms/imessage/types.js +29 -0
- package/dist/src/platforms/imessage/types.js.map +1 -0
- package/dist/src/platforms/instagram/cli.js +1 -1
- package/dist/src/platforms/instagram/client.d.ts +6 -1
- package/dist/src/platforms/instagram/client.d.ts.map +1 -1
- package/dist/src/platforms/instagram/client.js +17 -0
- package/dist/src/platforms/instagram/client.js.map +1 -1
- package/dist/src/platforms/instagram/hybrid-listener.d.ts +48 -0
- package/dist/src/platforms/instagram/hybrid-listener.d.ts.map +1 -0
- package/dist/src/platforms/instagram/hybrid-listener.js +154 -0
- package/dist/src/platforms/instagram/hybrid-listener.js.map +1 -0
- package/dist/src/platforms/instagram/index.d.ts +2 -0
- package/dist/src/platforms/instagram/index.d.ts.map +1 -1
- package/dist/src/platforms/instagram/index.js +2 -0
- package/dist/src/platforms/instagram/index.js.map +1 -1
- package/dist/src/platforms/instagram/mqtt/connection.d.ts +4 -0
- package/dist/src/platforms/instagram/mqtt/connection.d.ts.map +1 -0
- package/dist/src/platforms/instagram/mqtt/connection.js +66 -0
- package/dist/src/platforms/instagram/mqtt/connection.js.map +1 -0
- package/dist/src/platforms/instagram/mqtt/thrift.d.ts +22 -0
- package/dist/src/platforms/instagram/mqtt/thrift.d.ts.map +1 -0
- package/dist/src/platforms/instagram/mqtt/thrift.js +143 -0
- package/dist/src/platforms/instagram/mqtt/thrift.js.map +1 -0
- package/dist/src/platforms/instagram/mqtt/transport.d.ts +42 -0
- package/dist/src/platforms/instagram/mqtt/transport.d.ts.map +1 -0
- package/dist/src/platforms/instagram/mqtt/transport.js +246 -0
- package/dist/src/platforms/instagram/mqtt/transport.js.map +1 -0
- package/dist/src/platforms/instagram/realtime-listener.d.ts +33 -0
- package/dist/src/platforms/instagram/realtime-listener.d.ts.map +1 -0
- package/dist/src/platforms/instagram/realtime-listener.js +151 -0
- package/dist/src/platforms/instagram/realtime-listener.js.map +1 -0
- package/dist/src/platforms/kakaotalk/cli.js +1 -1
- package/dist/src/platforms/line/cli.js +1 -1
- package/dist/src/platforms/webex/cli.js +1 -1
- package/dist/src/platforms/webexbot/cli.js +1 -1
- package/dist/src/platforms/wechatbot/cli.js +1 -1
- package/dist/src/platforms/whatsapp/cli.js +1 -1
- package/dist/src/platforms/whatsappbot/cli.js +1 -1
- package/dist/src/tui/adapters/imessage-adapter.d.ts +21 -0
- package/dist/src/tui/adapters/imessage-adapter.d.ts.map +1 -0
- package/dist/src/tui/adapters/imessage-adapter.js +110 -0
- package/dist/src/tui/adapters/imessage-adapter.js.map +1 -0
- package/dist/src/tui/app.d.ts.map +1 -1
- package/dist/src/tui/app.js +9 -0
- package/dist/src/tui/app.js.map +1 -1
- package/docs/content/docs/agent-skills.mdx +1 -1
- package/docs/content/docs/cli/imessage.mdx +99 -0
- package/docs/content/docs/cli/instagram.mdx +1 -1
- package/docs/content/docs/cli/meta.json +1 -0
- package/docs/content/docs/index.mdx +9 -6
- package/docs/content/docs/quick-start.mdx +2 -0
- package/docs/content/docs/sdk/instagram.mdx +90 -5
- package/docs/content/docs/tui.mdx +4 -3
- package/package.json +10 -2
- package/scripts/postbuild.ts +12 -15
- package/skills/agent-channeltalk/SKILL.md +1 -1
- package/skills/agent-channeltalkbot/SKILL.md +1 -1
- package/skills/agent-discord/SKILL.md +1 -1
- package/skills/agent-discordbot/SKILL.md +1 -1
- package/skills/agent-imessage/SKILL.md +133 -0
- package/skills/agent-imessage/references/authentication.md +66 -0
- package/skills/agent-imessage/references/permissions.md +31 -0
- package/skills/agent-imessage/references/setup.md +49 -0
- package/skills/agent-instagram/SKILL.md +72 -2
- package/skills/agent-instagram/references/common-patterns.md +36 -0
- package/skills/agent-instagram/templates/monitor-chat.sh +5 -0
- package/skills/agent-kakaotalk/SKILL.md +1 -1
- package/skills/agent-line/SKILL.md +1 -1
- package/skills/agent-slack/SKILL.md +1 -1
- package/skills/agent-slackbot/SKILL.md +1 -1
- package/skills/agent-teams/SKILL.md +1 -1
- package/skills/agent-telegram/SKILL.md +1 -1
- package/skills/agent-telegrambot/SKILL.md +1 -1
- package/skills/agent-webex/SKILL.md +1 -1
- package/skills/agent-webexbot/SKILL.md +1 -1
- package/skills/agent-wechatbot/SKILL.md +1 -1
- package/skills/agent-whatsapp/SKILL.md +1 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/cli.ts +4 -0
- package/src/platforms/imessage/cli.ts +39 -0
- package/src/platforms/imessage/client.test.ts +137 -0
- package/src/platforms/imessage/client.ts +253 -0
- package/src/platforms/imessage/commands/auth.ts +142 -0
- package/src/platforms/imessage/commands/chat.ts +51 -0
- package/src/platforms/imessage/commands/doctor.test.ts +80 -0
- package/src/platforms/imessage/commands/doctor.ts +139 -0
- package/src/platforms/imessage/commands/index.ts +6 -0
- package/src/platforms/imessage/commands/message.test.ts +20 -0
- package/src/platforms/imessage/commands/message.ts +151 -0
- package/src/platforms/imessage/commands/setup.test.ts +52 -0
- package/src/platforms/imessage/commands/setup.ts +76 -0
- package/src/platforms/imessage/commands/shared.test.ts +19 -0
- package/src/platforms/imessage/commands/shared.ts +69 -0
- package/src/platforms/imessage/commands/whoami.ts +22 -0
- package/src/platforms/imessage/credential-manager.test.ts +122 -0
- package/src/platforms/imessage/credential-manager.ts +123 -0
- package/src/platforms/imessage/ensure-auth.ts +18 -0
- package/src/platforms/imessage/errors.ts +27 -0
- package/src/platforms/imessage/index.test.ts +13 -0
- package/src/platforms/imessage/index.ts +16 -0
- package/src/platforms/imessage/rpc.ts +157 -0
- package/src/platforms/imessage/test-stub-imsg.mjs +147 -0
- package/src/platforms/imessage/types.test.ts +47 -0
- package/src/platforms/imessage/types.ts +95 -0
- package/src/platforms/instagram/client.ts +23 -0
- package/src/platforms/instagram/hybrid-listener.test.ts +135 -0
- package/src/platforms/instagram/hybrid-listener.ts +196 -0
- package/src/platforms/instagram/index.ts +10 -0
- package/src/platforms/instagram/mqtt/connection.ts +72 -0
- package/src/platforms/instagram/mqtt/thrift.test.ts +91 -0
- package/src/platforms/instagram/mqtt/thrift.ts +159 -0
- package/src/platforms/instagram/mqtt/transport.ts +285 -0
- package/src/platforms/instagram/realtime-listener.test.ts +34 -0
- package/src/platforms/instagram/realtime-listener.ts +201 -0
- package/src/platforms/webexbot/cli.ts +0 -0
- package/src/tui/adapters/imessage-adapter.ts +131 -0
- package/src/tui/app.ts +13 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
|
|
3
|
+
import { handleError } from '@/shared/utils/error-handler'
|
|
4
|
+
import { formatOutput } from '@/shared/utils/output'
|
|
5
|
+
|
|
6
|
+
import { IMessageCredentialManager } from '../credential-manager'
|
|
7
|
+
import { createAccountId } from '../types'
|
|
8
|
+
|
|
9
|
+
async function guard(fn: () => Promise<void>): Promise<void> {
|
|
10
|
+
try {
|
|
11
|
+
await fn()
|
|
12
|
+
} catch (error) {
|
|
13
|
+
handleError(error as Error)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface SetOptions {
|
|
18
|
+
bin?: string
|
|
19
|
+
region?: string
|
|
20
|
+
account?: string
|
|
21
|
+
label?: string
|
|
22
|
+
current?: boolean
|
|
23
|
+
pretty?: boolean
|
|
24
|
+
_manager?: IMessageCredentialManager
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function runSet(options: SetOptions): Promise<void> {
|
|
28
|
+
const manager = options._manager ?? new IMessageCredentialManager()
|
|
29
|
+
const now = new Date().toISOString()
|
|
30
|
+
const accountId = createAccountId(options.account ?? options.label ?? options.bin ?? 'default')
|
|
31
|
+
|
|
32
|
+
await manager.setAccount({
|
|
33
|
+
account_id: accountId,
|
|
34
|
+
provider: 'imsg',
|
|
35
|
+
label: options.label,
|
|
36
|
+
binary_path: options.bin,
|
|
37
|
+
region: options.region,
|
|
38
|
+
created_at: now,
|
|
39
|
+
updated_at: now,
|
|
40
|
+
})
|
|
41
|
+
if (options.current) await manager.setCurrent(accountId)
|
|
42
|
+
|
|
43
|
+
console.log(
|
|
44
|
+
formatOutput({ success: true, account_id: accountId, binary_path: options.bin ?? 'imsg' }, options.pretty),
|
|
45
|
+
)
|
|
46
|
+
process.exit(0)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function runList(options: { pretty?: boolean; _manager?: IMessageCredentialManager }): Promise<void> {
|
|
50
|
+
const manager = options._manager ?? new IMessageCredentialManager()
|
|
51
|
+
const accounts = await manager.listAccounts()
|
|
52
|
+
console.log(
|
|
53
|
+
formatOutput(
|
|
54
|
+
accounts.map((a) => ({
|
|
55
|
+
account_id: a.account_id,
|
|
56
|
+
label: a.label,
|
|
57
|
+
binary_path: a.binary_path ?? 'imsg',
|
|
58
|
+
region: a.region,
|
|
59
|
+
is_current: a.is_current,
|
|
60
|
+
})),
|
|
61
|
+
options.pretty,
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
process.exit(0)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function runUse(
|
|
68
|
+
accountId: string,
|
|
69
|
+
options: { pretty?: boolean; _manager?: IMessageCredentialManager },
|
|
70
|
+
): Promise<void> {
|
|
71
|
+
const manager = options._manager ?? new IMessageCredentialManager()
|
|
72
|
+
const found = await manager.setCurrent(accountId)
|
|
73
|
+
if (!found) {
|
|
74
|
+
console.log(formatOutput({ error: `Account "${accountId}" not found. Run "auth list".` }, options.pretty))
|
|
75
|
+
process.exit(1)
|
|
76
|
+
}
|
|
77
|
+
console.log(formatOutput({ success: true, account_id: createAccountId(accountId) }, options.pretty))
|
|
78
|
+
process.exit(0)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function runRemove(
|
|
82
|
+
accountId: string,
|
|
83
|
+
options: { pretty?: boolean; _manager?: IMessageCredentialManager },
|
|
84
|
+
): Promise<void> {
|
|
85
|
+
const manager = options._manager ?? new IMessageCredentialManager()
|
|
86
|
+
const removed = await manager.removeAccount(accountId)
|
|
87
|
+
if (!removed) {
|
|
88
|
+
console.log(formatOutput({ error: `Account "${accountId}" not found. Run "auth list".` }, options.pretty))
|
|
89
|
+
process.exit(1)
|
|
90
|
+
}
|
|
91
|
+
console.log(formatOutput({ success: true }, options.pretty))
|
|
92
|
+
process.exit(0)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function runLogout(options: { pretty?: boolean; _manager?: IMessageCredentialManager }): Promise<void> {
|
|
96
|
+
const manager = options._manager ?? new IMessageCredentialManager()
|
|
97
|
+
await manager.clearCredentials()
|
|
98
|
+
console.log(formatOutput({ success: true }, options.pretty))
|
|
99
|
+
process.exit(0)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const authCommand = new Command('auth')
|
|
103
|
+
.description('Account configuration commands')
|
|
104
|
+
.addCommand(
|
|
105
|
+
new Command('set')
|
|
106
|
+
.description('Configure an iMessage account (imsg binary path / region)')
|
|
107
|
+
.option('--bin <path>', 'Path to the imsg binary (default: imsg on PATH)')
|
|
108
|
+
.option('--region <code>', 'Default region for local-format phone numbers (e.g. US)')
|
|
109
|
+
.option('--account <id>', 'Account id/alias')
|
|
110
|
+
.option('--label <label>', 'Human-friendly label')
|
|
111
|
+
.option('--current', 'Set as the active account')
|
|
112
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
113
|
+
.action(async (opts: SetOptions) => guard(() => runSet(opts))),
|
|
114
|
+
)
|
|
115
|
+
.addCommand(
|
|
116
|
+
new Command('list')
|
|
117
|
+
.description('List configured accounts')
|
|
118
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
119
|
+
.action(async (opts: { pretty?: boolean }) => guard(() => runList(opts))),
|
|
120
|
+
)
|
|
121
|
+
.addCommand(
|
|
122
|
+
new Command('use')
|
|
123
|
+
.description('Switch the active account')
|
|
124
|
+
.argument('<account-id>', 'Account id')
|
|
125
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
126
|
+
.action(async (accountId: string, opts: { pretty?: boolean }) => guard(() => runUse(accountId, opts))),
|
|
127
|
+
)
|
|
128
|
+
.addCommand(
|
|
129
|
+
new Command('remove')
|
|
130
|
+
.description('Remove a configured account')
|
|
131
|
+
.argument('<account-id>', 'Account id')
|
|
132
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
133
|
+
.action(async (accountId: string, opts: { pretty?: boolean }) => guard(() => runRemove(accountId, opts))),
|
|
134
|
+
)
|
|
135
|
+
.addCommand(
|
|
136
|
+
new Command('logout')
|
|
137
|
+
.description('Clear all stored accounts')
|
|
138
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
139
|
+
.action(async (opts: { pretty?: boolean }) => guard(() => runLogout(opts))),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
export { runSet, runList, runUse, runRemove, runLogout }
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
|
|
3
|
+
import { handleError } from '@/shared/utils/error-handler'
|
|
4
|
+
import { formatOutput } from '@/shared/utils/output'
|
|
5
|
+
|
|
6
|
+
import { parseLimitOption, withImsgClient } from './shared'
|
|
7
|
+
|
|
8
|
+
async function listAction(options: { account?: string; pretty?: boolean; limit?: string }): Promise<void> {
|
|
9
|
+
try {
|
|
10
|
+
const limit = parseLimitOption(options.limit, 25)
|
|
11
|
+
const chats = await withImsgClient(options, (client) => client.listChats(limit))
|
|
12
|
+
console.log(formatOutput(chats, options.pretty))
|
|
13
|
+
process.exit(0)
|
|
14
|
+
} catch (error) {
|
|
15
|
+
handleError(error as Error)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function searchAction(
|
|
20
|
+
query: string,
|
|
21
|
+
options: { account?: string; pretty?: boolean; limit?: string },
|
|
22
|
+
): Promise<void> {
|
|
23
|
+
try {
|
|
24
|
+
const limit = parseLimitOption(options.limit, 25)
|
|
25
|
+
const chats = await withImsgClient(options, (client) => client.searchChats(query, limit))
|
|
26
|
+
console.log(formatOutput(chats, options.pretty))
|
|
27
|
+
process.exit(0)
|
|
28
|
+
} catch (error) {
|
|
29
|
+
handleError(error as Error)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const chatCommand = new Command('chat')
|
|
34
|
+
.description('iMessage chat commands')
|
|
35
|
+
.addCommand(
|
|
36
|
+
new Command('list')
|
|
37
|
+
.description('List chats')
|
|
38
|
+
.option('--limit <n>', 'Number of chats to fetch', '25')
|
|
39
|
+
.option('--account <id>', 'Use a specific iMessage account')
|
|
40
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
41
|
+
.action(listAction),
|
|
42
|
+
)
|
|
43
|
+
.addCommand(
|
|
44
|
+
new Command('search')
|
|
45
|
+
.description('Search chats by name or identifier')
|
|
46
|
+
.argument('<query>', 'Search query')
|
|
47
|
+
.option('--limit <n>', 'Number of results to return', '25')
|
|
48
|
+
.option('--account <id>', 'Use a specific iMessage account')
|
|
49
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
50
|
+
.action(searchAction),
|
|
51
|
+
)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'bun:test'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { runDoctor } from './doctor'
|
|
5
|
+
|
|
6
|
+
const STUB = join(import.meta.dir, '..', 'test-stub-imsg.mjs')
|
|
7
|
+
const saved: Record<string, string | undefined> = {}
|
|
8
|
+
|
|
9
|
+
function setMode(mode: string, bin = STUB): void {
|
|
10
|
+
saved.IMSG_STUB_MODE = process.env.IMSG_STUB_MODE
|
|
11
|
+
saved.AGENT_IMESSAGE_BIN = process.env.AGENT_IMESSAGE_BIN
|
|
12
|
+
process.env.IMSG_STUB_MODE = mode
|
|
13
|
+
process.env.AGENT_IMESSAGE_BIN = bin
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
for (const [k, v] of Object.entries(saved)) {
|
|
18
|
+
if (v === undefined) delete process.env[k]
|
|
19
|
+
else process.env[k] = v
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('runDoctor', () => {
|
|
24
|
+
it('reports imsg_not_found when the binary is missing', async () => {
|
|
25
|
+
setMode('ok', '/nonexistent/imsg-xyz')
|
|
26
|
+
const report = await runDoctor({})
|
|
27
|
+
expect(report.ok).toBe(false)
|
|
28
|
+
expect(report.code).toBe('imsg_not_found')
|
|
29
|
+
expect(report.suggestion).toContain('brew install')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('reports full_disk_access denied with Settings guidance', async () => {
|
|
33
|
+
setMode('fda_denied')
|
|
34
|
+
const report = await runDoctor({})
|
|
35
|
+
expect(report.ok).toBe(false)
|
|
36
|
+
expect(report.full_disk_access).toBe('denied')
|
|
37
|
+
expect(report.code).toBe('full_disk_access')
|
|
38
|
+
expect(report.suggestion).toContain('Full Disk Access')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('reports healthy with version, FDA ok, and bridge note', async () => {
|
|
42
|
+
setMode('ok')
|
|
43
|
+
const report = await runDoctor({})
|
|
44
|
+
expect(report.ok).toBe(true)
|
|
45
|
+
expect(report.imsg).toBe('found')
|
|
46
|
+
expect(report.imsg_version).toBe('0.11.1')
|
|
47
|
+
expect(report.full_disk_access).toBe('ok')
|
|
48
|
+
expect(report.bridge).toBe('disabled')
|
|
49
|
+
expect(report.warnings.some((w) => w.includes('Private API bridge'))).toBe(true)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('does not mislabel a non-FDA connect failure as Full Disk Access denied', async () => {
|
|
53
|
+
setMode('connect_rpc_fail')
|
|
54
|
+
const report = await runDoctor({})
|
|
55
|
+
expect(report.ok).toBe(false)
|
|
56
|
+
expect(report.full_disk_access).toBe('unknown')
|
|
57
|
+
expect(report.code).toBe('rpc_error')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('honors an explicit --bin override (verifies the supplied binary, not the default)', async () => {
|
|
61
|
+
setMode('ok', '/nonexistent/default-imsg')
|
|
62
|
+
const report = await runDoctor({ bin: STUB })
|
|
63
|
+
expect(report.ok).toBe(true)
|
|
64
|
+
expect(report.binary_path).toBe(STUB)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('fails on an invalid --bin even when the default/env binary is valid', async () => {
|
|
68
|
+
setMode('ok', STUB)
|
|
69
|
+
const report = await runDoctor({ bin: '/nonexistent/custom-imsg' })
|
|
70
|
+
expect(report.ok).toBe(false)
|
|
71
|
+
expect(report.code).toBe('imsg_not_found')
|
|
72
|
+
expect(report.binary_path).toBe('/nonexistent/custom-imsg')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('runs a test-chat send when requested', async () => {
|
|
76
|
+
setMode('ok')
|
|
77
|
+
const report = await runDoctor({ testChat: 42 })
|
|
78
|
+
expect(report.test_chat).toBe('sent')
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
|
|
3
|
+
import { formatOutput } from '@/shared/utils/output'
|
|
4
|
+
|
|
5
|
+
import { ImsgClient } from '../client'
|
|
6
|
+
import { IMessageCredentialManager } from '../credential-manager'
|
|
7
|
+
import { IMessageError } from '../types'
|
|
8
|
+
|
|
9
|
+
export interface DoctorReport {
|
|
10
|
+
ok: boolean
|
|
11
|
+
imsg: 'found' | 'missing'
|
|
12
|
+
imsg_version?: string | null
|
|
13
|
+
binary_path: string
|
|
14
|
+
full_disk_access: 'ok' | 'denied' | 'unknown'
|
|
15
|
+
automation: string
|
|
16
|
+
bridge: 'enabled' | 'disabled'
|
|
17
|
+
test_chat?: string
|
|
18
|
+
warnings: string[]
|
|
19
|
+
error?: string
|
|
20
|
+
code?: string
|
|
21
|
+
suggestion?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function runDoctor(options: {
|
|
25
|
+
account?: string
|
|
26
|
+
bin?: string
|
|
27
|
+
region?: string
|
|
28
|
+
testChat?: number
|
|
29
|
+
client?: ImsgClient
|
|
30
|
+
}): Promise<DoctorReport> {
|
|
31
|
+
const resolved = await new IMessageCredentialManager().resolveAccount(options.account)
|
|
32
|
+
const binaryPath = options.bin ?? resolved?.binary_path ?? process.env.AGENT_IMESSAGE_BIN ?? 'imsg'
|
|
33
|
+
const region = options.region ?? resolved?.region
|
|
34
|
+
|
|
35
|
+
const client = options.client ?? new ImsgClient()
|
|
36
|
+
await client.login({ binaryPath, region })
|
|
37
|
+
|
|
38
|
+
const warnings: string[] = []
|
|
39
|
+
|
|
40
|
+
let version: string | null = null
|
|
41
|
+
let versionError: IMessageError | undefined
|
|
42
|
+
try {
|
|
43
|
+
version = await client.getVersion()
|
|
44
|
+
} catch (error) {
|
|
45
|
+
versionError = error as IMessageError
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (version === null) {
|
|
49
|
+
const code = versionError?.code ?? 'imsg_not_found'
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
imsg: 'missing',
|
|
53
|
+
binary_path: binaryPath,
|
|
54
|
+
full_disk_access: 'unknown',
|
|
55
|
+
automation: 'unknown',
|
|
56
|
+
bridge: 'disabled',
|
|
57
|
+
warnings,
|
|
58
|
+
error: versionError?.message ?? `Could not run "${binaryPath} --version".`,
|
|
59
|
+
code,
|
|
60
|
+
suggestion:
|
|
61
|
+
versionError?.suggestion ??
|
|
62
|
+
(code === 'imsg_not_found' ? 'Install imsg: "brew install steipete/tap/imsg".' : undefined),
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let connectError: IMessageError | undefined
|
|
67
|
+
try {
|
|
68
|
+
await client.connect()
|
|
69
|
+
} catch (error) {
|
|
70
|
+
connectError = error as IMessageError
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (connectError) {
|
|
74
|
+
await client.close()
|
|
75
|
+
const isFda = connectError.code === 'full_disk_access'
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
imsg: 'found',
|
|
79
|
+
imsg_version: version,
|
|
80
|
+
binary_path: binaryPath,
|
|
81
|
+
full_disk_access: isFda ? 'denied' : 'unknown',
|
|
82
|
+
automation: 'unknown',
|
|
83
|
+
bridge: 'disabled',
|
|
84
|
+
warnings,
|
|
85
|
+
error: connectError.message,
|
|
86
|
+
code: connectError.code,
|
|
87
|
+
suggestion:
|
|
88
|
+
connectError.suggestion ??
|
|
89
|
+
(isFda
|
|
90
|
+
? 'Grant Full Disk Access to the app/terminal launching agent-messenger (System Settings → Privacy & Security → Full Disk Access). macOS grants to the parent process.'
|
|
91
|
+
: undefined),
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
warnings.push(
|
|
96
|
+
'Automation (Messages) is verified on first send. If sends fail, grant Privacy → Automation → Messages.',
|
|
97
|
+
)
|
|
98
|
+
warnings.push(
|
|
99
|
+
'Private API bridge disabled — send/read/watch/standard tapbacks work; typing/edit/group mgmt need "imsg launch" (SIP off).',
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
let testChatResult: string | undefined
|
|
103
|
+
if (options.testChat !== undefined) {
|
|
104
|
+
try {
|
|
105
|
+
await client.sendMessage({ chatId: options.testChat }, 'agent-imessage doctor test message')
|
|
106
|
+
testChatResult = 'sent'
|
|
107
|
+
} catch (error) {
|
|
108
|
+
testChatResult = `failed: ${(error as Error).message}`
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await client.close()
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
ok: true,
|
|
116
|
+
imsg: 'found',
|
|
117
|
+
imsg_version: version,
|
|
118
|
+
binary_path: binaryPath,
|
|
119
|
+
full_disk_access: 'ok',
|
|
120
|
+
automation: 'unknown',
|
|
121
|
+
bridge: 'disabled',
|
|
122
|
+
test_chat: testChatResult,
|
|
123
|
+
warnings,
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const doctorCommand = new Command('doctor')
|
|
128
|
+
.description('Diagnose imsg availability and macOS permissions')
|
|
129
|
+
.option('--account <id>', 'Check a specific account (default: current)')
|
|
130
|
+
.option('--bin <path>', 'Check a specific imsg binary path')
|
|
131
|
+
.option('--region <code>', 'Default region for local-format phone numbers')
|
|
132
|
+
.option('--test-chat <chatId>', 'Send a test message to this chat id')
|
|
133
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
134
|
+
.action(async (opts: { account?: string; bin?: string; region?: string; testChat?: string; pretty?: boolean }) => {
|
|
135
|
+
const testChat = opts.testChat && /^\d+$/.test(opts.testChat) ? Number.parseInt(opts.testChat, 10) : undefined
|
|
136
|
+
const report = await runDoctor({ account: opts.account, bin: opts.bin, region: opts.region, testChat })
|
|
137
|
+
console.log(formatOutput(report, opts.pretty))
|
|
138
|
+
process.exit(report.ok ? 0 : 1)
|
|
139
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { IMessageError } from '../types'
|
|
4
|
+
import { parseRowId } from './message'
|
|
5
|
+
|
|
6
|
+
describe('parseRowId', () => {
|
|
7
|
+
it('parses a non-negative integer', () => {
|
|
8
|
+
expect(parseRowId('9000')).toBe(9000)
|
|
9
|
+
expect(parseRowId('0')).toBe(0)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('returns undefined when absent', () => {
|
|
13
|
+
expect(parseRowId(undefined)).toBeUndefined()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('rejects non-integers', () => {
|
|
17
|
+
expect(() => parseRowId('abc')).toThrow(IMessageError)
|
|
18
|
+
expect(() => parseRowId('-5')).toThrow(IMessageError)
|
|
19
|
+
})
|
|
20
|
+
})
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
|
|
3
|
+
import { handleError } from '@/shared/utils/error-handler'
|
|
4
|
+
import { formatOutput } from '@/shared/utils/output'
|
|
5
|
+
|
|
6
|
+
import { IMessageError, type IMessageMessageSummary } from '../types'
|
|
7
|
+
import { parseLimitOption, resolveChatId, resolveChatTarget, withImsgClient } from './shared'
|
|
8
|
+
|
|
9
|
+
async function listAction(
|
|
10
|
+
chat: string,
|
|
11
|
+
options: { account?: string; pretty?: boolean; limit?: string; start?: string },
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
try {
|
|
14
|
+
const limit = parseLimitOption(options.limit, 25)
|
|
15
|
+
const messages = await withImsgClient(options, async (client) => {
|
|
16
|
+
const chatId = await resolveChatId(client, chat)
|
|
17
|
+
return client.getMessages(chatId, limit, options.start)
|
|
18
|
+
})
|
|
19
|
+
console.log(formatOutput(messages, options.pretty))
|
|
20
|
+
process.exit(0)
|
|
21
|
+
} catch (error) {
|
|
22
|
+
handleError(error as Error)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function sendAction(chat: string, text: string, options: { account?: string; pretty?: boolean }): Promise<void> {
|
|
27
|
+
try {
|
|
28
|
+
const message = await withImsgClient(options, (client) => client.sendMessage(resolveChatTarget(chat), text))
|
|
29
|
+
console.log(formatOutput(message, options.pretty))
|
|
30
|
+
process.exit(0)
|
|
31
|
+
} catch (error) {
|
|
32
|
+
handleError(error as Error)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function reactAction(
|
|
37
|
+
chat: string,
|
|
38
|
+
reaction: string,
|
|
39
|
+
options: { account?: string; pretty?: boolean },
|
|
40
|
+
): Promise<void> {
|
|
41
|
+
try {
|
|
42
|
+
await withImsgClient(options, async (client) => {
|
|
43
|
+
const chatId = await resolveChatId(client, chat)
|
|
44
|
+
await client.sendReaction(chatId, reaction)
|
|
45
|
+
})
|
|
46
|
+
console.log(formatOutput({ success: true, chat, reaction }, options.pretty))
|
|
47
|
+
process.exit(0)
|
|
48
|
+
} catch (error) {
|
|
49
|
+
handleError(error as Error)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseRowId(raw: string | undefined): number | undefined {
|
|
54
|
+
if (raw === undefined) return undefined
|
|
55
|
+
if (!/^\d+$/.test(raw.trim())) {
|
|
56
|
+
throw new IMessageError('--since-rowid must be a non-negative integer.', 'invalid_limit')
|
|
57
|
+
}
|
|
58
|
+
return Number.parseInt(raw.trim(), 10)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function watchAction(options: {
|
|
62
|
+
account?: string
|
|
63
|
+
pretty?: boolean
|
|
64
|
+
chat?: string
|
|
65
|
+
sinceRowid?: string
|
|
66
|
+
jsonl?: boolean
|
|
67
|
+
}): Promise<void> {
|
|
68
|
+
try {
|
|
69
|
+
const sinceRowId = parseRowId(options.sinceRowid)
|
|
70
|
+
await withImsgClient(options, async (client) => {
|
|
71
|
+
const chatId = options.chat && options.chat !== 'all' ? await resolveChatId(client, options.chat) : undefined
|
|
72
|
+
const seen = new Set<string>()
|
|
73
|
+
const SEEN_CAP = 2000
|
|
74
|
+
let running = true
|
|
75
|
+
|
|
76
|
+
const emit = (msg: IMessageMessageSummary): void => {
|
|
77
|
+
if (msg.guid) {
|
|
78
|
+
if (seen.has(msg.guid)) return
|
|
79
|
+
seen.add(msg.guid)
|
|
80
|
+
if (seen.size > SEEN_CAP) {
|
|
81
|
+
const oldest = seen.values().next().value
|
|
82
|
+
if (oldest !== undefined) seen.delete(oldest)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
console.log(options.jsonl ? JSON.stringify(msg) : formatOutput(msg, options.pretty))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const stop = await client.watch(emit, {
|
|
89
|
+
chatId,
|
|
90
|
+
sinceRowId,
|
|
91
|
+
onError: (m) => console.error(JSON.stringify({ warning: m })),
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
await new Promise<void>((resolve) => {
|
|
95
|
+
const shutdown = (): void => {
|
|
96
|
+
if (!running) return
|
|
97
|
+
running = false
|
|
98
|
+
void stop().finally(resolve)
|
|
99
|
+
}
|
|
100
|
+
process.on('SIGINT', shutdown)
|
|
101
|
+
process.on('SIGTERM', shutdown)
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
process.exit(0)
|
|
105
|
+
} catch (error) {
|
|
106
|
+
handleError(error as Error)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const messageCommand = new Command('message')
|
|
111
|
+
.description('iMessage message commands')
|
|
112
|
+
.addCommand(
|
|
113
|
+
new Command('list')
|
|
114
|
+
.description('List messages from a chat')
|
|
115
|
+
.argument('<chat>', 'Chat id, guid, or identifier')
|
|
116
|
+
.option('--limit <n>', 'Number of messages to fetch', '25')
|
|
117
|
+
.option('--start <iso>', 'Only messages on/after this ISO timestamp')
|
|
118
|
+
.option('--account <id>', 'Use a specific iMessage account')
|
|
119
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
120
|
+
.action(listAction),
|
|
121
|
+
)
|
|
122
|
+
.addCommand(
|
|
123
|
+
new Command('send')
|
|
124
|
+
.description('Send a text message to a chat or recipient')
|
|
125
|
+
.argument('<chat>', 'Chat id/guid, or a phone/email recipient')
|
|
126
|
+
.argument('<text>', 'Message text')
|
|
127
|
+
.option('--account <id>', 'Use a specific iMessage account')
|
|
128
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
129
|
+
.action(sendAction),
|
|
130
|
+
)
|
|
131
|
+
.addCommand(
|
|
132
|
+
new Command('react')
|
|
133
|
+
.description('React to the most recent incoming message in a chat (standard tapbacks)')
|
|
134
|
+
.argument('<chat>', 'Chat id, guid, or identifier')
|
|
135
|
+
.argument('<reaction>', 'love | like | dislike | laugh | emphasis | question')
|
|
136
|
+
.option('--account <id>', 'Use a specific iMessage account')
|
|
137
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
138
|
+
.action(reactAction),
|
|
139
|
+
)
|
|
140
|
+
.addCommand(
|
|
141
|
+
new Command('watch')
|
|
142
|
+
.description('Stream new messages via imsg (resumable with --since-rowid)')
|
|
143
|
+
.option('--chat <ref|all>', 'Watch a specific chat or "all"', 'all')
|
|
144
|
+
.option('--since-rowid <n>', 'Replay messages after this message rowid, then stream live')
|
|
145
|
+
.option('--jsonl', 'Emit one JSON object per line')
|
|
146
|
+
.option('--account <id>', 'Use a specific iMessage account')
|
|
147
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
148
|
+
.action(watchAction),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
export { parseRowId }
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'bun:test'
|
|
2
|
+
import { existsSync, rmSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import { IMessageCredentialManager } from '../credential-manager'
|
|
6
|
+
import { performSetup } from './setup'
|
|
7
|
+
|
|
8
|
+
const STUB = join(import.meta.dir, '..', 'test-stub-imsg.mjs')
|
|
9
|
+
const testDirs: string[] = []
|
|
10
|
+
const saved: Record<string, string | undefined> = {}
|
|
11
|
+
|
|
12
|
+
function freshManager(): { manager: IMessageCredentialManager; dir: string } {
|
|
13
|
+
const dir = join(import.meta.dir, `.test-setup-${Date.now()}-${Math.random().toString(36).slice(2)}`)
|
|
14
|
+
testDirs.push(dir)
|
|
15
|
+
return { manager: new IMessageCredentialManager(dir), dir }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function setEnv(bin?: string): void {
|
|
19
|
+
saved.IMSG_STUB_MODE = process.env.IMSG_STUB_MODE
|
|
20
|
+
saved.AGENT_IMESSAGE_BIN = process.env.AGENT_IMESSAGE_BIN
|
|
21
|
+
process.env.IMSG_STUB_MODE = 'ok'
|
|
22
|
+
if (bin === undefined) delete process.env.AGENT_IMESSAGE_BIN
|
|
23
|
+
else process.env.AGENT_IMESSAGE_BIN = bin
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
for (const [k, v] of Object.entries(saved)) {
|
|
28
|
+
if (v === undefined) delete process.env[k]
|
|
29
|
+
else process.env[k] = v
|
|
30
|
+
}
|
|
31
|
+
for (const dir of testDirs) rmSync(dir, { recursive: true, force: true })
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('performSetup', () => {
|
|
35
|
+
it('saves the account when the supplied --bin verifies', async () => {
|
|
36
|
+
setEnv()
|
|
37
|
+
const { manager, dir } = freshManager()
|
|
38
|
+
const result = await performSetup({ bin: STUB, manager })
|
|
39
|
+
expect(result.ok).toBe(true)
|
|
40
|
+
expect(existsSync(join(dir, 'imessage-credentials.json'))).toBe(true)
|
|
41
|
+
expect((await manager.resolveAccount())?.binary_path).toBe(STUB)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('does NOT persist an account when an invalid --bin fails, even if the default binary is valid', async () => {
|
|
45
|
+
setEnv(STUB)
|
|
46
|
+
const { manager, dir } = freshManager()
|
|
47
|
+
const result = await performSetup({ bin: '/nonexistent/custom-imsg', manager })
|
|
48
|
+
expect(result.ok).toBe(false)
|
|
49
|
+
expect(result.report.code).toBe('imsg_not_found')
|
|
50
|
+
expect(existsSync(join(dir, 'imessage-credentials.json'))).toBe(false)
|
|
51
|
+
})
|
|
52
|
+
})
|