notes-poke 0.1.0 → 0.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/dist/appleScript.js +20 -2
- package/dist/cli.js +84 -7
- package/dist/notes.js +2 -2
- package/dist/server.js +40 -0
- package/package.json +1 -1
package/dist/appleScript.js
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
export class AppleScriptError extends Error {
|
|
3
|
+
kind;
|
|
4
|
+
constructor(kind, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "AppleScriptError";
|
|
7
|
+
this.kind = kind;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
const notAuthorizedHelp = "macOS Automation permission is missing or was denied for the notes-poke daemon. " +
|
|
11
|
+
"Open System Settings > Privacy & Security > Automation, find \"notes-poke-node\" (or \"node\"), and enable Notes. " +
|
|
12
|
+
"Then rerun `notes-poke install`. If no toggle appears, run `tccutil reset AppleEvents` in Terminal and rerun install.";
|
|
13
|
+
function isNotAuthorized(stderr) {
|
|
14
|
+
return stderr.includes("-1743") || stderr.includes("Not authorized to send Apple events");
|
|
15
|
+
}
|
|
2
16
|
export async function runAppleScript(script, options = {}) {
|
|
3
17
|
const timeoutMs = options.timeoutMs ?? 20_000;
|
|
4
18
|
return await new Promise((resolve, reject) => {
|
|
@@ -9,7 +23,7 @@ export async function runAppleScript(script, options = {}) {
|
|
|
9
23
|
let stderr = "";
|
|
10
24
|
const timer = setTimeout(() => {
|
|
11
25
|
child.kill("SIGKILL");
|
|
12
|
-
reject(new
|
|
26
|
+
reject(new AppleScriptError("timeout", `AppleScript timed out after ${timeoutMs}ms. If a macOS permission dialog is waiting for approval, click Allow and retry; if dialogs keep appearing, rerun \`notes-poke install\`.`));
|
|
13
27
|
}, timeoutMs);
|
|
14
28
|
child.stdout.setEncoding("utf8");
|
|
15
29
|
child.stderr.setEncoding("utf8");
|
|
@@ -29,7 +43,11 @@ export async function runAppleScript(script, options = {}) {
|
|
|
29
43
|
resolve(stdout.trim());
|
|
30
44
|
return;
|
|
31
45
|
}
|
|
32
|
-
|
|
46
|
+
if (isNotAuthorized(stderr)) {
|
|
47
|
+
reject(new AppleScriptError("not-authorized", notAuthorizedHelp));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
reject(new AppleScriptError("script-error", stderr.trim() || `osascript exited with code ${code}`));
|
|
33
51
|
});
|
|
34
52
|
child.stdin.end(script);
|
|
35
53
|
});
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { execFile, spawn } from "node:child_process";
|
|
4
|
-
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { access, chmod, copyFile, mkdir, readFile, realpath, rm, writeFile } from "node:fs/promises";
|
|
5
5
|
import { constants } from "node:fs";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { join } from "node:path";
|
|
@@ -24,9 +24,79 @@ async function runSetup(options) {
|
|
|
24
24
|
const version = await getVersion();
|
|
25
25
|
console.log(`OK Apple Notes responded: ${version}`);
|
|
26
26
|
}
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
}
|
|
28
|
+
async function ensureDedicatedNode() {
|
|
29
|
+
const binDir = join(stateDir, "bin");
|
|
30
|
+
const binPath = join(binDir, "notes-poke-node");
|
|
31
|
+
const sourcePath = await realpath(process.execPath);
|
|
32
|
+
const smokeTest = async () => {
|
|
33
|
+
const result = await execFileAsync(binPath, ["--version"], { rejectOnError: false });
|
|
34
|
+
return result.code === 0;
|
|
35
|
+
};
|
|
36
|
+
try {
|
|
37
|
+
await access(binPath, constants.X_OK);
|
|
38
|
+
if (await smokeTest()) {
|
|
39
|
+
return binPath;
|
|
40
|
+
}
|
|
41
|
+
await rm(binPath, { force: true });
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// No usable copy yet.
|
|
29
45
|
}
|
|
46
|
+
await mkdir(binDir, { recursive: true });
|
|
47
|
+
await copyFile(sourcePath, binPath);
|
|
48
|
+
await chmod(binPath, 0o755);
|
|
49
|
+
if (await smokeTest()) {
|
|
50
|
+
console.log(`OK dedicated runtime at ${binPath}`);
|
|
51
|
+
return binPath;
|
|
52
|
+
}
|
|
53
|
+
await rm(binPath, { force: true });
|
|
54
|
+
console.warn(`Warning: your node binary (${sourcePath}) is not relocatable; using it directly. Upgrading node may require rerunning \`notes-poke install\`.`);
|
|
55
|
+
return sourcePath;
|
|
56
|
+
}
|
|
57
|
+
async function waitForServer(port) {
|
|
58
|
+
const deadline = Date.now() + 15_000;
|
|
59
|
+
while (Date.now() < deadline) {
|
|
60
|
+
try {
|
|
61
|
+
const response = await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(2_000) });
|
|
62
|
+
if (response.ok) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Server not up yet.
|
|
68
|
+
}
|
|
69
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
70
|
+
}
|
|
71
|
+
throw new Error(`Server did not respond on port ${port} within 15s. Check logs in ${logDir}.`);
|
|
72
|
+
}
|
|
73
|
+
async function authorizeAutomation(port) {
|
|
74
|
+
console.log("");
|
|
75
|
+
console.log("Requesting macOS Automation permission for the background service...");
|
|
76
|
+
console.log("macOS will show a dialog: \"notes-poke-node\" wants access to control \"Notes\". Click Allow.");
|
|
77
|
+
console.log("Waiting up to 2 minutes...");
|
|
78
|
+
let result;
|
|
79
|
+
try {
|
|
80
|
+
const response = await fetch(`http://127.0.0.1:${port}/health/automation?timeoutMs=120000`, {
|
|
81
|
+
signal: AbortSignal.timeout(130_000),
|
|
82
|
+
});
|
|
83
|
+
result = (await response.json());
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
result = { status: "error", message: error instanceof Error ? error.message : String(error) };
|
|
87
|
+
}
|
|
88
|
+
if (result.status === "granted") {
|
|
89
|
+
console.log(`OK Automation permission granted (Notes ${result.appVersion}). You will not be asked again.`);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
console.error("");
|
|
93
|
+
console.error(`Automation permission was not granted (${result.status}).`);
|
|
94
|
+
if (result.message) {
|
|
95
|
+
console.error(result.message);
|
|
96
|
+
}
|
|
97
|
+
console.error("To fix: open System Settings > Privacy & Security > Automation, enable Notes under \"notes-poke-node\", then rerun `notes-poke install`.");
|
|
98
|
+
console.error("If no toggle appears there, run `tccutil reset AppleEvents` and rerun `notes-poke install`.");
|
|
99
|
+
return false;
|
|
30
100
|
}
|
|
31
101
|
function xmlEscape(value) {
|
|
32
102
|
return value
|
|
@@ -124,12 +194,13 @@ ${envEntries ? `\n${envEntries}` : ""}
|
|
|
124
194
|
`;
|
|
125
195
|
}
|
|
126
196
|
async function installServices(options) {
|
|
127
|
-
await runSetup({ touchNotes:
|
|
197
|
+
await runSetup({ touchNotes: false });
|
|
128
198
|
if (options.tunnel) {
|
|
129
199
|
await ensurePokeLogin();
|
|
130
200
|
}
|
|
131
201
|
await mkdir(logDir, { recursive: true });
|
|
132
202
|
await mkdir(launchAgentsDir, { recursive: true });
|
|
203
|
+
const nodeBin = await ensureDedicatedNode();
|
|
133
204
|
const serverPlistPath = join(launchAgentsDir, `${serverLabel}.plist`);
|
|
134
205
|
const tunnelPlistPath = join(launchAgentsDir, `${tunnelLabel}.plist`);
|
|
135
206
|
const url = `http://localhost:${options.port}/mcp`;
|
|
@@ -143,12 +214,19 @@ async function installServices(options) {
|
|
|
143
214
|
shellQuote(options.name),
|
|
144
215
|
...(options.recipe ? ["--recipe"] : []),
|
|
145
216
|
].join(" ");
|
|
146
|
-
await writeLaunchAgent(serverPlistPath, plist(serverLabel, [
|
|
217
|
+
await writeLaunchAgent(serverPlistPath, plist(serverLabel, [nodeBin, cliPath, "start", "--host", options.host, "--port", options.port], join(logDir, "server.log"), join(logDir, "server.error.log"), {
|
|
147
218
|
NOTES_POKE_HOST: options.host,
|
|
148
219
|
NOTES_POKE_PORT: options.port,
|
|
149
220
|
}));
|
|
150
221
|
await bootstrap(serverLabel, serverPlistPath);
|
|
151
222
|
console.log(`OK installed and started ${serverLabel}`);
|
|
223
|
+
await waitForServer(options.port);
|
|
224
|
+
const authorized = await authorizeAutomation(options.port);
|
|
225
|
+
if (!authorized) {
|
|
226
|
+
console.error("Skipping tunnel setup until Automation permission is granted.");
|
|
227
|
+
process.exitCode = 1;
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
152
230
|
if (options.tunnel) {
|
|
153
231
|
await writeLaunchAgent(tunnelPlistPath, plist(tunnelLabel, ["/bin/zsh", "-lc", tunnelCommand], join(logDir, "tunnel.log"), join(logDir, "tunnel.error.log")));
|
|
154
232
|
await bootstrap(tunnelLabel, tunnelPlistPath);
|
|
@@ -192,7 +270,7 @@ async function status() {
|
|
|
192
270
|
program
|
|
193
271
|
.name("notes-poke")
|
|
194
272
|
.description("Local Apple Notes MCP server for Poke")
|
|
195
|
-
.version("0.1.
|
|
273
|
+
.version("0.1.1");
|
|
196
274
|
program
|
|
197
275
|
.command("setup")
|
|
198
276
|
.description("Check local prerequisites for running the Apple Notes MCP server")
|
|
@@ -244,7 +322,6 @@ program
|
|
|
244
322
|
.option("--port <port>", "Port to bind", process.env.NOTES_POKE_PORT ?? "8766")
|
|
245
323
|
.option("--recipe", "Ask Poke CLI to create a shareable recipe link from the tunnel")
|
|
246
324
|
.option("--no-tunnel", "Only install the local MCP server; do not start the Poke tunnel")
|
|
247
|
-
.option("--no-touch-notes", "Do not touch Apple Notes during setup")
|
|
248
325
|
.action((options) => installServices(options));
|
|
249
326
|
program
|
|
250
327
|
.command("uninstall")
|
package/dist/notes.js
CHANGED
|
@@ -140,12 +140,12 @@ on noteJson(n, includeBody)
|
|
|
140
140
|
end tell
|
|
141
141
|
end noteJson
|
|
142
142
|
`;
|
|
143
|
-
export async function getVersion() {
|
|
143
|
+
export async function getVersion(options = {}) {
|
|
144
144
|
return await runAppleScript(`
|
|
145
145
|
tell application "Notes"
|
|
146
146
|
return version
|
|
147
147
|
end tell
|
|
148
|
-
|
|
148
|
+
`, options);
|
|
149
149
|
}
|
|
150
150
|
export async function getAccounts() {
|
|
151
151
|
const output = await runAppleScript(`
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
2
2
|
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
|
3
|
+
import { AppleScriptError } from "./appleScript.js";
|
|
3
4
|
import { createNotesPokeMcpServer } from "./mcp.js";
|
|
5
|
+
import { getVersion } from "./notes.js";
|
|
4
6
|
function bearerAuth(token) {
|
|
5
7
|
return (req, res, next) => {
|
|
6
8
|
if (!token) {
|
|
@@ -25,6 +27,28 @@ function bearerAuth(token) {
|
|
|
25
27
|
function isMcpPath(path) {
|
|
26
28
|
return path === "/mcp" || path.endsWith("/mcp");
|
|
27
29
|
}
|
|
30
|
+
let automationCheck = null;
|
|
31
|
+
function checkAutomation(timeoutMs) {
|
|
32
|
+
if (automationCheck) {
|
|
33
|
+
return automationCheck;
|
|
34
|
+
}
|
|
35
|
+
automationCheck = getVersion({ timeoutMs })
|
|
36
|
+
.then((appVersion) => ({ status: "granted", appVersion }))
|
|
37
|
+
.catch((error) => {
|
|
38
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
39
|
+
if (error instanceof AppleScriptError && error.kind === "not-authorized") {
|
|
40
|
+
return { status: "denied", message };
|
|
41
|
+
}
|
|
42
|
+
if (error instanceof AppleScriptError && error.kind === "timeout") {
|
|
43
|
+
return { status: "timeout", message };
|
|
44
|
+
}
|
|
45
|
+
return { status: "error", message };
|
|
46
|
+
})
|
|
47
|
+
.finally(() => {
|
|
48
|
+
automationCheck = null;
|
|
49
|
+
});
|
|
50
|
+
return automationCheck;
|
|
51
|
+
}
|
|
28
52
|
const mcpRoutePattern = /^(?:\/[^/]+)*\/mcp$/;
|
|
29
53
|
export function startServer(options = {}) {
|
|
30
54
|
const host = options.host ?? process.env.NOTES_POKE_HOST ?? "127.0.0.1";
|
|
@@ -40,6 +64,12 @@ export function startServer(options = {}) {
|
|
|
40
64
|
auth: apiToken ? "bearer" : "none",
|
|
41
65
|
});
|
|
42
66
|
});
|
|
67
|
+
app.get("/health/automation", async (req, res) => {
|
|
68
|
+
const requested = Number.parseInt(String(req.query.timeoutMs ?? ""), 10);
|
|
69
|
+
const timeoutMs = Math.min(Number.isFinite(requested) && requested > 0 ? requested : 120_000, 180_000);
|
|
70
|
+
const result = await checkAutomation(timeoutMs);
|
|
71
|
+
res.json(result);
|
|
72
|
+
});
|
|
43
73
|
app.use((req, res, next) => {
|
|
44
74
|
if (!isMcpPath(req.path)) {
|
|
45
75
|
next();
|
|
@@ -97,6 +127,16 @@ export function startServer(options = {}) {
|
|
|
97
127
|
const httpServer = app.listen(port, host, () => {
|
|
98
128
|
console.log(`notes-poke MCP server listening at http://${host}:${port}/mcp`);
|
|
99
129
|
console.log(apiToken ? "Bearer auth enabled via NOTES_POKE_API_TOKEN" : "Bearer auth disabled for localhost development");
|
|
130
|
+
checkAutomation(120_000)
|
|
131
|
+
.then((result) => {
|
|
132
|
+
if (result.status === "granted") {
|
|
133
|
+
console.log(`Automation permission OK (Notes ${result.appVersion})`);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
console.error(`Automation permission check: ${result.status}. ${result.message ?? ""}`);
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
.catch(() => undefined);
|
|
100
140
|
});
|
|
101
141
|
return httpServer;
|
|
102
142
|
}
|