@l22-io/orchard-mcp 0.3.2 → 0.6.1
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/README.md +11 -8
- package/build/bridge.d.ts +13 -2
- package/build/bridge.js +140 -23
- package/build/bridge.js.map +1 -1
- package/build/index.js +11 -1
- package/build/index.js.map +1 -1
- package/build/tools/contacts.d.ts +2 -0
- package/build/tools/contacts.js +37 -0
- package/build/tools/contacts.js.map +1 -0
- package/build/tools/files.js +4 -1
- package/build/tools/files.js.map +1 -1
- package/build/tools/keynote.d.ts +2 -0
- package/build/tools/keynote.js +198 -0
- package/build/tools/keynote.js.map +1 -0
- package/build/tools/mail.js +69 -13
- package/build/tools/mail.js.map +1 -1
- package/build/tools/notes.d.ts +2 -0
- package/build/tools/notes.js +83 -0
- package/build/tools/notes.js.map +1 -0
- package/build/tools/numbers.d.ts +2 -0
- package/build/tools/numbers.js +167 -0
- package/build/tools/numbers.js.map +1 -0
- package/build/tools/pages.d.ts +2 -0
- package/build/tools/pages.js +143 -0
- package/build/tools/pages.js.map +1 -0
- package/package.json +12 -9
- package/scripts/postinstall.sh +77 -8
- package/swift/.build/AppleBridge.app/Contents/MacOS/apple-bridge +0 -0
- package/swift/.build/AppleBridge.app.sha256 +1 -0
- package/swift/Package.resolved +14 -0
- package/swift/Package.swift +28 -0
- package/swift/Sources/AppleBridge/AppleBridge.swift +846 -0
- package/swift/Sources/AppleBridge/Calendar.swift +221 -0
- package/swift/Sources/AppleBridge/Contacts.swift +225 -0
- package/swift/Sources/AppleBridge/Doctor.swift +252 -0
- package/swift/Sources/AppleBridge/Files.swift +474 -0
- package/swift/Sources/AppleBridge/JSON.swift +57 -0
- package/swift/Sources/AppleBridge/Keynote.swift +599 -0
- package/swift/Sources/AppleBridge/Mail.swift +854 -0
- package/swift/Sources/AppleBridge/Notes.swift +263 -0
- package/swift/Sources/AppleBridge/Numbers.swift +601 -0
- package/swift/Sources/AppleBridge/Pages.swift +467 -0
- package/swift/Sources/AppleBridge/Reminders.swift +347 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
// Notes.app access uses AppleScript via osascript. Requires Automation
|
|
4
|
+
// permission (prompted on first call). Same pattern as Mail.swift.
|
|
5
|
+
|
|
6
|
+
enum NotesBridge {
|
|
7
|
+
|
|
8
|
+
// MARK: - Public API
|
|
9
|
+
|
|
10
|
+
static func listFolders() {
|
|
11
|
+
let script = """
|
|
12
|
+
tell application "Notes"
|
|
13
|
+
set resultList to {}
|
|
14
|
+
repeat with acct in every account
|
|
15
|
+
set acctName to name of acct
|
|
16
|
+
repeat with f in every folder of acct
|
|
17
|
+
set fName to name of f
|
|
18
|
+
set fCount to count of notes of f
|
|
19
|
+
set end of resultList to acctName & "|||" & fName & "|||" & (fCount as string)
|
|
20
|
+
end repeat
|
|
21
|
+
end repeat
|
|
22
|
+
return my joinList(resultList, "###")
|
|
23
|
+
end tell
|
|
24
|
+
|
|
25
|
+
on joinList(theList, delim)
|
|
26
|
+
set oldDelim to AppleScript's text item delimiters
|
|
27
|
+
set AppleScript's text item delimiters to delim
|
|
28
|
+
set theResult to theList as string
|
|
29
|
+
set AppleScript's text item delimiters to oldDelim
|
|
30
|
+
return theResult
|
|
31
|
+
end joinList
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
guard let raw = runAppleScript(script) else { return }
|
|
35
|
+
JSONOutput.success(parseFolderList(raw))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static func listNotes(folder: String?, account: String?, limit: Int) {
|
|
39
|
+
let target: String
|
|
40
|
+
if let f = folder, let a = account {
|
|
41
|
+
target = "folder \"\(escapeForAppleScript(f))\" of account \"\(escapeForAppleScript(a))\""
|
|
42
|
+
} else if let f = folder {
|
|
43
|
+
target = "folder \"\(escapeForAppleScript(f))\""
|
|
44
|
+
} else {
|
|
45
|
+
target = "every note"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let collect: String
|
|
49
|
+
if folder != nil {
|
|
50
|
+
collect = "set noteList to (every note of \(target))"
|
|
51
|
+
} else {
|
|
52
|
+
collect = "set noteList to \(target)"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let script = """
|
|
56
|
+
tell application "Notes"
|
|
57
|
+
set resultList to {}
|
|
58
|
+
\(collect)
|
|
59
|
+
set noteCount to count of noteList
|
|
60
|
+
set maxItems to \(limit)
|
|
61
|
+
if noteCount < maxItems then set maxItems to noteCount
|
|
62
|
+
repeat with i from 1 to maxItems
|
|
63
|
+
set n to item i of noteList
|
|
64
|
+
set nId to id of n
|
|
65
|
+
set nName to name of n
|
|
66
|
+
set nModified to (modification date of n) as «class isot» as string
|
|
67
|
+
set end of resultList to nId & "|||" & nName & "|||" & nModified
|
|
68
|
+
end repeat
|
|
69
|
+
return my joinList(resultList, "###") & "###TOTAL:::" & (noteCount as string)
|
|
70
|
+
end tell
|
|
71
|
+
|
|
72
|
+
on joinList(theList, delim)
|
|
73
|
+
set oldDelim to AppleScript's text item delimiters
|
|
74
|
+
set AppleScript's text item delimiters to delim
|
|
75
|
+
set theResult to theList as string
|
|
76
|
+
set AppleScript's text item delimiters to oldDelim
|
|
77
|
+
return theResult
|
|
78
|
+
end joinList
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
guard let raw = runAppleScript(script) else { return }
|
|
82
|
+
let parts = raw.components(separatedBy: "###TOTAL:::")
|
|
83
|
+
let total = parts.count > 1 ? Int(parts[1].trimmingCharacters(in: .whitespaces)) ?? 0 : 0
|
|
84
|
+
let notes = parseNoteList(parts[0])
|
|
85
|
+
JSONOutput.success([
|
|
86
|
+
"notes": notes,
|
|
87
|
+
"total": total,
|
|
88
|
+
"limit": limit,
|
|
89
|
+
"hasMore": notes.count < total
|
|
90
|
+
] as [String: Any])
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
static func search(query: String, limit: Int, searchIn: String) {
|
|
94
|
+
let escapedQuery = escapeForAppleScript(query)
|
|
95
|
+
let whereClause: String
|
|
96
|
+
switch searchIn {
|
|
97
|
+
case "title":
|
|
98
|
+
whereClause = "whose name contains searchQuery"
|
|
99
|
+
case "body":
|
|
100
|
+
whereClause = "whose plaintext contains searchQuery"
|
|
101
|
+
default:
|
|
102
|
+
whereClause = "whose name contains searchQuery or plaintext contains searchQuery"
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let script = """
|
|
106
|
+
tell application "Notes"
|
|
107
|
+
set searchQuery to "\(escapedQuery)"
|
|
108
|
+
set resultList to {}
|
|
109
|
+
set matches to (every note \(whereClause))
|
|
110
|
+
set matchCount to count of matches
|
|
111
|
+
set maxItems to \(limit)
|
|
112
|
+
if matchCount < maxItems then set maxItems to matchCount
|
|
113
|
+
repeat with i from 1 to maxItems
|
|
114
|
+
set n to item i of matches
|
|
115
|
+
set nId to id of n
|
|
116
|
+
set nName to name of n
|
|
117
|
+
set nModified to (modification date of n) as «class isot» as string
|
|
118
|
+
set end of resultList to nId & "|||" & nName & "|||" & nModified
|
|
119
|
+
end repeat
|
|
120
|
+
return my joinList(resultList, "###") & "###TOTAL:::" & (matchCount as string)
|
|
121
|
+
end tell
|
|
122
|
+
|
|
123
|
+
on joinList(theList, delim)
|
|
124
|
+
set oldDelim to AppleScript's text item delimiters
|
|
125
|
+
set AppleScript's text item delimiters to delim
|
|
126
|
+
set theResult to theList as string
|
|
127
|
+
set AppleScript's text item delimiters to oldDelim
|
|
128
|
+
return theResult
|
|
129
|
+
end joinList
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
guard let raw = runAppleScript(script) else { return }
|
|
133
|
+
let parts = raw.components(separatedBy: "###TOTAL:::")
|
|
134
|
+
let total = parts.count > 1 ? Int(parts[1].trimmingCharacters(in: .whitespaces)) ?? 0 : 0
|
|
135
|
+
let notes = parseNoteList(parts[0])
|
|
136
|
+
JSONOutput.success([
|
|
137
|
+
"notes": notes,
|
|
138
|
+
"total": total,
|
|
139
|
+
"limit": limit,
|
|
140
|
+
"hasMore": notes.count < total
|
|
141
|
+
] as [String: Any])
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
static func readNote(id: String, maxBodyLength: Int) {
|
|
145
|
+
let escapedId = escapeForAppleScript(id)
|
|
146
|
+
let script = """
|
|
147
|
+
tell application "Notes"
|
|
148
|
+
try
|
|
149
|
+
set n to note id "\(escapedId)"
|
|
150
|
+
on error
|
|
151
|
+
return "ERROR_NOT_FOUND"
|
|
152
|
+
end try
|
|
153
|
+
set nName to name of n
|
|
154
|
+
set nModified to (modification date of n) as «class isot» as string
|
|
155
|
+
set nCreated to (creation date of n) as «class isot» as string
|
|
156
|
+
set nBody to plaintext of n
|
|
157
|
+
return nName & "|||" & nCreated & "|||" & nModified & "|||" & nBody
|
|
158
|
+
end tell
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
guard let raw = runAppleScript(script) else { return }
|
|
162
|
+
if raw == "ERROR_NOT_FOUND" {
|
|
163
|
+
JSONOutput.error("Note not found: \(id)")
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
let parts = raw.components(separatedBy: "|||")
|
|
167
|
+
guard parts.count >= 4 else {
|
|
168
|
+
JSONOutput.error("Unexpected response format from Notes.app")
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
var body = parts[3]
|
|
172
|
+
if maxBodyLength > 0 && body.count > maxBodyLength {
|
|
173
|
+
body = String(body.prefix(maxBodyLength)) + "\n\n[truncated — \(body.count) chars total]"
|
|
174
|
+
}
|
|
175
|
+
JSONOutput.success([
|
|
176
|
+
"id": id,
|
|
177
|
+
"title": parts[0],
|
|
178
|
+
"created": parts[1],
|
|
179
|
+
"modified": parts[2],
|
|
180
|
+
"body": body
|
|
181
|
+
])
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// MARK: - AppleScript plumbing
|
|
185
|
+
|
|
186
|
+
private static func runAppleScript(_ script: String) -> String? {
|
|
187
|
+
let task = Process()
|
|
188
|
+
task.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
|
|
189
|
+
task.arguments = ["-e", script]
|
|
190
|
+
|
|
191
|
+
let outPipe = Pipe()
|
|
192
|
+
let errPipe = Pipe()
|
|
193
|
+
task.standardOutput = outPipe
|
|
194
|
+
task.standardError = errPipe
|
|
195
|
+
|
|
196
|
+
do {
|
|
197
|
+
try task.run()
|
|
198
|
+
task.waitUntilExit()
|
|
199
|
+
|
|
200
|
+
if task.terminationStatus != 0 {
|
|
201
|
+
let errData = errPipe.fileHandleForReading.readDataToEndOfFile()
|
|
202
|
+
let errStr = String(data: errData, encoding: .utf8)?
|
|
203
|
+
.trimmingCharacters(in: .whitespacesAndNewlines) ?? "Unknown error"
|
|
204
|
+
|
|
205
|
+
if errStr.contains("-1743") || errStr.contains("not allowed") {
|
|
206
|
+
JSONOutput.error("Notes automation permission denied. Grant access in System Settings > Privacy & Security > Automation > apple-bridge > Notes.")
|
|
207
|
+
} else if errStr.contains("-600") || errStr.contains("not running") {
|
|
208
|
+
JSONOutput.error("Notes.app is not running. Open Notes.app and try again.")
|
|
209
|
+
} else {
|
|
210
|
+
JSONOutput.error("AppleScript error: \(errStr)")
|
|
211
|
+
}
|
|
212
|
+
return nil
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let data = outPipe.fileHandleForReading.readDataToEndOfFile()
|
|
216
|
+
return String(data: data, encoding: .utf8)?
|
|
217
|
+
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
218
|
+
} catch {
|
|
219
|
+
JSONOutput.error("Failed to run osascript: \(error.localizedDescription)")
|
|
220
|
+
return nil
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private static func escapeForAppleScript(_ str: String) -> String {
|
|
225
|
+
return str
|
|
226
|
+
.replacingOccurrences(of: "\\", with: "\\\\")
|
|
227
|
+
.replacingOccurrences(of: "\"", with: "\\\"")
|
|
228
|
+
.replacingOccurrences(of: "\r\n", with: "\" & (ASCII character 13) & (ASCII character 10) & \"")
|
|
229
|
+
.replacingOccurrences(of: "\n", with: "\" & (ASCII character 10) & \"")
|
|
230
|
+
.replacingOccurrences(of: "\r", with: "\" & (ASCII character 13) & \"")
|
|
231
|
+
.replacingOccurrences(of: "\t", with: "\" & (ASCII character 9) & \"")
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// MARK: - Parsers
|
|
235
|
+
|
|
236
|
+
private static func parseFolderList(_ raw: String) -> [[String: Any]] {
|
|
237
|
+
guard !raw.isEmpty else { return [] }
|
|
238
|
+
let chunks = raw.components(separatedBy: "###")
|
|
239
|
+
return chunks.compactMap { chunk in
|
|
240
|
+
let parts = chunk.components(separatedBy: "|||")
|
|
241
|
+
guard parts.count >= 3 else { return nil }
|
|
242
|
+
return [
|
|
243
|
+
"account": parts[0].trimmingCharacters(in: .whitespaces),
|
|
244
|
+
"name": parts[1].trimmingCharacters(in: .whitespaces),
|
|
245
|
+
"noteCount": Int(parts[2].trimmingCharacters(in: .whitespaces)) ?? 0
|
|
246
|
+
]
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private static func parseNoteList(_ raw: String) -> [[String: Any]] {
|
|
251
|
+
guard !raw.isEmpty else { return [] }
|
|
252
|
+
let chunks = raw.components(separatedBy: "###")
|
|
253
|
+
return chunks.compactMap { chunk in
|
|
254
|
+
let parts = chunk.components(separatedBy: "|||")
|
|
255
|
+
guard parts.count >= 3 else { return nil }
|
|
256
|
+
return [
|
|
257
|
+
"id": parts[0],
|
|
258
|
+
"title": parts[1],
|
|
259
|
+
"modified": parts[2]
|
|
260
|
+
]
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|