@prohost/cli 0.6.0 → 0.8.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 +96 -0
- package/README.md +51 -0
- package/dist/agent/account_runtime.d.ts +75 -0
- package/dist/agent/account_runtime.js +130 -0
- package/dist/agent/accounts.d.ts +162 -0
- package/dist/agent/accounts.js +413 -0
- package/dist/agent/agent_commands.d.ts +22 -0
- package/dist/agent/agent_commands.js +170 -0
- package/dist/agent/api.d.ts +12 -0
- package/dist/agent/api.js +14 -2
- package/dist/agent/claude.d.ts +35 -0
- package/dist/agent/claude.js +205 -0
- package/dist/agent/command.d.ts +18 -1
- package/dist/agent/command.js +88 -4
- package/dist/agent/contract.d.ts +79 -0
- package/dist/agent/contract.js +51 -0
- package/dist/agent/credentials.d.ts +5 -0
- package/dist/agent/events.d.ts +104 -0
- package/dist/agent/events.js +175 -0
- package/dist/agent/list.d.ts +41 -0
- package/dist/agent/list.js +126 -0
- package/dist/agent/lock.d.ts +37 -0
- package/dist/agent/lock.js +91 -0
- package/dist/agent/prompt.js +29 -7
- package/dist/agent/run.d.ts +8 -2
- package/dist/agent/run.js +153 -17
- package/dist/agent/runtime_status.d.ts +70 -4
- package/dist/agent/runtime_status.js +145 -20
- package/dist/agent/workspace.d.ts +40 -0
- package/dist/agent/workspace.js +97 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +27 -5
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,8 +4,104 @@ 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.8.0
|
|
8
|
+
|
|
9
|
+
A paired agent woken as a bystander can now stay silent.
|
|
10
|
+
|
|
11
|
+
- **`reply_expected: false`.** When a team-chat message lands in a thread the
|
|
12
|
+
agent belongs to but is addressed to someone else (ProhostAI's participation
|
|
13
|
+
walk), the run event now says so. The prompt tells the agent the message was
|
|
14
|
+
not addressed to it and nobody is waiting on it — instead of "someone is
|
|
15
|
+
waiting on a reply" — and renders the server's `trigger.brief`, the same
|
|
16
|
+
bystander guidance ProhostAI's in-product agents get. Absent (every older
|
|
17
|
+
server) keeps today's prompt.
|
|
18
|
+
- **`[NO_REPLY]`.** An agent whose whole answer is `[NO_REPLY]` (any case,
|
|
19
|
+
surrounding whitespace ignored) posts nothing, and the run is completed
|
|
20
|
+
`{status: "succeeded", outcome: "silent"}` so ProhostAI records a deliberate
|
|
21
|
+
silence and clears the thinking indicator as one. Checked on every run, so
|
|
22
|
+
the literal token can never reach a thread. A server that predates `outcome`
|
|
23
|
+
ignores it and records a plain success.
|
|
24
|
+
- **`trigger.brief` is rendered** whenever the server sends one that is not just
|
|
25
|
+
the message restated — a work order used to be dropped on any run that also
|
|
26
|
+
had a message.
|
|
27
|
+
|
|
28
|
+
## 0.7.0
|
|
29
|
+
|
|
30
|
+
Machines, accounts and agents: a paired AI employee now runs on a named
|
|
31
|
+
Claude Code or Codex login, and ProhostAI can show — and change — which one.
|
|
32
|
+
|
|
33
|
+
- **Accounts.** `prohost agent account add <label> --runtime claude|codex`
|
|
34
|
+
creates a login in its own config directory under `~/.prohost-accounts`
|
|
35
|
+
(`$PROHOST_ACCOUNTS_ROOT`), prints the exact sign-in command
|
|
36
|
+
(`CLAUDE_CONFIG_DIR=… claude auth login` / `CODEX_HOME=… codex login`) and
|
|
37
|
+
waits until the agent CLI reports it signed in. The CLI never reads the
|
|
38
|
+
login: it asks `claude auth status` / `codex login status` and keeps only
|
|
39
|
+
"signed in or not". `account list` / `account remove` round it out, and
|
|
40
|
+
`default` is the CLI's usual `~/.claude` / `~/.codex`, so existing agents
|
|
41
|
+
change nothing.
|
|
42
|
+
- **One account per agent, applied per run.** `--account <label>` on `pair`
|
|
43
|
+
and `install-daemon` stores it in `agent.json`; every run, idle usage probe
|
|
44
|
+
and Codex MCP probe is spawned with that account's `CLAUDE_CONFIG_DIR` /
|
|
45
|
+
`CODEX_HOME`, so a change applies from the next run with no restart. An agent
|
|
46
|
+
set to an account that is not on the machine refuses to run rather than spend
|
|
47
|
+
another subscription. **Nothing ever switches accounts automatically.**
|
|
48
|
+
- **Picked in ProhostAI.** An `agent.config_updated` event (signed, verified
|
|
49
|
+
like a run) or `auth_ok.agent_config` on reconnect moves the agent to the
|
|
50
|
+
account an operator picked in the web app, from its next run. An account
|
|
51
|
+
that isn't on this machine is ignored with a warning. Picks carry the
|
|
52
|
+
server's `requested_at`; one older than a pick already applied — or than the
|
|
53
|
+
floor `auth_ok` sends on connect — arrived out of order and is dropped.
|
|
54
|
+
- **A stable machine id.** The first agent on a computer writes a random UUID
|
|
55
|
+
to `~/.prohost-accounts/machine.json`; every agent there sends it on auth as
|
|
56
|
+
`machine_id`. ProhostAI keys the machine's accounts by it — the label
|
|
57
|
+
(hostname by default) is display only, since two computers can share one.
|
|
58
|
+
- **The status frame names the account.** `runtime_status` gains `account`
|
|
59
|
+
(random key, label, runtime, billing as the login reports it, sign-in state) and `machine_accounts`
|
|
60
|
+
(every account on the machine), plus per-model weekly windows
|
|
61
|
+
(`weekly_model`, e.g. Fable) from Claude Code's `get_usage`.
|
|
62
|
+
- **Fix: the 5-hour window disappeared after five idle hours.** Claude Code
|
|
63
|
+
omits a rolled-over window from its own `rate_limit_event`, reports an idle
|
|
64
|
+
account's 5-hour window with no reset time, and the harness dropped any
|
|
65
|
+
window without a future reset — so an idle agent showed only its weekly
|
|
66
|
+
window. A window with known usage is now always reported: rolled over or not
|
|
67
|
+
started yet, it is `0%` with `resets_at: null`. New observations are merged
|
|
68
|
+
per window kind instead of replacing the whole list.
|
|
69
|
+
- **`prohost agent list`** shows every agent home on the machine (default,
|
|
70
|
+
`$PROHOST_HOME`, and every home a `ai.prohost.agent.*` launchd plist pins),
|
|
71
|
+
its account, and whether its daemon or a harness is running — plus every
|
|
72
|
+
account with its runtime, billing and sign-in state.
|
|
73
|
+
- **`$PROHOST_HOME/run.lock`.** A second `agent run` on the same home exits
|
|
74
|
+
with the pid that holds it, instead of executing every run twice. A lock
|
|
75
|
+
left by a dead process is taken over.
|
|
76
|
+
- **`prohost agent workspace init --developer --repo <checkout>`** (opt-in)
|
|
77
|
+
adds the ProhostAI PR-protocol skill (a pointer to the repo's
|
|
78
|
+
`pr_protocol.md`, not a copy) and the fleet's `gh pr create` gate as a
|
|
79
|
+
`PreToolUse` hook to the agent workspace's `.claude/`.
|
|
80
|
+
|
|
7
81
|
## 0.6.0
|
|
8
82
|
|
|
83
|
+
The thread now shows what a paired agent is doing, not just that it is doing
|
|
84
|
+
something.
|
|
85
|
+
|
|
86
|
+
- **Tool calls stream into the conversation.** With Claude Code, every
|
|
87
|
+
`tool_use` / `tool_result` in the `--output-format stream-json` stream is
|
|
88
|
+
reported to ProhostAI as it happens — the tool, a one-line preview of its
|
|
89
|
+
input (the command, the path, the query), whether it succeeded, and a short
|
|
90
|
+
line of what came back — and rendered in the thread exactly as an
|
|
91
|
+
in-product AI employee's tool calls are, beneath the "Working on
|
|
92
|
+
\<machine\>…" line. Events are batched on a one-second debounce
|
|
93
|
+
(`POST /v1/agent-runs/{id}/events`, at most 50 per request) and always
|
|
94
|
+
flushed before the reply is posted and before the run is completed.
|
|
95
|
+
- **Best-effort, and additive.** The path comes from the run event
|
|
96
|
+
(`events.path`); a server that doesn't advertise it gets no requests at
|
|
97
|
+
all, and one that answers `404` is told so once per run and then left alone.
|
|
98
|
+
No event failure can fail the run or delay its reply. The 60-second
|
|
99
|
+
heartbeat and its one-line `progress` are unchanged.
|
|
100
|
+
- **What stays on your machine.** Previews are capped at 200 characters and
|
|
101
|
+
only the headline field of a tool's input travels — a `Write`'s file
|
|
102
|
+
content, an `Edit`'s old and new text, never do. The server clips again and
|
|
103
|
+
redacts before anything reaches a browser.
|
|
104
|
+
|
|
9
105
|
ProhostAI can now show which runtime, model and machine a paired AI employee
|
|
10
106
|
runs on, and how much of the operator's Claude or Codex subscription is left.
|
|
11
107
|
|
package/README.md
CHANGED
|
@@ -117,6 +117,10 @@ nothing is posted and the run is left open.
|
|
|
117
117
|
4. Your command's **stdout** is the reply. It's posted into the conversation
|
|
118
118
|
verbatim, so print the message and nothing else — no preamble, no reasoning.
|
|
119
119
|
(Claude Code and Codex are the exceptions: see below.)
|
|
120
|
+
To reply with nothing, print exactly `[NO_REPLY]`: nothing is posted and the
|
|
121
|
+
run is completed as a deliberate silence. That is the expected answer when
|
|
122
|
+
your agent is woken only because it is in a thread where someone else was
|
|
123
|
+
addressed — the prompt says so when that is the case.
|
|
120
124
|
5. The run is completed either way. A command that crashes, times out, or prints
|
|
121
125
|
nothing reports `failed`; a run is never left hanging.
|
|
122
126
|
|
|
@@ -442,6 +446,46 @@ unmodified CLI through its own protocol (`get_usage`,
|
|
|
442
446
|
`codex app-server`), which answers with its own login. Account ids, emails and
|
|
443
447
|
org names are never sent.
|
|
444
448
|
|
|
449
|
+
### Accounts: which login an agent runs on
|
|
450
|
+
|
|
451
|
+
A machine can hold several Claude Code or Codex logins — say a work Max plan
|
|
452
|
+
and a personal one — and each paired agent uses exactly one. Several agents can
|
|
453
|
+
share one account; they then share its limits.
|
|
454
|
+
|
|
455
|
+
```sh
|
|
456
|
+
prohost agent account add work --runtime claude # prints the sign-in command, waits for it
|
|
457
|
+
# CLAUDE_CONFIG_DIR=~/.prohost-accounts/accounts/claude_code/work claude auth login
|
|
458
|
+
prohost agent install-daemon --exec 'claude -p' --account work
|
|
459
|
+
prohost agent account list
|
|
460
|
+
prohost agent list # every agent here, its account and daemon
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
- Each account is its own config directory under `~/.prohost-accounts` (or
|
|
464
|
+
`$PROHOST_ACCOUNTS_ROOT`). You sign in with the agent CLI's own login command;
|
|
465
|
+
`prohost` only asks the CLI whether it is signed in and never reads the login.
|
|
466
|
+
- `default` is the CLI's usual `~/.claude` / `~/.codex`, so an agent paired
|
|
467
|
+
before accounts existed keeps working unchanged.
|
|
468
|
+
- The account is applied to every run (and every usage check) separately, so
|
|
469
|
+
picking another one — with `--account`, or in ProhostAI — takes effect from
|
|
470
|
+
the agent's next run, with no restart.
|
|
471
|
+
- **Accounts never switch on their own.** When an account's limit is reached,
|
|
472
|
+
the agent rests until it resets; moving it to another subscription is always
|
|
473
|
+
a person's decision.
|
|
474
|
+
- ProhostAI sees a random id for each account and the label you gave it. Never
|
|
475
|
+
the email, org or anything from the login.
|
|
476
|
+
|
|
477
|
+
Only one `agent run` can serve an agent home at a time: a second one exits,
|
|
478
|
+
naming the pid that holds `$PROHOST_HOME/run.lock`.
|
|
479
|
+
|
|
480
|
+
### A developer workspace
|
|
481
|
+
|
|
482
|
+
For an agent that works on the ProhostAI codebase, `prohost agent workspace
|
|
483
|
+
init --developer --repo <backend-service checkout>` adds two things to its
|
|
484
|
+
workspace's `.claude/`: a skill that points it at the repo's PR protocol
|
|
485
|
+
(`prohost_agents/support/prompts/pr_protocol.md`, referenced, not copied) and
|
|
486
|
+
the `gh pr create` gate hook the fleet uses. Nothing else about the default
|
|
487
|
+
workspace changes.
|
|
488
|
+
|
|
445
489
|
### The trust model — read this once
|
|
446
490
|
|
|
447
491
|
`agent run` is a standing instruction to execute a command on your machine when
|
|
@@ -514,6 +558,12 @@ for it:
|
|
|
514
558
|
which is what stops the server reaping a run that is still being worked on.
|
|
515
559
|
Against an older ProhostAI server the heartbeat endpoint answers `404`; the
|
|
516
560
|
CLI says so once per run and otherwise behaves exactly as it did before.
|
|
561
|
+
- **The thread sees the tool calls.** With Claude Code, each tool the agent
|
|
562
|
+
calls — and whether it succeeded — is reported to ProhostAI as it happens
|
|
563
|
+
and shown in the conversation under the "Working on your machine…" line,
|
|
564
|
+
the way an in-product AI employee's tool calls are. Only a one-line preview
|
|
565
|
+
travels (the command, the path, the query; never a file's contents), and a
|
|
566
|
+
server that doesn't support it is told nothing.
|
|
517
567
|
- **Cancel works.** Stopping a run in ProhostAI is answered on the next
|
|
518
568
|
heartbeat: the command's whole process group gets `SIGTERM`, then `SIGKILL`
|
|
519
569
|
five seconds later, and the run is reported as cancelled. No reply is posted.
|
|
@@ -576,6 +626,7 @@ decision to make.
|
|
|
576
626
|
| Flag | Default | What it does |
|
|
577
627
|
| --- | --- | --- |
|
|
578
628
|
| `--code` | `$PROHOST_PAIRING_CODE` | The one-time pairing code. |
|
|
629
|
+
| `--account` | the home's current account, else `default` | Which Claude Code / Codex login this agent runs on (see Accounts). |
|
|
579
630
|
| `--webhook-url` | none | Also push events to this HTTPS endpoint. Omit for stream-only, which is the normal laptop setup. |
|
|
580
631
|
| `--base-url` | `https://connect.prohost.ai` | Public API base. |
|
|
581
632
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The account a running harness uses — resolved per spawn, switched on request.
|
|
3
|
+
*
|
|
4
|
+
* `agent run` reads the account from `agent.json` at every run and every idle
|
|
5
|
+
* probe rather than once at start-up, so a switch (made here with
|
|
6
|
+
* `--account`, or in ProhostAI through `agent.config_updated`) takes effect on
|
|
7
|
+
* the next run without restarting or reinstalling the daemon.
|
|
8
|
+
*/
|
|
9
|
+
import type { AccountRecord, AccountRuntime, CommandRunner, ConfigUpdateOutcome } from './accounts.js';
|
|
10
|
+
import type { AccountsSnapshot } from './runtime_status.js';
|
|
11
|
+
export type AccountResolution = {
|
|
12
|
+
ok: true;
|
|
13
|
+
record: AccountRecord;
|
|
14
|
+
} | {
|
|
15
|
+
ok: false;
|
|
16
|
+
error: string;
|
|
17
|
+
};
|
|
18
|
+
export declare class HarnessAccount {
|
|
19
|
+
readonly runtime: AccountRuntime;
|
|
20
|
+
private readonly env;
|
|
21
|
+
private readonly binary;
|
|
22
|
+
private readonly run;
|
|
23
|
+
/** Label from `agent.json` at start-up; the file wins once it has one. */
|
|
24
|
+
private readonly initialLabel;
|
|
25
|
+
/** Label the most recent run was spawned with — what a run in flight is using. */
|
|
26
|
+
private spawnedLabel;
|
|
27
|
+
/** `requested_at` of the newest pick applied, so a late older one is dropped. */
|
|
28
|
+
private appliedRequestedAt;
|
|
29
|
+
constructor(options: {
|
|
30
|
+
runtime: AccountRuntime;
|
|
31
|
+
/** Environment that locates `$PROHOST_HOME` and the accounts root. */
|
|
32
|
+
env?: NodeJS.ProcessEnv;
|
|
33
|
+
initialLabel?: string;
|
|
34
|
+
/** Agent CLI binary used for sign-in checks. */
|
|
35
|
+
binary?: string;
|
|
36
|
+
run?: CommandRunner;
|
|
37
|
+
});
|
|
38
|
+
/** The label this agent home names now. */
|
|
39
|
+
label(): string;
|
|
40
|
+
/**
|
|
41
|
+
* The account for the next spawn. An account named but missing from this
|
|
42
|
+
* machine is an error: running on some other login would spend a
|
|
43
|
+
* subscription nobody picked.
|
|
44
|
+
*/
|
|
45
|
+
resolve(): AccountResolution;
|
|
46
|
+
/** {@link resolve} for a run about to be spawned; remembers which account it got. */
|
|
47
|
+
resolveForSpawn(): AccountResolution;
|
|
48
|
+
/** Whether the chosen account differs from the one the last run was spawned with. */
|
|
49
|
+
changedSinceSpawn(): boolean;
|
|
50
|
+
/** Spawn environment for the current account; empty when it can't be resolved. */
|
|
51
|
+
spawnEnv(): NodeJS.ProcessEnv;
|
|
52
|
+
/**
|
|
53
|
+
* Apply an account picked in ProhostAI. Picks carry `requested_at` (the
|
|
54
|
+
* server stamps them in the order they were made); one older than a pick
|
|
55
|
+
* already applied arrived out of order and is ignored. A pick without a
|
|
56
|
+
* stamp (an older server) always applies.
|
|
57
|
+
*/
|
|
58
|
+
apply(key: unknown, requestedAt?: unknown): ConfigUpdateOutcome;
|
|
59
|
+
/**
|
|
60
|
+
* Raise the floor without applying anything: `auth_ok` carries the newest
|
|
61
|
+
* pick's stamp even when nothing is pending, so a live pick still in flight
|
|
62
|
+
* from before the connect — or one a later pick cancelled — is dropped.
|
|
63
|
+
*/
|
|
64
|
+
noteRequestedAt(requestedAt: unknown): void;
|
|
65
|
+
/**
|
|
66
|
+
* Accounts for the status frame: this agent's, plus every account on the machine.
|
|
67
|
+
*
|
|
68
|
+
* While a run executes, "this agent's" is the account it was spawned with,
|
|
69
|
+
* 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.
|
|
71
|
+
*/
|
|
72
|
+
snapshot(busy?: boolean): Promise<AccountsSnapshot>;
|
|
73
|
+
/** The command that signs the current account in, for a log line. */
|
|
74
|
+
signInHint(): string | undefined;
|
|
75
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The account a running harness uses — resolved per spawn, switched on request.
|
|
3
|
+
*
|
|
4
|
+
* `agent run` reads the account from `agent.json` at every run and every idle
|
|
5
|
+
* probe rather than once at start-up, so a switch (made here with
|
|
6
|
+
* `--account`, or in ProhostAI through `agent.config_updated`) takes effect on
|
|
7
|
+
* the next run without restarting or reinstalling the daemon.
|
|
8
|
+
*/
|
|
9
|
+
import { DEFAULT_ACCOUNT_LABEL, MAX_REPORTED_ACCOUNTS, accountBlock, accountEnv, applyAccountKey, checkAuth, loadAccounts, resolveAgentAccount, runtimeName, signInCommand, } from './accounts.js';
|
|
10
|
+
import { tryLoadCredentials } from './credentials.js';
|
|
11
|
+
export class HarnessAccount {
|
|
12
|
+
runtime;
|
|
13
|
+
env;
|
|
14
|
+
binary;
|
|
15
|
+
run;
|
|
16
|
+
/** Label from `agent.json` at start-up; the file wins once it has one. */
|
|
17
|
+
initialLabel;
|
|
18
|
+
/** Label the most recent run was spawned with — what a run in flight is using. */
|
|
19
|
+
spawnedLabel;
|
|
20
|
+
/** `requested_at` of the newest pick applied, so a late older one is dropped. */
|
|
21
|
+
appliedRequestedAt;
|
|
22
|
+
constructor(options) {
|
|
23
|
+
this.runtime = options.runtime;
|
|
24
|
+
this.env = { ...process.env, ...options.env };
|
|
25
|
+
this.binary = options.binary;
|
|
26
|
+
this.run = options.run;
|
|
27
|
+
this.initialLabel = options.initialLabel;
|
|
28
|
+
}
|
|
29
|
+
/** The label this agent home names now. */
|
|
30
|
+
label() {
|
|
31
|
+
return tryLoadCredentials(this.env)?.account ?? this.initialLabel ?? DEFAULT_ACCOUNT_LABEL;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The account for the next spawn. An account named but missing from this
|
|
35
|
+
* machine is an error: running on some other login would spend a
|
|
36
|
+
* subscription nobody picked.
|
|
37
|
+
*/
|
|
38
|
+
resolve() {
|
|
39
|
+
const label = this.label();
|
|
40
|
+
const record = resolveAgentAccount(label, this.runtime, this.env);
|
|
41
|
+
if (record)
|
|
42
|
+
return { ok: true, record };
|
|
43
|
+
return {
|
|
44
|
+
ok: false,
|
|
45
|
+
error: `this agent is set to the ${runtimeName(this.runtime)} account "${label}", which is not on this machine — ` +
|
|
46
|
+
`add it (prohost agent account add ${label} --runtime ${this.runtime === 'claude_code' ? 'claude' : 'codex'}) ` +
|
|
47
|
+
'or pick another account for this agent in ProhostAI',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** {@link resolve} for a run about to be spawned; remembers which account it got. */
|
|
51
|
+
resolveForSpawn() {
|
|
52
|
+
const resolved = this.resolve();
|
|
53
|
+
if (resolved.ok)
|
|
54
|
+
this.spawnedLabel = resolved.record.label;
|
|
55
|
+
return resolved;
|
|
56
|
+
}
|
|
57
|
+
/** Whether the chosen account differs from the one the last run was spawned with. */
|
|
58
|
+
changedSinceSpawn() {
|
|
59
|
+
return this.spawnedLabel !== undefined && this.spawnedLabel !== this.label();
|
|
60
|
+
}
|
|
61
|
+
/** Spawn environment for the current account; empty when it can't be resolved. */
|
|
62
|
+
spawnEnv() {
|
|
63
|
+
const resolved = this.resolve();
|
|
64
|
+
return resolved.ok ? accountEnv(resolved.record) : {};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Apply an account picked in ProhostAI. Picks carry `requested_at` (the
|
|
68
|
+
* server stamps them in the order they were made); one older than a pick
|
|
69
|
+
* already applied arrived out of order and is ignored. A pick without a
|
|
70
|
+
* stamp (an older server) always applies.
|
|
71
|
+
*/
|
|
72
|
+
apply(key, requestedAt) {
|
|
73
|
+
const at = typeof requestedAt === 'string' ? Date.parse(requestedAt) : Number.NaN;
|
|
74
|
+
if (!Number.isNaN(at) && this.appliedRequestedAt !== undefined && at < this.appliedRequestedAt) {
|
|
75
|
+
return { applied: false, reason: 'a newer account pick was already applied' };
|
|
76
|
+
}
|
|
77
|
+
const outcome = applyAccountKey(key, this.runtime, this.env);
|
|
78
|
+
if (outcome.applied && !Number.isNaN(at))
|
|
79
|
+
this.appliedRequestedAt = at;
|
|
80
|
+
return outcome;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Raise the floor without applying anything: `auth_ok` carries the newest
|
|
84
|
+
* pick's stamp even when nothing is pending, so a live pick still in flight
|
|
85
|
+
* from before the connect — or one a later pick cancelled — is dropped.
|
|
86
|
+
*/
|
|
87
|
+
noteRequestedAt(requestedAt) {
|
|
88
|
+
const at = typeof requestedAt === 'string' ? Date.parse(requestedAt) : Number.NaN;
|
|
89
|
+
if (Number.isNaN(at))
|
|
90
|
+
return;
|
|
91
|
+
if (this.appliedRequestedAt === undefined || at > this.appliedRequestedAt)
|
|
92
|
+
this.appliedRequestedAt = at;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Accounts for the status frame: this agent's, plus every account on the machine.
|
|
96
|
+
*
|
|
97
|
+
* While a run executes, "this agent's" is the account it was spawned with,
|
|
98
|
+
* 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.
|
|
100
|
+
*/
|
|
101
|
+
async snapshot(busy = false) {
|
|
102
|
+
const label = busy && this.spawnedLabel ? this.spawnedLabel : this.label();
|
|
103
|
+
const record = resolveAgentAccount(label, this.runtime, this.env);
|
|
104
|
+
const resolved = record ? { ok: true, record } : { ok: false, error: '' };
|
|
105
|
+
const records = loadAccounts(this.env);
|
|
106
|
+
// The current account first, so a machine with more than the cap still
|
|
107
|
+
// reports the one this agent is on.
|
|
108
|
+
const ordered = resolved.ok
|
|
109
|
+
? [resolved.record, ...records.filter((r) => r.key !== resolved.record.key)]
|
|
110
|
+
: records;
|
|
111
|
+
const all = await Promise.all(ordered.slice(0, MAX_REPORTED_ACCOUNTS).map(async (record) => {
|
|
112
|
+
// Only the login of this runtime's CLI is checked with this binary;
|
|
113
|
+
// another runtime's accounts are checked with that CLI's default name.
|
|
114
|
+
const check = await checkAuth(record, {
|
|
115
|
+
binary: record.runtime === this.runtime ? this.binary : undefined,
|
|
116
|
+
run: this.run,
|
|
117
|
+
});
|
|
118
|
+
return accountBlock(record, check.state, check.billing);
|
|
119
|
+
}));
|
|
120
|
+
const current = resolved.ok ? all.find((a) => a.key === resolved.record.key) : undefined;
|
|
121
|
+
// More accounts than a frame carries (a hand-edited registry): send none
|
|
122
|
+
// rather than a partial list the server would prune to.
|
|
123
|
+
return { current, all: ordered.length > MAX_REPORTED_ACCOUNTS ? undefined : all };
|
|
124
|
+
}
|
|
125
|
+
/** The command that signs the current account in, for a log line. */
|
|
126
|
+
signInHint() {
|
|
127
|
+
const resolved = this.resolve();
|
|
128
|
+
return resolved.ok ? signInCommand(resolved.record) : undefined;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brain accounts — which Claude Code or Codex login a paired agent runs on.
|
|
3
|
+
*
|
|
4
|
+
* A machine can hold several logins ("work", "personal"), each in its own
|
|
5
|
+
* config directory: `CLAUDE_CONFIG_DIR` for Claude Code, `CODEX_HOME` for
|
|
6
|
+
* Codex. Every agent home on the machine uses exactly one of them, named in its
|
|
7
|
+
* `agent.json` as `account`, and several agents may share one (and so share its
|
|
8
|
+
* quota). The implicit `default` account is today's `~/.claude` / `~/.codex`,
|
|
9
|
+
* so an agent paired before accounts existed keeps running exactly as it did.
|
|
10
|
+
*
|
|
11
|
+
* **This module never reads a credential.** Signing in is done by the operator
|
|
12
|
+
* running the agent CLI's own login command with the account's directory; we
|
|
13
|
+
* only ever ask the unmodified CLI whether it is signed in (`claude auth status`
|
|
14
|
+
* → `loggedIn`, `codex login status` → its exit code) and keep nothing else
|
|
15
|
+
* from the answer — not the email, org or account id either one prints.
|
|
16
|
+
*
|
|
17
|
+
* **Accounts never switch on their own.** An exhausted subscription makes the
|
|
18
|
+
* agent rest until its window resets; only an operator (here, or through the
|
|
19
|
+
* picker in ProhostAI, which arrives as `agent.config_updated`) moves an agent
|
|
20
|
+
* to another account.
|
|
21
|
+
*
|
|
22
|
+
* The registry lives in a machine-wide root shared by every agent home —
|
|
23
|
+
* `~/.prohost-accounts`, or `$PROHOST_ACCOUNTS_ROOT` — so `account add` once
|
|
24
|
+
* serves every agent on the machine.
|
|
25
|
+
*/
|
|
26
|
+
/** Server → CLI: an operator picked another account for this agent in ProhostAI. */
|
|
27
|
+
export declare const EVENT_AGENT_CONFIG_UPDATED = "agent.config_updated";
|
|
28
|
+
/** Label of the implicit account that is the agent CLI's own default config dir. */
|
|
29
|
+
export declare const DEFAULT_ACCOUNT_LABEL = "default";
|
|
30
|
+
/** Most accounts reported to the server in one frame (the server caps it too). */
|
|
31
|
+
export declare const MAX_REPORTED_ACCOUNTS = 16;
|
|
32
|
+
export type AccountRuntime = 'claude_code' | 'codex';
|
|
33
|
+
export type AccountBilling = 'subscription' | 'api_key';
|
|
34
|
+
export type AuthState = 'ok' | 'expired' | 'unknown';
|
|
35
|
+
export interface AccountRecord {
|
|
36
|
+
/** Random UUID made here when the account is created — the only identity the server sees. */
|
|
37
|
+
key: string;
|
|
38
|
+
/** Operator-chosen, unique per runtime on this machine. */
|
|
39
|
+
label: string;
|
|
40
|
+
runtime: AccountRuntime;
|
|
41
|
+
billing: AccountBilling;
|
|
42
|
+
/** Config directory for this login; `null` for the implicit default (the CLI's own default). */
|
|
43
|
+
dir: string | null;
|
|
44
|
+
created_at: string;
|
|
45
|
+
}
|
|
46
|
+
/** An account as it goes on the wire (`runtime_status.account` / `machine_accounts[]`). */
|
|
47
|
+
export interface AccountBlock {
|
|
48
|
+
key: string;
|
|
49
|
+
label: string;
|
|
50
|
+
runtime: AccountRuntime;
|
|
51
|
+
billing: AccountBilling;
|
|
52
|
+
auth_state: AuthState;
|
|
53
|
+
}
|
|
54
|
+
export declare class AccountError extends Error {
|
|
55
|
+
}
|
|
56
|
+
export declare function isValidLabel(label: string): boolean;
|
|
57
|
+
/** `claude` → `claude_code`; anything else that is not `codex` is `undefined`. */
|
|
58
|
+
export declare function parseAccountRuntime(value: string | undefined): AccountRuntime | undefined;
|
|
59
|
+
/** Machine-wide root shared by every agent home. */
|
|
60
|
+
export declare function accountsRoot(env?: NodeJS.ProcessEnv): string;
|
|
61
|
+
/** Every account on this machine. An unreadable registry is an empty one. */
|
|
62
|
+
export declare function loadAccounts(env?: NodeJS.ProcessEnv): AccountRecord[];
|
|
63
|
+
/**
|
|
64
|
+
* This computer's id: a random UUID made once and kept in the machine-wide
|
|
65
|
+
* accounts root, so every agent home on the machine sends the same one. It is
|
|
66
|
+
* what ProhostAI keys a machine's accounts by — the label (hostname by
|
|
67
|
+
* default) is display only, since two computers can share one. Nothing about
|
|
68
|
+
* the computer goes into it. `undefined` when it can't be stored, in which
|
|
69
|
+
* case the agent reports no accounts rather than risk another machine's.
|
|
70
|
+
*/
|
|
71
|
+
export declare function machineId(env?: NodeJS.ProcessEnv): string | undefined;
|
|
72
|
+
export declare function findAccount(label: string, runtime: AccountRuntime, env?: NodeJS.ProcessEnv): AccountRecord | undefined;
|
|
73
|
+
/**
|
|
74
|
+
* The implicit default account for a runtime, created on first use so it has
|
|
75
|
+
* a stable key the server can recognise across restarts.
|
|
76
|
+
*/
|
|
77
|
+
export declare function ensureDefaultAccount(runtime: AccountRuntime, env?: NodeJS.ProcessEnv): AccountRecord;
|
|
78
|
+
/** Create an account and its config directory. Signing in is the operator's next step. */
|
|
79
|
+
export declare function addAccount(options: {
|
|
80
|
+
label: string;
|
|
81
|
+
runtime: AccountRuntime;
|
|
82
|
+
billing?: AccountBilling;
|
|
83
|
+
}, env?: NodeJS.ProcessEnv): AccountRecord;
|
|
84
|
+
/**
|
|
85
|
+
* Forget an account and delete its config directory (the login inside it goes
|
|
86
|
+
* with it). The implicit default can't be removed — it is the CLI's own login.
|
|
87
|
+
*/
|
|
88
|
+
export declare function removeAccount(label: string, runtime: AccountRuntime, env?: NodeJS.ProcessEnv): AccountRecord;
|
|
89
|
+
export declare function runtimeName(runtime: AccountRuntime): string;
|
|
90
|
+
/** Environment a spawn needs to run as this account. Empty for the default. */
|
|
91
|
+
export declare function accountEnv(record: AccountRecord | undefined): NodeJS.ProcessEnv;
|
|
92
|
+
/** POSIX single-quote a path for a shell command (pasted, or run as a hook). */
|
|
93
|
+
export declare function shellQuote(value: string): string;
|
|
94
|
+
/** The exact command that signs this account in, for the operator to run. */
|
|
95
|
+
export declare function signInCommand(record: AccountRecord): string;
|
|
96
|
+
export interface AuthCheck {
|
|
97
|
+
state: AuthState;
|
|
98
|
+
/** What the login itself says it bills to, when it says. */
|
|
99
|
+
billing?: AccountBilling;
|
|
100
|
+
}
|
|
101
|
+
/** Runs one short command with extra env; resolves exit code and stdout. Never rejects. */
|
|
102
|
+
export type CommandRunner = (command: string, args: string[], env: NodeJS.ProcessEnv) => Promise<{
|
|
103
|
+
code: number | null;
|
|
104
|
+
stdout: string;
|
|
105
|
+
}>;
|
|
106
|
+
export declare const defaultCommandRunner: CommandRunner;
|
|
107
|
+
/**
|
|
108
|
+
* Ask the agent CLI whether this account is signed in.
|
|
109
|
+
*
|
|
110
|
+
* Claude Code: `claude auth status --json` — only `loggedIn` and `authMethod`
|
|
111
|
+
* are read (the same JSON carries the email and org, which are dropped here).
|
|
112
|
+
* Codex: `codex login status` — exit 0 means signed in; its one line says
|
|
113
|
+
* whether that is ChatGPT or an API key. A CLI that can't be run, or answers
|
|
114
|
+
* something we don't recognise, is `unknown` — never `expired` on a guess.
|
|
115
|
+
*/
|
|
116
|
+
export declare function checkAuth(record: AccountRecord, options?: {
|
|
117
|
+
binary?: string;
|
|
118
|
+
run?: CommandRunner;
|
|
119
|
+
}): Promise<AuthCheck>;
|
|
120
|
+
/**
|
|
121
|
+
* Poll until the operator has finished signing in, or until the deadline.
|
|
122
|
+
* Resolves the last check either way.
|
|
123
|
+
*/
|
|
124
|
+
export declare function waitForSignIn(record: AccountRecord, options: {
|
|
125
|
+
timeoutMs: number;
|
|
126
|
+
intervalMs?: number;
|
|
127
|
+
check?: (record: AccountRecord) => Promise<AuthCheck>;
|
|
128
|
+
sleep?: (ms: number) => Promise<void>;
|
|
129
|
+
now?: () => number;
|
|
130
|
+
}): Promise<AuthCheck>;
|
|
131
|
+
/**
|
|
132
|
+
* Wire block for one account. `billing` is what the login itself reported
|
|
133
|
+
* when it said; the value given at `account add` is only the fallback.
|
|
134
|
+
*/
|
|
135
|
+
export declare function accountBlock(record: AccountRecord, state: AuthState, billing?: AccountBilling): AccountBlock;
|
|
136
|
+
/**
|
|
137
|
+
* The account an agent home runs on: `agent.json`'s `account`, or the default.
|
|
138
|
+
*
|
|
139
|
+
* `undefined` when the home names an account that is not on this machine —
|
|
140
|
+
* the caller refuses to run rather than quietly using another subscription.
|
|
141
|
+
*/
|
|
142
|
+
export declare function resolveAgentAccount(label: string | undefined, runtime: AccountRuntime, env?: NodeJS.ProcessEnv): AccountRecord | undefined;
|
|
143
|
+
export type ConfigUpdateOutcome = {
|
|
144
|
+
applied: true;
|
|
145
|
+
record: AccountRecord;
|
|
146
|
+
changed: boolean;
|
|
147
|
+
} | {
|
|
148
|
+
applied: false;
|
|
149
|
+
reason: string;
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* Apply an account picked in ProhostAI (`agent.config_updated` or
|
|
153
|
+
* `auth_ok.agent_config`): find it on this machine and persist it to the agent
|
|
154
|
+
* home's `agent.json`, so it is used from the next run and survives a restart.
|
|
155
|
+
*
|
|
156
|
+
* An unknown key, or one belonging to another runtime, changes nothing.
|
|
157
|
+
*/
|
|
158
|
+
export declare function applyAccountKey(key: unknown, runtime: AccountRuntime, env?: NodeJS.ProcessEnv): ConfigUpdateOutcome;
|
|
159
|
+
/** Persist an account choice made on this machine (`--account`). Validates it exists. */
|
|
160
|
+
export declare function setAgentAccount(label: string, runtime: AccountRuntime | undefined, env?: NodeJS.ProcessEnv): void;
|
|
161
|
+
/** Whether the registry file exists at all (for `agent list`'s empty state). */
|
|
162
|
+
export declare function hasRegistry(env?: NodeJS.ProcessEnv): boolean;
|