@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,467 @@
1
+ import Foundation
2
+
3
+ enum PagesBridge {
4
+
5
+ // MARK: - Info
6
+
7
+ static func info(file: String) {
8
+ guard let resolvedFile = FilesBridge.validatePath(file) else {
9
+ JSONOutput.error("Path is outside home directory or does not exist: \(file)")
10
+ return
11
+ }
12
+ let escaped = escapeForAppleScript(resolvedFile)
13
+ let script = """
14
+ tell application "Pages"
15
+ set doc to open POSIX file "\(escaped)"
16
+ set docName to name of doc
17
+ set bodyText to body text of doc
18
+ set wc to count of words of bodyText
19
+ set pc to count of pages of doc
20
+ close doc saving no
21
+ return docName & "|||" & (wc as string) & "|||" & (pc as string)
22
+ end tell
23
+ """
24
+
25
+ guard let raw = runAppleScript(script) else { return }
26
+ let parts = raw.components(separatedBy: "|||")
27
+ guard parts.count >= 3 else {
28
+ JSONOutput.error("Unexpected response format from Pages")
29
+ return
30
+ }
31
+
32
+ let result: [String: Any] = [
33
+ "name": parts[0],
34
+ "wordCount": Int(parts[1].trimmingCharacters(in: .whitespaces)) ?? 0,
35
+ "pageCount": Int(parts[2].trimmingCharacters(in: .whitespaces)) ?? 0,
36
+ "path": resolvedFile
37
+ ]
38
+ JSONOutput.success(result)
39
+ }
40
+
41
+ // MARK: - Read
42
+
43
+ static func read(file: String) {
44
+ guard let resolvedFile = FilesBridge.validatePath(file) else {
45
+ JSONOutput.error("Path is outside home directory or does not exist: \(file)")
46
+ return
47
+ }
48
+ let escaped = escapeForAppleScript(resolvedFile)
49
+ let script = """
50
+ tell application "Pages"
51
+ set doc to open POSIX file "\(escaped)"
52
+ set bodyText to body text of doc
53
+ set wc to count of words of bodyText
54
+ set pc to count of pages of doc
55
+ close doc saving no
56
+ return bodyText & "|||" & (wc as string) & "|||" & (pc as string)
57
+ end tell
58
+ """
59
+
60
+ guard let raw = runAppleScript(script) else { return }
61
+ let parts = raw.components(separatedBy: "|||")
62
+ guard parts.count >= 3 else {
63
+ JSONOutput.error("Unexpected response format from Pages")
64
+ return
65
+ }
66
+
67
+ let result: [String: Any] = [
68
+ "text": parts[0],
69
+ "wordCount": Int(parts[1].trimmingCharacters(in: .whitespaces)) ?? 0,
70
+ "pageCount": Int(parts[2].trimmingCharacters(in: .whitespaces)) ?? 0,
71
+ "path": resolvedFile
72
+ ]
73
+ JSONOutput.success(result)
74
+ }
75
+
76
+ // MARK: - Write
77
+
78
+ static func write(file: String, text: String) {
79
+ guard let resolvedFile = FilesBridge.validatePath(file) else {
80
+ JSONOutput.error("Path is outside home directory or does not exist: \(file)")
81
+ return
82
+ }
83
+ let escapedFile = escapeForAppleScript(resolvedFile)
84
+ let escapedText = escapeForAppleScript(text)
85
+ let script = """
86
+ tell application "Pages"
87
+ set doc to open POSIX file "\(escapedFile)"
88
+ set body text of doc to "\(escapedText)"
89
+ save doc
90
+ close doc
91
+ return "ok"
92
+ end tell
93
+ """
94
+
95
+ guard let _ = runAppleScript(script) else { return }
96
+ JSONOutput.success(["path": resolvedFile, "written": true])
97
+ }
98
+
99
+ // MARK: - Create
100
+
101
+ static func create(file: String, text: String?, template: String?) {
102
+ guard let resolvedFile = FilesBridge.validatePath(file, mustExist: false) else {
103
+ JSONOutput.error("Path is outside home directory: \(file)")
104
+ return
105
+ }
106
+ let escaped = escapeForAppleScript(resolvedFile)
107
+
108
+ let templateClause: String
109
+ if let template = template {
110
+ templateClause = "set doc to make new document with properties {document template:template \"\(escapeForAppleScript(template))\"}"
111
+ } else {
112
+ templateClause = "set doc to make new document"
113
+ }
114
+
115
+ let textClause: String
116
+ if let text = text, !text.isEmpty {
117
+ textClause = """
118
+ set body text of doc to "\(escapeForAppleScript(text))"
119
+ """
120
+ } else {
121
+ textClause = ""
122
+ }
123
+
124
+ let script = """
125
+ tell application "Pages"
126
+ \(templateClause)
127
+ \(textClause)
128
+ set docPath to POSIX file "\(escaped)"
129
+ save doc in docPath
130
+ close doc
131
+ return "ok"
132
+ end tell
133
+ """
134
+
135
+ guard let _ = runAppleScript(script) else { return }
136
+ JSONOutput.success(["path": resolvedFile, "created": true])
137
+ }
138
+
139
+ // MARK: - Find & Replace
140
+
141
+ static func findReplace(file: String, find: String, replace: String, all: Bool) {
142
+ guard let resolvedFile = FilesBridge.validatePath(file) else {
143
+ JSONOutput.error("Path is outside home directory or does not exist: \(file)")
144
+ return
145
+ }
146
+ let escapedFile = escapeForAppleScript(resolvedFile)
147
+ let escapedFind = escapeForAppleScript(find)
148
+ let escapedReplace = escapeForAppleScript(replace)
149
+
150
+ let replaceLogic: String
151
+ if all {
152
+ replaceLogic = """
153
+ set AppleScript's text item delimiters to "\(escapedFind)"
154
+ set textItems to text items of bodyText
155
+ set matchCount to (count of textItems) - 1
156
+ set AppleScript's text item delimiters to "\(escapedReplace)"
157
+ set newText to textItems as string
158
+ set AppleScript's text item delimiters to oldDelim
159
+ """
160
+ } else {
161
+ replaceLogic = """
162
+ set AppleScript's text item delimiters to "\(escapedFind)"
163
+ set textItems to text items of bodyText
164
+ if (count of textItems) > 1 then
165
+ set matchCount to 1
166
+ set firstPart to item 1 of textItems
167
+ set restItems to items 2 thru -1 of textItems
168
+ set AppleScript's text item delimiters to "\(escapedFind)"
169
+ set restText to restItems as string
170
+ set AppleScript's text item delimiters to oldDelim
171
+ set newText to firstPart & "\(escapedReplace)" & restText
172
+ else
173
+ set matchCount to 0
174
+ set newText to bodyText
175
+ end if
176
+ set AppleScript's text item delimiters to oldDelim
177
+ """
178
+ }
179
+
180
+ let script = """
181
+ tell application "Pages"
182
+ set doc to open POSIX file "\(escapedFile)"
183
+ set bodyText to body text of doc
184
+ end tell
185
+ set oldDelim to AppleScript's text item delimiters
186
+ set matchCount to 0
187
+ \(replaceLogic)
188
+ tell application "Pages"
189
+ set body text of doc to newText
190
+ save doc
191
+ close doc
192
+ return matchCount as string
193
+ end tell
194
+ """
195
+
196
+ guard let raw = runAppleScript(script) else { return }
197
+ let count = Int(raw.trimmingCharacters(in: .whitespacesAndNewlines)) ?? 0
198
+ JSONOutput.success(["replacements": count, "path": resolvedFile])
199
+ }
200
+
201
+ // MARK: - Insert Table
202
+
203
+ static func insertTable(file: String, dataJSON: String) {
204
+ guard let resolvedFile = FilesBridge.validatePath(file) else {
205
+ JSONOutput.error("Path is outside home directory or does not exist: \(file)")
206
+ return
207
+ }
208
+ let escapedFile = escapeForAppleScript(resolvedFile)
209
+
210
+ guard let jsonData = dataJSON.data(using: .utf8),
211
+ let parsed = try? JSONSerialization.jsonObject(with: jsonData) as? [[Any]] else {
212
+ JSONOutput.error("Invalid data JSON: expected array of arrays")
213
+ return
214
+ }
215
+
216
+ let rowCount = parsed.count
217
+ guard rowCount > 0 else {
218
+ JSONOutput.error("Data must contain at least one row")
219
+ return
220
+ }
221
+ let colCount = parsed[0].count
222
+
223
+ var cellScripts: [String] = []
224
+ for (r, row) in parsed.enumerated() {
225
+ for (c, val) in row.enumerated() {
226
+ let cellVal: String
227
+ if let num = val as? NSNumber {
228
+ cellVal = "\(num)"
229
+ } else if let str = val as? String {
230
+ cellVal = "\"\(escapeForAppleScript(str))\""
231
+ } else {
232
+ cellVal = "\"\(escapeForAppleScript(String(describing: val)))\""
233
+ }
234
+ cellScripts.append("set value of cell \(c + 1) of row \(r + 1) of newTable to \(cellVal)")
235
+ }
236
+ }
237
+
238
+ let cellBlock = cellScripts.joined(separator: "\n ")
239
+
240
+ let script = """
241
+ tell application "Pages"
242
+ set doc to open POSIX file "\(escapedFile)"
243
+ tell doc
244
+ set newTable to make new table with properties {row count:\(rowCount), column count:\(colCount)}
245
+ \(cellBlock)
246
+ end tell
247
+ save doc
248
+ close doc
249
+ return "ok"
250
+ end tell
251
+ """
252
+
253
+ guard let _ = runAppleScript(script) else { return }
254
+ JSONOutput.success([
255
+ "path": resolvedFile,
256
+ "rows": rowCount,
257
+ "columns": colCount,
258
+ "inserted": true
259
+ ])
260
+ }
261
+
262
+ // MARK: - List Sections
263
+
264
+ static func listSections(file: String) {
265
+ guard let resolvedFile = FilesBridge.validatePath(file) else {
266
+ JSONOutput.error("Path is outside home directory or does not exist: \(file)")
267
+ return
268
+ }
269
+ let escaped = escapeForAppleScript(resolvedFile)
270
+ let script = """
271
+ tell application "Pages"
272
+ set doc to open POSIX file "\(escaped)"
273
+ set sectionList to {}
274
+ repeat with s in sections of doc
275
+ set sBody to body text of s
276
+ set sWords to word count of s
277
+ set preview to ""
278
+ if (count of characters of sBody) > 200 then
279
+ set preview to text 1 thru 200 of sBody
280
+ else
281
+ set preview to sBody
282
+ end if
283
+ set end of sectionList to preview & "|||" & (sWords as string)
284
+ end repeat
285
+ close doc saving no
286
+ set oldDelim to AppleScript's text item delimiters
287
+ set AppleScript's text item delimiters to "###"
288
+ set resultText to sectionList as string
289
+ set AppleScript's text item delimiters to oldDelim
290
+ return resultText
291
+ end tell
292
+ """
293
+
294
+ guard let raw = runAppleScript(script) else { return }
295
+ let sections = parseSectionList(raw)
296
+ JSONOutput.success(sections)
297
+ }
298
+
299
+ // MARK: - Export
300
+
301
+ static func export(file: String, format: String, dest: String?) {
302
+ guard let resolvedFile = FilesBridge.validatePath(file) else {
303
+ JSONOutput.error("Path is outside home directory or does not exist: \(file)")
304
+ return
305
+ }
306
+ let escapedFile = escapeForAppleScript(resolvedFile)
307
+ let outputPath: String
308
+ if let dest = dest {
309
+ guard let resolvedDest = FilesBridge.validatePath(dest, mustExist: false) else {
310
+ JSONOutput.error("Destination path is outside home directory: \(dest)")
311
+ return
312
+ }
313
+ outputPath = resolvedDest
314
+ } else {
315
+ let ext: String
316
+ switch format {
317
+ case "pdf": ext = "pdf"
318
+ case "docx": ext = "docx"
319
+ case "txt": ext = "txt"
320
+ case "epub": ext = "epub"
321
+ default: ext = format
322
+ }
323
+ outputPath = resolvedFile.replacingOccurrences(of: ".pages", with: ".\(ext)")
324
+ }
325
+ let escapedOutput = escapeForAppleScript(outputPath)
326
+
327
+ let formatMap: [String: String] = [
328
+ "pdf": "PDF",
329
+ "docx": "Microsoft Word",
330
+ "txt": "unformatted text",
331
+ "epub": "EPUB"
332
+ ]
333
+ guard let pagesFormat = formatMap[format] else {
334
+ JSONOutput.error("Unsupported export format: \(format). Use pdf, docx, txt, or epub.")
335
+ return
336
+ }
337
+
338
+ let script = """
339
+ tell application "Pages"
340
+ set doc to open POSIX file "\(escapedFile)"
341
+ export doc to POSIX file "\(escapedOutput)" as \(pagesFormat)
342
+ close doc saving no
343
+ return "ok"
344
+ end tell
345
+ """
346
+
347
+ guard let _ = runAppleScript(script) else { return }
348
+ JSONOutput.success(["path": outputPath])
349
+ }
350
+
351
+ // MARK: - Search
352
+
353
+ static func search(query: String, limit: Int) {
354
+ let sanitized = query.replacingOccurrences(of: "'", with: "")
355
+ .replacingOccurrences(of: "\\", with: "")
356
+ let task = Process()
357
+ task.executableURL = URL(fileURLWithPath: "/usr/bin/mdfind")
358
+ task.arguments = [
359
+ "kMDItemContentType == 'com.apple.iWork.pages.sffpages' && kMDItemDisplayName == '*\(sanitized)*'cd"
360
+ ]
361
+
362
+ let pipe = Pipe()
363
+ task.standardOutput = pipe
364
+ task.standardError = Pipe()
365
+
366
+ do {
367
+ try task.run()
368
+ task.waitUntilExit()
369
+
370
+ let data = pipe.fileHandleForReading.readDataToEndOfFile()
371
+ guard let output = String(data: data, encoding: .utf8) else {
372
+ JSONOutput.success([])
373
+ return
374
+ }
375
+
376
+ let paths = output.components(separatedBy: "\n")
377
+ .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
378
+ .filter { !$0.isEmpty }
379
+ .prefix(limit)
380
+
381
+ let fm = FileManager.default
382
+ let results: [[String: Any]] = paths.compactMap { path in
383
+ guard let attrs = try? fm.attributesOfItem(atPath: path) else { return nil }
384
+ var entry: [String: Any] = [
385
+ "path": path,
386
+ "name": URL(fileURLWithPath: path).lastPathComponent
387
+ ]
388
+ if let size = attrs[.size] as? Int {
389
+ entry["size"] = size
390
+ }
391
+ if let modified = attrs[.modificationDate] as? Date {
392
+ entry["modified"] = iso8601(modified)
393
+ }
394
+ return entry
395
+ }
396
+
397
+ JSONOutput.success(results)
398
+ } catch {
399
+ JSONOutput.error("Spotlight search failed: \(error.localizedDescription)")
400
+ }
401
+ }
402
+
403
+ // MARK: - AppleScript Execution
404
+
405
+ private static func runAppleScript(_ script: String) -> String? {
406
+ let task = Process()
407
+ task.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
408
+ task.arguments = ["-e", script]
409
+
410
+ let outPipe = Pipe()
411
+ let errPipe = Pipe()
412
+ task.standardOutput = outPipe
413
+ task.standardError = errPipe
414
+
415
+ do {
416
+ try task.run()
417
+ task.waitUntilExit()
418
+
419
+ if task.terminationStatus != 0 {
420
+ let errData = errPipe.fileHandleForReading.readDataToEndOfFile()
421
+ let errStr = String(data: errData, encoding: .utf8)?
422
+ .trimmingCharacters(in: .whitespacesAndNewlines) ?? "Unknown error"
423
+
424
+ if errStr.contains("-1743") || errStr.contains("not allowed") {
425
+ JSONOutput.error("Pages automation permission denied. Grant access in System Settings > Privacy & Security > Automation.")
426
+ } else if errStr.contains("-600") || errStr.contains("not running") {
427
+ JSONOutput.error("Pages is not running. It will be launched automatically on next attempt.")
428
+ } else {
429
+ JSONOutput.error("AppleScript error: \(errStr)")
430
+ }
431
+ return nil
432
+ }
433
+
434
+ let data = outPipe.fileHandleForReading.readDataToEndOfFile()
435
+ return String(data: data, encoding: .utf8)?
436
+ .trimmingCharacters(in: .whitespacesAndNewlines)
437
+ } catch {
438
+ JSONOutput.error("Failed to run osascript: \(error.localizedDescription)")
439
+ return nil
440
+ }
441
+ }
442
+
443
+ private static func escapeForAppleScript(_ str: String) -> String {
444
+ return str.replacingOccurrences(of: "\\", with: "\\\\")
445
+ .replacingOccurrences(of: "\"", with: "\\\"")
446
+ .replacingOccurrences(of: "\n", with: "\\n")
447
+ .replacingOccurrences(of: "\r", with: "\\r")
448
+ .replacingOccurrences(of: "\t", with: "\\t")
449
+ }
450
+
451
+ // MARK: - Parsers
452
+
453
+ private static func parseSectionList(_ raw: String) -> [[String: Any]] {
454
+ guard !raw.isEmpty else { return [] }
455
+ let sectionChunks = raw.components(separatedBy: "###")
456
+ return sectionChunks.enumerated().compactMap { (index, chunk) -> [String: Any]? in
457
+ let parts = chunk.components(separatedBy: "|||")
458
+ guard parts.count >= 2 else { return nil }
459
+
460
+ return [
461
+ "index": index,
462
+ "preview": parts[0].trimmingCharacters(in: .whitespaces),
463
+ "wordCount": Int(parts[1].trimmingCharacters(in: .whitespaces)) ?? 0
464
+ ]
465
+ }
466
+ }
467
+ }