@prohost/cli 0.16.1 → 0.16.2

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/CHANGELOG.md CHANGED
@@ -6,6 +6,24 @@ builds via `prepack`, runs the suite, publishes, and tags `cli-v<version>`.
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 0.16.2
10
+
11
+ More `prohost host` follow-ups (they missed #22513):
12
+
13
+ - **Unlabelled `host pause` / `resume` / `status` act on this home only.** A dedicated service on
14
+ the same computer is addressed only by its `--label`: a personal sign-out (the desktop runs
15
+ `host pause`) never pauses it. A labelled `pause` / `resume` / `status` is about that service
16
+ alone: this home's pause, pairing and service never mix into it.
17
+ - **`host install --dedicated --remote-terminal | --no-remote-terminal`:** terminal sessions on a
18
+ dedicated service, off unless asked for; set in the service's definition, shown as
19
+ `remote_terminal` in `host status --json` and declared as the `remote_terminal` capability.
20
+ Every reader of the switches — the daemon, the relay and the permission hook in each Claude
21
+ window — sees the same setting, so remote approvals wait for an answer from ProhostAI.
22
+ - **`host logins add` on Windows** starts npm's `.cmd` shims through `cmd.exe /d /s /c`.
23
+ - **Reply classes:** only the server's (`general`, `status_update`, `pricing_action`,
24
+ `billing_action`, `legal_contractual`, `bug_in_our_code`); any other is sent as none, so the
25
+ server holds the reply.
26
+
9
27
  ## 0.16.1
10
28
 
11
29
  Review follow-ups to `prohost host` that landed on its branch after #22455 merged:
package/README.md CHANGED
@@ -206,6 +206,10 @@ from an administrator prompt. It:
206
206
  made in one step with administrators-only access, given the credential, and
207
207
  only then opened to the account (an existing one is never written into). If your own (personal) service runs on the same computer
208
208
  key, install says so and how to remove it;
209
+ - hosts terminal sessions only with `--remote-terminal` (`--no-remote-terminal`
210
+ or nothing: none) — decided in the service's definition, shown as
211
+ `remote_terminal` in `host status --json` and declared as the
212
+ `remote_terminal` capability only when on;
209
213
  - logs where the service account can't redirect it: the journal on Linux,
210
214
  `/Library/Logs/Prohost/<label>.log` (in a root-owned folder) on macOS,
211
215
  `C:\ProgramData\Prohost\Logs` (ACL: SYSTEM, administrators, the service
@@ -214,8 +218,9 @@ from an administrator prompt. It:
214
218
  emails, in a root-owned `/Library/Application Support/Prohost/hosts/<label>`,
215
219
  `/var/lib/prohost/hosts/<label>` or `C:\ProgramData\Prohost\hosts\<label>`: the service owns only
216
220
  `state.json`, `activity.json` and `events.json` there, and `control.json` is
217
- the pause switch anyone on the computer may flip. `host status` and
218
- `host pause` run as the logged-in user find it there, and
221
+ the pause switch anyone on the computer may flip. `host status --label <id>`
222
+ and `host pause|resume --label <id>` run as the logged-in user address it
223
+ there (unlabelled, they act on their own home only), and
219
224
  `sudo prohost host uninstall` finds the service by it (install prints the exact
220
225
  command with its `--label`).
221
226
 
@@ -287,8 +292,9 @@ start it again. `--dry-run` shows what it would do; `--json` prints the report.
287
292
  `login_moved`, `hold_started`, `hold_ended`, with the `/events` fields plus
288
293
  `computer_id` / `computer_name`), newest last, each with a stable `id`.
289
294
  `service` says whether a service runs for this home (`host/service.lock`), how
290
- many homes it adopted, and — run as the logged-in user on a dedicated computer —
291
- that it is the dedicated service (`activity_file` is then its shared copy).
295
+ many homes it adopted, and — with `--label <id>` of a dedicated service — that
296
+ it is the dedicated service (`activity_file` is then its shared copy).
297
+ `remote_terminal` says whether remote terminal sessions are on.
292
298
  - `host logins --json` → `{logins: [...same shape as status...]}`.
293
299
  - `$PROHOST_HOME/host/activity.json` → `{v: 1, busy, running: [...as in status...], updatedAt}`.
294
300
  - `$PROHOST_HOME/host/state.json` is the service's own state (0600); read
@@ -178,7 +178,7 @@ export function buildPrompt(run, context) {
178
178
  if (context.resumeNote)
179
179
  lines.push(context.resumeNote, '');
180
180
  if (context.askConfidence) {
181
- lines.push('End your reply with one last line, exactly: [[confidence=<0 to 1> reply_class=<class>]] — how sure you are', 'the reply is right and safe to send as is, and its class: general, pricing_action, billing_action,', 'legal_contractual or bug_in_our_code. That line is removed before sending; below the bar, or for a class', 'that needs approval, your reply is saved as a draft for the team instead of sent.', '');
181
+ lines.push('End your reply with one last line, exactly: [[confidence=<0 to 1> reply_class=<class>]] — how sure you are', 'the reply is right and safe to send as is, and its class: general, status_update, pricing_action,', 'billing_action, legal_contractual or bug_in_our_code. That line is removed before sending; below the bar, or for a class', 'that needs approval, your reply is saved as a draft for the team instead of sent.', '');
182
182
  }
183
183
  // The operator's own configuration comes first, above everything the server
184
184
  // generated. It is who this agent *is* — persona, house rules, escalation
@@ -282,6 +282,9 @@ export declare function isHeldAsSuggestion(result: ApiResult): boolean;
282
282
  * and a Codex check must never see the Claude token.
283
283
  */
284
284
  export declare function scopedCommandRunner(binary: string, base: CommandRunner | undefined, extra: () => NodeJS.ProcessEnv): CommandRunner;
285
+ /** A line that is the confidence trailer, however the model dressed it (backticks, bold, commas, quotes). */
286
+ /** The reply classes the server's send rule knows (PR B); any other is sent as none. */
287
+ export declare const REPLY_CLASSES: ReadonlySet<string>;
285
288
  /**
286
289
  * A reply without its confidence trailer (`[[confidence=0.9 reply_class=general]]`,
287
290
  * see `replyConfidence`), and what the last one said. Every trailer is removed,
package/dist/agent/run.js CHANGED
@@ -292,6 +292,15 @@ function extraEnvOf(options) {
292
292
  return typeof options.extraEnv === 'function' ? options.extraEnv() : (options.extraEnv ?? {});
293
293
  }
294
294
  /** A line that is the confidence trailer, however the model dressed it (backticks, bold, commas, quotes). */
295
+ /** The reply classes the server's send rule knows (PR B); any other is sent as none. */
296
+ export const REPLY_CLASSES = new Set([
297
+ 'general',
298
+ 'status_update',
299
+ 'pricing_action',
300
+ 'billing_action',
301
+ 'legal_contractual',
302
+ 'bug_in_our_code',
303
+ ]);
295
304
  /** A trailer token anywhere, with any decoration hugging it. */
296
305
  const TRAILER_TOKEN = /[`*_]*\[\[\s*confidence\b[^\]]*\]\][`*_]*\.?/gi;
297
306
  /** The confidence a trailer states: a plain decimal from 0 to 1, read whole — anything else is no confidence. */
@@ -328,7 +337,9 @@ export function splitConfidence(reply) {
328
337
  .join('\n')
329
338
  .replace(/\s+$/, '');
330
339
  const confidence = trailerConfidence(last);
331
- const replyClass = /reply_class\s*[=:]\s*["']?([A-Za-z_]{1,40})/i.exec(last)?.[1]?.toLowerCase();
340
+ const named = /reply_class\s*[=:]\s*["']?([A-Za-z_]{1,40})/i.exec(last)?.[1]?.toLowerCase();
341
+ // Only the server's own classes; anything else is no class, so the server holds the reply.
342
+ const replyClass = named && REPLY_CLASSES.has(named) ? named : undefined;
332
343
  if (confidence === undefined)
333
344
  return { text };
334
345
  return { text, confidence, ...(replyClass ? { replyClass } : {}) };
@@ -72,7 +72,7 @@ const OFFERED_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*(\/[A-Za-z0-9][A-Za-z0-9._-]*)?
72
72
  /** What a person reads when a thread names a repository this Mac has no checkout of. */
73
73
  export function notOfferedReason(repo, offered) {
74
74
  if (offered.length === 0) {
75
- return `This Mac isn't set up with any repositories for Code threads yet, so it can't work on ${repo}.`;
75
+ return `This Mac isn't set up with any repositories for terminals yet, so it can't work on ${repo}.`;
76
76
  }
77
77
  return `This Mac isn't set up for ${repo}. Pick one of its repositories: ${offered.join(', ')}.`;
78
78
  }
@@ -24,6 +24,13 @@ export interface CodeHostSettings {
24
24
  allowRemoteTerminal: boolean;
25
25
  }
26
26
  export declare const DEFAULT_SETTINGS: CodeHostSettings;
27
+ /**
28
+ * A dedicated `prohost host` service decides terminal sessions in its own definition
29
+ * (`host install --dedicated --remote-terminal | --no-remote-terminal`), not in the home's
30
+ * `settings.json`: `1` hosts with remote control on, `0` hosts nothing. Every reader — the daemon,
31
+ * the relay and the permission hook in each Claude window (which inherits it) — sees the same.
32
+ */
33
+ export declare const TERMINAL_SESSIONS_ENV = "PROHOST_HOST_TERMINAL_SESSIONS";
27
34
  /** `$PROHOST_HOME/code-host`. */
28
35
  export declare function codeHostDir(env?: NodeJS.ProcessEnv): string;
29
36
  export declare function settingsPath(env?: NodeJS.ProcessEnv): string;
@@ -27,6 +27,13 @@ export const DEFAULT_SETTINGS = Object.freeze({
27
27
  enabled: false,
28
28
  allowRemoteTerminal: false,
29
29
  });
30
+ /**
31
+ * A dedicated `prohost host` service decides terminal sessions in its own definition
32
+ * (`host install --dedicated --remote-terminal | --no-remote-terminal`), not in the home's
33
+ * `settings.json`: `1` hosts with remote control on, `0` hosts nothing. Every reader — the daemon,
34
+ * the relay and the permission hook in each Claude window (which inherits it) — sees the same.
35
+ */
36
+ export const TERMINAL_SESSIONS_ENV = 'PROHOST_HOST_TERMINAL_SESSIONS';
30
37
  /** `$PROHOST_HOME/code-host`. */
31
38
  export function codeHostDir(env = process.env) {
32
39
  return path.join(prohostHome(env), 'code-host');
@@ -94,6 +101,11 @@ export function readJsonObject(file) {
94
101
  * is the default — both off — and only a literal `true` turns a switch on.
95
102
  */
96
103
  export function loadSettings(env = process.env) {
104
+ const decided = env[TERMINAL_SESSIONS_ENV];
105
+ if (decided === '1')
106
+ return { version: 1, enabled: true, allowRemoteTerminal: true };
107
+ if (decided === '0')
108
+ return { ...DEFAULT_SETTINGS };
97
109
  const raw = readJsonObject(settingsPath(env));
98
110
  if (!raw || raw.version !== 1)
99
111
  return { ...DEFAULT_SETTINGS };
@@ -33,6 +33,7 @@ import { readHistory } from './history.js';
33
33
  import { CODE_THREAD_ID_ENV, TERMINAL_ID_ENV } from './hooks.js';
34
34
  import { MAX_OPEN_PTYS } from './pty.js';
35
35
  import { claudeSessionSince, isReady, markReady } from './ready.js';
36
+ import { TERMINAL_SESSIONS_ENV } from './settings.js';
36
37
  import { defaultSizeArgs, HOLD_OPTION, holdChannel, holdCommand, resizeWindowArgs, TerminalSizing } from './sizing.js';
37
38
  import { noteInputTakeover } from './sizing.js';
38
39
  import { attachArgs, baseArgs, clampDim, DEFAULT_COLS, DEFAULT_ROWS, hasSessionArgs, isCopyModeExitKey, isHumanInput, isTypedText, isInterruptInput, isScrollGesture, leaveCopyModeArgs, mayEnterCopyMode, paneInModeArgs, isValidId, killSessionArgs, listWindowsArgs, newSessionArgs, newWindowArgs, parseListWindows, sendTextCommands, setWindowMetaArgs, TERMINAL_WINDOWS, DAEMON_ONLY_ENV, paneEnv, viewSessionName, windowName, windowTarget, } from './tmux.js';
@@ -169,6 +170,14 @@ const CLAUDE_THREAD_ENV = { CLAUDE_CODE_ENABLE_CFC: '0' };
169
170
  * and the like), then the thread's overrides, which a login's environment never undoes. The one
170
171
  * builder for it: `startOnce` launches with it, and `adoptReadiness` reads the config dir from it.
171
172
  */
173
+ /**
174
+ * A dedicated service's terminal switch (`TERMINAL_SESSIONS_ENV`), handed to each window so its
175
+ * permission hook reads the same switches as the daemon whatever the tmux server inherited.
176
+ */
177
+ function terminalSessionsEnv(env) {
178
+ const decided = env[TERMINAL_SESSIONS_ENV];
179
+ return decided === undefined ? {} : { [TERMINAL_SESSIONS_ENV]: decided };
180
+ }
172
181
  function agentWindowEnv(agent, accountEnv) {
173
182
  return { ...stringEnv(accountEnv), ...(agent === 'claude_code' ? CLAUDE_THREAD_ENV : {}) };
174
183
  }
@@ -480,6 +489,7 @@ export class CodeTerminals {
480
489
  cwd: checkout.path,
481
490
  env: {
482
491
  ...agentWindowEnv(request.agent, account.env),
492
+ ...terminalSessionsEnv(this.options.processEnv ?? process.env),
483
493
  PROHOST_HOME: this.options.home,
484
494
  [TERMINAL_ID_ENV]: request.terminalId,
485
495
  [CODE_THREAD_ID_ENV]: request.codeThreadId,
@@ -55,5 +55,13 @@ export declare function statusJson(env: NodeJS.ProcessEnv, now?: number, options
55
55
  label?: string;
56
56
  platform?: NodeJS.Platform;
57
57
  }): Record<string, unknown>;
58
+ /**
59
+ * How to start a sign-in. On Windows npm installs `claude` / `codex` as `.cmd`
60
+ * shims, which only `cmd.exe` can run: the whole line goes through
61
+ * `cmd.exe /d /s /c "…"`, each part quoted (they are fixed words here).
62
+ */
63
+ export declare function signInSpawn(command: string, args: string[], platform?: NodeJS.Platform): [string, string[], {
64
+ windowsVerbatimArguments?: boolean;
65
+ }];
58
66
  export declare function runHost(subcommand: string | undefined, flags: Flags, printHelp: () => void, io?: HostIo): Promise<number>;
59
67
  export {};
@@ -97,26 +97,29 @@ export function hardwareFacts() {
97
97
  */
98
98
  export function statusJson(env, now = Date.now(), options = {}) {
99
99
  const computer = tryLoadComputer(env);
100
- const own = readObject(statePath(env));
101
- const ownFresh = own !== undefined && now - Date.parse(own.updatedAt) < STATE_STALE_MS;
102
- // `--label` asks for that service, even when this home runs one of its own.
103
- const shared = ownFresh && !options.label
104
- ? undefined
105
- : dedicatedState(options.sharedBase ?? sharedBase(options.platform ?? process.platform), now, options.label);
106
- const state = shared ? shared.state : own;
107
- const fresh = ownFresh || shared !== undefined;
100
+ // A dedicated service is reported only when asked for by its `--label`, and then only from what
101
+ // it publishes: unlabelled, status is about this home's own service, whatever else runs on the
102
+ // computer; labelled, nothing of this home's (its pause, its pairing, its lock) is mixed in.
103
+ const labelDir = options.label
104
+ ? path.join(options.sharedBase ?? sharedBase(options.platform ?? process.platform), options.label)
105
+ : undefined;
106
+ const shared = options.label
107
+ ? dedicatedState(options.sharedBase ?? sharedBase(options.platform ?? process.platform), now, options.label)
108
+ : undefined;
109
+ const own = labelDir ? undefined : readObject(statePath(env));
110
+ const state = labelDir ? shared?.state : own;
111
+ const fresh = labelDir ? shared !== undefined : own !== undefined && now - Date.parse(own.updatedAt) < STATE_STALE_MS;
108
112
  const sharedComputer = shared?.state.computer;
109
- const paused = readObject(controlPath(env))?.paused === true ||
110
- (shared !== undefined && readObject(path.join(shared.dir, 'control.json'))?.paused === true) ||
113
+ const paused = readObject(labelDir ? path.join(labelDir, 'control.json') : controlPath(env))?.paused === true ||
111
114
  (fresh && state?.paused === true);
112
115
  const recommended = state?.recommended_capacity ?? recommendCapacity({ memoryGb: os.totalmem() / 1024 ** 3, cores: os.cpus().length });
113
116
  const capacity = state?.capacity ?? recommended;
114
- const lock = readObject(hostLockPath(env));
115
- const events = shared ? readEvents(path.join(shared.dir, 'events.json'), now) : readEvents(journalPath(env), now);
117
+ const lock = labelDir ? undefined : readObject(hostLockPath(env));
118
+ const events = readEvents(labelDir ? path.join(labelDir, 'events.json') : journalPath(env), now);
116
119
  return {
117
120
  // Reporting the dedicated service: its own computer, never this home's (they can differ).
118
- paired: shared ? sharedComputer !== undefined : computer !== null,
119
- computer: shared
121
+ paired: labelDir ? sharedComputer !== undefined : computer !== null,
122
+ computer: labelDir
120
123
  ? (sharedComputer ?? null)
121
124
  : computer
122
125
  ? { id: computer.computer_id, name: (fresh && state?.name) || computer.name, account: computer.account }
@@ -131,12 +134,13 @@ export function statusJson(env, now = Date.now(), options = {}) {
131
134
  hardware: hardwareFacts(),
132
135
  events,
133
136
  service: {
134
- running: shared !== undefined || alive(lock?.pid),
135
- pid: shared ? (shared.state.pid ?? null) : alive(lock?.pid) ? lock?.pid : null,
136
- adopted: loadAdopted(env)?.homes.length ?? 0,
137
- dedicated: shared !== undefined,
138
- ...(shared ? { activity_file: path.join(shared.dir, 'activity.json') } : {}),
137
+ running: labelDir ? shared !== undefined : alive(lock?.pid),
138
+ pid: labelDir ? (shared?.state.pid ?? null) : alive(lock?.pid) ? lock?.pid : null,
139
+ adopted: labelDir ? 0 : (loadAdopted(env)?.homes.length ?? 0),
140
+ dedicated: labelDir !== undefined,
141
+ ...(labelDir ? { activity_file: path.join(labelDir, 'activity.json') } : {}),
139
142
  },
143
+ remote_terminal: Boolean(fresh && state?.remote_terminal),
140
144
  version: CLI_VERSION,
141
145
  };
142
146
  }
@@ -222,9 +226,21 @@ function homeOfUser(platform, user) {
222
226
  }
223
227
  return path.join('/home', user, '.prohost');
224
228
  }
229
+ /**
230
+ * How to start a sign-in. On Windows npm installs `claude` / `codex` as `.cmd`
231
+ * shims, which only `cmd.exe` can run: the whole line goes through
232
+ * `cmd.exe /d /s /c "…"`, each part quoted (they are fixed words here).
233
+ */
234
+ export function signInSpawn(command, args, platform = process.platform) {
235
+ if (platform !== 'win32')
236
+ return [command, args, {}];
237
+ const quote = (part) => (/^[A-Za-z0-9._:\\/-]+$/.test(part) ? part : `"${part.replace(/"/g, '""')}"`);
238
+ return ['cmd.exe', ['/d', '/s', '/c', `"${[command, ...args].map(quote).join(' ')}"`], { windowsVerbatimArguments: true }];
239
+ }
225
240
  /** `claude auth login` / `codex login`, in this terminal, as the new login. */
226
241
  const defaultSignIn = (command, args, env) => new Promise((resolve) => {
227
- const child = spawn(command, args, { stdio: 'inherit', env: { ...process.env, ...env } });
242
+ const [file, argv, extra] = signInSpawn(command, args);
243
+ const child = spawn(file, argv, { stdio: 'inherit', env: { ...process.env, ...env }, ...extra });
228
244
  child.on('error', () => resolve(null));
229
245
  child.on('exit', (code) => resolve(code));
230
246
  });
@@ -411,8 +427,14 @@ export async function runHost(subcommand, flags, printHelp, io = {}) {
411
427
  pathEnv: env.PATH ?? '',
412
428
  userHome,
413
429
  label: str(flags, 'label') ?? defaultLabel(home, path.join(userHome, '.prohost')),
430
+ // Terminal sessions on a dedicated service: off unless --remote-terminal.
431
+ ...(dedicated ? { terminalSessions: flags['remote-terminal'] === true && flags['no-remote-terminal'] !== true } : {}),
414
432
  electron: io.electron ?? (Boolean(process.versions.electron) || env.ELECTRON_RUN_AS_NODE === '1'),
415
433
  };
434
+ if (flags['remote-terminal'] === true && flags['no-remote-terminal'] === true) {
435
+ err('Pass --remote-terminal or --no-remote-terminal, not both.');
436
+ return 1;
437
+ }
416
438
  const tokenFile = str(flags, 'claude-token-file');
417
439
  if (flags['claude-token-file'] === true) {
418
440
  err('--claude-token-file needs a path.');
@@ -516,20 +538,28 @@ export async function runHost(subcommand, flags, printHelp, io = {}) {
516
538
  case 'resume': {
517
539
  const paused = subcommand === 'pause';
518
540
  const control = { v: 1, paused, updated_at: new Date().toISOString() };
519
- writePrivateFile(controlPath(env), control);
520
- // A dedicated service on this computer reads its own shared switch (writable by everyone on it).
541
+ // A dedicated service reads its own shared switch (writable by everyone on the computer):
542
+ // flipped only when it is named (`--label`), and then it alone. Unlabelled, this home alone
543
+ // is paused — a personal sign-out never pauses a dedicated service on the same computer.
521
544
  const label = str(flags, 'label');
522
- for (const dir of sharedDirs(io.sharedBase ?? sharedBase(platform), label)) {
545
+ if (label) {
546
+ const dir = path.join(io.sharedBase ?? sharedBase(platform), label);
523
547
  try {
524
548
  writeFileSync(path.join(dir, 'control.json'), `${JSON.stringify(control)}\n`);
525
549
  }
526
550
  catch (e) {
527
- err(` ! could not ${subcommand} the service in ${dir}: ${e instanceof Error ? e.message : String(e)}`);
551
+ err(`Could not ${subcommand} the service in ${dir}: ${e instanceof Error ? e.message : String(e)}`);
552
+ return 1;
528
553
  }
529
554
  }
555
+ else {
556
+ writePrivateFile(controlPath(env), control);
557
+ }
558
+ const which = label ? `the service ${label}` : 'this computer';
559
+ const again = label ? `prohost host resume --label ${label}` : 'prohost host resume';
530
560
  out(paused
531
- ? '⏸ Paused: this computer takes no new work. Work already going finishes. Resume with: prohost host resume'
532
- : '▶ Resumed: this computer takes new work again.');
561
+ ? `⏸ Paused: ${which} takes no new work. Work already going finishes. Resume with: ${again}`
562
+ : `▶ Resumed: ${which} takes new work again.`);
533
563
  return 0;
534
564
  }
535
565
  case 'migrate': {
@@ -31,6 +31,7 @@ export declare const STOP_TIMEOUT_SECONDS = 600;
31
31
  export declare const RESTART_WAIT_MS: number;
32
32
  /** Set in a dedicated service's environment: where it publishes its state for the logged-in user. */
33
33
  export declare const SHARED_DIR_ENV = "PROHOST_HOST_SHARED_DIR";
34
+ /** Set in a dedicated service's environment: terminal sessions on (`1`) or off (`0`), from install. */
34
35
  /** The `claude setup-token` token a dedicated install hands the service user (read by `host run`). */
35
36
  export declare const CLAUDE_TOKEN_FILE = "claude-oauth-token";
36
37
  export type ServiceRunner = (command: string, args: string[], input?: string) => {
@@ -65,6 +66,11 @@ export interface ServiceSpec {
65
66
  * service keeps `ELECTRON_RUN_AS_NODE=1`, or launching it would open the app.
66
67
  */
67
68
  electron?: boolean;
69
+ /**
70
+ * Dedicated only: terminal sessions (remote terminals) on this service —
71
+ * `--remote-terminal`. Off unless asked for.
72
+ */
73
+ terminalSessions?: boolean;
68
74
  }
69
75
  /** The label for a home: the plain one for the default home, else one with a short digest of the home. */
70
76
  export declare function defaultLabel(home: string, defaultHome: string): string;
@@ -24,6 +24,7 @@ import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, rmSync, writ
24
24
  import os from 'node:os';
25
25
  import path from 'node:path';
26
26
  import { buildLaunchdPlist, waitUntilUnloaded } from '../agent/daemon.js';
27
+ import { TERMINAL_SESSIONS_ENV } from '../codehost/settings.js';
27
28
  export const LAUNCHD_HOST_LABEL = 'ai.prohost.host';
28
29
  export const SYSTEMD_HOST_UNIT = 'prohost-host.service';
29
30
  export const WINDOWS_TASK_NAME = '\\ProhostAI\\Host';
@@ -37,6 +38,7 @@ export const STOP_TIMEOUT_SECONDS = 600;
37
38
  export const RESTART_WAIT_MS = (STOP_TIMEOUT_SECONDS + 30) * 1000;
38
39
  /** Set in a dedicated service's environment: where it publishes its state for the logged-in user. */
39
40
  export const SHARED_DIR_ENV = 'PROHOST_HOST_SHARED_DIR';
41
+ /** Set in a dedicated service's environment: terminal sessions on (`1`) or off (`0`), from install. */
40
42
  /** The `claude setup-token` token a dedicated install hands the service user (read by `host run`). */
41
43
  export const CLAUDE_TOKEN_FILE = 'claude-oauth-token';
42
44
  export const defaultServiceRunner = (command, args, input) => {
@@ -101,7 +103,9 @@ export function serviceEnv(spec) {
101
103
  return {
102
104
  PATH: spec.pathEnv,
103
105
  PROHOST_HOME: spec.home,
104
- ...(spec.dedicated ? { PROHOST_HOST_DEDICATED: '1', [SHARED_DIR_ENV]: sharedDirFor(spec) } : {}),
106
+ ...(spec.dedicated
107
+ ? { PROHOST_HOST_DEDICATED: '1', [SHARED_DIR_ENV]: sharedDirFor(spec), [TERMINAL_SESSIONS_ENV]: spec.terminalSessions ? '1' : '0' }
108
+ : {}),
105
109
  ...(spec.dedicated && spec.platform === 'darwin' && spec.serviceUser
106
110
  ? { HOME: path.posix.join('/Users', spec.serviceUser) }
107
111
  : {}),
@@ -185,6 +185,9 @@ async function serveHost(options) {
185
185
  const codeHostEnv = computer.code_host_home
186
186
  ? { ...process.env, ...env, PROHOST_HOME: computer.code_host_home }
187
187
  : { ...process.env, ...env };
188
+ // A dedicated install decides terminal sessions in the service's own definition
189
+ // (`--remote-terminal` / `--no-remote-terminal`, `TERMINAL_SESSIONS_ENV`), which `loadSettings`
190
+ // reads over the home's switches; a personal service follows the home's switches.
188
191
  const codeHost = options.codeHost !== undefined
189
192
  ? (options.codeHost ?? undefined)
190
193
  : loadCodeHostSettings(codeHostEnv).enabled
@@ -367,6 +370,7 @@ async function serveHost(options) {
367
370
  online,
368
371
  paused: localPaused || config.paused,
369
372
  draining,
373
+ remote_terminal: capabilities().includes(REMOTE_TERMINAL_CAPABILITY),
370
374
  slots: slotsFor(capacity(), active.size + terminals(), held),
371
375
  capacity: capacity(),
372
376
  recommended_capacity: config.recommended_capacity ?? recommended(),
@@ -80,6 +80,8 @@ export interface HostState {
80
80
  online: boolean;
81
81
  paused: boolean;
82
82
  draining: boolean;
83
+ /** Remote terminal sessions are on (the `remote_terminal` capability). */
84
+ remote_terminal?: boolean;
83
85
  slots: Slots;
84
86
  capacity: number;
85
87
  recommended_capacity: number;
package/dist/index.js CHANGED
@@ -35,7 +35,7 @@ function printHelp() {
35
35
  ' prohost host unpair Remove this computer from its workspace (revokes its key)',
36
36
  ' prohost host run One service for every AI employee placed on this computer',
37
37
  ' prohost host install [--label <id>] [--dedicated --service-user <name> [--service-home <dir>]',
38
- ' [--claude-token-file <path>]] | uninstall [--label <id>]',
38
+ ' [--claude-token-file <path>] [--remote-terminal|--no-remote-terminal]] | uninstall [--label <id>]',
39
39
  ' prohost host status [--json] [--label <id>] | pause | resume',
40
40
  ' prohost host logins [--json] | logins add --runtime claude_code|codex [--label <l>]',
41
41
  ' prohost host migrate [--from <home>]… [--home <dir,…>] [--keep-services] [--dry-run] [--json]',
@@ -72,6 +72,8 @@ function printHelp() {
72
72
  ' publishes its status for `host status` run by anyone on the computer.',
73
73
  ' --claude-token-file `host install --dedicated`: a file holding a `claude setup-token` token, installed',
74
74
  ' for the service user as CLAUDE_CODE_OAUTH_TOKEN. Read once, never printed.',
75
+ ' --remote-terminal / --no-remote-terminal `host install --dedicated`: terminal sessions on this',
76
+ ' service (off unless --remote-terminal); `host status --json` shows remote_terminal.',
75
77
  ' --from-home `host install --dedicated`: the home whose computer.json the service takes over',
76
78
  " (default: $PROHOST_HOME, or $SUDO_USER's ~/.prohost under sudo).",
77
79
  ' --label `host install` / `uninstall`: the service\'s label (launchd label, systemd unit,',
package/dist/version.d.ts CHANGED
@@ -7,6 +7,6 @@
7
7
  * `package.json`, so a release bump that forgets this file fails the suite
8
8
  * instead of shipping a `User-Agent` that lies about which build is calling.
9
9
  */
10
- export declare const CLI_VERSION = "0.16.1";
10
+ export declare const CLI_VERSION = "0.16.2";
11
11
  /** Sent on every HTTP request the CLI makes back into ProhostAI. */
12
- export declare const USER_AGENT = "prohost-cli/0.16.1";
12
+ export declare const USER_AGENT = "prohost-cli/0.16.2";
package/dist/version.js CHANGED
@@ -7,6 +7,6 @@
7
7
  * `package.json`, so a release bump that forgets this file fails the suite
8
8
  * instead of shipping a `User-Agent` that lies about which build is calling.
9
9
  */
10
- export const CLI_VERSION = '0.16.1';
10
+ export const CLI_VERSION = '0.16.2';
11
11
  /** Sent on every HTTP request the CLI makes back into ProhostAI. */
12
12
  export const USER_AGENT = `prohost-cli/${CLI_VERSION}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prohost/cli",
3
- "version": "0.16.1",
3
+ "version": "0.16.2",
4
4
  "description": "Run your own AI agent as a ProhostAI teammate, and stream your account's webhooks to your laptop.",
5
5
  "type": "module",
6
6
  "license": "MIT",