@prohost/cli 0.1.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/LICENSE +21 -0
- package/README.md +229 -0
- package/dist/agent/api.d.ts +45 -0
- package/dist/agent/api.js +80 -0
- package/dist/agent/command.d.ts +10 -0
- package/dist/agent/command.js +109 -0
- package/dist/agent/contract.d.ts +94 -0
- package/dist/agent/contract.js +125 -0
- package/dist/agent/credentials.d.ts +72 -0
- package/dist/agent/credentials.js +92 -0
- package/dist/agent/exec.d.ts +48 -0
- package/dist/agent/exec.js +151 -0
- package/dist/agent/pair.d.ts +44 -0
- package/dist/agent/pair.js +149 -0
- package/dist/agent/prompt.d.ts +24 -0
- package/dist/agent/prompt.js +77 -0
- package/dist/agent/run.d.ts +78 -0
- package/dist/agent/run.js +400 -0
- package/dist/agent/runstore.d.ts +50 -0
- package/dist/agent/runstore.js +107 -0
- package/dist/agent/signature.d.ts +35 -0
- package/dist/agent/signature.js +99 -0
- package/dist/forward.d.ts +39 -0
- package/dist/forward.js +68 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +216 -0
- package/dist/listen.d.ts +48 -0
- package/dist/listen.js +113 -0
- package/dist/version.d.ts +12 -0
- package/dist/version.js +12 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Prohost AI, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# `@prohost/cli`
|
|
2
|
+
|
|
3
|
+
The [ProhostAI](https://www.prohost.ai) command-line tool. Two things it does:
|
|
4
|
+
|
|
5
|
+
- **`prohost agent`** — run your own local agent as a ProhostAI teammate. Someone
|
|
6
|
+
@-mentions it in the app, it runs on your machine, and its answer is posted
|
|
7
|
+
back into the conversation.
|
|
8
|
+
- **`prohost listen`** — stream your account's webhooks to a local HTTP endpoint
|
|
9
|
+
while you develop against them (like `stripe listen`).
|
|
10
|
+
|
|
11
|
+
You need a ProhostAI account and **Node.js 18 or newer**. Nothing to install —
|
|
12
|
+
`npx` fetches it on demand:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx @prohost/cli --help # all commands and flags
|
|
16
|
+
npx @prohost/cli --version # the version you're running
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
Pair once, then leave the agent running:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# 1. In ProhostAI: Hire an AI employee → Bring your own agent → copy the code.
|
|
25
|
+
# Codes are single-use and expire 15 minutes after they're generated.
|
|
26
|
+
npx @prohost/cli agent pair --code <pairing-code>
|
|
27
|
+
|
|
28
|
+
# 2. Point it at whatever agent you already use.
|
|
29
|
+
npx @prohost/cli agent run --exec 'claude -p'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
That's it — @-mention the agent in ProhostAI and it answers from your machine.
|
|
33
|
+
The rest of this page is detail on each step.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Bring your own agent
|
|
38
|
+
|
|
39
|
+
Any agent that reads a prompt on stdin and writes its answer to stdout works —
|
|
40
|
+
Claude Code, Codex CLI, goose, or a shell script you wrote yourself. The CLI
|
|
41
|
+
doesn't know or care which one it is.
|
|
42
|
+
|
|
43
|
+
### 1. Get a pairing code
|
|
44
|
+
|
|
45
|
+
In ProhostAI: **Hire an AI employee → Bring your own agent**. You'll get a
|
|
46
|
+
one-time code. It is single-use and expires 15 minutes after it's generated.
|
|
47
|
+
|
|
48
|
+
### 2. Pair
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npx @prohost/cli agent pair --code <pairing-code>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
This exchanges the code for a scoped API key and stores it in
|
|
55
|
+
`~/.prohost/agent.json` (`chmod 600`). The key is never printed in full — only a
|
|
56
|
+
masked prefix — and the file should never be committed or copied between
|
|
57
|
+
machines; re-pair instead.
|
|
58
|
+
|
|
59
|
+
That gives you a **stream-only pairing**: events arrive over the websocket and
|
|
60
|
+
nothing is ever POSTed to you, which is what you want on a laptop with no public
|
|
61
|
+
HTTPS address. Events are still signed, and the CLI still verifies every one.
|
|
62
|
+
|
|
63
|
+
Pass `--webhook-url https://…` only if something else of yours *also* needs the
|
|
64
|
+
events pushed over HTTPS. It changes where events are delivered; it is not
|
|
65
|
+
required to receive runs.
|
|
66
|
+
|
|
67
|
+
You can also pass the code as `$PROHOST_PAIRING_CODE`.
|
|
68
|
+
|
|
69
|
+
### 3. Run
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npx @prohost/cli agent run --exec 'claude -p'
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Leave it running. On every mention you'll see:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
14:22:07 ● connected as "Ada" — waiting for mentions (exec: claude -p)
|
|
79
|
+
14:24:31 ▶ run 01JQ8Z4K2M started (conversation 9f3c…)
|
|
80
|
+
14:24:58 ✓ run 01JQ8Z4K2M replied in 26.4s (412 chars)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Other agents, same command:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npx @prohost/cli agent run --exec 'codex exec -'
|
|
87
|
+
npx @prohost/cli agent run --exec 'goose run -i -'
|
|
88
|
+
npx @prohost/cli agent run --exec './my-agent.sh'
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Try it without sending anything first:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npx @prohost/cli agent run --exec 'claude -p' --dry-run
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`--dry-run` prints the reply your agent produced and makes no writes at all —
|
|
98
|
+
nothing is posted and the run is left open.
|
|
99
|
+
|
|
100
|
+
### How a run works
|
|
101
|
+
|
|
102
|
+
1. Someone @-mentions your agent. ProhostAI emits an `agent.run_requested` event
|
|
103
|
+
on the authenticated websocket. Your machine shows up in ProhostAI as
|
|
104
|
+
connected while `agent run` holds the socket open.
|
|
105
|
+
2. The CLI verifies the event's HMAC signature. An event that doesn't verify is
|
|
106
|
+
**not executed** — the whole point of the signature is that a run event makes
|
|
107
|
+
your machine execute a command.
|
|
108
|
+
3. It builds a prompt (who your agent is, the conversation, the message that
|
|
109
|
+
mentioned it) and runs your `--exec` command with that prompt on **stdin**.
|
|
110
|
+
4. Your command's **stdout** is the reply. It's posted into the conversation
|
|
111
|
+
verbatim, so print the message and nothing else — no preamble, no reasoning.
|
|
112
|
+
5. The run is completed either way. A command that crashes, times out, or prints
|
|
113
|
+
nothing reports `failed`; a run is never left hanging.
|
|
114
|
+
|
|
115
|
+
Runs are handled one at a time, and each run executes **at most once on this
|
|
116
|
+
machine** — even across restarts. Started runs are recorded in
|
|
117
|
+
`~/.prohost/runs.json` before the command launches, so a webhook redelivery
|
|
118
|
+
that arrives after a crash or restart will close the run out rather than run
|
|
119
|
+
your agent a second time. That matters because your agent holds a credential:
|
|
120
|
+
a second execution could file the same task or send the same email again, which
|
|
121
|
+
no amount of message deduplication would undo.
|
|
122
|
+
|
|
123
|
+
**Treat mention text as untrusted input.** The prompt contains a message someone
|
|
124
|
+
else wrote, and your agent runs on your machine with whatever credentials and
|
|
125
|
+
tools you gave it. Give it the access the job needs and no more — the same
|
|
126
|
+
judgement you'd apply to any agent reading input from the internet. (The message
|
|
127
|
+
only ever reaches your command on stdin, never the shell, so it cannot inject a
|
|
128
|
+
command.)
|
|
129
|
+
|
|
130
|
+
### `agent run` flags
|
|
131
|
+
|
|
132
|
+
| Flag | Default | What it does |
|
|
133
|
+
| --- | --- | --- |
|
|
134
|
+
| `--exec` | *(required)* | Command to run for each mention. Prompt arrives on stdin. |
|
|
135
|
+
| `--timeout` | `300` | Seconds one run may take before the command is killed. |
|
|
136
|
+
| `--dry-run` | off | Print the reply that would be posted; write nothing. |
|
|
137
|
+
| `--cwd` | current dir | Working directory for the command. |
|
|
138
|
+
| `--machine` | OS hostname | Name shown next to the connection in ProhostAI. |
|
|
139
|
+
| `--url` | paired value | Websocket endpoint override. |
|
|
140
|
+
| `--allow-unverified` | off | Run events whose signature can't be verified. See below. |
|
|
141
|
+
|
|
142
|
+
### `agent pair` flags
|
|
143
|
+
|
|
144
|
+
| Flag | Default | What it does |
|
|
145
|
+
| --- | --- | --- |
|
|
146
|
+
| `--code` | `$PROHOST_PAIRING_CODE` | The one-time pairing code. |
|
|
147
|
+
| `--webhook-url` | none | Also push events to this HTTPS endpoint. Omit for stream-only, which is the normal laptop setup. |
|
|
148
|
+
| `--base-url` | `https://connect.prohost.ai` | Public API base. |
|
|
149
|
+
|
|
150
|
+
### About `--allow-unverified`
|
|
151
|
+
|
|
152
|
+
Every pairing gets a signing secret, including a stream-only one, so
|
|
153
|
+
verification works out of the box. If a pairing somehow has no secret, the CLI
|
|
154
|
+
refuses to execute run events rather than running an unauthenticated command,
|
|
155
|
+
and says so. `--allow-unverified` opts out of that check — reasonable when
|
|
156
|
+
you're pointing `--url` at a local test server, a bad idea anywhere your agent
|
|
157
|
+
has real credentials.
|
|
158
|
+
|
|
159
|
+
### Troubleshooting
|
|
160
|
+
|
|
161
|
+
| You see | What it means |
|
|
162
|
+
| --- | --- |
|
|
163
|
+
| `No paired agent found` | Run `prohost agent pair --code <code>` first. |
|
|
164
|
+
| `no event subscription was provisioned` | Pairing half-failed server-side and nothing was saved. The code is spent — generate a fresh one and pair again. |
|
|
165
|
+
| `Pairing code is invalid, expired, or already used` | Codes are single-use with a 15-minute life. Generate a fresh one. |
|
|
166
|
+
| `auth_error: invalid_api_key` | The connection was revoked in ProhostAI. Re-pair. |
|
|
167
|
+
| `auth_error: insufficient_scope` | The credential isn't allowed to stream events — ask your ProhostAI admin to re-issue the pairing. |
|
|
168
|
+
| `auth_error: stream_subscription_unavailable` | The connection has no event subscription to stream. Re-pair. |
|
|
169
|
+
| `signature did not verify` | The event didn't come from ProhostAI, or your stored secret is stale. Re-pair. |
|
|
170
|
+
| Replies contain your agent's reasoning | Your command is printing more than the message. Use a "print only the answer" flag (e.g. `claude -p`). |
|
|
171
|
+
|
|
172
|
+
Credentials live in `~/.prohost/agent.json` and the run ledger in
|
|
173
|
+
`~/.prohost/runs.json`; set `$PROHOST_HOME` to move that directory. Delete the file to un-pair locally, and revoke the connection in
|
|
174
|
+
ProhostAI to invalidate the key itself.
|
|
175
|
+
|
|
176
|
+
### What the pairing can and can't do
|
|
177
|
+
|
|
178
|
+
The credential is deliberately narrow. It can stream events, post in internal
|
|
179
|
+
team chat, manage its own task queue, file approvals, and close out its own
|
|
180
|
+
runs. It **cannot** create or delete webhook subscriptions, and it cannot send
|
|
181
|
+
to guest channels (OTA, SMS, email, WhatsApp) — a guest-facing send has to go
|
|
182
|
+
through ProhostAI's own approval path, not around it via an API key.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Local webhook forwarding
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
npx @prohost/cli listen --forward-to http://localhost:3000/webhook
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Streams your account's webhook deliveries over an authenticated websocket and
|
|
193
|
+
POSTs each payload to your local URL, preserving the `X-ProhostAI-*` headers so
|
|
194
|
+
your handler sees the same shape as a production delivery.
|
|
195
|
+
|
|
196
|
+
| Flag | Default | What it does |
|
|
197
|
+
| --- | --- | --- |
|
|
198
|
+
| `--forward-to` | *(required)* | Local URL each payload is POSTed to. |
|
|
199
|
+
| `--api-key` | `$PROHOST_API_KEY` | ProhostAI API key (`ph_live_…`). |
|
|
200
|
+
| `--url` | `wss://api.prohost.ai/ws/webhooks` | Websocket endpoint. |
|
|
201
|
+
| `--machine` | OS hostname | Name displayed in the UI. |
|
|
202
|
+
| `--subscription-id` | none | Stamp `connected_from` on a specific subscription. |
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Support
|
|
207
|
+
|
|
208
|
+
Email [support@prohost.ai](mailto:support@prohost.ai). Include the output of
|
|
209
|
+
`npx @prohost/cli --version`.
|
|
210
|
+
|
|
211
|
+
The source lives in ProhostAI's (private) backend monorepo under
|
|
212
|
+
`packages/prohost-cli`; there is no public issue tracker.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Development
|
|
217
|
+
|
|
218
|
+
For ProhostAI engineers working in the monorepo:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
bun install # from the repo root
|
|
222
|
+
bun test # package test suite
|
|
223
|
+
npm run lint # tsc --noEmit
|
|
224
|
+
npm run build # emit dist/
|
|
225
|
+
npm pack # build + tarball, exactly as published
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
`prepack` runs the build, so `npm pack` and `npm publish` can never ship a
|
|
229
|
+
stale or missing `dist/`.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two HTTP writes the harness makes back into ProhostAI: post the reply,
|
|
3
|
+
* then close out the run.
|
|
4
|
+
*
|
|
5
|
+
* Both authenticate with the paired API key over ``X-API-Key`` and both return
|
|
6
|
+
* a result object rather than throwing — the runner has to keep going (and
|
|
7
|
+
* still complete the run) whichever one fails.
|
|
8
|
+
*
|
|
9
|
+
* The paths come from the run event itself (`reply_instructions.path` /
|
|
10
|
+
* `completion.path`), which is why these take a path rather than building one.
|
|
11
|
+
*/
|
|
12
|
+
export interface ApiOptions {
|
|
13
|
+
baseUrl: string;
|
|
14
|
+
apiKey: string;
|
|
15
|
+
fetchImpl?: typeof fetch;
|
|
16
|
+
/** Per-request timeout. Defaults to 30 seconds. */
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface ApiResult {
|
|
20
|
+
ok: boolean;
|
|
21
|
+
status: number;
|
|
22
|
+
error?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Post the agent's reply into the conversation that triggered the run.
|
|
26
|
+
*
|
|
27
|
+
* Keyed on ``run_id`` so the send is idempotent server-side. The harness dedupes
|
|
28
|
+
* runs in memory, but that memory dies with the process: without this header a
|
|
29
|
+
* redelivered run after a restart would post the reply a second time.
|
|
30
|
+
*/
|
|
31
|
+
export declare function postReply(options: ApiOptions, path: string, bodyKey: string, message: string, runId: string): Promise<ApiResult>;
|
|
32
|
+
/**
|
|
33
|
+
* Report a run's terminal state. Called for every run — including one whose
|
|
34
|
+
* command crashed — so the server never waits on a run this harness abandoned.
|
|
35
|
+
*
|
|
36
|
+
* A ``409`` is reported as success: it means the run already reached a terminal
|
|
37
|
+
* state (a redelivery we raced, or the 2h stale-run sweeper got there first).
|
|
38
|
+
* Treating that as a failure would only produce noise about work that is done.
|
|
39
|
+
*/
|
|
40
|
+
export declare function completeRun(options: ApiOptions, path: string, outcome: {
|
|
41
|
+
ok: true;
|
|
42
|
+
} | {
|
|
43
|
+
ok: false;
|
|
44
|
+
error: string;
|
|
45
|
+
}): Promise<ApiResult>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two HTTP writes the harness makes back into ProhostAI: post the reply,
|
|
3
|
+
* then close out the run.
|
|
4
|
+
*
|
|
5
|
+
* Both authenticate with the paired API key over ``X-API-Key`` and both return
|
|
6
|
+
* a result object rather than throwing — the runner has to keep going (and
|
|
7
|
+
* still complete the run) whichever one fails.
|
|
8
|
+
*
|
|
9
|
+
* The paths come from the run event itself (`reply_instructions.path` /
|
|
10
|
+
* `completion.path`), which is why these take a path rather than building one.
|
|
11
|
+
*/
|
|
12
|
+
import { USER_AGENT } from '../version.js';
|
|
13
|
+
import { MAX_ERROR_CHARS, RUN_STATUS_FAILED, RUN_STATUS_SUCCEEDED } from './contract.js';
|
|
14
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
15
|
+
async function post(options, path, body, idempotencyKey) {
|
|
16
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
17
|
+
const controller = new AbortController();
|
|
18
|
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
19
|
+
try {
|
|
20
|
+
const response = await fetchImpl(`${options.baseUrl.replace(/\/+$/, '')}${path}`, {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
headers: {
|
|
23
|
+
'Content-Type': 'application/json',
|
|
24
|
+
'X-API-Key': options.apiKey,
|
|
25
|
+
'User-Agent': USER_AGENT,
|
|
26
|
+
...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}),
|
|
27
|
+
},
|
|
28
|
+
body: JSON.stringify(body),
|
|
29
|
+
signal: controller.signal,
|
|
30
|
+
});
|
|
31
|
+
const ok = response.status >= 200 && response.status < 300;
|
|
32
|
+
let error;
|
|
33
|
+
if (!ok) {
|
|
34
|
+
// Surface the server's reason — a scope or tier denial is the most
|
|
35
|
+
// likely failure here and the host can act on it.
|
|
36
|
+
error = await response
|
|
37
|
+
.text()
|
|
38
|
+
.then((t) => t.slice(0, 500))
|
|
39
|
+
.catch(() => undefined);
|
|
40
|
+
}
|
|
41
|
+
return { ok, status: response.status, error };
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
status: 0,
|
|
47
|
+
error: err instanceof Error ? err.message : String(err),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
clearTimeout(timer);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Post the agent's reply into the conversation that triggered the run.
|
|
56
|
+
*
|
|
57
|
+
* Keyed on ``run_id`` so the send is idempotent server-side. The harness dedupes
|
|
58
|
+
* runs in memory, but that memory dies with the process: without this header a
|
|
59
|
+
* redelivered run after a restart would post the reply a second time.
|
|
60
|
+
*/
|
|
61
|
+
export async function postReply(options, path, bodyKey, message, runId) {
|
|
62
|
+
return post(options, path, { [bodyKey]: message }, `prohost-cli-run-${runId}`);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Report a run's terminal state. Called for every run — including one whose
|
|
66
|
+
* command crashed — so the server never waits on a run this harness abandoned.
|
|
67
|
+
*
|
|
68
|
+
* A ``409`` is reported as success: it means the run already reached a terminal
|
|
69
|
+
* state (a redelivery we raced, or the 2h stale-run sweeper got there first).
|
|
70
|
+
* Treating that as a failure would only produce noise about work that is done.
|
|
71
|
+
*/
|
|
72
|
+
export async function completeRun(options, path, outcome) {
|
|
73
|
+
const body = outcome.ok
|
|
74
|
+
? { status: RUN_STATUS_SUCCEEDED }
|
|
75
|
+
: { status: RUN_STATUS_FAILED, error: outcome.error.slice(0, MAX_ERROR_CHARS) };
|
|
76
|
+
const result = await post(options, path, body);
|
|
77
|
+
if (result.status === 409)
|
|
78
|
+
return { ...result, ok: true };
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flag plumbing for `prohost agent <pair|run>`.
|
|
3
|
+
*
|
|
4
|
+
* Kept separate from the harness itself so `pair.ts` / `run.ts` stay callable
|
|
5
|
+
* (and testable) as plain async functions with no argv or process concerns.
|
|
6
|
+
*/
|
|
7
|
+
type Flags = Record<string, string | boolean>;
|
|
8
|
+
/** Dispatch an ``agent`` subcommand. Returns the process exit code. */
|
|
9
|
+
export declare function runAgent(subcommand: string | undefined, flags: Flags, printHelp: () => void): Promise<number>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flag plumbing for `prohost agent <pair|run>`.
|
|
3
|
+
*
|
|
4
|
+
* Kept separate from the harness itself so `pair.ts` / `run.ts` stay callable
|
|
5
|
+
* (and testable) as plain async functions with no argv or process concerns.
|
|
6
|
+
*/
|
|
7
|
+
import process from 'node:process';
|
|
8
|
+
import { PairError, runPair } from './pair.js';
|
|
9
|
+
import { CredentialsMissingError, loadCredentials } from './credentials.js';
|
|
10
|
+
import { DEFAULT_EXEC_TIMEOUT_MS, runAgentHarness } from './run.js';
|
|
11
|
+
function flagString(flags, name) {
|
|
12
|
+
const value = flags[name];
|
|
13
|
+
return typeof value === 'string' ? value : undefined;
|
|
14
|
+
}
|
|
15
|
+
/** Dispatch an ``agent`` subcommand. Returns the process exit code. */
|
|
16
|
+
export async function runAgent(subcommand, flags, printHelp) {
|
|
17
|
+
if (subcommand === 'pair')
|
|
18
|
+
return pairCommand(flags);
|
|
19
|
+
if (subcommand === 'run')
|
|
20
|
+
return runCommand(flags);
|
|
21
|
+
process.stderr.write(subcommand
|
|
22
|
+
? `Unknown agent subcommand: ${subcommand}\n\n`
|
|
23
|
+
: 'Missing agent subcommand — expected `pair` or `run`.\n\n');
|
|
24
|
+
printHelp();
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
async function pairCommand(flags) {
|
|
28
|
+
const code = flagString(flags, 'code') ?? process.env.PROHOST_PAIRING_CODE;
|
|
29
|
+
if (!code) {
|
|
30
|
+
process.stderr.write('Missing pairing code. Pass --code or set $PROHOST_PAIRING_CODE.\n');
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
await runPair({
|
|
35
|
+
code,
|
|
36
|
+
baseUrl: flagString(flags, 'base-url'),
|
|
37
|
+
wsUrl: flagString(flags, 'url') ?? process.env.PROHOST_WS_URL,
|
|
38
|
+
webhookUrl: flagString(flags, 'webhook-url'),
|
|
39
|
+
});
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
if (err instanceof PairError) {
|
|
44
|
+
process.stderr.write(`✗ ${err.message}\n`);
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function runCommand(flags) {
|
|
51
|
+
const exec = flagString(flags, 'exec');
|
|
52
|
+
if (!exec) {
|
|
53
|
+
process.stderr.write("Missing required flag: --exec. Example: prohost agent run --exec 'claude -p'\n");
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
let credentials;
|
|
57
|
+
try {
|
|
58
|
+
credentials = loadCredentials();
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
if (err instanceof CredentialsMissingError) {
|
|
62
|
+
process.stderr.write(`✗ ${err.message}\n`);
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
const timeoutSeconds = flagString(flags, 'timeout');
|
|
68
|
+
const parsedTimeout = timeoutSeconds === undefined ? undefined : Number(timeoutSeconds);
|
|
69
|
+
if (parsedTimeout !== undefined && (!Number.isFinite(parsedTimeout) || parsedTimeout <= 0)) {
|
|
70
|
+
process.stderr.write('--timeout must be a positive number of seconds.\n');
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
73
|
+
// First signal stops accepting work and lets the in-flight run finish
|
|
74
|
+
// reporting itself (so the server doesn't wait on a run we abandoned); a
|
|
75
|
+
// second one means the operator wants out now.
|
|
76
|
+
const controller = new AbortController();
|
|
77
|
+
const onSig = () => {
|
|
78
|
+
if (controller.signal.aborted) {
|
|
79
|
+
process.stdout.write('\nQuitting — the in-flight run was not reported.\n');
|
|
80
|
+
process.exit(130);
|
|
81
|
+
}
|
|
82
|
+
controller.abort();
|
|
83
|
+
process.stdout.write('\nFinishing the in-flight run… press Ctrl-C again to quit immediately.\n');
|
|
84
|
+
};
|
|
85
|
+
process.on('SIGINT', onSig);
|
|
86
|
+
process.on('SIGTERM', onSig);
|
|
87
|
+
try {
|
|
88
|
+
const metrics = await runAgentHarness({
|
|
89
|
+
exec,
|
|
90
|
+
credentials,
|
|
91
|
+
url: flagString(flags, 'url') ?? process.env.PROHOST_WS_URL,
|
|
92
|
+
machine: flagString(flags, 'machine'),
|
|
93
|
+
cwd: flagString(flags, 'cwd'),
|
|
94
|
+
timeoutMs: parsedTimeout === undefined ? DEFAULT_EXEC_TIMEOUT_MS : parsedTimeout * 1000,
|
|
95
|
+
dryRun: flags['dry-run'] === true,
|
|
96
|
+
allowUnverified: flags['allow-unverified'] === true,
|
|
97
|
+
signal: controller.signal,
|
|
98
|
+
});
|
|
99
|
+
process.stdout.write(`\nRuns: ${metrics.runsSucceeded} succeeded, ${metrics.runsFailed} failed, ` +
|
|
100
|
+
`${metrics.signatureRejections} rejected.\n`);
|
|
101
|
+
// A harness that never connected must not look like a clean shutdown to a
|
|
102
|
+
// service manager or a wrapper script.
|
|
103
|
+
return metrics.fatal === undefined ? 0 : 1;
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
process.off('SIGINT', onSig);
|
|
107
|
+
process.off('SIGTERM', onSig);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire contract between this CLI and ProhostAI's agent-run loop.
|
|
3
|
+
*
|
|
4
|
+
* Everything the server owns — event names, HTTP paths, run-status values —
|
|
5
|
+
* lives here and nowhere else, so a server-side rename is a one-line change
|
|
6
|
+
* in this file rather than a hunt through the runner.
|
|
7
|
+
*
|
|
8
|
+
* The `agent.run_requested` payload spells out its own reply and completion
|
|
9
|
+
* paths precisely so a harness doesn't hand-derive them, and this module
|
|
10
|
+
* prefers those server-supplied paths. The constants below are the fallback
|
|
11
|
+
* for a payload that omits them.
|
|
12
|
+
*
|
|
13
|
+
* Server-side sources of truth:
|
|
14
|
+
* - payload shape — `shared/events/payloads/agent_runs.py`
|
|
15
|
+
* - `POST /v1/agent-runs/{id}/complete` — `api_public/routers/agent_runs.py`
|
|
16
|
+
* - `POST /v1/agent-pairing/redeem` — `api_public/routers/agent_pairing.py`
|
|
17
|
+
* - `POST /v1/conversations/{id}/messages` — `api_public/routers/conversations.py`
|
|
18
|
+
*/
|
|
19
|
+
/** Default host for the public API (`api_public`, mounted under `/v1`). */
|
|
20
|
+
export declare const DEFAULT_API_BASE_URL = "https://connect.prohost.ai";
|
|
21
|
+
/** Fired when a paired agent is given work — the trigger to run. */
|
|
22
|
+
export declare const EVENT_AGENT_RUN_REQUESTED = "agent.run_requested";
|
|
23
|
+
/** Informational events the harness logs but does not act on. */
|
|
24
|
+
export declare const EVENT_MENTION_CREATED = "mention.created";
|
|
25
|
+
export declare const EVENT_MESSAGE_TEAM_CHAT = "message.team_chat";
|
|
26
|
+
/** Terminal statuses accepted by the run-completion endpoint. */
|
|
27
|
+
export declare const RUN_STATUS_SUCCEEDED = "succeeded";
|
|
28
|
+
export declare const RUN_STATUS_FAILED = "failed";
|
|
29
|
+
/** Server-side cap on the `error` field of a completion (`MAX_ERROR_CHARS`). */
|
|
30
|
+
export declare const MAX_ERROR_CHARS = 1000;
|
|
31
|
+
/** `POST` — exchange a one-time pairing code for a scoped credential. */
|
|
32
|
+
export declare const REDEEM_PATH = "/v1/agent-pairing/redeem";
|
|
33
|
+
/** `POST` — send a message into a conversation as the paired agent. */
|
|
34
|
+
export declare function conversationMessagesPath(conversationId: string): string;
|
|
35
|
+
/** `POST` — close out a run. Must be called for every run, success or not. */
|
|
36
|
+
export declare function agentRunCompletePath(runId: string): string;
|
|
37
|
+
/** The `agent.run_requested` payload, as far as the harness cares about it. */
|
|
38
|
+
export interface AgentRunRequest {
|
|
39
|
+
run_id: string;
|
|
40
|
+
agent_id?: string;
|
|
41
|
+
agent_user_id?: string;
|
|
42
|
+
agent_name?: string;
|
|
43
|
+
account_id?: string;
|
|
44
|
+
/** Why the agent woke — `team_chat_mention`, `task_comment`, … */
|
|
45
|
+
kind?: string;
|
|
46
|
+
conversation_id?: string;
|
|
47
|
+
task_id?: string;
|
|
48
|
+
cleaning_id?: string;
|
|
49
|
+
trigger_source?: string;
|
|
50
|
+
trigger_message_id?: string;
|
|
51
|
+
/** Capped inline excerpt of the message that woke the agent. */
|
|
52
|
+
trigger_text?: string;
|
|
53
|
+
trigger_user_id?: string;
|
|
54
|
+
/** For a trigger fan-out with no surface: the event type that fired it. */
|
|
55
|
+
trigger_type?: string;
|
|
56
|
+
/** For a trigger fan-out with no surface: the entity that event was about. */
|
|
57
|
+
trigger_entity_id?: string;
|
|
58
|
+
/**
|
|
59
|
+
* What the run is about, exactly as the server declared it. This is NOT the
|
|
60
|
+
* same question as "can the reply be posted": a task run is a task run even
|
|
61
|
+
* though the external API has no task-comment route yet, and the agent needs
|
|
62
|
+
* to be told that to answer sensibly. Postability is `reply_path` +
|
|
63
|
+
* `reply_body_key`.
|
|
64
|
+
*/
|
|
65
|
+
reply_surface: ReplySurface;
|
|
66
|
+
/** Server-supplied path to post the reply to. Absent when there is nowhere. */
|
|
67
|
+
reply_path?: string;
|
|
68
|
+
/**
|
|
69
|
+
* JSON field the reply text goes in — taken from the event's
|
|
70
|
+
* ``reply_instructions.body_field`` when present. Conversations take
|
|
71
|
+
* ``message``; comment surfaces take ``content``
|
|
72
|
+
* (`shared/comments/schemas.py`).
|
|
73
|
+
*/
|
|
74
|
+
reply_body_key?: string;
|
|
75
|
+
/** Server-supplied path to complete the run at. */
|
|
76
|
+
completion_path: string;
|
|
77
|
+
}
|
|
78
|
+
/** Reply destinations the server can bind a run to, including future surfaces. */
|
|
79
|
+
export type ReplySurface = 'conversation' | 'task' | 'cleaning' | 'none' | (string & {});
|
|
80
|
+
/**
|
|
81
|
+
* Extract the event body from a websocket frame's payload.
|
|
82
|
+
*
|
|
83
|
+
* Deliveries arrive wrapped in the outbound-webhook envelope
|
|
84
|
+
* (`{id, event, created_at, schema_version, data}`); the fields we want live
|
|
85
|
+
* in `data`. Frames without an envelope (the server's `echo` test frame) are
|
|
86
|
+
* passed through unchanged.
|
|
87
|
+
*/
|
|
88
|
+
export declare function eventData(payload: Record<string, unknown>): Record<string, unknown>;
|
|
89
|
+
/**
|
|
90
|
+
* Validate an `agent.run_requested` body. Returns ``null`` when the frame is
|
|
91
|
+
* missing a ``run_id`` — without one there is no run to complete, so the only
|
|
92
|
+
* safe move is to ignore it rather than execute an unattributable prompt.
|
|
93
|
+
*/
|
|
94
|
+
export declare function parseRunRequest(data: Record<string, unknown>): AgentRunRequest | null;
|