@prohost/cli 0.1.0 → 0.1.2

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 ADDED
@@ -0,0 +1,40 @@
1
+ # Changelog
2
+
3
+ Versions follow [semver](https://semver.org/). Publishing is automated: merging
4
+ a version bump to `main` triggers `.github/workflows/npm-publish-cli.yml`, which
5
+ builds via `prepack`, runs the suite, publishes, and tags `cli-v<version>`.
6
+
7
+ ## 0.1.2
8
+
9
+ The release that carries 0.1.1's fix to users. 0.1.1 was tagged in the monorepo
10
+ but never reached npm — there was no publish pipeline — so every `npx
11
+ @prohost/cli` install was still resolving 0.1.0 and still 403-looping after
12
+ pairing. This version exists so the fix ships.
13
+
14
+ - **Stale v1 credential files heal themselves.** `~/.prohost/agent.json` written
15
+ by 0.1.0 records the wrong websocket host, and the stored URL wins over any
16
+ client-side default — so upgrading the CLI alone would not have unbroken those
17
+ installs. `CREDENTIALS_VERSION` is now `2`; a v1 file is recognised as
18
+ untrustworthy and its socket URL is re-derived from `base_url` (which the
19
+ server did report, and which is correct even in a v1 file). Already-paired
20
+ agents recover on the next `agent run` without re-pairing.
21
+ - Added the publish workflow itself, so a version bump on `main` is the whole
22
+ release process.
23
+
24
+ ## 0.1.1
25
+
26
+ Never published to npm — superseded by 0.1.2.
27
+
28
+ - **The websocket host comes from the server.** Pairing now stores the `ws_url`
29
+ the server reports instead of composing one from a hardcoded constant. 0.1.0's
30
+ constant named a host that rejects the pairing credential, so `agent run`
31
+ connected, took a 403, and retried forever.
32
+
33
+ ## 0.1.0
34
+
35
+ First publish. Broken on `agent run` — the hardcoded `wss://` host 403-loops
36
+ after pairing. Do not use.
37
+
38
+ - `prohost agent pair` / `prohost agent run` — run a local agent as a ProhostAI
39
+ teammate.
40
+ - `prohost listen` — forward account webhooks to a local HTTP endpoint.
package/README.md CHANGED
@@ -72,10 +72,11 @@ You can also pass the code as `$PROHOST_PAIRING_CODE`.
72
72
  npx @prohost/cli agent run --exec 'claude -p'
73
73
  ```
74
74
 
75
- Leave it running. On every mention you'll see:
75
+ Leave it running. Each time ProhostAI asks this agent for work — an `@`-mention
76
+ in a team thread, a 1:1 DM, a task comment, a trigger — you'll see:
76
77
 
77
78
  ```
78
- 14:22:07 ● connected as "Ada" — waiting for mentions (exec: claude -p)
79
+ 14:22:07 ● connected as "Ada" — waiting for agent.run_requested (exec: claude -p)
79
80
  14:24:31 ▶ run 01JQ8Z4K2M started (conversation 9f3c…)
80
81
  14:24:58 ✓ run 01JQ8Z4K2M replied in 26.4s (412 chars)
81
82
  ```
@@ -131,12 +132,12 @@ command.)
131
132
 
132
133
  | Flag | Default | What it does |
133
134
  | --- | --- | --- |
134
- | `--exec` | *(required)* | Command to run for each mention. Prompt arrives on stdin. |
135
+ | `--exec` | *(required)* | Command to run for each requested run. Prompt arrives on stdin. |
135
136
  | `--timeout` | `300` | Seconds one run may take before the command is killed. |
136
137
  | `--dry-run` | off | Print the reply that would be posted; write nothing. |
137
138
  | `--cwd` | current dir | Working directory for the command. |
138
139
  | `--machine` | OS hostname | Name shown next to the connection in ProhostAI. |
139
- | `--url` | paired value | Websocket endpoint override. |
140
+ | `--url` | paired value | Websocket endpoint override. Pairing stores the URL the server reports, so you should not normally need this. |
140
141
  | `--allow-unverified` | off | Run events whose signature can't be verified. See below. |
141
142
 
142
143
  ### `agent pair` flags
@@ -197,7 +198,7 @@ your handler sees the same shape as a production delivery.
197
198
  | --- | --- | --- |
198
199
  | `--forward-to` | *(required)* | Local URL each payload is POSTed to. |
199
200
  | `--api-key` | `$PROHOST_API_KEY` | ProhostAI API key (`ph_live_…`). |
200
- | `--url` | `wss://api.prohost.ai/ws/webhooks` | Websocket endpoint. |
201
+ | `--url` | `wss://connect.prohost.ai/ws/webhooks` | Websocket endpoint. |
201
202
  | `--machine` | OS hostname | Name displayed in the UI. |
202
203
  | `--subscription-id` | none | Stamp `connected_from` on a specific subscription. |
203
204
 
@@ -208,6 +209,10 @@ your handler sees the same shape as a production delivery.
208
209
  Email [support@prohost.ai](mailto:support@prohost.ai). Include the output of
209
210
  `npx @prohost/cli --version`.
210
211
 
212
+ What changed between versions is in `CHANGELOG.md`, shipped alongside this file
213
+ in the package. **If `--version` reports `0.1.0`, upgrade** — that build cannot
214
+ connect after pairing.
215
+
211
216
  The source lives in ProhostAI's (private) backend monorepo under
212
217
  `packages/prohost-cli`; there is no public issue tracker.
213
218
 
@@ -39,7 +39,31 @@ export interface AgentCredentials {
39
39
  agent: StoredAgent;
40
40
  paired_at: string;
41
41
  }
42
- export declare const CREDENTIALS_VERSION = 1;
42
+ /**
43
+ * On-disk credential schema version.
44
+ *
45
+ * ``2`` means "``ws_url`` is trustworthy". Version ``1`` files were written by
46
+ * a CLI that composed that URL from a hardcoded constant, and 0.1.0 shipped a
47
+ * constant naming the wrong host — so a v1 file may carry a socket URL that can
48
+ * only ever 403. Upgrading the CLI alone would not have helped those installs:
49
+ * {@link streamUrlFor} prefers the stored value, so the bad URL would keep
50
+ * being used until the operator re-paired. The version bump is what lets a new
51
+ * build recognise a stale file and re-derive instead.
52
+ */
53
+ export declare const CREDENTIALS_VERSION = 2;
54
+ /**
55
+ * Websocket URL to use for a stored credential.
56
+ *
57
+ * For a current (v2+) file this is just ``ws_url`` — the value the server
58
+ * reported at pair time. For a v1 file the stored URL is not trusted: it came
59
+ * from a client-side constant, and 0.1.0's was wrong. It is re-derived from
60
+ * ``base_url``, which the server did report (via ``mcp_url``) and which is
61
+ * therefore right even in a v1 file.
62
+ *
63
+ * @param credentials Stored credentials.
64
+ * @param fallback Used when neither the stored URL nor ``base_url`` is usable.
65
+ */
66
+ export declare function streamUrlFor(credentials: AgentCredentials, fallback: string): string;
43
67
  /** Directory holding CLI state. Overridable with ``$PROHOST_HOME`` for tests. */
44
68
  export declare function prohostHome(env?: NodeJS.ProcessEnv): string;
45
69
  /** Absolute path of the credential file. */
@@ -8,7 +8,61 @@
8
8
  import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
9
9
  import os from 'node:os';
10
10
  import path from 'node:path';
11
- export const CREDENTIALS_VERSION = 1;
11
+ /**
12
+ * On-disk credential schema version.
13
+ *
14
+ * ``2`` means "``ws_url`` is trustworthy". Version ``1`` files were written by
15
+ * a CLI that composed that URL from a hardcoded constant, and 0.1.0 shipped a
16
+ * constant naming the wrong host — so a v1 file may carry a socket URL that can
17
+ * only ever 403. Upgrading the CLI alone would not have helped those installs:
18
+ * {@link streamUrlFor} prefers the stored value, so the bad URL would keep
19
+ * being used until the operator re-paired. The version bump is what lets a new
20
+ * build recognise a stale file and re-derive instead.
21
+ */
22
+ export const CREDENTIALS_VERSION = 2;
23
+ /**
24
+ * Websocket URL to use for a stored credential.
25
+ *
26
+ * For a current (v2+) file this is just ``ws_url`` — the value the server
27
+ * reported at pair time. For a v1 file the stored URL is not trusted: it came
28
+ * from a client-side constant, and 0.1.0's was wrong. It is re-derived from
29
+ * ``base_url``, which the server did report (via ``mcp_url``) and which is
30
+ * therefore right even in a v1 file.
31
+ *
32
+ * @param credentials Stored credentials.
33
+ * @param fallback Used when neither the stored URL nor ``base_url`` is usable.
34
+ */
35
+ export function streamUrlFor(credentials, fallback) {
36
+ if ((credentials.version ?? 1) >= 2 && credentials.ws_url)
37
+ return credentials.ws_url;
38
+ // Truthiness, not `??`: a hand-edited or truncated file can hold `""`, which
39
+ // is not a URL but is not nullish either, so `??` would return it.
40
+ return wsUrlFromBaseUrl(credentials.base_url) || credentials.ws_url || fallback;
41
+ }
42
+ /**
43
+ * Turn an API base URL into the websocket URL on the same host.
44
+ *
45
+ * Mirrors the server's derivation (`shared/agents/connections.py`
46
+ * ``pairing_endpoints``): only plain ``http`` maps to ``ws``, so a local dev
47
+ * base keeps working and nothing else can downgrade a socket that carries an
48
+ * API key. Returns ``undefined`` for anything not parseable as a URL.
49
+ */
50
+ function wsUrlFromBaseUrl(baseUrl) {
51
+ if (!baseUrl)
52
+ return undefined;
53
+ let parsed;
54
+ try {
55
+ parsed = new URL(baseUrl);
56
+ }
57
+ catch {
58
+ return undefined;
59
+ }
60
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
61
+ return undefined;
62
+ const scheme = parsed.protocol === 'http:' ? 'ws' : 'wss';
63
+ const prefix = parsed.pathname.replace(/\/+$/, '');
64
+ return `${scheme}://${parsed.host}${prefix}/ws/webhooks`;
65
+ }
12
66
  /** Directory holding CLI state. Overridable with ``$PROHOST_HOME`` for tests. */
13
67
  export function prohostHome(env = process.env) {
14
68
  return env.PROHOST_HOME ?? path.join(os.homedir(), '.prohost');
@@ -15,7 +15,11 @@ export interface PairOptions {
15
15
  code: string;
16
16
  /** Public API base. Defaults to production. */
17
17
  baseUrl?: string;
18
- /** Websocket endpoint stored for `prohost agent run`. */
18
+ /**
19
+ * Explicit websocket endpoint to store for `prohost agent run` (`--url` /
20
+ * `$PROHOST_WS_URL`). An operator override, so it beats what the server
21
+ * reports; leave it unset and the server's `ws_url` is used.
22
+ */
19
23
  wsUrl?: string;
20
24
  /**
21
25
  * HTTPS endpoint to ALSO deliver agent events to. Optional and rarely
@@ -24,6 +24,25 @@ function isUuid(value) {
24
24
  function baseFromMcpUrl(mcpUrl, fallback) {
25
25
  return mcpUrl.endsWith('/mcp') ? mcpUrl.slice(0, -'/mcp'.length) : fallback;
26
26
  }
27
+ /**
28
+ * The websocket URL the server told us to use, or ``undefined``.
29
+ *
30
+ * ``undefined`` covers both "this server predates the field" and "the value is
31
+ * not a websocket URL". A cosmetic bad value must not fail a redeem — the code
32
+ * is already spent by the time we read the body — so the caller falls back to
33
+ * {@link DEFAULT_WS_URL} and says so.
34
+ */
35
+ function serverWsUrl(value) {
36
+ if (typeof value !== 'string' || value.length === 0)
37
+ return undefined;
38
+ try {
39
+ const protocol = new URL(value).protocol;
40
+ return protocol === 'ws:' || protocol === 'wss:' ? value : undefined;
41
+ }
42
+ catch {
43
+ return undefined;
44
+ }
45
+ }
27
46
  /**
28
47
  * Redeem ``code`` and persist the resulting credentials.
29
48
  *
@@ -33,7 +52,6 @@ export async function runPair(options) {
33
52
  const fetchImpl = options.fetchImpl ?? fetch;
34
53
  const onLine = options.onLine ?? ((line) => process.stdout.write(`${line}\n`));
35
54
  const baseUrl = (options.baseUrl ?? DEFAULT_API_BASE_URL).replace(/\/+$/, '');
36
- const wsUrl = options.wsUrl ?? DEFAULT_WS_URL;
37
55
  if (options.webhookUrl && !options.webhookUrl.startsWith('https://')) {
38
56
  throw new PairError('--webhook-url must be an https:// URL');
39
57
  }
@@ -111,6 +129,16 @@ export async function runPair(options) {
111
129
  'redeem and has been removed.'
112
130
  : ' Nothing was saved.'));
113
131
  }
132
+ // Where the websocket host comes from, in order:
133
+ //
134
+ // 1. `--url` / `$PROHOST_WS_URL` — an explicit operator override, and the
135
+ // escape hatch that let the field work around 0.1.0's bad default.
136
+ // 2. The server's `ws_url`, derived server-side from the same setting as
137
+ // `mcp_url` (`shared/agents/connections.py::pairing_endpoints`). This is
138
+ // the normal path, and the reason a host move no longer needs a release.
139
+ // 3. `DEFAULT_WS_URL`, for a server that predates the field.
140
+ const fromServer = serverWsUrl(body.ws_url);
141
+ const wsUrl = options.wsUrl ?? fromServer ?? DEFAULT_WS_URL;
114
142
  const credentials = {
115
143
  version: CREDENTIALS_VERSION,
116
144
  base_url: baseFromMcpUrl(body.mcp_url ?? '', baseUrl),
@@ -129,10 +157,22 @@ export async function runPair(options) {
129
157
  onLine(` API key ${maskSecret(body.api_key)} (stored, never printed in full)`);
130
158
  onLine(` Credentials ${file} (chmod 600)`);
131
159
  onLine(` MCP URL ${body.mcp_url}`);
160
+ // Printed because 0.1.0's wrong websocket host was invisible here: pairing
161
+ // reported success and the mismatch only surfaced as a 403 reconnect loop in
162
+ // a different command. Showing it makes a bad host obvious at pair time.
163
+ onLine(` Stream URL ${credentials.ws_url}`);
132
164
  onLine(` Events ${credentials.events.join(', ') || '(none)'}`);
133
165
  onLine(` Delivery ${credentials.stream_only
134
166
  ? 'stream-only — events arrive over the websocket, nothing is POSTed to you'
135
167
  : `over the websocket, and POSTed to ${options.webhookUrl ?? 'your registered endpoint'}`}`);
168
+ if (body.ws_url && !fromServer && !options.wsUrl) {
169
+ // Present but not a ws:// or wss:// URL. Don't fail the redeem over it —
170
+ // the code is spent — but don't let the fallback look like the server's
171
+ // answer either.
172
+ onLine('');
173
+ onLine(` ! The server reported an unusable stream URL (${JSON.stringify(body.ws_url)}).`);
174
+ onLine(` Using ${DEFAULT_WS_URL} instead — override with --url if that is wrong.`);
175
+ }
136
176
  if (!credentials.webhook_secret) {
137
177
  // Without a secret the runner can't prove an event came from ProhostAI,
138
178
  // and a run event makes this machine execute a command — so it refuses to
package/dist/agent/run.js CHANGED
@@ -21,6 +21,7 @@ import os from 'node:os';
21
21
  import WebSocket from 'ws';
22
22
  import { completeRun, postReply } from './api.js';
23
23
  import { EVENT_AGENT_RUN_REQUESTED, EVENT_MENTION_CREATED, EVENT_MESSAGE_TEAM_CHAT, eventData, parseRunRequest, } from './contract.js';
24
+ import { streamUrlFor } from './credentials.js';
24
25
  import { execAgent } from './exec.js';
25
26
  import { buildPrompt, replyFromStdout } from './prompt.js';
26
27
  import { RunStore } from './runstore.js';
@@ -189,8 +190,16 @@ export async function runAgentHarness(options) {
189
190
  const Ctor = options.webSocketCtor ?? WebSocket;
190
191
  const sleep = options.sleepImpl ?? defaultSleep;
191
192
  const machine = options.machine ?? os.hostname();
192
- const url = options.url ?? options.credentials.ws_url ?? DEFAULT_WS_URL;
193
+ const url = options.url ?? streamUrlFor(options.credentials, DEFAULT_WS_URL);
193
194
  const metrics = newMetrics();
195
+ // A credential file written by 0.1.0 stores a socket URL on a host that only
196
+ // ever answers 403. Upgrading the CLI would not have fixed those installs on
197
+ // its own — the stored value wins over the constant — so say what happened
198
+ // rather than silently using a different URL than the file names.
199
+ if (!options.url && options.credentials.ws_url && url !== options.credentials.ws_url) {
200
+ log(`${timestamp()} ! stored stream URL ${options.credentials.ws_url} predates this build ` +
201
+ `and is not used; connecting to ${url} instead (re-pair to refresh the file)`);
202
+ }
194
203
  const api = {
195
204
  baseUrl: options.credentials.base_url,
196
205
  apiKey: options.credentials.api_key,
@@ -274,7 +283,14 @@ export async function runAgentHarness(options) {
274
283
  if (f.type === 'auth_ok') {
275
284
  authed = true;
276
285
  const name = options.credentials.agent?.name ?? 'agent';
277
- log(`${timestamp()} ● connected as "${name}" — waiting for mentions (exec: ${options.exec})`);
286
+ // Names the event, not "mentions". A mention is one of several things
287
+ // that make the server request a run — a 1:1 DM to this agent is
288
+ // another, and it carries no mention at all — so "waiting for
289
+ // mentions" describes a narrower trigger than the one this harness
290
+ // actually waits on, and sends anyone debugging a silent agent to
291
+ // read mention code instead of the run dispatcher.
292
+ log(`${timestamp()} ● connected as "${name}" — waiting for ` +
293
+ `${EVENT_AGENT_RUN_REQUESTED} (exec: ${options.exec})`);
278
294
  return;
279
295
  }
280
296
  if (f.type === 'auth_error') {
@@ -307,7 +323,12 @@ export async function runAgentHarness(options) {
307
323
  };
308
324
  if (frame.event !== EVENT_AGENT_RUN_REQUESTED) {
309
325
  if (frame.event === EVENT_MENTION_CREATED || frame.event === EVENT_MESSAGE_TEAM_CHAT) {
310
- log(`${timestamp()} ◦ ${frame.event} (no action — runs are triggered by mentions)`);
326
+ // "runs are triggered by mentions" was wrong twice over: the gate
327
+ // is the `agent.run_requested` event, and this very frame can BE a
328
+ // mention — so the line claimed a mention triggers a run while
329
+ // ignoring a mention. State the gate instead.
330
+ log(`${timestamp()} ◦ ${frame.event} (no action — this harness runs on ` +
331
+ `${EVENT_AGENT_RUN_REQUESTED}, which the server sends separately)`);
311
332
  }
312
333
  metrics.ignored += 1;
313
334
  ack(ACK_IGNORED);
package/dist/index.js CHANGED
@@ -29,7 +29,7 @@ function printHelp() {
29
29
  'listen flags:',
30
30
  ' --forward-to Local URL to POST each webhook payload to (required)',
31
31
  ' --api-key ProhostAI API key. Defaults to $PROHOST_API_KEY.',
32
- ' --url Websocket endpoint. Defaults to wss://api.prohost.ai/ws/webhooks.',
32
+ ' --url Websocket endpoint. Defaults to wss://connect.prohost.ai/ws/webhooks.',
33
33
  ' --machine Machine name displayed in the UI. Defaults to the OS hostname.',
34
34
  ' --subscription-id Stamp connected_from on a specific subscription row.',
35
35
  '',
package/dist/listen.d.ts CHANGED
@@ -38,7 +38,19 @@ export interface ListenMetrics {
38
38
  eventsForwarded: number;
39
39
  forwardErrors: number;
40
40
  }
41
- export declare const DEFAULT_WS_URL = "wss://api.prohost.ai/ws/webhooks";
41
+ /**
42
+ * `/ws/webhooks` is served by **api_public**, which is `connect.prohost.ai` —
43
+ * the same host as {@link DEFAULT_API_BASE_URL}. It is NOT on `api.prohost.ai`
44
+ * (the main ECS API), which answers the handshake with a bare 403.
45
+ *
46
+ * 0.1.0 shipped `api.prohost.ai` here while the REST base correctly pointed at
47
+ * `connect.prohost.ai`, so every paired agent redeemed its code fine and then
48
+ * failed to connect, reconnecting forever against a host that does not serve
49
+ * this route. Mentions dispatched, no runtime was listening, and the thread sat
50
+ * on "…is thinking" until the delivery expired. Keep this host in step with
51
+ * `DEFAULT_API_BASE_URL`.
52
+ */
53
+ export declare const DEFAULT_WS_URL = "wss://connect.prohost.ai/ws/webhooks";
42
54
  /**
43
55
  * Run the listener until the abort signal fires or the socket closes.
44
56
  *
package/dist/listen.js CHANGED
@@ -5,7 +5,19 @@
5
5
  import os from 'node:os';
6
6
  import WebSocket from 'ws';
7
7
  import { forwardEvent, isWebhookEvent } from './forward.js';
8
- export const DEFAULT_WS_URL = 'wss://api.prohost.ai/ws/webhooks';
8
+ /**
9
+ * `/ws/webhooks` is served by **api_public**, which is `connect.prohost.ai` —
10
+ * the same host as {@link DEFAULT_API_BASE_URL}. It is NOT on `api.prohost.ai`
11
+ * (the main ECS API), which answers the handshake with a bare 403.
12
+ *
13
+ * 0.1.0 shipped `api.prohost.ai` here while the REST base correctly pointed at
14
+ * `connect.prohost.ai`, so every paired agent redeemed its code fine and then
15
+ * failed to connect, reconnecting forever against a host that does not serve
16
+ * this route. Mentions dispatched, no runtime was listening, and the thread sat
17
+ * on "…is thinking" until the delivery expired. Keep this host in step with
18
+ * `DEFAULT_API_BASE_URL`.
19
+ */
20
+ export const DEFAULT_WS_URL = 'wss://connect.prohost.ai/ws/webhooks';
9
21
  /**
10
22
  * Run the listener until the abort signal fires or the socket closes.
11
23
  *
package/dist/version.d.ts CHANGED
@@ -7,6 +7,6 @@
7
7
  * `package.json`, so a release bump that forgets this file fails the suite
8
8
  * instead of shipping a `User-Agent` that lies about which build is calling.
9
9
  */
10
- export declare const CLI_VERSION = "0.1.0";
10
+ export declare const CLI_VERSION = "0.1.2";
11
11
  /** Sent on every HTTP request the CLI makes back into ProhostAI. */
12
- export declare const USER_AGENT = "prohost-cli/0.1.0";
12
+ export declare const USER_AGENT = "prohost-cli/0.1.2";
package/dist/version.js CHANGED
@@ -7,6 +7,6 @@
7
7
  * `package.json`, so a release bump that forgets this file fails the suite
8
8
  * instead of shipping a `User-Agent` that lies about which build is calling.
9
9
  */
10
- export const CLI_VERSION = '0.1.0';
10
+ export const CLI_VERSION = '0.1.2';
11
11
  /** Sent on every HTTP request the CLI makes back into ProhostAI. */
12
12
  export const USER_AGENT = `prohost-cli/${CLI_VERSION}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prohost/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Run your own AI agent as a ProhostAI teammate, and stream your account's webhooks to your laptop.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -15,6 +15,7 @@
15
15
  "files": [
16
16
  "dist",
17
17
  "README.md",
18
+ "CHANGELOG.md",
18
19
  "LICENSE"
19
20
  ],
20
21
  "scripts": {