@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.
- 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 +11 -8
- 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,601 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
enum NumbersBridge {
|
|
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 "Numbers"
|
|
15
|
+
set doc to open POSIX file "\(escaped)"
|
|
16
|
+
set sheetCount to count of sheets of doc
|
|
17
|
+
set docName to name of doc
|
|
18
|
+
set result to {}
|
|
19
|
+
set tableCount to 0
|
|
20
|
+
repeat with s in sheets of doc
|
|
21
|
+
set tableCount to tableCount + (count of tables of s)
|
|
22
|
+
end repeat
|
|
23
|
+
close doc saving no
|
|
24
|
+
return docName & "|||" & (sheetCount as string) & "|||" & (tableCount as string)
|
|
25
|
+
end tell
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
guard let raw = runAppleScript(script) else { return }
|
|
29
|
+
let parts = raw.components(separatedBy: "|||")
|
|
30
|
+
guard parts.count >= 3 else {
|
|
31
|
+
JSONOutput.error("Unexpected response format from Numbers")
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let result: [String: Any] = [
|
|
36
|
+
"name": parts[0],
|
|
37
|
+
"sheetCount": Int(parts[1].trimmingCharacters(in: .whitespaces)) ?? 0,
|
|
38
|
+
"tableCount": Int(parts[2].trimmingCharacters(in: .whitespaces)) ?? 0,
|
|
39
|
+
"path": resolvedFile
|
|
40
|
+
]
|
|
41
|
+
JSONOutput.success(result)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// MARK: - List Sheets
|
|
45
|
+
|
|
46
|
+
static func listSheets(file: String) {
|
|
47
|
+
guard let resolvedFile = FilesBridge.validatePath(file) else {
|
|
48
|
+
JSONOutput.error("Path is outside home directory or does not exist: \(file)")
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
let escaped = escapeForAppleScript(resolvedFile)
|
|
52
|
+
let script = """
|
|
53
|
+
tell application "Numbers"
|
|
54
|
+
set doc to open POSIX file "\(escaped)"
|
|
55
|
+
set resultList to {}
|
|
56
|
+
repeat with s in sheets of doc
|
|
57
|
+
set sheetName to name of s
|
|
58
|
+
set tableNames to {}
|
|
59
|
+
repeat with t in tables of s
|
|
60
|
+
set tName to name of t
|
|
61
|
+
set rowCount to row count of t
|
|
62
|
+
set colCount to column count of t
|
|
63
|
+
set end of tableNames to tName & "::" & (rowCount as string) & "::" & (colCount as string)
|
|
64
|
+
end repeat
|
|
65
|
+
set end of resultList to sheetName & "|||" & (my joinList(tableNames, "^^^"))
|
|
66
|
+
end repeat
|
|
67
|
+
close doc saving no
|
|
68
|
+
return my joinList(resultList, "###")
|
|
69
|
+
end tell
|
|
70
|
+
|
|
71
|
+
on joinList(theList, delim)
|
|
72
|
+
set oldDelim to AppleScript's text item delimiters
|
|
73
|
+
set AppleScript's text item delimiters to delim
|
|
74
|
+
set theResult to theList as string
|
|
75
|
+
set AppleScript's text item delimiters to oldDelim
|
|
76
|
+
return theResult
|
|
77
|
+
end joinList
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
guard let raw = runAppleScript(script) else { return }
|
|
81
|
+
let sheets = parseSheetList(raw)
|
|
82
|
+
JSONOutput.success(sheets)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// MARK: - Read (JXA)
|
|
86
|
+
|
|
87
|
+
static func read(file: String, sheet: String?, table: String?, range: String?) {
|
|
88
|
+
guard let resolvedFile = FilesBridge.validatePath(file) else {
|
|
89
|
+
JSONOutput.error("Path is outside home directory or does not exist: \(file)")
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
let escapedFile = escapeForJXA(resolvedFile)
|
|
93
|
+
let sheetSelector = sheet != nil ? "sheets.byName(\"\(escapeForJXA(sheet!))\")" : "sheets[0]"
|
|
94
|
+
let tableSelector = table != nil ? "tables.byName(\"\(escapeForJXA(table!))\")" : "tables[0]"
|
|
95
|
+
|
|
96
|
+
let rangeClause: String
|
|
97
|
+
if let range = range {
|
|
98
|
+
let parsed = parseA1Range(range)
|
|
99
|
+
rangeClause = """
|
|
100
|
+
var startRow = \(parsed.startRow); var startCol = \(parsed.startCol);
|
|
101
|
+
var endRow = \(parsed.endRow); var endCol = \(parsed.endCol);
|
|
102
|
+
"""
|
|
103
|
+
} else {
|
|
104
|
+
rangeClause = """
|
|
105
|
+
var startRow = 0; var startCol = 0;
|
|
106
|
+
var endRow = tbl.rowCount() - 1; var endCol = tbl.columnCount() - 1;
|
|
107
|
+
"""
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let script = """
|
|
111
|
+
var app = Application("Numbers");
|
|
112
|
+
var doc = app.open(Path("\(escapedFile)"));
|
|
113
|
+
var sht = doc.\(sheetSelector);
|
|
114
|
+
var tbl = sht.\(tableSelector);
|
|
115
|
+
\(rangeClause)
|
|
116
|
+
var rows = [];
|
|
117
|
+
for (var r = startRow; r <= endRow; r++) {
|
|
118
|
+
var row = [];
|
|
119
|
+
for (var c = startCol; c <= endCol; c++) {
|
|
120
|
+
var cell = tbl.cells[r * tbl.columnCount() + c];
|
|
121
|
+
var val = cell.value();
|
|
122
|
+
row.push(val === null ? "" : val);
|
|
123
|
+
}
|
|
124
|
+
rows.push(row);
|
|
125
|
+
}
|
|
126
|
+
var result = JSON.stringify({
|
|
127
|
+
sheet: sht.name(),
|
|
128
|
+
table: tbl.name(),
|
|
129
|
+
rows: rows,
|
|
130
|
+
rowCount: endRow - startRow + 1,
|
|
131
|
+
columnCount: endCol - startCol + 1
|
|
132
|
+
});
|
|
133
|
+
doc.close({saving: "no"});
|
|
134
|
+
result;
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
guard let raw = runJXA(script) else { return }
|
|
138
|
+
guard let data = raw.data(using: .utf8),
|
|
139
|
+
let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
140
|
+
JSONOutput.error("Failed to parse JXA JSON output")
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
JSONOutput.success(parsed)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// MARK: - Write (JXA)
|
|
147
|
+
|
|
148
|
+
static func write(file: String, sheet: String?, table: String?, range: String?, dataJSON: String) {
|
|
149
|
+
guard let resolvedFile = FilesBridge.validatePath(file) else {
|
|
150
|
+
JSONOutput.error("Path is outside home directory or does not exist: \(file)")
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
let escapedFile = escapeForJXA(resolvedFile)
|
|
154
|
+
let sheetSelector = sheet != nil ? "sheets.byName(\"\(escapeForJXA(sheet!))\")" : "sheets[0]"
|
|
155
|
+
let tableSelector = table != nil ? "tables.byName(\"\(escapeForJXA(table!))\")" : "tables[0]"
|
|
156
|
+
|
|
157
|
+
let startPos: String
|
|
158
|
+
if let range = range {
|
|
159
|
+
let parsed = parseA1Range(range)
|
|
160
|
+
startPos = "var startRow = \(parsed.startRow); var startCol = \(parsed.startCol);"
|
|
161
|
+
} else {
|
|
162
|
+
startPos = "var startRow = 0; var startCol = 0;"
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let escapedData = escapeForJXA(dataJSON)
|
|
166
|
+
|
|
167
|
+
let script = """
|
|
168
|
+
var app = Application("Numbers");
|
|
169
|
+
var doc = app.open(Path("\(escapedFile)"));
|
|
170
|
+
var sht = doc.\(sheetSelector);
|
|
171
|
+
var tbl = sht.\(tableSelector);
|
|
172
|
+
\(startPos)
|
|
173
|
+
var data = JSON.parse("\(escapedData)");
|
|
174
|
+
var written = 0;
|
|
175
|
+
for (var r = 0; r < data.length; r++) {
|
|
176
|
+
var row = data[r];
|
|
177
|
+
for (var c = 0; c < row.length; c++) {
|
|
178
|
+
tbl.cells[(startRow + r) * tbl.columnCount() + (startCol + c)].value = row[c];
|
|
179
|
+
written++;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
doc.close({saving: "yes"});
|
|
183
|
+
JSON.stringify({cellsWritten: written, rows: data.length, columns: data[0].length});
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
guard let raw = runJXA(script) else { return }
|
|
187
|
+
guard let data = raw.data(using: .utf8),
|
|
188
|
+
let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
189
|
+
JSONOutput.error("Failed to parse JXA JSON output")
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
JSONOutput.success(parsed)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// MARK: - Get Formulas (JXA)
|
|
196
|
+
|
|
197
|
+
static func getFormulas(file: String, sheet: String?, table: String?, range: String?) {
|
|
198
|
+
guard let resolvedFile = FilesBridge.validatePath(file) else {
|
|
199
|
+
JSONOutput.error("Path is outside home directory or does not exist: \(file)")
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
let escapedFile = escapeForJXA(resolvedFile)
|
|
203
|
+
let sheetSelector = sheet != nil ? "sheets.byName(\"\(escapeForJXA(sheet!))\")" : "sheets[0]"
|
|
204
|
+
let tableSelector = table != nil ? "tables.byName(\"\(escapeForJXA(table!))\")" : "tables[0]"
|
|
205
|
+
|
|
206
|
+
let rangeClause: String
|
|
207
|
+
if let range = range {
|
|
208
|
+
let parsed = parseA1Range(range)
|
|
209
|
+
rangeClause = """
|
|
210
|
+
var startRow = \(parsed.startRow); var startCol = \(parsed.startCol);
|
|
211
|
+
var endRow = \(parsed.endRow); var endCol = \(parsed.endCol);
|
|
212
|
+
"""
|
|
213
|
+
} else {
|
|
214
|
+
rangeClause = """
|
|
215
|
+
var startRow = 0; var startCol = 0;
|
|
216
|
+
var endRow = tbl.rowCount() - 1; var endCol = tbl.columnCount() - 1;
|
|
217
|
+
"""
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let script = """
|
|
221
|
+
var app = Application("Numbers");
|
|
222
|
+
var doc = app.open(Path("\(escapedFile)"));
|
|
223
|
+
var sht = doc.\(sheetSelector);
|
|
224
|
+
var tbl = sht.\(tableSelector);
|
|
225
|
+
\(rangeClause)
|
|
226
|
+
var rows = [];
|
|
227
|
+
for (var r = startRow; r <= endRow; r++) {
|
|
228
|
+
var row = [];
|
|
229
|
+
for (var c = startCol; c <= endCol; c++) {
|
|
230
|
+
var cell = tbl.cells[r * tbl.columnCount() + c];
|
|
231
|
+
var formula = cell.formula();
|
|
232
|
+
row.push(formula === null ? "" : formula);
|
|
233
|
+
}
|
|
234
|
+
rows.push(row);
|
|
235
|
+
}
|
|
236
|
+
var result = JSON.stringify({
|
|
237
|
+
sheet: sht.name(),
|
|
238
|
+
table: tbl.name(),
|
|
239
|
+
formulas: rows,
|
|
240
|
+
rowCount: endRow - startRow + 1,
|
|
241
|
+
columnCount: endCol - startCol + 1
|
|
242
|
+
});
|
|
243
|
+
doc.close({saving: "no"});
|
|
244
|
+
result;
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
guard let raw = runJXA(script) else { return }
|
|
248
|
+
guard let data = raw.data(using: .utf8),
|
|
249
|
+
let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
250
|
+
JSONOutput.error("Failed to parse JXA JSON output")
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
JSONOutput.success(parsed)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// MARK: - Create
|
|
257
|
+
|
|
258
|
+
static func create(file: String, dataJSON: String?, template: String?) {
|
|
259
|
+
guard let resolvedFile = FilesBridge.validatePath(file, mustExist: false) else {
|
|
260
|
+
JSONOutput.error("Path is outside home directory: \(file)")
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
let escaped = escapeForAppleScript(resolvedFile)
|
|
264
|
+
|
|
265
|
+
let templateClause: String
|
|
266
|
+
if let template = template {
|
|
267
|
+
templateClause = "set doc to make new document with properties {document template:template \"\(escapeForAppleScript(template))\"}"
|
|
268
|
+
} else {
|
|
269
|
+
templateClause = "set doc to make new document"
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
let script = """
|
|
273
|
+
tell application "Numbers"
|
|
274
|
+
\(templateClause)
|
|
275
|
+
set docPath to POSIX file "\(escaped)"
|
|
276
|
+
save doc in docPath
|
|
277
|
+
close doc
|
|
278
|
+
return "ok"
|
|
279
|
+
end tell
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
guard let _ = runAppleScript(script) else { return }
|
|
283
|
+
|
|
284
|
+
if let dataJSON = dataJSON, !dataJSON.isEmpty {
|
|
285
|
+
write(file: resolvedFile, sheet: nil, table: nil, range: nil, dataJSON: dataJSON)
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
JSONOutput.success(["path": resolvedFile, "created": true])
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// MARK: - Add Sheet
|
|
293
|
+
|
|
294
|
+
static func addSheet(file: String, name: String) {
|
|
295
|
+
guard let resolvedFile = FilesBridge.validatePath(file) else {
|
|
296
|
+
JSONOutput.error("Path is outside home directory or does not exist: \(file)")
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
let escapedFile = escapeForAppleScript(resolvedFile)
|
|
300
|
+
let escapedName = escapeForAppleScript(name)
|
|
301
|
+
let script = """
|
|
302
|
+
tell application "Numbers"
|
|
303
|
+
set doc to open POSIX file "\(escapedFile)"
|
|
304
|
+
tell doc
|
|
305
|
+
set newSheet to make new sheet with properties {name:"\(escapedName)"}
|
|
306
|
+
end tell
|
|
307
|
+
save doc
|
|
308
|
+
close doc
|
|
309
|
+
return "ok"
|
|
310
|
+
end tell
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
guard let _ = runAppleScript(script) else { return }
|
|
314
|
+
JSONOutput.success(["sheet": name, "added": true])
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// MARK: - Remove Sheet
|
|
318
|
+
|
|
319
|
+
static func removeSheet(file: String, name: String) {
|
|
320
|
+
guard let resolvedFile = FilesBridge.validatePath(file) else {
|
|
321
|
+
JSONOutput.error("Path is outside home directory or does not exist: \(file)")
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
let escapedFile = escapeForAppleScript(resolvedFile)
|
|
325
|
+
let escapedName = escapeForAppleScript(name)
|
|
326
|
+
let script = """
|
|
327
|
+
tell application "Numbers"
|
|
328
|
+
set doc to open POSIX file "\(escapedFile)"
|
|
329
|
+
tell doc
|
|
330
|
+
delete sheet "\(escapedName)"
|
|
331
|
+
end tell
|
|
332
|
+
save doc
|
|
333
|
+
close doc
|
|
334
|
+
return "ok"
|
|
335
|
+
end tell
|
|
336
|
+
"""
|
|
337
|
+
|
|
338
|
+
guard let _ = runAppleScript(script) else { return }
|
|
339
|
+
JSONOutput.success(["sheet": name, "removed": true])
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// MARK: - Export
|
|
343
|
+
|
|
344
|
+
static func export(file: String, format: String, dest: String?) {
|
|
345
|
+
guard let resolvedFile = FilesBridge.validatePath(file) else {
|
|
346
|
+
JSONOutput.error("Path is outside home directory or does not exist: \(file)")
|
|
347
|
+
return
|
|
348
|
+
}
|
|
349
|
+
let escapedFile = escapeForAppleScript(resolvedFile)
|
|
350
|
+
let outputPath: String
|
|
351
|
+
if let dest = dest {
|
|
352
|
+
guard let resolvedDest = FilesBridge.validatePath(dest, mustExist: false) else {
|
|
353
|
+
JSONOutput.error("Destination path is outside home directory: \(dest)")
|
|
354
|
+
return
|
|
355
|
+
}
|
|
356
|
+
outputPath = resolvedDest
|
|
357
|
+
} else {
|
|
358
|
+
let ext: String
|
|
359
|
+
switch format {
|
|
360
|
+
case "csv": ext = "csv"
|
|
361
|
+
case "pdf": ext = "pdf"
|
|
362
|
+
case "xlsx": ext = "xlsx"
|
|
363
|
+
default: ext = format
|
|
364
|
+
}
|
|
365
|
+
outputPath = resolvedFile.replacingOccurrences(of: ".numbers", with: ".\(ext)")
|
|
366
|
+
}
|
|
367
|
+
let escapedOutput = escapeForAppleScript(outputPath)
|
|
368
|
+
|
|
369
|
+
let formatMap: [String: String] = [
|
|
370
|
+
"csv": "CSV",
|
|
371
|
+
"pdf": "PDF",
|
|
372
|
+
"xlsx": "Microsoft Excel"
|
|
373
|
+
]
|
|
374
|
+
guard let numbersFormat = formatMap[format] else {
|
|
375
|
+
JSONOutput.error("Unsupported export format: \(format). Use csv, pdf, or xlsx.")
|
|
376
|
+
return
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
let script = """
|
|
380
|
+
tell application "Numbers"
|
|
381
|
+
set doc to open POSIX file "\(escapedFile)"
|
|
382
|
+
export doc to POSIX file "\(escapedOutput)" as \(numbersFormat)
|
|
383
|
+
close doc saving no
|
|
384
|
+
return "ok"
|
|
385
|
+
end tell
|
|
386
|
+
"""
|
|
387
|
+
|
|
388
|
+
guard let _ = runAppleScript(script) else { return }
|
|
389
|
+
JSONOutput.success(["path": outputPath])
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// MARK: - Search
|
|
393
|
+
|
|
394
|
+
static func search(query: String, limit: Int) {
|
|
395
|
+
let sanitized = query.replacingOccurrences(of: "'", with: "")
|
|
396
|
+
.replacingOccurrences(of: "\\", with: "")
|
|
397
|
+
let task = Process()
|
|
398
|
+
task.executableURL = URL(fileURLWithPath: "/usr/bin/mdfind")
|
|
399
|
+
task.arguments = [
|
|
400
|
+
"kMDItemContentType == 'com.apple.iWork.numbers.sffnumbers' && kMDItemDisplayName == '*\(sanitized)*'cd"
|
|
401
|
+
]
|
|
402
|
+
|
|
403
|
+
let pipe = Pipe()
|
|
404
|
+
task.standardOutput = pipe
|
|
405
|
+
task.standardError = Pipe()
|
|
406
|
+
|
|
407
|
+
do {
|
|
408
|
+
try task.run()
|
|
409
|
+
task.waitUntilExit()
|
|
410
|
+
|
|
411
|
+
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
|
412
|
+
guard let output = String(data: data, encoding: .utf8) else {
|
|
413
|
+
JSONOutput.success([])
|
|
414
|
+
return
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
let paths = output.components(separatedBy: "\n")
|
|
418
|
+
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
|
419
|
+
.filter { !$0.isEmpty }
|
|
420
|
+
.prefix(limit)
|
|
421
|
+
|
|
422
|
+
let fm = FileManager.default
|
|
423
|
+
let results: [[String: Any]] = paths.compactMap { path in
|
|
424
|
+
guard let attrs = try? fm.attributesOfItem(atPath: path) else { return nil }
|
|
425
|
+
var entry: [String: Any] = [
|
|
426
|
+
"path": path,
|
|
427
|
+
"name": URL(fileURLWithPath: path).lastPathComponent
|
|
428
|
+
]
|
|
429
|
+
if let size = attrs[.size] as? Int {
|
|
430
|
+
entry["size"] = size
|
|
431
|
+
}
|
|
432
|
+
if let modified = attrs[.modificationDate] as? Date {
|
|
433
|
+
entry["modified"] = iso8601(modified)
|
|
434
|
+
}
|
|
435
|
+
return entry
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
JSONOutput.success(results)
|
|
439
|
+
} catch {
|
|
440
|
+
JSONOutput.error("Spotlight search failed: \(error.localizedDescription)")
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// MARK: - AppleScript Execution
|
|
445
|
+
|
|
446
|
+
private static func runAppleScript(_ script: String) -> String? {
|
|
447
|
+
let task = Process()
|
|
448
|
+
task.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
|
|
449
|
+
task.arguments = ["-e", script]
|
|
450
|
+
|
|
451
|
+
let outPipe = Pipe()
|
|
452
|
+
let errPipe = Pipe()
|
|
453
|
+
task.standardOutput = outPipe
|
|
454
|
+
task.standardError = errPipe
|
|
455
|
+
|
|
456
|
+
do {
|
|
457
|
+
try task.run()
|
|
458
|
+
task.waitUntilExit()
|
|
459
|
+
|
|
460
|
+
if task.terminationStatus != 0 {
|
|
461
|
+
let errData = errPipe.fileHandleForReading.readDataToEndOfFile()
|
|
462
|
+
let errStr = String(data: errData, encoding: .utf8)?
|
|
463
|
+
.trimmingCharacters(in: .whitespacesAndNewlines) ?? "Unknown error"
|
|
464
|
+
|
|
465
|
+
if errStr.contains("-1743") || errStr.contains("not allowed") {
|
|
466
|
+
JSONOutput.error("Numbers automation permission denied. Grant access in System Settings > Privacy & Security > Automation.")
|
|
467
|
+
} else if errStr.contains("-600") || errStr.contains("not running") {
|
|
468
|
+
JSONOutput.error("Numbers is not running. It will be launched automatically on next attempt.")
|
|
469
|
+
} else {
|
|
470
|
+
JSONOutput.error("AppleScript error: \(errStr)")
|
|
471
|
+
}
|
|
472
|
+
return nil
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
let data = outPipe.fileHandleForReading.readDataToEndOfFile()
|
|
476
|
+
return String(data: data, encoding: .utf8)?
|
|
477
|
+
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
478
|
+
} catch {
|
|
479
|
+
JSONOutput.error("Failed to run osascript: \(error.localizedDescription)")
|
|
480
|
+
return nil
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// MARK: - JXA Execution
|
|
485
|
+
|
|
486
|
+
private static func runJXA(_ script: String) -> String? {
|
|
487
|
+
let task = Process()
|
|
488
|
+
task.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
|
|
489
|
+
task.arguments = ["-l", "JavaScript", "-e", script]
|
|
490
|
+
|
|
491
|
+
let outPipe = Pipe()
|
|
492
|
+
let errPipe = Pipe()
|
|
493
|
+
task.standardOutput = outPipe
|
|
494
|
+
task.standardError = errPipe
|
|
495
|
+
|
|
496
|
+
do {
|
|
497
|
+
try task.run()
|
|
498
|
+
task.waitUntilExit()
|
|
499
|
+
|
|
500
|
+
if task.terminationStatus != 0 {
|
|
501
|
+
let errData = errPipe.fileHandleForReading.readDataToEndOfFile()
|
|
502
|
+
let errStr = String(data: errData, encoding: .utf8)?
|
|
503
|
+
.trimmingCharacters(in: .whitespacesAndNewlines) ?? "Unknown error"
|
|
504
|
+
JSONOutput.error("JXA error: \(errStr)")
|
|
505
|
+
return nil
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
let data = outPipe.fileHandleForReading.readDataToEndOfFile()
|
|
509
|
+
return String(data: data, encoding: .utf8)?
|
|
510
|
+
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
511
|
+
} catch {
|
|
512
|
+
JSONOutput.error("Failed to run osascript: \(error.localizedDescription)")
|
|
513
|
+
return nil
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
private static func escapeForAppleScript(_ str: String) -> String {
|
|
518
|
+
return str.replacingOccurrences(of: "\\", with: "\\\\")
|
|
519
|
+
.replacingOccurrences(of: "\"", with: "\\\"")
|
|
520
|
+
.replacingOccurrences(of: "\n", with: "\\n")
|
|
521
|
+
.replacingOccurrences(of: "\r", with: "\\r")
|
|
522
|
+
.replacingOccurrences(of: "\t", with: "\\t")
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
private static func escapeForJXA(_ str: String) -> String {
|
|
526
|
+
return str.replacingOccurrences(of: "\\", with: "\\\\")
|
|
527
|
+
.replacingOccurrences(of: "\"", with: "\\\"")
|
|
528
|
+
.replacingOccurrences(of: "\n", with: "\\n")
|
|
529
|
+
.replacingOccurrences(of: "\r", with: "\\r")
|
|
530
|
+
.replacingOccurrences(of: "\t", with: "\\t")
|
|
531
|
+
.replacingOccurrences(of: "\0", with: "")
|
|
532
|
+
.replacingOccurrences(of: "\u{2028}", with: "\\u2028")
|
|
533
|
+
.replacingOccurrences(of: "\u{2029}", with: "\\u2029")
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// MARK: - A1 Range Parser
|
|
537
|
+
|
|
538
|
+
private struct CellRange {
|
|
539
|
+
let startRow: Int
|
|
540
|
+
let startCol: Int
|
|
541
|
+
let endRow: Int
|
|
542
|
+
let endCol: Int
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
private static func parseA1Range(_ range: String) -> CellRange {
|
|
546
|
+
let parts = range.uppercased().components(separatedBy: ":")
|
|
547
|
+
let start = parseA1Cell(parts[0])
|
|
548
|
+
let end = parts.count > 1 ? parseA1Cell(parts[1]) : start
|
|
549
|
+
return CellRange(startRow: start.row, startCol: start.col, endRow: end.row, endCol: end.col)
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
private static func parseA1Cell(_ cell: String) -> (row: Int, col: Int) {
|
|
553
|
+
var colStr = ""
|
|
554
|
+
var rowStr = ""
|
|
555
|
+
for char in cell {
|
|
556
|
+
if char.isLetter {
|
|
557
|
+
colStr.append(char)
|
|
558
|
+
} else {
|
|
559
|
+
rowStr.append(char)
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
var col = 0
|
|
563
|
+
for char in colStr {
|
|
564
|
+
col = col * 26 + Int(char.asciiValue! - 65) + 1
|
|
565
|
+
}
|
|
566
|
+
col -= 1
|
|
567
|
+
let row = (Int(rowStr) ?? 1) - 1
|
|
568
|
+
return (row, col)
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// MARK: - Parsers
|
|
572
|
+
|
|
573
|
+
private static func parseSheetList(_ raw: String) -> [[String: Any]] {
|
|
574
|
+
guard !raw.isEmpty else { return [] }
|
|
575
|
+
let sheetChunks = raw.components(separatedBy: "###")
|
|
576
|
+
return sheetChunks.compactMap { chunk -> [String: Any]? in
|
|
577
|
+
let parts = chunk.components(separatedBy: "|||")
|
|
578
|
+
guard parts.count >= 1 else { return nil }
|
|
579
|
+
|
|
580
|
+
var sheet: [String: Any] = [
|
|
581
|
+
"name": parts[0].trimmingCharacters(in: .whitespaces)
|
|
582
|
+
]
|
|
583
|
+
|
|
584
|
+
if parts.count > 1 && !parts[1].isEmpty {
|
|
585
|
+
let tableStrings = parts[1].components(separatedBy: "^^^")
|
|
586
|
+
let tables: [[String: Any]] = tableStrings.compactMap { tStr in
|
|
587
|
+
let fields = tStr.components(separatedBy: "::")
|
|
588
|
+
guard fields.count >= 3 else { return nil }
|
|
589
|
+
return [
|
|
590
|
+
"name": fields[0].trimmingCharacters(in: .whitespaces),
|
|
591
|
+
"rowCount": Int(fields[1].trimmingCharacters(in: .whitespaces)) ?? 0,
|
|
592
|
+
"columnCount": Int(fields[2].trimmingCharacters(in: .whitespaces)) ?? 0
|
|
593
|
+
]
|
|
594
|
+
}
|
|
595
|
+
sheet["tables"] = tables
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return sheet
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|