소스 정보
- 저장소
- wende/macbeth
- 최근 소스 활동
- 2026년 4월 16일 00:44
- 감지된 SKILL.md 언어
- 영어
- 스타
- 3
- 포크
- 0
설치 방법
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
소스 파일 검토
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
메뉴
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/wende/macbeth --skill messages명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
How to drive macOS apps with Macbeth MCP tools — discovery, locators, actions, menus, screenshots/OCR, handles, and fallbacks
Automate Electron apps (Slack, VS Code, Discord, etc.) whose UI is Chromium web content exposed through the Accessibility API
Search and create contacts via CNContact, with AppleScript/Shortcuts helpers
SOC 직업 분류 기준
SKILL.md 표시 중
| name | messages |
| description | Browse conversations, read messages, and send texts in Apple Messages |
connect_app({ name: "Messages" })
Messages has a two-pane layout: conversation list (left) and transcript (right).
SceneWindow.| Query | Label | Action |
|---|---|---|
[{role:"window"}, {role:"toolbar"}, {role:"button"}] | Compose | Start new conversation |
[{role:"window"}, {role:"toolbar"}, {role:"menu_button", index:0}] | filter | Filter: Messages / Spam / Recently Deleted |
[{role:"window"}, {role:"toolbar"}, {role:"menu_button", index:1}] | Start FaceTime | Call menu |
[{"role":"window"}, {"role":"group", "identifier":"CKConversationListCollectionView"}, {"role":"text_field"}]
Label: "Search". Use fill to type a search query.
Container:
[{"role":"window"}, {"role":"group", "identifier":"ConversationList"}]
Each conversation is a text element. Iterate with index:
[{"role":"window"}, {"role":"group", "identifier":"ConversationList"}, {"role":"text", "index": N}]
"Name, Pinned""Name, Preview text, Date"The label field contains all info — contact name, last message preview, and timestamp are comma-separated.
To open a conversation, the user must click it manually — see Gotchas below.
[{"role":"window"}, {"role":"button", "identifier":"ConversationTitle"}]
Shows the currently open contact's name in label. Click to view contact details.
Container:
[{"role":"window"}, {"role":"group", "identifier":"TranscriptCollectionView"}]
Two types of children:
Date/time separators — text elements. Iterate with index:
[..., {"role":"text", "index": N}]
Label examples: "Nov 6, 2022 at 16:30", "Sun, Mar 1 at 00:50"
Message bubbles — group elements with identifier: "Sticker". Iterate with index:
[..., {"role":"group", "identifier":"Sticker", "index": N}]
label contains a summary: "Your iMessage, message text, time" or "Contact name, message text, time". Use this to determine sender and direction.[..., {"role":"group", "identifier":"Sticker", "index": N}, {"role":"text_area", "identifier":"CKBalloonTextView"}]
The value field has the complete message content.CKBalloonTextView child. In that case, extract the text from the Sticker group's label — the format is "Sender, message text, time", so strip the first and last comma-separated segments.Container:
[{"role":"window"}, {"role":"group", "identifier":"MessageEntryView"}]
| Query (from MessageEntryView) | Label | Purpose |
|---|---|---|
{"role":"text_field", "identifier":"messageBodyField"} | Message / iMessage | Text input |
{"role":"button", "index":0} | add | Attachment picker (+) |
{"role":"button", "index":1} | Record audio | Voice message |
{"role":"button", "index":2} | Emoji picker | Emoji keyboard |
| Path | Action |
|---|---|
File > New Message (id: new_message) | Cmd+N — new conversation |
Edit > Send Message (id: keyCommandSend:) | Send the composed message |
Conversation > Delete Conversation… (id: keyCommandDeleteConversation:) | Delete current conversation |
Conversation > Mark as Unread (id: keyCommandToggleUnreadState:) | Toggle unread state |
View > Show Times (id: keyCommandToggleTimeStamp:) | Show/hide timestamps |
Messages uses a SwiftUI list for the conversation sidebar. Synthesized CGEvent clicks (coordinate-based) are always interpreted as Cmd+click, causing multi-select instead of switching conversations. This is a macOS-level behavior — physical hardware clicks work, synthesized ones don't. No workaround exists at the CGEvent level.
Consequence: You cannot programmatically switch between conversations. You can only read from the currently open conversation.
What to do: Ask the user to manually open the conversation you need, then read its messages. Or use the search field + keyboard navigation as a partial workaround (fill search, press Down, press Return — results vary).
iMessage bubbles have a text_area id:"CKBalloonTextView" child with the full text in value. SMS bubbles may not. Fall back to parsing the Sticker group's label field: format is "Sender, message text, time" — strip the first and last comma-separated segments to get the message body.
press_key activates the target app before posting keyboard events. Prefer select_menu_item for menu actions (e.g. select_menu_item({ app: "Messages", menuPath: ["Edit", "Send Message"] })) and click/fill for UI elements. Only use press_key as a last resort when no AX or menu alternative exists.
Pinned conversation items have label format "Name, Pinned" with no date or preview text. You cannot determine when the last message was sent without opening the conversation.
When you fill the search field, results appear in a popover > scroll_area > table > row structure — not inside the ConversationList. The popover rows don't support AXPress. Use keyboard navigation (Down arrow + Return) to select a search result — but note this depends on the app being frontmost.
for index 0..N:
get_element [window, {group id:"ConversationList"}, {text, index}]
→ read label (name, preview, date are comma-separated)
stop when error (no more items)
# Check which conversation is open
get_element [window, {button id:"ConversationTitle"}]
→ label contains the contact name
# Read messages (oldest-first, iterate with index)
for index 0..N:
get_element [window, {group id:"TranscriptCollectionView"}, {group id:"Sticker", index}]
→ label gives sender + text + time
# For full text (iMessage):
get_element [..., {text_area id:"CKBalloonTextView"}] → value
# For SMS (no CKBalloonTextView): parse label instead
fill [window, {group id:"MessageEntryView"}, {text_field id:"messageBodyField"}] with "Hello!"
select_menu_item({ app: "Messages", menuPath: ["Edit", "Send Message"] })
click [window, toolbar, button] # Compose (AXPress works on buttons)
# A "To:" field appears — fill with the recipient name/number
# Then fill the message body and press Return