@brigid/mcp-apple-calendar 0.0.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 shadowgate15
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # @brigid/mcp-apple-calendar
2
+
3
+ A read-only Apple Calendar [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server.
4
+ It is the **reference implementation** of brigid's
5
+ [calendar capability contract](../../docs/contracts/calendar.md) — it exposes only the tool the
6
+ brigid GTD skills need (`find-events`), using the contract's canonical tool name so the skills
7
+ detect and use it automatically.
8
+
9
+ **macOS only.** It reads Apple Calendar via macOS **EventKit** — the same store `Calendar.app`
10
+ shows, so it sees every account you've added there (iCloud, Google, Exchange, …). See
11
+ [ADR 0002](../../docs/adr/0002-calendar-connector-eventkit-reference.md) for why.
12
+
13
+ ## Conformance
14
+
15
+ | Capability group | Status |
16
+ | ----------------------------------- | -------------- |
17
+ | **Core** (`find-events`, read-only) | ✅ implemented |
18
+
19
+ Write (event creation/moves) is intentionally out of scope for v1 — see the contract and ADR 0002.
20
+
21
+ ## Permissions
22
+
23
+ No API token or configuration is needed. The first time `find-events` runs, macOS will prompt you
24
+ to grant Calendar access to whatever process is invoking the server; grant it in the dialog, or
25
+ after the fact via **System Settings → Privacy & Security → Calendars**. Access persists across
26
+ runs once granted.
27
+
28
+ If access is denied, not yet granted, or the prompt times out, `find-events` returns an explicit
29
+ tool error — it never reports an empty calendar as a way of papering over a permission problem.
30
+
31
+ ## Structure
32
+
33
+ - `src/index.ts` — `createServer()`, the reusable/testable server factory (registers `find-events`, no transport).
34
+ - `src/bin.ts` — the executable entry (`bin`), which attaches the stdio transport and runs the server.
35
+ - `src/eventkit.ts` — the Node-side bridge to the native shim; translates its output into the contract's `Event` shape.
36
+ - `src/native/find-events.jxa.js` — the JXA (JavaScript for Automation) script that bridges into macOS EventKit. Not compiled by tsc; copied to `dist` as a build asset.
37
+
38
+ ## Develop
39
+
40
+ ```sh
41
+ nx build mcp-apple-calendar # compile to dist/packages/mcp-apple-calendar
42
+ nx lint mcp-apple-calendar --fix # lint + sync package.json deps from the root (single-version policy)
43
+ ```
44
+
45
+ ## Run
46
+
47
+ After building, launch the server over stdio (macOS only):
48
+
49
+ ```sh
50
+ node dist/packages/mcp-apple-calendar/src/bin.js
51
+ ```
52
+
53
+ Or, once published, via `npx @brigid/mcp-apple-calendar`.
54
+
55
+ 🚀
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@brigid/mcp-apple-calendar",
3
+ "version": "0.0.3",
4
+ "description": "Read-only Apple Calendar MCP server (via macOS EventKit) — the reference implementation of brigid's calendar capability contract.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./src/index.js",
8
+ "types": "./src/index.d.ts",
9
+ "bin": {
10
+ "mcp-apple-calendar": "./src/bin.js"
11
+ },
12
+ "files": [
13
+ "src",
14
+ "README.md"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "dependencies": {
20
+ "@modelcontextprotocol/sdk": "^1.29.0",
21
+ "zod": "^4.4.3"
22
+ },
23
+ "module": "./src/index.js"
24
+ }
package/src/bin.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=bin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../../../../packages/mcp-apple-calendar/src/bin.ts"],"names":[],"mappings":""}
package/src/bin.js ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { createServer } from './index.js';
4
+ /**
5
+ * Executable entry point: runs the Apple Calendar MCP server over stdio.
6
+ * Registered as the package `bin`, so it can be launched via `npx @brigid/mcp-apple-calendar`.
7
+ */
8
+ async function main() {
9
+ const server = createServer();
10
+ const transport = new StdioServerTransport();
11
+ await server.connect(transport);
12
+ }
13
+ main().catch((error) => {
14
+ // stderr only — stdout is reserved for the JSON-RPC protocol stream.
15
+ console.error('Fatal error running mcp-apple-calendar server:', error);
16
+ process.exit(1);
17
+ });
18
+ //# sourceMappingURL=bin.js.map
package/src/bin.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.js","sourceRoot":"","sources":["../../../../packages/mcp-apple-calendar/src/bin.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1C;;;GAGG;AACH,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;IAC9B,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACrB,qEAAqE;IACrE,OAAO,CAAC,KAAK,CAAC,gDAAgD,EAAE,KAAK,CAAC,CAAC;IACvE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,19 @@
1
+ export interface Event {
2
+ id: string;
3
+ title: string;
4
+ start: string;
5
+ end: string;
6
+ allDay: boolean;
7
+ location?: string;
8
+ }
9
+ export interface DateRange {
10
+ from: string;
11
+ to: string;
12
+ }
13
+ export interface EventWindow {
14
+ date?: string;
15
+ dateRange?: DateRange;
16
+ }
17
+ /** Run the JXA shim and parse its single-line JSON result. Never returns a partial/empty-on-error result. */
18
+ export declare function findEvents(window: EventWindow): Promise<Event[]>;
19
+ //# sourceMappingURL=eventkit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eventkit.d.ts","sourceRoot":"","sources":["../../../../packages/mcp-apple-calendar/src/eventkit.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB;AAeD,6GAA6G;AAC7G,wBAAsB,UAAU,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,CAwBtE"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Thin bridge to the JXA/EventKit shim in native/find-events.jxa.js — the only place that knows
3
+ * how to reach Apple Calendar. This module speaks the neutral capability-contract Event shape; see
4
+ * docs/contracts/calendar.md for the contract this implements.
5
+ */
6
+ import { execFile } from 'node:child_process';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { dirname, join } from 'node:path';
9
+ const SCRIPT_PATH = join(dirname(fileURLToPath(import.meta.url)), 'native', 'find-events.jxa.js');
10
+ /** Run the JXA shim and parse its single-line JSON result. Never returns a partial/empty-on-error result. */
11
+ export async function findEvents(window) {
12
+ const stdout = await new Promise((resolve, reject) => {
13
+ execFile('osascript', ['-l', 'JavaScript', SCRIPT_PATH, JSON.stringify(window)],
14
+ // maxBuffer raised well above the 1 MB default: a wide dateRange over a dense calendar can
15
+ // emit more than that, and the default would surface as an opaque "stdout maxBuffer exceeded".
16
+ { timeout: 35_000, maxBuffer: 16 * 1024 * 1024 }, (error, out, stderr) => {
17
+ if (error)
18
+ reject(new Error(`osascript failed: ${stderr || error.message}`));
19
+ else
20
+ resolve(out);
21
+ });
22
+ });
23
+ let result;
24
+ try {
25
+ result = JSON.parse(stdout);
26
+ }
27
+ catch {
28
+ throw new Error(`Unparseable response from the calendar shim: ${stdout}`);
29
+ }
30
+ if (!result.ok)
31
+ throw new Error(`[${result.reason}] ${result.message}`);
32
+ return result.events;
33
+ }
34
+ //# sourceMappingURL=eventkit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eventkit.js","sourceRoot":"","sources":["../../../../packages/mcp-apple-calendar/src/eventkit.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAE1C,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,EAAE,oBAAoB,CAAC,CAAC;AAkClG,6GAA6G;AAC7G,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,MAAmB;IAClD,MAAM,MAAM,GAAG,MAAM,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC3D,QAAQ,CACN,WAAW,EACX,CAAC,IAAI,EAAE,YAAY,EAAE,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QACzD,2FAA2F;QAC3F,+FAA+F;QAC/F,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI,EAAE,EAChD,CAAC,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE;YACrB,IAAI,KAAK;gBAAE,MAAM,CAAC,IAAI,KAAK,CAAC,qBAAqB,MAAM,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;;gBACxE,OAAO,CAAC,GAAG,CAAC,CAAC;QACpB,CAAC,CACF,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,IAAI,MAAkB,CAAC;IACvB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAe,CAAC;IAC5C,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,gDAAgD,MAAM,EAAE,CAAC,CAAC;IAC5E,CAAC;IAED,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,IAAI,MAAM,CAAC,MAAM,KAAK,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;IACxE,OAAO,MAAM,CAAC,MAAM,CAAC;AACvB,CAAC"}
package/src/index.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ /**
3
+ * Package version, kept in sync with package.json by `nx release`.
4
+ * Bump lives in package.json; this constant is the runtime source.
5
+ */
6
+ export declare const version = "0.0.1";
7
+ /**
8
+ * Create and configure the Apple Calendar MCP server — the reference implementation of the
9
+ * calendar capability contract. Registers the Core (read-only) tool under its canonical name; no
10
+ * transport is attached here, so callers (bin, tests) decide how to run it.
11
+ */
12
+ export declare function createServer(): McpServer;
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../packages/mcp-apple-calendar/src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAKpE;;;GAGG;AACH,eAAO,MAAM,OAAO,UAAU,CAAC;AAa/B;;;;GAIG;AACH,wBAAgB,YAAY,IAAI,SAAS,CA8BxC"}
package/src/index.js ADDED
@@ -0,0 +1,49 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import { findEvents } from './eventkit.js';
4
+ /**
5
+ * Package version, kept in sync with package.json by `nx release`.
6
+ * Bump lives in package.json; this constant is the runtime source.
7
+ */
8
+ export const version = '0.0.1';
9
+ /** Serialize a successful tool result as JSON text (skills read the JSON). */
10
+ function ok(value) {
11
+ return { content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] };
12
+ }
13
+ /** Turn a thrown error into a readable, non-fatal tool error for the skill to handle. */
14
+ function fail(error) {
15
+ const message = error instanceof Error ? error.message : String(error);
16
+ return { content: [{ type: 'text', text: message }], isError: true };
17
+ }
18
+ /**
19
+ * Create and configure the Apple Calendar MCP server — the reference implementation of the
20
+ * calendar capability contract. Registers the Core (read-only) tool under its canonical name; no
21
+ * transport is attached here, so callers (bin, tests) decide how to run it.
22
+ */
23
+ export function createServer() {
24
+ const server = new McpServer({ name: 'mcp-apple-calendar', version });
25
+ server.registerTool('find-events', {
26
+ title: 'Find events',
27
+ description: 'Query calendar events overlapping a date window.',
28
+ inputSchema: {
29
+ date: z.string().optional().describe('YYYY-MM-DD — mutually exclusive with dateRange'),
30
+ dateRange: z
31
+ .object({ from: z.string(), to: z.string() })
32
+ .optional()
33
+ .describe('Inclusive YYYY-MM-DD span — mutually exclusive with date'),
34
+ },
35
+ }, async ({ date, dateRange }) => {
36
+ try {
37
+ // Fail fast at the tool boundary — reject an ambiguous window before spawning the subprocess.
38
+ if (Boolean(date) === Boolean(dateRange)) {
39
+ throw new Error('Provide exactly one of "date" or "dateRange".');
40
+ }
41
+ return ok({ events: await findEvents({ date, dateRange }) });
42
+ }
43
+ catch (error) {
44
+ return fail(error);
45
+ }
46
+ });
47
+ return server;
48
+ }
49
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../packages/mcp-apple-calendar/src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE3C;;;GAGG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC;AAE/B,8EAA8E;AAC9E,SAAS,EAAE,CAAC,KAAc;IACxB,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;AAC/E,CAAC;AAED,yFAAyF;AACzF,SAAS,IAAI,CAAC,KAAc;IAC1B,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACvE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AACvE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY;IAC1B,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,EAAE,IAAI,EAAE,oBAAoB,EAAE,OAAO,EAAE,CAAC,CAAC;IAEtE,MAAM,CAAC,YAAY,CACjB,aAAa,EACb;QACE,KAAK,EAAE,aAAa;QACpB,WAAW,EAAE,kDAAkD;QAC/D,WAAW,EAAE;YACX,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,gDAAgD,CAAC;YACtF,SAAS,EAAE,CAAC;iBACT,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;iBAC5C,QAAQ,EAAE;iBACV,QAAQ,CAAC,0DAA0D,CAAC;SACxE;KACF,EACD,KAAK,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE;QAC5B,IAAI,CAAC;YACH,8FAA8F;YAC9F,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;gBACzC,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;YACnE,CAAC;YACD,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC,CAAC;QAC/D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC;QACrB,CAAC;IACH,CAAC,CACF,CAAC;IAEF,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,159 @@
1
+ /* eslint-disable */
2
+ // JXA (JavaScript for Automation) script, run via `osascript -l JavaScript`. Not compiled by tsc —
3
+ // copied verbatim to dist by the build (see project.json assets). Bridges into macOS EventKit to
4
+ // read calendar events. Always returns exactly one line of JSON on stdout, even on failure; never
5
+ // throws uncaught, so the Node side (../eventkit.ts) has a single, reliable parse path.
6
+ //
7
+ // Contract with the Node caller: argv[0] is `{"date":"YYYY-MM-DD"}` or
8
+ // `{"dateRange":{"from":"YYYY-MM-DD","to":"YYYY-MM-DD"}}`. Output is
9
+ // `{"ok":true,"events":[...]}` or `{"ok":false,"reason":"...","message":"..."}`.
10
+ ObjC.import('EventKit');
11
+
12
+ function pad(n) {
13
+ return n < 10 ? '0' + n : '' + n;
14
+ }
15
+
16
+ function parseWindow(input) {
17
+ if (input.date && input.dateRange) {
18
+ throw { reason: 'bad-input', message: '"date" and "dateRange" are mutually exclusive.' };
19
+ }
20
+ if (!input.date && !input.dateRange) {
21
+ throw { reason: 'bad-input', message: 'One of "date" or "dateRange" is required.' };
22
+ }
23
+ if (input.dateRange && (!input.dateRange.from || !input.dateRange.to)) {
24
+ throw { reason: 'bad-input', message: 'dateRange requires both "from" and "to".' };
25
+ }
26
+ // Reject reversed bounds: an inverted predicate window returns [] on EventKit, which would
27
+ // masquerade as a genuinely empty result and break the "empty means truly empty" guarantee.
28
+ if (input.dateRange && input.dateRange.from > input.dateRange.to) {
29
+ throw { reason: 'bad-input', message: 'dateRange "from" must not be after "to".' };
30
+ }
31
+ return input.date ? { from: input.date, to: input.date } : input.dateRange;
32
+ }
33
+
34
+ function localMidnight(isoDate) {
35
+ var parts = isoDate.split('-').map(Number);
36
+ var comps = $.NSDateComponents.alloc.init;
37
+ comps.year = parts[0];
38
+ comps.month = parts[1];
39
+ comps.day = parts[2];
40
+ comps.hour = 0;
41
+ comps.minute = 0;
42
+ comps.second = 0;
43
+ var date = $.NSCalendar.currentCalendar.dateFromComponents(comps);
44
+ if (!date) throw { reason: 'bad-input', message: 'Invalid date: ' + isoDate };
45
+ return date;
46
+ }
47
+
48
+ function addDays(date, n) {
49
+ var comps = $.NSDateComponents.alloc.init;
50
+ comps.day = n;
51
+ return $.NSCalendar.currentCalendar.dateByAddingComponentsToDateOptions(comps, date, 0);
52
+ }
53
+
54
+ function ymd(date) {
55
+ var units = $.NSCalendarUnitYear | $.NSCalendarUnitMonth | $.NSCalendarUnitDay;
56
+ var comps = $.NSCalendar.currentCalendar.componentsFromDate(units, date);
57
+ return comps.year + '-' + pad(comps.month) + '-' + pad(comps.day);
58
+ }
59
+
60
+ /** Authorized/fullAccess is EKAuthorizationStatus 3 on every macOS version that supports it. */
61
+ function isAuthorized(status) {
62
+ return Number(status) === 3;
63
+ }
64
+
65
+ /**
66
+ * Ensure calendar read access, prompting the user if this is the first run. Returns once resolved;
67
+ * throws a structured {reason, message} on denial or on an unresolved prompt (no UI session, or the
68
+ * user hasn't responded within the bound). Never hangs indefinitely.
69
+ */
70
+ function ensureAccess(store) {
71
+ var status = $.EKEventStore.authorizationStatusForEntityType($.EKEntityTypeEvent);
72
+ if (isAuthorized(status)) return;
73
+
74
+ var granted = null;
75
+ store.requestAccessToEntityTypeCompletion($.EKEntityTypeEvent, function (g) {
76
+ granted = g;
77
+ });
78
+
79
+ var waited = 0;
80
+ var boundSeconds = 30;
81
+ while (granted === null && waited < boundSeconds) {
82
+ delay(0.25);
83
+ waited += 0.25;
84
+ }
85
+
86
+ if (granted === null) {
87
+ throw {
88
+ reason: 'permission-timeout',
89
+ message:
90
+ 'Calendar access prompt did not resolve within ' +
91
+ boundSeconds +
92
+ 's. Grant access in System Settings → Privacy & Security → Calendars, then retry.',
93
+ };
94
+ }
95
+ if (!granted) {
96
+ throw {
97
+ reason: 'permission-denied',
98
+ message:
99
+ 'Calendar access was not granted. Enable it in System Settings → Privacy & Security → Calendars.',
100
+ };
101
+ }
102
+ }
103
+
104
+ function findEvents(input) {
105
+ var window = parseWindow(input);
106
+ var store = $.EKEventStore.alloc.init;
107
+
108
+ ensureAccess(store);
109
+
110
+ var calendars = store.calendarsForEntityType($.EKEntityTypeEvent);
111
+ if (calendars.count === 0) {
112
+ throw {
113
+ reason: 'no-calendars',
114
+ message: 'Calendar access is granted but no calendars are available to read.',
115
+ };
116
+ }
117
+
118
+ var start = localMidnight(window.from);
119
+ var end = addDays(localMidnight(window.to), 1);
120
+
121
+ var predicate = store.predicateForEventsWithStartDateEndDateCalendars(start, end, calendars);
122
+ var events = store.eventsMatchingPredicate(predicate);
123
+
124
+ var isoFmt = $.NSISO8601DateFormatter.alloc.init;
125
+ isoFmt.formatOptions =
126
+ $.NSISO8601DateFormatWithInternetDateTime | $.NSISO8601DateFormatWithColonSeparatorInTimeZone;
127
+ isoFmt.timeZone = $.NSTimeZone.localTimeZone;
128
+
129
+ var out = [];
130
+ for (var i = 0; i < events.count; i++) {
131
+ var ev = events.objectAtIndex(i);
132
+ var allDay = Boolean(ObjC.unwrap(ev.isAllDay));
133
+ var location = ev.location;
134
+ var event = {
135
+ id: ObjC.unwrap(ev.eventIdentifier),
136
+ title: ObjC.unwrap(ev.title),
137
+ allDay: allDay,
138
+ start: allDay ? ymd(ev.startDate) : isoFmt.stringFromDate(ev.startDate).js,
139
+ end: allDay ? ymd(ev.endDate) : isoFmt.stringFromDate(ev.endDate).js,
140
+ };
141
+ if (location) event.location = ObjC.unwrap(location);
142
+ out.push(event);
143
+ }
144
+
145
+ return out;
146
+ }
147
+
148
+ function run(argv) {
149
+ try {
150
+ var input = JSON.parse(argv[0] || '{}');
151
+ var events = findEvents(input);
152
+ return JSON.stringify({ ok: true, events: events });
153
+ } catch (error) {
154
+ if (error && error.reason) {
155
+ return JSON.stringify({ ok: false, reason: error.reason, message: error.message });
156
+ }
157
+ return JSON.stringify({ ok: false, reason: 'error', message: String(error) });
158
+ }
159
+ }