itwillsync 1.7.0 → 1.8.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 CHANGED
@@ -1,79 +1,215 @@
1
+ <div align="center">
2
+
1
3
  # itwillsync
2
4
 
3
- **[Website](https://shrijayan.github.io/itwillsync/)** | **[Docs](https://shrijayan.github.io/itwillsync/docs/)** | **[npm](https://www.npmjs.com/package/itwillsync)** | **[Demo Video](https://youtu.be/Zc0Tb98CXh0)**
5
+ **Sync any terminal-based AI coding agent to your phone over local network.**
4
6
 
5
- Sync any terminal-based coding agent to your phone. Local network or Tailscale. Open source, agent-agnostic, zero cloud.
7
+ [![npm version](https://img.shields.io/npm/v/itwillsync)](https://www.npmjs.com/package/itwillsync)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
+ [![npm downloads](https://img.shields.io/npm/dm/itwillsync)](https://www.npmjs.com/package/itwillsync)
10
+ [![CI](https://github.com/shrijayan/itwillsync/actions/workflows/ci.yml/badge.svg)](https://github.com/shrijayan/itwillsync/actions/workflows/ci.yml)
6
11
 
12
+ Sync any coding agent to your phone — one dashboard for all your sessions. Agent-agnostic, privacy-first, zero cloud.
13
+
14
+ [Website](https://shrijayan.github.io/itwillsync/) | [Docs](https://shrijayan.github.io/itwillsync/docs/) | [Demo Video](https://youtu.be/Zc0Tb98CXh0)
15
+
16
+ <!-- TODO: Replace with demo GIF once recorded -->
17
+ <!-- <img src="docs/assets/demo.gif" alt="itwillsync demo: scanning QR code to connect phone to Claude Code terminal session over local WiFi" width="700"> -->
18
+
19
+ </div>
20
+
21
+ ---
22
+
23
+ ## Quick Start
24
+
25
+ ```bash
26
+ npx itwillsync claude # Claude Code
27
+ npx itwillsync aider # Aider
28
+ npx itwillsync bash # or any terminal command
7
29
  ```
8
- npx itwillsync claude
9
- npx itwillsync aider
10
- npx itwillsync bash
11
- ```
12
30
 
13
- ## How it works
31
+ No install needed. Node.js 20+ required.
32
+
33
+ ## Why itwillsync
34
+
35
+ - **Control your agents from your phone**
36
+ - Scan a QR code and get full terminal access in your browser — no app install needed
37
+ - Type commands, approve prompts, and fix errors right from your phone
38
+ - Stay in the loop while you're away from your desk — kitchen, couch, coffee shop
39
+
40
+ - **One dashboard for all your sessions**
41
+ - See every running agent at a glance — status, working directory, uptime
42
+ - Tap any session to jump into the full terminal
43
+ - Get alerted when an agent needs your attention
44
+
45
+ - **Works with any agent**
46
+ - Claude Code, Aider, Codex, Goose, Cline — if it runs in a terminal, it works
47
+ - Switch between agents without changing your workflow
48
+ - Not locked to any single vendor or platform
49
+
50
+ - **Your data stays yours**
51
+ - Everything runs on your local network — nothing goes to the cloud
52
+ - No accounts, no signup, no telemetry
53
+ - End-to-end encrypted connections with per-session tokens
54
+
55
+ - **Zero friction to get started**
56
+ - One command: `npx itwillsync claude` — no install, no config
57
+ - Works on macOS, Windows, and Linux
58
+
59
+ ## How It Works
60
+
61
+ ```
62
+ ┌─────────────────┐ ┌─────────────────┐
63
+ │ Your Laptop │ Local WiFi / Tailscale │ Your Phone │
64
+ │ │ │ │
65
+ │ Agent (Claude, │ WebSocket + Auth │ Browser-based │
66
+ │ Aider, etc.) │ ◄═════════════════════► │ Terminal │
67
+ │ ↕ │ Token in QR code │ (xterm.js) │
68
+ │ PTY (node-pty) │ │ Touch keyboard │
69
+ │ ↕ │ │ Extra keys bar │
70
+ │ HTTP + WS │ │ │
71
+ │ Server │ │ │
72
+ └─────────────────┘ └─────────────────┘
73
+ ```
14
74
 
15
75
  1. Run `itwillsync` with your agent command
16
76
  2. A QR code appears in your terminal
17
- 3. Scan it on your phone — opens a terminal in your browser
18
- 4. Control your agent from your phone (or both phone and laptop simultaneously)
77
+ 3. Scan it on your phone — a terminal opens in your browser
78
+ 4. Control your agent from your phone, laptop, or both simultaneously
19
79
 
20
- ## Requirements
80
+ ## When To Use It
21
81
 
22
- - Node.js 20+
23
- - Any terminal-based coding agent (Claude Code, Aider, Goose, Codex, or just `bash`)
82
+ - **Walking to the kitchen** while Claude works on a long refactor — check progress from your phone
83
+ - **Monitoring multiple agents** from the couch via the multi-session dashboard
84
+ - **Getting a notification** when your agent needs attention (auto-detects BEL/OSC signals)
85
+ - **Working from a coffee shop** via Tailscale — no need to be on the same WiFi
86
+ - **Showing a colleague** what your AI agents are doing — just share the QR code
87
+ - **Quick approval** from your phone while you're in a meeting
24
88
 
25
- ## Install & Use
89
+ ## Multi-Session Dashboard
26
90
 
27
- ```bash
28
- # Run directly (no install needed)
29
- npx itwillsync claude
91
+ Running multiple agents? The hub daemon manages all your sessions from one place.
92
+
93
+ ```
94
+ ┌─────────────────────────────────────┐
95
+ │ itwillsync Dashboard │
96
+ │ │
97
+ │ ┌─────────────┐ ┌─────────────┐ │
98
+ │ │ Claude Code │ │ Aider │ │
99
+ │ │ ~/myproject │ │ ~/api │ │
100
+ │ │ ● Active │ │ ⚠ Attention │ │
101
+ │ │ 12m uptime │ │ 3m uptime │ │
102
+ │ └─────────────┘ └─────────────┘ │
103
+ │ │
104
+ │ ┌─────────────┐ │
105
+ │ │ Bash │ │
106
+ │ │ ~/scripts │ │
107
+ │ │ ○ Idle │ │
108
+ │ │ 45m uptime │ │
109
+ │ └─────────────┘ │
110
+ └─────────────────────────────────────┘
30
111
  ```
31
112
 
32
- On first run, a setup wizard asks how you want to connect — local WiFi or Tailscale. Your choice is saved for future sessions.
113
+ - Session cards show agent name, working directory, status, and uptime
114
+ - Real-time updates via WebSocket
115
+ - Tap a card to open the full terminal
116
+ - Attention detection alerts you when an agent needs input
117
+ - Sleep prevention keeps your machine awake during long tasks
33
118
 
34
- ## Connect from Anywhere with Tailscale
119
+ ## Works With
35
120
 
36
- By default, your phone needs to be on the same WiFi. With [Tailscale](https://tailscale.com), you can connect from anywhere coffee shop, cellular, different network.
121
+ Claude Code, Aider, Goose, Codex, Cline, Copilot CLIor any terminal-based tool.
37
122
 
38
123
  ```bash
39
- # First time: the setup wizard will detect Tailscale automatically
40
- itwillsync claude
124
+ npx itwillsync claude # Claude Code
125
+ npx itwillsync aider # Aider
126
+ npx itwillsync goose # Goose
127
+ npx itwillsync "codex --quiet" # Codex
128
+ npx itwillsync bash # Plain shell
129
+ ```
41
130
 
42
- # Or use Tailscale for a single session
43
- itwillsync --tailscale claude
131
+ If it runs in a terminal, itwillsync can sync it.
44
132
 
45
- # Switch back to local WiFi for a session
46
- itwillsync --local claude
133
+ ## Connection Modes
47
134
 
48
- # Re-run setup anytime
49
- itwillsync setup
50
- ```
135
+ | Mode | Command | When |
136
+ |------|---------|------|
137
+ | **Local WiFi** (default) | `npx itwillsync claude` | Phone on same network |
138
+ | **Tailscale** | `npx itwillsync --tailscale claude` | Any network, anywhere |
139
+ | **Cloudflare Tunnel** | `npx itwillsync --tunnel cloudflare claude` | Remote, no VPN needed |
140
+ | **Localhost** | `npx itwillsync --localhost claude` | Same machine only |
51
141
 
52
- **Setup:** Install Tailscale on both your computer and phone. That's it — itwillsync detects it automatically.
142
+ On first run, a setup wizard detects your network and saves your preference.
53
143
 
54
- ## Options
144
+ ```bash
145
+ # First run — wizard auto-detects Tailscale
146
+ npx itwillsync claude
55
147
 
148
+ # Override for a single session
149
+ npx itwillsync --tailscale claude
150
+ npx itwillsync --local claude
151
+
152
+ # Re-run the setup wizard
153
+ npx itwillsync setup
56
154
  ```
57
- Commands:
58
- setup Run the setup wizard (change networking mode)
59
-
60
- Options:
61
- --port <number> Port to listen on (default: 3456)
62
- --localhost Bind to 127.0.0.1 only (no LAN access)
63
- --tailscale Use Tailscale for this session
64
- --local Use local WiFi for this session
65
- --no-qr Don't display QR code
66
- -h, --help Show help
67
- -v, --version Show version
68
- ```
155
+
156
+ ## Commands
157
+
158
+ | Flag | Description |
159
+ |------|-------------|
160
+ | `--port <number>` | Port to listen on (default: 3456) |
161
+ | `--localhost` | Bind to 127.0.0.1 only |
162
+ | `--tailscale` | Use Tailscale for this session |
163
+ | `--local` | Use local WiFi for this session |
164
+ | `--tunnel <provider>` | Use a tunnel for remote access (cloudflare) |
165
+ | `--no-qr` | Don't display QR code |
166
+ | `setup` | Run the setup wizard |
167
+ | `-h, --help` | Show help |
168
+ | `-v, --version` | Show version |
69
169
 
70
170
  ## Security
71
171
 
72
- - Each session generates a random 64-character token
73
- - Token is embedded in the QR code URL
74
- - All WebSocket connections require the token
75
- - No data leaves your network (local mode) or your Tailscale tailnet
172
+ - **E2E encrypted** all WebSocket messages encrypted with NaCl secretbox (XSalsa20-Poly1305)
173
+ - **Random tokens** each session generates a cryptographically random 64-character token
174
+ - **QR code auth** token is embedded in the QR code URL, no manual entry needed
175
+ - **WebSocket auth** all connections require the token (constant-time comparison)
176
+ - **Rate limiting** — 5 failed auth attempts locks out the IP for 60 seconds
177
+ - **Zero cloud** — no data leaves your local network (or Tailscale tailnet)
178
+ - **No accounts** — no signup, no telemetry, no tracking
179
+
180
+ ## Mobile-Optimized
181
+
182
+ The phone terminal isn't just a mirror — it's built for mobile:
183
+
184
+ - **Touch-friendly extra keys bar** — Ctrl, Alt, Tab, Escape, arrows, and function keys
185
+ - **WebGL-accelerated rendering** on desktop, canvas fallback on mobile
186
+ - **Auto-reconnect** with scrollback buffer sync if connection drops
187
+ - **Audio notifications** when agents need attention
188
+
189
+ ## Development
190
+
191
+ ```bash
192
+ git clone https://github.com/shrijayan/itwillsync.git
193
+ cd itwillsync
194
+ nvm use # Node 22
195
+ pnpm install
196
+ pnpm build # Build all packages
197
+ pnpm test # Run tests
198
+ ```
199
+
200
+ Monorepo structure:
201
+
202
+ ```
203
+ packages/
204
+ ├── cli/ → Main npm package (itwillsync)
205
+ ├── web-client/ → Browser terminal (xterm.js)
206
+ ├── hub/ → Dashboard daemon
207
+ ├── landing/ → Landing page
208
+ └── docs/ → VitePress documentation
209
+ ```
210
+
211
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and PR guidelines.
76
212
 
77
- ## Works with
213
+ ## License
78
214
 
79
- Claude Code, Aider, Goose, Codex, Cline, Copilot CLI, or any terminal-based tool.
215
+ [MIT](LICENSE)
@@ -4147,7 +4147,7 @@ function createInternalApi(options) {
4147
4147
  }
4148
4148
  res.writeHead(404);
4149
4149
  res.end(JSON.stringify({ error: "Not found" }));
4150
- } catch (err) {
4150
+ } catch {
4151
4151
  res.writeHead(500);
4152
4152
  res.end(JSON.stringify({ error: "Internal server error" }));
4153
4153
  }
@@ -4189,6 +4189,7 @@ var import_websocket = __toESM(require_websocket(), 1);
4189
4189
  var import_websocket_server = __toESM(require_websocket_server(), 1);
4190
4190
 
4191
4191
  // src/server.ts
4192
+ import { deriveEncryptionKey, encrypt, decrypt } from "@itwillsync/shared/crypto";
4192
4193
  var MIME_TYPES = {
4193
4194
  ".html": "text/html; charset=utf-8",
4194
4195
  ".js": "application/javascript; charset=utf-8",
@@ -4202,6 +4203,10 @@ var COMPRESSIBLE = /* @__PURE__ */ new Set([".html", ".js", ".css", ".json", ".s
4202
4203
  var PING_INTERVAL_MS = 3e4;
4203
4204
  function createDashboardServer(options) {
4204
4205
  const { registry, masterToken, dashboardPath, host, port, previewCollector, toolHistory, sleepPrevention, onCreateSession } = options;
4206
+ const encryptionKey = deriveEncryptionKey(masterToken);
4207
+ function sendMsg(ws, msg) {
4208
+ ws.send(encrypt(JSON.stringify(msg), encryptionKey));
4209
+ }
4205
4210
  const homeDir = homedir3();
4206
4211
  const clients = /* @__PURE__ */ new Set();
4207
4212
  const aliveMap = /* @__PURE__ */ new WeakMap();
@@ -4357,26 +4362,33 @@ function createDashboardServer(options) {
4357
4362
  clients.add(ws);
4358
4363
  aliveMap.set(ws, true);
4359
4364
  const sessions = registry.getAll();
4360
- ws.send(JSON.stringify({ type: "sessions", sessions }));
4365
+ sendMsg(ws, { type: "sessions", sessions });
4361
4366
  if (sleepPrevention) {
4362
- ws.send(JSON.stringify({ type: "sleep-state", state: sleepPrevention.getState() }));
4367
+ sendMsg(ws, { type: "sleep-state", state: sleepPrevention.getState() });
4363
4368
  }
4364
4369
  if (previewCollector) {
4365
4370
  const previews = previewCollector.getAllPreviews();
4366
4371
  for (const [sessionId, lines] of previews) {
4367
4372
  if (lines.length > 0) {
4368
- ws.send(JSON.stringify({ type: "preview", sessionId, lines }));
4373
+ sendMsg(ws, { type: "preview", sessionId, lines });
4369
4374
  }
4370
4375
  }
4371
4376
  }
4372
4377
  ws.on("message", async (raw) => {
4378
+ let plaintext;
4379
+ try {
4380
+ const rawStr = typeof raw === "string" ? raw : raw.toString("utf-8");
4381
+ plaintext = decrypt(rawStr, encryptionKey);
4382
+ } catch {
4383
+ return;
4384
+ }
4373
4385
  try {
4374
- const msg = JSON.parse(typeof raw === "string" ? raw : raw.toString("utf-8"));
4386
+ const msg = JSON.parse(plaintext);
4375
4387
  switch (msg.type) {
4376
4388
  case "stop-session": {
4377
4389
  const session = registry.getById(msg.sessionId);
4378
4390
  if (!session) {
4379
- ws.send(JSON.stringify({ type: "operation-error", operation: "stop", sessionId: msg.sessionId, error: "Session not found" }));
4391
+ sendMsg(ws, { type: "operation-error", operation: "stop", sessionId: msg.sessionId, error: "Session not found" });
4380
4392
  return;
4381
4393
  }
4382
4394
  try {
@@ -4389,7 +4401,7 @@ function createDashboardServer(options) {
4389
4401
  case "rename-session": {
4390
4402
  const renamed = registry.rename(msg.sessionId, msg.name?.trim());
4391
4403
  if (!renamed) {
4392
- ws.send(JSON.stringify({ type: "operation-error", operation: "rename", sessionId: msg.sessionId, error: "Session not found" }));
4404
+ sendMsg(ws, { type: "operation-error", operation: "rename", sessionId: msg.sessionId, error: "Session not found" });
4393
4405
  }
4394
4406
  break;
4395
4407
  }
@@ -4402,13 +4414,13 @@ function createDashboardServer(options) {
4402
4414
  }
4403
4415
  case "create-session": {
4404
4416
  if (!onCreateSession) {
4405
- ws.send(JSON.stringify({ type: "session-create-error", error: "Session creation not available" }));
4417
+ sendMsg(ws, { type: "session-create-error", error: "Session creation not available" });
4406
4418
  break;
4407
4419
  }
4408
4420
  const tool = (msg.tool || "").trim();
4409
4421
  const rawCwd = (msg.cwd || "").trim();
4410
4422
  if (!tool) {
4411
- ws.send(JSON.stringify({ type: "session-create-error", error: "Tool name is required" }));
4423
+ sendMsg(ws, { type: "session-create-error", error: "Tool name is required" });
4412
4424
  break;
4413
4425
  }
4414
4426
  const cwd = rawCwd ? rawCwd.replace(/^~/, homeDir) : homeDir;
@@ -4416,31 +4428,31 @@ function createDashboardServer(options) {
4416
4428
  const resolved = await realpath(cwd);
4417
4429
  const dirStat = await stat(resolved);
4418
4430
  if (!dirStat.isDirectory()) {
4419
- ws.send(JSON.stringify({ type: "session-create-error", error: "Not a directory" }));
4431
+ sendMsg(ws, { type: "session-create-error", error: "Not a directory" });
4420
4432
  break;
4421
4433
  }
4422
- ws.send(JSON.stringify({ type: "session-creating", tool, cwd: rawCwd || "~" }));
4434
+ sendMsg(ws, { type: "session-creating", tool, cwd: rawCwd || "~" });
4423
4435
  onCreateSession(tool, resolved);
4424
4436
  } catch (err) {
4425
- ws.send(JSON.stringify({ type: "session-create-error", error: err.message }));
4437
+ sendMsg(ws, { type: "session-create-error", error: err.message });
4426
4438
  }
4427
4439
  break;
4428
4440
  }
4429
4441
  case "enable-sleep-prevention": {
4430
4442
  if (!sleepPrevention) {
4431
- ws.send(JSON.stringify({ type: "sleep-error", error: "Sleep prevention not available" }));
4443
+ sendMsg(ws, { type: "sleep-error", error: "Sleep prevention not available" });
4432
4444
  break;
4433
4445
  }
4434
4446
  const password = typeof msg.password === "string" ? msg.password : "";
4435
4447
  if (!password) {
4436
- ws.send(JSON.stringify({ type: "sleep-error", error: "Password is required" }));
4448
+ sendMsg(ws, { type: "sleep-error", error: "Password is required" });
4437
4449
  break;
4438
4450
  }
4439
4451
  const enableResult = await sleepPrevention.enable(password);
4440
4452
  if (enableResult.success) {
4441
4453
  broadcast({ type: "sleep-state", state: sleepPrevention.getState() });
4442
4454
  } else {
4443
- ws.send(JSON.stringify({ type: "sleep-error", error: enableResult.error }));
4455
+ sendMsg(ws, { type: "sleep-error", error: enableResult.error });
4444
4456
  }
4445
4457
  break;
4446
4458
  }
@@ -4453,7 +4465,7 @@ function createDashboardServer(options) {
4453
4465
  case "get-metadata": {
4454
4466
  const session = registry.getById(msg.sessionId);
4455
4467
  if (!session) {
4456
- ws.send(JSON.stringify({ type: "operation-error", operation: "metadata", sessionId: msg.sessionId, error: "Session not found" }));
4468
+ sendMsg(ws, { type: "operation-error", operation: "metadata", sessionId: msg.sessionId, error: "Session not found" });
4457
4469
  return;
4458
4470
  }
4459
4471
  let memoryKB = 0;
@@ -4466,7 +4478,7 @@ function createDashboardServer(options) {
4466
4478
  memoryKB = parseInt(output.trim(), 10) || 0;
4467
4479
  } catch {
4468
4480
  }
4469
- ws.send(JSON.stringify({
4481
+ sendMsg(ws, {
4470
4482
  type: "metadata",
4471
4483
  sessionId: msg.sessionId,
4472
4484
  metadata: {
@@ -4478,7 +4490,7 @@ function createDashboardServer(options) {
4478
4490
  uptimeMs: Date.now() - session.connectedAt,
4479
4491
  connectedAt: session.connectedAt
4480
4492
  }
4481
- }));
4493
+ });
4482
4494
  break;
4483
4495
  }
4484
4496
  }
@@ -4496,7 +4508,7 @@ function createDashboardServer(options) {
4496
4508
  });
4497
4509
  });
4498
4510
  function broadcast(message) {
4499
- const msg = JSON.stringify(message);
4511
+ const msg = encrypt(JSON.stringify(message), encryptionKey);
4500
4512
  for (const client of clients) {
4501
4513
  if (client.readyState === client.OPEN) {
4502
4514
  client.send(msg);
@@ -4540,6 +4552,7 @@ function createDashboardServer(options) {
4540
4552
 
4541
4553
  // src/preview-collector.ts
4542
4554
  import { EventEmitter as EventEmitter2 } from "events";
4555
+ import { deriveEncryptionKey as deriveEncryptionKey2, encrypt as encrypt2, decrypt as decrypt2 } from "@itwillsync/shared/crypto";
4543
4556
  var MAX_PREVIEW_LINES = 5;
4544
4557
  var THROTTLE_MS = 500;
4545
4558
  var MAX_LINE_LENGTH = 80;
@@ -4571,6 +4584,7 @@ var PreviewCollector = class extends EventEmitter2 {
4571
4584
  connectToSession(session) {
4572
4585
  const conn = {
4573
4586
  ws: null,
4587
+ encryptionKey: deriveEncryptionKey2(session.token),
4574
4588
  lines: [],
4575
4589
  rawBuffer: "",
4576
4590
  throttleTimer: null,
@@ -4591,10 +4605,18 @@ var PreviewCollector = class extends EventEmitter2 {
4591
4605
  conn.ws = ws;
4592
4606
  ws.on("open", () => {
4593
4607
  conn.reconnectAttempt = 0;
4608
+ ws.send(encrypt2(JSON.stringify({ type: "sync", lastSeq: -1 }), conn.encryptionKey));
4594
4609
  });
4595
4610
  ws.on("message", (raw) => {
4611
+ let plaintext;
4612
+ try {
4613
+ const rawStr = typeof raw === "string" ? raw : raw.toString("utf-8");
4614
+ plaintext = decrypt2(rawStr, conn.encryptionKey);
4615
+ } catch {
4616
+ return;
4617
+ }
4596
4618
  try {
4597
- const msg = JSON.parse(typeof raw === "string" ? raw : raw.toString("utf-8"));
4619
+ const msg = JSON.parse(plaintext);
4598
4620
  if (msg.type === "data" && typeof msg.data === "string") {
4599
4621
  this.handleData(sessionId, msg.data);
4600
4622
  }
@@ -4612,7 +4634,7 @@ var PreviewCollector = class extends EventEmitter2 {
4612
4634
  this.scheduleReconnect(sessionId, session);
4613
4635
  }
4614
4636
  }
4615
- scheduleReconnect(sessionId, session) {
4637
+ scheduleReconnect(sessionId, _session) {
4616
4638
  const conn = this.connections.get(sessionId);
4617
4639
  if (!conn || conn.closed) return;
4618
4640
  conn.ws = null;