@prohost/cli 0.9.0 → 0.10.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 +63 -0
- package/README.md +77 -2
- 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/base-url.d.ts +43 -0
- package/dist/agent/base-url.js +72 -0
- package/dist/agent/command.js +21 -6
- package/dist/agent/credentials.d.ts +9 -0
- package/dist/agent/credentials.js +1 -1
- package/dist/agent/pair.js +41 -20
- package/dist/index.d.ts +1 -0
- package/dist/index.js +27 -2
- package/dist/usage/backoff.d.ts +42 -0
- package/dist/usage/backoff.js +85 -0
- package/dist/usage/command.d.ts +34 -0
- package/dist/usage/command.js +344 -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/key-input.d.ts +36 -0
- package/dist/usage/key-input.js +79 -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 +307 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/agent/pair.js
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
* specific than "invalid, expired, or already used".
|
|
11
11
|
*/
|
|
12
12
|
import { USER_AGENT } from '../version.js';
|
|
13
|
+
import { baseUrlProblem, endpointProblem, isAllowedBaseUrl } from './base-url.js';
|
|
13
14
|
import { DEFAULT_API_BASE_URL, REDEEM_PATH } from './contract.js';
|
|
14
|
-
import { CREDENTIALS_VERSION, clearCredentials, maskSecret, saveCredentials, tryLoadCredentials, } from './credentials.js';
|
|
15
|
+
import { CREDENTIALS_VERSION, clearCredentials, maskSecret, saveCredentials, tryLoadCredentials, wsUrlFromBaseUrl, } from './credentials.js';
|
|
15
16
|
import { DEFAULT_WS_URL } from '../listen.js';
|
|
16
17
|
export class PairError extends Error {
|
|
17
18
|
}
|
|
@@ -20,28 +21,34 @@ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{
|
|
|
20
21
|
function isUuid(value) {
|
|
21
22
|
return typeof value === 'string' && UUID_PATTERN.test(value);
|
|
22
23
|
}
|
|
23
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Derive the API base from the server-reported MCP URL, so the server wins —
|
|
26
|
+
* unless it is one the API key may not be sent to (see `base-url.ts`).
|
|
27
|
+
*/
|
|
24
28
|
function baseFromMcpUrl(mcpUrl, fallback) {
|
|
25
|
-
|
|
29
|
+
const base = mcpUrl.endsWith('/mcp') ? mcpUrl.slice(0, -'/mcp'.length) : '';
|
|
30
|
+
return base && isAllowedBaseUrl(base) ? base : fallback;
|
|
26
31
|
}
|
|
27
32
|
/**
|
|
28
33
|
* The websocket URL the server told us to use, or ``undefined``.
|
|
29
34
|
*
|
|
30
|
-
* ``undefined`` covers
|
|
31
|
-
*
|
|
32
|
-
* is already spent by the time we read the body — so
|
|
33
|
-
*
|
|
35
|
+
* ``undefined`` covers "this server predates the field", "the value is not a
|
|
36
|
+
* websocket URL" and "it is plain ws:// off loopback". A bad value must not
|
|
37
|
+
* fail a redeem — the code is already spent by the time we read the body — so
|
|
38
|
+
* the caller falls back to the stream on the stored API base's host and says so.
|
|
34
39
|
*/
|
|
35
40
|
function serverWsUrl(value) {
|
|
36
41
|
if (typeof value !== 'string' || value.length === 0)
|
|
37
42
|
return undefined;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
// `wss://`, or `ws://` on loopback only: the stream's auth frame carries the key.
|
|
44
|
+
return endpointProblem(value, 'ws') === undefined ? value : undefined;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The MCP URL to store: the server's, unless the API key may not be sent to
|
|
48
|
+
* it, in which case the one on the (already validated) API base.
|
|
49
|
+
*/
|
|
50
|
+
function safeMcpUrl(mcpUrl, baseUrl) {
|
|
51
|
+
return mcpUrl && isAllowedBaseUrl(mcpUrl) ? mcpUrl : `${baseUrl}/mcp`;
|
|
45
52
|
}
|
|
46
53
|
/**
|
|
47
54
|
* Redeem ``code`` and persist the resulting credentials.
|
|
@@ -52,6 +59,14 @@ export async function runPair(options) {
|
|
|
52
59
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
53
60
|
const onLine = options.onLine ?? ((line) => process.stdout.write(`${line}\n`));
|
|
54
61
|
const baseUrl = (options.baseUrl ?? DEFAULT_API_BASE_URL).replace(/\/+$/, '');
|
|
62
|
+
// Checked before the code is spent: the code goes to this URL, and the API
|
|
63
|
+
// key comes back from it.
|
|
64
|
+
const baseProblem = baseUrlProblem(baseUrl);
|
|
65
|
+
if (baseProblem)
|
|
66
|
+
throw new PairError(`--base-url rejected: ${baseProblem}`);
|
|
67
|
+
const wsProblem = options.wsUrl ? endpointProblem(options.wsUrl, 'ws') : undefined;
|
|
68
|
+
if (wsProblem)
|
|
69
|
+
throw new PairError(`--url rejected: ${wsProblem}`);
|
|
55
70
|
if (options.webhookUrl && !options.webhookUrl.startsWith('https://')) {
|
|
56
71
|
throw new PairError('--webhook-url must be an https:// URL');
|
|
57
72
|
}
|
|
@@ -136,18 +151,24 @@ export async function runPair(options) {
|
|
|
136
151
|
// 2. The server's `ws_url`, derived server-side from the same setting as
|
|
137
152
|
// `mcp_url` (`shared/agents/connections.py::pairing_endpoints`). This is
|
|
138
153
|
// the normal path, and the reason a host move no longer needs a release.
|
|
139
|
-
// 3. `
|
|
154
|
+
// 3. No usable `ws_url` (a server that predates the field, or a value that
|
|
155
|
+
// is not a websocket URL or is plain ws:// off loopback): the stream on
|
|
156
|
+
// the stored API base's host, as the server derives it — never another
|
|
157
|
+
// deployment's, which would get this key. For the default base that is
|
|
158
|
+
// `DEFAULT_WS_URL`, which is also the last resort.
|
|
159
|
+
const storedBase = baseFromMcpUrl(body.mcp_url ?? '', baseUrl);
|
|
140
160
|
const fromServer = serverWsUrl(body.ws_url);
|
|
141
|
-
const
|
|
161
|
+
const fallbackWsUrl = wsUrlFromBaseUrl(storedBase) ?? DEFAULT_WS_URL;
|
|
162
|
+
const wsUrl = options.wsUrl ?? fromServer ?? fallbackWsUrl;
|
|
142
163
|
const credentials = {
|
|
143
164
|
version: CREDENTIALS_VERSION,
|
|
144
|
-
base_url:
|
|
165
|
+
base_url: storedBase,
|
|
145
166
|
ws_url: wsUrl,
|
|
146
167
|
api_key: body.api_key,
|
|
147
168
|
webhook_secret: body.webhook_secret ?? null,
|
|
148
169
|
subscription_id: body.subscription_id ?? null,
|
|
149
170
|
stream_only: body.stream_only,
|
|
150
|
-
mcp_url: body.mcp_url,
|
|
171
|
+
mcp_url: safeMcpUrl(body.mcp_url, storedBase),
|
|
151
172
|
events: body.events ?? [],
|
|
152
173
|
agent: body.agent,
|
|
153
174
|
paired_at: new Date().toISOString(),
|
|
@@ -156,7 +177,7 @@ export async function runPair(options) {
|
|
|
156
177
|
onLine(`✓ Paired as "${body.agent.name}" (agent ${body.agent.id})`);
|
|
157
178
|
onLine(` API key ${maskSecret(body.api_key)} (stored, never printed in full)`);
|
|
158
179
|
onLine(` Credentials ${file} (chmod 600)`);
|
|
159
|
-
onLine(` MCP URL ${
|
|
180
|
+
onLine(` MCP URL ${credentials.mcp_url}`);
|
|
160
181
|
// Printed because 0.1.0's wrong websocket host was invisible here: pairing
|
|
161
182
|
// reported success and the mismatch only surfaced as a 403 reconnect loop in
|
|
162
183
|
// a different command. Showing it makes a bad host obvious at pair time.
|
|
@@ -171,7 +192,7 @@ export async function runPair(options) {
|
|
|
171
192
|
// answer either.
|
|
172
193
|
onLine('');
|
|
173
194
|
onLine(` ! The server reported an unusable stream URL (${JSON.stringify(body.ws_url)}).`);
|
|
174
|
-
onLine(` Using ${
|
|
195
|
+
onLine(` Using ${fallbackWsUrl} instead — override with --url if that is wrong.`);
|
|
175
196
|
}
|
|
176
197
|
if (!credentials.webhook_secret) {
|
|
177
198
|
// Without a secret the runner can't prove an event came from ProhostAI,
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* npx @prohost/cli agent pair --code <pairing-code>
|
|
9
9
|
* npx @prohost/cli agent run --exec 'claude -p'
|
|
10
10
|
* npx @prohost/cli agent install-daemon --exec 'claude -p'
|
|
11
|
+
* npx @prohost/cli usage connect --key <api-key>
|
|
11
12
|
*
|
|
12
13
|
* Parses arguments with a zero-dep, zero-magic flag parser so the published
|
|
13
14
|
* binary stays small and starts fast.
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* npx @prohost/cli agent pair --code <pairing-code>
|
|
9
9
|
* npx @prohost/cli agent run --exec 'claude -p'
|
|
10
10
|
* npx @prohost/cli agent install-daemon --exec 'claude -p'
|
|
11
|
+
* npx @prohost/cli usage connect --key <api-key>
|
|
11
12
|
*
|
|
12
13
|
* Parses arguments with a zero-dep, zero-magic flag parser so the published
|
|
13
14
|
* binary stays small and starts fast.
|
|
@@ -15,8 +16,10 @@
|
|
|
15
16
|
import { realpathSync } from 'node:fs';
|
|
16
17
|
import process from 'node:process';
|
|
17
18
|
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { endpointProblem } from './agent/base-url.js';
|
|
18
20
|
import { runAgent } from './agent/command.js';
|
|
19
21
|
import { DEFAULT_WS_URL, runListen } from './listen.js';
|
|
22
|
+
import { runUsage } from './usage/command.js';
|
|
20
23
|
import { CLI_VERSION } from './version.js';
|
|
21
24
|
function printHelp() {
|
|
22
25
|
const lines = [
|
|
@@ -33,6 +36,9 @@ function printHelp() {
|
|
|
33
36
|
' prohost agent account list | remove <label>',
|
|
34
37
|
' prohost agent list Every paired agent on this machine, and its accounts',
|
|
35
38
|
' prohost agent workspace init --developer --repo <checkout> [--workdir <dir>]',
|
|
39
|
+
' prohost usage connect [--key -] [--base-url <url>] Report AI usage without pairing an agent (prompts for the key; --key - reads it from stdin)',
|
|
40
|
+
' prohost usage discover [--dry-run] | start [--once] [--machine <name>] | status',
|
|
41
|
+
' prohost usage install-daemon [--machine <name>] | uninstall-daemon',
|
|
36
42
|
'',
|
|
37
43
|
'listen flags:',
|
|
38
44
|
' --forward-to Local URL to POST each webhook payload to (required)',
|
|
@@ -44,7 +50,7 @@ function printHelp() {
|
|
|
44
50
|
'agent pair flags:',
|
|
45
51
|
' --code One-time pairing code from the hire modal. Defaults to $PROHOST_PAIRING_CODE.',
|
|
46
52
|
' --webhook-url Also push events to this HTTPS endpoint. Omit it: stream-only is the normal setup.',
|
|
47
|
-
' --base-url Public API base. Defaults to https://connect.prohost.ai.',
|
|
53
|
+
' --base-url Public API base. Defaults to https://connect.prohost.ai. Must be https:// (http:// only for localhost).',
|
|
48
54
|
'',
|
|
49
55
|
'agent run flags:',
|
|
50
56
|
' --exec Command to run for each mention; the prompt arrives on stdin (required)',
|
|
@@ -99,6 +105,15 @@ function printHelp() {
|
|
|
99
105
|
' Adds the ProhostAI PR-protocol skill and the `gh pr create` gate hook from --repo (a',
|
|
100
106
|
' backend-service checkout) to the agent workspace\'s .claude/. Opt-in; nothing else changes.',
|
|
101
107
|
'',
|
|
108
|
+
'Usage reporting (observer mode):',
|
|
109
|
+
' `usage connect` stores an API key with the usage:write scope ($PROHOST_HOME/usage.json, owner-only;',
|
|
110
|
+
' a machine with a paired agent can reuse its credential) and registers every Claude Code and Codex',
|
|
111
|
+
' login it finds: ~/.claude*, ~/.codex*, $CLAUDE_CONFIG_DIR, $CODEX_HOME (`~/.claude-work` → "work").',
|
|
112
|
+
' `usage start` then reports, every 5 minutes, each login\'s remaining capacity — asked of the',
|
|
113
|
+
' unmodified CLI, at most every 15 minutes per login — and daily token totals per model and project',
|
|
114
|
+
' folder, read from the local session logs (last 30 days, then incrementally). Never prompt text.',
|
|
115
|
+
' `--once` runs one pass and exits. `usage status` shows what was last sent.',
|
|
116
|
+
'',
|
|
102
117
|
'agent install-daemon flags: (macOS: launchd; Linux: prints a systemd-user unit)',
|
|
103
118
|
' --exec The same command you would pass to `agent run` (required).',
|
|
104
119
|
' --workdir, --concurrency, --repos, --idle-timeout, --timeout, --machine, --url,',
|
|
@@ -148,7 +163,8 @@ export function parseArgs(argv) {
|
|
|
148
163
|
i += 1;
|
|
149
164
|
continue;
|
|
150
165
|
}
|
|
151
|
-
|
|
166
|
+
// A bare `-` is a value ("read it from stdin", as in `--key -`), not a flag.
|
|
167
|
+
if (next === undefined || (next.startsWith('-') && next !== '-')) {
|
|
152
168
|
flags[key] = true;
|
|
153
169
|
i += 1;
|
|
154
170
|
}
|
|
@@ -182,6 +198,9 @@ async function main(argv) {
|
|
|
182
198
|
if (command === 'agent') {
|
|
183
199
|
return runAgent(subcommand, flags, printHelp, args);
|
|
184
200
|
}
|
|
201
|
+
if (command === 'usage') {
|
|
202
|
+
return runUsage(subcommand, flags, printHelp);
|
|
203
|
+
}
|
|
185
204
|
if (command !== 'listen') {
|
|
186
205
|
process.stderr.write(`Unknown command: ${command}\n\n`);
|
|
187
206
|
printHelp();
|
|
@@ -201,6 +220,12 @@ async function main(argv) {
|
|
|
201
220
|
const url = (typeof flags['url'] === 'string' ? flags['url'] : undefined) ??
|
|
202
221
|
process.env.PROHOST_WS_URL ??
|
|
203
222
|
DEFAULT_WS_URL;
|
|
223
|
+
// The auth frame carries the API key: wss://, or ws:// on loopback only.
|
|
224
|
+
const urlProblem = endpointProblem(url, 'ws');
|
|
225
|
+
if (urlProblem) {
|
|
226
|
+
process.stderr.write(`✗ --url rejected: ${urlProblem}\n`);
|
|
227
|
+
return 1;
|
|
228
|
+
}
|
|
204
229
|
const machine = typeof flags['machine'] === 'string' ? flags['machine'] : undefined;
|
|
205
230
|
const subscriptionId = typeof flags['subscription-id'] === 'string' ? flags['subscription-id'] : undefined;
|
|
206
231
|
const controller = new AbortController();
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* When the usage reporter may try an endpoint again, and what it says about it.
|
|
3
|
+
*
|
|
4
|
+
* The reporter runs for weeks under launchd, so it must never crash and never
|
|
5
|
+
* fill a log. Three kinds of failure, treated differently:
|
|
6
|
+
*
|
|
7
|
+
* - **Gated** — 401, 403, 404. The routes are not deployed yet, the workspace
|
|
8
|
+
* doesn't have the feature, or the key lacks `usage:write`. None of that
|
|
9
|
+
* changes in minutes, so wait {@link GATED_BACKOFF_MS} and say so once —
|
|
10
|
+
* not on every retry.
|
|
11
|
+
* - **Transient** — no response, 408, 429, 5xx. Retry with exponential backoff
|
|
12
|
+
* from {@link BASE_BACKOFF_MS} up to {@link MAX_BACKOFF_MS}; logged once
|
|
13
|
+
* per streak.
|
|
14
|
+
* - **Rejected** — any other 4xx. A body this build got wrong; retrying soon
|
|
15
|
+
* won't fix it, so it waits like a gated one and logs the server's reason.
|
|
16
|
+
*
|
|
17
|
+
* A success after any of these says "recovered" once and resets everything.
|
|
18
|
+
*/
|
|
19
|
+
import type { ApiResult } from '../agent/api.js';
|
|
20
|
+
export declare const BASE_BACKOFF_MS = 60000;
|
|
21
|
+
export declare const MAX_BACKOFF_MS: number;
|
|
22
|
+
export declare const GATED_BACKOFF_MS: number;
|
|
23
|
+
export type FailureKind = 'gated' | 'transient' | 'rejected';
|
|
24
|
+
export declare function classifyFailure(result: ApiResult): FailureKind;
|
|
25
|
+
export declare class Backoff {
|
|
26
|
+
private readonly name;
|
|
27
|
+
private readonly now;
|
|
28
|
+
private nextAt;
|
|
29
|
+
private failures;
|
|
30
|
+
private lastKind;
|
|
31
|
+
private lastStatus;
|
|
32
|
+
constructor(name: string, now?: () => number);
|
|
33
|
+
/** Whether a request may be made now. */
|
|
34
|
+
ready(): boolean;
|
|
35
|
+
/** When the next attempt is allowed (epoch ms; 0 = now). */
|
|
36
|
+
retryAt(): number;
|
|
37
|
+
/**
|
|
38
|
+
* Record an attempt's outcome. Returns a line to log, or `undefined` when
|
|
39
|
+
* this outcome was already reported.
|
|
40
|
+
*/
|
|
41
|
+
record(result: ApiResult): string | undefined;
|
|
42
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* When the usage reporter may try an endpoint again, and what it says about it.
|
|
3
|
+
*
|
|
4
|
+
* The reporter runs for weeks under launchd, so it must never crash and never
|
|
5
|
+
* fill a log. Three kinds of failure, treated differently:
|
|
6
|
+
*
|
|
7
|
+
* - **Gated** — 401, 403, 404. The routes are not deployed yet, the workspace
|
|
8
|
+
* doesn't have the feature, or the key lacks `usage:write`. None of that
|
|
9
|
+
* changes in minutes, so wait {@link GATED_BACKOFF_MS} and say so once —
|
|
10
|
+
* not on every retry.
|
|
11
|
+
* - **Transient** — no response, 408, 429, 5xx. Retry with exponential backoff
|
|
12
|
+
* from {@link BASE_BACKOFF_MS} up to {@link MAX_BACKOFF_MS}; logged once
|
|
13
|
+
* per streak.
|
|
14
|
+
* - **Rejected** — any other 4xx. A body this build got wrong; retrying soon
|
|
15
|
+
* won't fix it, so it waits like a gated one and logs the server's reason.
|
|
16
|
+
*
|
|
17
|
+
* A success after any of these says "recovered" once and resets everything.
|
|
18
|
+
*/
|
|
19
|
+
import { describeFailure } from '../agent/api.js';
|
|
20
|
+
export const BASE_BACKOFF_MS = 60_000;
|
|
21
|
+
export const MAX_BACKOFF_MS = 30 * 60_000;
|
|
22
|
+
export const GATED_BACKOFF_MS = 30 * 60_000;
|
|
23
|
+
export function classifyFailure(result) {
|
|
24
|
+
const { status } = result;
|
|
25
|
+
if (status === 401 || status === 403 || status === 404)
|
|
26
|
+
return 'gated';
|
|
27
|
+
if (status === 0 || status === 408 || status === 429 || status >= 500)
|
|
28
|
+
return 'transient';
|
|
29
|
+
return 'rejected';
|
|
30
|
+
}
|
|
31
|
+
export class Backoff {
|
|
32
|
+
name;
|
|
33
|
+
now;
|
|
34
|
+
nextAt = 0;
|
|
35
|
+
failures = 0;
|
|
36
|
+
lastKind;
|
|
37
|
+
lastStatus;
|
|
38
|
+
constructor(name, now = Date.now) {
|
|
39
|
+
this.name = name;
|
|
40
|
+
this.now = now;
|
|
41
|
+
}
|
|
42
|
+
/** Whether a request may be made now. */
|
|
43
|
+
ready() {
|
|
44
|
+
return this.now() >= this.nextAt;
|
|
45
|
+
}
|
|
46
|
+
/** When the next attempt is allowed (epoch ms; 0 = now). */
|
|
47
|
+
retryAt() {
|
|
48
|
+
return this.nextAt;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Record an attempt's outcome. Returns a line to log, or `undefined` when
|
|
52
|
+
* this outcome was already reported.
|
|
53
|
+
*/
|
|
54
|
+
record(result) {
|
|
55
|
+
if (result.ok) {
|
|
56
|
+
const wasFailing = this.failures > 0;
|
|
57
|
+
this.failures = 0;
|
|
58
|
+
this.nextAt = 0;
|
|
59
|
+
this.lastKind = undefined;
|
|
60
|
+
this.lastStatus = undefined;
|
|
61
|
+
return wasFailing ? `${this.name}: recovered` : undefined;
|
|
62
|
+
}
|
|
63
|
+
const kind = classifyFailure(result);
|
|
64
|
+
this.failures += 1;
|
|
65
|
+
const delay = kind === 'transient'
|
|
66
|
+
? Math.min(MAX_BACKOFF_MS, BASE_BACKOFF_MS * 2 ** Math.min(this.failures - 1, 16))
|
|
67
|
+
: GATED_BACKOFF_MS;
|
|
68
|
+
this.nextAt = this.now() + delay;
|
|
69
|
+
const repeat = this.lastKind === kind && (kind === 'transient' || this.lastStatus === result.status);
|
|
70
|
+
this.lastKind = kind;
|
|
71
|
+
this.lastStatus = result.status;
|
|
72
|
+
if (repeat)
|
|
73
|
+
return undefined;
|
|
74
|
+
const minutes = Math.round(delay / 60_000);
|
|
75
|
+
if (kind === 'gated') {
|
|
76
|
+
return (`${this.name}: ${describeFailure(result)} — usage reporting is not available for this key yet ` +
|
|
77
|
+
`(not deployed, not enabled for the workspace, or the key lacks usage:write). ` +
|
|
78
|
+
`Retrying every ${minutes} min.`);
|
|
79
|
+
}
|
|
80
|
+
if (kind === 'rejected') {
|
|
81
|
+
return `${this.name}: ${describeFailure(result)} rejected${result.error ? ` (${result.error.slice(0, 200)})` : ''}. Retrying in ${minutes} min.`;
|
|
82
|
+
}
|
|
83
|
+
return `${this.name}: ${describeFailure(result)}. Retrying with backoff (next in ${minutes} min).`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `prohost usage …` — observer mode: report this machine's Claude Code and
|
|
3
|
+
* Codex capacity and token totals to ProhostAI without pairing an agent.
|
|
4
|
+
*
|
|
5
|
+
* Flag plumbing only; the work lives in `discovery.ts`, `quota.ts`,
|
|
6
|
+
* `scanner.ts` and `service.ts`.
|
|
7
|
+
*/
|
|
8
|
+
import type { DiscoveryResult } from './discovery.js';
|
|
9
|
+
import type { KeyStdin } from './key-input.js';
|
|
10
|
+
type Flags = Record<string, string | boolean>;
|
|
11
|
+
export interface UsageIO {
|
|
12
|
+
out: (line: string) => void;
|
|
13
|
+
err: (line: string) => void;
|
|
14
|
+
env?: NodeJS.ProcessEnv;
|
|
15
|
+
/** Where `connect` reads a key from (`--key -`, a pipe, or a hidden prompt). Defaults to `process.stdin`. */
|
|
16
|
+
stdin?: KeyStdin;
|
|
17
|
+
}
|
|
18
|
+
/** launchd label prefix for the usage reporter — distinct from the agent daemon's. */
|
|
19
|
+
export declare const USAGE_LAUNCHD_PREFIX = "ai.prohost.usage";
|
|
20
|
+
/** Label for the usage daemon serving one `$PROHOST_HOME`. Same digest as the agent's, other prefix. */
|
|
21
|
+
export declare function usageLaunchdLabel(home: string): string;
|
|
22
|
+
export declare function usageLogPath(env?: NodeJS.ProcessEnv): string;
|
|
23
|
+
/** `prohost usage start` argv for a supervisor. */
|
|
24
|
+
export declare function usageDaemonArguments(options: {
|
|
25
|
+
execPath: string;
|
|
26
|
+
scriptPath: string;
|
|
27
|
+
machine?: string;
|
|
28
|
+
}): string[];
|
|
29
|
+
/** Dispatch a `usage` subcommand. Returns the exit code. */
|
|
30
|
+
export declare function runUsage(subcommand: string | undefined, flags: Flags, printHelp: () => void, io?: UsageIO): Promise<number>;
|
|
31
|
+
export declare function renderDiscovery(result: DiscoveryResult, env: NodeJS.ProcessEnv, dryRun: boolean): string[];
|
|
32
|
+
/** `1234567` → `1.2M`. */
|
|
33
|
+
export declare function compactTokens(n: number): string;
|
|
34
|
+
export {};
|