|
2 | 2 |
|
3 | 3 | Sync tasks between **Google Tasks** and **Microsoft To Do**. |
4 | 4 |
|
5 | | -Providers: |
| 5 | +Works as a CLI for power users, or as a self-hosted web UI for everyone else. |
6 | 6 |
|
7 | | -- Google Tasks (OAuth refresh-token) |
8 | | -- Microsoft To Do via Microsoft Graph (OAuth refresh-token) |
| 7 | +## Features |
9 | 8 |
|
10 | | -## Quickstart |
| 9 | +- **Bidirectional sync** between Google Tasks and Microsoft To Do |
| 10 | +- **Field-level conflict resolution** (last-write-wins per field) |
| 11 | +- **Cold-start matching** — deduplicates tasks on first sync by title + notes |
| 12 | +- **Delete propagation** — tombstones prevent resurrecting deleted tasks |
| 13 | +- **Dry-run mode** — preview changes before applying |
| 14 | +- **Polling mode** — auto-sync on an interval |
| 15 | +- **Web UI** — connect accounts with OAuth, sync with one click |
| 16 | +- **Self-hosted** — your data stays on your machine |
11 | 17 |
|
12 | | -### Requirements |
| 18 | +## Requirements |
13 | 19 |
|
14 | 20 | - Node.js **>= 22** |
15 | 21 |
|
16 | | -### Install |
| 22 | +## Quick Start — Web UI |
17 | 23 |
|
18 | | -```bash |
19 | | -npm install |
20 | | -``` |
| 24 | +The web UI lets you connect your Google and Microsoft accounts via OAuth |
| 25 | +and sync tasks with one click. No manual token management needed. |
21 | 26 |
|
22 | | -### Build + run doctor |
| 27 | +### 1. Set up OAuth apps |
23 | 28 |
|
24 | | -```bash |
25 | | -npm run build |
26 | | -node dist/cli.js doctor |
27 | | -``` |
| 29 | +**Google Tasks:** |
| 30 | + |
| 31 | +1. Go to [Google Cloud Console](https://console.cloud.google.com/) |
| 32 | +2. Create a project (or select existing) |
| 33 | +3. Enable the **Google Tasks API** |
| 34 | +4. Go to **APIs & Services → Credentials → Create Credentials → OAuth client ID** |
| 35 | +5. Application type: **Web application** |
| 36 | +6. Add authorized redirect URI: `http://localhost:3000/api/auth/google/callback` |
| 37 | +7. Copy the **Client ID** and **Client Secret** |
28 | 38 |
|
29 | | -### Run sync once |
| 39 | +**Microsoft To Do:** |
| 40 | + |
| 41 | +1. Go to [Azure Portal → App registrations](https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade) |
| 42 | +2. **New registration** |
| 43 | +3. Supported account types: **Personal Microsoft accounts only** (or multi-tenant) |
| 44 | +4. Redirect URI (Web): `http://localhost:3000/api/auth/microsoft/callback` |
| 45 | +5. Go to **API permissions** → Add: `Tasks.ReadWrite`, `User.Read`, `offline_access` |
| 46 | +6. Copy the **Application (client) ID** |
| 47 | + |
| 48 | +### 2. Configure |
| 49 | + |
| 50 | +Create `.env.local` in the project root: |
30 | 51 |
|
31 | 52 | ```bash |
32 | | -node dist/cli.js sync |
| 53 | +TASK_SYNC_PROVIDER_A=google |
| 54 | +TASK_SYNC_PROVIDER_B=microsoft |
| 55 | + |
| 56 | +TASK_SYNC_GOOGLE_CLIENT_ID=your-google-client-id |
| 57 | +TASK_SYNC_GOOGLE_CLIENT_SECRET=your-google-client-secret |
| 58 | + |
| 59 | +TASK_SYNC_MS_CLIENT_ID=your-microsoft-client-id |
| 60 | +TASK_SYNC_MS_TENANT_ID=consumers |
33 | 61 | ``` |
34 | 62 |
|
35 | | -### Polling mode |
| 63 | +### 3. Install and run |
36 | 64 |
|
37 | 65 | ```bash |
38 | | -# every 5 minutes |
39 | | -node dist/cli.js sync --poll 5 |
40 | | - |
41 | | -# or env |
42 | | -export TASK_SYNC_POLL_INTERVAL_MINUTES=5 |
43 | | -node dist/cli.js sync |
| 66 | +npm install |
| 67 | +npm run web:install |
| 68 | +npm run web:dev |
44 | 69 | ``` |
45 | 70 |
|
46 | | -### Dry-run |
| 71 | +Open [http://localhost:3000](http://localhost:3000). Click **Connect** for each |
| 72 | +provider, approve the OAuth consent, then hit **Sync Now**. |
47 | 73 |
|
48 | | -Dry-run still uses your configured providers, but **does not write** any changes. |
| 74 | +### Production |
49 | 75 |
|
50 | 76 | ```bash |
51 | | -node dist/cli.js sync --dry-run |
| 77 | +npm run web:build |
| 78 | +npm run web:start |
52 | 79 | ``` |
53 | 80 |
|
54 | | -## Configuration (.env) |
| 81 | +## Quick Start — CLI |
55 | 82 |
|
56 | | -Create a `.env.local` (recommended) or `.env`: |
| 83 | +For headless environments, scripts, or cron jobs. |
57 | 84 |
|
58 | | -### Provider selection |
| 85 | +### 1. Install |
59 | 86 |
|
60 | 87 | ```bash |
61 | | -TASK_SYNC_PROVIDER_A=google |
62 | | -TASK_SYNC_PROVIDER_B=microsoft |
| 88 | +npm install |
| 89 | +npm run build |
63 | 90 | ``` |
64 | 91 |
|
65 | | -### State |
| 92 | +### 2. Get refresh tokens |
| 93 | + |
| 94 | +You need refresh tokens for each provider. Helper scripts are included: |
66 | 95 |
|
67 | 96 | ```bash |
68 | | -TASK_SYNC_STATE_DIR=.task-sync |
69 | | -TASK_SYNC_LOG_LEVEL=info |
| 97 | +# Google |
| 98 | +export TASK_SYNC_GOOGLE_CLIENT_ID=... |
| 99 | +export TASK_SYNC_GOOGLE_CLIENT_SECRET=... |
| 100 | +npm run oauth:google |
| 101 | + |
| 102 | +# Microsoft |
| 103 | +export TASK_SYNC_MS_CLIENT_ID=... |
| 104 | +export TASK_SYNC_MS_TENANT_ID=consumers |
| 105 | +npm run oauth:microsoft |
70 | 106 | ``` |
71 | 107 |
|
72 | | -### Google Tasks |
| 108 | +Each script opens a browser for consent, then prints the refresh token. |
| 109 | + |
| 110 | +### 3. Configure |
| 111 | + |
| 112 | +Add all tokens to `.env.local`: |
73 | 113 |
|
74 | 114 | ```bash |
| 115 | +TASK_SYNC_PROVIDER_A=google |
| 116 | +TASK_SYNC_PROVIDER_B=microsoft |
| 117 | + |
75 | 118 | TASK_SYNC_GOOGLE_CLIENT_ID=... |
76 | 119 | TASK_SYNC_GOOGLE_CLIENT_SECRET=... |
77 | 120 | TASK_SYNC_GOOGLE_REFRESH_TOKEN=... |
78 | | -TASK_SYNC_GOOGLE_TASKLIST_ID=@default # optional |
79 | | -``` |
80 | 121 |
|
81 | | -### Microsoft To Do (Graph) |
82 | | - |
83 | | -```bash |
84 | 122 | TASK_SYNC_MS_CLIENT_ID=... |
85 | | -TASK_SYNC_MS_TENANT_ID=common # or your tenant id |
| 123 | +TASK_SYNC_MS_TENANT_ID=consumers |
86 | 124 | TASK_SYNC_MS_REFRESH_TOKEN=... |
87 | | -TASK_SYNC_MS_LIST_ID=... # optional (defaults to first list) |
88 | 125 | ``` |
89 | 126 |
|
90 | | -Run: |
| 127 | +### 4. Run |
91 | 128 |
|
92 | 129 | ```bash |
93 | | -task-sync doctor |
| 130 | +# Check config |
| 131 | +node dist/cli.js doctor |
| 132 | + |
| 133 | +# Sync once |
| 134 | +node dist/cli.js sync |
| 135 | + |
| 136 | +# Dry-run (preview changes) |
| 137 | +node dist/cli.js sync --dry-run |
| 138 | + |
| 139 | +# Auto-sync every 5 minutes |
| 140 | +node dist/cli.js sync --poll 5 |
| 141 | + |
| 142 | +# JSON output (for scripts) |
| 143 | +node dist/cli.js sync --format json |
94 | 144 | ``` |
95 | 145 |
|
96 | | -to see what's missing. |
| 146 | +## Configuration |
97 | 147 |
|
98 | | -## OAuth helper scripts (refresh tokens) |
| 148 | +All configuration is via environment variables. Create a `.env.local` file in the |
| 149 | +project root. |
99 | 150 |
|
100 | | -These scripts spin up a local HTTP callback server, print an auth URL, and on success print the refresh token. |
| 151 | +### Required |
101 | 152 |
|
102 | | -### Google refresh token |
| 153 | +| Variable | Description | |
| 154 | +|---|---| |
| 155 | +| `TASK_SYNC_PROVIDER_A` | First provider (`google` or `microsoft`) | |
| 156 | +| `TASK_SYNC_PROVIDER_B` | Second provider (`google` or `microsoft`) | |
| 157 | +| `TASK_SYNC_GOOGLE_CLIENT_ID` | Google OAuth client ID | |
| 158 | +| `TASK_SYNC_GOOGLE_CLIENT_SECRET` | Google OAuth client secret | |
| 159 | +| `TASK_SYNC_MS_CLIENT_ID` | Microsoft app (client) ID | |
103 | 160 |
|
104 | | -1) Create OAuth credentials in Google Cloud Console: |
105 | | -- APIs & Services → Credentials |
106 | | -- Create Credentials → OAuth client ID |
107 | | -- Application type: **Desktop app** (recommended) |
108 | | -- Enable the **Google Tasks API** on the project |
| 161 | +### Optional |
109 | 162 |
|
110 | | -2) Set env vars and run: |
| 163 | +| Variable | Default | Description | |
| 164 | +|---|---|---| |
| 165 | +| `TASK_SYNC_MS_TENANT_ID` | `consumers` | Azure tenant ID | |
| 166 | +| `TASK_SYNC_GOOGLE_REFRESH_TOKEN` | — | CLI only: Google refresh token | |
| 167 | +| `TASK_SYNC_MS_REFRESH_TOKEN` | — | CLI only: Microsoft refresh token | |
| 168 | +| `TASK_SYNC_GOOGLE_TASKLIST_ID` | `@default` | Google task list to sync | |
| 169 | +| `TASK_SYNC_MS_LIST_ID` | First list | Microsoft To Do list to sync | |
| 170 | +| `TASK_SYNC_STATE_DIR` | `.task-sync` | Directory for sync state | |
| 171 | +| `TASK_SYNC_LOG_LEVEL` | `info` | Log level: `silent\|error\|warn\|info\|debug` | |
| 172 | +| `TASK_SYNC_POLL_INTERVAL_MINUTES` | — | Auto-sync interval (CLI only) | |
| 173 | +| `TASK_SYNC_MODE` | `bidirectional` | Sync mode: `bidirectional\|a-to-b-only\|mirror` | |
| 174 | +| `TASK_SYNC_TOMBSTONE_TTL_DAYS` | `30` | How long to keep delete tombstones | |
111 | 175 |
|
112 | | -```bash |
113 | | -export TASK_SYNC_GOOGLE_CLIENT_ID=... |
114 | | -export TASK_SYNC_GOOGLE_CLIENT_SECRET=... |
115 | | -npm run oauth:google |
116 | | -``` |
| 176 | +## How It Works |
117 | 177 |
|
118 | | -### Microsoft refresh token |
| 178 | +### Sync State |
119 | 179 |
|
120 | | -1) Create an app registration in Azure: |
121 | | -- Azure Portal → App registrations → New registration |
122 | | -- Add a **redirect URI** (platform: *Mobile and desktop applications*): |
123 | | - - `http://localhost:53683/callback` |
124 | | -- API permissions (Delegated): |
125 | | - - `offline_access` |
126 | | - - `User.Read` |
127 | | - - `Tasks.ReadWrite` |
| 180 | +`task-sync` stores local state in `.task-sync/state.json`: |
128 | 181 |
|
129 | | -2) Run: |
| 182 | +- **`lastSyncAt`** — watermark timestamp for incremental sync |
| 183 | +- **`mappings`** — links canonical task IDs to provider-specific IDs |
| 184 | +- **`tombstones`** — prevents resurrecting deleted tasks |
130 | 185 |
|
131 | | -```bash |
132 | | -export TASK_SYNC_MS_CLIENT_ID=... |
133 | | -export TASK_SYNC_MS_TENANT_ID=common |
134 | | -npm run oauth:microsoft |
135 | | -``` |
| 186 | +Delete `.task-sync/` to reset all sync state. |
136 | 187 |
|
137 | | -## How state works |
| 188 | +### Web UI Token Storage |
138 | 189 |
|
139 | | -`task-sync` writes local state under: |
| 190 | +The web UI stores OAuth refresh tokens in `.task-sync/tokens.json`. These |
| 191 | +tokens never leave your machine. The web server uses them to authenticate |
| 192 | +with Google and Microsoft APIs on your behalf during sync. |
140 | 193 |
|
141 | | -- `.task-sync/state.json` |
| 194 | +### Sync Algorithm |
142 | 195 |
|
143 | | -This includes: |
| 196 | +1. **Fetch** tasks from all providers (incremental via `lastSyncAt`) |
| 197 | +2. **Cold-start match** — on first run, match tasks by title+notes to avoid duplicates |
| 198 | +3. **Tombstone check** — skip tasks that were intentionally deleted |
| 199 | +4. **Field-level diff** — compare each field (title, notes, status, due date) against the last canonical snapshot |
| 200 | +5. **Conflict resolution** — if multiple providers changed the same field, last-write-wins |
| 201 | +6. **Fan out** — apply the resolved canonical state to all providers |
144 | 202 |
|
145 | | -- `lastSyncAt` watermark (ISO timestamp) |
146 | | -- `mappings`: links a canonical ID to provider IDs |
147 | | -- `tombstones`: prevents resurrecting deleted tasks |
| 203 | +## Project Structure |
148 | 204 |
|
149 | | -Delete `.task-sync/` to reset sync state. |
| 205 | +``` |
| 206 | +task-sync/ |
| 207 | +├── src/ # Core engine + CLI |
| 208 | +│ ├── cli.ts # CLI entry point |
| 209 | +│ ├── sync/engine.ts # Sync algorithm |
| 210 | +│ ├── providers/ # Google, Microsoft, Mock providers |
| 211 | +│ ├── store/ # State persistence (JSON) |
| 212 | +│ ├── model.ts # Task data model |
| 213 | +│ └── ... |
| 214 | +├── web/ # Next.js web UI |
| 215 | +│ ├── app/ # Pages + API routes |
| 216 | +│ │ ├── page.tsx # Dashboard |
| 217 | +│ │ └── api/ # OAuth + sync endpoints |
| 218 | +│ ├── components/ # React components (shadcn/ui) |
| 219 | +│ └── lib/ # Env loading, token storage |
| 220 | +├── test/ # Vitest tests |
| 221 | +├── scripts/ # OAuth helper scripts |
| 222 | +└── .env.local # Your credentials (git-ignored) |
| 223 | +``` |
150 | 224 |
|
151 | 225 | ## Development |
152 | 226 |
|
153 | 227 | ```bash |
| 228 | +# Run CLI in dev mode |
154 | 229 | npm run dev -- doctor |
155 | 230 | npm run dev -- sync --dry-run |
| 231 | + |
| 232 | +# Run web in dev mode (builds core first) |
| 233 | +npm run web:dev |
| 234 | + |
| 235 | +# Tests |
156 | 236 | npm test |
| 237 | + |
| 238 | +# Lint & typecheck |
157 | 239 | npm run lint |
158 | 240 | npm run typecheck |
159 | 241 | ``` |
160 | 242 |
|
| 243 | +## Security Notes |
| 244 | + |
| 245 | +- **Self-hosted only** — this project is designed to run on your own machine or server. |
| 246 | +- **No telemetry** — no data is sent anywhere except Google and Microsoft APIs. |
| 247 | +- **Tokens on disk** — refresh tokens are stored in `.task-sync/tokens.json`. Treat this file like a password. |
| 248 | +- **No auth on the web UI** — if you expose the web UI to the internet, put it behind a reverse proxy with authentication (e.g., Caddy, nginx + basic auth, Cloudflare Tunnel). |
| 249 | + |
161 | 250 | ## License |
162 | 251 |
|
163 | | -MIT (see LICENSE) |
| 252 | +MIT (see [LICENSE](LICENSE)) |
0 commit comments