@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.
- package/CHANGELOG.md +95 -0
- package/README.md +129 -0
- package/dist/agent/claude.d.ts +28 -0
- package/dist/agent/claude.js +83 -4
- package/dist/agent/command.d.ts +8 -0
- package/dist/agent/command.js +40 -7
- package/dist/agent/daemon.d.ts +3 -1
- package/dist/agent/daemon.js +1 -2
- package/dist/agent/run.d.ts +30 -1
- package/dist/agent/run.js +311 -16
- package/dist/agent/runtime_status.d.ts +99 -3
- package/dist/agent/runtime_status.js +173 -5
- package/dist/agent/worktrees.d.ts +2 -0
- package/dist/agent/worktrees.js +3 -2
- package/dist/codehost/activity.d.ts +64 -0
- package/dist/codehost/activity.js +135 -0
- package/dist/codehost/agents.d.ts +31 -0
- package/dist/codehost/agents.js +57 -0
- package/dist/codehost/checkouts.d.ts +130 -0
- package/dist/codehost/checkouts.js +270 -0
- package/dist/codehost/claude_trust.d.ts +40 -0
- package/dist/codehost/claude_trust.js +129 -0
- package/dist/codehost/command.d.ts +25 -0
- package/dist/codehost/command.js +136 -0
- package/dist/codehost/decision.d.ts +156 -0
- package/dist/codehost/decision.js +553 -0
- package/dist/codehost/events.d.ts +49 -0
- package/dist/codehost/events.js +87 -0
- package/dist/codehost/folders.d.ts +122 -0
- package/dist/codehost/folders.js +317 -0
- package/dist/codehost/hooks.d.ts +178 -0
- package/dist/codehost/hooks.js +726 -0
- package/dist/codehost/host.d.ts +290 -0
- package/dist/codehost/host.js +625 -0
- package/dist/codehost/host_stats.d.ts +71 -0
- package/dist/codehost/host_stats.js +166 -0
- package/dist/codehost/invocations.d.ts +12 -0
- package/dist/codehost/invocations.js +82 -0
- package/dist/codehost/ledger.d.ts +35 -0
- package/dist/codehost/ledger.js +91 -0
- package/dist/codehost/local_server.d.ts +106 -0
- package/dist/codehost/local_server.js +339 -0
- package/dist/codehost/pty.d.ts +38 -0
- package/dist/codehost/pty.js +47 -0
- package/dist/codehost/ready.d.ts +31 -0
- package/dist/codehost/ready.js +109 -0
- package/dist/codehost/relay.d.ts +170 -0
- package/dist/codehost/relay.js +548 -0
- package/dist/codehost/settings.d.ts +52 -0
- package/dist/codehost/settings.js +114 -0
- package/dist/codehost/sizing.d.ts +163 -0
- package/dist/codehost/sizing.js +394 -0
- package/dist/codehost/terminals.d.ts +390 -0
- package/dist/codehost/terminals.js +965 -0
- package/dist/codehost/tmux.d.ts +196 -0
- package/dist/codehost/tmux.js +391 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +21 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- 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;
|