@prohost/cli 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +265 -1
  3. package/dist/agent/accounts.d.ts +11 -0
  4. package/dist/agent/accounts.js +12 -1
  5. package/dist/agent/api.d.ts +6 -0
  6. package/dist/agent/api.js +5 -0
  7. package/dist/agent/claude.d.ts +6 -0
  8. package/dist/agent/claude.js +2 -2
  9. package/dist/agent/daemon.d.ts +33 -0
  10. package/dist/agent/daemon.js +56 -0
  11. package/dist/agent/prompt.d.ts +12 -0
  12. package/dist/agent/prompt.js +5 -0
  13. package/dist/agent/run.d.ts +110 -1
  14. package/dist/agent/run.js +198 -22
  15. package/dist/codehost/history.d.ts +80 -0
  16. package/dist/codehost/history.js +98 -0
  17. package/dist/codehost/hooks.d.ts +10 -2
  18. package/dist/codehost/hooks.js +15 -4
  19. package/dist/codehost/host.d.ts +32 -0
  20. package/dist/codehost/host.js +26 -1
  21. package/dist/codehost/host_stats.d.ts +49 -2
  22. package/dist/codehost/host_stats.js +153 -2
  23. package/dist/codehost/relay.d.ts +16 -1
  24. package/dist/codehost/relay.js +69 -3
  25. package/dist/codehost/sizing.d.ts +22 -4
  26. package/dist/codehost/sizing.js +46 -6
  27. package/dist/codehost/terminals.d.ts +33 -2
  28. package/dist/codehost/terminals.js +92 -30
  29. package/dist/codehost/tmux.d.ts +6 -1
  30. package/dist/codehost/tmux.js +3 -0
  31. package/dist/host/api.d.ts +82 -0
  32. package/dist/host/api.js +122 -0
  33. package/dist/host/capacity.d.ts +41 -0
  34. package/dist/host/capacity.js +48 -0
  35. package/dist/host/command.d.ts +59 -0
  36. package/dist/host/command.js +584 -0
  37. package/dist/host/computer.d.ts +120 -0
  38. package/dist/host/computer.js +291 -0
  39. package/dist/host/events.d.ts +24 -0
  40. package/dist/host/events.js +47 -0
  41. package/dist/host/install.d.ts +152 -0
  42. package/dist/host/install.js +755 -0
  43. package/dist/host/ledger.d.ts +52 -0
  44. package/dist/host/ledger.js +131 -0
  45. package/dist/host/legacy.d.ts +38 -0
  46. package/dist/host/legacy.js +115 -0
  47. package/dist/host/logins.d.ts +109 -0
  48. package/dist/host/logins.js +190 -0
  49. package/dist/host/migrate.d.ts +165 -0
  50. package/dist/host/migrate.js +364 -0
  51. package/dist/host/pair.d.ts +62 -0
  52. package/dist/host/pair.js +219 -0
  53. package/dist/host/peak_memory.d.ts +44 -0
  54. package/dist/host/peak_memory.js +139 -0
  55. package/dist/host/scheduler.d.ts +86 -0
  56. package/dist/host/scheduler.js +186 -0
  57. package/dist/host/service.d.ts +114 -0
  58. package/dist/host/service.js +1027 -0
  59. package/dist/host/session_move.d.ts +48 -0
  60. package/dist/host/session_move.js +106 -0
  61. package/dist/host/skills.d.ts +29 -0
  62. package/dist/host/skills.js +135 -0
  63. package/dist/host/status.d.ts +91 -0
  64. package/dist/host/status.js +130 -0
  65. package/dist/index.d.ts +4 -0
  66. package/dist/index.js +47 -3
  67. package/dist/version.d.ts +2 -2
  68. package/dist/version.js +1 -1
  69. package/package.json +1 -1
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The public API `prohost host` speaks: `/v1/computers/*`.
3
+ *
4
+ * Pairing is unauthenticated (the device code or redeem token is the proof);
5
+ * everything else carries the computer key as `X-API-Key`, the header every
6
+ * public-API call from this CLI uses. A run's own token replaces it for that
7
+ * run's heartbeat, events and completion (see `service.ts`).
8
+ */
9
+ export interface ComputerApiOptions {
10
+ baseUrl: string;
11
+ apiKey?: string;
12
+ fetchImpl?: typeof fetch;
13
+ timeoutMs?: number;
14
+ }
15
+ export interface JsonResponse {
16
+ /** HTTP status, or `0` when no answer arrived. */
17
+ status: number;
18
+ /** The parsed body, when it was a JSON object. */
19
+ body?: Record<string, unknown>;
20
+ /** Why no answer arrived, or a short excerpt of a refusal. */
21
+ error?: string;
22
+ }
23
+ export declare function callJson(options: ComputerApiOptions, path: string, body: unknown, method?: 'POST' | 'GET' | 'DELETE'): Promise<JsonResponse>;
24
+ export declare const PAIRING_START_PATH = "/v1/computers/pairing/start";
25
+ export declare const PAIRING_POLL_PATH = "/v1/computers/pairing/poll";
26
+ export declare const PAIRING_REDEEM_PATH = "/v1/computers/pairing/redeem";
27
+ export declare const PAIRING_ADOPT_PATH = "/v1/computers/pairing/adopt";
28
+ export declare function computerPath(computerId: string): string;
29
+ export type RevokeResult = 'revoked' | 'already_gone' | 'failed';
30
+ /**
31
+ * Remove this computer from its workspace, as the computer (`DELETE
32
+ * /v1/computers/{id}`): its key stops working. A key the server no longer
33
+ * knows (401) or a computer already removed (404 / 410) is already done.
34
+ */
35
+ export declare function revokeComputer(options: ComputerApiOptions, computerId: string): Promise<{
36
+ result: RevokeResult;
37
+ status: number;
38
+ error?: string;
39
+ }>;
40
+ export declare function statusPath(computerId: string): string;
41
+ export declare function eventsPath(computerId: string): string;
42
+ export declare function claimPath(computerId: string, runId: string): string;
43
+ /** One skill file the claim hands over, written into the run's workspace. */
44
+ export interface SkillFile {
45
+ path: string;
46
+ content: string;
47
+ }
48
+ export interface ClaimedSkill {
49
+ key: string;
50
+ version?: string | number;
51
+ files: SkillFile[];
52
+ }
53
+ export type ClaimResult = {
54
+ kind: 'claimed';
55
+ runToken: string;
56
+ expiresAt?: string;
57
+ payload: Record<string, unknown>;
58
+ skills: ClaimedSkill[];
59
+ }
60
+ /** Another computer has it (409): skip it, never run it. */
61
+ | {
62
+ kind: 'elsewhere';
63
+ }
64
+ /** Gone server-side (404 / 410): nothing to run. */
65
+ | {
66
+ kind: 'gone';
67
+ status: number;
68
+ }
69
+ /** No answer, or one worth asking again (429 / 5xx). */
70
+ | {
71
+ kind: 'retry';
72
+ status: number;
73
+ error?: string;
74
+ }
75
+ /** Any other refusal. */
76
+ | {
77
+ kind: 'refused';
78
+ status: number;
79
+ error?: string;
80
+ };
81
+ /** Claim a run for this computer. Exactly one computer ever gets a 200. */
82
+ export declare function claimRun(options: ComputerApiOptions, computerId: string, runId: string): Promise<ClaimResult>;
@@ -0,0 +1,122 @@
1
+ /**
2
+ * The public API `prohost host` speaks: `/v1/computers/*`.
3
+ *
4
+ * Pairing is unauthenticated (the device code or redeem token is the proof);
5
+ * everything else carries the computer key as `X-API-Key`, the header every
6
+ * public-API call from this CLI uses. A run's own token replaces it for that
7
+ * run's heartbeat, events and completion (see `service.ts`).
8
+ */
9
+ import { USER_AGENT } from '../version.js';
10
+ const DEFAULT_TIMEOUT_MS = 30_000;
11
+ export async function callJson(options, path, body, method = 'POST') {
12
+ const fetchImpl = options.fetchImpl ?? fetch;
13
+ const controller = new AbortController();
14
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
15
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
16
+ try {
17
+ const response = await fetchImpl(`${options.baseUrl.replace(/\/+$/, '')}${path}`, {
18
+ method,
19
+ redirect: 'error',
20
+ headers: {
21
+ 'Content-Type': 'application/json',
22
+ 'User-Agent': USER_AGENT,
23
+ ...(options.apiKey ? { 'X-API-Key': options.apiKey } : {}),
24
+ },
25
+ ...(method === 'POST' ? { body: JSON.stringify(body ?? {}) } : {}),
26
+ signal: controller.signal,
27
+ });
28
+ const text = await response.text().catch(() => '');
29
+ let parsed;
30
+ try {
31
+ const value = text ? JSON.parse(text) : undefined;
32
+ if (value && typeof value === 'object' && !Array.isArray(value))
33
+ parsed = value;
34
+ }
35
+ catch {
36
+ parsed = undefined;
37
+ }
38
+ const ok = response.status >= 200 && response.status < 300;
39
+ return { status: response.status, body: parsed, ...(ok ? {} : { error: text.slice(0, 300) }) };
40
+ }
41
+ catch (err) {
42
+ if (controller.signal.aborted)
43
+ return { status: 0, error: `timed out after ${Math.round(timeoutMs / 1000)}s` };
44
+ return { status: 0, error: err instanceof Error ? err.message : String(err) };
45
+ }
46
+ finally {
47
+ clearTimeout(timer);
48
+ }
49
+ }
50
+ export const PAIRING_START_PATH = '/v1/computers/pairing/start';
51
+ export const PAIRING_POLL_PATH = '/v1/computers/pairing/poll';
52
+ export const PAIRING_REDEEM_PATH = '/v1/computers/pairing/redeem';
53
+ export const PAIRING_ADOPT_PATH = '/v1/computers/pairing/adopt';
54
+ export function computerPath(computerId) {
55
+ return `/v1/computers/${encodeURIComponent(computerId)}`;
56
+ }
57
+ /**
58
+ * Remove this computer from its workspace, as the computer (`DELETE
59
+ * /v1/computers/{id}`): its key stops working. A key the server no longer
60
+ * knows (401) or a computer already removed (404 / 410) is already done.
61
+ */
62
+ export async function revokeComputer(options, computerId) {
63
+ const response = await callJson(options, computerPath(computerId), undefined, 'DELETE');
64
+ if (response.status >= 200 && response.status < 300)
65
+ return { result: 'revoked', status: response.status };
66
+ if ([401, 404, 410].includes(response.status))
67
+ return { result: 'already_gone', status: response.status };
68
+ return { result: 'failed', status: response.status, ...(response.error ? { error: response.error } : {}) };
69
+ }
70
+ export function statusPath(computerId) {
71
+ return `/v1/computers/${encodeURIComponent(computerId)}/status`;
72
+ }
73
+ export function eventsPath(computerId) {
74
+ return `/v1/computers/${encodeURIComponent(computerId)}/events`;
75
+ }
76
+ export function claimPath(computerId, runId) {
77
+ return `/v1/computers/${encodeURIComponent(computerId)}/runs/${encodeURIComponent(runId)}/claim`;
78
+ }
79
+ function parseSkills(value) {
80
+ if (!Array.isArray(value))
81
+ return [];
82
+ return value.flatMap((raw) => {
83
+ const skill = raw;
84
+ if (!skill || typeof skill.key !== 'string' || !Array.isArray(skill.files))
85
+ return [];
86
+ const files = skill.files.flatMap((f) => {
87
+ const file = f;
88
+ return file && typeof file.path === 'string' && typeof file.content === 'string'
89
+ ? [{ path: file.path, content: file.content }]
90
+ : [];
91
+ });
92
+ const version = typeof skill.version === 'string' || typeof skill.version === 'number' ? skill.version : undefined;
93
+ return [{ key: skill.key, ...(version !== undefined ? { version } : {}), files }];
94
+ });
95
+ }
96
+ /** Claim a run for this computer. Exactly one computer ever gets a 200. */
97
+ export async function claimRun(options, computerId, runId) {
98
+ const response = await callJson(options, claimPath(computerId, runId), {});
99
+ if (response.status === 200 || response.status === 201) {
100
+ const body = response.body ?? {};
101
+ const token = body.run_token;
102
+ const payload = body.payload;
103
+ if (typeof token !== 'string' || !token || !payload || typeof payload !== 'object' || Array.isArray(payload)) {
104
+ return { kind: 'refused', status: response.status, error: 'claim answer carried no run token or payload' };
105
+ }
106
+ return {
107
+ kind: 'claimed',
108
+ runToken: token,
109
+ ...(typeof body.expires_at === 'string' ? { expiresAt: body.expires_at } : {}),
110
+ payload: payload,
111
+ skills: parseSkills(body.skills),
112
+ };
113
+ }
114
+ if (response.status === 409)
115
+ return { kind: 'elsewhere' };
116
+ if (response.status === 404 || response.status === 410)
117
+ return { kind: 'gone', status: response.status };
118
+ if (response.status === 0 || response.status === 429 || response.status >= 500) {
119
+ return { kind: 'retry', status: response.status, error: response.error };
120
+ }
121
+ return { kind: 'refused', status: response.status, error: response.error };
122
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * How many things this computer works on at once, and when it takes no new work.
3
+ */
4
+ import type { MemoryPressure } from '../codehost/host_stats.js';
5
+ export type Workload = 'coding' | 'chat';
6
+ /** Memory one run takes, by what the employees here mostly do. */
7
+ export declare const RUN_MEMORY_GB: Record<Workload, number>;
8
+ /** Never recommend more than this, however big the machine. */
9
+ export declare const MAX_RECOMMENDED_CAPACITY = 16;
10
+ /** Memory kept for the person using the computer: the larger of these. */
11
+ export declare const RESERVE_MIN_GB = 8;
12
+ export declare const RESERVE_SHARE = 0.25;
13
+ /**
14
+ * The recommended capacity: memory after the reserve, divided by what one run
15
+ * takes; at most half the cores (a run is a CLI plus the tools it starts); at
16
+ * least 1, at most {@link MAX_RECOMMENDED_CAPACITY}.
17
+ */
18
+ export declare function recommendCapacity(hardware: {
19
+ memoryGb: number;
20
+ cores: number;
21
+ workload?: Workload;
22
+ }): number;
23
+ /** What the server's `capacity` (null = use recommended) and the local recommendation come to. */
24
+ export declare function effectiveCapacity(configured: number | null | undefined, recommended: number): number;
25
+ export type HeldReason = 'paused' | 'pressure' | 'battery';
26
+ export interface HoldInputs {
27
+ paused: boolean;
28
+ holdOnPressure: boolean;
29
+ holdOnBattery: boolean;
30
+ memoryPressure: MemoryPressure | null;
31
+ onBattery: boolean | null;
32
+ }
33
+ /**
34
+ * Why new work waits, or `null`. Work already running is never stopped by a
35
+ * hold; it only keeps the next run from starting.
36
+ *
37
+ * Pressure holds only at `critical`: `elevated` is routine on a busy Mac
38
+ * (the compressor is working), and holding on it would idle a computer that is
39
+ * coping. A battery hold needs the OS to say it is on battery — unknown is not.
40
+ */
41
+ export declare function holdReason(inputs: HoldInputs): HeldReason | null;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * How many things this computer works on at once, and when it takes no new work.
3
+ */
4
+ /** Memory one run takes, by what the employees here mostly do. */
5
+ export const RUN_MEMORY_GB = { coding: 4, chat: 1.5 };
6
+ /** Never recommend more than this, however big the machine. */
7
+ export const MAX_RECOMMENDED_CAPACITY = 16;
8
+ /** Memory kept for the person using the computer: the larger of these. */
9
+ export const RESERVE_MIN_GB = 8;
10
+ export const RESERVE_SHARE = 0.25;
11
+ /**
12
+ * The recommended capacity: memory after the reserve, divided by what one run
13
+ * takes; at most half the cores (a run is a CLI plus the tools it starts); at
14
+ * least 1, at most {@link MAX_RECOMMENDED_CAPACITY}.
15
+ */
16
+ export function recommendCapacity(hardware) {
17
+ const memory = Number.isFinite(hardware.memoryGb) ? Math.max(0, hardware.memoryGb) : 0;
18
+ const cores = Number.isFinite(hardware.cores) ? Math.max(1, Math.floor(hardware.cores)) : 1;
19
+ const perRun = RUN_MEMORY_GB[hardware.workload ?? 'chat'];
20
+ const reserve = Math.max(RESERVE_MIN_GB, memory * RESERVE_SHARE);
21
+ const byMemory = Math.floor((memory - reserve) / perRun);
22
+ const byCores = Math.floor(cores / 2);
23
+ return Math.max(1, Math.min(MAX_RECOMMENDED_CAPACITY, byMemory, byCores));
24
+ }
25
+ /** What the server's `capacity` (null = use recommended) and the local recommendation come to. */
26
+ export function effectiveCapacity(configured, recommended) {
27
+ if (typeof configured === 'number' && Number.isInteger(configured) && configured >= 1) {
28
+ return Math.min(configured, 64);
29
+ }
30
+ return recommended;
31
+ }
32
+ /**
33
+ * Why new work waits, or `null`. Work already running is never stopped by a
34
+ * hold; it only keeps the next run from starting.
35
+ *
36
+ * Pressure holds only at `critical`: `elevated` is routine on a busy Mac
37
+ * (the compressor is working), and holding on it would idle a computer that is
38
+ * coping. A battery hold needs the OS to say it is on battery — unknown is not.
39
+ */
40
+ export function holdReason(inputs) {
41
+ if (inputs.paused)
42
+ return 'paused';
43
+ if (inputs.holdOnPressure && inputs.memoryPressure === 'critical')
44
+ return 'pressure';
45
+ if (inputs.holdOnBattery && inputs.onBattery === true)
46
+ return 'battery';
47
+ return null;
48
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * `prohost host …` — one background service per computer, serving every AI
3
+ * employee placed on it, plus the terminal sessions opened on it.
4
+ *
5
+ * prohost host pair [--name <n>] [--token <redeem_token>|-] [--expect-account <id>]
6
+ * prohost host unpair
7
+ * prohost host run
8
+ * prohost host install [--label <id>] [--dedicated --service-user <name> [--service-home <dir>]
9
+ * [--claude-token-file <path>]] | uninstall [--label <id>]
10
+ * prohost host status [--json] [--label <id>] | logins [--json] | logins add --runtime claude_code|codex
11
+ * prohost host pause | resume
12
+ * prohost host migrate [--from <home>]… [--home <dir,…>] [--keep-services] [--dry-run] [--json]
13
+ */
14
+ import type { InstallIo } from './install.js';
15
+ import type { LoginInfo } from './logins.js';
16
+ import { runMigrate } from './migrate.js';
17
+ type Flags = Record<string, string | boolean>;
18
+ export interface HostIo {
19
+ env?: NodeJS.ProcessEnv;
20
+ out?: (line: string) => void;
21
+ err?: (line: string) => void;
22
+ stdin?: AsyncIterable<string | Buffer>;
23
+ now?: () => number;
24
+ fetchImpl?: typeof fetch;
25
+ detectLogins?: () => Promise<LoginInfo[]>;
26
+ platform?: NodeJS.Platform;
27
+ /** Seams for `install` / `uninstall` (service manager calls, files). */
28
+ serviceIo?: InstallIo;
29
+ /** Seams for `migrate`. */
30
+ migrate?: Partial<Parameters<typeof runMigrate>[0]>;
31
+ /** Positionals after the subcommand (`logins add`). */
32
+ args?: string[];
33
+ /** Every value of a repeated flag (`--from a --from b`). */
34
+ repeated?: Record<string, string[]>;
35
+ /** Is this an interactive terminal (asks before adding a computer to a workspace)? */
36
+ interactive?: boolean;
37
+ /** Answers a yes/no question on the terminal. */
38
+ ask?: (question: string) => Promise<boolean>;
39
+ /** Runs a sign-in in this terminal (`logins add`); resolves its exit code. */
40
+ signIn?: (command: string, args: string[], env: NodeJS.ProcessEnv) => Promise<number | null>;
41
+ /** Whether the CLI runs as Electron-as-Node (the desktop app's bundled CLI). */
42
+ electron?: boolean;
43
+ /** Where dedicated services publish their state (tests). */
44
+ sharedBase?: string;
45
+ }
46
+ export declare function hardwareFacts(): Record<string, unknown>;
47
+ /**
48
+ * `host status --json`: what the desktop app and scripts read. Always an
49
+ * answer — `{paired: false, …}` before pairing. When this home runs no
50
+ * service but a dedicated one runs on the computer, that one is reported
51
+ * (`service.dedicated`), from the state it publishes for the logged-in user.
52
+ */
53
+ export declare function statusJson(env: NodeJS.ProcessEnv, now?: number, options?: {
54
+ sharedBase?: string;
55
+ label?: string;
56
+ platform?: NodeJS.Platform;
57
+ }): Record<string, unknown>;
58
+ export declare function runHost(subcommand: string | undefined, flags: Flags, printHelp: () => void, io?: HostIo): Promise<number>;
59
+ export {};