thinkpool-pair 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.
Files changed (3) hide show
  1. package/README.md +31 -0
  2. package/bridge.mjs +95 -0
  3. package/package.json +14 -0
package/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # thinkpool-pair — the ThinkPool Code bridge
2
+
3
+ Share your local **Claude Code** (or any terminal CLI) into a ThinkPool Code
4
+ room, live. Both people see the real terminal stream and can both drive it —
5
+ no copy/paste.
6
+
7
+ > A web page can't read your terminal directly (browser security). This tiny
8
+ > helper is the bridge: it runs the CLI in a PTY and streams it to the room.
9
+
10
+ ## Run it (one command)
11
+
12
+ ```bash
13
+ cd bridge && npm i # one-time (builds node-pty — needs Xcode CLT / build-essential)
14
+ node bridge.mjs <ROOM> # launches `claude`, shared into room <ROOM>
15
+ ```
16
+
17
+ - `<ROOM>` is the 5-char room code from the ThinkPool Code web UI (`/code`).
18
+ - Share any other CLI: `node bridge.mjs <ROOM> -- aider` / `… -- bash`.
19
+ - Use the terminal exactly as normal — it's seamless locally, and every byte
20
+ mirrors to the web for your partner. Prompts/keys typed in the web come back
21
+ into your session.
22
+
23
+ ## How it works
24
+
25
+ `bridge.mjs` ⇄ **Supabase realtime** (`tpcode:<ROOM>`) ⇄ web `xterm`:
26
+ - `pty-out` — terminal bytes (base64) → both web clients render them.
27
+ - `pty-in` — keystrokes/prompts from the web → written to your PTY.
28
+ - `resize` — web viewport size → `term.resize`.
29
+
30
+ Public anon creds are embedded (the same ones the web app ships). Override with
31
+ `TP_SUPABASE_URL` / `TP_SUPABASE_ANON` if needed. Set `TP_NAME` to label yourself.
package/bridge.mjs ADDED
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ /* ─────────────────────────────────────────────────────────────
3
+ thinkpool-pair — share your local Claude (or any CLI) into a
4
+ ThinkPool Code room, live. ONE command, seamless:
5
+
6
+ npx thinkpool-pair <ROOM> # launches `claude`, shared
7
+ npx thinkpool-pair <ROOM> -- <cmd…> # share any other CLI
8
+
9
+ It spawns the CLI in a PTY so you use it exactly like your normal
10
+ terminal — and every byte streams to the room over Supabase realtime
11
+ (the relay), while prompts/keys from the web come back into the PTY.
12
+ No copy/paste. A web page can't read your terminal directly — this
13
+ tiny helper is the bridge.
14
+ ───────────────────────────────────────────────────────────── */
15
+
16
+ import os from 'node:os'
17
+ import { createClient } from '@supabase/supabase-js'
18
+
19
+ // Public client creds (the same anon values the web app ships — safe to embed).
20
+ // Override with TP_SUPABASE_URL / TP_SUPABASE_ANON if you ever need to.
21
+ const SUPABASE_URL = process.env.TP_SUPABASE_URL || 'https://daytvtakmlixpfbbqzjd.supabase.co'
22
+ const SUPABASE_ANON = process.env.TP_SUPABASE_ANON || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRheXR2dGFrbWxpeHBmYmJxempkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzgwNTMxNzEsImV4cCI6MjA5MzYyOTE3MX0.zYmz8bHUNEY4STUr2JuXChAsKwxNwkwFHe1Hd0Betqo'
23
+
24
+ // node-pty is a native module — loaded lazily so a friendly message shows
25
+ // if it isn't built yet.
26
+ let pty
27
+ try { pty = (await import('node-pty')).default ?? (await import('node-pty')) }
28
+ catch {
29
+ console.error('\n node-pty isn\'t installed. From the bridge folder run:\n npm i\n then re-run. (it needs a C toolchain — Xcode CLT on mac, build-essential on linux)\n')
30
+ process.exit(1)
31
+ }
32
+
33
+ const argv = process.argv.slice(2)
34
+ const room = (argv[0] || '').toUpperCase().trim()
35
+ if (!room) { console.error('usage: npx thinkpool-pair <ROOM> [-- <command…>]'); process.exit(1) }
36
+ const dashIdx = argv.indexOf('--')
37
+ const [cmd, ...cmdArgs] = dashIdx >= 0 ? argv.slice(dashIdx + 1) : ['claude']
38
+ const name = process.env.TP_NAME || os.userInfo().username || 'host'
39
+
40
+ const supabase = createClient(SUPABASE_URL, SUPABASE_ANON, {
41
+ realtime: { params: { eventsPerSecond: 60 } },
42
+ })
43
+ const channel = supabase.channel(`tpcode:${room}`, {
44
+ config: { broadcast: { self: false }, presence: { key: `bridge:${name}` } },
45
+ })
46
+
47
+ // ── spawn the CLI in a PTY; the host uses it like a normal terminal ──
48
+ const cols = process.stdout.columns || 100
49
+ const rows = process.stdout.rows || 30
50
+ const term = pty.spawn(cmd || 'claude', cmdArgs, {
51
+ name: 'xterm-256color', cols, rows, cwd: process.cwd(), env: process.env,
52
+ })
53
+
54
+ // host's own keystrokes → the CLI (so it's seamless locally too)
55
+ if (process.stdin.isTTY) process.stdin.setRawMode(true)
56
+ process.stdin.resume()
57
+ process.stdin.on('data', d => term.write(d.toString('utf8')))
58
+
59
+ // CLI output → host's screen + batched out to the room
60
+ let buf = ''
61
+ term.onData(d => { process.stdout.write(d); buf += d })
62
+ const flush = () => {
63
+ if (!buf) return
64
+ const b64 = Buffer.from(buf, 'utf8').toString('base64'); buf = ''
65
+ channel.send({ type: 'broadcast', event: 'pty-out', payload: { b64 } })
66
+ }
67
+ const flushTimer = setInterval(flush, 35)
68
+
69
+ const announce = () =>
70
+ channel.send({ type: 'broadcast', event: 'bridge', payload: { name, cmd: cmd || 'claude', cols, rows } })
71
+
72
+ channel
73
+ .on('broadcast', { event: 'pty-in' }, ({ payload }) => { if (payload?.data) term.write(payload.data) })
74
+ .on('broadcast', { event: 'resize' }, ({ payload }) => {
75
+ if (payload?.cols && payload?.rows) { try { term.resize(payload.cols, payload.rows) } catch { /* noop */ } }
76
+ })
77
+ .on('broadcast', { event: 'who' }, announce)
78
+ .subscribe(status => {
79
+ if (status === 'SUBSCRIBED') {
80
+ channel.track({ name, role: 'bridge' })
81
+ announce()
82
+ process.stderr.write(`\n ◆ thinkpool — sharing "${cmd || 'claude'}" into room ${room}. Open the web UI and you're both in.\n\n`)
83
+ }
84
+ })
85
+
86
+ const shutdown = () => {
87
+ clearInterval(flushTimer)
88
+ try { channel.send({ type: 'broadcast', event: 'pty-out', payload: { b64: Buffer.from('\r\n[ shared session ended ]\r\n', 'utf8').toString('base64') } }) } catch { /* noop */ }
89
+ try { supabase.removeChannel(channel) } catch { /* noop */ }
90
+ try { term.kill() } catch { /* noop */ }
91
+ process.exit(0)
92
+ }
93
+ term.onExit(shutdown)
94
+ process.on('SIGINT', shutdown)
95
+ process.on('SIGTERM', shutdown)
package/package.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "thinkpool-pair",
3
+ "version": "0.1.0",
4
+ "description": "Share your local Claude (or any CLI) into a ThinkPool Code room, live.",
5
+ "type": "module",
6
+ "bin": "bridge.mjs",
7
+ "engines": {
8
+ "node": ">=18"
9
+ },
10
+ "dependencies": {
11
+ "@supabase/supabase-js": "^2.45.0",
12
+ "node-pty": "^1.0.0"
13
+ }
14
+ }