@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,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
|
+
}
|