@prohost/cli 0.1.2 → 0.2.1
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 +65 -0
- package/README.md +151 -13
- package/dist/agent/api.d.ts +6 -1
- package/dist/agent/api.js +10 -2
- package/dist/agent/claude.d.ts +161 -0
- package/dist/agent/claude.js +336 -0
- package/dist/agent/command.js +169 -19
- package/dist/agent/contract.d.ts +57 -4
- package/dist/agent/contract.js +71 -0
- package/dist/agent/credentials.d.ts +15 -0
- package/dist/agent/credentials.js +21 -0
- package/dist/agent/daemon.d.ts +131 -0
- package/dist/agent/daemon.js +274 -0
- package/dist/agent/prompt.d.ts +17 -2
- package/dist/agent/prompt.js +61 -3
- package/dist/agent/run.d.ts +13 -1
- package/dist/agent/run.js +188 -25
- package/dist/agent/sessionstore.d.ts +71 -0
- package/dist/agent/sessionstore.js +138 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +18 -4
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,71 @@ Versions follow [semver](https://semver.org/). Publishing is automated: merging
|
|
|
4
4
|
a version bump to `main` triggers `.github/workflows/npm-publish-cli.yml`, which
|
|
5
5
|
builds via `prepack`, runs the suite, publishes, and tags `cli-v<version>`.
|
|
6
6
|
|
|
7
|
+
## 0.2.1
|
|
8
|
+
|
|
9
|
+
- Prompt: tell brains with a ToolSearch tool that part of their toolkit
|
|
10
|
+
(notably WebSearch/WebFetch) is deferred and must be loaded before use, so
|
|
11
|
+
a paired agent stops telling customers it cannot browse the web when it can.
|
|
12
|
+
|
|
13
|
+
## 0.2.0
|
|
14
|
+
|
|
15
|
+
The harness stopped being a one-shot prompt pipe. 0.1.x answered every mention
|
|
16
|
+
from a single 500-character excerpt, in whatever directory you happened to be
|
|
17
|
+
standing in, with no memory of the previous message and no way to look anything
|
|
18
|
+
up — and told the agent about an MCP endpoint it had no client for, which
|
|
19
|
+
produced replies confidently describing lookups that never happened.
|
|
20
|
+
|
|
21
|
+
- **The agent gets the thread.** `agent.run_requested` now carries
|
|
22
|
+
`recent_messages` and `memories` when the server sends them, rendered as
|
|
23
|
+
delimited "Conversation so far" and "Things you remember" blocks ahead of the
|
|
24
|
+
triggering message. Both fields are optional in both directions: an older
|
|
25
|
+
server behaves exactly as before, and a newer one can add or move fields
|
|
26
|
+
without breaking a CLI that is already on someone's laptop.
|
|
27
|
+
- **Claude Code runs keep their session.** When `--exec` is `claude` in print
|
|
28
|
+
mode, the harness runs it with `--output-format json`, reads the reply and
|
|
29
|
+
`session_id` out of the result, and `--resume`s that session for the next run
|
|
30
|
+
on the same conversation (ledger: `~/.prohost/sessions.json`). A session that
|
|
31
|
+
has expired is retried once without `--resume` rather than failing the surface
|
|
32
|
+
forever. Any other `--exec` command keeps the exact stdin/stdout behaviour it
|
|
33
|
+
had before.
|
|
34
|
+
- **Claude Code runs can actually act.** Runs are invoked with
|
|
35
|
+
`--dangerously-skip-permissions`: in print mode nobody is at a terminal to
|
|
36
|
+
approve a tool call, so a gated brain silently declines the work it was hired
|
|
37
|
+
for. `--safe-tools` (alias `--no-skip-permissions`) opts back into gating. The
|
|
38
|
+
README now states the trust model plainly — the agent runs as you, unsandboxed,
|
|
39
|
+
on instructions written by workspace teammates — and the harness says so on
|
|
40
|
+
every start-up. Codex users should pass `codex --yolo` in their `--exec`.
|
|
41
|
+
- **Reply bodies carry the server's extra fields.** `reply_instructions` may now
|
|
42
|
+
include `body_extra`, merged into the reply POST alongside the body field —
|
|
43
|
+
first use is `reply_to_message_id`, so a team-chat reply lands in the right
|
|
44
|
+
sub-thread. Merged without being interpreted, so the server can extend the
|
|
45
|
+
reply body without a CLI release; the body field always wins a key collision,
|
|
46
|
+
so nothing can displace the reply text.
|
|
47
|
+
- **Claude Code runs get ProhostAI tools.** The paired MCP endpoint is written
|
|
48
|
+
to a `0600` `~/.prohost/mcp.json` and passed as `--mcp-config` with
|
|
49
|
+
`--strict-mcp-config` (so the run never picks up the operator's own MCP
|
|
50
|
+
servers) and `--allowedTools`, since nothing can be approved interactively in
|
|
51
|
+
print mode. Tools the run asked for and was refused are now printed.
|
|
52
|
+
- **The prompt is honest about the run.** It states that this is a single
|
|
53
|
+
non-interactive turn, that tools needing approval are unavailable, and that
|
|
54
|
+
the agent should name what it could not do instead of narrating a lookup. The
|
|
55
|
+
ProhostAI tools are described only when they are actually wired in, and no
|
|
56
|
+
workspace domain is named — a paired credential's scopes don't reach every
|
|
57
|
+
tool (reservations and listings are excluded today), so the prompt says the
|
|
58
|
+
credential is narrow and that a permission error is a real limit.
|
|
59
|
+
- **A dedicated workspace.** `agent run` gains `--workdir`, defaulting to
|
|
60
|
+
`$PROHOST_HOME/workspace` instead of the current directory. Agents read
|
|
61
|
+
`CLAUDE.md`, `.claude/settings.json` and `.mcp.json` from their working
|
|
62
|
+
directory; started from a code checkout they inherit that repo's instructions
|
|
63
|
+
and permission denials. `--cwd` still works as an alias.
|
|
64
|
+
- **`prohost agent install-daemon` / `uninstall-daemon`.** Generates and loads a
|
|
65
|
+
`KeepAlive` launchd agent on macOS so the harness survives logout and reboot,
|
|
66
|
+
logging to `$PROHOST_HOME/agent.log`. The plist carries your `PATH` forward,
|
|
67
|
+
since launchd's default one contains no `claude`, and the daemon is scoped to
|
|
68
|
+
`$PROHOST_HOME` — installing twice from one home replaces the daemon instead
|
|
69
|
+
of adding a second one running the same credential. On Linux it prints a
|
|
70
|
+
systemd-user unit to install by hand.
|
|
71
|
+
|
|
7
72
|
## 0.1.2
|
|
8
73
|
|
|
9
74
|
The release that carries 0.1.1's fix to users. 0.1.1 was tagged in the monorepo
|
package/README.md
CHANGED
|
@@ -27,6 +27,9 @@ npx @prohost/cli agent pair --code <pairing-code>
|
|
|
27
27
|
|
|
28
28
|
# 2. Point it at whatever agent you already use.
|
|
29
29
|
npx @prohost/cli agent run --exec 'claude -p'
|
|
30
|
+
|
|
31
|
+
# 3. Optional, but you probably want it: keep it running across reboots.
|
|
32
|
+
npx @prohost/cli agent install-daemon --exec 'claude -p'
|
|
30
33
|
```
|
|
31
34
|
|
|
32
35
|
That's it — @-mention the agent in ProhostAI and it answers from your machine.
|
|
@@ -84,7 +87,7 @@ in a team thread, a 1:1 DM, a task comment, a trigger — you'll see:
|
|
|
84
87
|
Other agents, same command:
|
|
85
88
|
|
|
86
89
|
```bash
|
|
87
|
-
npx @prohost/cli agent run --exec 'codex exec -'
|
|
90
|
+
npx @prohost/cli agent run --exec 'codex exec - --yolo'
|
|
88
91
|
npx @prohost/cli agent run --exec 'goose run -i -'
|
|
89
92
|
npx @prohost/cli agent run --exec './my-agent.sh'
|
|
90
93
|
```
|
|
@@ -106,13 +109,74 @@ nothing is posted and the run is left open.
|
|
|
106
109
|
2. The CLI verifies the event's HMAC signature. An event that doesn't verify is
|
|
107
110
|
**not executed** — the whole point of the signature is that a run event makes
|
|
108
111
|
your machine execute a command.
|
|
109
|
-
3. It builds a prompt
|
|
110
|
-
|
|
112
|
+
3. It builds a prompt — who your agent is, what it's replying to, the recent
|
|
113
|
+
messages in the thread, anything the account remembers that bears on it, and
|
|
114
|
+
the message that mentioned it — and runs your `--exec` command with that
|
|
115
|
+
prompt on **stdin**.
|
|
111
116
|
4. Your command's **stdout** is the reply. It's posted into the conversation
|
|
112
117
|
verbatim, so print the message and nothing else — no preamble, no reasoning.
|
|
118
|
+
(Claude Code is the exception: see below.)
|
|
113
119
|
5. The run is completed either way. A command that crashes, times out, or prints
|
|
114
120
|
nothing reports `failed`; a run is never left hanging.
|
|
115
121
|
|
|
122
|
+
### Where your agent runs
|
|
123
|
+
|
|
124
|
+
By default the command runs in `~/.prohost/workspace`, which the CLI creates —
|
|
125
|
+
**not** the directory you started `agent run` from. That matters more than it
|
|
126
|
+
sounds like it does: coding agents read their working directory as
|
|
127
|
+
configuration. Start one inside a code checkout and it picks up that repo's
|
|
128
|
+
`CLAUDE.md`, its `.claude/settings.json` permission list, and its `.mcp.json`
|
|
129
|
+
servers — so your agent answers a customer's question carrying instructions
|
|
130
|
+
about a codebase, and quietly refuses tools that repo happens to deny.
|
|
131
|
+
|
|
132
|
+
Point `--workdir` somewhere else if you want the agent to work in a particular
|
|
133
|
+
directory. Just choose it deliberately.
|
|
134
|
+
|
|
135
|
+
### If your agent is Claude Code
|
|
136
|
+
|
|
137
|
+
When `--exec` starts with `claude` and asks for print mode (`-p` / `--print`),
|
|
138
|
+
the CLI does three extra things. Every other command — Codex, goose, your own
|
|
139
|
+
script — is left exactly alone.
|
|
140
|
+
|
|
141
|
+
**It keeps the conversation.** Each run is a fresh process, so without this your
|
|
142
|
+
agent meets every message as its first: someone answers its clarifying question
|
|
143
|
+
and gets a reply that has forgotten asking. The CLI reads the `session_id` out
|
|
144
|
+
of Claude Code's JSON output, records it per conversation in
|
|
145
|
+
`~/.prohost/sessions.json`, and `--resume`s it next time. Sessions don't live
|
|
146
|
+
forever; when one is gone the run is retried once from scratch rather than
|
|
147
|
+
failing.
|
|
148
|
+
|
|
149
|
+
**It lets the agent actually do things.** Runs are invoked with
|
|
150
|
+
`--dangerously-skip-permissions`, so tool calls execute without stopping to ask.
|
|
151
|
+
This is deliberate: in print mode there is nobody at a terminal to approve
|
|
152
|
+
anything, so a permission-gated brain silently declines to do the work you hired
|
|
153
|
+
it for. **Read the trust model below before you leave this running.** Pass
|
|
154
|
+
`--safe-tools` (or `--no-skip-permissions`) to gate tools instead — expect the
|
|
155
|
+
agent to answer rather than act.
|
|
156
|
+
|
|
157
|
+
**It gives the agent your ProhostAI tools.** The MCP endpoint from your pairing
|
|
158
|
+
is written to `~/.prohost/mcp.json` (`chmod 600` — it holds your API key) and
|
|
159
|
+
passed with `--mcp-config`, so the agent can look things up instead of guessing.
|
|
160
|
+
`--strict-mcp-config` is passed too: the run gets *only* this server, never the
|
|
161
|
+
MCP servers you use for your own work.
|
|
162
|
+
|
|
163
|
+
A pairing is deliberately narrow, so not every tool will work: today's default
|
|
164
|
+
scopes cover conversations, tasks, cleanings and approvals, but **not**
|
|
165
|
+
reservations or listings — those tools return a permission error. The prompt
|
|
166
|
+
tells the agent to say what it couldn't reach rather than answer as if it had,
|
|
167
|
+
and refused tools are printed in the run log.
|
|
168
|
+
|
|
169
|
+
**It reads the reply out of JSON.** The run is invoked with
|
|
170
|
+
`--output-format json`, and the `result` field is what gets posted. If that
|
|
171
|
+
output isn't JSON — a login prompt, a crash banner — the run is reported failed
|
|
172
|
+
rather than pasting it into a customer's conversation.
|
|
173
|
+
|
|
174
|
+
Detection is deliberately narrow: it matches only when `claude` is the first
|
|
175
|
+
word of the command *and* print mode is asked for. Anything else — `npx claude
|
|
176
|
+
-p`, `env FOO=1 claude -p`, a wrapper script, or any command containing a pipe,
|
|
177
|
+
redirect, or `&&` — keeps the plain stdin/stdout contract untouched. So
|
|
178
|
+
`--exec 'sh -c "claude -p"'` is the way to opt out.
|
|
179
|
+
|
|
116
180
|
Runs are handled one at a time, and each run executes **at most once on this
|
|
117
181
|
machine** — even across restarts. Started runs are recorded in
|
|
118
182
|
`~/.prohost/runs.json` before the command launches, so a webhook redelivery
|
|
@@ -121,12 +185,35 @@ your agent a second time. That matters because your agent holds a credential:
|
|
|
121
185
|
a second execution could file the same task or send the same email again, which
|
|
122
186
|
no amount of message deduplication would undo.
|
|
123
187
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
188
|
+
### The trust model — read this once
|
|
189
|
+
|
|
190
|
+
`agent run` is a standing instruction to execute a command on your machine when
|
|
191
|
+
someone in your ProhostAI workspace asks. Be clear-eyed about what that means:
|
|
192
|
+
|
|
193
|
+
- **The agent runs as you.** Same user, same filesystem, same SSH keys, same
|
|
194
|
+
logged-in CLIs. It is not sandboxed.
|
|
195
|
+
- **By default its tools are not gated.** With `--dangerously-skip-permissions`
|
|
196
|
+
(the default for Claude Code) nothing pauses for confirmation. That is what
|
|
197
|
+
makes it useful and it is also the whole risk.
|
|
198
|
+
- **The instructions come from other people.** The prompt carries a message a
|
|
199
|
+
teammate wrote, plus thread history. Treat all of it as untrusted input — the
|
|
200
|
+
same judgement you'd apply to any agent reading text from the internet. A
|
|
201
|
+
colleague's compromised account, or a guest message quoted into a thread, is
|
|
202
|
+
an instruction channel into your laptop.
|
|
203
|
+
|
|
204
|
+
What the harness does to bound this: the message reaches your command on
|
|
205
|
+
**stdin only**, never the shell, so it cannot inject a command; runs execute at
|
|
206
|
+
most once each; every event's signature is verified before anything runs; the
|
|
207
|
+
agent works in `~/.prohost/workspace` rather than your code; and
|
|
208
|
+
`--strict-mcp-config` keeps your own MCP servers out of the run.
|
|
209
|
+
|
|
210
|
+
What it does **not** do: sandbox the process. If that matters for your machine,
|
|
211
|
+
run `agent run` inside a container or a dedicated user account, or use
|
|
212
|
+
`--safe-tools` and accept that the agent will answer rather than act.
|
|
213
|
+
|
|
214
|
+
Other agents: the CLI shapes no command but Claude Code's, so give yours its own
|
|
215
|
+
equivalent flag — `codex --yolo` for Codex CLI, whatever your runner calls it —
|
|
216
|
+
and apply the same judgement.
|
|
130
217
|
|
|
131
218
|
### `agent run` flags
|
|
132
219
|
|
|
@@ -135,10 +222,51 @@ command.)
|
|
|
135
222
|
| `--exec` | *(required)* | Command to run for each requested run. Prompt arrives on stdin. |
|
|
136
223
|
| `--timeout` | `300` | Seconds one run may take before the command is killed. |
|
|
137
224
|
| `--dry-run` | off | Print the reply that would be posted; write nothing. |
|
|
138
|
-
| `--
|
|
225
|
+
| `--workdir` | `~/.prohost/workspace` | Working directory for the command. See "Where your agent runs". |
|
|
226
|
+
| `--cwd` | — | Deprecated alias for `--workdir`. |
|
|
139
227
|
| `--machine` | OS hostname | Name shown next to the connection in ProhostAI. |
|
|
140
228
|
| `--url` | paired value | Websocket endpoint override. Pairing stores the URL the server reports, so you should not normally need this. |
|
|
141
229
|
| `--allow-unverified` | off | Run events whose signature can't be verified. See below. |
|
|
230
|
+
| `--safe-tools` | off | Gate the agent's tools behind permission prompts instead of bypassing them. Claude Code only. Alias: `--no-skip-permissions`. |
|
|
231
|
+
|
|
232
|
+
### Keeping it running
|
|
233
|
+
|
|
234
|
+
`agent run` in a terminal window stops being an agent the moment you close the
|
|
235
|
+
window or the laptop reboots — and nothing tells you it stopped. Someone
|
|
236
|
+
@-mentions it and the run times out server-side.
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
npx @prohost/cli agent install-daemon --exec 'claude -p'
|
|
240
|
+
npx @prohost/cli agent uninstall-daemon
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
On macOS this writes a launchd agent to
|
|
244
|
+
`~/Library/LaunchAgents/ai.prohost.agent.<id>.plist` and loads it, with
|
|
245
|
+
`KeepAlive` on so it starts at login and restarts if it exits. Output goes to
|
|
246
|
+
`$PROHOST_HOME/agent.log` — the first place to look when the agent is quiet.
|
|
247
|
+
Re-running it with a different `--exec` replaces the old daemon.
|
|
248
|
+
|
|
249
|
+
The daemon is scoped to `$PROHOST_HOME`, because that's where the one credential
|
|
250
|
+
lives: installing twice from the same home replaces the daemon rather than
|
|
251
|
+
adding a second one. To run two agents on one machine, give each its own
|
|
252
|
+
`$PROHOST_HOME` — separate credentials, separate ledgers, separate daemons.
|
|
253
|
+
|
|
254
|
+
**The daemon does not inherit your shell.** It gets `PATH` and `PROHOST_HOME`
|
|
255
|
+
and nothing else, because a supervised process starts from a bare environment.
|
|
256
|
+
If your `--exec` command needs anything more — an API key, a proxy setting —
|
|
257
|
+
point it at a wrapper script that sets those itself. Copying them into the plist
|
|
258
|
+
would put your secrets in a file on disk that nothing else protects.
|
|
259
|
+
|
|
260
|
+
It also records the `PATH` you had when you ran `install-daemon`. launchd starts
|
|
261
|
+
jobs with a bare `PATH` that contains no `claude`, so without this the daemon
|
|
262
|
+
installs, reports success, and then quietly fails every run with
|
|
263
|
+
`command not found`. If you later move your agent binary, re-run
|
|
264
|
+
`install-daemon`.
|
|
265
|
+
|
|
266
|
+
`install-daemon` takes the same flags as `agent run` and passes them through. On
|
|
267
|
+
Linux it prints a systemd-user unit for you to install by hand, because making
|
|
268
|
+
it survive logout needs `loginctl enable-linger`, which isn't this CLI's
|
|
269
|
+
decision to make.
|
|
142
270
|
|
|
143
271
|
### `agent pair` flags
|
|
144
272
|
|
|
@@ -169,10 +297,20 @@ has real credentials.
|
|
|
169
297
|
| `auth_error: stream_subscription_unavailable` | The connection has no event subscription to stream. Re-pair. |
|
|
170
298
|
| `signature did not verify` | The event didn't come from ProhostAI, or your stored secret is stale. Re-pair. |
|
|
171
299
|
| 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`). |
|
|
300
|
+
| `claude did not return JSON` | Claude Code printed something else — usually a login prompt. Run `claude -p hello` yourself and fix what it says. |
|
|
301
|
+
| `tools the agent asked for and was refused` | The run wanted a tool nothing could approve (nobody is at a terminal). Expected for most built-ins; if it names a `mcp__prohost__…` tool, re-pair. |
|
|
302
|
+
| The agent answers as if it were working on some repo | It's running in a code checkout and inherited that repo's `CLAUDE.md`. Drop `--workdir`, or point it somewhere neutral. |
|
|
303
|
+
|
|
304
|
+
Everything the CLI stores lives under `~/.prohost` (`$PROHOST_HOME` moves it):
|
|
172
305
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
ProhostAI to invalidate the key itself.
|
|
306
|
+
| File | What it is |
|
|
307
|
+
| --- | --- |
|
|
308
|
+
| `agent.json` | Credentials. Delete to un-pair locally; revoke in ProhostAI to invalidate the key itself. |
|
|
309
|
+
| `runs.json` | Runs already executed on this machine, so a redelivery can't run your agent twice. |
|
|
310
|
+
| `sessions.json` | Conversation → Claude Code session, for continuity. Safe to delete; costs the agent its memory of open threads. |
|
|
311
|
+
| `mcp.json` | Generated MCP config. Holds your API key — `chmod 600`. |
|
|
312
|
+
| `workspace/` | Where your agent command runs by default. |
|
|
313
|
+
| `agent.log` | Daemon output. |
|
|
176
314
|
|
|
177
315
|
### What the pairing can and can't do
|
|
178
316
|
|
package/dist/agent/api.d.ts
CHANGED
|
@@ -27,8 +27,13 @@ export interface ApiResult {
|
|
|
27
27
|
* Keyed on ``run_id`` so the send is idempotent server-side. The harness dedupes
|
|
28
28
|
* runs in memory, but that memory dies with the process: without this header a
|
|
29
29
|
* redelivered run after a restart would post the reply a second time.
|
|
30
|
+
*
|
|
31
|
+
* ``extra`` carries the event's ``reply_instructions.body_extra`` verbatim —
|
|
32
|
+
* threading fields and whatever the server adds later. It is merged, not
|
|
33
|
+
* interpreted: the point is that the server can extend the reply body without
|
|
34
|
+
* a CLI release.
|
|
30
35
|
*/
|
|
31
|
-
export declare function postReply(options: ApiOptions, path: string, bodyKey: string, message: string, runId: string): Promise<ApiResult>;
|
|
36
|
+
export declare function postReply(options: ApiOptions, path: string, bodyKey: string, message: string, runId: string, extra?: Record<string, unknown>): Promise<ApiResult>;
|
|
32
37
|
/**
|
|
33
38
|
* Report a run's terminal state. Called for every run — including one whose
|
|
34
39
|
* command crashed — so the server never waits on a run this harness abandoned.
|
package/dist/agent/api.js
CHANGED
|
@@ -57,9 +57,17 @@ async function post(options, path, body, idempotencyKey) {
|
|
|
57
57
|
* Keyed on ``run_id`` so the send is idempotent server-side. The harness dedupes
|
|
58
58
|
* runs in memory, but that memory dies with the process: without this header a
|
|
59
59
|
* redelivered run after a restart would post the reply a second time.
|
|
60
|
+
*
|
|
61
|
+
* ``extra`` carries the event's ``reply_instructions.body_extra`` verbatim —
|
|
62
|
+
* threading fields and whatever the server adds later. It is merged, not
|
|
63
|
+
* interpreted: the point is that the server can extend the reply body without
|
|
64
|
+
* a CLI release.
|
|
60
65
|
*/
|
|
61
|
-
export async function postReply(options, path, bodyKey, message, runId) {
|
|
62
|
-
|
|
66
|
+
export async function postReply(options, path, bodyKey, message, runId, extra) {
|
|
67
|
+
// `extra` first, so the message can never be clobbered by a `body_extra` key
|
|
68
|
+
// that happens to collide with the body field. The reply text is the one
|
|
69
|
+
// thing in this request that must survive.
|
|
70
|
+
return post(options, path, { ...extra, [bodyKey]: message }, `prohost-cli-run-${runId}`);
|
|
63
71
|
}
|
|
64
72
|
/**
|
|
65
73
|
* Report a run's terminal state. Called for every run — including one whose
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one place that knows anything about Claude Code specifically.
|
|
3
|
+
*
|
|
4
|
+
* `exec.ts` is deliberately agent-agnostic — prompt on stdin, reply on stdout,
|
|
5
|
+
* the lowest common denominator every CLI agent already speaks — and it stays
|
|
6
|
+
* that way. But that contract costs two things Claude Code can actually
|
|
7
|
+
* provide: a run has no memory of the previous one, and it has no tools for
|
|
8
|
+
* looking anything up in the workspace it is answering about. Both are fixed
|
|
9
|
+
* with flags, and flags are not portable. So the knowledge lives here, behind a
|
|
10
|
+
* detection function, and every other exec command keeps the exact behaviour it
|
|
11
|
+
* had before this module existed.
|
|
12
|
+
*
|
|
13
|
+
* Nothing in here is load-bearing for correctness: if detection misses, if the
|
|
14
|
+
* session is unresumable, if the JSON is not what we expect — the run still
|
|
15
|
+
* happens and still completes. These are enhancements that must fail soft.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Name the ProhostAI MCP server is registered under, which is also what makes
|
|
19
|
+
* its tools appear as `mcp__prohost__<tool>`. Referenced in the prompt, in the
|
|
20
|
+
* `--allowedTools` pattern, and in the config file — keep them in sync from
|
|
21
|
+
* here rather than by hand.
|
|
22
|
+
*/
|
|
23
|
+
export declare const MCP_SERVER_NAME = "prohost";
|
|
24
|
+
/**
|
|
25
|
+
* What goes to `--allowedTools` so the ProhostAI tools are callable.
|
|
26
|
+
*
|
|
27
|
+
* Two spellings, comma-joined. `mcp__<server>` is the documented way to approve
|
|
28
|
+
* a whole MCP server and is the one expected to do the work; `mcp__<server>__*`
|
|
29
|
+
* is a pattern people write by analogy with `Bash(git *)`. Sending both costs
|
|
30
|
+
* a few bytes and removes an entire class of silent failure: an unapproved tool
|
|
31
|
+
* in print mode is not an error the agent reports, it is a tool the agent
|
|
32
|
+
* quietly never calls — and the visible symptom is an answer that sounds like
|
|
33
|
+
* the model just didn't feel like looking anything up.
|
|
34
|
+
*
|
|
35
|
+
* Comma-joined into ONE argument rather than passed as two, because
|
|
36
|
+
* `--allowedTools` is variadic (`<tools...>`) and would otherwise keep eating
|
|
37
|
+
* whatever followed it on the command line.
|
|
38
|
+
*/
|
|
39
|
+
export declare const MCP_ALLOWED_TOOLS = "mcp__prohost,mcp__prohost__*";
|
|
40
|
+
/** Filename of the generated MCP config, inside `$PROHOST_HOME`. */
|
|
41
|
+
export declare const MCP_CONFIG_FILENAME = "mcp.json";
|
|
42
|
+
/**
|
|
43
|
+
* Is this exec command Claude Code in print mode?
|
|
44
|
+
*
|
|
45
|
+
* A **heuristic**, and intentionally a narrow one: it matches when the first
|
|
46
|
+
* word of the command is `claude` (or a path ending in `/claude`) *and* the
|
|
47
|
+
* command asks for print mode. Both halves matter. The first because the whole
|
|
48
|
+
* point of `--exec` is that it takes an arbitrary command, and the second
|
|
49
|
+
* because `--output-format` is a print-mode flag — appending it to an
|
|
50
|
+
* interactive invocation would break a command that works today.
|
|
51
|
+
*
|
|
52
|
+
* Things this deliberately does **not** match: `env X=1 claude -p`,
|
|
53
|
+
* `npx claude -p`, a wrapper script that calls Claude Code internally,
|
|
54
|
+
* `sh -c 'claude -p'`. Each of those keeps the plain stdin/stdout behaviour —
|
|
55
|
+
* which is correct output, just without session continuity or MCP tools. The
|
|
56
|
+
* failure direction is what was chosen here: a missed detection costs features,
|
|
57
|
+
* while a false positive would append flags to somebody's shell script and
|
|
58
|
+
* break their agent outright.
|
|
59
|
+
*
|
|
60
|
+
* Nor does it match a command containing shell control operators, even one that
|
|
61
|
+
* starts with `claude`. Flags are appended to the *end* of the string, so for
|
|
62
|
+
* `claude -p | jq -r .result` they would land on `jq` — breaking a command that
|
|
63
|
+
* works today, which is precisely the outcome the narrowness is for.
|
|
64
|
+
*/
|
|
65
|
+
export declare function isClaudeExec(command: string): boolean;
|
|
66
|
+
export interface ClaudeFlags {
|
|
67
|
+
/** Session to resume. Omitted for the first turn on a surface. */
|
|
68
|
+
sessionId?: string;
|
|
69
|
+
/** Path to the generated MCP config, when MCP wiring is active. */
|
|
70
|
+
mcpConfigPath?: string;
|
|
71
|
+
/**
|
|
72
|
+
* Run the agent with permission checks bypassed. Default ON — see
|
|
73
|
+
* {@link claudeCommand}. Set false by `agent run --safe-tools`.
|
|
74
|
+
*/
|
|
75
|
+
skipPermissions?: boolean;
|
|
76
|
+
/** Quoting target. Defaults to the running platform; tests set it. */
|
|
77
|
+
platform?: NodeJS.Platform;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Append the flags this harness needs to a Claude Code command.
|
|
81
|
+
*
|
|
82
|
+
* `--dangerously-skip-permissions` is on by default, by explicit product
|
|
83
|
+
* decision: a paired brain is hired to *do* things, and in print mode there is
|
|
84
|
+
* nobody to approve a tool call — so without it the agent silently declines to
|
|
85
|
+
* act and the feature is a chat box. The trust model this assumes is stated
|
|
86
|
+
* plainly in the README: the brain runs as the host user with that user's full
|
|
87
|
+
* local permissions, on prompts written by workspace teammates. `--safe-tools`
|
|
88
|
+
* opts out for hosts who want prompt-gated behaviour instead.
|
|
89
|
+
*
|
|
90
|
+
* `--strict-mcp-config` is not optional decoration, and matters *more* under
|
|
91
|
+
* skipped permissions rather than less. Without it Claude Code merges the
|
|
92
|
+
* servers in the operator's own configuration with ours, so a run answering a
|
|
93
|
+
* customer's question would hold whatever MCP servers they use for their day
|
|
94
|
+
* job — with permission checks off. Ours is the only one this run is entitled
|
|
95
|
+
* to.
|
|
96
|
+
*/
|
|
97
|
+
export declare function claudeCommand(base: string, flags?: ClaudeFlags): string;
|
|
98
|
+
/** What the harness got back from one Claude Code invocation. */
|
|
99
|
+
export type ClaudeOutput = {
|
|
100
|
+
ok: true;
|
|
101
|
+
text: string;
|
|
102
|
+
sessionId?: string;
|
|
103
|
+
denials: string[];
|
|
104
|
+
} | {
|
|
105
|
+
ok: false;
|
|
106
|
+
error: string;
|
|
107
|
+
sessionId?: string;
|
|
108
|
+
denials: string[];
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Read the reply and the session id out of `--output-format json` stdout.
|
|
112
|
+
*
|
|
113
|
+
* Unparseable output is a **failure**, not a fallback to posting the raw text.
|
|
114
|
+
* We asked for JSON; stdout that isn't JSON means something went wrong that we
|
|
115
|
+
* do not understand — a crash banner, a login prompt, an npm warning — and the
|
|
116
|
+
* one thing that must not happen is that being posted verbatim into a customer's
|
|
117
|
+
* conversation. Reporting the run failed is recoverable; sending a stack trace
|
|
118
|
+
* to a guest is not.
|
|
119
|
+
*/
|
|
120
|
+
export declare function parseClaudeOutput(stdout: string): ClaudeOutput;
|
|
121
|
+
/**
|
|
122
|
+
* Does this look like the resume having been rejected, rather than the agent
|
|
123
|
+
* having failed at its actual work?
|
|
124
|
+
*
|
|
125
|
+
* Sessions do not live forever: the transcript is on disk and gets cleaned up,
|
|
126
|
+
* and the ledger has no way to know. A stale entry must not turn into a
|
|
127
|
+
* permanently broken surface, so the caller retries once without `--resume`.
|
|
128
|
+
*
|
|
129
|
+
* The observed failure is exit 1, empty stdout, and
|
|
130
|
+
* `No conversation found with session ID: <uuid>` on stderr. Matching is
|
|
131
|
+
* broader than that string because the wording is not ours to depend on, and it
|
|
132
|
+
* is gated by the caller on the run having actually failed — so a *successful*
|
|
133
|
+
* run whose reply text happens to quote a session id is never mistaken for one.
|
|
134
|
+
*/
|
|
135
|
+
export declare function looksLikeResumeFailure(output: string, sessionId: string): boolean;
|
|
136
|
+
/** Absolute path of the generated MCP config. */
|
|
137
|
+
export declare function mcpConfigPath(env?: NodeJS.ProcessEnv): string;
|
|
138
|
+
export interface McpConfigOptions {
|
|
139
|
+
/** The MCP endpoint the pairing reported, e.g. `https://connect.prohost.ai/mcp`. */
|
|
140
|
+
mcpUrl: string;
|
|
141
|
+
/** The paired API key. Written to a `0600` file and nowhere else. */
|
|
142
|
+
apiKey: string;
|
|
143
|
+
env?: NodeJS.ProcessEnv;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The config object handed to `--mcp-config`.
|
|
147
|
+
*
|
|
148
|
+
* Split out from the write so a test can assert on the shape without going near
|
|
149
|
+
* the filesystem, and so the header scheme is stated in exactly one place.
|
|
150
|
+
*/
|
|
151
|
+
export declare function buildMcpConfig(options: McpConfigOptions): Record<string, unknown>;
|
|
152
|
+
/**
|
|
153
|
+
* Write the MCP config and return its path.
|
|
154
|
+
*
|
|
155
|
+
* The file holds a live API key, so it gets the same treatment as
|
|
156
|
+
* `agent.json`: `0600` inside a `0700` directory, written with the mode set at
|
|
157
|
+
* create time rather than chmod'ed afterwards — and chmod'ed anyway, because
|
|
158
|
+
* `writeFileSync`'s mode is masked by the umask and is ignored outright when
|
|
159
|
+
* the file already exists.
|
|
160
|
+
*/
|
|
161
|
+
export declare function writeMcpConfig(options: McpConfigOptions): string;
|