@prohost/cli 0.16.1 → 0.16.3
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 +37 -0
- package/README.md +10 -4
- package/dist/agent/api.d.ts +4 -3
- package/dist/agent/prompt.js +1 -1
- package/dist/agent/run.d.ts +8 -6
- package/dist/agent/run.js +96 -22
- package/dist/codehost/checkouts.js +1 -1
- package/dist/codehost/settings.d.ts +20 -0
- package/dist/codehost/settings.js +35 -0
- package/dist/host/command.d.ts +8 -0
- package/dist/host/command.js +56 -26
- package/dist/host/install.d.ts +6 -0
- package/dist/host/install.js +5 -1
- package/dist/host/service.d.ts +3 -0
- package/dist/host/service.js +17 -6
- package/dist/host/status.d.ts +2 -0
- package/dist/index.js +3 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,43 @@ builds via `prepack`, runs the suite, publishes, and tags `cli-v<version>`.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.16.3
|
|
10
|
+
|
|
11
|
+
- **A dedicated service records its terminal switch in its home's code-host settings** as it
|
|
12
|
+
starts (`--remote-terminal` / `--no-remote-terminal`), instead of an environment variable a
|
|
13
|
+
surviving tmux window would keep: after a reinstall with `--no-remote-terminal`, a Claude window
|
|
14
|
+
still open from before stops advertising remote approvals nobody can give, and answers its
|
|
15
|
+
permission prompts in the terminal. It fails closed: `--no-remote-terminal` never hosts (the
|
|
16
|
+
definition decides in the service, whatever the file says), and a switch it can't record hosts
|
|
17
|
+
nothing and removes the old file when it can, so hooks read remote control as off (the log says
|
|
18
|
+
when even that fails).
|
|
19
|
+
- **A muted thread's turn is never resumed.** When an early answer was dropped because the thread
|
|
20
|
+
is muted (`409 ai_muted`), the conversation's session is forgotten on every exit — cancelled,
|
|
21
|
+
usage-limited, closed server-side, expired or failed too — not only when the run finishes.
|
|
22
|
+
- **Replies the server holds back are handled, not failed or retried** (dispatch #22485):
|
|
23
|
+
`409 approval_required` ("waiting for approval": sent when someone approves the card — the
|
|
24
|
+
session continues), `409 send_blocked` ("blocked: <reason>", e.g. a duplicate — never delivered,
|
|
25
|
+
so the session is forgotten), and `held_as_suggestion` with any reason, `pinned_rule` included
|
|
26
|
+
(the session continues, as before). Any other 409 is still a failure. Works with today's server.
|
|
27
|
+
|
|
28
|
+
## 0.16.2
|
|
29
|
+
|
|
30
|
+
More `prohost host` follow-ups (they missed #22513):
|
|
31
|
+
|
|
32
|
+
- **Unlabelled `host pause` / `resume` / `status` act on this home only.** A dedicated service on
|
|
33
|
+
the same computer is addressed only by its `--label`: a personal sign-out (the desktop runs
|
|
34
|
+
`host pause`) never pauses it. A labelled `pause` / `resume` / `status` is about that service
|
|
35
|
+
alone: this home's pause, pairing and service never mix into it.
|
|
36
|
+
- **`host install --dedicated --remote-terminal | --no-remote-terminal`:** terminal sessions on a
|
|
37
|
+
dedicated service, off unless asked for; set in the service's definition, shown as
|
|
38
|
+
`remote_terminal` in `host status --json` and declared as the `remote_terminal` capability.
|
|
39
|
+
Every reader of the switches — the daemon, the relay and the permission hook in each Claude
|
|
40
|
+
window — sees the same setting, so remote approvals wait for an answer from ProhostAI.
|
|
41
|
+
- **`host logins add` on Windows** starts npm's `.cmd` shims through `cmd.exe /d /s /c`.
|
|
42
|
+
- **Reply classes:** only the server's (`general`, `status_update`, `pricing_action`,
|
|
43
|
+
`billing_action`, `legal_contractual`, `bug_in_our_code`); any other is sent as none, so the
|
|
44
|
+
server holds the reply.
|
|
45
|
+
|
|
9
46
|
## 0.16.1
|
|
10
47
|
|
|
11
48
|
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
|
|
218
|
-
`host pause
|
|
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 —
|
|
291
|
-
|
|
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
|
package/dist/agent/api.d.ts
CHANGED
|
@@ -31,10 +31,11 @@ export interface ApiResult {
|
|
|
31
31
|
ok: boolean;
|
|
32
32
|
/**
|
|
33
33
|
* A send the server answered without sending (still `ok`: the run is done):
|
|
34
|
-
* saved as a draft for the team (`suggestion`),
|
|
35
|
-
* is muted for AI (`muted`)
|
|
34
|
+
* saved as a draft for the team (`suggestion`), waiting on an approval card
|
|
35
|
+
* (`approval`), dropped because the thread is muted for AI (`muted`), or
|
|
36
|
+
* stopped by the outbound guard (`blocked`). See `withheldBy` in run.ts.
|
|
36
37
|
*/
|
|
37
|
-
withheld?: 'suggestion' | 'muted';
|
|
38
|
+
withheld?: 'suggestion' | 'approval' | 'muted' | 'blocked';
|
|
38
39
|
/** HTTP status, or ``0`` when no response arrived at all — see ``failure``. */
|
|
39
40
|
status: number;
|
|
40
41
|
error?: string;
|
package/dist/agent/prompt.js
CHANGED
|
@@ -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,
|
|
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
|
package/dist/agent/run.d.ts
CHANGED
|
@@ -270,18 +270,20 @@ export interface AgentRunMetrics {
|
|
|
270
270
|
* nor a failure and must not be counted as either.
|
|
271
271
|
*/
|
|
272
272
|
type RunOutcome = 'succeeded' | 'failed' | 'cancelled' | 'resolved' | 'unreported' | 'skipped';
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
export declare function isHeldAsSuggestion(result: ApiResult): boolean;
|
|
273
|
+
export declare function withheldBy(result: ApiResult): {
|
|
274
|
+
kind: NonNullable<ApiResult['withheld']>;
|
|
275
|
+
reason?: string;
|
|
276
|
+
approvalRequestId?: string;
|
|
277
|
+
} | undefined;
|
|
279
278
|
/**
|
|
280
279
|
* A sign-in checker that adds `extra` only when it runs `binary` (this
|
|
281
280
|
* harness's Claude): the account snapshot checks other runtimes' logins too,
|
|
282
281
|
* and a Codex check must never see the Claude token.
|
|
283
282
|
*/
|
|
284
283
|
export declare function scopedCommandRunner(binary: string, base: CommandRunner | undefined, extra: () => NodeJS.ProcessEnv): CommandRunner;
|
|
284
|
+
/** A line that is the confidence trailer, however the model dressed it (backticks, bold, commas, quotes). */
|
|
285
|
+
/** The reply classes the server's send rule knows (PR B); any other is sent as none. */
|
|
286
|
+
export declare const REPLY_CLASSES: ReadonlySet<string>;
|
|
285
287
|
/**
|
|
286
288
|
* A reply without its confidence trailer (`[[confidence=0.9 reply_class=general]]`,
|
|
287
289
|
* see `replyConfidence`), and what the last one said. Every trailer is removed,
|
package/dist/agent/run.js
CHANGED
|
@@ -210,13 +210,18 @@ const REPLAY_IN_FLIGHT_DELAY_MS = 5_000;
|
|
|
210
210
|
*/
|
|
211
211
|
async function postReplyWithReplay(api, send, label, log, sleep) {
|
|
212
212
|
const result = await postReplyOnce(api, send, label, log, sleep);
|
|
213
|
-
// The
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
213
|
+
// The server decided not to send it (yet): handled, not failed, and never retried.
|
|
214
|
+
const held = withheldBy(result);
|
|
215
|
+
if (held) {
|
|
216
|
+
const why = held.reason ? `: ${held.reason}` : '';
|
|
217
|
+
log(held.kind === 'muted'
|
|
217
218
|
? `${timestamp()} ◦ ${label} reply was not sent: the thread is muted for AI`
|
|
218
|
-
:
|
|
219
|
-
|
|
219
|
+
: held.kind === 'approval'
|
|
220
|
+
? `${timestamp()} ◦ ${label} reply is waiting for approval${held.approvalRequestId ? ` (approval ${held.approvalRequestId})` : ''}: it is sent when someone approves it`
|
|
221
|
+
: held.kind === 'blocked'
|
|
222
|
+
? `${timestamp()} ◦ ${label} reply was not sent: blocked: ${held.reason ?? 'the outbound guard'}`
|
|
223
|
+
: `${timestamp()} ◦ ${label} reply was held as a suggestion for the team (the employee's send rule${why})`);
|
|
224
|
+
return { ...result, ok: true, withheld: held.kind };
|
|
220
225
|
}
|
|
221
226
|
return result;
|
|
222
227
|
}
|
|
@@ -272,12 +277,62 @@ function resolveWorkdir(options, log) {
|
|
|
272
277
|
}
|
|
273
278
|
}
|
|
274
279
|
/**
|
|
275
|
-
* A send the server answered without sending, deliberately
|
|
276
|
-
*
|
|
277
|
-
*
|
|
280
|
+
* A send the server answered without sending, deliberately (a 409). Either way the run is done,
|
|
281
|
+
* nothing is retried, and whether the conversation's session continues follows from it:
|
|
282
|
+
*
|
|
283
|
+
* - `held_as_suggestion` → `suggestion`: saved as a draft for the team (the send rule, or a
|
|
284
|
+
* `pinned_rule`). The team may send it as is: the session continues.
|
|
285
|
+
* - `approval_required` → `approval`: below the employee's threshold, it waits on an approval card
|
|
286
|
+
* and the server sends it when someone approves: the session continues.
|
|
287
|
+
* - `ai_muted` → `muted`: the thread is muted; nothing saved. The guest never sees it: the session
|
|
288
|
+
* is forgotten.
|
|
289
|
+
* - `send_blocked` → `blocked`: the shared outbound guard stopped it (e.g. a duplicate). Not
|
|
290
|
+
* delivered by this run: the session is forgotten.
|
|
291
|
+
*
|
|
292
|
+
* Any other 409 is not a decision and stays a failure.
|
|
278
293
|
*/
|
|
279
|
-
|
|
280
|
-
|
|
294
|
+
const WITHHELD_CODES = new Map([
|
|
295
|
+
['ai_muted', 'muted'],
|
|
296
|
+
['approval_required', 'approval'],
|
|
297
|
+
['send_blocked', 'blocked'],
|
|
298
|
+
['held_as_suggestion', 'suggestion'],
|
|
299
|
+
]);
|
|
300
|
+
export function withheldBy(result) {
|
|
301
|
+
if (result.status !== 409)
|
|
302
|
+
return undefined;
|
|
303
|
+
const text = result.error ?? '';
|
|
304
|
+
// The server's own code, matched exactly — never a word found elsewhere in the body (a 409
|
|
305
|
+
// like `reply_race_held` quotes another reply's text, which may name any of these):
|
|
306
|
+
// `{detail: {error, reason, approval_request_id, …}}`, the same fields at the top, or
|
|
307
|
+
// `{detail: "<code>"}`. A body cut short (`api.ts` keeps 500 chars) is read for its first
|
|
308
|
+
// `"error": "…"` field only.
|
|
309
|
+
let fields = {};
|
|
310
|
+
let code;
|
|
311
|
+
try {
|
|
312
|
+
const parsed = JSON.parse(text);
|
|
313
|
+
const top = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
314
|
+
const detail = top.detail;
|
|
315
|
+
if (typeof detail === 'string')
|
|
316
|
+
code = detail;
|
|
317
|
+
else {
|
|
318
|
+
fields = detail && typeof detail === 'object' && !Array.isArray(detail) ? detail : top;
|
|
319
|
+
code = typeof fields.error === 'string' ? fields.error : undefined;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
code = /"error"\s*:\s*"([a-z_]+)"/.exec(text)?.[1];
|
|
324
|
+
}
|
|
325
|
+
const kind = code === undefined ? undefined : WITHHELD_CODES.get(code);
|
|
326
|
+
if (!kind)
|
|
327
|
+
return undefined;
|
|
328
|
+
const str = (value) => (typeof value === 'string' && value ? value.slice(0, 120) : undefined);
|
|
329
|
+
const reason = str(fields.reason);
|
|
330
|
+
const approvalRequestId = str(fields.approval_request_id);
|
|
331
|
+
return { kind, ...(reason ? { reason } : {}), ...(approvalRequestId ? { approvalRequestId } : {}) };
|
|
332
|
+
}
|
|
333
|
+
/** A withheld reply the guest will never get from this run: its session must not continue. */
|
|
334
|
+
function undeliveredFor(withheld) {
|
|
335
|
+
return withheld === 'muted' || withheld === 'blocked';
|
|
281
336
|
}
|
|
282
337
|
/**
|
|
283
338
|
* A sign-in checker that adds `extra` only when it runs `binary` (this
|
|
@@ -292,6 +347,15 @@ function extraEnvOf(options) {
|
|
|
292
347
|
return typeof options.extraEnv === 'function' ? options.extraEnv() : (options.extraEnv ?? {});
|
|
293
348
|
}
|
|
294
349
|
/** A line that is the confidence trailer, however the model dressed it (backticks, bold, commas, quotes). */
|
|
350
|
+
/** The reply classes the server's send rule knows (PR B); any other is sent as none. */
|
|
351
|
+
export const REPLY_CLASSES = new Set([
|
|
352
|
+
'general',
|
|
353
|
+
'status_update',
|
|
354
|
+
'pricing_action',
|
|
355
|
+
'billing_action',
|
|
356
|
+
'legal_contractual',
|
|
357
|
+
'bug_in_our_code',
|
|
358
|
+
]);
|
|
295
359
|
/** A trailer token anywhere, with any decoration hugging it. */
|
|
296
360
|
const TRAILER_TOKEN = /[`*_]*\[\[\s*confidence\b[^\]]*\]\][`*_]*\.?/gi;
|
|
297
361
|
/** The confidence a trailer states: a plain decimal from 0 to 1, read whole — anything else is no confidence. */
|
|
@@ -328,7 +392,9 @@ export function splitConfidence(reply) {
|
|
|
328
392
|
.join('\n')
|
|
329
393
|
.replace(/\s+$/, '');
|
|
330
394
|
const confidence = trailerConfidence(last);
|
|
331
|
-
const
|
|
395
|
+
const named = /reply_class\s*[=:]\s*["']?([A-Za-z_]{1,40})/i.exec(last)?.[1]?.toLowerCase();
|
|
396
|
+
// Only the server's own classes; anything else is no class, so the server holds the reply.
|
|
397
|
+
const replyClass = named && REPLY_CLASSES.has(named) ? named : undefined;
|
|
332
398
|
if (confidence === undefined)
|
|
333
399
|
return { text };
|
|
334
400
|
return { text, confidence, ...(replyClass ? { replyClass } : {}) };
|
|
@@ -1182,8 +1248,11 @@ limited) {
|
|
|
1182
1248
|
let earlyCount = 0;
|
|
1183
1249
|
let earlyPosted = 0;
|
|
1184
1250
|
let earlyPosts = Promise.resolve();
|
|
1185
|
-
/**
|
|
1186
|
-
|
|
1251
|
+
/**
|
|
1252
|
+
* An answer of this run will never reach the guest (the thread is muted, or the outbound guard
|
|
1253
|
+
* blocked it — `undeliveredFor`): its session must not continue.
|
|
1254
|
+
*/
|
|
1255
|
+
let undelivered = false;
|
|
1187
1256
|
const earlyPath = run.reply_path;
|
|
1188
1257
|
const earlyBodyKey = run.reply_body_key;
|
|
1189
1258
|
const stoppedNow = () => control.signal.aborted || control.disposition() !== undefined;
|
|
@@ -1211,8 +1280,8 @@ limited) {
|
|
|
1211
1280
|
}
|
|
1212
1281
|
early.shift();
|
|
1213
1282
|
earlyPosted += 1;
|
|
1214
|
-
if (posted.withheld
|
|
1215
|
-
|
|
1283
|
+
if (undeliveredFor(posted.withheld))
|
|
1284
|
+
undelivered = true;
|
|
1216
1285
|
log(`${timestamp()} ✓ ${label} posted an answer; a background task is still running`);
|
|
1217
1286
|
}
|
|
1218
1287
|
};
|
|
@@ -1239,15 +1308,19 @@ limited) {
|
|
|
1239
1308
|
const sessionId = venue ? undefined : result.ok ? result.sessionId : undefined;
|
|
1240
1309
|
/**
|
|
1241
1310
|
* Continue the conversation's session from this turn — unless an answer of
|
|
1242
|
-
* it
|
|
1311
|
+
* it will never reach the guest (muted thread, blocked send; any runtime): then the session
|
|
1243
1312
|
* holds a turn nobody saw, so it is forgotten, eager record or resumed one alike.
|
|
1244
1313
|
*/
|
|
1245
1314
|
const settleSession = (id) => {
|
|
1246
|
-
if (
|
|
1315
|
+
if (undelivered)
|
|
1247
1316
|
runtime.sessions.forget(sessionKeyFor(run));
|
|
1248
1317
|
else if (id)
|
|
1249
1318
|
runtime.sessions.remember(sessionKeyFor(run), id);
|
|
1250
1319
|
};
|
|
1320
|
+
// An early answer the guest will never get (muted, blocked): forgotten on every exit, the
|
|
1321
|
+
// stopped, failed and usage-limited ones below included — none may continue a turn nobody saw.
|
|
1322
|
+
if (undelivered)
|
|
1323
|
+
runtime.sessions.forget(sessionKeyFor(run));
|
|
1251
1324
|
// Whatever the turn did, the timeline is whole BEFORE the reply is posted or
|
|
1252
1325
|
// the run is closed — a completion that beats its own last tool row would
|
|
1253
1326
|
// leave a `calling` row on a finished run. Never rejects, and after `stop`
|
|
@@ -1412,10 +1485,11 @@ limited) {
|
|
|
1412
1485
|
}
|
|
1413
1486
|
// Advance continuity only after the answer is durable on its reply surface.
|
|
1414
1487
|
// Remembering at process exit would strand the conversation behind an answer
|
|
1415
|
-
// it never received whenever this POST is rejected. A muted thread
|
|
1416
|
-
// nothing of it, so its session is not advanced either (and an eager record undone)
|
|
1417
|
-
|
|
1418
|
-
|
|
1488
|
+
// it never received whenever this POST is rejected. A muted thread or a blocked send delivers
|
|
1489
|
+
// nothing of it, so its session is not advanced either (and an eager record undone); a
|
|
1490
|
+
// suggestion or a reply waiting for approval may still be sent as written, so it continues.
|
|
1491
|
+
if (undeliveredFor(posted.withheld))
|
|
1492
|
+
undelivered = true;
|
|
1419
1493
|
settleSession(sessionId);
|
|
1420
1494
|
const completed = await completeWithRetry(api, run.completion_path, { ok: true }, log, sleep, runtime.model());
|
|
1421
1495
|
if (!completed.ok) {
|
|
@@ -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
|
|
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,14 @@ 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`): `1` hosts with remote
|
|
30
|
+
* control on, `0` hosts nothing. The service writes it into its home's `settings.json` as it
|
|
31
|
+
* starts, so every reader — the daemon, the relay and the permission hook of a Claude window
|
|
32
|
+
* that outlived a reinstall — reads the one file.
|
|
33
|
+
*/
|
|
34
|
+
export declare const TERMINAL_SESSIONS_ENV = "PROHOST_HOST_TERMINAL_SESSIONS";
|
|
27
35
|
/** `$PROHOST_HOME/code-host`. */
|
|
28
36
|
export declare function codeHostDir(env?: NodeJS.ProcessEnv): string;
|
|
29
37
|
export declare function settingsPath(env?: NodeJS.ProcessEnv): string;
|
|
@@ -50,3 +58,15 @@ export declare function readJsonObject(file: string): Record<string, unknown> |
|
|
|
50
58
|
*/
|
|
51
59
|
export declare function loadSettings(env?: NodeJS.ProcessEnv): CodeHostSettings;
|
|
52
60
|
export declare function saveSettings(settings: CodeHostSettings, env?: NodeJS.ProcessEnv): void;
|
|
61
|
+
/**
|
|
62
|
+
* A dedicated service's terminal switch (`TERMINAL_SESSIONS_ENV`), recorded in the home's
|
|
63
|
+
* `settings.json` as it starts — for the readers outside the service's process: the permission
|
|
64
|
+
* hook of each window, which may have outlived a reinstall. True only when it is on AND recorded.
|
|
65
|
+
* Fails closed: a switch it can't record hosts nothing, and the old file is removed when it can be
|
|
66
|
+
* (a missing or unreadable file is off to every reader). When even that fails — `code-host/`
|
|
67
|
+
* itself unwritable — the error says so: windows still open keep reading the old switches.
|
|
68
|
+
*/
|
|
69
|
+
export declare function recordServiceSwitch(on: boolean, env: NodeJS.ProcessEnv, save?: (settings: CodeHostSettings, env: NodeJS.ProcessEnv) => void): {
|
|
70
|
+
hosting: boolean;
|
|
71
|
+
error?: string;
|
|
72
|
+
};
|
|
@@ -27,6 +27,14 @@ 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`): `1` hosts with remote
|
|
33
|
+
* control on, `0` hosts nothing. The service writes it into its home's `settings.json` as it
|
|
34
|
+
* starts, so every reader — the daemon, the relay and the permission hook of a Claude window
|
|
35
|
+
* that outlived a reinstall — reads the one file.
|
|
36
|
+
*/
|
|
37
|
+
export const TERMINAL_SESSIONS_ENV = 'PROHOST_HOST_TERMINAL_SESSIONS';
|
|
30
38
|
/** `$PROHOST_HOME/code-host`. */
|
|
31
39
|
export function codeHostDir(env = process.env) {
|
|
32
40
|
return path.join(prohostHome(env), 'code-host');
|
|
@@ -112,3 +120,30 @@ export function saveSettings(settings, env = process.env) {
|
|
|
112
120
|
allowRemoteTerminal: settings.enabled && settings.allowRemoteTerminal,
|
|
113
121
|
});
|
|
114
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* A dedicated service's terminal switch (`TERMINAL_SESSIONS_ENV`), recorded in the home's
|
|
125
|
+
* `settings.json` as it starts — for the readers outside the service's process: the permission
|
|
126
|
+
* hook of each window, which may have outlived a reinstall. True only when it is on AND recorded.
|
|
127
|
+
* Fails closed: a switch it can't record hosts nothing, and the old file is removed when it can be
|
|
128
|
+
* (a missing or unreadable file is off to every reader). When even that fails — `code-host/`
|
|
129
|
+
* itself unwritable — the error says so: windows still open keep reading the old switches.
|
|
130
|
+
*/
|
|
131
|
+
export function recordServiceSwitch(on, env, save = saveSettings) {
|
|
132
|
+
try {
|
|
133
|
+
save({ version: 1, enabled: on, allowRemoteTerminal: on }, env);
|
|
134
|
+
return { hosting: on && loadSettings(env).allowRemoteTerminal };
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
138
|
+
try {
|
|
139
|
+
rmSync(settingsPath(env), { force: true });
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return {
|
|
143
|
+
hosting: false,
|
|
144
|
+
error: `${error}; the old switches could not be removed either, so windows still open may wait for remote approvals until ${settingsPath(env)} is writable`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return { hosting: false, error };
|
|
148
|
+
}
|
|
149
|
+
}
|
package/dist/host/command.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/host/command.js
CHANGED
|
@@ -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
|
-
|
|
101
|
-
|
|
102
|
-
//
|
|
103
|
-
const
|
|
104
|
-
?
|
|
105
|
-
:
|
|
106
|
-
const
|
|
107
|
-
|
|
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 =
|
|
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:
|
|
119
|
-
computer:
|
|
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
|
|
135
|
-
pid:
|
|
136
|
-
adopted: loadAdopted(env)?.homes.length ?? 0,
|
|
137
|
-
dedicated:
|
|
138
|
-
...(
|
|
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
|
|
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
|
-
|
|
520
|
-
//
|
|
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
|
-
|
|
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(`
|
|
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
|
-
?
|
|
532
|
-
:
|
|
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': {
|
package/dist/host/install.d.ts
CHANGED
|
@@ -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;
|
package/dist/host/install.js
CHANGED
|
@@ -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
|
|
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
|
: {}),
|
package/dist/host/service.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { executeHostedRun } from '../agent/run.js';
|
|
|
30
30
|
import type { AgentRunOptions } from '../agent/run.js';
|
|
31
31
|
import type { CodeHostLike } from '../codehost/host.js';
|
|
32
32
|
import type { HostStats } from '../codehost/host_stats.js';
|
|
33
|
+
import { recordServiceSwitch } from '../codehost/settings.js';
|
|
33
34
|
import type { Workload } from './capacity.js';
|
|
34
35
|
import type { ComputerCredentials } from './computer.js';
|
|
35
36
|
import type { LoginInfo } from './logins.js';
|
|
@@ -92,6 +93,8 @@ export interface HostServiceOptions {
|
|
|
92
93
|
peak?: PeakMemory;
|
|
93
94
|
detectLogins?: () => Promise<LoginInfo[]>;
|
|
94
95
|
codeHost?: CodeHostLike | null;
|
|
96
|
+
/** Records a dedicated service's terminal switch in the home (tests: one that fails). */
|
|
97
|
+
recordTerminalSwitch?: typeof recordServiceSwitch;
|
|
95
98
|
executeRun?: typeof executeHostedRun;
|
|
96
99
|
updateAvailable?: () => boolean;
|
|
97
100
|
statusIntervalMs?: number;
|
package/dist/host/service.js
CHANGED
|
@@ -40,7 +40,7 @@ import { SessionStore, sessionKeyFor } from '../agent/sessionstore.js';
|
|
|
40
40
|
import { verifySignature } from '../agent/signature.js';
|
|
41
41
|
import { CODE_THREAD_FOLDERS_CAPABILITY, CODE_THREAD_SKIP_PERMISSIONS_CAPABILITY, CODE_THREADS_CAPABILITY, CodeHost, EVENT_CODE_THREAD_APPROVAL_DECIDED, EVENT_CODE_THREAD_INPUT_REQUESTED, EVENT_CODE_THREAD_START_REQUESTED, REMOTE_TERMINAL_CAPABILITY, RefusedDeliveryError, } from '../codehost/host.js';
|
|
42
42
|
import { HostStatsSampler } from '../codehost/host_stats.js';
|
|
43
|
-
import { loadSettings as loadCodeHostSettings } from '../codehost/settings.js';
|
|
43
|
+
import { TERMINAL_SESSIONS_ENV, loadSettings as loadCodeHostSettings, recordServiceSwitch } from '../codehost/settings.js';
|
|
44
44
|
import { isWebhookEvent } from '../forward.js';
|
|
45
45
|
import { ServerLiveness, enableTcpKeepAlive, terminateSocket } from '../liveness.js';
|
|
46
46
|
import { CLI_VERSION, USER_AGENT } from '../version.js';
|
|
@@ -185,11 +185,21 @@ 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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
188
|
+
// A dedicated install decides terminal sessions in the service's own definition
|
|
189
|
+
// (`--remote-terminal` / `--no-remote-terminal`): written into the home's switches as it starts,
|
|
190
|
+
// the one file the daemon, the relay and every window's permission hook read (a window that
|
|
191
|
+
// outlived a reinstall too). A personal service follows the home's switches as the owner set them.
|
|
192
|
+
// The definition is authoritative in this process: off never hosts, whatever the file says or
|
|
193
|
+
// whether it could be written; on hosts only once recorded (`recordServiceSwitch` fails closed).
|
|
194
|
+
const decided = env[TERMINAL_SESSIONS_ENV] === '1' ? true : env[TERMINAL_SESSIONS_ENV] === '0' ? false : undefined;
|
|
195
|
+
let hosting = decided ?? loadCodeHostSettings(codeHostEnv).enabled;
|
|
196
|
+
if (decided !== undefined) {
|
|
197
|
+
const recorded = (options.recordTerminalSwitch ?? recordServiceSwitch)(decided, codeHostEnv);
|
|
198
|
+
hosting = decided && recorded.hosting;
|
|
199
|
+
if (recorded.error)
|
|
200
|
+
log(`terminal sessions: off — could not record the service's switch (${recorded.error})`);
|
|
201
|
+
}
|
|
202
|
+
const codeHost = options.codeHost !== undefined ? (options.codeHost ?? undefined) : hosting ? buildCodeHost() : undefined;
|
|
193
203
|
function buildCodeHost() {
|
|
194
204
|
try {
|
|
195
205
|
return new CodeHost({
|
|
@@ -367,6 +377,7 @@ async function serveHost(options) {
|
|
|
367
377
|
online,
|
|
368
378
|
paused: localPaused || config.paused,
|
|
369
379
|
draining,
|
|
380
|
+
remote_terminal: capabilities().includes(REMOTE_TERMINAL_CAPABILITY),
|
|
370
381
|
slots: slotsFor(capacity(), active.size + terminals(), held),
|
|
371
382
|
capacity: capacity(),
|
|
372
383
|
recommended_capacity: config.recommended_capacity ?? recommended(),
|
package/dist/host/status.d.ts
CHANGED
|
@@ -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.
|
|
10
|
+
export declare const CLI_VERSION = "0.16.3";
|
|
11
11
|
/** Sent on every HTTP request the CLI makes back into ProhostAI. */
|
|
12
|
-
export declare const USER_AGENT = "prohost-cli/0.16.
|
|
12
|
+
export declare const USER_AGENT = "prohost-cli/0.16.3";
|
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.
|
|
10
|
+
export const CLI_VERSION = '0.16.3';
|
|
11
11
|
/** Sent on every HTTP request the CLI makes back into ProhostAI. */
|
|
12
12
|
export const USER_AGENT = `prohost-cli/${CLI_VERSION}`;
|