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 +151 -0
- package/bin/wakeupneo-mcp.js +42 -0
- package/package.json +33 -0
- package/src/auth.js +226 -0
- package/src/browser.js +183 -0
- package/src/navigation.js +41 -0
- package/src/ritual-engine.js +260 -0
- package/src/server.js +110 -0
- package/src/supabase.js +113 -0
- package/src/tools/admin.js +385 -0
- package/src/tools/documents.js +317 -0
- package/src/tools/index.js +261 -0
- package/src/tools/matrix.js +262 -0
- package/src/tools/rituals.js +236 -0
- package/src/tools/tasks.js +283 -0
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;">✓ 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
|
+
}
|