@prohost/cli 0.13.2 → 0.14.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 (61) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/README.md +129 -0
  3. package/dist/agent/claude.d.ts +28 -0
  4. package/dist/agent/claude.js +83 -4
  5. package/dist/agent/command.d.ts +8 -0
  6. package/dist/agent/command.js +40 -7
  7. package/dist/agent/daemon.d.ts +3 -1
  8. package/dist/agent/daemon.js +1 -2
  9. package/dist/agent/run.d.ts +30 -1
  10. package/dist/agent/run.js +311 -16
  11. package/dist/agent/runtime_status.d.ts +99 -3
  12. package/dist/agent/runtime_status.js +173 -5
  13. package/dist/agent/worktrees.d.ts +2 -0
  14. package/dist/agent/worktrees.js +3 -2
  15. package/dist/codehost/activity.d.ts +64 -0
  16. package/dist/codehost/activity.js +135 -0
  17. package/dist/codehost/agents.d.ts +31 -0
  18. package/dist/codehost/agents.js +57 -0
  19. package/dist/codehost/checkouts.d.ts +130 -0
  20. package/dist/codehost/checkouts.js +270 -0
  21. package/dist/codehost/claude_trust.d.ts +40 -0
  22. package/dist/codehost/claude_trust.js +129 -0
  23. package/dist/codehost/command.d.ts +25 -0
  24. package/dist/codehost/command.js +136 -0
  25. package/dist/codehost/decision.d.ts +156 -0
  26. package/dist/codehost/decision.js +553 -0
  27. package/dist/codehost/events.d.ts +49 -0
  28. package/dist/codehost/events.js +87 -0
  29. package/dist/codehost/folders.d.ts +122 -0
  30. package/dist/codehost/folders.js +317 -0
  31. package/dist/codehost/hooks.d.ts +178 -0
  32. package/dist/codehost/hooks.js +726 -0
  33. package/dist/codehost/host.d.ts +290 -0
  34. package/dist/codehost/host.js +625 -0
  35. package/dist/codehost/host_stats.d.ts +71 -0
  36. package/dist/codehost/host_stats.js +166 -0
  37. package/dist/codehost/invocations.d.ts +12 -0
  38. package/dist/codehost/invocations.js +82 -0
  39. package/dist/codehost/ledger.d.ts +35 -0
  40. package/dist/codehost/ledger.js +91 -0
  41. package/dist/codehost/local_server.d.ts +106 -0
  42. package/dist/codehost/local_server.js +339 -0
  43. package/dist/codehost/pty.d.ts +38 -0
  44. package/dist/codehost/pty.js +47 -0
  45. package/dist/codehost/ready.d.ts +31 -0
  46. package/dist/codehost/ready.js +109 -0
  47. package/dist/codehost/relay.d.ts +170 -0
  48. package/dist/codehost/relay.js +548 -0
  49. package/dist/codehost/settings.d.ts +52 -0
  50. package/dist/codehost/settings.js +114 -0
  51. package/dist/codehost/sizing.d.ts +163 -0
  52. package/dist/codehost/sizing.js +394 -0
  53. package/dist/codehost/terminals.d.ts +390 -0
  54. package/dist/codehost/terminals.js +965 -0
  55. package/dist/codehost/tmux.d.ts +196 -0
  56. package/dist/codehost/tmux.js +391 -0
  57. package/dist/index.d.ts +5 -0
  58. package/dist/index.js +21 -0
  59. package/dist/version.d.ts +2 -2
  60. package/dist/version.js +1 -1
  61. package/package.json +4 -1
@@ -0,0 +1,136 @@
1
+ /**
2
+ * `prohost agent code-host …` — the machine owner's switches for hosting Code
3
+ * threads on this Mac. Only someone with a shell on this machine can run it;
4
+ * nothing the server sends can change them.
5
+ *
6
+ * prohost agent code-host status
7
+ * prohost agent code-host enable [--allow-remote-terminal]
8
+ * prohost agent code-host remote on|off
9
+ * prohost agent code-host disable
10
+ * prohost agent code-host folders add <path> | list [--json] | remove <path>
11
+ *
12
+ * A running `agent run` reads the switches at start, and the remote switch
13
+ * again on every attach: `remote off` refuses the next viewer at once; `enable`
14
+ * and `disable` take effect when the agent restarts.
15
+ *
16
+ * `folders` is the list of folders this Mac has opened for Code threads
17
+ * (`folders.ts`): a thread started from a phone or the web may only run in one
18
+ * of them, or in a folder inside one. The desktop app adds each folder picked
19
+ * in its folder picker; a running agent reads the list at every start.
20
+ */
21
+ import { addFolder, FoldersBusyError, foldersPath, loadFolders, reportFolders, removeFolder } from './folders.js';
22
+ import { loadSettings, saveSettings, settingsPath } from './settings.js';
23
+ function describe(settings) {
24
+ return [
25
+ `Code-thread hosting: ${settings.enabled ? 'on' : 'off'}`,
26
+ `Remote terminal: ${settings.allowRemoteTerminal ? 'ALLOWED — the machine owner may attach and type from ProhostAI' : 'off'}`,
27
+ `Settings file: ${settingsPath()}`,
28
+ ].join('\n');
29
+ }
30
+ const REMOTE_WARNING = 'A remote terminal lets you (this machine\'s owner, signed in to ProhostAI) type into this ' +
31
+ "Mac's agent terminals from the web, phone or desktop app — that is running commands on this " +
32
+ 'machine. Nobody else can attach; turn it off with `prohost agent code-host remote off`.';
33
+ export function codeHostCommand(args, flags, out = (line) => process.stdout.write(`${line}\n`), err = (line) => process.stderr.write(`${line}\n`)) {
34
+ const [action, value] = args;
35
+ if (action === 'folders')
36
+ return foldersCommand(args.slice(1), flags, out, err);
37
+ const current = loadSettings();
38
+ if (!action || action === 'status') {
39
+ out(describe(current));
40
+ return 0;
41
+ }
42
+ if (action === 'enable') {
43
+ const next = {
44
+ version: 1,
45
+ enabled: true,
46
+ allowRemoteTerminal: flags['allow-remote-terminal'] === true || current.allowRemoteTerminal,
47
+ };
48
+ saveSettings(next);
49
+ out(describe(next));
50
+ if (next.allowRemoteTerminal)
51
+ out(`\n${REMOTE_WARNING}`);
52
+ out('\nRestart `prohost agent run` (or the daemon) to start hosting.');
53
+ return 0;
54
+ }
55
+ if (action === 'disable') {
56
+ saveSettings({ version: 1, enabled: false, allowRemoteTerminal: false });
57
+ out(describe(loadSettings()));
58
+ out('\nRunning terminals keep running in tmux; restart the agent to stop hosting.');
59
+ return 0;
60
+ }
61
+ if (action === 'remote') {
62
+ if (value !== 'on' && value !== 'off') {
63
+ err('Usage: prohost agent code-host remote on|off');
64
+ return 1;
65
+ }
66
+ if (value === 'on' && !current.enabled) {
67
+ err('Turn hosting on first: prohost agent code-host enable');
68
+ return 1;
69
+ }
70
+ const next = { ...current, allowRemoteTerminal: value === 'on' };
71
+ saveSettings(next);
72
+ out(describe(next));
73
+ if (next.allowRemoteTerminal)
74
+ out(`\n${REMOTE_WARNING}`);
75
+ return 0;
76
+ }
77
+ err(`Unknown code-host action: ${action}. Expected status, enable, disable, remote on|off, or folders.`);
78
+ return 1;
79
+ }
80
+ const FOLDERS_USAGE = 'Usage: prohost agent code-host folders add <path> | list [--json] | remove <path>';
81
+ /** `prohost agent code-host folders …`. Prints paths only; nothing in the list is secret. */
82
+ export function foldersCommand(args, flags, out, err) {
83
+ const [action, target, extra] = args;
84
+ if (action === 'add' || action === 'remove') {
85
+ if (!target || extra !== undefined) {
86
+ err(FOLDERS_USAGE);
87
+ return 1;
88
+ }
89
+ if (action === 'add') {
90
+ const added = addFolder(target);
91
+ if (!added.ok) {
92
+ err(added.reason);
93
+ return 1;
94
+ }
95
+ out(added.path);
96
+ return 0;
97
+ }
98
+ let removed;
99
+ try {
100
+ removed = removeFolder(target);
101
+ }
102
+ catch (error) {
103
+ if (!(error instanceof FoldersBusyError))
104
+ throw error;
105
+ err(error.message);
106
+ return 1;
107
+ }
108
+ if (!removed) {
109
+ err(`${target} is not in this Mac's opened folders.`);
110
+ return 1;
111
+ }
112
+ out(`Removed ${removed}`);
113
+ return 0;
114
+ }
115
+ if (action === undefined || action === 'list') {
116
+ if (target !== undefined) {
117
+ err(FOLDERS_USAGE);
118
+ return 1;
119
+ }
120
+ if (flags.json === true) {
121
+ const git = new Map(reportFolders().map((folder) => [folder.path, folder.git]));
122
+ const folders = loadFolders().map(({ path, openedAt }) => ({ path, openedAt, git: git.get(path) ?? false, exists: git.has(path) }));
123
+ out(JSON.stringify({ folders }));
124
+ return 0;
125
+ }
126
+ const folders = loadFolders();
127
+ if (folders.length === 0)
128
+ out('No folders opened for Code threads yet.');
129
+ for (const folder of folders)
130
+ out(folder.path);
131
+ out(`\nList file: ${foldersPath()}`);
132
+ return 0;
133
+ }
134
+ err(FOLDERS_USAGE);
135
+ return 1;
136
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * A decision made in ProhostAI, answered by the agent's own `PermissionRequest`
3
+ * hook rather than by keys pressed into its pane.
4
+ *
5
+ * Claude Code draws its permission menu while that hook is still running, and
6
+ * applies whichever comes first: the hook's printed decision or an answer in
7
+ * the terminal (checked against 2.1.314). So the hook waits, bounded, for a
8
+ * decision for its own prompt, and a decision can never land on another one.
9
+ *
10
+ * Two files per prompt, in the private state dir, named by its `approvalId`:
11
+ *
12
+ * - `<terminal>.waiter-<id>.json`: the hook waiting on it (`pid`, `deadline`,
13
+ * tool fingerprint). Fingerprints are diagnostic only: they do not establish
14
+ * invocation identity or application.
15
+ * - `<terminal>.decision-<id>.json`: **the settlement**, created exactly once
16
+ * (a temp file `link`ed into place, which never overwrites) by whoever
17
+ * settles the prompt first:
18
+ * - `host`: the decision from ProhostAI, which the waiting hook prints;
19
+ * - `timeout`: the hook's wait ran out; the menu is answered in the terminal;
20
+ * - `local`: turn/user progress retired the prompt;
21
+ * - `cancelled`: the hook received a stop signal; not proof the menu closed.
22
+ *
23
+ * A host acknowledgement records handoff to a hook, not proof the parent
24
+ * applied it. PermissionRequest lacks invocation identity. A verified PreToolUse
25
+ * hook records the candidate IDs first; only their own transcript completions,
26
+ * a new user prompt or a completed turn release replies. Legacy per-tool
27
+ * receipts are not trusted.
28
+ */
29
+ import type { CodeThreadEvent } from './events.js';
30
+ export type HostDecision = 'allow' | 'allow_session' | 'deny';
31
+ export type Settlement = {
32
+ by: 'host';
33
+ decision: HostDecision;
34
+ at: number;
35
+ } | {
36
+ by: 'timeout' | 'local' | 'cancelled';
37
+ at: number;
38
+ };
39
+ export interface Waiter {
40
+ approvalId: string;
41
+ /** The waiting hook's process. */
42
+ pid: number;
43
+ /** Epoch ms past which it no longer waits. */
44
+ deadline: number;
45
+ /** {@link toolFingerprint} of the call its prompt is for. */
46
+ fingerprint: string;
47
+ /** The transcript and the causal PreToolUse IDs captured before this permission request. */
48
+ transcript?: string;
49
+ toolUseIds?: readonly string[];
50
+ /** Durable original ingest: a daemon can retry after cancellation or a lost response. */
51
+ permissionRequest?: CodeThreadEvent;
52
+ /** `beatenDeny` reported this prompt's deny as overridden in the terminal: said once. */
53
+ overrideReported?: boolean;
54
+ }
55
+ /** What the host did with a decision from ProhostAI. */
56
+ export type DecideOutcome =
57
+ /** Settled with it: the waiting hook prints it. */
58
+ 'delivered'
59
+ /** Already settled with a decision from ProhostAI (a redelivery). */
60
+ | 'already'
61
+ /** Nothing waits for it: the prompt is (or was) answered in the terminal. */
62
+ | 'local';
63
+ export declare function waiterPath(terminalId: string, approvalId: string, env?: NodeJS.ProcessEnv): string;
64
+ export declare function settlementPath(terminalId: string, approvalId: string, env?: NodeJS.ProcessEnv): string;
65
+ /** A tool call as `PermissionRequest` saw it. Not an invocation's identity: identical calls share it. */
66
+ export declare function toolFingerprint(toolName: unknown, toolInput: unknown): string;
67
+ /** Lines of a transcript parsed, ever: for tests that a second read parses only what was appended. */
68
+ export declare const transcriptParseCounter: {
69
+ lines: number;
70
+ };
71
+ /** For tests: forget every transcript read so far. */
72
+ export declare function forgetTranscriptsForTests(): void;
73
+ /** Exact IDs, not content, counts or timestamp proximity, establish transcript completion. */
74
+ export declare function transcriptReleases(transcript: string, ids: readonly string[], now?: number): boolean;
75
+ /** For tests: the transcripts the cache holds now. */
76
+ export declare function cachedTranscriptsForTests(): string[];
77
+ export declare function readSettlement(terminalId: string, approvalId: string, env?: NodeJS.ProcessEnv): Settlement | undefined;
78
+ /**
79
+ * Settle the prompt with `settlement` unless it is settled already. Created
80
+ * whole and never overwritten: its contents go to a unique temp file, linked
81
+ * into place (EEXIST: someone settled it first). Returns the settlement that
82
+ * stands afterwards, ours or theirs; `undefined` only when the state can't be
83
+ * written or read, which every caller treats as "not by ProhostAI".
84
+ */
85
+ export declare function settle(terminalId: string, approvalId: string, settlement: Settlement, env?: NodeJS.ProcessEnv): {
86
+ settlement: Settlement | undefined;
87
+ ours: boolean;
88
+ };
89
+ export declare function readWaiter(terminalId: string, approvalId: string, env?: NodeJS.ProcessEnv): Waiter | undefined;
90
+ /**
91
+ * The hook registers itself as waiting for `waiter.approvalId`, before the
92
+ * server can hear of the prompt (so a decision that comes back at once finds
93
+ * it). Prior prompts, including completed ones, retain their fingerprints until
94
+ * turn progress: a delayed completion must never be mistaken for a new identical call.
95
+ */
96
+ export declare function registerWaiter(terminalId: string, waiter: Waiter, env?: NodeJS.ProcessEnv, now?: () => number): void;
97
+ /**
98
+ * Whether a `PermissionRequest` hook in this terminal reported a prompt within `withinMs`: one
99
+ * waits now, or one was settled that recently. A `permission_prompt` notification then is the
100
+ * same prompt, already reported.
101
+ */
102
+ export declare function recentPrompt(terminalId: string, env: NodeJS.ProcessEnv, now: number, withinMs: number, alive?: (pid: number) => boolean): boolean;
103
+ /**
104
+ * Whether this terminal's agent may have a permission menu up, so a key typed now (a relayed
105
+ * reply's Enter) could answer it. Every prompt without a confirming lifecycle receipt counts:
106
+ * - `open`: the menu is the terminal's: the hook's wait ran out (`timeout`), or the hook is gone or
107
+ * past its deadline unsettled (Claude Code killed it; its menu stays). Only the person there
108
+ * ends it, and turn progress (`markTurnAnswered`) or the transcript says so.
109
+ * - `deciding`: the hook still runs, waiting for a decision (one may be on its way) or about to
110
+ * print one it was handed (`host`; it exits within a poll).
111
+ * - `unconfirmed`: a host-settled hook exited without a later hook confirming progress. Neither
112
+ * its exit nor writing its stdout proves Claude Code consumed/applied the decision.
113
+ * - `clear`: turn/user progress removed the waiter, or Claude Code's own transcript shows the prompt
114
+ * over (`transcriptReleases`). A legacy `local` or `confirmed` receipt with a remaining waiter
115
+ * does not establish application.
116
+ *
117
+ * Keys a viewer sent (Escape, Ctrl+C) never count: tmux may consume them, and the menu stays.
118
+ */
119
+ export type MenuState = 'open' | 'unconfirmed' | 'deciding' | 'clear';
120
+ export declare function menuState(terminalId: string, env?: NodeJS.ProcessEnv, now?: number, alive?: (pid: number) => boolean): MenuState;
121
+ /** Completed IDs are reported only once every local permission barrier has cleared. */
122
+ export declare function completedApprovals(terminalId: string, env?: NodeJS.ProcessEnv): string[];
123
+ /** Retain the waiter until the server acknowledges the approval-scoped recovery event. */
124
+ export declare function acknowledgeCompletedApproval(terminalId: string, approvalId: string, env: NodeJS.ProcessEnv): void;
125
+ /** Whether `approvalId` is a prompt a hook waits on (or waited on) here, so the hook answers it. */
126
+ export declare function hookDecides(terminalId: string, approvalId: string, env?: NodeJS.ProcessEnv): boolean;
127
+ /**
128
+ * The host, for a decision from ProhostAI: settle the prompt with it, only
129
+ * while its hook still waits (alive, before its deadline, unsettled). Writes
130
+ * nothing otherwise, and presses no key on any path.
131
+ */
132
+ export declare function decide(terminalId: string, approvalId: string, decision: HostDecision, env?: NodeJS.ProcessEnv, now?: () => number, alive?: (pid: number) => boolean): DecideOutcome;
133
+ /**
134
+ * The hook, after registering: wait until its prompt is settled or `deadline`
135
+ * passes, then settle it `timeout` itself (unless a decision beat it to it).
136
+ * Returns ProhostAI's decision, or how it was otherwise settled.
137
+ */
138
+ export declare function awaitSettlement(terminalId: string, approvalId: string, deadline: number, env?: NodeJS.ProcessEnv, now?: () => number, sleep?: (ms: number) => Promise<void>, pollMs?: number): Promise<Settlement | undefined>;
139
+ /** How often a waiting hook looks in the transcript for its call's end. */
140
+ export declare const TRANSCRIPT_RECHECK_MS = 1000;
141
+ /**
142
+ * A `PostToolUse` for invocation `toolUseId`: that tool ran. If ProhostAI had settled its prompt with a
143
+ * deny, the terminal's "Yes" came first (Claude Code ignores a decision once the prompt is answered),
144
+ * and the record must not say "denied" for a command that ran.
145
+ *
146
+ * Exact IDs only, as for releases: exactly one held prompt names `toolUseId` among the invocations
147
+ * that could own it, and it names no other, so no other call (an identical one, or a subagent's) could
148
+ * be the prompt's own; the call that ran is the prompt's own call (its tool and input, `fingerprint`):
149
+ * a lone candidate may be a `Task`/`Agent` whose subagent prompted, and that Task finishing says
150
+ * nothing about the denied command; it was settled with a deny from ProhostAI, not reported yet.
151
+ * Anything less returns `undefined`: no correction, never a false one. Marks the prompt reported and
152
+ * returns its approval id. Retires nothing: only turn progress does (`markTurnAnswered`).
153
+ */
154
+ export declare function beatenDeny(terminalId: string, toolUseId: unknown, toolName: unknown, toolInput: unknown, env?: NodeJS.ProcessEnv): string | undefined;
155
+ /** A new user prompt or completed turn retires its permission waiters. */
156
+ export declare function markTurnAnswered(terminalId: string, env?: NodeJS.ProcessEnv, now?: () => number): void;