agent-messenger 2.27.1 → 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/kakaotalk/client.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/client.js +4 -4
- package/dist/src/platforms/kakaotalk/client.js.map +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/kakaotalk/client.test.ts +40 -0
- package/src/platforms/kakaotalk/client.ts +6 -4
- 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,253 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
import { classifyImsgFailure } from './errors'
|
|
4
|
+
import { ImsgRpc } from './rpc'
|
|
5
|
+
import { type IMessageChatSummary, type IMessageMessageSummary, type IMessageStatus, IMessageError } from './types'
|
|
6
|
+
|
|
7
|
+
interface ImsgChat {
|
|
8
|
+
id: number
|
|
9
|
+
name?: string | null
|
|
10
|
+
identifier?: string | null
|
|
11
|
+
guid?: string | null
|
|
12
|
+
service?: string | null
|
|
13
|
+
is_group?: boolean
|
|
14
|
+
participants?: string[] | null
|
|
15
|
+
last_message?: ImsgMessage | null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ImsgMessage {
|
|
19
|
+
id: number
|
|
20
|
+
chat_id: number
|
|
21
|
+
guid: string
|
|
22
|
+
sender?: string | null
|
|
23
|
+
sender_name?: string | null
|
|
24
|
+
is_from_me?: boolean
|
|
25
|
+
text?: string | null
|
|
26
|
+
created_at?: string | null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const STANDARD_REACTIONS = new Set(['love', 'like', 'dislike', 'laugh', 'emphasis', 'question'])
|
|
30
|
+
|
|
31
|
+
export interface SendTarget {
|
|
32
|
+
chatId?: number
|
|
33
|
+
chatGuid?: string
|
|
34
|
+
chatIdentifier?: string
|
|
35
|
+
to?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type OneShotRunner = (
|
|
39
|
+
binaryPath: string,
|
|
40
|
+
args: string[],
|
|
41
|
+
) => Promise<{ code: number; stdout: string; stderr: string }>
|
|
42
|
+
|
|
43
|
+
const defaultRunner: OneShotRunner = (binaryPath, args) =>
|
|
44
|
+
new Promise((resolve, reject) => {
|
|
45
|
+
let proc: ReturnType<typeof spawn>
|
|
46
|
+
try {
|
|
47
|
+
proc = spawn(binaryPath, args, { stdio: ['ignore', 'pipe', 'pipe'] })
|
|
48
|
+
} catch {
|
|
49
|
+
reject(new IMessageError(`Could not run "${binaryPath}".`, 'imsg_not_found'))
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
let stdout = ''
|
|
53
|
+
let stderr = ''
|
|
54
|
+
proc.stdout?.on('data', (c: Buffer) => (stdout += c.toString()))
|
|
55
|
+
proc.stderr?.on('data', (c: Buffer) => (stderr += c.toString()))
|
|
56
|
+
proc.once('error', (err: NodeJS.ErrnoException) => {
|
|
57
|
+
reject(
|
|
58
|
+
err.code === 'ENOENT'
|
|
59
|
+
? new IMessageError(`Could not run "${binaryPath}".`, 'imsg_not_found', {
|
|
60
|
+
suggestion: 'Install imsg: "brew install steipete/tap/imsg".',
|
|
61
|
+
doctorCommand: 'agent-imessage doctor',
|
|
62
|
+
})
|
|
63
|
+
: err,
|
|
64
|
+
)
|
|
65
|
+
})
|
|
66
|
+
proc.once('close', (code) => resolve({ code: code ?? 0, stdout, stderr }))
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
function summarizeMessage(msg: ImsgMessage): IMessageMessageSummary {
|
|
70
|
+
return {
|
|
71
|
+
id: msg.id,
|
|
72
|
+
guid: msg.guid,
|
|
73
|
+
chat_id: msg.chat_id,
|
|
74
|
+
from: msg.is_from_me ? '' : (msg.sender ?? ''),
|
|
75
|
+
from_name: msg.sender_name ?? undefined,
|
|
76
|
+
is_outgoing: Boolean(msg.is_from_me),
|
|
77
|
+
timestamp: msg.created_at ?? new Date(0).toISOString(),
|
|
78
|
+
text: msg.text ?? undefined,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function summarizeChat(chat: ImsgChat): IMessageChatSummary {
|
|
83
|
+
return {
|
|
84
|
+
id: chat.id,
|
|
85
|
+
guid: chat.guid ?? null,
|
|
86
|
+
identifier: chat.identifier ?? null,
|
|
87
|
+
name: chat.name?.trim() || chat.identifier || chat.guid || String(chat.id),
|
|
88
|
+
service: chat.service ?? 'iMessage',
|
|
89
|
+
is_group: Boolean(chat.is_group),
|
|
90
|
+
participants: chat.participants ?? [],
|
|
91
|
+
last_message: chat.last_message ? summarizeMessage(chat.last_message) : undefined,
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export class ImsgClient {
|
|
96
|
+
private binaryPath = 'imsg'
|
|
97
|
+
private region: string | undefined
|
|
98
|
+
private rpc: ImsgRpc
|
|
99
|
+
private runner: OneShotRunner
|
|
100
|
+
|
|
101
|
+
constructor(deps?: { rpc?: ImsgRpc; runner?: OneShotRunner }) {
|
|
102
|
+
this.rpc = deps?.rpc ?? new ImsgRpc()
|
|
103
|
+
this.runner = deps?.runner ?? defaultRunner
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async login(credentials?: { binaryPath?: string; region?: string }): Promise<this> {
|
|
107
|
+
if (credentials) {
|
|
108
|
+
this.binaryPath = credentials.binaryPath ?? 'imsg'
|
|
109
|
+
this.region = credentials.region
|
|
110
|
+
return this
|
|
111
|
+
}
|
|
112
|
+
const { IMessageCredentialManager } = await import('./credential-manager')
|
|
113
|
+
const resolved = await new IMessageCredentialManager().resolveAccount()
|
|
114
|
+
this.binaryPath = resolved?.binary_path ?? 'imsg'
|
|
115
|
+
this.region = resolved?.region
|
|
116
|
+
return this
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async connect(): Promise<void> {
|
|
120
|
+
await this.rpc.start(this.binaryPath)
|
|
121
|
+
await this.rpc.request('chats.list', { limit: 1 })
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async getVersion(): Promise<string | null> {
|
|
125
|
+
const result = await this.runner(this.binaryPath, ['--version'])
|
|
126
|
+
if (result.code !== 0) return null
|
|
127
|
+
return result.stdout.trim() || null
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async getStatus(): Promise<IMessageStatus> {
|
|
131
|
+
let version: string | null = null
|
|
132
|
+
let fullDiskAccess = false
|
|
133
|
+
try {
|
|
134
|
+
version = await this.getVersion()
|
|
135
|
+
} catch {
|
|
136
|
+
version = null
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
await this.rpc.request('chats.list', { limit: 1 })
|
|
140
|
+
fullDiskAccess = true
|
|
141
|
+
} catch {
|
|
142
|
+
fullDiskAccess = false
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
imsg_version: version,
|
|
146
|
+
binary_path: this.binaryPath,
|
|
147
|
+
full_disk_access: fullDiskAccess,
|
|
148
|
+
automation: 'unknown',
|
|
149
|
+
bridge_available: false,
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async listChats(limit = 25): Promise<IMessageChatSummary[]> {
|
|
154
|
+
const result = await this.rpc.request<{ chats: ImsgChat[] }>('chats.list', { limit })
|
|
155
|
+
return result.chats.map(summarizeChat)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async searchChats(query: string, limit = 25): Promise<IMessageChatSummary[]> {
|
|
159
|
+
const all = await this.listChats(Math.max(limit, 100))
|
|
160
|
+
const lower = query.toLowerCase()
|
|
161
|
+
return all
|
|
162
|
+
.filter((c) => c.name.toLowerCase().includes(lower) || (c.identifier ?? '').toLowerCase().includes(lower))
|
|
163
|
+
.slice(0, limit)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async getMessages(chatId: number, limit = 25, start?: string): Promise<IMessageMessageSummary[]> {
|
|
167
|
+
const result = await this.rpc.request<{ messages: ImsgMessage[] }>('messages.history', {
|
|
168
|
+
chat_id: chatId,
|
|
169
|
+
limit,
|
|
170
|
+
...(start ? { start } : {}),
|
|
171
|
+
})
|
|
172
|
+
return result.messages.map(summarizeMessage).sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp))
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async sendMessage(target: SendTarget, text: string): Promise<IMessageMessageSummary> {
|
|
176
|
+
const params: Record<string, unknown> = { text }
|
|
177
|
+
if (target.chatId !== undefined) params.chat_id = target.chatId
|
|
178
|
+
else if (target.chatGuid) params.chat_guid = target.chatGuid
|
|
179
|
+
else if (target.chatIdentifier) params.chat_identifier = target.chatIdentifier
|
|
180
|
+
else if (target.to) params.to = target.to
|
|
181
|
+
else throw new IMessageError('A chat id, guid, or recipient is required to send.', 'chat_not_found')
|
|
182
|
+
if (this.region) params.region = this.region
|
|
183
|
+
|
|
184
|
+
const result = await this.rpc.request<{ ok?: boolean; guid?: string; id?: number; service?: string }>(
|
|
185
|
+
'send',
|
|
186
|
+
params,
|
|
187
|
+
)
|
|
188
|
+
return {
|
|
189
|
+
id: result.id ?? 0,
|
|
190
|
+
guid: result.guid ?? '',
|
|
191
|
+
chat_id: target.chatId ?? 0,
|
|
192
|
+
from: '',
|
|
193
|
+
is_outgoing: true,
|
|
194
|
+
timestamp: new Date().toISOString(),
|
|
195
|
+
text,
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async sendReaction(chatId: number, reaction: string, messageGuid?: string): Promise<void> {
|
|
200
|
+
if (messageGuid) {
|
|
201
|
+
throw new IMessageError(
|
|
202
|
+
'Reacting to a specific message requires the imsg Private API bridge.',
|
|
203
|
+
'private_api_required',
|
|
204
|
+
{ suggestion: 'Enable the bridge with "imsg launch" (requires SIP disabled).' },
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
if (!STANDARD_REACTIONS.has(reaction)) {
|
|
208
|
+
throw new IMessageError(
|
|
209
|
+
`Custom reactions require the imsg Private API bridge. Standard tapbacks: ${[...STANDARD_REACTIONS].join(', ')}.`,
|
|
210
|
+
'private_api_required',
|
|
211
|
+
{ suggestion: 'Enable the bridge with "imsg launch" (requires SIP disabled).' },
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
const result = await this.runner(this.binaryPath, [
|
|
215
|
+
'react',
|
|
216
|
+
'--chat-id',
|
|
217
|
+
String(chatId),
|
|
218
|
+
'--reaction',
|
|
219
|
+
reaction,
|
|
220
|
+
'--json',
|
|
221
|
+
])
|
|
222
|
+
if (result.code !== 0) {
|
|
223
|
+
const detail = result.stderr.trim() || result.stdout.trim() || 'Reaction failed.'
|
|
224
|
+
throw classifyImsgFailure(detail, 'send_failed')
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async watch(
|
|
229
|
+
onMessage: (msg: IMessageMessageSummary) => void,
|
|
230
|
+
options: { chatId?: number; sinceRowId?: number; onError?: (msg: string) => void } = {},
|
|
231
|
+
): Promise<() => Promise<void>> {
|
|
232
|
+
const params: Record<string, unknown> = { include_reactions: false }
|
|
233
|
+
if (options.chatId !== undefined) params.chat_id = options.chatId
|
|
234
|
+
if (options.sinceRowId !== undefined) params.since_rowid = options.sinceRowId
|
|
235
|
+
|
|
236
|
+
const subscription = await this.rpc.subscribe(
|
|
237
|
+
params,
|
|
238
|
+
(raw) => onMessage(summarizeMessage(raw as ImsgMessage)),
|
|
239
|
+
options.onError,
|
|
240
|
+
)
|
|
241
|
+
return async () => {
|
|
242
|
+
await this.rpc.unsubscribe(subscription)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async getProfile(): Promise<IMessageStatus> {
|
|
247
|
+
return this.getStatus()
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async close(): Promise<void> {
|
|
251
|
+
this.rpc.close()
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -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
|
+
})
|