@l22-io/orchard-mcp 0.3.1 → 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.
- package/README.md +11 -8
- package/build/bridge.js +18 -0
- 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 +57 -10
- 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 +14 -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 +794 -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,599 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
enum KeynoteBridge {
|
|
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 "Keynote"
|
|
15
|
+
set doc to open POSIX file "\(escaped)"
|
|
16
|
+
set docName to name of doc
|
|
17
|
+
set sc to count of slides of doc
|
|
18
|
+
set themeName to name of document theme of doc
|
|
19
|
+
close doc saving no
|
|
20
|
+
return docName & "|||" & (sc as string) & "|||" & themeName
|
|
21
|
+
end tell
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
guard let raw = runAppleScript(script) else { return }
|
|
25
|
+
let parts = raw.components(separatedBy: "|||")
|
|
26
|
+
guard parts.count >= 3 else {
|
|
27
|
+
JSONOutput.error("Unexpected response format from Keynote")
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let result: [String: Any] = [
|
|
32
|
+
"name": parts[0].trimmingCharacters(in: .whitespaces),
|
|
33
|
+
"slideCount": Int(parts[1].trimmingCharacters(in: .whitespaces)) ?? 0,
|
|
34
|
+
"theme": parts[2].trimmingCharacters(in: .whitespaces),
|
|
35
|
+
"path": resolvedFile
|
|
36
|
+
]
|
|
37
|
+
JSONOutput.success(result)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// MARK: - Read
|
|
41
|
+
|
|
42
|
+
static func read(file: String, slideIndex: Int?) {
|
|
43
|
+
guard let resolvedFile = FilesBridge.validatePath(file) else {
|
|
44
|
+
JSONOutput.error("Path is outside home directory or does not exist: \(file)")
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
let escaped = escapeForAppleScript(resolvedFile)
|
|
48
|
+
|
|
49
|
+
let slideScript: String
|
|
50
|
+
if let idx = slideIndex {
|
|
51
|
+
slideScript = """
|
|
52
|
+
set slideList to {slide \(idx) of doc}
|
|
53
|
+
"""
|
|
54
|
+
} else {
|
|
55
|
+
slideScript = """
|
|
56
|
+
set slideList to every slide of doc
|
|
57
|
+
"""
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let script = """
|
|
61
|
+
tell application "Keynote"
|
|
62
|
+
set doc to open POSIX file "\(escaped)"
|
|
63
|
+
\(slideScript)
|
|
64
|
+
set resultParts to {}
|
|
65
|
+
set slideIdx to 0
|
|
66
|
+
repeat with s in slideList
|
|
67
|
+
set slideIdx to slideIdx + 1
|
|
68
|
+
set actualIdx to slideIdx
|
|
69
|
+
\(slideIndex != nil ? "set actualIdx to \(slideIndex!)" : "")
|
|
70
|
+
set slideTitle to ""
|
|
71
|
+
try
|
|
72
|
+
set slideTitle to object text of default title item of s
|
|
73
|
+
end try
|
|
74
|
+
set slideBody to ""
|
|
75
|
+
try
|
|
76
|
+
set slideBody to object text of default body item of s
|
|
77
|
+
end try
|
|
78
|
+
set slideNotes to presenter notes of s
|
|
79
|
+
set slideLayout to name of base slide of s
|
|
80
|
+
set slideSkipped to skipped of s
|
|
81
|
+
set slideLine to (actualIdx as string) & "|||" & slideTitle & "|||" & slideBody & "|||" & slideNotes & "|||" & slideLayout & "|||" & (slideSkipped as string)
|
|
82
|
+
set end of resultParts to slideLine
|
|
83
|
+
end repeat
|
|
84
|
+
close doc saving no
|
|
85
|
+
set oldDelim to AppleScript's text item delimiters
|
|
86
|
+
set AppleScript's text item delimiters to "###"
|
|
87
|
+
set resultText to resultParts as string
|
|
88
|
+
set AppleScript's text item delimiters to oldDelim
|
|
89
|
+
return resultText
|
|
90
|
+
end tell
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
guard let raw = runAppleScript(script) else { return }
|
|
94
|
+
let slides = parseSlideList(raw)
|
|
95
|
+
JSONOutput.success(slides)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// MARK: - Create
|
|
99
|
+
|
|
100
|
+
static func create(file: String, theme: String?) {
|
|
101
|
+
guard let resolvedFile = FilesBridge.validatePath(file, mustExist: false) else {
|
|
102
|
+
JSONOutput.error("Path is outside home directory: \(file)")
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
let escaped = escapeForAppleScript(resolvedFile)
|
|
106
|
+
|
|
107
|
+
let themeClause: String
|
|
108
|
+
if let theme = theme {
|
|
109
|
+
themeClause = "set doc to make new document with properties {document theme:theme \"\(escapeForAppleScript(theme))\"}"
|
|
110
|
+
} else {
|
|
111
|
+
themeClause = "set doc to make new document"
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let script = """
|
|
115
|
+
tell application "Keynote"
|
|
116
|
+
\(themeClause)
|
|
117
|
+
set docPath to POSIX file "\(escaped)"
|
|
118
|
+
save doc in docPath
|
|
119
|
+
close doc
|
|
120
|
+
return "ok"
|
|
121
|
+
end tell
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
guard let _ = runAppleScript(script) else { return }
|
|
125
|
+
JSONOutput.success(["path": resolvedFile, "created": true])
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// MARK: - Add Slide
|
|
129
|
+
|
|
130
|
+
static func addSlide(file: String, layout: String?, title: String?, body: String?, notes: String?, position: Int?) {
|
|
131
|
+
guard let resolvedFile = FilesBridge.validatePath(file) else {
|
|
132
|
+
JSONOutput.error("Path is outside home directory or does not exist: \(file)")
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
let escaped = escapeForAppleScript(resolvedFile)
|
|
136
|
+
|
|
137
|
+
let makeClause: String
|
|
138
|
+
if let pos = position {
|
|
139
|
+
makeClause = "set newSlide to make new slide at after slide \(pos) of doc"
|
|
140
|
+
} else {
|
|
141
|
+
makeClause = "set newSlide to make new slide at end of doc"
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let layoutClause: String
|
|
145
|
+
if let layout = layout {
|
|
146
|
+
layoutClause = """
|
|
147
|
+
try
|
|
148
|
+
set base slide of newSlide to base slide "\(escapeForAppleScript(layout))" of document theme of doc
|
|
149
|
+
end try
|
|
150
|
+
"""
|
|
151
|
+
} else {
|
|
152
|
+
layoutClause = ""
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let titleClause: String
|
|
156
|
+
if let title = title {
|
|
157
|
+
titleClause = """
|
|
158
|
+
try
|
|
159
|
+
set object text of default title item of newSlide to "\(escapeForAppleScript(title))"
|
|
160
|
+
end try
|
|
161
|
+
"""
|
|
162
|
+
} else {
|
|
163
|
+
titleClause = ""
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let bodyClause: String
|
|
167
|
+
if let body = body {
|
|
168
|
+
bodyClause = """
|
|
169
|
+
try
|
|
170
|
+
set object text of default body item of newSlide to "\(escapeForAppleScript(body))"
|
|
171
|
+
end try
|
|
172
|
+
"""
|
|
173
|
+
} else {
|
|
174
|
+
bodyClause = ""
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let notesClause: String
|
|
178
|
+
if let notes = notes {
|
|
179
|
+
notesClause = """
|
|
180
|
+
set presenter notes of newSlide to "\(escapeForAppleScript(notes))"
|
|
181
|
+
"""
|
|
182
|
+
} else {
|
|
183
|
+
notesClause = ""
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let script = """
|
|
187
|
+
tell application "Keynote"
|
|
188
|
+
set doc to open POSIX file "\(escaped)"
|
|
189
|
+
\(makeClause)
|
|
190
|
+
\(layoutClause)
|
|
191
|
+
\(titleClause)
|
|
192
|
+
\(bodyClause)
|
|
193
|
+
\(notesClause)
|
|
194
|
+
set sc to count of slides of doc
|
|
195
|
+
save doc
|
|
196
|
+
close doc
|
|
197
|
+
return sc as string
|
|
198
|
+
end tell
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
guard let raw = runAppleScript(script) else { return }
|
|
202
|
+
let slideCount = Int(raw.trimmingCharacters(in: .whitespacesAndNewlines)) ?? 0
|
|
203
|
+
JSONOutput.success(["path": resolvedFile, "slideCount": slideCount])
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// MARK: - Edit Slide
|
|
207
|
+
|
|
208
|
+
static func editSlide(file: String, slideIndex: Int, title: String?, body: String?, notes: String?) {
|
|
209
|
+
guard let resolvedFile = FilesBridge.validatePath(file) else {
|
|
210
|
+
JSONOutput.error("Path is outside home directory or does not exist: \(file)")
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
let escaped = escapeForAppleScript(resolvedFile)
|
|
214
|
+
|
|
215
|
+
let titleClause: String
|
|
216
|
+
if let title = title {
|
|
217
|
+
titleClause = """
|
|
218
|
+
try
|
|
219
|
+
set object text of default title item of s to "\(escapeForAppleScript(title))"
|
|
220
|
+
end try
|
|
221
|
+
"""
|
|
222
|
+
} else {
|
|
223
|
+
titleClause = ""
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let bodyClause: String
|
|
227
|
+
if let body = body {
|
|
228
|
+
bodyClause = """
|
|
229
|
+
try
|
|
230
|
+
set object text of default body item of s to "\(escapeForAppleScript(body))"
|
|
231
|
+
end try
|
|
232
|
+
"""
|
|
233
|
+
} else {
|
|
234
|
+
bodyClause = ""
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let notesClause: String
|
|
238
|
+
if let notes = notes {
|
|
239
|
+
notesClause = """
|
|
240
|
+
set presenter notes of s to "\(escapeForAppleScript(notes))"
|
|
241
|
+
"""
|
|
242
|
+
} else {
|
|
243
|
+
notesClause = ""
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
let script = """
|
|
247
|
+
tell application "Keynote"
|
|
248
|
+
set doc to open POSIX file "\(escaped)"
|
|
249
|
+
set s to slide \(slideIndex) of doc
|
|
250
|
+
\(titleClause)
|
|
251
|
+
\(bodyClause)
|
|
252
|
+
\(notesClause)
|
|
253
|
+
save doc
|
|
254
|
+
close doc
|
|
255
|
+
return "ok"
|
|
256
|
+
end tell
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
guard let _ = runAppleScript(script) else { return }
|
|
260
|
+
JSONOutput.success(["path": resolvedFile, "slide": slideIndex, "edited": true])
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// MARK: - Remove Slide
|
|
264
|
+
|
|
265
|
+
static func removeSlide(file: String, slideIndex: Int) {
|
|
266
|
+
guard let resolvedFile = FilesBridge.validatePath(file) else {
|
|
267
|
+
JSONOutput.error("Path is outside home directory or does not exist: \(file)")
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
let escaped = escapeForAppleScript(resolvedFile)
|
|
271
|
+
let script = """
|
|
272
|
+
tell application "Keynote"
|
|
273
|
+
set doc to open POSIX file "\(escaped)"
|
|
274
|
+
delete slide \(slideIndex) of doc
|
|
275
|
+
set sc to count of slides of doc
|
|
276
|
+
save doc
|
|
277
|
+
close doc
|
|
278
|
+
return sc as string
|
|
279
|
+
end tell
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
guard let raw = runAppleScript(script) else { return }
|
|
283
|
+
let remaining = Int(raw.trimmingCharacters(in: .whitespacesAndNewlines)) ?? 0
|
|
284
|
+
JSONOutput.success(["path": resolvedFile, "remainingSlides": remaining])
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// MARK: - Reorder Slides
|
|
288
|
+
|
|
289
|
+
static func reorderSlides(file: String, from: Int, to: Int) {
|
|
290
|
+
guard let resolvedFile = FilesBridge.validatePath(file) else {
|
|
291
|
+
JSONOutput.error("Path is outside home directory or does not exist: \(file)")
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
let escaped = escapeForAppleScript(resolvedFile)
|
|
295
|
+
|
|
296
|
+
let moveClause: String
|
|
297
|
+
if to < from {
|
|
298
|
+
moveClause = "move slide \(from) of doc to before slide \(to) of doc"
|
|
299
|
+
} else {
|
|
300
|
+
moveClause = "move slide \(from) of doc to after slide \(to) of doc"
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
let script = """
|
|
304
|
+
tell application "Keynote"
|
|
305
|
+
set doc to open POSIX file "\(escaped)"
|
|
306
|
+
\(moveClause)
|
|
307
|
+
save doc
|
|
308
|
+
close doc
|
|
309
|
+
return "ok"
|
|
310
|
+
end tell
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
guard let _ = runAppleScript(script) else { return }
|
|
314
|
+
JSONOutput.success(["path": resolvedFile, "movedFrom": from, "movedTo": to])
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// MARK: - List Themes
|
|
318
|
+
|
|
319
|
+
static func listThemes() {
|
|
320
|
+
let script = """
|
|
321
|
+
tell application "Keynote"
|
|
322
|
+
set themeNames to name of every theme
|
|
323
|
+
set oldDelim to AppleScript's text item delimiters
|
|
324
|
+
set AppleScript's text item delimiters to "|||"
|
|
325
|
+
set resultText to themeNames as string
|
|
326
|
+
set AppleScript's text item delimiters to oldDelim
|
|
327
|
+
return resultText
|
|
328
|
+
end tell
|
|
329
|
+
"""
|
|
330
|
+
|
|
331
|
+
guard let raw = runAppleScript(script) else { return }
|
|
332
|
+
let themes = raw.components(separatedBy: "|||")
|
|
333
|
+
.map { $0.trimmingCharacters(in: .whitespaces) }
|
|
334
|
+
.filter { !$0.isEmpty }
|
|
335
|
+
JSONOutput.success(themes)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// MARK: - Export
|
|
339
|
+
|
|
340
|
+
static func export(file: String, format: String, dest: String?, slideIndex: Int?) {
|
|
341
|
+
guard let resolvedFile = FilesBridge.validatePath(file) else {
|
|
342
|
+
JSONOutput.error("Path is outside home directory or does not exist: \(file)")
|
|
343
|
+
return
|
|
344
|
+
}
|
|
345
|
+
let escapedFile = escapeForAppleScript(resolvedFile)
|
|
346
|
+
|
|
347
|
+
let isImageExport = (format == "png" || format == "jpeg")
|
|
348
|
+
|
|
349
|
+
if isImageExport && slideIndex == nil {
|
|
350
|
+
exportSlideImages(file: resolvedFile, format: format, dest: dest)
|
|
351
|
+
return
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
let outputPath: String
|
|
355
|
+
if let dest = dest {
|
|
356
|
+
guard let resolvedDest = FilesBridge.validatePath(dest, mustExist: false) else {
|
|
357
|
+
JSONOutput.error("Destination path is outside home directory: \(dest)")
|
|
358
|
+
return
|
|
359
|
+
}
|
|
360
|
+
outputPath = resolvedDest
|
|
361
|
+
} else {
|
|
362
|
+
let ext: String
|
|
363
|
+
switch format {
|
|
364
|
+
case "pdf": ext = "pdf"
|
|
365
|
+
case "pptx": ext = "pptx"
|
|
366
|
+
case "png": ext = "png"
|
|
367
|
+
case "jpeg": ext = "jpeg"
|
|
368
|
+
default: ext = format
|
|
369
|
+
}
|
|
370
|
+
outputPath = resolvedFile.replacingOccurrences(of: ".key", with: ".\(ext)")
|
|
371
|
+
}
|
|
372
|
+
let escapedOutput = escapeForAppleScript(outputPath)
|
|
373
|
+
|
|
374
|
+
let formatMap: [String: String] = [
|
|
375
|
+
"pdf": "PDF",
|
|
376
|
+
"pptx": "Microsoft PowerPoint",
|
|
377
|
+
"png": "PNG",
|
|
378
|
+
"jpeg": "JPEG"
|
|
379
|
+
]
|
|
380
|
+
guard let keynoteFormat = formatMap[format] else {
|
|
381
|
+
JSONOutput.error("Unsupported export format: \(format). Use pdf, pptx, png, or jpeg.")
|
|
382
|
+
return
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if isImageExport, let idx = slideIndex {
|
|
386
|
+
let script = """
|
|
387
|
+
tell application "Keynote"
|
|
388
|
+
set doc to open POSIX file "\(escapedFile)"
|
|
389
|
+
export doc as slide images to POSIX file "\(escapedOutput)" with properties {image format:\(keynoteFormat), skipped slides:false}
|
|
390
|
+
close doc saving no
|
|
391
|
+
return "ok"
|
|
392
|
+
end tell
|
|
393
|
+
"""
|
|
394
|
+
guard let _ = runAppleScript(script) else { return }
|
|
395
|
+
|
|
396
|
+
let fm = FileManager.default
|
|
397
|
+
let dirURL = URL(fileURLWithPath: outputPath).deletingLastPathComponent()
|
|
398
|
+
let ext = format == "jpeg" ? "jpeg" : "png"
|
|
399
|
+
let pattern = dirURL.path
|
|
400
|
+
if let files = try? fm.contentsOfDirectory(atPath: pattern) {
|
|
401
|
+
let sorted = files.filter { $0.hasSuffix(ext) }.sorted()
|
|
402
|
+
if idx > 0 && idx <= sorted.count {
|
|
403
|
+
let targetFile = dirURL.appendingPathComponent(sorted[idx - 1]).path
|
|
404
|
+
JSONOutput.success(["path": targetFile])
|
|
405
|
+
return
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
JSONOutput.success(["path": outputPath])
|
|
409
|
+
return
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
let script = """
|
|
413
|
+
tell application "Keynote"
|
|
414
|
+
set doc to open POSIX file "\(escapedFile)"
|
|
415
|
+
export doc to POSIX file "\(escapedOutput)" as \(keynoteFormat)
|
|
416
|
+
close doc saving no
|
|
417
|
+
return "ok"
|
|
418
|
+
end tell
|
|
419
|
+
"""
|
|
420
|
+
|
|
421
|
+
guard let _ = runAppleScript(script) else { return }
|
|
422
|
+
JSONOutput.success(["path": outputPath])
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private static func exportSlideImages(file: String, format: String, dest: String?) {
|
|
426
|
+
let escapedFile = escapeForAppleScript(file)
|
|
427
|
+
let outputDir: String
|
|
428
|
+
if let dest = dest {
|
|
429
|
+
guard let resolvedDest = FilesBridge.validatePath(dest, mustExist: false) else {
|
|
430
|
+
JSONOutput.error("Destination path is outside home directory: \(dest)")
|
|
431
|
+
return
|
|
432
|
+
}
|
|
433
|
+
outputDir = resolvedDest
|
|
434
|
+
} else {
|
|
435
|
+
let base = file.replacingOccurrences(of: ".key", with: "_slides")
|
|
436
|
+
outputDir = base
|
|
437
|
+
}
|
|
438
|
+
let escapedOutput = escapeForAppleScript(outputDir)
|
|
439
|
+
|
|
440
|
+
let imageFormat = format == "jpeg" ? "JPEG" : "PNG"
|
|
441
|
+
|
|
442
|
+
let fm = FileManager.default
|
|
443
|
+
if !fm.fileExists(atPath: outputDir) {
|
|
444
|
+
do {
|
|
445
|
+
try fm.createDirectory(atPath: outputDir, withIntermediateDirectories: true)
|
|
446
|
+
} catch {
|
|
447
|
+
JSONOutput.error("Failed to create output directory: \(error.localizedDescription)")
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
let script = """
|
|
453
|
+
tell application "Keynote"
|
|
454
|
+
set doc to open POSIX file "\(escapedFile)"
|
|
455
|
+
export doc as slide images to POSIX file "\(escapedOutput)" with properties {image format:\(imageFormat)}
|
|
456
|
+
close doc saving no
|
|
457
|
+
return "ok"
|
|
458
|
+
end tell
|
|
459
|
+
"""
|
|
460
|
+
|
|
461
|
+
guard let _ = runAppleScript(script) else { return }
|
|
462
|
+
|
|
463
|
+
let ext = format == "jpeg" ? "jpeg" : "png"
|
|
464
|
+
var exportedFiles: [String] = []
|
|
465
|
+
if let files = try? fm.contentsOfDirectory(atPath: outputDir) {
|
|
466
|
+
exportedFiles = files.filter { $0.hasSuffix(ext) || $0.hasSuffix("jpg") }
|
|
467
|
+
.sorted()
|
|
468
|
+
.map { "\(outputDir)/\($0)" }
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
JSONOutput.success([
|
|
472
|
+
"directory": outputDir,
|
|
473
|
+
"files": exportedFiles,
|
|
474
|
+
"count": exportedFiles.count
|
|
475
|
+
] as [String: Any])
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// MARK: - Search
|
|
479
|
+
|
|
480
|
+
static func search(query: String, limit: Int) {
|
|
481
|
+
let sanitized = query.replacingOccurrences(of: "'", with: "")
|
|
482
|
+
.replacingOccurrences(of: "\\", with: "")
|
|
483
|
+
let task = Process()
|
|
484
|
+
task.executableURL = URL(fileURLWithPath: "/usr/bin/mdfind")
|
|
485
|
+
task.arguments = [
|
|
486
|
+
"kMDItemContentType == 'com.apple.iWork.keynote.sffkey' && kMDItemDisplayName == '*\(sanitized)*'cd"
|
|
487
|
+
]
|
|
488
|
+
|
|
489
|
+
let pipe = Pipe()
|
|
490
|
+
task.standardOutput = pipe
|
|
491
|
+
task.standardError = Pipe()
|
|
492
|
+
|
|
493
|
+
do {
|
|
494
|
+
try task.run()
|
|
495
|
+
task.waitUntilExit()
|
|
496
|
+
|
|
497
|
+
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
|
498
|
+
guard let output = String(data: data, encoding: .utf8) else {
|
|
499
|
+
JSONOutput.success([])
|
|
500
|
+
return
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
let paths = output.components(separatedBy: "\n")
|
|
504
|
+
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
|
505
|
+
.filter { !$0.isEmpty }
|
|
506
|
+
.prefix(limit)
|
|
507
|
+
|
|
508
|
+
let fm = FileManager.default
|
|
509
|
+
let results: [[String: Any]] = paths.compactMap { path in
|
|
510
|
+
guard let attrs = try? fm.attributesOfItem(atPath: path) else { return nil }
|
|
511
|
+
var entry: [String: Any] = [
|
|
512
|
+
"path": path,
|
|
513
|
+
"name": URL(fileURLWithPath: path).lastPathComponent
|
|
514
|
+
]
|
|
515
|
+
if let size = attrs[.size] as? Int {
|
|
516
|
+
entry["size"] = size
|
|
517
|
+
}
|
|
518
|
+
if let modified = attrs[.modificationDate] as? Date {
|
|
519
|
+
entry["modified"] = iso8601(modified)
|
|
520
|
+
}
|
|
521
|
+
return entry
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
JSONOutput.success(results)
|
|
525
|
+
} catch {
|
|
526
|
+
JSONOutput.error("Spotlight search failed: \(error.localizedDescription)")
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// MARK: - AppleScript Execution
|
|
531
|
+
|
|
532
|
+
private static func runAppleScript(_ script: String) -> String? {
|
|
533
|
+
let task = Process()
|
|
534
|
+
task.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
|
|
535
|
+
task.arguments = ["-e", script]
|
|
536
|
+
|
|
537
|
+
let outPipe = Pipe()
|
|
538
|
+
let errPipe = Pipe()
|
|
539
|
+
task.standardOutput = outPipe
|
|
540
|
+
task.standardError = errPipe
|
|
541
|
+
|
|
542
|
+
do {
|
|
543
|
+
try task.run()
|
|
544
|
+
task.waitUntilExit()
|
|
545
|
+
|
|
546
|
+
if task.terminationStatus != 0 {
|
|
547
|
+
let errData = errPipe.fileHandleForReading.readDataToEndOfFile()
|
|
548
|
+
let errStr = String(data: errData, encoding: .utf8)?
|
|
549
|
+
.trimmingCharacters(in: .whitespacesAndNewlines) ?? "Unknown error"
|
|
550
|
+
|
|
551
|
+
if errStr.contains("-1743") || errStr.contains("not allowed") {
|
|
552
|
+
JSONOutput.error("Keynote automation permission denied. Grant access in System Settings > Privacy & Security > Automation.")
|
|
553
|
+
} else if errStr.contains("-600") || errStr.contains("not running") {
|
|
554
|
+
JSONOutput.error("Keynote is not running. It will be launched automatically on next attempt.")
|
|
555
|
+
} else {
|
|
556
|
+
JSONOutput.error("AppleScript error: \(errStr)")
|
|
557
|
+
}
|
|
558
|
+
return nil
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
let data = outPipe.fileHandleForReading.readDataToEndOfFile()
|
|
562
|
+
return String(data: data, encoding: .utf8)?
|
|
563
|
+
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
564
|
+
} catch {
|
|
565
|
+
JSONOutput.error("Failed to run osascript: \(error.localizedDescription)")
|
|
566
|
+
return nil
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
private static func escapeForAppleScript(_ str: String) -> String {
|
|
571
|
+
return str.replacingOccurrences(of: "\\", with: "\\\\")
|
|
572
|
+
.replacingOccurrences(of: "\"", with: "\\\"")
|
|
573
|
+
.replacingOccurrences(of: "\n", with: "\\n")
|
|
574
|
+
.replacingOccurrences(of: "\r", with: "\\r")
|
|
575
|
+
.replacingOccurrences(of: "\t", with: "\\t")
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// MARK: - Parsers
|
|
579
|
+
|
|
580
|
+
private static func parseSlideList(_ raw: String) -> [[String: Any]] {
|
|
581
|
+
guard !raw.isEmpty else { return [] }
|
|
582
|
+
let slideChunks = raw.components(separatedBy: "###")
|
|
583
|
+
return slideChunks.compactMap { chunk -> [String: Any]? in
|
|
584
|
+
let trimmed = chunk.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
585
|
+
guard !trimmed.isEmpty else { return nil }
|
|
586
|
+
let parts = trimmed.components(separatedBy: "|||")
|
|
587
|
+
guard parts.count >= 6 else { return nil }
|
|
588
|
+
|
|
589
|
+
return [
|
|
590
|
+
"index": Int(parts[0].trimmingCharacters(in: .whitespaces)) ?? 0,
|
|
591
|
+
"title": parts[1].trimmingCharacters(in: .whitespaces),
|
|
592
|
+
"body": parts[2].trimmingCharacters(in: .whitespaces),
|
|
593
|
+
"notes": parts[3].trimmingCharacters(in: .whitespaces),
|
|
594
|
+
"layout": parts[4].trimmingCharacters(in: .whitespaces),
|
|
595
|
+
"skipped": parts[5].trimmingCharacters(in: .whitespaces).lowercased() == "true"
|
|
596
|
+
]
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|