@l22-io/orchard-mcp 0.3.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +11 -8
  2. package/build/bridge.js +18 -0
  3. package/build/bridge.js.map +1 -1
  4. package/build/index.js +11 -1
  5. package/build/index.js.map +1 -1
  6. package/build/tools/contacts.d.ts +2 -0
  7. package/build/tools/contacts.js +37 -0
  8. package/build/tools/contacts.js.map +1 -0
  9. package/build/tools/files.js +4 -1
  10. package/build/tools/files.js.map +1 -1
  11. package/build/tools/keynote.d.ts +2 -0
  12. package/build/tools/keynote.js +198 -0
  13. package/build/tools/keynote.js.map +1 -0
  14. package/build/tools/mail.js +57 -10
  15. package/build/tools/mail.js.map +1 -1
  16. package/build/tools/notes.d.ts +2 -0
  17. package/build/tools/notes.js +83 -0
  18. package/build/tools/notes.js.map +1 -0
  19. package/build/tools/numbers.d.ts +2 -0
  20. package/build/tools/numbers.js +167 -0
  21. package/build/tools/numbers.js.map +1 -0
  22. package/build/tools/pages.d.ts +2 -0
  23. package/build/tools/pages.js +143 -0
  24. package/build/tools/pages.js.map +1 -0
  25. package/package.json +11 -8
  26. package/scripts/postinstall.sh +77 -8
  27. package/swift/.build/AppleBridge.app/Contents/MacOS/apple-bridge +0 -0
  28. package/swift/.build/AppleBridge.app.sha256 +1 -0
  29. package/swift/Package.resolved +14 -0
  30. package/swift/Package.swift +28 -0
  31. package/swift/Sources/AppleBridge/AppleBridge.swift +846 -0
  32. package/swift/Sources/AppleBridge/Calendar.swift +221 -0
  33. package/swift/Sources/AppleBridge/Contacts.swift +225 -0
  34. package/swift/Sources/AppleBridge/Doctor.swift +252 -0
  35. package/swift/Sources/AppleBridge/Files.swift +474 -0
  36. package/swift/Sources/AppleBridge/JSON.swift +57 -0
  37. package/swift/Sources/AppleBridge/Keynote.swift +599 -0
  38. package/swift/Sources/AppleBridge/Mail.swift +794 -0
  39. package/swift/Sources/AppleBridge/Notes.swift +263 -0
  40. package/swift/Sources/AppleBridge/Numbers.swift +601 -0
  41. package/swift/Sources/AppleBridge/Pages.swift +467 -0
  42. 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
+ }