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.
@@ -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 Error(`AppleScript timed out after ${timeoutMs}ms`));
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
- reject(new Error(stderr.trim() || `osascript exited with code ${code}`));
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
- else {
28
- console.log("Tip: run `notes-poke setup --touch-notes` to request/check macOS Automation permission.");
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: options.touchNotes ?? true });
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, [process.execPath, cliPath, "start", "--host", options.host, "--port", options.port], join(logDir, "server.log"), join(logDir, "server.error.log"), {
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.0");
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "notes-poke",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Local Apple Notes MCP server for Poke",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",