@prohost/cli 0.8.3 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -0
- package/README.md +119 -2
- package/dist/agent/account_runtime.d.ts +19 -3
- package/dist/agent/account_runtime.js +28 -6
- package/dist/agent/accounts.d.ts +17 -1
- package/dist/agent/accounts.js +43 -2
- package/dist/agent/agent_commands.js +6 -3
- package/dist/agent/api.d.ts +6 -0
- package/dist/agent/api.js +10 -5
- package/dist/agent/claude.js +9 -3
- package/dist/agent/command.d.ts +9 -0
- package/dist/agent/command.js +64 -2
- package/dist/agent/daemon.d.ts +4 -0
- package/dist/agent/daemon.js +4 -0
- package/dist/agent/prompt.d.ts +27 -0
- package/dist/agent/prompt.js +21 -1
- package/dist/agent/run.d.ts +28 -3
- package/dist/agent/run.js +90 -18
- package/dist/agent/scheduler.d.ts +44 -0
- package/dist/agent/scheduler.js +93 -0
- package/dist/agent/worktrees.d.ts +136 -0
- package/dist/agent/worktrees.js +317 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +29 -2
- package/dist/usage/backoff.d.ts +42 -0
- package/dist/usage/backoff.js +85 -0
- package/dist/usage/command.d.ts +31 -0
- package/dist/usage/command.js +269 -0
- package/dist/usage/config.d.ts +38 -0
- package/dist/usage/config.js +75 -0
- package/dist/usage/discovery.d.ts +63 -0
- package/dist/usage/discovery.js +187 -0
- package/dist/usage/quota.d.ts +96 -0
- package/dist/usage/quota.js +145 -0
- package/dist/usage/scanner.d.ts +117 -0
- package/dist/usage/scanner.js +438 -0
- package/dist/usage/service.d.ts +122 -0
- package/dist/usage/service.js +299 -0
- 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.10.0
|
|
8
|
+
|
|
9
|
+
Every Claude Code and Codex login on a machine can report its remaining
|
|
10
|
+
capacity and token use to ProhostAI, without pairing an agent.
|
|
11
|
+
|
|
12
|
+
- **`prohost usage connect [--key <key>]`** stores an API key with the
|
|
13
|
+
`usage:write` scope in `$PROHOST_HOME/usage.json` (`0600`, never printed). A
|
|
14
|
+
machine that already paired an agent can skip `--key`; its credential is
|
|
15
|
+
accepted.
|
|
16
|
+
- **Discovery.** `connect`, `usage discover [--dry-run]` and every `usage start`
|
|
17
|
+
register the config directories already on the machine — `~/.claude*`,
|
|
18
|
+
`~/.codex*`, `$CLAUDE_CONFIG_DIR`, `$CODEX_HOME` — in the machine-wide account
|
|
19
|
+
registry, labelled from the directory name (`~/.claude-work` → `work`). Only
|
|
20
|
+
directory names and non-secret markers are looked at.
|
|
21
|
+
- **`prohost usage start`** reports every 5 minutes (`POST /v1/usage/reports`,
|
|
22
|
+
source `cli`): each login's windows from the same idle probes `agent run`
|
|
23
|
+
uses, asked at most every 15 minutes per login, three at a time.
|
|
24
|
+
- **Token totals** from the CLIs' own session logs, summed per local day,
|
|
25
|
+
login, model and project folder name (`POST /v1/usage/token-totals`). Claude
|
|
26
|
+
Code responses are counted once however often their lines repeat; Codex usage
|
|
27
|
+
is the growth of each session's running total, with cached input split out.
|
|
28
|
+
The first run looks back 30 days, then reads only appended lines. No prompt
|
|
29
|
+
text is read into the totals or sent.
|
|
30
|
+
Totals for a login are sent only once a capacity report for it has been
|
|
31
|
+
accepted (`acceptedAccountKeys` when the server names them; otherwise any
|
|
32
|
+
non-zero `accepted`), so a first run or a newly found login reports first.
|
|
33
|
+
Skipped rows follow the server's `skipped[].reason`: `not_owned`,
|
|
34
|
+
`other_source` and `out_of_range` are dropped (logged once), `throttled` is
|
|
35
|
+
retried next pass, `not_reported` waits for the next accepted report. Rows
|
|
36
|
+
skipped without a reason are retried at most three times.
|
|
37
|
+
- **Fails soft.** A 401/403/404 (routes not deployed, feature off, key without
|
|
38
|
+
the scope) is logged once and retried every 30 minutes; network errors back
|
|
39
|
+
off exponentially. A tick never throws.
|
|
40
|
+
- **`prohost usage install-daemon` / `uninstall-daemon`** keep it running under
|
|
41
|
+
launchd with its own label (`ai.prohost.usage.*`), next to any agent daemon.
|
|
42
|
+
**`prohost usage status`** shows what was last sent and local totals.
|
|
43
|
+
- `prohost agent account remove` on a login registered from an existing
|
|
44
|
+
directory forgets it and leaves the directory in place.
|
|
45
|
+
|
|
46
|
+
## 0.9.0
|
|
47
|
+
|
|
48
|
+
A quick question no longer waits behind a long job in another thread.
|
|
49
|
+
|
|
50
|
+
- **`agent run --concurrency <n>`** (1–16, default 1) lets runs in different
|
|
51
|
+
conversations execute at once. Runs in the same conversation still take
|
|
52
|
+
turns in arrival order, because each turn resumes the session the previous
|
|
53
|
+
one left. Without the flag nothing changes: one run at a time, in order.
|
|
54
|
+
Queued runs are heartbeated and can be stopped exactly as before.
|
|
55
|
+
- **`agent run --repos <checkout,…>`** gives every conversation its own
|
|
56
|
+
`git worktree` of each named repository under `$PROHOST_HOME/worktrees`,
|
|
57
|
+
so two runs never share a branch or a half-edited file. The prompt names
|
|
58
|
+
the checkouts and tells the agent to leave the originals alone, and the
|
|
59
|
+
directory is exported as `PROHOST_RUN_REPOS`. A conversation keeps its
|
|
60
|
+
checkouts between runs; they are removed after 14 idle days unless they
|
|
61
|
+
hold uncommitted or untracked files. A checkout that cannot be made does
|
|
62
|
+
not fail the run — the agent is told that repository is read-only for it.
|
|
63
|
+
- **`install-daemon` passes both flags through**, and validates them first so
|
|
64
|
+
a typo cannot crash-loop the daemon.
|
|
65
|
+
- **`PROHOST_PAIRED_RUN=1`** is set for every agent command the harness
|
|
66
|
+
starts. ProhostAI's own PR protocol uses it to end a run once the pull
|
|
67
|
+
request is open instead of holding the slot while CI runs.
|
|
68
|
+
- With `--concurrency` above 1 the agent is told other runs may be executing
|
|
69
|
+
beside it. The strict Claude MCP config is now written atomically, since
|
|
70
|
+
another run's `claude` may be reading it at that moment.
|
|
71
|
+
|
|
7
72
|
## 0.8.3
|
|
8
73
|
|
|
9
74
|
A paired agent's shell steps read as what they do, not "Running a command".
|
package/README.md
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# `@prohost/cli`
|
|
2
2
|
|
|
3
|
-
The [ProhostAI](https://www.prohost.ai) command-line tool.
|
|
3
|
+
The [ProhostAI](https://www.prohost.ai) command-line tool. Three things it does:
|
|
4
4
|
|
|
5
5
|
- **`prohost agent`** — run your own local agent as a ProhostAI teammate. Someone
|
|
6
6
|
@-mentions it in the app, it runs on your machine, and its answer is posted
|
|
7
7
|
back into the conversation.
|
|
8
8
|
- **`prohost listen`** — stream your account's webhooks to a local HTTP endpoint
|
|
9
9
|
while you develop against them (like `stripe listen`).
|
|
10
|
+
- **`prohost usage`** — show your workspace how much Claude Code and Codex
|
|
11
|
+
capacity every login on this machine has left, and how many tokens each one
|
|
12
|
+
used, without pairing an agent.
|
|
10
13
|
|
|
11
14
|
You need a ProhostAI account and **Node.js 18 or newer**. Nothing to install —
|
|
12
15
|
`npx` fetches it on demand:
|
|
@@ -430,7 +433,8 @@ on the machine via `ps` and a process environment is not. Literal `headers`,
|
|
|
430
433
|
treatment: the CLI translates them to Codex's environment-indirection fields,
|
|
431
434
|
so their values do not appear in process arguments.
|
|
432
435
|
|
|
433
|
-
Runs are handled one at a time
|
|
436
|
+
Runs are handled one at a time unless you raise `--concurrency` (see "Running
|
|
437
|
+
several conversations at once"), and each run executes **at most once on this
|
|
434
438
|
machine** — even across restarts. Started runs are recorded in
|
|
435
439
|
`~/.prohost/runs.json` before the command launches, so a webhook redelivery
|
|
436
440
|
that arrives after a crash or restart will close the run out rather than run
|
|
@@ -483,6 +487,48 @@ prohost agent list # every agent here, its ac
|
|
|
483
487
|
Only one `agent run` can serve an agent home at a time: a second one exits,
|
|
484
488
|
naming the pid that holds `$PROHOST_HOME/run.lock`.
|
|
485
489
|
|
|
490
|
+
### Running several conversations at once
|
|
491
|
+
|
|
492
|
+
By default a second request waits for the first, however unrelated they are —
|
|
493
|
+
a one-line question in one thread sits behind a two-hour job in another.
|
|
494
|
+
`--concurrency <n>` (1 to 16) lets up to `n` runs execute at once:
|
|
495
|
+
|
|
496
|
+
```bash
|
|
497
|
+
prohost agent run --exec 'claude -p' --concurrency 3 --repos ~/code/app,~/code/site
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
- **Runs in the same conversation still take turns, in order.** Each turn
|
|
501
|
+
resumes the session the last one left, so two turns of one thread never
|
|
502
|
+
overlap. Only different conversations run side by side.
|
|
503
|
+
- **Every run spends the same login's quota.** Three runs at once use a
|
|
504
|
+
subscription's 5-hour window about three times as fast. Start small.
|
|
505
|
+
- **They share the machine.** The agent is told other runs may be executing
|
|
506
|
+
beside it. If it edits code, also pass `--repos`.
|
|
507
|
+
|
|
508
|
+
`--repos` takes a comma-separated list of git checkouts the agent works on.
|
|
509
|
+
Each conversation then gets its own `git worktree` of every one, at
|
|
510
|
+
`~/.prohost/worktrees/<conversation>/<repo-name>`, made on that conversation's
|
|
511
|
+
first run. The agent is told to do its repository work there and to leave your
|
|
512
|
+
originals alone; the directory is also exported to the command as
|
|
513
|
+
`$PROHOST_RUN_REPOS`. Its working directory does not change — it is still the
|
|
514
|
+
workspace, which is where its configuration and its sessions live.
|
|
515
|
+
|
|
516
|
+
A conversation keeps its checkouts between runs, so a follow-up finds the
|
|
517
|
+
branch the last run left. After 14 days without a run they are removed with
|
|
518
|
+
`git worktree remove`, which refuses a checkout holding uncommitted or
|
|
519
|
+
untracked files — those are kept and logged. Committed branches live in your
|
|
520
|
+
original repository and are never touched. Each worktree is a full working
|
|
521
|
+
copy, and dependencies installed inside it are not shared, so budget disk for
|
|
522
|
+
it.
|
|
523
|
+
|
|
524
|
+
`--repos` works without `--concurrency` too. If a checkout cannot be made (a
|
|
525
|
+
full disk, a path that is no longer a repository), the run still happens: the
|
|
526
|
+
agent is told to treat that repository as read-only for the run.
|
|
527
|
+
|
|
528
|
+
Every command the harness starts has `PROHOST_PAIRED_RUN=1` in its
|
|
529
|
+
environment, so instructions your agent reads from disk can tell a paired run
|
|
530
|
+
from any other use of the same files.
|
|
531
|
+
|
|
486
532
|
### A developer workspace
|
|
487
533
|
|
|
488
534
|
For an agent that works on the ProhostAI codebase, `prohost agent workspace
|
|
@@ -533,6 +579,8 @@ same judgement.
|
|
|
533
579
|
| `--exec` | *(required)* | Command to run for each requested run. Prompt arrives on stdin. |
|
|
534
580
|
| `--idle-timeout` | `900` | Seconds a run may produce **no output at all** before it is treated as stalled and killed. Any byte on stdout or stderr resets it. `0` disables it — see "Long runs". |
|
|
535
581
|
| `--timeout` | `0` | Seconds one run may take before the command is killed, however busy it is. `0` — the default — means no wall-clock limit. Set it if you want a hard ceiling as well. |
|
|
582
|
+
| `--concurrency` | `1` | How many runs may execute at once (1–16). Runs in one conversation always take turns. See "Running several conversations at once". |
|
|
583
|
+
| `--repos` | — | Comma-separated git checkouts the agent works on. Each conversation gets its own worktree of every one. |
|
|
536
584
|
| `--dry-run` | off | Print the reply that would be posted; write nothing. |
|
|
537
585
|
| `--workdir` | `~/.prohost/workspace` | Working directory for the command. See "Where your agent runs". |
|
|
538
586
|
| `--cwd` | — | Deprecated alias for `--workdir`. |
|
|
@@ -683,6 +731,75 @@ through ProhostAI's own approval path, not around it via an API key.
|
|
|
683
731
|
|
|
684
732
|
---
|
|
685
733
|
|
|
734
|
+
## AI usage reporting
|
|
735
|
+
|
|
736
|
+
`prohost usage` reports, for every Claude Code and Codex login on this machine,
|
|
737
|
+
how much of its subscription is left (the 5-hour and weekly windows, with their
|
|
738
|
+
reset times) and how many tokens it used per day, model and project folder.
|
|
739
|
+
ProhostAI shows it in the workspace's AI Usage view. No agent pairing needed.
|
|
740
|
+
|
|
741
|
+
```bash
|
|
742
|
+
# 1. In ProhostAI: Settings → API keys → create a key with the usage:write scope.
|
|
743
|
+
# (A machine with a paired agent can skip --key: its credential is accepted.)
|
|
744
|
+
npx @prohost/cli usage connect --key <api-key>
|
|
745
|
+
|
|
746
|
+
# 2. Report now, then every 5 minutes, in the foreground…
|
|
747
|
+
npx @prohost/cli usage start
|
|
748
|
+
|
|
749
|
+
# …or keep it running across reboots (macOS launchd; prints a systemd unit on Linux).
|
|
750
|
+
npx @prohost/cli usage install-daemon
|
|
751
|
+
|
|
752
|
+
npx @prohost/cli usage status # logins, last report, local token totals
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
**Which logins.** `connect`, `discover` and every `start` register the config
|
|
756
|
+
directories already on this machine: `~/.claude`, `~/.codex` (as `default`) and
|
|
757
|
+
any `~/.claude-<name>` / `~/.codex-<name>` (as `<name>` — `~/.claude-work` is
|
|
758
|
+
`work`), plus `$CLAUDE_CONFIG_DIR` and `$CODEX_HOME`. They join the same
|
|
759
|
+
machine-wide registry `prohost agent account` uses (`~/.prohost-accounts`), so a
|
|
760
|
+
login added there is reported too. `prohost usage discover --dry-run` shows what
|
|
761
|
+
would be registered without writing anything. Removing a discovered login with
|
|
762
|
+
`prohost agent account remove` leaves its directory alone.
|
|
763
|
+
|
|
764
|
+
**Capacity.** Each login is asked at most every 15 minutes, three at a time,
|
|
765
|
+
through the unmodified CLI with that login's directory — `claude`'s `get_usage`
|
|
766
|
+
control request and `codex app-server`'s `account/rateLimits/read`. Neither
|
|
767
|
+
spends a model turn. Sign-in state comes from `claude auth status` /
|
|
768
|
+
`codex login status`. `prohost` never opens a credential file or keychain item.
|
|
769
|
+
|
|
770
|
+
**Tokens.** Read from the session logs the CLIs already write —
|
|
771
|
+
`<config dir>/projects/**/*.jsonl` for Claude Code, `sessions/**/rollout-*.jsonl`
|
|
772
|
+
for Codex — and summed per local day, login, model and project, where the
|
|
773
|
+
project is the **name** of the working directory (`backend-service`, never its
|
|
774
|
+
path). Only usage counters are read: no prompt, reply, tool call or file content
|
|
775
|
+
is kept or sent. The first run looks back 30 days; after that only what was
|
|
776
|
+
appended is read, using a cursor in `$PROHOST_HOME/usage/`. A day that changes
|
|
777
|
+
is re-sent whole, and the server replaces it.
|
|
778
|
+
Totals for a login go only after the server has accepted a capacity report
|
|
779
|
+
for it (the server ignores totals for logins it hasn't seen), so each pass
|
|
780
|
+
reports first. When the server says why it skipped a login's rows, the reason
|
|
781
|
+
decides: another user's or another source's login, or a day out of range, is
|
|
782
|
+
dropped (logged once); `throttled` is retried next pass; `not_reported` waits
|
|
783
|
+
for the next accepted report. Unexplained skips are retried at most three
|
|
784
|
+
times, so a login the server never takes is never resent forever and never
|
|
785
|
+
holds up the others.
|
|
786
|
+
|
|
787
|
+
**If the server says no.** Before the feature is enabled for your workspace
|
|
788
|
+
(or with a key that lacks `usage:write`) the server answers 403/404. The reporter
|
|
789
|
+
logs that once, waits 30 minutes, and tries again — it never exits over it.
|
|
790
|
+
Network errors are retried with backoff. Logs go to `$PROHOST_HOME/usage.log`
|
|
791
|
+
under the daemon.
|
|
792
|
+
|
|
793
|
+
| Command | What it does |
|
|
794
|
+
| --- | --- |
|
|
795
|
+
| `usage connect [--key <key>] [--base-url <url>]` | Store the key (`$PROHOST_HOME/usage.json`, `0600`; `$PROHOST_API_KEY` works too), then discover logins. |
|
|
796
|
+
| `usage discover [--dry-run]` | Register logins found on this machine. |
|
|
797
|
+
| `usage start [--once] [--machine <name>]` | Report every 5 minutes; `--once` runs one pass and exits. |
|
|
798
|
+
| `usage status` | Connection, daemon, last report and upload, and each login's local totals. |
|
|
799
|
+
| `usage install-daemon [--machine <name>]` / `uninstall-daemon` | Keep `usage start` running (its own launchd job, separate from the agent's). |
|
|
800
|
+
|
|
801
|
+
---
|
|
802
|
+
|
|
686
803
|
## Local webhook forwarding
|
|
687
804
|
|
|
688
805
|
```bash
|
|
@@ -24,6 +24,15 @@ export declare class HarnessAccount {
|
|
|
24
24
|
private readonly initialLabel;
|
|
25
25
|
/** Label the most recent run was spawned with — what a run in flight is using. */
|
|
26
26
|
private spawnedLabel;
|
|
27
|
+
/**
|
|
28
|
+
* Labels of the runs executing right now, oldest first.
|
|
29
|
+
*
|
|
30
|
+
* One value was enough while runs were serialized. With `--concurrency`, a
|
|
31
|
+
* pick can land while one run is on the old account and the next run then
|
|
32
|
+
* starts on the new one — and a single field would let the second overwrite
|
|
33
|
+
* what the first is using.
|
|
34
|
+
*/
|
|
35
|
+
private readonly inFlight;
|
|
27
36
|
/** `requested_at` of the newest pick applied, so a late older one is dropped. */
|
|
28
37
|
private appliedRequestedAt;
|
|
29
38
|
constructor(options: {
|
|
@@ -45,8 +54,13 @@ export declare class HarnessAccount {
|
|
|
45
54
|
resolve(): AccountResolution;
|
|
46
55
|
/** {@link resolve} for a run about to be spawned; remembers which account it got. */
|
|
47
56
|
resolveForSpawn(): AccountResolution;
|
|
48
|
-
/**
|
|
49
|
-
|
|
57
|
+
/** The run spawned on `label` has ended. Pairs with {@link resolveForSpawn}. */
|
|
58
|
+
finishSpawn(label: string): void;
|
|
59
|
+
/**
|
|
60
|
+
* Whether the chosen account differs from the one a run was spawned with —
|
|
61
|
+
* the given run's, or the most recent run's when none is named.
|
|
62
|
+
*/
|
|
63
|
+
changedSinceSpawn(spawned?: string | undefined): boolean;
|
|
50
64
|
/** Spawn environment for the current account; empty when it can't be resolved. */
|
|
51
65
|
spawnEnv(): NodeJS.ProcessEnv;
|
|
52
66
|
/**
|
|
@@ -67,7 +81,9 @@ export declare class HarnessAccount {
|
|
|
67
81
|
*
|
|
68
82
|
* While a run executes, "this agent's" is the account it was spawned with,
|
|
69
83
|
* not a newer pick — the server would otherwise mark the pick fulfilled and
|
|
70
|
-
* credit that run's capacity to an account it isn't using.
|
|
84
|
+
* credit that run's capacity to an account it isn't using. With several runs
|
|
85
|
+
* executing it is the oldest one's: the pick is not fulfilled until the last
|
|
86
|
+
* run on the previous account has ended.
|
|
71
87
|
*/
|
|
72
88
|
snapshot(busy?: boolean): Promise<AccountsSnapshot>;
|
|
73
89
|
/** The command that signs the current account in, for a log line. */
|
|
@@ -17,6 +17,15 @@ export class HarnessAccount {
|
|
|
17
17
|
initialLabel;
|
|
18
18
|
/** Label the most recent run was spawned with — what a run in flight is using. */
|
|
19
19
|
spawnedLabel;
|
|
20
|
+
/**
|
|
21
|
+
* Labels of the runs executing right now, oldest first.
|
|
22
|
+
*
|
|
23
|
+
* One value was enough while runs were serialized. With `--concurrency`, a
|
|
24
|
+
* pick can land while one run is on the old account and the next run then
|
|
25
|
+
* starts on the new one — and a single field would let the second overwrite
|
|
26
|
+
* what the first is using.
|
|
27
|
+
*/
|
|
28
|
+
inFlight = [];
|
|
20
29
|
/** `requested_at` of the newest pick applied, so a late older one is dropped. */
|
|
21
30
|
appliedRequestedAt;
|
|
22
31
|
constructor(options) {
|
|
@@ -50,13 +59,24 @@ export class HarnessAccount {
|
|
|
50
59
|
/** {@link resolve} for a run about to be spawned; remembers which account it got. */
|
|
51
60
|
resolveForSpawn() {
|
|
52
61
|
const resolved = this.resolve();
|
|
53
|
-
if (resolved.ok)
|
|
62
|
+
if (resolved.ok) {
|
|
54
63
|
this.spawnedLabel = resolved.record.label;
|
|
64
|
+
this.inFlight.push(resolved.record.label);
|
|
65
|
+
}
|
|
55
66
|
return resolved;
|
|
56
67
|
}
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
|
|
68
|
+
/** The run spawned on `label` has ended. Pairs with {@link resolveForSpawn}. */
|
|
69
|
+
finishSpawn(label) {
|
|
70
|
+
const index = this.inFlight.indexOf(label);
|
|
71
|
+
if (index !== -1)
|
|
72
|
+
this.inFlight.splice(index, 1);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Whether the chosen account differs from the one a run was spawned with —
|
|
76
|
+
* the given run's, or the most recent run's when none is named.
|
|
77
|
+
*/
|
|
78
|
+
changedSinceSpawn(spawned = this.spawnedLabel) {
|
|
79
|
+
return spawned !== undefined && spawned !== this.label();
|
|
60
80
|
}
|
|
61
81
|
/** Spawn environment for the current account; empty when it can't be resolved. */
|
|
62
82
|
spawnEnv() {
|
|
@@ -96,10 +116,12 @@ export class HarnessAccount {
|
|
|
96
116
|
*
|
|
97
117
|
* While a run executes, "this agent's" is the account it was spawned with,
|
|
98
118
|
* not a newer pick — the server would otherwise mark the pick fulfilled and
|
|
99
|
-
* credit that run's capacity to an account it isn't using.
|
|
119
|
+
* credit that run's capacity to an account it isn't using. With several runs
|
|
120
|
+
* executing it is the oldest one's: the pick is not fulfilled until the last
|
|
121
|
+
* run on the previous account has ended.
|
|
100
122
|
*/
|
|
101
123
|
async snapshot(busy = false) {
|
|
102
|
-
const label = busy
|
|
124
|
+
const label = busy ? (this.inFlight[0] ?? this.spawnedLabel ?? this.label()) : this.label();
|
|
103
125
|
const record = resolveAgentAccount(label, this.runtime, this.env);
|
|
104
126
|
const resolved = record ? { ok: true, record } : { ok: false, error: '' };
|
|
105
127
|
const records = loadAccounts(this.env);
|
package/dist/agent/accounts.d.ts
CHANGED
|
@@ -81,9 +81,25 @@ export declare function addAccount(options: {
|
|
|
81
81
|
runtime: AccountRuntime;
|
|
82
82
|
billing?: AccountBilling;
|
|
83
83
|
}, env?: NodeJS.ProcessEnv): AccountRecord;
|
|
84
|
+
/**
|
|
85
|
+
* Register a config directory that already exists — a login the operator made
|
|
86
|
+
* outside this CLI (`~/.claude-work`, a `$CODEX_HOME`). Nothing in it is read
|
|
87
|
+
* or written; signing in stays the agent CLI's own business. A directory that
|
|
88
|
+
* is already registered (under any label) is not registered twice.
|
|
89
|
+
*/
|
|
90
|
+
export declare function registerAccountDir(options: {
|
|
91
|
+
label: string;
|
|
92
|
+
runtime: AccountRuntime;
|
|
93
|
+
dir: string;
|
|
94
|
+
billing?: AccountBilling;
|
|
95
|
+
}, env?: NodeJS.ProcessEnv): AccountRecord;
|
|
96
|
+
/** Whether removing this account deletes its directory (only ones this CLI created). */
|
|
97
|
+
export declare function ownsAccountDir(record: AccountRecord, env?: NodeJS.ProcessEnv): boolean;
|
|
84
98
|
/**
|
|
85
99
|
* Forget an account and delete its config directory (the login inside it goes
|
|
86
|
-
* with it)
|
|
100
|
+
* with it) — but only a directory this CLI created; a registered existing one
|
|
101
|
+
* is left where it is. The implicit default can't be removed — it is the CLI's
|
|
102
|
+
* own login.
|
|
87
103
|
*/
|
|
88
104
|
export declare function removeAccount(label: string, runtime: AccountRuntime, env?: NodeJS.ProcessEnv): AccountRecord;
|
|
89
105
|
export declare function runtimeName(runtime: AccountRuntime): string;
|
package/dist/agent/accounts.js
CHANGED
|
@@ -236,9 +236,50 @@ function addAccountLocked(label, runtime, billing, env) {
|
|
|
236
236
|
saveAccounts([...loadAccounts(env), record], env);
|
|
237
237
|
return record;
|
|
238
238
|
}
|
|
239
|
+
/**
|
|
240
|
+
* Register a config directory that already exists — a login the operator made
|
|
241
|
+
* outside this CLI (`~/.claude-work`, a `$CODEX_HOME`). Nothing in it is read
|
|
242
|
+
* or written; signing in stays the agent CLI's own business. A directory that
|
|
243
|
+
* is already registered (under any label) is not registered twice.
|
|
244
|
+
*/
|
|
245
|
+
export function registerAccountDir(options, env = process.env) {
|
|
246
|
+
const { label, runtime } = options;
|
|
247
|
+
if (!isValidLabel(label) || label === DEFAULT_ACCOUNT_LABEL) {
|
|
248
|
+
throw new AccountError(`"${label}" is not a usable label for an existing login directory.`);
|
|
249
|
+
}
|
|
250
|
+
const dir = path.resolve(options.dir);
|
|
251
|
+
return withRegistryLock(env, () => {
|
|
252
|
+
const accounts = loadAccounts(env);
|
|
253
|
+
const same = accounts.find((a) => a.runtime === runtime && a.dir !== null && path.resolve(a.dir) === dir);
|
|
254
|
+
if (same)
|
|
255
|
+
return same;
|
|
256
|
+
if (findAccount(label, runtime, env)) {
|
|
257
|
+
throw new AccountError(`A ${runtimeName(runtime)} account named "${label}" already exists on this machine.`);
|
|
258
|
+
}
|
|
259
|
+
if (accounts.filter((a) => a.label !== DEFAULT_ACCOUNT_LABEL).length >= MAX_REPORTED_ACCOUNTS - 2) {
|
|
260
|
+
throw new AccountError(`This machine already has ${MAX_REPORTED_ACCOUNTS - 2} accounts, the most ProhostAI can show. Remove one first.`);
|
|
261
|
+
}
|
|
262
|
+
const record = {
|
|
263
|
+
key: randomUUID(),
|
|
264
|
+
label,
|
|
265
|
+
runtime,
|
|
266
|
+
billing: options.billing ?? 'subscription',
|
|
267
|
+
dir,
|
|
268
|
+
created_at: new Date().toISOString(),
|
|
269
|
+
};
|
|
270
|
+
saveAccounts([...accounts, record], env);
|
|
271
|
+
return record;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
/** Whether removing this account deletes its directory (only ones this CLI created). */
|
|
275
|
+
export function ownsAccountDir(record, env = process.env) {
|
|
276
|
+
return !!record.dir && record.dir.startsWith(path.join(accountsRoot(env), 'accounts') + path.sep);
|
|
277
|
+
}
|
|
239
278
|
/**
|
|
240
279
|
* Forget an account and delete its config directory (the login inside it goes
|
|
241
|
-
* with it)
|
|
280
|
+
* with it) — but only a directory this CLI created; a registered existing one
|
|
281
|
+
* is left where it is. The implicit default can't be removed — it is the CLI's
|
|
282
|
+
* own login.
|
|
242
283
|
*/
|
|
243
284
|
export function removeAccount(label, runtime, env = process.env) {
|
|
244
285
|
if (label === DEFAULT_ACCOUNT_LABEL) {
|
|
@@ -253,7 +294,7 @@ export function removeAccount(label, runtime, env = process.env) {
|
|
|
253
294
|
return found;
|
|
254
295
|
});
|
|
255
296
|
// Only ever a directory this module created under the root.
|
|
256
|
-
if (record
|
|
297
|
+
if (ownsAccountDir(record, env) && record.dir) {
|
|
257
298
|
rmSync(record.dir, { recursive: true, force: true });
|
|
258
299
|
}
|
|
259
300
|
return record;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* `accounts.ts`, `list.ts` and `workspace.ts`.
|
|
5
5
|
*/
|
|
6
6
|
import process from 'node:process';
|
|
7
|
-
import { AccountError, addAccount, checkAuth, loadAccounts, parseAccountRuntime, removeAccount, runtimeName, signInCommand, waitForSignIn, } from './accounts.js';
|
|
7
|
+
import { AccountError, addAccount, checkAuth, loadAccounts, ownsAccountDir, parseAccountRuntime, removeAccount, runtimeName, signInCommand, waitForSignIn, } from './accounts.js';
|
|
8
8
|
import { ensureWorkspace } from './credentials.js';
|
|
9
9
|
import { findAgentHomes, renderAgentList } from './list.js';
|
|
10
10
|
import { WorkspaceInitError, initDeveloperWorkspace } from './workspace.js';
|
|
@@ -118,8 +118,9 @@ export async function accountCommand(args, flags, io = defaultIO) {
|
|
|
118
118
|
'Move them to another account first, or pass --force.');
|
|
119
119
|
return 1;
|
|
120
120
|
}
|
|
121
|
+
let removed;
|
|
121
122
|
try {
|
|
122
|
-
removeAccount(label, target?.runtime ?? runtime ?? 'claude_code', env);
|
|
123
|
+
removed = removeAccount(label, target?.runtime ?? runtime ?? 'claude_code', env);
|
|
123
124
|
}
|
|
124
125
|
catch (err) {
|
|
125
126
|
if (err instanceof AccountError) {
|
|
@@ -128,7 +129,9 @@ export async function accountCommand(args, flags, io = defaultIO) {
|
|
|
128
129
|
}
|
|
129
130
|
throw err;
|
|
130
131
|
}
|
|
131
|
-
io.out(
|
|
132
|
+
io.out(ownsAccountDir(removed, env)
|
|
133
|
+
? `✓ Removed "${label}" and its login directory.`
|
|
134
|
+
: `✓ Removed "${label}". Its login directory${removed.dir ? ` (${removed.dir})` : ''} was left in place.`);
|
|
132
135
|
return 0;
|
|
133
136
|
}
|
|
134
137
|
io.err('Expected: prohost agent account add|list|remove');
|
package/dist/agent/api.d.ts
CHANGED
|
@@ -68,6 +68,12 @@ export declare function describeFailure(result: ApiResult): string;
|
|
|
68
68
|
* cached, and the next replay receives its response.
|
|
69
69
|
*/
|
|
70
70
|
export declare function isIdempotencyInFlight(result: ApiResult): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* POST a JSON body with the API key. Never throws: every failure comes back as
|
|
73
|
+
* an {@link ApiResult}. Exported for the `prohost usage` reporter, which owns
|
|
74
|
+
* its own paths.
|
|
75
|
+
*/
|
|
76
|
+
export declare function postJson(options: ApiOptions, path: string, body: unknown, idempotencyKey?: string, captureBody?: boolean, timeoutMs?: number): Promise<ApiResult>;
|
|
71
77
|
/**
|
|
72
78
|
* Post the agent's reply into the conversation that triggered the run.
|
|
73
79
|
*
|
package/dist/agent/api.js
CHANGED
|
@@ -52,7 +52,12 @@ export function describeFailure(result) {
|
|
|
52
52
|
export function isIdempotencyInFlight(result) {
|
|
53
53
|
return result.status === 409 && (result.error ?? '').includes('idempotency_key_in_flight');
|
|
54
54
|
}
|
|
55
|
-
|
|
55
|
+
/**
|
|
56
|
+
* POST a JSON body with the API key. Never throws: every failure comes back as
|
|
57
|
+
* an {@link ApiResult}. Exported for the `prohost usage` reporter, which owns
|
|
58
|
+
* its own paths.
|
|
59
|
+
*/
|
|
60
|
+
export async function postJson(options, path, body, idempotencyKey, captureBody = false, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
56
61
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
57
62
|
const controller = new AbortController();
|
|
58
63
|
const deadlineMs = options.timeoutMs ?? timeoutMs;
|
|
@@ -126,7 +131,7 @@ export async function postReply(options, path, bodyKey, message, runId, extra) {
|
|
|
126
131
|
// thing in this request that must survive.
|
|
127
132
|
// Byte-identical on every attempt: the server hashes the body against the
|
|
128
133
|
// key, so a replay must serialise exactly as the original did.
|
|
129
|
-
return
|
|
134
|
+
return postJson(options, path, { ...extra, [bodyKey]: message }, `prohost-cli-run-${runId}`, false, SEND_TIMEOUT_MS);
|
|
130
135
|
}
|
|
131
136
|
function booleanField(body, key) {
|
|
132
137
|
if (!body)
|
|
@@ -154,7 +159,7 @@ function booleanField(body, key) {
|
|
|
154
159
|
*/
|
|
155
160
|
export async function heartbeatRun(options, path, progress) {
|
|
156
161
|
const trimmed = progress?.trim();
|
|
157
|
-
const result = await
|
|
162
|
+
const result = await postJson(options, path, trimmed ? { progress: trimmed.slice(0, MAX_PROGRESS_CHARS) } : {}, undefined, true);
|
|
158
163
|
if (result.ok)
|
|
159
164
|
return { kind: 'alive', cancelRequested: booleanField(result.body, 'cancel_requested') };
|
|
160
165
|
if (result.status === 404)
|
|
@@ -180,7 +185,7 @@ export async function completeRun(options, path, outcome, model) {
|
|
|
180
185
|
: { status: RUN_STATUS_FAILED, error: outcome.error.slice(0, MAX_ERROR_CHARS) };
|
|
181
186
|
if (model)
|
|
182
187
|
body.model = model.slice(0, MAX_MODEL_CHARS);
|
|
183
|
-
const result = await
|
|
188
|
+
const result = await postJson(options, path, body, undefined, false, SEND_TIMEOUT_MS);
|
|
184
189
|
if (result.status === 409)
|
|
185
190
|
return { ...result, ok: true };
|
|
186
191
|
return result;
|
|
@@ -195,5 +200,5 @@ export async function completeRun(options, path, outcome, model) {
|
|
|
195
200
|
* over-fills one gets it clipped rather than rejected whole.
|
|
196
201
|
*/
|
|
197
202
|
export async function postToolEvents(options, path, events) {
|
|
198
|
-
return
|
|
203
|
+
return postJson(options, path, { events: events.slice(0, MAX_TOOL_EVENTS_PER_BATCH) });
|
|
199
204
|
}
|
package/dist/agent/claude.js
CHANGED
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
* session is unresumable, if the JSON is not what we expect — the run still
|
|
15
15
|
* happens and still completes. These are enhancements that must fail soft.
|
|
16
16
|
*/
|
|
17
|
-
import {
|
|
17
|
+
import { randomBytes } from 'node:crypto';
|
|
18
|
+
import { chmodSync, mkdirSync, renameSync, writeFileSync } from 'node:fs';
|
|
18
19
|
import path from 'node:path';
|
|
19
20
|
import { prohostHome } from './credentials.js';
|
|
20
21
|
import { MCP_SERVER_NAME } from './mcp.js';
|
|
@@ -697,7 +698,12 @@ export function writeMcpConfig(options) {
|
|
|
697
698
|
const file = mcpConfigPath(env);
|
|
698
699
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
699
700
|
chmodSync(dir, 0o700);
|
|
700
|
-
|
|
701
|
-
|
|
701
|
+
// Written beside the target and renamed over it. Every run rewrites this
|
|
702
|
+
// file just before spawning, and with `--concurrency` another run's `claude`
|
|
703
|
+
// may be reading it at that instant — a plain write would let it read half.
|
|
704
|
+
const staged = `${file}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`;
|
|
705
|
+
writeFileSync(staged, `${JSON.stringify(buildMcpConfig(options), null, 2)}\n`, { mode: 0o600 });
|
|
706
|
+
chmodSync(staged, 0o600);
|
|
707
|
+
renameSync(staged, file);
|
|
702
708
|
return file;
|
|
703
709
|
}
|
package/dist/agent/command.d.ts
CHANGED
|
@@ -29,6 +29,15 @@ export declare const INVALID_DURATION: unique symbol;
|
|
|
29
29
|
* had not.
|
|
30
30
|
*/
|
|
31
31
|
export declare function parseSeconds(raw: string | boolean | undefined): number | undefined | typeof INVALID_DURATION;
|
|
32
|
+
/** Returned by {@link parseConcurrency} for a value that isn't a run count. */
|
|
33
|
+
export declare const INVALID_CONCURRENCY: unique symbol;
|
|
34
|
+
/**
|
|
35
|
+
* Read `--concurrency`. Pure, like {@link parseSeconds}, and strict for the
|
|
36
|
+
* same reason: `--concurrency` with nothing after it arrives as `true`, and
|
|
37
|
+
* treating that as absent would leave someone believing their agent runs four
|
|
38
|
+
* things at once while it runs one.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseConcurrency(raw: string | boolean | undefined): number | undefined | typeof INVALID_CONCURRENCY;
|
|
32
41
|
/**
|
|
33
42
|
* Environment the daemon must be given explicitly.
|
|
34
43
|
*
|