thinkpool-pair 0.7.94 → 0.7.95
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/account.mjs +22 -2
- package/auth-store.mjs +17 -0
- package/package.json +2 -1
- package/serve-dir.mjs +40 -0
package/account.mjs
CHANGED
|
@@ -13,9 +13,10 @@ import path from 'node:path'
|
|
|
13
13
|
import fs from 'node:fs'
|
|
14
14
|
import { createClient } from '@supabase/supabase-js'
|
|
15
15
|
import os from 'node:os'
|
|
16
|
-
import { saveAuth, loadAuth, loadDirs, bindDir } from './auth-store.mjs'
|
|
16
|
+
import { saveAuth, loadAuth, loadDirs, bindDir, loadServed, rememberServed } from './auth-store.mjs'
|
|
17
17
|
import { isSafeToRestart } from './update-gate.mjs'
|
|
18
18
|
import { makeThrottledTrack } from './presence.mjs'
|
|
19
|
+
import { resolveServeDir } from './serve-dir.mjs'
|
|
19
20
|
|
|
20
21
|
const VERSION = (() => { try { return JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version } catch { return null } })()
|
|
21
22
|
|
|
@@ -380,16 +381,35 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
380
381
|
rooms = data || []
|
|
381
382
|
} catch { /* transient — keep existing children, retry next tick */ }
|
|
382
383
|
const dirs = loadDirs()
|
|
384
|
+
const served = loadServed()
|
|
383
385
|
for (const r of rooms) {
|
|
384
386
|
const room = r.code
|
|
385
387
|
if (!room || children.has(room)) continue
|
|
386
388
|
if (r.owner_id && r.owner_id !== session.user.id) continue // never serve a room we don't own (defense-in-depth behind the .eq above)
|
|
387
|
-
|
|
389
|
+
// Resolve the serve dir: explicit bind > last-served memory > launch dir. REFUSE
|
|
390
|
+
// to auto-serve an unbound room from $HOME — that strips the agent's resume context
|
|
391
|
+
// (cwd-keyed SDK sessions) and shows your home folder as the repo (FVIHV1DE).
|
|
392
|
+
const choice = resolveServeDir({ bound: dirs[room], remembered: served[room], defaultDir: DEFAULT_DIR, home: os.homedir() })
|
|
393
|
+
if (choice.refuse) {
|
|
394
|
+
if (!warned.has(room)) {
|
|
395
|
+
warned.add(room)
|
|
396
|
+
const why = choice.reason === 'home'
|
|
397
|
+
? `no project directory set — refusing to serve it from your home folder (${choice.dir}); the agent would lose its session context and the room would show "${path.basename(choice.dir)}" as the repo.`
|
|
398
|
+
: 'no directory to serve it from.'
|
|
399
|
+
process.stderr.write(`\n ◇ ${room}: ${why}\n Bind it to its repo: npx thinkpool-pair bind ${room} <repo-dir>\n`)
|
|
400
|
+
}
|
|
401
|
+
continue
|
|
402
|
+
}
|
|
403
|
+
const dir = choice.dir
|
|
388
404
|
if (!fs.existsSync(dir)) {
|
|
389
405
|
if (!warned.has(room)) { warned.add(room); process.stderr.write(`\n ◇ ${room}: bound directory is gone (${dir}) — re-bind it.\n`) }
|
|
390
406
|
continue
|
|
391
407
|
}
|
|
392
408
|
warned.delete(room)
|
|
409
|
+
// Learn this room → dir so a later relaunch from $HOME resumes it HERE (the real
|
|
410
|
+
// repo) instead of the home-dir fallback. Only the auto path is remembered; an
|
|
411
|
+
// explicit bind already persists in dirs.json.
|
|
412
|
+
if (choice.fromDefault) rememberServed(room, dir)
|
|
393
413
|
roomNames.set(room, r.name || null) // feed list_sessions (cross-room roster)
|
|
394
414
|
process.stderr.write(`\n ◆ serving ${room}${r.name ? ` "${r.name}"` : ''} → ${dir}${dirs[room] ? '' : ' (default)'}\n`)
|
|
395
415
|
// Children NEVER self-update (only the supervisor restart re-resolves
|
package/auth-store.mjs
CHANGED
|
@@ -11,6 +11,11 @@ import path from 'node:path'
|
|
|
11
11
|
const DIR = path.join(os.homedir(), '.thinkpool-pair')
|
|
12
12
|
const AUTH = path.join(DIR, 'auth.json')
|
|
13
13
|
const DIRS = path.join(DIR, 'dirs.json')
|
|
14
|
+
// served.json: per-room memory of the last real (non-home) directory a room was
|
|
15
|
+
// auto-served from. Distinct from dirs.json (explicit binds): this is learned, so a
|
|
16
|
+
// supervisor relaunched from $HOME still resumes each room in the repo it last ran in
|
|
17
|
+
// instead of falling back to the home folder (FVIHV1DE, 2026-06-30).
|
|
18
|
+
const SERVED = path.join(DIR, 'served.json')
|
|
14
19
|
|
|
15
20
|
function ensureDir() { try { fs.mkdirSync(DIR, { recursive: true }) } catch { /* noop */ } }
|
|
16
21
|
|
|
@@ -41,3 +46,15 @@ export function unbindDir(room) {
|
|
|
41
46
|
try { fs.writeFileSync(DIRS, JSON.stringify(d, null, 2)) } catch { /* noop */ }
|
|
42
47
|
return d
|
|
43
48
|
}
|
|
49
|
+
|
|
50
|
+
// Learned per-room serve directory (see SERVED note above). loadServed feeds the
|
|
51
|
+
// dir-resolution precedence; rememberServed records the dir a room was just served
|
|
52
|
+
// from so a later relaunch-from-home resumes it there rather than in $HOME.
|
|
53
|
+
export function loadServed() { try { return JSON.parse(fs.readFileSync(SERVED, 'utf8')) } catch { return {} } }
|
|
54
|
+
export function rememberServed(room, dir) {
|
|
55
|
+
const d = loadServed(); if (!room || !dir || d[room] === dir) return d
|
|
56
|
+
d[room] = dir
|
|
57
|
+
ensureDir()
|
|
58
|
+
try { fs.writeFileSync(SERVED, JSON.stringify(d, null, 2)) } catch { /* noop */ }
|
|
59
|
+
return d
|
|
60
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thinkpool-pair",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.95",
|
|
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": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"transcript-sanitize.mjs",
|
|
17
17
|
"session-store.mjs",
|
|
18
18
|
"cross-terminal.mjs",
|
|
19
|
+
"serve-dir.mjs",
|
|
19
20
|
"service.mjs",
|
|
20
21
|
"presence.mjs",
|
|
21
22
|
"account.mjs",
|
package/serve-dir.mjs
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/* serve-dir.mjs — decide which working directory the account supervisor serves a
|
|
2
|
+
room from, and REFUSE the home-directory trap.
|
|
3
|
+
|
|
4
|
+
Why this exists (FVIHV1DE, 2026-06-30): the supervisor auto-serves an unbound room
|
|
5
|
+
from its own launch dir (DEFAULT_DIR = process.cwd()). When the supervisor is
|
|
6
|
+
relaunched from $HOME instead of the repo, every unbound room is served from $HOME.
|
|
7
|
+
That silently breaks two things:
|
|
8
|
+
1. The agent's cwd becomes ~, so the Claude SDK (which keys a session's transcript
|
|
9
|
+
by project directory) can't find the session that was created under the real
|
|
10
|
+
repo → resume fails → the agent is BLIND to its own history (the UI still replays
|
|
11
|
+
the persisted ThinkPool log, so "I see the chat but the agent can't").
|
|
12
|
+
2. The room's repo label is basename(cwd) = your home folder name ("max"/"conrad")
|
|
13
|
+
instead of "thinkpool" / a branch.
|
|
14
|
+
Loud refusal beats silent context corruption. Precedence:
|
|
15
|
+
explicit bind > last-served memory > the supervisor's launch dir
|
|
16
|
+
and an UNBOUND, never-served room is refused when the launch dir IS $HOME.
|
|
17
|
+
Pure (no fs/process) so it unit-tests without spawning bridges. */
|
|
18
|
+
import path from 'node:path'
|
|
19
|
+
|
|
20
|
+
const samePath = (a, b) => {
|
|
21
|
+
try { return !!a && !!b && path.resolve(a) === path.resolve(b) } catch { return false }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Returns { dir, refuse, reason, fromDefault }.
|
|
25
|
+
// - dir: the directory to serve from (even when refusing, for the message)
|
|
26
|
+
// - refuse: true → do NOT serve; warn the user to bind instead
|
|
27
|
+
// - reason: 'home' | 'none' when refusing
|
|
28
|
+
// - fromDefault:true → resolved to the supervisor's launch dir with no bind/memory
|
|
29
|
+
// (the caller should remember it after a successful serve, so a later
|
|
30
|
+
// relaunch-from-home resumes the room from this same repo)
|
|
31
|
+
export function resolveServeDir({ bound, remembered, defaultDir, home } = {}) {
|
|
32
|
+
const pick = bound || remembered || defaultDir || null
|
|
33
|
+
if (!pick) return { dir: null, refuse: true, reason: 'none' }
|
|
34
|
+
// Only the AUTO path (no explicit bind, no remembered dir) is guarded — if a person
|
|
35
|
+
// deliberately bound a room to their home dir, that's their call, honour it.
|
|
36
|
+
if (!bound && !remembered && samePath(pick, home)) {
|
|
37
|
+
return { dir: pick, refuse: true, reason: 'home' }
|
|
38
|
+
}
|
|
39
|
+
return { dir: pick, refuse: false, fromDefault: !bound && !remembered }
|
|
40
|
+
}
|