@prohost/cli 0.1.0 → 0.2.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.
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Which agent session belongs to which conversation.
3
+ *
4
+ * Every run spawns a fresh one-shot process, so without this the agent meets
5
+ * each message as its first: someone answers a clarifying question and gets a
6
+ * reply that has forgotten the question. Server-supplied thread history
7
+ * (`recent_messages`) narrows that gap but does not close it — history is what
8
+ * was *said*, not what the agent worked out, looked up, or decided on the last
9
+ * turn.
10
+ *
11
+ * Keyed by surface rather than by run: a run is one turn, and continuity is a
12
+ * property of the thread the turns are in.
13
+ *
14
+ * Deliberately a separate ledger from `runs.json`. That one is a safety
15
+ * mechanism whose entries must never be lost; this one is an optimisation whose
16
+ * worst failure is an agent starting fresh. Mixing them would give the
17
+ * important file the churn rate of the unimportant one.
18
+ */
19
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
20
+ import path from 'node:path';
21
+ import { prohostHome } from './credentials.js';
22
+ /** Cap on retained sessions. Oldest are dropped first (insertion order). */
23
+ export const SESSION_LEDGER_LIMIT = 200;
24
+ export function sessionLedgerPath(env = process.env) {
25
+ return path.join(prohostHome(env), 'sessions.json');
26
+ }
27
+ /**
28
+ * The thread a run belongs to, or ``undefined`` when it belongs to none.
29
+ *
30
+ * A trigger fan-out (`reservation.created` and friends) has no surface and no
31
+ * predecessor, so it deliberately gets no key: resuming an unrelated session
32
+ * into it would carry over context that has nothing to do with the event, which
33
+ * is worse than starting clean.
34
+ *
35
+ * Keyed by agent as well as surface. The ledger outlives a pairing — re-pair
36
+ * this machine to a different AI employee and the file is still there — so a
37
+ * surface-only key would have the new agent resume the old one's session and
38
+ * answer in its predecessor's voice, carrying its reasoning and tool state.
39
+ */
40
+ export function sessionKeyFor(run) {
41
+ const surface = run.conversation_id
42
+ ? `conversation:${run.conversation_id}`
43
+ : run.task_id
44
+ ? `task:${run.task_id}`
45
+ : run.cleaning_id
46
+ ? `cleaning:${run.cleaning_id}`
47
+ : undefined;
48
+ if (!surface)
49
+ return undefined;
50
+ return `${run.agent_id ?? 'agent'}/${surface}`;
51
+ }
52
+ /**
53
+ * Surface → session id, backed by a JSON file.
54
+ *
55
+ * Like {@link RunStore}, every persistence failure degrades to memory-only
56
+ * rather than throwing: a read-only home directory should cost continuity, not
57
+ * the ability to answer.
58
+ */
59
+ export class SessionStore {
60
+ file;
61
+ onWarn;
62
+ records = new Map();
63
+ constructor(file, onWarn) {
64
+ this.file = file;
65
+ this.onWarn = onWarn;
66
+ }
67
+ /** Read the ledger from disk. A missing or corrupt file starts empty. */
68
+ static load(env = process.env, onWarn = () => { }) {
69
+ const store = new SessionStore(sessionLedgerPath(env), onWarn);
70
+ try {
71
+ const parsed = JSON.parse(readFileSync(store.file, 'utf8'));
72
+ if (Array.isArray(parsed)) {
73
+ for (const entry of parsed) {
74
+ const record = entry;
75
+ if (typeof record?.key === 'string' && record.key && typeof record.session_id === 'string' && record.session_id) {
76
+ store.records.set(record.key, {
77
+ key: record.key,
78
+ session_id: record.session_id,
79
+ updated_at: typeof record.updated_at === 'string' ? record.updated_at : '',
80
+ });
81
+ }
82
+ }
83
+ }
84
+ }
85
+ catch {
86
+ // No ledger yet, or an unreadable one — either way, start clean.
87
+ }
88
+ return store;
89
+ }
90
+ /** The session to resume for a surface, if one is remembered. */
91
+ get(key) {
92
+ if (!key)
93
+ return undefined;
94
+ return this.records.get(key)?.session_id;
95
+ }
96
+ /**
97
+ * Remember the session a surface's last turn ran in.
98
+ *
99
+ * Re-inserted rather than mutated in place so the map's insertion order stays
100
+ * a real least-recently-used order — {@link trim} drops from the front, and
101
+ * mutating in place would make the busiest thread the first one evicted.
102
+ */
103
+ remember(key, sessionId) {
104
+ if (!key || !sessionId)
105
+ return;
106
+ this.records.delete(key);
107
+ this.records.set(key, { key, session_id: sessionId, updated_at: new Date().toISOString() });
108
+ this.trim();
109
+ this.persist();
110
+ }
111
+ /** Drop a surface's session — used when the agent rejected it as unresumable. */
112
+ forget(key) {
113
+ if (!key)
114
+ return;
115
+ if (this.records.delete(key))
116
+ this.persist();
117
+ }
118
+ trim() {
119
+ while (this.records.size > SESSION_LEDGER_LIMIT) {
120
+ const oldest = this.records.keys().next().value;
121
+ if (oldest === undefined)
122
+ return;
123
+ this.records.delete(oldest);
124
+ }
125
+ }
126
+ persist() {
127
+ try {
128
+ mkdirSync(path.dirname(this.file), { recursive: true, mode: 0o700 });
129
+ writeFileSync(this.file, `${JSON.stringify([...this.records.values()], null, 2)}\n`, {
130
+ mode: 0o600,
131
+ });
132
+ }
133
+ catch (err) {
134
+ this.onWarn(` ! could not write the session ledger (${err instanceof Error ? err.message : String(err)}); ` +
135
+ 'the agent will start a fresh session each run until this is fixed');
136
+ }
137
+ }
138
+ }
package/dist/index.d.ts CHANGED
@@ -2,11 +2,12 @@
2
2
  /**
3
3
  * `@prohost/cli` — entrypoint.
4
4
  *
5
- * Two commands:
5
+ * Commands:
6
6
  *
7
7
  * npx @prohost/cli listen --forward-to http://localhost:3000/webhook
8
8
  * npx @prohost/cli agent pair --code <pairing-code>
9
9
  * npx @prohost/cli agent run --exec 'claude -p'
10
+ * npx @prohost/cli agent install-daemon --exec 'claude -p'
10
11
  *
11
12
  * Parses arguments with a zero-dep, zero-magic flag parser so the published
12
13
  * binary stays small and starts fast.
package/dist/index.js CHANGED
@@ -2,11 +2,12 @@
2
2
  /**
3
3
  * `@prohost/cli` — entrypoint.
4
4
  *
5
- * Two commands:
5
+ * Commands:
6
6
  *
7
7
  * npx @prohost/cli listen --forward-to http://localhost:3000/webhook
8
8
  * npx @prohost/cli agent pair --code <pairing-code>
9
9
  * npx @prohost/cli agent run --exec 'claude -p'
10
+ * npx @prohost/cli agent install-daemon --exec 'claude -p'
10
11
  *
11
12
  * Parses arguments with a zero-dep, zero-magic flag parser so the published
12
13
  * binary stays small and starts fast.
@@ -24,12 +25,14 @@ function printHelp() {
24
25
  'Usage:',
25
26
  ' prohost listen --forward-to <local-url> [--api-key <key>] [--url <ws-url>] [--machine <name>] [--subscription-id <id>]',
26
27
  ' prohost agent pair --code <pairing-code> [--webhook-url <https-url>] [--base-url <url>]',
27
- " prohost agent run --exec '<command>' [--timeout <seconds>] [--dry-run] [--cwd <dir>]",
28
+ " prohost agent run --exec '<command>' [--timeout <seconds>] [--dry-run] [--workdir <dir>]",
29
+ " prohost agent install-daemon --exec '<command>' [--workdir <dir>] Keep the agent running",
30
+ ' prohost agent uninstall-daemon',
28
31
  '',
29
32
  'listen flags:',
30
33
  ' --forward-to Local URL to POST each webhook payload to (required)',
31
34
  ' --api-key ProhostAI API key. Defaults to $PROHOST_API_KEY.',
32
- ' --url Websocket endpoint. Defaults to wss://api.prohost.ai/ws/webhooks.',
35
+ ' --url Websocket endpoint. Defaults to wss://connect.prohost.ai/ws/webhooks.',
33
36
  ' --machine Machine name displayed in the UI. Defaults to the OS hostname.',
34
37
  ' --subscription-id Stamp connected_from on a specific subscription row.',
35
38
  '',
@@ -42,10 +45,21 @@ function printHelp() {
42
45
  ' --exec Command to run for each mention; the prompt arrives on stdin (required)',
43
46
  ' --timeout Seconds one run may take before it is killed. Defaults to 300.',
44
47
  ' --dry-run Print the reply that would be posted; write nothing.',
45
- ' --cwd Working directory for the agent command.',
48
+ ' --workdir Working directory for the agent command. Defaults to $PROHOST_HOME/workspace.',
49
+ ' Avoid pointing it at a code checkout: agents read CLAUDE.md, .claude/settings.json',
50
+ ' and .mcp.json from their working directory and will inherit them.',
51
+ ' --cwd Deprecated alias for --workdir.',
46
52
  ' --machine Machine name displayed in the UI. Defaults to the OS hostname.',
47
53
  ' --url Websocket endpoint. Defaults to the paired value.',
48
54
  ' --allow-unverified Run events whose signature could not be verified (not recommended).',
55
+ ' --safe-tools Gate the agent\'s tools behind permission prompts instead of bypassing them.',
56
+ ' Claude Code runs default to --dangerously-skip-permissions so the agent can',
57
+ ' act; with --safe-tools expect it to answer rather than do. Alias:',
58
+ ' --no-skip-permissions.',
59
+ '',
60
+ 'agent install-daemon flags: (macOS: launchd; Linux: prints a systemd-user unit)',
61
+ ' --exec The same command you would pass to `agent run` (required).',
62
+ ' --workdir, --timeout, --machine, --url, --allow-unverified, --safe-tools — passed to `agent run`.',
49
63
  '',
50
64
  'Global flags:',
51
65
  ' --help, -h Show this help and exit.',
@@ -55,7 +69,7 @@ function printHelp() {
55
69
  ' PROHOST_API_KEY Used by `listen` when --api-key is not provided.',
56
70
  ' PROHOST_WS_URL Used when --url is not provided.',
57
71
  ' PROHOST_PAIRING_CODE Used by `agent pair` when --code is not provided.',
58
- ' PROHOST_HOME Credential directory. Defaults to ~/.prohost.',
72
+ ' PROHOST_HOME Credential, ledger, workspace and log directory. Defaults to ~/.prohost.',
59
73
  ];
60
74
  process.stdout.write(`${lines.join('\n')}\n`);
61
75
  }
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.2.0";
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.2.0";
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.2.0';
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.2.0",
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": {