thinkpool-pair 0.7.336 → 0.7.337

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
@@ -17,6 +17,11 @@ not require an inbound port, tunnel, or public IP.
17
17
  - Hermes
18
18
  - The provider login or API credentials required by that runtime
19
19
 
20
+ **The bridge does not include a coding agent.** Claude Code, Codex, or Hermes
21
+ must already be installed and runnable on the bridge machine. BYOK only supplies
22
+ model-provider credentials to an installed runtime; a provider key does not
23
+ install or replace the agent itself.
24
+
20
25
  ## Start the bridge
21
26
 
22
27
  Run the launcher from the project directory the agents should use:
@@ -60,6 +65,35 @@ when that trade-off is intentional:
60
65
  npx thinkpool-pair@latest install-service --auto-update
61
66
  ```
62
67
 
68
+ ### Optional: keep the bridge computer awake
69
+
70
+ The launcher’s **Settings → Keep computer awake** option prevents system sleep
71
+ while the bridge is running. The same choice is available from the CLI:
72
+
73
+ ```bash
74
+ # Foreground, for this run only
75
+ npx thinkpool-pair@latest --keep-awake
76
+
77
+ # Persist the choice and install the background service
78
+ npx thinkpool-pair@latest install-service --keep-awake
79
+
80
+ # Persistently turn it back off while reinstalling/updating the service
81
+ npx thinkpool-pair@latest install-service --no-keep-awake
82
+ ```
83
+
84
+ **Important:** keep-awake can substantially increase battery use. It prevents
85
+ the computer from sleeping; it does **not** keep the display illuminated.
86
+ Display dimming, screen locking, and display sleep continue normally. Closing a
87
+ laptop lid may still put it to sleep depending on the operating system and its
88
+ power settings. On some Linux systems the sleep inhibitor can also block an
89
+ explicit suspend request while the bridge runs.
90
+
91
+ The implementation is process-bound and releases automatically when the bridge
92
+ stops: macOS uses `caffeinate -i`, Linux uses `systemd-inhibit --what=sleep`,
93
+ and Windows uses a system-only power request without `ES_DISPLAY_REQUIRED`.
94
+ If the platform helper is unavailable, the bridge warns and continues without
95
+ keep-awake rather than failing startup.
96
+
63
97
  Service implementation by platform:
64
98
 
65
99
  - macOS: LaunchAgent
@@ -231,9 +265,10 @@ log before reinstalling; repeated installation can hide the original failure.
231
265
 
232
266
  ### No runtimes are available
233
267
 
234
- Install/sign in to Claude, Codex, or Hermes on the host, then restart the
235
- launcher. Runtime availability is detected from the host; the web room cannot
236
- install a missing CLI for you.
268
+ Install and configure Claude Code, Codex, or Hermes on the host, then restart
269
+ the launcher. Runtime availability is detected from the host; the web room
270
+ cannot install a missing CLI. BYOK configures provider credentials only and
271
+ does not replace the local agent runtime.
237
272
 
238
273
  ### A custom model fails immediately
239
274
 
package/bridge.mjs CHANGED
@@ -68,6 +68,7 @@ import { hermesUserInputResponse } from './question-response.mjs'
68
68
  import { hostMemoryAdmission } from './host-memory.mjs'
69
69
  import { queueAbortBarrier, waitForAbortBarrier } from './abort-turn-barrier.mjs'
70
70
  import { PAIR_CLI, pairCli } from './command-guidance.mjs'
71
+ import { explicitKeepAwakeChoice, keepAwakeEnabled, saveKeepAwakePreference, startKeepAwake } from './keep-awake.mjs'
71
72
 
72
73
  const STRUCTURED_MODES = new Set(['default', 'acceptEdits', 'plan', 'review', 'bypassPermissions'])
73
74
  import { FLOW_CONDUCTOR_PROMPT, FLOW_LANE_PROMPT, FLOW_CODEX_CONDUCTOR_PROMPT, FLOW_CODEX_LANE_PROMPT, buildConductorEnv, assembleCrossWaveContext, buildLanePrompt } from './flow-conductor.mjs'
@@ -236,6 +237,17 @@ const pickAgent = (installed) => new Promise((resolve) => {
236
237
 
237
238
  const argv = process.argv.slice(2)
238
239
 
240
+ // CLI service flags also set the durable machine preference. Service updates read
241
+ // the same preference, so a later pinned-runtime update cannot silently turn the
242
+ // requested power behavior off. Foreground `--keep-awake` remains a one-run override.
243
+ if (argv[0] === 'install-service' || argv[0] === 'restart-service') {
244
+ const choice = explicitKeepAwakeChoice(argv)
245
+ if (choice !== null && !saveKeepAwakePreference(choice)) {
246
+ process.stderr.write(' ⚠ could not save the keep-awake setting; service left unchanged.\n')
247
+ process.exit(1)
248
+ }
249
+ }
250
+
239
251
  if (argv[0] === 'privacy-report') {
240
252
  const { runPrivacyReport } = await import('./privacy-report.mjs')
241
253
  runPrivacyReport()
@@ -362,7 +374,10 @@ if ((!argv[0] || argv[0] === 'setup') && process.stdin.isTTY) {
362
374
  const child = spawn(process.execPath, args, { stdio: 'inherit' })
363
375
  child.on('exit', (code) => process.exit(code == null ? 0 : code))
364
376
  }),
365
- serveAccountForeground: async () => { const { runAccount } = await import('./account.mjs'); await runAccount(SUPABASE_URL, SUPABASE_ANON) },
377
+ serveAccountForeground: async () => {
378
+ startKeepAwake({ enabled: keepAwakeEnabled([]) })
379
+ const { runAccount } = await import('./account.mjs'); await runAccount(SUPABASE_URL, SUPABASE_ANON)
380
+ },
366
381
  // Confirmed installs/updates continue in the exact runtime just proven live. This
367
382
  // keeps the person inside the npx menu without letting the stale installer claim
368
383
  // its in-memory VERSION matches the newly installed managed service.
@@ -407,6 +422,17 @@ if ((!argv[0] || argv[0] === 'setup') && process.stdin.isTTY) {
407
422
  : ['anthropic']
408
423
  await runProvider(args)
409
424
  },
425
+ setKeepAwake: async ({ enabled }) => {
426
+ if (!saveKeepAwakePreference(enabled)) {
427
+ io.print('\n ⚠ could not save the keep-awake setting; nothing was changed.')
428
+ return false
429
+ }
430
+ // A managed Unix service must restart to acquire/release its process-bound
431
+ // inhibitor. Windows' Startup entry has no daemon control; restartService
432
+ // prints the honest close-and-relaunch instruction instead.
433
+ if (svc.isServiceInstalled(null)) svc.restartService(null)
434
+ return true
435
+ },
410
436
  setupHermes: async ({ mode }) => {
411
437
  const { setupHermesRuntime } = await import('./hermes-setup.mjs')
412
438
  const result = setupHermesRuntime({ mode })
@@ -461,7 +487,10 @@ function checkSdkCompat() {
461
487
  }
462
488
  }
463
489
 
464
- if (!argv[0] || argv[0].startsWith('-')) { const { runAccount } = await import('./account.mjs'); await runAccount(SUPABASE_URL, SUPABASE_ANON) }
490
+ if (!argv[0] || argv[0].startsWith('-')) {
491
+ startKeepAwake({ enabled: keepAwakeEnabled(argv) })
492
+ const { runAccount } = await import('./account.mjs'); await runAccount(SUPABASE_URL, SUPABASE_ANON)
493
+ }
465
494
 
466
495
  const room = (argv[0] || '').toUpperCase().trim()
467
496
  if (!room) { console.error(`usage: ${pairCli('<ROOM>', '[--headless]', '[--continue|--fresh]', '[-- <command…>]')} | ${PAIR_CLI} (account mode)`); process.exit(1) }
@@ -533,6 +562,11 @@ if (_superOwn.includes('--supervise') || _superOwn.includes('--keep-alive')) {
533
562
  if (shouldHoldMachineLock(process.env)) process.on('exit', () => { try { releaseMachineLock() } catch { /* noop */ } })
534
563
  }
535
564
 
565
+ // Direct single-room mode owns one inhibitor in its actual serving process. Account
566
+ // supervisor children deliberately skip this (the account parent already owns it),
567
+ // and the --supervise wrapper above never reaches this line—only its child does.
568
+ startKeepAwake({ enabled: keepAwakeEnabled(argv) })
569
+
536
570
  const headless = argv.includes('--headless')
537
571
  // Account-mode children pass --auto=<agent> so a served session opens a live
538
572
  // terminal automatically (headless, no TTY/picker) instead of an empty room.
package/keep-awake.mjs ADDED
@@ -0,0 +1,148 @@
1
+ /* keep-awake.mjs — one explicit, machine-wide bridge preference with a
2
+ platform-native implementation. The assertion is SYSTEM-only: it never asks
3
+ any platform to keep the display lit, so normal dimming, display sleep, and
4
+ screen locking remain available.
5
+
6
+ The helper is deliberately tied to the bridge PID. A crash, Ctrl-C, update,
7
+ or service-manager replacement therefore releases the assertion without a
8
+ sticky machine-level power-policy change. */
9
+
10
+ import os from 'node:os'
11
+ import fs from 'node:fs'
12
+ import path from 'node:path'
13
+ import { spawn } from 'node:child_process'
14
+ import { fileURLToPath } from 'node:url'
15
+
16
+ const SETTINGS_FILE = 'settings.json'
17
+
18
+ function settingsPath(home = os.homedir()) {
19
+ return path.join(home, '.thinkpool-pair', SETTINGS_FILE)
20
+ }
21
+
22
+ export function loadKeepAwakePreference({ home = os.homedir(), fsImpl = fs } = {}) {
23
+ try { return JSON.parse(fsImpl.readFileSync(settingsPath(home), 'utf8')).keepAwake === true }
24
+ catch { return false }
25
+ }
26
+
27
+ export function saveKeepAwakePreference(enabled, { home = os.homedir(), fsImpl = fs } = {}) {
28
+ const file = settingsPath(home)
29
+ const dir = path.dirname(file)
30
+ let current = {}
31
+ try { current = JSON.parse(fsImpl.readFileSync(file, 'utf8')) || {} } catch { /* first setting */ }
32
+ const next = { ...current, keepAwake: enabled === true }
33
+ const tmp = `${file}.tmp.${process.pid}.${Date.now()}`
34
+ try {
35
+ fsImpl.mkdirSync(dir, { recursive: true })
36
+ fsImpl.writeFileSync(tmp, JSON.stringify(next, null, 2) + '\n', { mode: 0o600 })
37
+ fsImpl.renameSync(tmp, file)
38
+ return true
39
+ } catch {
40
+ try { fsImpl.rmSync(tmp, { force: true }) } catch { /* noop */ }
41
+ return false
42
+ }
43
+ }
44
+
45
+ export function explicitKeepAwakeChoice(argv = []) {
46
+ const dash = argv.indexOf('--')
47
+ const own = dash >= 0 ? argv.slice(0, dash) : argv
48
+ if (own.includes('--no-keep-awake')) return false
49
+ if (own.includes('--keep-awake')) return true
50
+ return null
51
+ }
52
+
53
+ export function keepAwakeEnabled(argv = [], { env = process.env, loadPreference = loadKeepAwakePreference } = {}) {
54
+ const explicit = explicitKeepAwakeChoice(argv)
55
+ if (explicit !== null) return explicit
56
+ if (env.THINKPOOL_PAIR_KEEP_AWAKE === '1') return true
57
+ if (env.THINKPOOL_PAIR_KEEP_AWAKE === '0') return false
58
+ return loadPreference() === true
59
+ }
60
+
61
+ const WINDOWS_HELPER = (parentPid) => [
62
+ '$ErrorActionPreference = "Stop"',
63
+ 'Add-Type -TypeDefinition \'using System; using System.Runtime.InteropServices; public static class ThinkpoolPower { [DllImport("kernel32.dll")] public static extern uint SetThreadExecutionState(uint flags); }\'',
64
+ // ES_CONTINUOUS | ES_SYSTEM_REQUIRED. ES_DISPLAY_REQUIRED (0x2) is intentionally absent.
65
+ '[ThinkpoolPower]::SetThreadExecutionState([uint32]0x80000001) | Out-Null',
66
+ `try { while (Get-Process -Id ${parentPid} -ErrorAction SilentlyContinue) { Start-Sleep -Seconds 5 } }`,
67
+ 'finally { [ThinkpoolPower]::SetThreadExecutionState([uint32]0x80000000) | Out-Null }',
68
+ ].join('; ')
69
+
70
+ // Pure command construction keeps the display-sleep contract testable on any host.
71
+ export function keepAwakeCommand(platform, parentPid, { node = process.execPath, modulePath = fileURLToPath(import.meta.url) } = {}) {
72
+ if (!Number.isInteger(parentPid) || parentPid <= 0) throw new Error('keep-awake requires a live bridge pid')
73
+ if (platform === 'darwin') {
74
+ // -i is the idle SYSTEM-sleep assertion. Do not add -d (display).
75
+ // -w makes caffeinate release automatically when the bridge PID exits.
76
+ return { command: '/usr/bin/caffeinate', args: ['-i', '-w', String(parentPid)], adapter: 'caffeinate' }
77
+ }
78
+ if (platform === 'linux') {
79
+ // Inhibit the sleep operation, not desktop "idle" handling: an idle inhibitor
80
+ // can also suppress screen blanking in some desktop environments. The tiny Node
81
+ // waiter exits with the parent, releasing systemd-logind's inhibitor lock.
82
+ return {
83
+ command: 'systemd-inhibit',
84
+ args: ['--what=sleep', '--mode=block', '--who=Thinkpool', '--why=Keep the Thinkpool bridge reachable', '--', node, modulePath, '--wait-for', String(parentPid)],
85
+ adapter: 'systemd-inhibit',
86
+ }
87
+ }
88
+ if (platform === 'win32') {
89
+ return {
90
+ command: 'powershell.exe',
91
+ args: ['-NoLogo', '-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-Command', WINDOWS_HELPER(parentPid)],
92
+ adapter: 'Windows power request',
93
+ }
94
+ }
95
+ return null
96
+ }
97
+
98
+ export function startKeepAwake({
99
+ enabled,
100
+ platform = process.platform,
101
+ parentPid = process.pid,
102
+ spawnImpl = spawn,
103
+ stderr = process.stderr,
104
+ accountChild = process.env.THINKPOOL_PAIR_ACCOUNT_CHILD === '1',
105
+ } = {}) {
106
+ if (!enabled || accountChild) return null
107
+ let spec
108
+ try { spec = keepAwakeCommand(platform, parentPid) }
109
+ catch (error) { stderr.write(` ⚠ keep-awake unavailable: ${error?.message || error}\n`); return null }
110
+ if (!spec) {
111
+ stderr.write(` ⚠ keep-awake is not supported on ${platform}; bridge startup will continue normally.\n`)
112
+ return null
113
+ }
114
+
115
+ let child
116
+ const startedAt = Date.now()
117
+ try {
118
+ child = spawnImpl(spec.command, spec.args, { stdio: 'ignore', windowsHide: true })
119
+ child.once?.('spawn', () => {
120
+ stderr.write(` ◆ keep-awake ON via ${spec.adapter}: system sleep is inhibited while this bridge runs.\n Your screen can still dim, lock, and turn off normally.\n`)
121
+ })
122
+ child.once?.('error', (error) => {
123
+ stderr.write(` ⚠ keep-awake could not start (${spec.adapter}: ${error?.message || error}); bridge startup will continue normally.\n`)
124
+ })
125
+ child.once?.('exit', (code) => {
126
+ if (code && Date.now() - startedAt < 30_000) stderr.write(` ⚠ keep-awake helper exited early (${spec.adapter}, code ${code}); the computer may sleep normally.\n`)
127
+ })
128
+ child.unref?.()
129
+ return child
130
+ } catch (error) {
131
+ stderr.write(` ⚠ keep-awake could not start (${spec.adapter}: ${error?.message || error}); bridge startup will continue normally.\n`)
132
+ return null
133
+ }
134
+ }
135
+
136
+ async function waitForProcess(pid) {
137
+ for (;;) {
138
+ try { process.kill(pid, 0) } catch { return }
139
+ await new Promise((resolve) => setTimeout(resolve, 5_000))
140
+ }
141
+ }
142
+
143
+ // Linux systemd-inhibit owns this helper as its COMMAND. It holds no power
144
+ // assertion itself; its lifetime simply matches the bridge process.
145
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) && process.argv[2] === '--wait-for') {
146
+ const pid = Number(process.argv[3])
147
+ if (Number.isInteger(pid) && pid > 0) await waitForProcess(pid)
148
+ }
package/launcher.mjs CHANGED
@@ -17,6 +17,7 @@ import { execSync } from 'node:child_process'
17
17
  import { detectProvider, fetchModels, ANTHROPIC_COMPATIBLE, gatewayHint } from './byok-detect.mjs'
18
18
  import { probeHermesRuntime } from './hermes-probe.mjs'
19
19
  import { darwinServiceRunning, serviceRuntimeVersion } from './service.mjs'
20
+ import { loadKeepAwakePreference } from './keep-awake.mjs'
20
21
 
21
22
  const HOME = os.homedir()
22
23
  const CFG_DIR = path.join(HOME, '.thinkpool-pair')
@@ -86,6 +87,7 @@ export function detectState() {
86
87
  hermesInstalled,
87
88
  hermesReady,
88
89
  accountSvc,
90
+ keepAwake: loadKeepAwakePreference(),
89
91
  serviceVersion: accountSvc ? serviceRuntimeVersion(null) : null,
90
92
  platform: process.platform,
91
93
  cwd: process.cwd(),
@@ -110,6 +112,7 @@ export async function runLauncher({ actions, io, state = detectState(), refresh
110
112
  loggedIn: !!next?.loggedIn,
111
113
  reconnectRequired: !!next?.reconnectRequired,
112
114
  accountSvc: !!next?.accountSvc,
115
+ keepAwake: next?.keepAwake === true,
113
116
  serviceVersion: typeof next?.serviceVersion === 'string' && next.serviceVersion ? next.serviceVersion : null,
114
117
  platform: next?.platform || process.platform,
115
118
  provider: next?.provider || 'Anthropic (default)',
@@ -134,7 +137,7 @@ export async function runLauncher({ actions, io, state = detectState(), refresh
134
137
  io.print(' ' + C.dim('┌ ') + C.bold('thinkpool-pair') + C.dim(' ' + '─'.repeat(40)))
135
138
  io.print(` ${C.dim('│ launcher ')} ${state.version ? `v${state.version}` : 'version unavailable'}`)
136
139
  io.print(` ${C.dim('│ account ')} ${state.reconnectRequired ? C.yellow('reconnect required') : state.loggedIn ? C.green((state.email || 'linked') + ' ✓') : C.yellow('not linked')}`)
137
- io.print(` ${C.dim('│ agents ')} ${state.agents.length ? state.agents.map(a => a.label).join(', ') : C.yellow('none ready')}`)
140
+ io.print(` ${C.dim('│ agents ')} ${state.agents.length ? state.agents.map(a => a.label).join(', ') : C.yellow('none ready: install Claude Code, Codex, or Hermes')}`)
138
141
  io.print(` ${C.dim('│ directory')} ${C.dim(state.cwd)}`)
139
142
  const bridgeStatus = !state.accountSvc
140
143
  ? 'not installed'
@@ -142,6 +145,7 @@ export async function runLauncher({ actions, io, state = detectState(), refresh
142
145
  ? C.green('startup entry installed')
143
146
  : C.green(`running${state.serviceVersion ? ` v${state.serviceVersion}` : ' (version unavailable)'}`)
144
147
  io.print(` ${C.dim('│ bridge ')} ${bridgeStatus}`)
148
+ io.print(` ${C.dim('│ keep awake')} ${state.keepAwake ? C.green('on') + C.dim(', display may still dim') : 'off'}`)
145
149
  io.print(' ' + C.dim('└' + '─'.repeat(54)) + '\n')
146
150
  }
147
151
 
@@ -176,8 +180,25 @@ export async function runLauncher({ actions, io, state = detectState(), refresh
176
180
  return true
177
181
  }
178
182
 
183
+ // A provider key is credentials, not an executable agent. Stop new installs
184
+ // before they create an apparently healthy but unusable background bridge.
185
+ const ensureAgentReady = async () => {
186
+ if (state.agents.length) return true
187
+ io.print('\n ' + C.yellow('No coding agent is ready on this computer'))
188
+ io.print(' Thinkpool connects an agent already installed on this machine. It does not')
189
+ io.print(' include one. Install and configure at least one of:')
190
+ io.print(`\n ${C.cyan('Claude Code')} command: claude`)
191
+ io.print(` ${C.cyan('Codex')} command: codex`)
192
+ io.print(` ${C.cyan('Hermes')} command: hermes, then set up its Thinkpool profile here`)
193
+ io.print('\n ' + C.bold('BYOK does not replace the agent. It only gives an installed agent'))
194
+ io.print(' credentials for a model provider.')
195
+ io.print('\n Install one, then reopen this launcher.')
196
+ return false
197
+ }
198
+
179
199
  const providerMenu = async () => {
180
200
  const cur = state.provider + (state.providerModel ? `, model ${state.providerModel}` : '')
201
+ io.print('\n ' + C.dim('Provider settings supply model credentials. Claude Code, Codex, or Hermes must still be installed locally.'))
181
202
  const p = await askChoice('\n provider', [
182
203
  { label: 'Anthropic (default)', hint: 'regular Claude login' },
183
204
  { label: 'Paste a key, pick a model', hint: 'auto-detects the provider · OpenRouter = any model' },
@@ -236,6 +257,7 @@ export async function runLauncher({ actions, io, state = detectState(), refresh
236
257
  const options = [
237
258
  { key: 'provider', label: 'Provider', hint: `current: ${state.provider}` },
238
259
  { key: 'account', label: 'Account', hint: state.reconnectRequired ? 'reconnect required' : state.loggedIn ? `linked: ${state.email || 'yes'}` : 'not linked' },
260
+ { key: 'awake', label: 'Keep computer awake', hint: `${state.keepAwake ? 'ON' : 'off'} · the screen can still dim, lock, and turn off` },
239
261
  ]
240
262
  if (state.hermesInstalled) options.push({ key: 'hermes', label: state.hermesReady ? 'Hermes profile' : 'Set up Hermes', hint: state.hermesReady ? 'isolated profile ready' : 'set up isolated profile + delegation guard' })
241
263
  options.push({ key: 'back', label: 'Back to main menu', hint: '' })
@@ -243,6 +265,30 @@ export async function runLauncher({ actions, io, state = detectState(), refresh
243
265
  if (picked.key === 'back') return
244
266
  if (picked.key === 'provider') await providerMenu()
245
267
  else if (picked.key === 'account') { await actions.login(); resync(); return }
268
+ else if (picked.key === 'awake') {
269
+ if (state.keepAwake) {
270
+ const changed = await actions.setKeepAwake({ enabled: false })
271
+ if (changed === false) continue
272
+ state = { ...state, keepAwake: false }
273
+ resync()
274
+ io.print('\n ' + C.green('✓ keep-awake is off. The computer may sleep normally.'))
275
+ } else {
276
+ io.print('\n ' + C.yellow('Important: keep-awake changes this computer’s power behavior'))
277
+ io.print(' While the bridge runs, Thinkpool will prevent the computer itself from')
278
+ io.print(' automatically sleeping. This can use substantially more battery and may')
279
+ io.print(' also block suspend requests on some Linux systems.')
280
+ io.print('\n ' + C.green('It does NOT keep the screen on. Display dimming, screen locking, and'))
281
+ io.print(' display sleep continue to work normally. Closing a laptop lid may still')
282
+ io.print(' sleep it, depending on the operating system and power settings.')
283
+ if (await askYesNo('\n Enable keep-awake while the Thinkpool bridge runs?', false)) {
284
+ const changed = await actions.setKeepAwake({ enabled: true })
285
+ if (changed === false) continue
286
+ state = { ...state, keepAwake: true }
287
+ resync()
288
+ io.print('\n ' + C.green('✓ keep-awake is on. Your screen is still free to dim and turn off.'))
289
+ } else io.print('\n ' + C.yellow('unchanged: keep-awake remains off.'))
290
+ }
291
+ }
246
292
  else if (picked.key === 'hermes') {
247
293
  const setup = await askChoice('\n Hermes profile', [
248
294
  { label: 'Clone active profile', hint: 'copies provider/config explicitly; fresh ThinkPool session history' },
@@ -277,13 +323,13 @@ export async function runLauncher({ actions, io, state = detectState(), refresh
277
323
  { key: 'serve', label: 'Serve all my sessions', hint: 'runs here, Ctrl-C stops it' },
278
324
  { key: 'service', label: 'Always-on background service', hint: 'recommended — survives reboot, restarts on crash' },
279
325
  ]
280
- items.push({ key: 'settings', label: 'Settings', hint: `provider · account${state.hermesInstalled ? ' · Hermes profile' : ''}` })
326
+ items.push({ key: 'settings', label: 'Settings', hint: `provider · account · keep-awake ${state.keepAwake ? 'ON' : 'off'}${state.hermesInstalled ? ' · Hermes profile' : ''}` })
281
327
  items.push({ key: 'quit', label: 'Quit', hint: '' })
282
328
  const pick = items[await askChoice('choose', items)]
283
329
  if (pick.key === 'quit') return
284
- if (pick.key === 'serve') { if (await ensureLoggedIn()) await actions.serveAccountForeground() }
330
+ if (pick.key === 'serve') { if (await ensureAgentReady() && await ensureLoggedIn()) await actions.serveAccountForeground() }
285
331
  else if (pick.key === 'service') {
286
- if (await ensureLoggedIn()) {
332
+ if (await ensureAgentReady() && await ensureLoggedIn()) {
287
333
  const installed = await actions.installService({ room: null })
288
334
  if (installed === true) {
289
335
  if (!actions.relaunchLauncher || await actions.relaunchLauncher()) return
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.336",
4
- "description": "Share a local coding-agent CLI (Claude Code, Codex, Gemini, Aider, …) into a ThinkPool Code room, live.",
3
+ "version": "0.7.337",
4
+ "description": "Connect Claude Code, Codex, or Hermes on your computer to a Thinkpool Code room.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "thinkpool-pair": "bridge.mjs"
@@ -12,6 +12,7 @@
12
12
  "command-guidance.mjs",
13
13
  "abort-turn-barrier.mjs",
14
14
  "host-memory.mjs",
15
+ "keep-awake.mjs",
15
16
  "sdk-smoke.mjs",
16
17
  "sdk-admission.mjs",
17
18
  "sdk-admission.mjs",
package/service.mjs CHANGED
@@ -25,6 +25,7 @@ import { execSync } from 'node:child_process'
25
25
  import { hostMemoryAdmission } from './host-memory.mjs'
26
26
  import { readSupervisorReady, supervisorReadyMatches } from './supervisor-ready.mjs'
27
27
  import { pairCli } from './command-guidance.mjs'
28
+ import { loadKeepAwakePreference } from './keep-awake.mjs'
28
29
 
29
30
  // Service identity. Account mode has no room → a single stable id so there's
30
31
  // exactly one account service per machine (a second install replaces it).
@@ -591,7 +592,11 @@ export function installService(room, cmdArgs = [], { autoUpdate = false, version
591
592
  : `PINNED to ${version} (stable local runtime; restarts do not depend on npx or the network). To update: use "Restart & update the bridge" (or run ${pairCli('install-service', room || undefined)}). Pass --auto-update to track @latest instead.`
592
593
  const removeArg = room ? ` ${room}` : ''
593
594
  const what = room ? `room ${room}` : 'your account (auto-serves every session)'
594
- process.stderr.write(` ◆ ${what}\n ◆ ${process.platform === 'darwin' ? 'Launchd is completing and verifying the reload independently.' : a.note}\n ◆ ${updateNote}\n ◆ logs: ${path.join(a.logDir, `${slug(room)}.log`)}\n ◆ remove with: ${pairCli('uninstall-service')}${removeArg}\n\n`)
595
+ const awakeNote = loadKeepAwakePreference()
596
+ ? ' ◆ Keep-awake on: system sleep is inhibited while the bridge runs (higher battery use); the display may still dim, lock, and turn off.\n'
597
+ : ''
598
+ const runtimeNote = ' ◆ Prerequisite: Claude Code, Codex, or Hermes must be installed on this machine. BYOK supplies model credentials; it does not install an agent.\n'
599
+ process.stderr.write(` ◆ ${what}\n ◆ ${process.platform === 'darwin' ? 'Launchd is completing and verifying the reload independently.' : a.note}\n${runtimeNote}${awakeNote} ◆ ${updateNote}\n ◆ logs: ${path.join(a.logDir, `${slug(room)}.log`)}\n ◆ remove with: ${pairCli('uninstall-service')}${removeArg}\n\n`)
595
600
  return true
596
601
  }
597
602