mcp-icloud 2.1.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/DISCLOSURE +35 -0
- package/LICENSE +21 -0
- package/README.md +244 -0
- package/auth/index.js +175 -0
- package/calendar/caldav-client.js +320 -0
- package/calendar/index.js +252 -0
- package/calendar/local-client.js +223 -0
- package/config.js +69 -0
- package/contacts/carddav-client.js +331 -0
- package/contacts/index.js +350 -0
- package/contacts/local-client.js +372 -0
- package/email/imap-client.js +351 -0
- package/email/index.js +278 -0
- package/email/local-client.js +257 -0
- package/email/smtp-client.js +79 -0
- package/index.js +123 -0
- package/messages/index.js +157 -0
- package/messages/local-client.js +74 -0
- package/mode.js +82 -0
- package/notes/index.js +162 -0
- package/notes/local-client.js +203 -0
- package/package.json +76 -0
- package/reminders/index.js +215 -0
- package/reminders/local-client.js +269 -0
- package/safari/index.js +122 -0
- package/safari/local-client.js +156 -0
- package/utils/applescript.js +214 -0
- package/utils/date-utils.js +72 -0
- package/utils/error-handler.js +71 -0
- package/utils/schemas.js +33 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Reminders Client
|
|
3
|
+
* Accesses Reminders.app via AppleScript
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { runAppleScript, runJXA, escapeAppleScript, escapeJXA, asInt, asBool, formatAppleScriptDate } = require('../utils/applescript');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* List all reminder lists
|
|
10
|
+
* @returns {Promise<Array>} - List of reminder lists
|
|
11
|
+
*/
|
|
12
|
+
async function listReminderLists() {
|
|
13
|
+
// Names and ids only, fetched as two batch calls. Counting reminders per
|
|
14
|
+
// list forces one Apple Event round-trip per reminder over the full
|
|
15
|
+
// history (completed included) and hangs for minutes on a real database.
|
|
16
|
+
const script = `
|
|
17
|
+
const reminders = Application('Reminders');
|
|
18
|
+
const lists = reminders.lists;
|
|
19
|
+
const names = lists.name();
|
|
20
|
+
const ids = lists.id();
|
|
21
|
+
let result = [];
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i < names.length; i++) {
|
|
24
|
+
result.push({ id: ids[i], name: names[i] });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
JSON.stringify(result);
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
const result = await runJXA(script);
|
|
31
|
+
return result ? JSON.parse(result) : [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* List reminders from a list
|
|
36
|
+
* @param {string} listName - List name (optional, defaults to all)
|
|
37
|
+
* @param {boolean} includeCompleted - Include completed reminders
|
|
38
|
+
* @param {number} count - Max reminders to return
|
|
39
|
+
* @returns {Promise<Array>} - List of reminders
|
|
40
|
+
*/
|
|
41
|
+
async function listReminders(listName = null, includeCompleted = false, count = 50) {
|
|
42
|
+
const limit = asInt(count, 50);
|
|
43
|
+
|
|
44
|
+
const script = `
|
|
45
|
+
const reminders = Application('Reminders');
|
|
46
|
+
let allReminders = [];
|
|
47
|
+
const includeCompleted = ${asBool(includeCompleted)};
|
|
48
|
+
const targetListName = ${listName ? `"${escapeJXA(listName)}"` : 'null'};
|
|
49
|
+
|
|
50
|
+
const lists = reminders.lists();
|
|
51
|
+
for (let i = 0; i < lists.length; i++) {
|
|
52
|
+
const list = lists[i];
|
|
53
|
+
if (targetListName && list.name() !== targetListName) continue;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const rems = list.reminders();
|
|
57
|
+
for (let j = 0; j < rems.length; j++) {
|
|
58
|
+
const r = rems[j];
|
|
59
|
+
const isCompleted = r.completed();
|
|
60
|
+
if (!includeCompleted && isCompleted) continue;
|
|
61
|
+
|
|
62
|
+
let dueDate = null;
|
|
63
|
+
try { dueDate = r.dueDate() ? r.dueDate().toISOString() : null; } catch(e) {}
|
|
64
|
+
|
|
65
|
+
allReminders.push({
|
|
66
|
+
id: r.id(),
|
|
67
|
+
name: r.name(),
|
|
68
|
+
body: r.body() || '',
|
|
69
|
+
completed: isCompleted,
|
|
70
|
+
dueDate: dueDate,
|
|
71
|
+
priority: r.priority(),
|
|
72
|
+
list: list.name()
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
} catch (e) {}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Sort by due date (nulls at end)
|
|
79
|
+
allReminders.sort(function(a, b) {
|
|
80
|
+
if (!a.dueDate && !b.dueDate) return 0;
|
|
81
|
+
if (!a.dueDate) return 1;
|
|
82
|
+
if (!b.dueDate) return -1;
|
|
83
|
+
return new Date(a.dueDate) - new Date(b.dueDate);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
JSON.stringify(allReminders.slice(0, ${limit}));
|
|
87
|
+
`;
|
|
88
|
+
|
|
89
|
+
const result = await runJXA(script);
|
|
90
|
+
return result ? JSON.parse(result) : [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Create a new reminder
|
|
95
|
+
* @param {Object} options - Reminder options
|
|
96
|
+
* @returns {Promise<Object>} - Created reminder info
|
|
97
|
+
*/
|
|
98
|
+
async function createReminder({ name, body, dueDate, listName = 'Reminders', priority = 0 }) {
|
|
99
|
+
let properties = [`name:"${escapeAppleScript(name)}"`];
|
|
100
|
+
|
|
101
|
+
if (body) properties.push(`body:"${escapeAppleScript(body)}"`);
|
|
102
|
+
if (priority) properties.push(`priority:${asInt(priority, 0)}`);
|
|
103
|
+
|
|
104
|
+
let script = `
|
|
105
|
+
tell application "Reminders"
|
|
106
|
+
tell list "${escapeAppleScript(listName)}"
|
|
107
|
+
set newReminder to make new reminder with properties {${properties.join(', ')}}
|
|
108
|
+
`;
|
|
109
|
+
|
|
110
|
+
if (dueDate) {
|
|
111
|
+
script += `
|
|
112
|
+
set due date of newReminder to date "${formatAppleScriptDate(new Date(dueDate))}"
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
script += `
|
|
117
|
+
return id of newReminder
|
|
118
|
+
end tell
|
|
119
|
+
end tell
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
const id = await runAppleScript(script);
|
|
123
|
+
return { success: true, id, message: 'Reminder created successfully' };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Update a reminder
|
|
128
|
+
* @param {string} reminderId - Reminder ID
|
|
129
|
+
* @param {Object} updates - Fields to update
|
|
130
|
+
* @returns {Promise<Object>} - Result
|
|
131
|
+
*/
|
|
132
|
+
async function updateReminder(reminderId, { name, body, dueDate, priority }) {
|
|
133
|
+
let updateCommands = [];
|
|
134
|
+
|
|
135
|
+
if (name) updateCommands.push(`set name of theReminder to "${escapeAppleScript(name)}"`);
|
|
136
|
+
if (body !== undefined) updateCommands.push(`set body of theReminder to "${escapeAppleScript(body || '')}"`);
|
|
137
|
+
if (priority !== undefined) updateCommands.push(`set priority of theReminder to ${asInt(priority, 0)}`);
|
|
138
|
+
if (dueDate) updateCommands.push(`set due date of theReminder to date "${formatAppleScriptDate(new Date(dueDate))}"`);
|
|
139
|
+
if (dueDate === null) updateCommands.push(`set due date of theReminder to missing value`);
|
|
140
|
+
|
|
141
|
+
const script = `
|
|
142
|
+
tell application "Reminders"
|
|
143
|
+
set allLists to lists
|
|
144
|
+
repeat with theList in allLists
|
|
145
|
+
try
|
|
146
|
+
set theReminder to reminder id "${escapeAppleScript(reminderId)}" of theList
|
|
147
|
+
${updateCommands.join('\n ')}
|
|
148
|
+
return "updated"
|
|
149
|
+
end try
|
|
150
|
+
end repeat
|
|
151
|
+
return "not found"
|
|
152
|
+
end tell
|
|
153
|
+
`;
|
|
154
|
+
|
|
155
|
+
const result = await runAppleScript(script);
|
|
156
|
+
if (result === 'not found') {
|
|
157
|
+
return { success: false, message: 'Reminder not found' };
|
|
158
|
+
}
|
|
159
|
+
return { success: true, message: 'Reminder updated successfully' };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Mark a reminder as complete/incomplete
|
|
164
|
+
* @param {string} reminderId - Reminder ID
|
|
165
|
+
* @param {boolean} completed - Completion status
|
|
166
|
+
* @returns {Promise<Object>} - Result
|
|
167
|
+
*/
|
|
168
|
+
async function completeReminder(reminderId, completed = true) {
|
|
169
|
+
const script = `
|
|
170
|
+
tell application "Reminders"
|
|
171
|
+
set allLists to lists
|
|
172
|
+
repeat with theList in allLists
|
|
173
|
+
try
|
|
174
|
+
set theReminder to reminder id "${escapeAppleScript(reminderId)}" of theList
|
|
175
|
+
set completed of theReminder to ${asBool(completed, true)}
|
|
176
|
+
return "done"
|
|
177
|
+
end try
|
|
178
|
+
end repeat
|
|
179
|
+
return "not found"
|
|
180
|
+
end tell
|
|
181
|
+
`;
|
|
182
|
+
|
|
183
|
+
const result = await runAppleScript(script);
|
|
184
|
+
if (result === 'not found') {
|
|
185
|
+
return { success: false, message: 'Reminder not found' };
|
|
186
|
+
}
|
|
187
|
+
return { success: true, message: `Reminder marked as ${completed ? 'complete' : 'incomplete'}` };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Delete a reminder
|
|
192
|
+
* @param {string} reminderId - Reminder ID
|
|
193
|
+
* @returns {Promise<Object>} - Result
|
|
194
|
+
*/
|
|
195
|
+
async function deleteReminder(reminderId) {
|
|
196
|
+
const script = `
|
|
197
|
+
tell application "Reminders"
|
|
198
|
+
set allLists to lists
|
|
199
|
+
repeat with theList in allLists
|
|
200
|
+
try
|
|
201
|
+
set theReminder to reminder id "${escapeAppleScript(reminderId)}" of theList
|
|
202
|
+
delete theReminder
|
|
203
|
+
return "deleted"
|
|
204
|
+
end try
|
|
205
|
+
end repeat
|
|
206
|
+
return "not found"
|
|
207
|
+
end tell
|
|
208
|
+
`;
|
|
209
|
+
|
|
210
|
+
const result = await runAppleScript(script);
|
|
211
|
+
if (result === 'not found') {
|
|
212
|
+
return { success: false, message: 'Reminder not found' };
|
|
213
|
+
}
|
|
214
|
+
return { success: true, message: 'Reminder deleted successfully' };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Search reminders
|
|
219
|
+
* @param {string} query - Search query
|
|
220
|
+
* @param {number} count - Max results
|
|
221
|
+
* @returns {Promise<Array>} - Matching reminders
|
|
222
|
+
*/
|
|
223
|
+
async function searchReminders(query, count = 25) {
|
|
224
|
+
const searchTerm = escapeJXA(query.toLowerCase());
|
|
225
|
+
const limit = asInt(count, 25);
|
|
226
|
+
|
|
227
|
+
const script = `
|
|
228
|
+
const reminders = Application('Reminders');
|
|
229
|
+
const lists = reminders.lists();
|
|
230
|
+
let results = [];
|
|
231
|
+
|
|
232
|
+
for (let list of lists) {
|
|
233
|
+
const rems = list.reminders();
|
|
234
|
+
for (let r of rems) {
|
|
235
|
+
if (results.length >= ${limit}) break;
|
|
236
|
+
|
|
237
|
+
const name = (r.name() || '').toLowerCase();
|
|
238
|
+
const body = (r.body() || '').toLowerCase();
|
|
239
|
+
|
|
240
|
+
if (name.includes("${searchTerm}") || body.includes("${searchTerm}")) {
|
|
241
|
+
results.push({
|
|
242
|
+
id: r.id(),
|
|
243
|
+
name: r.name(),
|
|
244
|
+
body: r.body() || '',
|
|
245
|
+
completed: r.completed(),
|
|
246
|
+
dueDate: r.dueDate() ? r.dueDate().toISOString() : null,
|
|
247
|
+
priority: r.priority(),
|
|
248
|
+
list: list.name()
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
JSON.stringify(results);
|
|
255
|
+
`;
|
|
256
|
+
|
|
257
|
+
const result = await runJXA(script);
|
|
258
|
+
return result ? JSON.parse(result) : [];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
module.exports = {
|
|
262
|
+
listReminderLists,
|
|
263
|
+
listReminders,
|
|
264
|
+
createReminder,
|
|
265
|
+
updateReminder,
|
|
266
|
+
completeReminder,
|
|
267
|
+
deleteReminder,
|
|
268
|
+
searchReminders
|
|
269
|
+
};
|
package/safari/index.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safari Module
|
|
3
|
+
* MCP tool definitions for Safari.app
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { z } = require('zod');
|
|
7
|
+
const localClient = require('./local-client');
|
|
8
|
+
const { formatError } = require('../utils/error-handler');
|
|
9
|
+
const { listOutput, listResult } = require('../utils/schemas');
|
|
10
|
+
const { isLocalMode } = require('../mode');
|
|
11
|
+
|
|
12
|
+
function requireLocalMode(toolName) {
|
|
13
|
+
if (!isLocalMode()) {
|
|
14
|
+
return formatError(new Error(`${toolName} is only available in LOCAL mode. Use set-mode to switch.`));
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const safariTools = [
|
|
20
|
+
{
|
|
21
|
+
name: 'list-safari-tabs',
|
|
22
|
+
outputSchema: listOutput('list-safari-tabs'),
|
|
23
|
+
title: 'List Safari Tabs',
|
|
24
|
+
description: 'Lists all open tabs in Safari across all windows',
|
|
25
|
+
inputSchema: {},
|
|
26
|
+
annotations: {"readOnlyHint":true,"idempotentHint":true,"openWorldHint":false},
|
|
27
|
+
handler: async () => {
|
|
28
|
+
const modeError = requireLocalMode('list-safari-tabs');
|
|
29
|
+
if (modeError) return modeError;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const tabs = await localClient.listTabs();
|
|
33
|
+
return {
|
|
34
|
+
content: [{
|
|
35
|
+
type: 'text',
|
|
36
|
+
text: JSON.stringify(tabs, null, 2)
|
|
37
|
+
}],
|
|
38
|
+
structuredContent: listResult(tabs)
|
|
39
|
+
};
|
|
40
|
+
} catch (error) {
|
|
41
|
+
return formatError(error, 'list-safari-tabs');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'get-current-safari-url',
|
|
47
|
+
title: 'Get Current Safari URL',
|
|
48
|
+
description: 'Gets the URL and title of the current/active Safari tab',
|
|
49
|
+
inputSchema: {},
|
|
50
|
+
annotations: {"readOnlyHint":true,"idempotentHint":true,"openWorldHint":false},
|
|
51
|
+
handler: async () => {
|
|
52
|
+
const modeError = requireLocalMode('get-current-safari-url');
|
|
53
|
+
if (modeError) return modeError;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const result = await localClient.getCurrentUrl();
|
|
57
|
+
return {
|
|
58
|
+
content: [{
|
|
59
|
+
type: 'text',
|
|
60
|
+
text: JSON.stringify(result, null, 2)
|
|
61
|
+
}]
|
|
62
|
+
};
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return formatError(error, 'get-current-safari-url');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'open-safari-url',
|
|
70
|
+
title: 'Open URL In Safari',
|
|
71
|
+
description: 'Opens a URL in Safari (new tab or new window)',
|
|
72
|
+
inputSchema: {
|
|
73
|
+
url: z.string().describe('URL to open (required)'),
|
|
74
|
+
inNewWindow: z.boolean().optional().describe('Open in new window instead of new tab (default: false)')
|
|
75
|
+
},
|
|
76
|
+
annotations: {"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false,"openWorldHint":true},
|
|
77
|
+
handler: async ({ url, inNewWindow = false }) => {
|
|
78
|
+
const modeError = requireLocalMode('open-safari-url');
|
|
79
|
+
if (modeError) return modeError;
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const result = await localClient.openUrl(url, inNewWindow);
|
|
83
|
+
return {
|
|
84
|
+
content: [{
|
|
85
|
+
type: 'text',
|
|
86
|
+
text: JSON.stringify(result, null, 2)
|
|
87
|
+
}]
|
|
88
|
+
};
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return formatError(error, 'open-safari-url');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'close-safari-tab',
|
|
96
|
+
title: 'Close Safari Tab',
|
|
97
|
+
description: 'Closes a Safari tab',
|
|
98
|
+
inputSchema: {
|
|
99
|
+
windowIndex: z.number().int().min(0).optional().describe('Window index (0-based, default: 0 for front window)'),
|
|
100
|
+
tabIndex: z.number().int().min(0).optional().describe('Tab index to close (0-based). If omitted, closes the current tab.')
|
|
101
|
+
},
|
|
102
|
+
annotations: {"readOnlyHint":false,"destructiveHint":true,"idempotentHint":true,"openWorldHint":false},
|
|
103
|
+
handler: async ({ windowIndex = 0, tabIndex }) => {
|
|
104
|
+
const modeError = requireLocalMode('close-safari-tab');
|
|
105
|
+
if (modeError) return modeError;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const result = await localClient.closeTab(windowIndex, tabIndex);
|
|
109
|
+
return {
|
|
110
|
+
content: [{
|
|
111
|
+
type: 'text',
|
|
112
|
+
text: JSON.stringify(result, null, 2)
|
|
113
|
+
}]
|
|
114
|
+
};
|
|
115
|
+
} catch (error) {
|
|
116
|
+
return formatError(error, 'close-safari-tab');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
module.exports = { safariTools };
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Safari Client
|
|
3
|
+
* Accesses Safari.app via AppleScript
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { runAppleScript, runJXA, escapeAppleScript, escapeJXA, asInt } = require('../utils/applescript');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* List all open tabs across all windows
|
|
10
|
+
* @returns {Promise<Array>} - List of tabs
|
|
11
|
+
*/
|
|
12
|
+
async function listTabs() {
|
|
13
|
+
const script = `
|
|
14
|
+
const safari = Application('Safari');
|
|
15
|
+
const windows = safari.windows();
|
|
16
|
+
let tabs = [];
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < windows.length; i++) {
|
|
19
|
+
const win = windows[i];
|
|
20
|
+
try {
|
|
21
|
+
const winTabs = win.tabs();
|
|
22
|
+
for (let j = 0; j < winTabs.length; j++) {
|
|
23
|
+
const tab = winTabs[j];
|
|
24
|
+
tabs.push({
|
|
25
|
+
windowIndex: i,
|
|
26
|
+
tabIndex: j,
|
|
27
|
+
name: tab.name(),
|
|
28
|
+
url: tab.url(),
|
|
29
|
+
isCurrent: win.currentTab().index() === tab.index()
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
} catch (e) {}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
JSON.stringify(tabs);
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
const result = await runJXA(script);
|
|
39
|
+
return result ? JSON.parse(result) : [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get the current/active tab URL
|
|
44
|
+
* @returns {Promise<Object>} - Current tab info
|
|
45
|
+
*/
|
|
46
|
+
async function getCurrentUrl() {
|
|
47
|
+
const script = `
|
|
48
|
+
const safari = Application('Safari');
|
|
49
|
+
const frontWindow = safari.windows[0];
|
|
50
|
+
const currentTab = frontWindow.currentTab();
|
|
51
|
+
|
|
52
|
+
JSON.stringify({
|
|
53
|
+
name: currentTab.name(),
|
|
54
|
+
url: currentTab.url(),
|
|
55
|
+
source: currentTab.source ? currentTab.source().substring(0, 1000) : null
|
|
56
|
+
});
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
const result = await runJXA(script);
|
|
60
|
+
return result ? JSON.parse(result) : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Open a URL in a new tab
|
|
65
|
+
* @param {string} url - URL to open
|
|
66
|
+
* @param {boolean} inNewWindow - Open in new window instead of new tab
|
|
67
|
+
* @returns {Promise<Object>} - Result
|
|
68
|
+
*/
|
|
69
|
+
async function openUrl(url, inNewWindow = false) {
|
|
70
|
+
// Ensure URL has protocol
|
|
71
|
+
let fullUrl = url;
|
|
72
|
+
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
73
|
+
fullUrl = 'https://' + url;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (inNewWindow) {
|
|
77
|
+
const script = `
|
|
78
|
+
tell application "Safari"
|
|
79
|
+
make new document with properties {URL:"${escapeAppleScript(fullUrl)}"}
|
|
80
|
+
activate
|
|
81
|
+
end tell
|
|
82
|
+
return "opened"
|
|
83
|
+
`;
|
|
84
|
+
await runAppleScript(script);
|
|
85
|
+
} else {
|
|
86
|
+
const script = `
|
|
87
|
+
tell application "Safari"
|
|
88
|
+
tell front window
|
|
89
|
+
set newTab to make new tab with properties {URL:"${escapeAppleScript(fullUrl)}"}
|
|
90
|
+
set current tab to newTab
|
|
91
|
+
end tell
|
|
92
|
+
activate
|
|
93
|
+
end tell
|
|
94
|
+
return "opened"
|
|
95
|
+
`;
|
|
96
|
+
await runAppleScript(script);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { success: true, message: `Opened ${fullUrl}` };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Close a tab
|
|
104
|
+
* @param {number} windowIndex - Window index
|
|
105
|
+
* @param {number} tabIndex - Tab index
|
|
106
|
+
* @returns {Promise<Object>} - Result
|
|
107
|
+
*/
|
|
108
|
+
async function closeTab(windowIndex = 0, tabIndex = null) {
|
|
109
|
+
if (tabIndex !== null && tabIndex !== undefined) {
|
|
110
|
+
const script = `
|
|
111
|
+
tell application "Safari"
|
|
112
|
+
tell window ${asInt(windowIndex, 0) + 1}
|
|
113
|
+
close tab ${asInt(tabIndex, 0) + 1}
|
|
114
|
+
end tell
|
|
115
|
+
end tell
|
|
116
|
+
return "closed"
|
|
117
|
+
`;
|
|
118
|
+
await runAppleScript(script);
|
|
119
|
+
} else {
|
|
120
|
+
// Close current tab
|
|
121
|
+
const script = `
|
|
122
|
+
tell application "Safari"
|
|
123
|
+
tell front window
|
|
124
|
+
close current tab
|
|
125
|
+
end tell
|
|
126
|
+
end tell
|
|
127
|
+
return "closed"
|
|
128
|
+
`;
|
|
129
|
+
await runAppleScript(script);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { success: true, message: 'Tab closed' };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get the reading list items
|
|
137
|
+
* Note: This requires parsing Safari's bookmarks plist
|
|
138
|
+
* @returns {Promise<Array>} - Reading list items
|
|
139
|
+
*/
|
|
140
|
+
async function getReadingList() {
|
|
141
|
+
// Reading list is stored in ~/Library/Safari/Bookmarks.plist
|
|
142
|
+
// This requires parsing a binary plist which is complex
|
|
143
|
+
// For now, return a note about this limitation
|
|
144
|
+
return {
|
|
145
|
+
message: 'Reading list access requires parsing Safari bookmarks plist. Use Safari directly for reading list management.',
|
|
146
|
+
items: []
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = {
|
|
151
|
+
listTabs,
|
|
152
|
+
getCurrentUrl,
|
|
153
|
+
openUrl,
|
|
154
|
+
closeTab,
|
|
155
|
+
getReadingList
|
|
156
|
+
};
|