thinkpool-pair 0.7.4 → 0.7.6

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 (2) hide show
  1. package/account.mjs +19 -11
  2. package/package.json +1 -1
package/account.mjs CHANGED
@@ -99,7 +99,8 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
99
99
  process.exit(1)
100
100
  }
101
101
 
102
- process.stderr.write(`\n ◆ thinkpool-pair account mode · ${email}\n Serving your code sessions. Add a session's directory with \`bind\`.\n`)
102
+ const DEFAULT_DIR = process.cwd() // unbound sessions auto-serve here; `bind` overrides per session
103
+ process.stderr.write(`\n ◆ thinkpool-pair — account mode · ${email}\n Auto-serving your sessions from ${DEFAULT_DIR}\n (point one at another repo: npx thinkpool-pair bind <code> <dir>)\n`)
103
104
 
104
105
  const children = new Map() // room -> child process
105
106
  const warned = new Set()
@@ -108,31 +109,29 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
108
109
  // account without per-room probing. We TRACK this machine (name + served room
109
110
  // codes + version) on a per-account channel; the dashboard subscribes read-only.
110
111
  const machine = os.hostname().replace(/\.local$/, '')
111
- const acct = sb.channel(`tpacct:${session.user.id}`, { config: { presence: { key: machine } } })
112
+ const acct = sb.channel(`tpacct:${session.user.id}`, { config: { presence: { key: machine }, broadcast: { self: false } } })
113
+ // Dashboard "Disconnect" → broadcast shutdown → clean exit (presence leaves, bar flips live).
114
+ acct.on('broadcast', { event: 'shutdown' }, () => { process.stderr.write('\n ◇ disconnect requested from the dashboard — stopping bridge.\n'); process.kill(process.pid, 'SIGTERM') })
112
115
  const pushPresence = () => { try { acct.track({ name: machine, version: VERSION, rooms: [...children.keys()], ts: Date.now() }) } catch { /* noop */ } }
113
116
  await new Promise((res) => acct.subscribe((st) => { if (st === 'SUBSCRIBED') { pushPresence(); res() } }))
114
117
 
115
118
  const tick = async () => {
116
119
  let rooms = []
117
120
  try {
118
- const { data } = await sb.from('code_sessions').select('code,name,last_active_at').order('last_active_at', { ascending: false })
121
+ const { data } = await sb.from('code_sessions').select('code,name,last_active_at').order('last_active_at', { ascending: false }).limit(20)
119
122
  rooms = data || []
120
123
  } catch { /* transient — keep existing children, retry next tick */ }
121
124
  const dirs = loadDirs()
122
125
  for (const r of rooms) {
123
126
  const room = r.code
124
127
  if (!room || children.has(room)) continue
125
- const dir = dirs[room]
126
- if (!dir) {
127
- if (!warned.has(room)) { warned.add(room); process.stderr.write(`\n ◇ session ${r.name ? `"${r.name}" ` : ''}(${room}) needs a directory:\n npx thinkpool-pair bind ${room} <path>\n`) }
128
- continue
129
- }
128
+ const dir = dirs[room] || DEFAULT_DIR // auto-serve unbound sessions in the launch dir
130
129
  if (!fs.existsSync(dir)) {
131
130
  if (!warned.has(room)) { warned.add(room); process.stderr.write(`\n ◇ ${room}: bound directory is gone (${dir}) — re-bind it.\n`) }
132
131
  continue
133
132
  }
134
133
  warned.delete(room)
135
- process.stderr.write(`\n ◆ serving ${room}${r.name ? ` "${r.name}"` : ''} → ${dir}\n`)
134
+ process.stderr.write(`\n ◆ serving ${room}${r.name ? ` "${r.name}"` : ''} → ${dir}${dirs[room] ? '' : ' (default)'}\n`)
136
135
  const child = spawn(process.execPath, [BRIDGE, room, '--headless'], { cwd: dir, stdio: 'inherit' })
137
136
  children.set(room, child)
138
137
  child.on('exit', () => { children.delete(room) }) // re-served on the next tick
@@ -142,7 +141,16 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
142
141
 
143
142
  await tick()
144
143
  const iv = setInterval(tick, 15000)
145
- const stop = (sig) => { clearInterval(iv); try { acct.untrack(); sb.removeChannel(acct) } catch { /* noop */ } for (const c of children.values()) { try { c.kill(sig) } catch { /* noop */ } } setTimeout(() => process.exit(0), 400) }
146
- for (const sig of ['SIGINT', 'SIGTERM']) process.on(sig, () => stop(sig))
144
+ let stopping = false
145
+ const stop = (sig) => {
146
+ if (stopping) return; stopping = true
147
+ clearInterval(iv)
148
+ for (const c of children.values()) { try { c.kill(sig || 'SIGTERM') } catch { /* noop */ } }
149
+ // Flush the presence LEAVE before exiting so the dashboard flips to
150
+ // "not connected" in realtime (don't fire-and-forget the untrack).
151
+ ;(async () => { try { await acct.untrack() } catch { /* noop */ } try { await sb.removeChannel(acct) } catch { /* noop */ } process.exit(0) })()
152
+ setTimeout(() => process.exit(0), 1500) // hard backstop if the flush hangs
153
+ }
154
+ for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.on(sig, () => stop(sig))
147
155
  await new Promise(() => {})
148
156
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.4",
3
+ "version": "0.7.6",
4
4
  "description": "Share a local coding-agent CLI (Claude Code, Codex, Gemini, Aider, …) into a ThinkPool Code room, live.",
5
5
  "type": "module",
6
6
  "bin": {