wakeupneo-mcp 0.1.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 ADDED
@@ -0,0 +1,151 @@
1
+ # WakeUpNeo MCP Server
2
+
3
+ Drive your Eisenhower Matrix, Compass documents, and guided rituals from any MCP client (Claude Code, Claude Desktop, Cursor, etc.).
4
+
5
+ ## Quick Start
6
+
7
+ ### 1. Authenticate
8
+
9
+ ```bash
10
+ npx wakeupneo-mcp setup
11
+ ```
12
+
13
+ This opens your browser for Google SSO login (same as the web app). Credentials are saved to `~/.wakeupneo/auth.json` — no manual token management.
14
+
15
+ ```bash
16
+ # Check auth status
17
+ npx wakeupneo-mcp status
18
+ ```
19
+
20
+ ### 2. Add to your MCP client
21
+
22
+ **Claude Code** (`~/.claude/settings.json`):
23
+
24
+ ```json
25
+ {
26
+ "mcpServers": {
27
+ "wakeupneo": {
28
+ "type": "stdio",
29
+ "command": "npx",
30
+ "args": ["-y", "wakeupneo-mcp@latest"]
31
+ }
32
+ }
33
+ }
34
+ ```
35
+
36
+ **Alternative: environment variables** (for CI or custom setups):
37
+
38
+ ```json
39
+ {
40
+ "mcpServers": {
41
+ "wakeupneo": {
42
+ "type": "stdio",
43
+ "command": "node",
44
+ "args": ["/path/to/wakeupneo/wakeupneo-mcp/bin/wakeupneo-mcp.js"],
45
+ "env": {
46
+ "WAKEUPNEO_SUPABASE_URL": "https://your-project.supabase.co",
47
+ "WAKEUPNEO_SUPABASE_ANON_KEY": "your-anon-key",
48
+ "WAKEUPNEO_REFRESH_TOKEN": "your-refresh-token"
49
+ }
50
+ }
51
+ }
52
+ }
53
+ ```
54
+
55
+ ### Environment Variables
56
+
57
+ | Variable | Required | Description |
58
+ |---|---|---|
59
+ | `WAKEUPNEO_APP_URL` | No | Web app URL (default: `https://www.wakeupneo.co`) |
60
+ | `WAKEUPNEO_SERVICE_ROLE_KEY` | No | Required for admin tools only |
61
+ | `WAKEUPNEO_SUPABASE_URL` | No | Fallback if `~/.wakeupneo/auth.json` not present |
62
+ | `WAKEUPNEO_SUPABASE_ANON_KEY` | No | Fallback if `~/.wakeupneo/auth.json` not present |
63
+ | `WAKEUPNEO_REFRESH_TOKEN` | No | Fallback if `~/.wakeupneo/auth.json` not present |
64
+
65
+ ## Available Tools
66
+
67
+ ### Matrix
68
+ - **get_matrix** — Full Eisenhower Matrix view (task counts, per-quadrant lists, flagged items)
69
+ - **move_task** — Move a task between quadrants
70
+ - **approve_schedule** — Batch-approve AI-proposed task placements
71
+
72
+ ### Tasks
73
+ - **create_task** — Create a native task
74
+ - **update_task** — Update task content, quadrant, priority, dates
75
+ - **complete_task** — Mark task as done
76
+ - **delete_task** — Remove a task
77
+ - **list_projects** — List native projects
78
+ - **create_project** — Create a native project
79
+
80
+ ### Documents (Compass)
81
+ - **list_documents** — Browse documents by folder
82
+ - **get_document** — Read a document's full content
83
+ - **create_document** — Create a new document
84
+ - **update_document** — Edit content or title
85
+ - **delete_document** — Remove a document
86
+ - **get_journal** — Recent journal entries
87
+ - **write_journal** — Write/append to today's journal
88
+ - **get_foundation** — Read all foundation docs (Values, Goals, Challenges, People, Soul)
89
+
90
+ ### Rituals
91
+ - **start_ritual** — Begin a guided workflow (morning, daily-brief, weekly-plan, weekly-wrapup)
92
+ - **advance_ritual** — Move to the next step
93
+ - **complete_ritual** — Finish the active ritual
94
+ - **cancel_ritual** — Cancel without completing
95
+ - **get_ritual_status** — Check current progress
96
+ - **list_rituals** — See available ritual types
97
+ - **get_ritual_history** — Past ritual completions
98
+
99
+ ### Browser & Settings
100
+ - **focus_browser** — Open/focus WakeUpNeo in the browser (respects Live Mirror)
101
+ - **get_settings** — View MCP settings (Live Mirror, etc.)
102
+ - **set_live_mirror** — Toggle Live Mirror on/off
103
+
104
+ ### Admin (requires service role key)
105
+ - **admin_get_users** — List active users
106
+ - **admin_feature_flags** — View/toggle feature flags
107
+ - **admin_usage_metrics** — Usage stats (rituals, docs, conversations)
108
+ - **admin_run_as_user** — Inspect a user's matrix state
109
+ - **admin_run_e2e** — Run Playwright e2e tests, returns pass/fail + report path
110
+
111
+ ## Live Mirror
112
+
113
+ **Live Mirror** controls whether MCP actions navigate the browser in real-time.
114
+
115
+ - **On** (default): MCP actions open/focus WakeUpNeo and navigate to the relevant view. Changes appear live.
116
+ - **Off**: MCP writes data to Supabase silently. Browser stays where it is.
117
+
118
+ Toggle via MCP: `set_live_mirror({ enabled: false })`
119
+
120
+ Per-action override: `focus_browser({ path: '/compass', live_mirror: true })` forces browser focus even when Live Mirror is off.
121
+
122
+ ## How It Works
123
+
124
+ ```
125
+ MCP Client (Claude Code, Cursor, etc.)
126
+ │ stdio
127
+
128
+ WakeUpNeo MCP Server (local)
129
+ │ authenticated Supabase calls
130
+
131
+ Supabase (remote)
132
+ │ Realtime subscriptions
133
+
134
+ WakeUpNeo Web UI (browser)
135
+ → navigates to the active step
136
+ → shows content building in real-time
137
+ → presents approval UI when human input needed
138
+ ```
139
+
140
+ The MCP server authenticates via your Supabase session (RLS enforced). The web app subscribes to Supabase Realtime and updates automatically — tasks appear, documents refresh, rituals progress, views navigate.
141
+
142
+ ## Authentication Flow
143
+
144
+ The `setup` command uses browser-based OAuth (like `gh auth login`):
145
+
146
+ 1. Starts a temporary local HTTP server
147
+ 2. Opens browser to WakeUpNeo's MCP auth page
148
+ 3. You log in via Google SSO (one click if already logged in)
149
+ 4. Tokens are sent back to the local server
150
+ 5. Saved to `~/.wakeupneo/auth.json` (file permissions: 600)
151
+ 6. Auto-refreshed on every server start — no manual token management
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * WakeUpNeo MCP Server — CLI entry point
5
+ *
6
+ * Usage:
7
+ * npx wakeupneo-mcp — Start the MCP server (stdio transport)
8
+ * npx wakeupneo-mcp setup — Authenticate via browser (like `gh auth login`)
9
+ * npx wakeupneo-mcp status — Show current auth status
10
+ *
11
+ * Auth is loaded from ~/.wakeupneo/auth.json (created by `setup`).
12
+ * Falls back to env vars: WAKEUPNEO_SUPABASE_URL, WAKEUPNEO_SUPABASE_ANON_KEY, WAKEUPNEO_REFRESH_TOKEN
13
+ */
14
+
15
+ import { setup, loadTokens } from '../src/auth.js';
16
+
17
+ const command = process.argv[2];
18
+
19
+ if (command === 'setup') {
20
+ try {
21
+ await setup();
22
+ process.exit(0);
23
+ } catch (err) {
24
+ console.error(`\n Setup failed: ${err.message}\n`);
25
+ process.exit(1);
26
+ }
27
+ } else if (command === 'status') {
28
+ const tokens = await loadTokens();
29
+ if (tokens) {
30
+ console.log(`\n Authenticated as: ${tokens.email || 'unknown'}`);
31
+ console.log(` Supabase URL: ${tokens.supabaseUrl}`);
32
+ console.log(` Saved at: ${tokens.savedAt || 'unknown'}\n`);
33
+ } else {
34
+ console.log('\n Not authenticated. Run `npx wakeupneo-mcp setup` to log in.\n');
35
+ }
36
+ process.exit(0);
37
+ } else {
38
+ // Default: start the MCP server
39
+ const { createServer } = await import('../src/server.js');
40
+ const server = await createServer();
41
+ await server.start();
42
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "wakeupneo-mcp",
3
+ "version": "0.1.0",
4
+ "description": "WakeUpNeo MCP server — drive your Eisenhower Matrix, Compass docs, and rituals from any MCP client",
5
+ "type": "module",
6
+ "bin": {
7
+ "wakeupneo-mcp": "./bin/wakeupneo-mcp.js"
8
+ },
9
+ "main": "src/server.js",
10
+ "files": [
11
+ "bin/",
12
+ "src/",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "start": "node bin/wakeupneo-mcp.js"
17
+ },
18
+ "dependencies": {
19
+ "@modelcontextprotocol/sdk": "^1.12.1",
20
+ "@supabase/supabase-js": "^2.89.0"
21
+ },
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "keywords": [
26
+ "mcp",
27
+ "wakeupneo",
28
+ "eisenhower",
29
+ "productivity",
30
+ "task-management"
31
+ ],
32
+ "license": "MIT"
33
+ }
package/src/auth.js ADDED
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Authentication for the WakeUpNeo MCP server.
3
+ *
4
+ * Supports two auth methods:
5
+ * 1. Token file at ~/.wakeupneo/auth.json (created via `npx wakeupneo-mcp setup`)
6
+ * 2. Environment variables (WAKEUPNEO_SUPABASE_URL, WAKEUPNEO_SUPABASE_ANON_KEY, WAKEUPNEO_REFRESH_TOKEN)
7
+ *
8
+ * The `setup` command runs a browser-based OAuth flow (like `gh auth login`):
9
+ * - Starts a local HTTP server on a random port
10
+ * - Opens browser to WakeUpNeo's MCP auth page
11
+ * - User logs in via Google SSO (one click if already logged in)
12
+ * - Tokens are sent back to the local server via redirect
13
+ * - Saved to ~/.wakeupneo/auth.json for future use
14
+ */
15
+
16
+ import { createServer } from 'http';
17
+ import { exec } from 'child_process';
18
+ import { promisify } from 'util';
19
+ import { readFile, writeFile, mkdir, open, unlink } from 'fs/promises';
20
+ import { homedir } from 'os';
21
+ import { join } from 'path';
22
+
23
+ const execAsync = promisify(exec);
24
+
25
+ const AUTH_DIR = join(homedir(), '.wakeupneo');
26
+ const AUTH_FILE = join(AUTH_DIR, 'auth.json');
27
+ const LOCK_FILE = join(AUTH_DIR, 'auth.lock');
28
+
29
+ const LOCK_TIMEOUT_MS = 10_000; // treat lock as stale after 10s
30
+ const LOCK_RETRY_MS = 150;
31
+
32
+ /**
33
+ * Acquire an exclusive file lock before refreshing tokens.
34
+ * Uses atomic O_EXCL creation — safe on all POSIX systems.
35
+ * Stale locks (from crashed processes) are removed after LOCK_TIMEOUT_MS.
36
+ */
37
+ export async function acquireLock() {
38
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
39
+ while (Date.now() < deadline) {
40
+ try {
41
+ const fh = await open(LOCK_FILE, 'wx'); // O_CREAT | O_EXCL — atomic
42
+ await fh.close();
43
+ return;
44
+ } catch (err) {
45
+ if (err.code !== 'EEXIST') throw err;
46
+ await new Promise(r => setTimeout(r, LOCK_RETRY_MS));
47
+ }
48
+ }
49
+ // Stale lock — clear it and proceed
50
+ try { await unlink(LOCK_FILE); } catch {}
51
+ }
52
+
53
+ /**
54
+ * Release the file lock.
55
+ */
56
+ export async function releaseLock() {
57
+ try { await unlink(LOCK_FILE); } catch {}
58
+ }
59
+
60
+ const DEFAULT_APP_URL = 'https://www.wakeupneo.co';
61
+
62
+ function getAppUrl() {
63
+ return process.env.WAKEUPNEO_APP_URL || DEFAULT_APP_URL;
64
+ }
65
+
66
+ /**
67
+ * Load saved credentials from ~/.wakeupneo/auth.json
68
+ * @returns {object|null} — { supabaseUrl, supabaseAnonKey, refreshToken } or null
69
+ */
70
+ export async function loadTokens() {
71
+ try {
72
+ const raw = await readFile(AUTH_FILE, 'utf-8');
73
+ const data = JSON.parse(raw);
74
+ if (data.supabaseUrl && data.supabaseAnonKey && data.refreshToken) {
75
+ return data;
76
+ }
77
+ return null;
78
+ } catch {
79
+ return null;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Save credentials to ~/.wakeupneo/auth.json
85
+ */
86
+ export async function saveTokens({ supabaseUrl, supabaseAnonKey, refreshToken, email }) {
87
+ await mkdir(AUTH_DIR, { recursive: true });
88
+ const data = {
89
+ supabaseUrl,
90
+ supabaseAnonKey,
91
+ refreshToken,
92
+ email,
93
+ savedAt: new Date().toISOString(),
94
+ };
95
+ await writeFile(AUTH_FILE, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 });
96
+ }
97
+
98
+ /**
99
+ * Get credentials from auth.json or environment variables.
100
+ * Auth file takes precedence; env vars are the fallback.
101
+ * @returns {{ supabaseUrl: string, supabaseAnonKey: string, refreshToken: string }}
102
+ */
103
+ export async function getCredentials() {
104
+ // Try auth file first
105
+ const saved = await loadTokens();
106
+ if (saved) {
107
+ return saved;
108
+ }
109
+
110
+ // Fall back to environment variables
111
+ const supabaseUrl = process.env.WAKEUPNEO_SUPABASE_URL;
112
+ const supabaseAnonKey = process.env.WAKEUPNEO_SUPABASE_ANON_KEY;
113
+ const refreshToken = process.env.WAKEUPNEO_REFRESH_TOKEN;
114
+
115
+ if (supabaseUrl && supabaseAnonKey && refreshToken) {
116
+ return { supabaseUrl, supabaseAnonKey, refreshToken };
117
+ }
118
+
119
+ throw new Error(
120
+ 'Not authenticated. Run `npx wakeupneo-mcp setup` to log in, ' +
121
+ 'or set WAKEUPNEO_SUPABASE_URL, WAKEUPNEO_SUPABASE_ANON_KEY, and WAKEUPNEO_REFRESH_TOKEN.'
122
+ );
123
+ }
124
+
125
+ /**
126
+ * Run the interactive setup flow.
127
+ * Opens the browser to WakeUpNeo's MCP auth page, waits for the OAuth callback.
128
+ */
129
+ export async function setup() {
130
+ console.log('\n WakeUpNeo MCP — Setup\n');
131
+ console.log(' Opening browser for authentication...\n');
132
+
133
+ const appUrl = getAppUrl();
134
+
135
+ return new Promise((resolve, reject) => {
136
+ const server = createServer((req, res) => {
137
+ const url = new URL(req.url, `http://localhost`);
138
+
139
+ if (url.pathname === '/callback') {
140
+ const supabaseUrl = url.searchParams.get('supabase_url');
141
+ const supabaseAnonKey = url.searchParams.get('supabase_anon_key');
142
+ const refreshToken = url.searchParams.get('refresh_token');
143
+ const email = url.searchParams.get('email');
144
+
145
+ if (!supabaseUrl || !supabaseAnonKey || !refreshToken) {
146
+ res.writeHead(400, { 'Content-Type': 'text/html' });
147
+ res.end('<html><body><h2>Authentication failed</h2><p>Missing tokens. Please try again.</p></body></html>');
148
+ server.close();
149
+ reject(new Error('Missing tokens in callback'));
150
+ return;
151
+ }
152
+
153
+ // Save credentials
154
+ saveTokens({ supabaseUrl, supabaseAnonKey, refreshToken, email })
155
+ .then(() => {
156
+ res.writeHead(200, { 'Content-Type': 'text/html' });
157
+ res.end(`
158
+ <html>
159
+ <body style="font-family: system-ui; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #0a0a0a; color: #e5e5e5;">
160
+ <div style="text-align: center;">
161
+ <h2 style="color: #22c55e;">&#10003; Authenticated</h2>
162
+ <p>Logged in as <strong>${email || 'unknown'}</strong></p>
163
+ <p style="color: #737373;">You can close this tab and return to your terminal.</p>
164
+ </div>
165
+ </body>
166
+ </html>
167
+ `);
168
+
169
+ console.log(` Authenticated as ${email || 'unknown'}`);
170
+ console.log(` Credentials saved to ${AUTH_FILE}\n`);
171
+
172
+ server.close();
173
+ resolve();
174
+ })
175
+ .catch((err) => {
176
+ res.writeHead(500, { 'Content-Type': 'text/html' });
177
+ res.end('<html><body><h2>Error saving credentials</h2></body></html>');
178
+ server.close();
179
+ reject(err);
180
+ });
181
+ return;
182
+ }
183
+
184
+ // Health check / catch-all
185
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
186
+ res.end('WakeUpNeo MCP auth server');
187
+ });
188
+
189
+ // Listen on random port
190
+ server.listen(0, '127.0.0.1', async () => {
191
+ const port = server.address().port;
192
+ const authUrl = `${appUrl}/auth/mcp?callback_port=${port}`;
193
+
194
+ console.log(` Auth server listening on port ${port}`);
195
+ console.log(` Opening: ${authUrl}\n`);
196
+
197
+ // Open browser
198
+ try {
199
+ const platform = process.platform;
200
+ if (platform === 'darwin') {
201
+ await execAsync(`open "${authUrl}"`);
202
+ } else if (platform === 'linux') {
203
+ await execAsync(`xdg-open "${authUrl}"`);
204
+ } else if (platform === 'win32') {
205
+ await execAsync(`start "" "${authUrl}"`);
206
+ }
207
+ } catch {
208
+ console.log(` Could not open browser automatically.`);
209
+ console.log(` Please open this URL manually: ${authUrl}\n`);
210
+ }
211
+
212
+ console.log(' Waiting for authentication...\n');
213
+
214
+ // Timeout after 5 minutes
215
+ setTimeout(() => {
216
+ console.log(' Authentication timed out. Please try again.\n');
217
+ server.close();
218
+ reject(new Error('Authentication timed out'));
219
+ }, 5 * 60 * 1000);
220
+ });
221
+
222
+ server.on('error', (err) => {
223
+ reject(new Error(`Failed to start auth server: ${err.message}`));
224
+ });
225
+ });
226
+ }
package/src/browser.js ADDED
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Browser automation — open or focus WakeUpNeo in the default browser.
3
+ * macOS-specific: uses `open` command and AppleScript for tab focusing.
4
+ *
5
+ * Supports Live Mirror setting — when disabled, browser focus is skipped.
6
+ */
7
+
8
+ import { exec } from 'child_process';
9
+ import { promisify } from 'util';
10
+ import { readFile } from 'fs/promises';
11
+ import { homedir } from 'os';
12
+ import { join } from 'path';
13
+ import { getSupabase, getUserId } from './supabase.js';
14
+
15
+ const execAsync = promisify(exec);
16
+
17
+ const DEFAULT_APP_URL = 'https://www.wakeupneo.co';
18
+ const AUTH_FILE = join(homedir(), '.wakeupneo', 'auth.json');
19
+
20
+ // Debounce: suppress duplicate focus calls fired within 500ms of each other
21
+ let focusDebounceTimer = null;
22
+ let focusDebounceResolvers = [];
23
+
24
+ function debouncedFocus(fn) {
25
+ return new Promise((resolve) => {
26
+ focusDebounceResolvers.push(resolve);
27
+ if (focusDebounceTimer) clearTimeout(focusDebounceTimer);
28
+ focusDebounceTimer = setTimeout(async () => {
29
+ focusDebounceTimer = null;
30
+ const resolvers = focusDebounceResolvers.splice(0);
31
+ try {
32
+ const result = await fn();
33
+ resolvers.forEach(r => r(result));
34
+ } catch {
35
+ resolvers.forEach(r => r({ action: 'failed' }));
36
+ }
37
+ }, 300);
38
+ });
39
+ }
40
+
41
+ async function getAppUrl(path = '') {
42
+ let base = process.env.WAKEUPNEO_APP_URL;
43
+ if (!base) {
44
+ try {
45
+ const raw = await readFile(AUTH_FILE, 'utf-8');
46
+ const data = JSON.parse(raw);
47
+ if (data.appUrl) base = data.appUrl;
48
+ } catch {
49
+ // ignore — fall through to default
50
+ }
51
+ }
52
+ base = base || DEFAULT_APP_URL;
53
+ return path ? `${base}${path}` : base;
54
+ }
55
+
56
+ /**
57
+ * Check if Live Mirror is enabled for the current user.
58
+ * Reads from user_profiles.settings.MCP_SETTINGS.live_mirror (default: true).
59
+ */
60
+ async function isLiveMirrorEnabled() {
61
+ try {
62
+ const supabase = getSupabase();
63
+ const userId = getUserId();
64
+ const { data } = await supabase
65
+ .from('user_profiles')
66
+ .select('settings')
67
+ .eq('user_id', userId)
68
+ .maybeSingle();
69
+ return data?.settings?.MCP_SETTINGS?.live_mirror !== false;
70
+ } catch {
71
+ // If we can't read the setting (e.g., not initialized yet), default to enabled
72
+ return true;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Open WakeUpNeo in the default browser, or focus an existing tab.
78
+ * Respects the Live Mirror setting — when disabled, returns a skipped result.
79
+ *
80
+ * @param {string} [path] — Optional path to navigate to (e.g., '/compass', '/settings')
81
+ * @param {object} [options] — Options
82
+ * @param {boolean} [options.live_mirror] — Per-action override (true/false). Omit to use user setting.
83
+ * @returns {Promise<{ action: string, url: string }>}
84
+ */
85
+ export async function focusBrowser(path = '', { live_mirror } = {}) {
86
+ const url = await getAppUrl(path);
87
+
88
+ // Check Live Mirror setting (per-action override takes precedence)
89
+ const mirrorEnabled = live_mirror !== undefined ? live_mirror : await isLiveMirrorEnabled();
90
+ if (!mirrorEnabled) {
91
+ return { action: 'skipped', url, reason: 'Live Mirror is off' };
92
+ }
93
+
94
+ return debouncedFocus(() => {
95
+ const platform = process.platform;
96
+ if (platform === 'darwin') return focusMacOS(url);
97
+ if (platform === 'linux') return focusLinux(url);
98
+ if (platform === 'win32') return focusWindows(url);
99
+ return execAsync(`open "${url}"`).then(() => ({ action: 'opened', url }));
100
+ });
101
+ }
102
+
103
+ async function focusMacOS(url) {
104
+ // Try to focus an existing Chrome tab first
105
+ try {
106
+ const script = `
107
+ tell application "Google Chrome"
108
+ set found to false
109
+ repeat with w in windows
110
+ set tabIndex to 0
111
+ repeat with t in tabs of w
112
+ set tabIndex to tabIndex + 1
113
+ if URL of t contains "wakeupneo" or URL of t contains "localhost:5173" then
114
+ set active tab index of w to tabIndex
115
+ set index of w to 1
116
+ activate
117
+ set found to true
118
+ exit repeat
119
+ end if
120
+ end repeat
121
+ if found then exit repeat
122
+ end repeat
123
+ if not found then
124
+ open location "${url}"
125
+ activate
126
+ end if
127
+ end tell
128
+ `;
129
+ await execAsync(`osascript -e '${script.replace(/'/g, "'\\''")}'`);
130
+ return { action: 'focused', url };
131
+ } catch {
132
+ // Chrome not running or AppleScript failed — try Safari
133
+ try {
134
+ const safariScript = `
135
+ tell application "Safari"
136
+ set found to false
137
+ repeat with w in windows
138
+ set tabIndex to 0
139
+ repeat with t in tabs of w
140
+ set tabIndex to tabIndex + 1
141
+ if URL of t contains "wakeupneo" or URL of t contains "localhost:5173" then
142
+ set current tab of w to t
143
+ set index of w to 1
144
+ activate
145
+ set found to true
146
+ exit repeat
147
+ end if
148
+ end repeat
149
+ if found then exit repeat
150
+ end repeat
151
+ if not found then
152
+ open location "${url}"
153
+ activate
154
+ end if
155
+ end tell
156
+ `;
157
+ await execAsync(`osascript -e '${safariScript.replace(/'/g, "'\\''")}'`);
158
+ return { action: 'focused', url };
159
+ } catch {
160
+ // Fallback: just open the URL in default browser
161
+ await execAsync(`open "${url}"`);
162
+ return { action: 'opened', url };
163
+ }
164
+ }
165
+ }
166
+
167
+ async function focusLinux(url) {
168
+ try {
169
+ await execAsync(`xdg-open "${url}"`);
170
+ return { action: 'opened', url };
171
+ } catch {
172
+ return { action: 'failed', url, error: 'xdg-open not available' };
173
+ }
174
+ }
175
+
176
+ async function focusWindows(url) {
177
+ try {
178
+ await execAsync(`start "" "${url}"`);
179
+ return { action: 'opened', url };
180
+ } catch {
181
+ return { action: 'failed', url, error: 'Failed to open browser' };
182
+ }
183
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * MCP Navigation — broadcasts navigation hints to the frontend via Supabase Realtime.
3
+ *
4
+ * Uses broadcast (not DB writes) so there's no race condition with the settings blob
5
+ * and no migration required. If the frontend isn't subscribed the event is silently dropped —
6
+ * navigation hints are best-effort and never block tool calls.
7
+ */
8
+
9
+ import { getSupabase, getUserId } from './supabase.js';
10
+
11
+ /**
12
+ * Broadcast a navigation signal so the frontend can switch to the relevant project/view.
13
+ * @param {object} opts
14
+ * @param {string|null} opts.project_id — Native project UUID (null = All Tasks)
15
+ * @param {string} opts.source — 'native' | 'todoist' | 'jira' | 'asana' | 'github'
16
+ * @param {string|null} opts.quadrant — Target quadrant hint ('do','decide','delegate','delete','inbox')
17
+ */
18
+ export async function broadcastNavigation({ project_id = null, source = 'native', quadrant = null } = {}) {
19
+ try {
20
+ const supabase = getSupabase();
21
+ const userId = getUserId();
22
+
23
+ const channel = supabase.channel(`mcp:navigation:${userId}`);
24
+
25
+ await new Promise((resolve) => {
26
+ channel.subscribe((status) => {
27
+ if (status === 'SUBSCRIBED') resolve();
28
+ });
29
+ });
30
+
31
+ await channel.send({
32
+ type: 'broadcast',
33
+ event: 'navigate',
34
+ payload: { project_id, source, quadrant, timestamp: Date.now() },
35
+ });
36
+
37
+ await supabase.removeChannel(channel);
38
+ } catch {
39
+ // Navigation is best-effort — never fail a tool call over this
40
+ }
41
+ }