itwillsync 0.1.0 → 1.0.1
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,127 @@
|
|
|
1
|
+
# itwillsync
|
|
2
|
+
|
|
3
|
+
Sync any terminal-based coding agent to your phone over local network. Open source, agent-agnostic, zero cloud.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npx itwillsync -- claude
|
|
7
|
+
npx itwillsync -- aider
|
|
8
|
+
npx itwillsync -- bash
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## How it works
|
|
12
|
+
|
|
13
|
+
1. Run `itwillsync` with your agent command
|
|
14
|
+
2. A QR code appears in your terminal
|
|
15
|
+
3. Scan it on your phone — opens a terminal in your browser
|
|
16
|
+
4. Control your agent from your phone (or both phone and laptop simultaneously)
|
|
17
|
+
|
|
18
|
+
All data stays on your local network. No cloud, no relay, no account needed.
|
|
19
|
+
|
|
20
|
+
## Requirements
|
|
21
|
+
|
|
22
|
+
- Node.js 20+
|
|
23
|
+
- Any terminal-based coding agent (Claude Code, Aider, Goose, Codex, or just `bash`)
|
|
24
|
+
|
|
25
|
+
## Install & Use
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Run directly (no install needed)
|
|
29
|
+
npx itwillsync -- claude
|
|
30
|
+
|
|
31
|
+
# Or install globally
|
|
32
|
+
npm install -g itwillsync
|
|
33
|
+
itwillsync -- aider --model gpt-4
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Options
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
--port <number> Port to listen on (default: 3456)
|
|
40
|
+
--localhost Bind to 127.0.0.1 only (no LAN access)
|
|
41
|
+
--no-qr Don't display QR code
|
|
42
|
+
-h, --help Show help
|
|
43
|
+
-v, --version Show version
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Remote Access
|
|
47
|
+
|
|
48
|
+
By default, itwillsync is accessible on your local network (same WiFi). For remote access from anywhere:
|
|
49
|
+
|
|
50
|
+
- **Tailscale** (recommended): Install on both devices, access via Tailscale IP
|
|
51
|
+
- **WireGuard / VPN**: Any VPN that puts devices on the same network
|
|
52
|
+
- **SSH tunnel**: `ssh -L 3456:localhost:3456 your-machine`
|
|
53
|
+
|
|
54
|
+
## Security
|
|
55
|
+
|
|
56
|
+
- Each session generates a random 64-character token
|
|
57
|
+
- Token is embedded in the QR code URL
|
|
58
|
+
- All WebSocket connections require the token
|
|
59
|
+
- No data leaves your network
|
|
60
|
+
|
|
61
|
+
## Architecture
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
Your Machine Your Phone
|
|
65
|
+
┌─────────────────────┐ ┌──────────────┐
|
|
66
|
+
│ itwillsync │ WiFi/LAN │ Browser │
|
|
67
|
+
│ ├─ PTY (your agent) │◄────────────►│ xterm.js │
|
|
68
|
+
│ ├─ HTTP server │ WebSocket │ terminal │
|
|
69
|
+
│ └─ WS server │ └──────────────┘
|
|
70
|
+
└─────────────────────┘
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Session Behavior
|
|
74
|
+
|
|
75
|
+
- **No timeout**: Sessions live as long as the agent process runs. No TTL, no idle disconnect.
|
|
76
|
+
- **Multiple devices**: Connect from phone, tablet, and laptop simultaneously — all see the same terminal.
|
|
77
|
+
- **Reconnect**: If your phone disconnects (WiFi switch, screen lock), it auto-reconnects and catches up with recent output.
|
|
78
|
+
- **Keepalive**: WebSocket pings every 30s prevent routers from closing idle connections.
|
|
79
|
+
- **One session per instance**: Run multiple `itwillsync` instances on different ports for multiple agents.
|
|
80
|
+
|
|
81
|
+
## Development
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# 1. Clone and enter the project
|
|
85
|
+
git clone https://github.com/your-username/itwillsync
|
|
86
|
+
cd itwillsync
|
|
87
|
+
|
|
88
|
+
# 2. Use Node 22 (required for node-pty native bindings)
|
|
89
|
+
nvm use # reads .nvmrc
|
|
90
|
+
|
|
91
|
+
# 3. Install dependencies
|
|
92
|
+
pnpm install
|
|
93
|
+
|
|
94
|
+
# 4. Build everything (web client first, then CLI)
|
|
95
|
+
pnpm build
|
|
96
|
+
|
|
97
|
+
# 5. Test it
|
|
98
|
+
node packages/cli/dist/index.js -- bash
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Project Structure
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
packages/
|
|
105
|
+
├── cli/ # Main npm package — PTY, server, auth, CLI
|
|
106
|
+
└── web-client/ # Browser terminal — xterm.js, mobile-friendly CSS
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Contributing
|
|
110
|
+
|
|
111
|
+
1. Fork the repo
|
|
112
|
+
2. Create a feature branch
|
|
113
|
+
3. Make your changes
|
|
114
|
+
4. Run `pnpm build` to verify
|
|
115
|
+
5. Open a PR
|
|
116
|
+
|
|
117
|
+
## Roadmap
|
|
118
|
+
|
|
119
|
+
- [ ] Chat-style input bar + quick action buttons on mobile
|
|
120
|
+
- [ ] Agent detection + structured view for Claude Code
|
|
121
|
+
- [ ] React Native mobile app
|
|
122
|
+
- [ ] VS Code extension adapter
|
|
123
|
+
- [ ] Agent Sync Protocol specification
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
MIT
|
package/dist/index.js
CHANGED
|
@@ -3776,6 +3776,7 @@ function findAvailablePort(startPort) {
|
|
|
3776
3776
|
import { createServer as createServer2 } from "http";
|
|
3777
3777
|
import { readFile } from "fs/promises";
|
|
3778
3778
|
import { join, extname } from "path";
|
|
3779
|
+
import { gzipSync } from "zlib";
|
|
3779
3780
|
|
|
3780
3781
|
// ../../node_modules/.pnpm/ws@8.19.0/node_modules/ws/wrapper.mjs
|
|
3781
3782
|
var import_stream = __toESM(require_stream(), 1);
|
|
@@ -3794,14 +3795,34 @@ var MIME_TYPES = {
|
|
|
3794
3795
|
".png": "image/png",
|
|
3795
3796
|
".ico": "image/x-icon"
|
|
3796
3797
|
};
|
|
3797
|
-
|
|
3798
|
+
var COMPRESSIBLE = /* @__PURE__ */ new Set([".html", ".js", ".css", ".json", ".svg"]);
|
|
3799
|
+
var gzipCache = /* @__PURE__ */ new Map();
|
|
3800
|
+
async function serveStaticFile(webClientPath, filePath, req, res) {
|
|
3798
3801
|
const fullPath = join(webClientPath, filePath);
|
|
3799
3802
|
const ext = extname(fullPath);
|
|
3800
3803
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
3801
3804
|
try {
|
|
3802
|
-
const
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
+
const raw = await readFile(fullPath);
|
|
3806
|
+
const acceptsGzip = (req.headers["accept-encoding"] || "").includes("gzip");
|
|
3807
|
+
if (acceptsGzip && COMPRESSIBLE.has(ext)) {
|
|
3808
|
+
let compressed = gzipCache.get(fullPath);
|
|
3809
|
+
if (!compressed) {
|
|
3810
|
+
compressed = gzipSync(raw);
|
|
3811
|
+
gzipCache.set(fullPath, compressed);
|
|
3812
|
+
}
|
|
3813
|
+
res.writeHead(200, {
|
|
3814
|
+
"Content-Type": contentType,
|
|
3815
|
+
"Content-Encoding": "gzip",
|
|
3816
|
+
"Content-Length": compressed.length
|
|
3817
|
+
});
|
|
3818
|
+
res.end(compressed);
|
|
3819
|
+
} else {
|
|
3820
|
+
res.writeHead(200, {
|
|
3821
|
+
"Content-Type": contentType,
|
|
3822
|
+
"Content-Length": raw.length
|
|
3823
|
+
});
|
|
3824
|
+
res.end(raw);
|
|
3825
|
+
}
|
|
3805
3826
|
} catch {
|
|
3806
3827
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
3807
3828
|
res.end("Not Found");
|
|
@@ -3820,7 +3841,7 @@ function createSyncServer(options) {
|
|
|
3820
3841
|
if (pathname === "/") {
|
|
3821
3842
|
pathname = "/index.html";
|
|
3822
3843
|
}
|
|
3823
|
-
await serveStaticFile(webClientPath, pathname, res);
|
|
3844
|
+
await serveStaticFile(webClientPath, pathname, req, res);
|
|
3824
3845
|
});
|
|
3825
3846
|
const wssServer = new import_websocket_server.default({ noServer: true });
|
|
3826
3847
|
httpServer.on("upgrade", (req, socket, head) => {
|
|
@@ -3921,6 +3942,29 @@ function displayQR(url) {
|
|
|
3921
3942
|
// src/index.ts
|
|
3922
3943
|
import { fileURLToPath } from "url";
|
|
3923
3944
|
import { join as join2, dirname } from "path";
|
|
3945
|
+
import { spawn as spawn2 } from "child_process";
|
|
3946
|
+
function preventSleep() {
|
|
3947
|
+
try {
|
|
3948
|
+
if (process.platform === "darwin") {
|
|
3949
|
+
const child = spawn2("caffeinate", ["-i", "-w", String(process.pid)], {
|
|
3950
|
+
stdio: "ignore",
|
|
3951
|
+
detached: true
|
|
3952
|
+
});
|
|
3953
|
+
child.unref();
|
|
3954
|
+
return child;
|
|
3955
|
+
} else if (process.platform === "linux") {
|
|
3956
|
+
return spawn2("systemd-inhibit", [
|
|
3957
|
+
"--what=idle",
|
|
3958
|
+
"--who=itwillsync",
|
|
3959
|
+
"--why=Terminal sync session active",
|
|
3960
|
+
"sleep",
|
|
3961
|
+
"infinity"
|
|
3962
|
+
], { stdio: "ignore" });
|
|
3963
|
+
}
|
|
3964
|
+
} catch {
|
|
3965
|
+
}
|
|
3966
|
+
return null;
|
|
3967
|
+
}
|
|
3924
3968
|
var DEFAULT_PORT = 3456;
|
|
3925
3969
|
function parseArgs(argv) {
|
|
3926
3970
|
const options = {
|
|
@@ -4010,9 +4054,11 @@ async function main() {
|
|
|
4010
4054
|
Connect at: ${url}
|
|
4011
4055
|
`);
|
|
4012
4056
|
}
|
|
4057
|
+
const sleepGuard = preventSleep();
|
|
4013
4058
|
console.log(` Server listening on ${host}:${port}`);
|
|
4014
4059
|
console.log(` Running: ${options.command.join(" ")}`);
|
|
4015
4060
|
console.log(` PID: ${ptyManager.pid}`);
|
|
4061
|
+
console.log(` Sleep prevention: ${sleepGuard ? "active" : "unavailable"}`);
|
|
4016
4062
|
console.log("");
|
|
4017
4063
|
if (process.stdin.isTTY) {
|
|
4018
4064
|
process.stdin.setRawMode(true);
|
|
@@ -4036,6 +4082,7 @@ async function main() {
|
|
|
4036
4082
|
if (process.stdin.isTTY) {
|
|
4037
4083
|
process.stdin.setRawMode(false);
|
|
4038
4084
|
}
|
|
4085
|
+
sleepGuard?.kill();
|
|
4039
4086
|
server.close();
|
|
4040
4087
|
ptyManager.kill();
|
|
4041
4088
|
}
|