@prohost/cli 0.13.3 → 0.15.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 +90 -0
- package/README.md +129 -0
- 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 +230 -9
- 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/history.d.ts +80 -0
- package/dist/codehost/history.js +98 -0
- package/dist/codehost/hooks.d.ts +186 -0
- package/dist/codehost/hooks.js +737 -0
- package/dist/codehost/host.d.ts +297 -0
- package/dist/codehost/host.js +635 -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 +175 -0
- package/dist/codehost/relay.js +598 -0
- package/dist/codehost/settings.d.ts +52 -0
- package/dist/codehost/settings.js +114 -0
- package/dist/codehost/sizing.d.ts +181 -0
- package/dist/codehost/sizing.js +434 -0
- package/dist/codehost/terminals.d.ts +421 -0
- package/dist/codehost/terminals.js +1027 -0
- package/dist/codehost/tmux.d.ts +201 -0
- package/dist/codehost/tmux.js +394 -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,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure tmux helpers for the Mac terminal host — the same shape as the cloud
|
|
3
|
+
* sandbox's terminal server (`shared/sandbox/template_coding/terminal-server/lib.js`),
|
|
4
|
+
* ported here rather than imported: the CLI ships on npm on its own.
|
|
5
|
+
*
|
|
6
|
+
* Same names as the sandbox — session `prohost`, viewers attached as grouped
|
|
7
|
+
* sessions so each keeps its own current window — with two differences a
|
|
8
|
+
* shared machine needs:
|
|
9
|
+
*
|
|
10
|
+
* - **Its own tmux server** (`tmux -L prohost-<home digest>`). The operator's own tmux, its
|
|
11
|
+
* sessions and its config are never touched, and nothing here can be
|
|
12
|
+
* attached to by accident from their terminal.
|
|
13
|
+
* - **A window per thread**, not one `agent` window per sandbox: `a-<terminal>`
|
|
14
|
+
* runs the agent, `s-<terminal>` is a shell in the same worktree, made the
|
|
15
|
+
* first time someone asks for it.
|
|
16
|
+
*
|
|
17
|
+
* tmux owns the processes, so a thread outlives the daemon: a restarted daemon
|
|
18
|
+
* finds its windows again with {@link listWindowsArgs} and the `@prohost_*`
|
|
19
|
+
* window options set at creation.
|
|
20
|
+
*/
|
|
21
|
+
import type { ViewerClient } from './sizing.js';
|
|
22
|
+
/** Prefix of `tmux -L <socket>`: this host's private tmux server (one per `$PROHOST_HOME`). */
|
|
23
|
+
export declare const TMUX_SOCKET_PREFIX = "prohost";
|
|
24
|
+
export declare const TMUX_SESSION = "prohost";
|
|
25
|
+
/** The window the session is created with, so it exists while no thread runs. */
|
|
26
|
+
export declare const HOME_WINDOW = "home";
|
|
27
|
+
export type TerminalWindow = 'agent' | 'shell';
|
|
28
|
+
export declare const TERMINAL_WINDOWS: ReadonlySet<string>;
|
|
29
|
+
/** Terminal and thread ids: what tmux names, env values and URL paths can all carry as-is. */
|
|
30
|
+
export declare const ID_PATTERN: RegExp;
|
|
31
|
+
export declare const DEFAULT_COLS = 120;
|
|
32
|
+
export declare const DEFAULT_ROWS = 32;
|
|
33
|
+
export declare const MAX_DIM = 1000;
|
|
34
|
+
/**
|
|
35
|
+
* The private server's config, written beside the settings and passed with
|
|
36
|
+
* `-f`. Mirrors `shared/sandbox/template_coding/tmux.conf`, except for sizing
|
|
37
|
+
* (each window is made `window-size manual` and sized by the host, see
|
|
38
|
+
* `sizing.ts`), the hidden window-edge border and copy-mode position box, and
|
|
39
|
+
* `scroll-on-clear off` (Mac-only for now: the sandbox config is rebuilt with
|
|
40
|
+
* its template).
|
|
41
|
+
*/
|
|
42
|
+
export declare const TMUX_CONF = "# Written by `prohost agent run` for this machine's Code-thread terminals.\n# A private tmux server (tmux -L prohost-\u2026); your own tmux is not affected.\nset -g status off\n# Esc must reach Claude Code immediately (it interrupts a turn).\nset -s escape-time 10\nset -g default-terminal \"tmux-256color\"\nset -g history-limit 50000\n# Claude Code clears the whole screen and redraws it on every width change; with this on\n# (tmux's default) each old screen was kept in scrollback, so its header showed up once per\n# resize. A redraw longer than the screen still scrolls into history, so the host also keeps\n# width changes few (`sizing.ts`). -q: tmux before 3.3 has no such option.\nset -gq scroll-on-clear off\nset -g mouse on\n# Three lines per wheel step, as in a native terminal; tmux's five makes a\n# trackpad jump. Both copy-mode tables: mode-keys follows $EDITOR.\nbind -T copy-mode WheelUpPane select-pane \\; send-keys -X -N 3 scroll-up\nbind -T copy-mode WheelDownPane select-pane \\; send-keys -X -N 3 scroll-down\nbind -T copy-mode-vi WheelUpPane select-pane \\; send-keys -X -N 3 scroll-up\nbind -T copy-mode-vi WheelDownPane select-pane \\; send-keys -X -N 3 scroll-down\n# A program in the pane may not set anyone's clipboard through tmux: OSC 52\n# from the pane is ignored. Pinned rather than left to the default, which was\n# `on` before tmux 2.6. tmux still offers its own copies to the attached\n# terminal as OSC 52; whether a viewer writes them to the system clipboard is\n# that viewer's choice (the web terminal's gated clipboard addon, #21271).\n# The default drag binding is kept on purpose: it leaves copy mode on mouse-up,\n# so the next keystroke (a person's, or a relayed reply's) reaches the agent.\nset -s set-clipboard external\n# Windows are sized by the host, never by whichever viewer was active last: it\n# picks one viewer per window (the first to show it, then whoever types, focuses\n# or asks) and resizes the window to it (`sizing.ts`); `resize-window` makes\n# that window `window-size manual`. A new window is resized at once to the\n# size it will be viewed at. Not manual globally: tmux 3.4's server exits at\n# start with that in its config.\nset -g window-size latest\n# A viewer bigger than the window (someone smaller sizes it) sees blank space\n# beyond it, not tmux's dotted fill. -q: tmux before 3.3 has no such option.\nset -gq fill-character \" \"\n# ...and no border at the window's edge: tmux draws one there in the active\n# border's green. Spaces in the terminal's own colours instead.\nset -g pane-border-style default\nset -g pane-active-border-style default\nset -gq pane-border-lines spaces\nsetw -g aggressive-resize on\n# Scrolling back (copy mode) shows no \"[4/120]\" position box over the agent's\n# screen. -q: an older tmux has no such option (and shows the box).\nset -gq copy-mode-position-format \"\"\n# Window names are an API (the host targets them by name) \u2014 nothing may rename them.\nsetw -g automatic-rename off\nsetw -g allow-rename off\nset -g set-titles off\nset -g focus-events on\n";
|
|
43
|
+
export declare function isValidId(value: unknown): value is string;
|
|
44
|
+
/** tmux window name for one of a thread's windows. */
|
|
45
|
+
export declare function windowName(terminalId: string, window: TerminalWindow): string;
|
|
46
|
+
/** `prohost:<window>` — an exact target, never a prefix match. */
|
|
47
|
+
export declare function windowTarget(name: string): string;
|
|
48
|
+
/**
|
|
49
|
+
* The private server's socket name for one `$PROHOST_HOME`. Two agent homes on
|
|
50
|
+
* one machine (the desktop app's and a terminal's) must not manage one
|
|
51
|
+
* server's windows: each hook would read the other's token.
|
|
52
|
+
*/
|
|
53
|
+
export declare function socketNameFor(home: string): string;
|
|
54
|
+
/** Arguments every invocation starts with. */
|
|
55
|
+
export declare function baseArgs(socket: string, confPath: string): string[];
|
|
56
|
+
export declare function hasSessionArgs(): string[];
|
|
57
|
+
export declare function newSessionArgs(cwd: string): string[];
|
|
58
|
+
export interface NewWindowSpec {
|
|
59
|
+
name: string;
|
|
60
|
+
cwd: string;
|
|
61
|
+
env: Record<string, string>;
|
|
62
|
+
/** A shell command line; tmux runs it with `sh -c`. */
|
|
63
|
+
command: string;
|
|
64
|
+
}
|
|
65
|
+
export declare function newWindowArgs(spec: NewWindowSpec): string[];
|
|
66
|
+
/** Window options that let a restarted daemon re-adopt a thread. */
|
|
67
|
+
export interface WindowMeta {
|
|
68
|
+
terminalId: string;
|
|
69
|
+
codeThreadId: string;
|
|
70
|
+
agent: string;
|
|
71
|
+
cwd: string;
|
|
72
|
+
/**
|
|
73
|
+
* Started by a build that tracks Claude Code's trust with the ready mark (`ready.ts`): such a
|
|
74
|
+
* window is past its dialog only once a hook wrote the mark, never by adoption.
|
|
75
|
+
*/
|
|
76
|
+
readyMark?: boolean;
|
|
77
|
+
}
|
|
78
|
+
export declare function setWindowMetaArgs(name: string, meta: WindowMeta): string[][];
|
|
79
|
+
export declare function listWindowsArgs(): string[];
|
|
80
|
+
export interface ListedWindow {
|
|
81
|
+
name: string;
|
|
82
|
+
meta?: WindowMeta;
|
|
83
|
+
/** The window's first pane's process (the agent's command), when tmux says. */
|
|
84
|
+
panePid?: number;
|
|
85
|
+
}
|
|
86
|
+
/** Parse {@link listWindowsArgs} output; windows without valid metadata keep only their name. */
|
|
87
|
+
export declare function parseListWindows(stdout: string): ListedWindow[];
|
|
88
|
+
/** A fresh, unguessable name for one viewer's grouped session. */
|
|
89
|
+
export declare function viewSessionName(): string;
|
|
90
|
+
/**
|
|
91
|
+
* argv (after {@link baseArgs}) that attaches one viewer to window `name`.
|
|
92
|
+
*
|
|
93
|
+
* A grouped session (`new-session -t prohost`), as in the sandbox: it shares
|
|
94
|
+
* every window but keeps its own current window, so one viewer on a shell
|
|
95
|
+
* never moves another off the agent. `destroy-unattached` reaps it when the
|
|
96
|
+
* viewer goes. `-u`: the viewer is xterm.js, which is always UTF-8 (see
|
|
97
|
+
* {@link utf8Env}).
|
|
98
|
+
*/
|
|
99
|
+
export declare function attachArgs(name: string, viewName: string): string[];
|
|
100
|
+
/** What tmux and its panes run under when the daemon's own env has no UTF-8 locale. */
|
|
101
|
+
export declare const UTF8_CTYPE = "C.UTF-8";
|
|
102
|
+
/**
|
|
103
|
+
* `env` with a UTF-8 character locale, for every tmux this host runs.
|
|
104
|
+
*
|
|
105
|
+
* A daemon started by launchd (or any GUI parent) usually has no LANG at all,
|
|
106
|
+
* and a tmux client without a UTF-8 locale draws every non-ASCII cell as `_` —
|
|
107
|
+
* Claude Code's ⏺ bullets, its logo, an em dash. The server that client starts
|
|
108
|
+
* passes the same locale to the agent and shell panes. Only the character set
|
|
109
|
+
* is set (`LC_CTYPE`): the operator's language and formats stay theirs.
|
|
110
|
+
*/
|
|
111
|
+
export declare function utf8Env<T extends Record<string, string | undefined>>(env: T): T;
|
|
112
|
+
/**
|
|
113
|
+
* What only this daemon's own process needs, never a Code thread's terminal. The desktop app runs
|
|
114
|
+
* the CLI on its bundled Electron (`ELECTRON_RUN_AS_NODE=1`, `PROHOST_CLI_RUN=1`): passed on to the
|
|
115
|
+
* tmux server, every shell and agent pane would inherit them, and any Electron app started there
|
|
116
|
+
* (`electron .`, a dev build) would start as plain Node instead. The hook shim sets the one it needs.
|
|
117
|
+
*/
|
|
118
|
+
export declare const DAEMON_ONLY_ENV: readonly ["ELECTRON_RUN_AS_NODE", "PROHOST_CLI_RUN"];
|
|
119
|
+
/** {@link utf8Env}, without {@link DAEMON_ONLY_ENV}: the env every tmux this host runs gets. */
|
|
120
|
+
export declare function paneEnv<T extends Record<string, string | undefined>>(env: T): T;
|
|
121
|
+
export declare function killSessionArgs(viewName: string): string[];
|
|
122
|
+
/**
|
|
123
|
+
* Text relayed from a composer, as tmux commands: C0 control characters other
|
|
124
|
+
* than newline and tab are dropped — a reply is text, never a key sequence —
|
|
125
|
+
* and multi-line text goes in as one bracketed paste so the agent sees one
|
|
126
|
+
* message rather than one per line.
|
|
127
|
+
*
|
|
128
|
+
* Returns the commands to run in order; an entry with `stdin` feeds that text
|
|
129
|
+
* to tmux on standard input (`load-buffer -`). Enter is sent last, separately,
|
|
130
|
+
* so the agent submits what was pasted.
|
|
131
|
+
*/
|
|
132
|
+
export declare function sendTextCommands(name: string, rawText: string): {
|
|
133
|
+
args: string[];
|
|
134
|
+
stdin?: string;
|
|
135
|
+
}[];
|
|
136
|
+
export type ApprovalDecision = 'allow' | 'allow_session' | 'deny';
|
|
137
|
+
export declare const APPROVAL_DECISIONS: ReadonlySet<string>;
|
|
138
|
+
export declare function clampDim(raw: unknown, fallback: number): number;
|
|
139
|
+
/**
|
|
140
|
+
* A text control frame. Input arrives as binary or `input`; `hello` says what the viewer is and
|
|
141
|
+
* `claim` asks to size the window to it (see `sizing.ts`); `history` asks for the pane's
|
|
142
|
+
* scroll-back as text (`history.ts`). Anything else is ignored.
|
|
143
|
+
*/
|
|
144
|
+
export type ControlFrame = {
|
|
145
|
+
type: 'resize';
|
|
146
|
+
cols: number;
|
|
147
|
+
rows: number;
|
|
148
|
+
} | {
|
|
149
|
+
type: 'input';
|
|
150
|
+
data: string;
|
|
151
|
+
} | {
|
|
152
|
+
type: 'hello';
|
|
153
|
+
client: ViewerClient;
|
|
154
|
+
} | {
|
|
155
|
+
type: 'claim';
|
|
156
|
+
} | {
|
|
157
|
+
type: 'history';
|
|
158
|
+
id: string;
|
|
159
|
+
lines: number;
|
|
160
|
+
} | {
|
|
161
|
+
type: 'ping';
|
|
162
|
+
} | {
|
|
163
|
+
type: 'pong';
|
|
164
|
+
};
|
|
165
|
+
export declare function parseControl(text: string): ControlFrame | null;
|
|
166
|
+
/**
|
|
167
|
+
* Whether a viewer's keystrokes stop the agent's turn: a lone Esc (Claude
|
|
168
|
+
* Code's interrupt) or a Ctrl+C anywhere in the chunk. Esc as the first byte of
|
|
169
|
+
* a longer sequence is an arrow key or a terminal report, not an interrupt.
|
|
170
|
+
*/
|
|
171
|
+
export declare function isInterruptInput(data: Uint8Array): boolean;
|
|
172
|
+
/** Whether `data` holds anything a person typed: what is left once the emulator's own reports are taken out. */
|
|
173
|
+
export declare function isHumanInput(data: Uint8Array): boolean;
|
|
174
|
+
/** A focus-in report (`focus-events on`): the viewer's terminal just got focus, which claims the window's size. */
|
|
175
|
+
export declare function hasFocusIn(data: Uint8Array): boolean;
|
|
176
|
+
/**
|
|
177
|
+
* Whether `data` may have put the pane in copy mode: a mouse report (a wheel step up or a drag
|
|
178
|
+
* enters it, `mouse on`) or tmux's prefix key (C-b [). Typed input that follows leaves it first.
|
|
179
|
+
*/
|
|
180
|
+
export declare function mayEnterCopyMode(data: Uint8Array): boolean;
|
|
181
|
+
/**
|
|
182
|
+
* Text a person typed (or pasted) for the program in the pane: a chunk that holds a printable
|
|
183
|
+
* character and isn't an escape sequence (arrows, PgUp, function keys move through copy mode) —
|
|
184
|
+
* except a bracketed paste (`ESC [200~`…), which is text.
|
|
185
|
+
*/
|
|
186
|
+
export declare function isTypedText(data: Uint8Array): boolean;
|
|
187
|
+
/**
|
|
188
|
+
* A gesture that scrolls (or drags in) the pane — what makes a viewer "the one who scrolled": a wheel
|
|
189
|
+
* step or a drag (an SGR or X10 mouse report with button 32 and up), or the prefix key. A plain
|
|
190
|
+
* click is not one.
|
|
191
|
+
*/
|
|
192
|
+
export declare function isScrollGesture(data: Uint8Array): boolean;
|
|
193
|
+
/**
|
|
194
|
+
* The keys a person presses to leave copy mode (q, Esc, Ctrl+C): typed while the pane was in it,
|
|
195
|
+
* they only leave it, as they did before — never reach the agent as a keystroke or an interrupt.
|
|
196
|
+
*/
|
|
197
|
+
export declare function isCopyModeExitKey(data: Uint8Array): boolean;
|
|
198
|
+
/** `1` when the window's pane is in a mode (copy mode, after a scroll back). */
|
|
199
|
+
export declare function paneInModeArgs(name: string): string[];
|
|
200
|
+
/** Leave copy mode, if the pane is in it, before relayed text is typed (it would be read as copy-mode keys). */
|
|
201
|
+
export declare function leaveCopyModeArgs(name: string): string[];
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure tmux helpers for the Mac terminal host — the same shape as the cloud
|
|
3
|
+
* sandbox's terminal server (`shared/sandbox/template_coding/terminal-server/lib.js`),
|
|
4
|
+
* ported here rather than imported: the CLI ships on npm on its own.
|
|
5
|
+
*
|
|
6
|
+
* Same names as the sandbox — session `prohost`, viewers attached as grouped
|
|
7
|
+
* sessions so each keeps its own current window — with two differences a
|
|
8
|
+
* shared machine needs:
|
|
9
|
+
*
|
|
10
|
+
* - **Its own tmux server** (`tmux -L prohost-<home digest>`). The operator's own tmux, its
|
|
11
|
+
* sessions and its config are never touched, and nothing here can be
|
|
12
|
+
* attached to by accident from their terminal.
|
|
13
|
+
* - **A window per thread**, not one `agent` window per sandbox: `a-<terminal>`
|
|
14
|
+
* runs the agent, `s-<terminal>` is a shell in the same worktree, made the
|
|
15
|
+
* first time someone asks for it.
|
|
16
|
+
*
|
|
17
|
+
* tmux owns the processes, so a thread outlives the daemon: a restarted daemon
|
|
18
|
+
* finds its windows again with {@link listWindowsArgs} and the `@prohost_*`
|
|
19
|
+
* window options set at creation.
|
|
20
|
+
*/
|
|
21
|
+
import crypto from 'node:crypto';
|
|
22
|
+
import { parseHistoryRequest } from './history.js';
|
|
23
|
+
import { parseViewerClient } from './sizing.js';
|
|
24
|
+
/** Prefix of `tmux -L <socket>`: this host's private tmux server (one per `$PROHOST_HOME`). */
|
|
25
|
+
export const TMUX_SOCKET_PREFIX = 'prohost';
|
|
26
|
+
export const TMUX_SESSION = 'prohost';
|
|
27
|
+
/** The window the session is created with, so it exists while no thread runs. */
|
|
28
|
+
export const HOME_WINDOW = 'home';
|
|
29
|
+
export const TERMINAL_WINDOWS = new Set(['agent', 'shell']);
|
|
30
|
+
/** Terminal and thread ids: what tmux names, env values and URL paths can all carry as-is. */
|
|
31
|
+
export const ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
32
|
+
export const DEFAULT_COLS = 120;
|
|
33
|
+
export const DEFAULT_ROWS = 32;
|
|
34
|
+
export const MAX_DIM = 1000;
|
|
35
|
+
/**
|
|
36
|
+
* The private server's config, written beside the settings and passed with
|
|
37
|
+
* `-f`. Mirrors `shared/sandbox/template_coding/tmux.conf`, except for sizing
|
|
38
|
+
* (each window is made `window-size manual` and sized by the host, see
|
|
39
|
+
* `sizing.ts`), the hidden window-edge border and copy-mode position box, and
|
|
40
|
+
* `scroll-on-clear off` (Mac-only for now: the sandbox config is rebuilt with
|
|
41
|
+
* its template).
|
|
42
|
+
*/
|
|
43
|
+
export const TMUX_CONF = `# Written by \`prohost agent run\` for this machine's Code-thread terminals.
|
|
44
|
+
# A private tmux server (tmux -L ${TMUX_SOCKET_PREFIX}-…); your own tmux is not affected.
|
|
45
|
+
set -g status off
|
|
46
|
+
# Esc must reach Claude Code immediately (it interrupts a turn).
|
|
47
|
+
set -s escape-time 10
|
|
48
|
+
set -g default-terminal "tmux-256color"
|
|
49
|
+
set -g history-limit 50000
|
|
50
|
+
# Claude Code clears the whole screen and redraws it on every width change; with this on
|
|
51
|
+
# (tmux's default) each old screen was kept in scrollback, so its header showed up once per
|
|
52
|
+
# resize. A redraw longer than the screen still scrolls into history, so the host also keeps
|
|
53
|
+
# width changes few (\`sizing.ts\`). -q: tmux before 3.3 has no such option.
|
|
54
|
+
set -gq scroll-on-clear off
|
|
55
|
+
set -g mouse on
|
|
56
|
+
# Three lines per wheel step, as in a native terminal; tmux's five makes a
|
|
57
|
+
# trackpad jump. Both copy-mode tables: mode-keys follows $EDITOR.
|
|
58
|
+
bind -T copy-mode WheelUpPane select-pane \\; send-keys -X -N 3 scroll-up
|
|
59
|
+
bind -T copy-mode WheelDownPane select-pane \\; send-keys -X -N 3 scroll-down
|
|
60
|
+
bind -T copy-mode-vi WheelUpPane select-pane \\; send-keys -X -N 3 scroll-up
|
|
61
|
+
bind -T copy-mode-vi WheelDownPane select-pane \\; send-keys -X -N 3 scroll-down
|
|
62
|
+
# A program in the pane may not set anyone's clipboard through tmux: OSC 52
|
|
63
|
+
# from the pane is ignored. Pinned rather than left to the default, which was
|
|
64
|
+
# \`on\` before tmux 2.6. tmux still offers its own copies to the attached
|
|
65
|
+
# terminal as OSC 52; whether a viewer writes them to the system clipboard is
|
|
66
|
+
# that viewer's choice (the web terminal's gated clipboard addon, #21271).
|
|
67
|
+
# The default drag binding is kept on purpose: it leaves copy mode on mouse-up,
|
|
68
|
+
# so the next keystroke (a person's, or a relayed reply's) reaches the agent.
|
|
69
|
+
set -s set-clipboard external
|
|
70
|
+
# Windows are sized by the host, never by whichever viewer was active last: it
|
|
71
|
+
# picks one viewer per window (the first to show it, then whoever types, focuses
|
|
72
|
+
# or asks) and resizes the window to it (\`sizing.ts\`); \`resize-window\` makes
|
|
73
|
+
# that window \`window-size manual\`. A new window is resized at once to the
|
|
74
|
+
# size it will be viewed at. Not manual globally: tmux 3.4's server exits at
|
|
75
|
+
# start with that in its config.
|
|
76
|
+
set -g window-size latest
|
|
77
|
+
# A viewer bigger than the window (someone smaller sizes it) sees blank space
|
|
78
|
+
# beyond it, not tmux's dotted fill. -q: tmux before 3.3 has no such option.
|
|
79
|
+
set -gq fill-character " "
|
|
80
|
+
# ...and no border at the window's edge: tmux draws one there in the active
|
|
81
|
+
# border's green. Spaces in the terminal's own colours instead.
|
|
82
|
+
set -g pane-border-style default
|
|
83
|
+
set -g pane-active-border-style default
|
|
84
|
+
set -gq pane-border-lines spaces
|
|
85
|
+
setw -g aggressive-resize on
|
|
86
|
+
# Scrolling back (copy mode) shows no "[4/120]" position box over the agent's
|
|
87
|
+
# screen. -q: an older tmux has no such option (and shows the box).
|
|
88
|
+
set -gq copy-mode-position-format ""
|
|
89
|
+
# Window names are an API (the host targets them by name) — nothing may rename them.
|
|
90
|
+
setw -g automatic-rename off
|
|
91
|
+
setw -g allow-rename off
|
|
92
|
+
set -g set-titles off
|
|
93
|
+
set -g focus-events on
|
|
94
|
+
`;
|
|
95
|
+
export function isValidId(value) {
|
|
96
|
+
return typeof value === 'string' && ID_PATTERN.test(value);
|
|
97
|
+
}
|
|
98
|
+
/** tmux window name for one of a thread's windows. */
|
|
99
|
+
export function windowName(terminalId, window) {
|
|
100
|
+
if (!isValidId(terminalId))
|
|
101
|
+
throw new Error('invalid terminal id');
|
|
102
|
+
return `${window === 'agent' ? 'a' : 's'}-${terminalId}`;
|
|
103
|
+
}
|
|
104
|
+
/** `prohost:<window>` — an exact target, never a prefix match. */
|
|
105
|
+
export function windowTarget(name) {
|
|
106
|
+
// `=` makes tmux match the name exactly: `a-1` must not resolve to `a-10`.
|
|
107
|
+
return `${TMUX_SESSION}:=${name}`;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The private server's socket name for one `$PROHOST_HOME`. Two agent homes on
|
|
111
|
+
* one machine (the desktop app's and a terminal's) must not manage one
|
|
112
|
+
* server's windows: each hook would read the other's token.
|
|
113
|
+
*/
|
|
114
|
+
export function socketNameFor(home) {
|
|
115
|
+
return `${TMUX_SOCKET_PREFIX}-${crypto.createHash('sha256').update(home).digest('hex').slice(0, 10)}`;
|
|
116
|
+
}
|
|
117
|
+
/** Arguments every invocation starts with. */
|
|
118
|
+
export function baseArgs(socket, confPath) {
|
|
119
|
+
return ['-L', socket, '-f', confPath];
|
|
120
|
+
}
|
|
121
|
+
export function hasSessionArgs() {
|
|
122
|
+
return ['has-session', '-t', `=${TMUX_SESSION}`];
|
|
123
|
+
}
|
|
124
|
+
export function newSessionArgs(cwd) {
|
|
125
|
+
return ['new-session', '-d', '-s', TMUX_SESSION, '-n', HOME_WINDOW, '-c', cwd];
|
|
126
|
+
}
|
|
127
|
+
export function newWindowArgs(spec) {
|
|
128
|
+
const env = Object.entries(spec.env).flatMap(([key, value]) => ['-e', `${key}=${value}`]);
|
|
129
|
+
return ['new-window', '-d', '-t', `${TMUX_SESSION}:`, '-n', spec.name, '-c', spec.cwd, ...env, spec.command];
|
|
130
|
+
}
|
|
131
|
+
export function setWindowMetaArgs(name, meta) {
|
|
132
|
+
const target = windowTarget(name);
|
|
133
|
+
return [
|
|
134
|
+
['set-option', '-w', '-t', target, '@prohost_terminal', meta.terminalId],
|
|
135
|
+
['set-option', '-w', '-t', target, '@prohost_thread', meta.codeThreadId],
|
|
136
|
+
['set-option', '-w', '-t', target, '@prohost_agent', meta.agent],
|
|
137
|
+
['set-option', '-w', '-t', target, '@prohost_cwd', meta.cwd],
|
|
138
|
+
...(meta.readyMark ? [['set-option', '-w', '-t', target, '@prohost_ready_mark', '1']] : []),
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
const LIST_FORMAT = '#{window_name}\t#{@prohost_terminal}\t#{@prohost_thread}\t#{@prohost_agent}\t#{@prohost_cwd}' +
|
|
142
|
+
'\t#{@prohost_ready_mark}\t#{pane_pid}';
|
|
143
|
+
export function listWindowsArgs() {
|
|
144
|
+
return ['list-windows', '-t', `=${TMUX_SESSION}`, '-F', LIST_FORMAT];
|
|
145
|
+
}
|
|
146
|
+
/** Parse {@link listWindowsArgs} output; windows without valid metadata keep only their name. */
|
|
147
|
+
export function parseListWindows(stdout) {
|
|
148
|
+
const windows = [];
|
|
149
|
+
for (const line of stdout.split('\n')) {
|
|
150
|
+
if (!line.trim())
|
|
151
|
+
continue;
|
|
152
|
+
const [name = '', terminalId = '', codeThreadId = '', agent = '', cwd = '', readyMark = '', pid = ''] = line.split('\t');
|
|
153
|
+
if (!name)
|
|
154
|
+
continue;
|
|
155
|
+
const meta = isValidId(terminalId) && isValidId(codeThreadId) && agent && cwd
|
|
156
|
+
? {
|
|
157
|
+
terminalId,
|
|
158
|
+
codeThreadId,
|
|
159
|
+
agent,
|
|
160
|
+
cwd,
|
|
161
|
+
...(readyMark === '1' ? { readyMark: true } : {}),
|
|
162
|
+
}
|
|
163
|
+
: undefined;
|
|
164
|
+
const panePid = /^\d+$/.test(pid) ? Number(pid) : undefined;
|
|
165
|
+
windows.push({ name, ...(meta ? { meta } : {}), ...(panePid ? { panePid } : {}) });
|
|
166
|
+
}
|
|
167
|
+
return windows;
|
|
168
|
+
}
|
|
169
|
+
/** A fresh, unguessable name for one viewer's grouped session. */
|
|
170
|
+
export function viewSessionName() {
|
|
171
|
+
return `view-${crypto.randomBytes(6).toString('hex')}`;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* argv (after {@link baseArgs}) that attaches one viewer to window `name`.
|
|
175
|
+
*
|
|
176
|
+
* A grouped session (`new-session -t prohost`), as in the sandbox: it shares
|
|
177
|
+
* every window but keeps its own current window, so one viewer on a shell
|
|
178
|
+
* never moves another off the agent. `destroy-unattached` reaps it when the
|
|
179
|
+
* viewer goes. `-u`: the viewer is xterm.js, which is always UTF-8 (see
|
|
180
|
+
* {@link utf8Env}).
|
|
181
|
+
*/
|
|
182
|
+
export function attachArgs(name, viewName) {
|
|
183
|
+
return [
|
|
184
|
+
'-u',
|
|
185
|
+
'new-session',
|
|
186
|
+
'-t',
|
|
187
|
+
`=${TMUX_SESSION}`,
|
|
188
|
+
'-s',
|
|
189
|
+
viewName,
|
|
190
|
+
';',
|
|
191
|
+
'set-option',
|
|
192
|
+
'destroy-unattached',
|
|
193
|
+
'on',
|
|
194
|
+
';',
|
|
195
|
+
'select-window',
|
|
196
|
+
'-t',
|
|
197
|
+
`${viewName}:=${name}`,
|
|
198
|
+
];
|
|
199
|
+
}
|
|
200
|
+
/** What tmux and its panes run under when the daemon's own env has no UTF-8 locale. */
|
|
201
|
+
export const UTF8_CTYPE = 'C.UTF-8';
|
|
202
|
+
/**
|
|
203
|
+
* `env` with a UTF-8 character locale, for every tmux this host runs.
|
|
204
|
+
*
|
|
205
|
+
* A daemon started by launchd (or any GUI parent) usually has no LANG at all,
|
|
206
|
+
* and a tmux client without a UTF-8 locale draws every non-ASCII cell as `_` —
|
|
207
|
+
* Claude Code's ⏺ bullets, its logo, an em dash. The server that client starts
|
|
208
|
+
* passes the same locale to the agent and shell panes. Only the character set
|
|
209
|
+
* is set (`LC_CTYPE`): the operator's language and formats stay theirs.
|
|
210
|
+
*/
|
|
211
|
+
export function utf8Env(env) {
|
|
212
|
+
const ctype = env.LC_ALL || env.LC_CTYPE || env.LANG || '';
|
|
213
|
+
if (/utf-?8/i.test(ctype))
|
|
214
|
+
return env;
|
|
215
|
+
const out = { ...env, LC_CTYPE: UTF8_CTYPE };
|
|
216
|
+
// A non-UTF-8 LC_ALL would outrank it.
|
|
217
|
+
delete out.LC_ALL;
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* What only this daemon's own process needs, never a Code thread's terminal. The desktop app runs
|
|
222
|
+
* the CLI on its bundled Electron (`ELECTRON_RUN_AS_NODE=1`, `PROHOST_CLI_RUN=1`): passed on to the
|
|
223
|
+
* tmux server, every shell and agent pane would inherit them, and any Electron app started there
|
|
224
|
+
* (`electron .`, a dev build) would start as plain Node instead. The hook shim sets the one it needs.
|
|
225
|
+
*/
|
|
226
|
+
export const DAEMON_ONLY_ENV = ['ELECTRON_RUN_AS_NODE', 'PROHOST_CLI_RUN'];
|
|
227
|
+
/** {@link utf8Env}, without {@link DAEMON_ONLY_ENV}: the env every tmux this host runs gets. */
|
|
228
|
+
export function paneEnv(env) {
|
|
229
|
+
const out = { ...env };
|
|
230
|
+
for (const key of DAEMON_ONLY_ENV)
|
|
231
|
+
delete out[key];
|
|
232
|
+
return utf8Env(out);
|
|
233
|
+
}
|
|
234
|
+
export function killSessionArgs(viewName) {
|
|
235
|
+
return ['kill-session', '-t', `=${viewName}`];
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Text relayed from a composer, as tmux commands: C0 control characters other
|
|
239
|
+
* than newline and tab are dropped — a reply is text, never a key sequence —
|
|
240
|
+
* and multi-line text goes in as one bracketed paste so the agent sees one
|
|
241
|
+
* message rather than one per line.
|
|
242
|
+
*
|
|
243
|
+
* Returns the commands to run in order; an entry with `stdin` feeds that text
|
|
244
|
+
* to tmux on standard input (`load-buffer -`). Enter is sent last, separately,
|
|
245
|
+
* so the agent submits what was pasted.
|
|
246
|
+
*/
|
|
247
|
+
export function sendTextCommands(name, rawText) {
|
|
248
|
+
// eslint-disable-next-line no-control-regex
|
|
249
|
+
const text = rawText.replace(/\r\n?/g, '\n').replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, '');
|
|
250
|
+
const target = windowTarget(name);
|
|
251
|
+
if (!text.trim())
|
|
252
|
+
return [];
|
|
253
|
+
if (!text.includes('\n')) {
|
|
254
|
+
return [
|
|
255
|
+
{ args: ['send-keys', '-t', target, '-l', '--', text] },
|
|
256
|
+
{ args: ['send-keys', '-t', target, 'Enter'] },
|
|
257
|
+
];
|
|
258
|
+
}
|
|
259
|
+
const buffer = `prohost-${crypto.randomBytes(4).toString('hex')}`;
|
|
260
|
+
return [
|
|
261
|
+
{ args: ['load-buffer', '-b', buffer, '-'], stdin: text },
|
|
262
|
+
// -p: bracketed paste when the application asked for it; -d: delete the buffer after.
|
|
263
|
+
{ args: ['paste-buffer', '-p', '-d', '-b', buffer, '-t', target] },
|
|
264
|
+
{ args: ['send-keys', '-t', target, 'Enter'] },
|
|
265
|
+
];
|
|
266
|
+
}
|
|
267
|
+
export const APPROVAL_DECISIONS = new Set(['allow', 'allow_session', 'deny']);
|
|
268
|
+
export function clampDim(raw, fallback) {
|
|
269
|
+
const n = typeof raw === 'number' ? Math.trunc(raw) : Number.parseInt(String(raw ?? ''), 10);
|
|
270
|
+
if (!Number.isFinite(n) || n < 1)
|
|
271
|
+
return fallback;
|
|
272
|
+
return Math.min(n, MAX_DIM);
|
|
273
|
+
}
|
|
274
|
+
export function parseControl(text) {
|
|
275
|
+
let frame;
|
|
276
|
+
try {
|
|
277
|
+
frame = JSON.parse(text);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
if (!frame || typeof frame !== 'object')
|
|
283
|
+
return null;
|
|
284
|
+
const f = frame;
|
|
285
|
+
if (f.type === 'resize') {
|
|
286
|
+
const cols = clampDim(f.cols, 0);
|
|
287
|
+
const rows = clampDim(f.rows, 0);
|
|
288
|
+
return cols && rows ? { type: 'resize', cols, rows } : null;
|
|
289
|
+
}
|
|
290
|
+
if (f.type === 'input' && typeof f.data === 'string')
|
|
291
|
+
return { type: 'input', data: f.data };
|
|
292
|
+
if (f.type === 'hello')
|
|
293
|
+
return { type: 'hello', client: parseViewerClient(f.client) };
|
|
294
|
+
if (f.type === 'claim')
|
|
295
|
+
return { type: 'claim' };
|
|
296
|
+
if (f.type === 'history')
|
|
297
|
+
return parseHistoryRequest(f);
|
|
298
|
+
if (f.type === 'ping' || f.type === 'pong')
|
|
299
|
+
return { type: f.type };
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Whether a viewer's keystrokes stop the agent's turn: a lone Esc (Claude
|
|
304
|
+
* Code's interrupt) or a Ctrl+C anywhere in the chunk. Esc as the first byte of
|
|
305
|
+
* a longer sequence is an arrow key or a terminal report, not an interrupt.
|
|
306
|
+
*/
|
|
307
|
+
export function isInterruptInput(data) {
|
|
308
|
+
if (data.length === 1 && data[0] === 0x1b)
|
|
309
|
+
return true;
|
|
310
|
+
return data.includes(0x03);
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* What a terminal emulator sends on its own, never a person: focus in/out
|
|
314
|
+
* (`focus-events on`), cursor-position and status replies, device-attribute
|
|
315
|
+
* and mode reports, window-size reports, mouse reports (`mouse on`: a click
|
|
316
|
+
* or a wheel step is reported, not typed), and OSC/DCS replies such as a
|
|
317
|
+
* colour query's answer. Several can arrive in one chunk. The same set as
|
|
318
|
+
* the web terminal's `AUTOMATIC_TERMINAL_SEQUENCE` (`web/src/features/coding/
|
|
319
|
+
* utils/terminalKeys.ts`); xterm.js hands each report to `onData` whole.
|
|
320
|
+
*/
|
|
321
|
+
const AUTOMATIC_REPORTS = new RegExp([
|
|
322
|
+
/\x1b\[[IO]/, // focus in / out
|
|
323
|
+
/\x1b\[\??\d+(?:;\d+){1,2}R/, // cursor position report, and xterm's DEC form (DECXCPR)
|
|
324
|
+
/\x1b\[\??\d*n/, // device status report reply
|
|
325
|
+
/\x1b\[[?>=][\d;]*c/, // primary / secondary / tertiary device attributes
|
|
326
|
+
/\x1b\[\??[\d;]*\$y/, // mode report (DECRPM)
|
|
327
|
+
/\x1b\[\d+(?:;\d+)*t/, // window size / position report
|
|
328
|
+
/\x1b\[<\d+;\d+;\d+[Mm]/, // SGR mouse report
|
|
329
|
+
/\x1b\[M[\s\S]{3}/, // X10 mouse report
|
|
330
|
+
/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/, // OSC reply (colours, clipboard query)
|
|
331
|
+
/\x1bP[^\x1b]*\x1b\\/, // DCS reply (terminal version, setting query)
|
|
332
|
+
]
|
|
333
|
+
.map((pattern) => pattern.source)
|
|
334
|
+
.join('|'), 'g');
|
|
335
|
+
/** Whether `data` holds anything a person typed: what is left once the emulator's own reports are taken out. */
|
|
336
|
+
export function isHumanInput(data) {
|
|
337
|
+
return Buffer.from(data).toString('latin1').replace(AUTOMATIC_REPORTS, '').length > 0;
|
|
338
|
+
}
|
|
339
|
+
/** A focus-in report (`focus-events on`): the viewer's terminal just got focus, which claims the window's size. */
|
|
340
|
+
export function hasFocusIn(data) {
|
|
341
|
+
return Buffer.from(data).toString('latin1').includes('\x1b[I');
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Whether `data` may have put the pane in copy mode: a mouse report (a wheel step up or a drag
|
|
345
|
+
* enters it, `mouse on`) or tmux's prefix key (C-b [). Typed input that follows leaves it first.
|
|
346
|
+
*/
|
|
347
|
+
export function mayEnterCopyMode(data) {
|
|
348
|
+
return data.includes(0x02) || /\x1b\[<\d+;\d+;\d+[Mm]|\x1b\[M/.test(Buffer.from(data).toString('latin1'));
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Text a person typed (or pasted) for the program in the pane: a chunk that holds a printable
|
|
352
|
+
* character and isn't an escape sequence (arrows, PgUp, function keys move through copy mode) —
|
|
353
|
+
* except a bracketed paste (`ESC [200~`…), which is text.
|
|
354
|
+
*/
|
|
355
|
+
export function isTypedText(data) {
|
|
356
|
+
if (data.length === 0)
|
|
357
|
+
return false;
|
|
358
|
+
if (data[0] === 0x1b && !Buffer.from(data).toString('latin1').startsWith('\x1b[200~'))
|
|
359
|
+
return false;
|
|
360
|
+
return data.some((byte) => byte >= 0x20 && byte !== 0x7f);
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* A gesture that scrolls (or drags in) the pane — what makes a viewer "the one who scrolled": a wheel
|
|
364
|
+
* step or a drag (an SGR or X10 mouse report with button 32 and up), or the prefix key. A plain
|
|
365
|
+
* click is not one.
|
|
366
|
+
*/
|
|
367
|
+
export function isScrollGesture(data) {
|
|
368
|
+
if (data.includes(0x02))
|
|
369
|
+
return true;
|
|
370
|
+
const text = Buffer.from(data).toString('latin1');
|
|
371
|
+
for (const match of text.matchAll(/\x1b\[<(\d+);\d+;\d+[Mm]/g))
|
|
372
|
+
if (Number(match[1]) >= 32)
|
|
373
|
+
return true;
|
|
374
|
+
for (const match of text.matchAll(/\x1b\[M([\s\S])[\s\S]{2}/g))
|
|
375
|
+
if (match[1].charCodeAt(0) - 32 >= 32)
|
|
376
|
+
return true;
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* The keys a person presses to leave copy mode (q, Esc, Ctrl+C): typed while the pane was in it,
|
|
381
|
+
* they only leave it, as they did before — never reach the agent as a keystroke or an interrupt.
|
|
382
|
+
*/
|
|
383
|
+
export function isCopyModeExitKey(data) {
|
|
384
|
+
return data.length === 1 && (data[0] === 0x71 || data[0] === 0x1b || data[0] === 0x03);
|
|
385
|
+
}
|
|
386
|
+
/** `1` when the window's pane is in a mode (copy mode, after a scroll back). */
|
|
387
|
+
export function paneInModeArgs(name) {
|
|
388
|
+
return ['display-message', '-p', '-t', windowTarget(name), '#{pane_in_mode}'];
|
|
389
|
+
}
|
|
390
|
+
/** Leave copy mode, if the pane is in it, before relayed text is typed (it would be read as copy-mode keys). */
|
|
391
|
+
export function leaveCopyModeArgs(name) {
|
|
392
|
+
const target = windowTarget(name);
|
|
393
|
+
return ['if-shell', '-F', '-t', target, '#{pane_in_mode}', `send-keys -X -t ${target} cancel`];
|
|
394
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -22,6 +22,11 @@ interface ParsedArgs {
|
|
|
22
22
|
flags: Record<string, string | boolean>;
|
|
23
23
|
}
|
|
24
24
|
export declare function parseArgs(argv: readonly string[]): ParsedArgs;
|
|
25
|
+
/**
|
|
26
|
+
* The `hook` command's arguments as the hook reads them: its positionals, and
|
|
27
|
+
* the PermissionRequest hook's `--wait` (which `parseArgs` keeps as a flag).
|
|
28
|
+
*/
|
|
29
|
+
export declare function hookArgs({ subcommand, args, flags }: ParsedArgs): string[];
|
|
25
30
|
declare function main(argv: readonly string[]): Promise<number>;
|
|
26
31
|
export { main };
|
|
27
32
|
/**
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@ import process from 'node:process';
|
|
|
18
18
|
import { fileURLToPath } from 'node:url';
|
|
19
19
|
import { endpointProblem } from './agent/base-url.js';
|
|
20
20
|
import { runAgent } from './agent/command.js';
|
|
21
|
+
import { runHook } from './codehost/hooks.js';
|
|
21
22
|
import { DEFAULT_WS_URL, runListen } from './listen.js';
|
|
22
23
|
import { runUsage } from './usage/command.js';
|
|
23
24
|
import { CLI_VERSION } from './version.js';
|
|
@@ -31,12 +32,17 @@ function printHelp() {
|
|
|
31
32
|
' [--webhook-url <https-url>] [--base-url <url>]',
|
|
32
33
|
" prohost agent run --exec '<command>' [--concurrency <n>] [--repos <checkout,…>] [--idle-timeout <seconds>]",
|
|
33
34
|
' [--timeout <seconds>] [--dry-run] [--workdir <dir>]',
|
|
35
|
+
' prohost agent run --host-only [--repos <checkout,…>] [--machine <name>] Host Code threads only; run nothing',
|
|
34
36
|
" prohost agent install-daemon --exec '<command>' [--workdir <dir>] [--account <label>] Keep the agent running",
|
|
37
|
+
' prohost agent install-daemon --host-only [--machine <name>]',
|
|
35
38
|
' prohost agent uninstall-daemon',
|
|
36
39
|
' prohost agent account add <label> --runtime claude|codex [--billing subscription|api_key]',
|
|
37
40
|
' prohost agent account list | remove <label>',
|
|
38
41
|
' prohost agent list Every paired agent on this machine, and its accounts',
|
|
39
42
|
' prohost agent workspace init --developer --repo <checkout> [--workdir <dir>]',
|
|
43
|
+
' prohost agent code-host status | enable [--allow-remote-terminal] | remote on|off | disable',
|
|
44
|
+
' Host Code threads (agent terminals) on this Mac; off by default',
|
|
45
|
+
' prohost agent code-host folders add <path> | list [--json] | remove <path>',
|
|
40
46
|
' prohost usage connect [--key -] [--base-url <url>] Report AI usage without pairing an agent (prompts for the key; --key - reads it from stdin)',
|
|
41
47
|
' prohost usage discover [--dry-run] | start [--once] [--machine <name>] | status',
|
|
42
48
|
' prohost usage install-daemon [--machine <name>] | uninstall-daemon',
|
|
@@ -73,6 +79,9 @@ function printHelp() {
|
|
|
73
79
|
' its own `git worktree` of every one under $PROHOST_HOME/worktrees, kept for',
|
|
74
80
|
' the life of the thread and removed after 14 idle days if it is clean. The',
|
|
75
81
|
' agent is told to work there and to leave the originals alone.',
|
|
82
|
+
' --host-only Instead of --exec: host Code threads on this machine (terminals you open',
|
|
83
|
+
' from ProhostAI) and execute nothing anyone sends. Every run requested of',
|
|
84
|
+
' this agent — a mention, a DM — is closed at once as refused.',
|
|
76
85
|
' --dry-run Print the reply that would be posted; write nothing.',
|
|
77
86
|
' --workdir Working directory for the agent command. Defaults to $PROHOST_HOME/workspace.',
|
|
78
87
|
' Avoid pointing it at a code checkout: agents read CLAUDE.md, .claude/settings.json',
|
|
@@ -183,6 +192,14 @@ export function parseArgs(argv) {
|
|
|
183
192
|
}
|
|
184
193
|
return { command, subcommand, args, flags };
|
|
185
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* The `hook` command's arguments as the hook reads them: its positionals, and
|
|
197
|
+
* the PermissionRequest hook's `--wait` (which `parseArgs` keeps as a flag).
|
|
198
|
+
*/
|
|
199
|
+
export function hookArgs({ subcommand, args, flags }) {
|
|
200
|
+
const wait = flags.wait;
|
|
201
|
+
return [subcommand ?? '', ...args, ...(typeof wait === 'string' ? ['--wait', wait] : [])];
|
|
202
|
+
}
|
|
186
203
|
async function main(argv) {
|
|
187
204
|
const { command, subcommand, args, flags } = parseArgs(argv);
|
|
188
205
|
// Checked before the help/no-command branch: `prohost --version` carries no
|
|
@@ -209,6 +226,10 @@ async function main(argv) {
|
|
|
209
226
|
if (command === 'usage') {
|
|
210
227
|
return runUsage(subcommand, flags, printHelp);
|
|
211
228
|
}
|
|
229
|
+
// Run by the Code-thread hook shim (codehost/hooks.ts), not by people; not in --help.
|
|
230
|
+
if (command === 'hook') {
|
|
231
|
+
return runHook(hookArgs({ command, subcommand, args, flags }));
|
|
232
|
+
}
|
|
212
233
|
if (command !== 'listen') {
|
|
213
234
|
process.stderr.write(`Unknown command: ${command}\n\n`);
|
|
214
235
|
printHelp();
|