@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.
@@ -91,6 +91,10 @@ const CLAUDE_WINDOWS = [
91
91
  ['five_hour', 'session'],
92
92
  ['seven_day', 'weekly'],
93
93
  ];
94
+ /** One window, or nothing when the usage itself is unknown. */
95
+ function quotaWindow(kind, used, resets) {
96
+ return used === undefined ? [] : [{ kind, used_percent: used, resets_at: resets ?? null }];
97
+ }
94
98
  /**
95
99
  * Windows from a stream `rate_limit_event.rate_limit_info`.
96
100
  *
@@ -101,24 +105,57 @@ const CLAUDE_WINDOWS = [
101
105
  export function fromClaudeRateLimitInfo(info) {
102
106
  const windows = asObject(asObject(info)?.unifiedWindows) ?? {};
103
107
  return CLAUDE_WINDOWS.flatMap(([key, kind]) => {
104
- const window = asObject(windows[key]);
105
- const used = percent(window?.utilization, 1);
106
- const at = isoTime(window?.resetsAt);
107
- return used !== undefined && at ? [{ kind, used_percent: used, resets_at: at }] : [];
108
+ const entry = asObject(windows[key]);
109
+ const used = percent(entry?.utilization, 1);
110
+ const at = isoTime(entry?.resetsAt);
111
+ // The event's schema requires `resetsAt` on every window it sends (it
112
+ // omits a rolled-over window instead), so one without it is malformed.
113
+ return at ? quotaWindow(kind, used, at) : [];
108
114
  });
109
115
  }
110
- /** Windows from a `get_usage` control response (0–100, ISO strings). */
116
+ /**
117
+ * Windows from a `get_usage` control response (0–100, ISO strings).
118
+ *
119
+ * A window with a usage but a `null` reset is kept, with `resets_at: null` —
120
+ * that is how an idle account's 5-hour window reads, and dropping it is what
121
+ * left paired agents showing a weekly window and nothing else (B3).
122
+ *
123
+ * Per-model weekly windows come from `rate_limits.limits[]` rows of kind
124
+ * `weekly_scoped` (classified on `kind`, as the schema asks, never on a
125
+ * label), falling back to `model_scoped[]`.
126
+ */
111
127
  export function fromClaudeGetUsage(usage) {
112
128
  const record = asObject(usage);
113
129
  if (record?.rate_limits_available !== true)
114
130
  return [];
115
131
  const limits = asObject(record.rate_limits) ?? {};
116
- return CLAUDE_WINDOWS.flatMap(([key, kind]) => {
117
- const window = asObject(limits[key]);
118
- const used = percent(window?.utilization, 100);
119
- const at = isoTime(window?.resets_at);
120
- return used !== undefined && at ? [{ kind, used_percent: used, resets_at: at }] : [];
132
+ const windows = CLAUDE_WINDOWS.flatMap(([key, kind]) => {
133
+ const entry = asObject(limits[key]);
134
+ return quotaWindow(kind, percent(entry?.utilization, 100), isoTime(entry?.resets_at));
121
135
  });
136
+ const perModel = new Map();
137
+ const rows = Array.isArray(limits.limits) ? limits.limits : [];
138
+ for (const raw of rows) {
139
+ const row = asObject(raw);
140
+ if (row?.kind !== 'weekly_scoped')
141
+ continue;
142
+ const model = text(asObject(asObject(row.scope)?.model)?.display_name);
143
+ const used = percent(row.percent, 100);
144
+ if (!model || used === undefined || perModel.has(model))
145
+ continue;
146
+ perModel.set(model, { kind: 'weekly_model', used_percent: used, resets_at: isoTime(row.resets_at) ?? null, model_label: model });
147
+ }
148
+ if (perModel.size === 0 && Array.isArray(limits.model_scoped)) {
149
+ for (const raw of limits.model_scoped) {
150
+ const row = asObject(raw);
151
+ const model = text(row?.display_name);
152
+ const used = percent(row?.utilization, 100);
153
+ if (!model || used === undefined || perModel.has(model))
154
+ continue;
155
+ perModel.set(model, { kind: 'weekly_model', used_percent: used, resets_at: isoTime(row?.resets_at) ?? null, model_label: model });
156
+ }
157
+ }
158
+ return [...windows, ...perModel.values()];
122
159
  }
123
160
  const CLAUDE_PLANS = {
124
161
  pro: 'Claude Pro',
@@ -163,7 +200,7 @@ export function fromCodexRateLimits(rateLimits, lineTimestamp) {
163
200
  if (!Number.isNaN(base))
164
201
  at = new Date(base + window.resets_in_seconds * 1000).toISOString();
165
202
  }
166
- return kind && used !== undefined && at ? [{ kind, used_percent: used, resets_at: at }] : [];
203
+ return kind ? quotaWindow(kind, used, at) : [];
167
204
  });
168
205
  }
169
206
  /** `plan_type` / `planType` → `ChatGPT Pro`; `undefined` when there is no plan. */
@@ -290,7 +327,7 @@ export function readCodexObservation(threadId, env = process.env) {
290
327
  * Spawn ``command args``, write ``lines`` as JSONL, and resolve with the first
291
328
  * stdout line ``pick`` accepts. Kills the child on answer, error or timeout.
292
329
  */
293
- function exchangeJsonLines(spawnImpl, command, args, lines, pick, timeoutMs) {
330
+ function exchangeJsonLines(spawnImpl, command, args, lines, pick, timeoutMs, env) {
294
331
  return new Promise((resolve) => {
295
332
  let settled = false;
296
333
  let buffer = '';
@@ -311,7 +348,12 @@ function exchangeJsonLines(spawnImpl, command, args, lines, pick, timeoutMs) {
311
348
  const timer = setTimeout(() => finish(undefined), timeoutMs);
312
349
  timer.unref?.();
313
350
  try {
314
- child = spawnImpl(command, args, { stdio: ['pipe', 'pipe', 'ignore'] });
351
+ // `env` selects the account (CLAUDE_CONFIG_DIR / CODEX_HOME) — the
352
+ // probe must ask the login the agent actually runs on.
353
+ child = spawnImpl(command, args, {
354
+ stdio: ['pipe', 'pipe', 'ignore'],
355
+ ...(env ? { env: { ...process.env, ...env } } : {}),
356
+ });
315
357
  }
316
358
  catch {
317
359
  finish(undefined);
@@ -371,7 +413,7 @@ export function claudeQuotaProvider(options) {
371
413
  plan_label: claudePlanLabel(usage.subscription_type),
372
414
  ...(windows.length > 0 ? { quota_windows: windows } : {}),
373
415
  };
374
- }, options.timeoutMs ?? PROVIDER_TIMEOUT_MS),
416
+ }, options.timeoutMs ?? PROVIDER_TIMEOUT_MS, options.env?.()),
375
417
  };
376
418
  }
377
419
  /** Codex's `account/rateLimits/read` over `codex app-server` stdio JSON-RPC. */
@@ -396,7 +438,7 @@ export function codexQuotaProvider(options) {
396
438
  plan_label: codexPlanLabel(rateLimits.planType),
397
439
  ...(windows.length > 0 ? { quota_windows: windows } : {}),
398
440
  };
399
- }, options.timeoutMs ?? PROVIDER_TIMEOUT_MS),
441
+ }, options.timeoutMs ?? PROVIDER_TIMEOUT_MS, options.env?.()),
400
442
  };
401
443
  }
402
444
  // ── The reporter ───────────────────────────────────────────────────────────
@@ -405,6 +447,31 @@ function band(windows) {
405
447
  const top = Math.max(0, ...(windows ?? []).map((w) => w.used_percent));
406
448
  return top >= EXHAUSTED_PERCENT ? 2 : top >= LOW_PERCENT ? 1 : 0;
407
449
  }
450
+ /**
451
+ * Fold new windows into the ones already known, one KIND at a time.
452
+ *
453
+ * Sources see different slices: a run's `rate_limit_event` carries only the
454
+ * 5-hour and weekly windows (and omits one whose reset has passed), while the
455
+ * idle `get_usage` probe also carries the per-model weekly ones. Replacing the
456
+ * whole list with whatever arrived last is how a stream event used to erase
457
+ * the windows it simply didn't mention. A kind the observation names replaces
458
+ * every window of that kind; a kind it doesn't name is kept.
459
+ */
460
+ export function mergeWindows(known, observed) {
461
+ const kinds = new Set(observed.map((w) => w.kind));
462
+ return [...(known ?? []).filter((w) => !kinds.has(w.kind)), ...observed];
463
+ }
464
+ /**
465
+ * A window as of ``nowMs``: one whose reset has passed has rolled over, so it
466
+ * is reported as a fresh window — 0%, not started — rather than dropped. The
467
+ * agent CLI omits a rolled-over window from its own events, so this is the
468
+ * only place that knows it exists.
469
+ */
470
+ function asOf(window, nowMs) {
471
+ if (window.resets_at === null || Date.parse(window.resets_at) > nowMs)
472
+ return window;
473
+ return { ...window, used_percent: 0, resets_at: null };
474
+ }
408
475
  function defaultSchedule(fn, ms) {
409
476
  const timer = setTimeout(fn, ms);
410
477
  timer.unref?.();
@@ -430,6 +497,9 @@ export class RuntimeStatusReporter {
430
497
  cancelPending;
431
498
  cancelTick;
432
499
  refreshing;
500
+ accountGeneration = 0;
501
+ accounts;
502
+ accountsObservedAt;
433
503
  constructor(options) {
434
504
  this.options = options;
435
505
  this.now = options.now ?? Date.now;
@@ -448,7 +518,7 @@ export class RuntimeStatusReporter {
448
518
  if (observation.plan_label)
449
519
  next.plan_label = observation.plan_label;
450
520
  if (observation.quota_windows && observation.quota_windows.length > 0) {
451
- next.quota_windows = observation.quota_windows;
521
+ next.quota_windows = mergeWindows(before.quota_windows, observation.quota_windows);
452
522
  this.quotaObservedAt = this.now();
453
523
  }
454
524
  this.state = next;
@@ -459,6 +529,41 @@ export class RuntimeStatusReporter {
459
529
  if (changed)
460
530
  this.sendSoon();
461
531
  }
532
+ /**
533
+ * Record the accounts snapshot. Moving to another account drops what was
534
+ * known about the previous one's capacity and plan — they describe a
535
+ * different subscription — and asks for fresh numbers straight away.
536
+ */
537
+ setAccounts(snapshot, options = {}) {
538
+ const before = this.accounts;
539
+ this.accounts = snapshot;
540
+ this.accountsObservedAt = this.now();
541
+ const switched = before?.current?.key !== snapshot?.current?.key;
542
+ if (switched) {
543
+ this.accountGeneration += 1;
544
+ // A probe belongs to the login it started with. Let the new login
545
+ // refresh immediately even while an older probe is still finishing.
546
+ this.refreshing = undefined;
547
+ const { quota_windows: _windows, plan_label: _plan, ...rest } = this.state;
548
+ this.state = rest;
549
+ this.quotaObservedAt = undefined;
550
+ }
551
+ if (!options.quiet && JSON.stringify(before) !== JSON.stringify(snapshot))
552
+ this.sendSoon();
553
+ }
554
+ /**
555
+ * The account choice changed (an operator picked one in ProhostAI): re-read
556
+ * accounts and capacity on the next tick rather than in fifteen minutes.
557
+ */
558
+ accountsChanged() {
559
+ this.accountsObservedAt = undefined;
560
+ this.quotaObservedAt = undefined;
561
+ if (!this.send)
562
+ return;
563
+ this.cancelTick?.();
564
+ this.cancelTick = undefined;
565
+ void this.tick();
566
+ }
462
567
  /** The model to report on a run's completion, when nothing more specific is known. */
463
568
  model() {
464
569
  return this.state.model;
@@ -481,8 +586,7 @@ export class RuntimeStatusReporter {
481
586
  /** The frame as it would be sent now. */
482
587
  frame() {
483
588
  const nowMs = this.now();
484
- // A window whose reset has passed describes a period that is over.
485
- const windows = (this.state.quota_windows ?? []).filter((w) => Date.parse(w.resets_at) > nowMs);
589
+ const windows = (this.state.quota_windows ?? []).map((w) => asOf(w, nowMs));
486
590
  return {
487
591
  type: 'runtime_status',
488
592
  runtime: this.options.runtime,
@@ -491,6 +595,9 @@ export class RuntimeStatusReporter {
491
595
  plan_label: this.state.plan_label ?? null,
492
596
  quota_windows: windows,
493
597
  observed_at: new Date(nowMs).toISOString(),
598
+ // Optional additions: an older server ignores keys it doesn't know.
599
+ ...(this.accounts?.current ? { account: this.accounts.current } : {}),
600
+ ...(this.accounts?.all ? { machine_accounts: this.accounts.all } : {}),
494
601
  };
495
602
  }
496
603
  /** Routine tick: refresh idle quota if it is stale, send, and re-arm. */
@@ -499,6 +606,19 @@ export class RuntimeStatusReporter {
499
606
  return;
500
607
  const stale = this.quotaObservedAt === undefined || this.now() - this.quotaObservedAt >= QUOTA_REFRESH_AFTER_MS;
501
608
  const sentBefore = this.lastSentAt;
609
+ const accountsStale = this.accountsObservedAt === undefined || this.now() - this.accountsObservedAt >= QUOTA_REFRESH_AFTER_MS;
610
+ if (accountsStale && this.options.accounts) {
611
+ // Before the quota probe: after a switch it must ask the new login.
612
+ try {
613
+ // Quiet: the tick sends one frame once the quota probe below is in.
614
+ this.setAccounts(await this.options.accounts(), { quiet: true });
615
+ }
616
+ catch {
617
+ /* fail soft — keep what we had */
618
+ }
619
+ if (!this.send)
620
+ return;
621
+ }
502
622
  if (stale && this.options.provider && !this.options.isBusy?.()) {
503
623
  await this.refresh();
504
624
  }
@@ -513,8 +633,11 @@ export class RuntimeStatusReporter {
513
633
  refresh() {
514
634
  if (!this.refreshing) {
515
635
  const provider = this.options.provider;
516
- this.refreshing = (provider ? provider.refresh() : Promise.resolve(undefined))
636
+ const generation = this.accountGeneration;
637
+ const refreshing = (provider ? provider.refresh() : Promise.resolve(undefined))
517
638
  .then((observed) => {
639
+ if (generation !== this.accountGeneration)
640
+ return;
518
641
  // `observe` would itself schedule a send on a change; the caller
519
642
  // sends right after, which the spacing rule coalesces.
520
643
  this.observe(observed);
@@ -523,8 +646,10 @@ export class RuntimeStatusReporter {
523
646
  /* fail soft — keep what we had */
524
647
  })
525
648
  .finally(() => {
526
- this.refreshing = undefined;
649
+ if (this.refreshing === refreshing)
650
+ this.refreshing = undefined;
527
651
  });
652
+ this.refreshing = refreshing;
528
653
  }
529
654
  return this.refreshing;
530
655
  }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * `prohost agent workspace init --developer` — make a paired agent's workspace
3
+ * one that opens pull requests the way the ProhostAI fleet does.
4
+ *
5
+ * Opt-in: the default workspace is left exactly as it is. With `--developer`
6
+ * and a path to a ProhostAI checkout (`--repo`), this writes two things into
7
+ * the workspace's `.claude/`:
8
+ *
9
+ * - a skill, `prohost-pr-protocol`, that points the agent at the repo's own
10
+ * `prohost_agents/support/prompts/pr_protocol.md` — by absolute path, not a
11
+ * copy, so the protocol has one source and cannot drift;
12
+ * - the fleet's `gh pr create` gate (`prohost_agents/platform/pr_create_gate.sh`)
13
+ * as a `PreToolUse` hook in `.claude/settings.json`, registered the way
14
+ * `prohost_agents/support/handler.py` registers it (matcher `.*`, the shim
15
+ * filters cheaply itself).
16
+ *
17
+ * Claude Code loads a workspace's `.claude/` when it starts there, which is
18
+ * what `agent run` does. Re-running is idempotent; other settings are kept.
19
+ */
20
+ export declare const PROTOCOL_RELATIVE_PATH: string;
21
+ export declare const GATE_RELATIVE_PATH: string;
22
+ export declare const SKILL_NAME = "prohost-pr-protocol";
23
+ export declare class WorkspaceInitError extends Error {
24
+ }
25
+ export interface WorkspaceInitResult {
26
+ skillPath: string;
27
+ settingsPath: string;
28
+ /** Whether the hook was newly added (false when it was already there). */
29
+ hookAdded: boolean;
30
+ }
31
+ /**
32
+ * Write the developer skill and PR gate hook into ``workdir``.
33
+ *
34
+ * @throws WorkspaceInitError when ``repo`` is not a ProhostAI checkout, or the
35
+ * existing settings file is not JSON (it is never overwritten blind).
36
+ */
37
+ export declare function initDeveloperWorkspace(options: {
38
+ workdir: string;
39
+ repo: string;
40
+ }): WorkspaceInitResult;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * `prohost agent workspace init --developer` — make a paired agent's workspace
3
+ * one that opens pull requests the way the ProhostAI fleet does.
4
+ *
5
+ * Opt-in: the default workspace is left exactly as it is. With `--developer`
6
+ * and a path to a ProhostAI checkout (`--repo`), this writes two things into
7
+ * the workspace's `.claude/`:
8
+ *
9
+ * - a skill, `prohost-pr-protocol`, that points the agent at the repo's own
10
+ * `prohost_agents/support/prompts/pr_protocol.md` — by absolute path, not a
11
+ * copy, so the protocol has one source and cannot drift;
12
+ * - the fleet's `gh pr create` gate (`prohost_agents/platform/pr_create_gate.sh`)
13
+ * as a `PreToolUse` hook in `.claude/settings.json`, registered the way
14
+ * `prohost_agents/support/handler.py` registers it (matcher `.*`, the shim
15
+ * filters cheaply itself).
16
+ *
17
+ * Claude Code loads a workspace's `.claude/` when it starts there, which is
18
+ * what `agent run` does. Re-running is idempotent; other settings are kept.
19
+ */
20
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
21
+ import path from 'node:path';
22
+ import { shellQuote } from './accounts.js';
23
+ export const PROTOCOL_RELATIVE_PATH = path.join('prohost_agents', 'support', 'prompts', 'pr_protocol.md');
24
+ export const GATE_RELATIVE_PATH = path.join('prohost_agents', 'platform', 'pr_create_gate.sh');
25
+ export const SKILL_NAME = 'prohost-pr-protocol';
26
+ export class WorkspaceInitError extends Error {
27
+ }
28
+ function skillBody(protocolPath, gatePath) {
29
+ return [
30
+ '---',
31
+ `name: ${SKILL_NAME}`,
32
+ 'description: >-',
33
+ ' ProhostAI pull-request protocol. Use before creating a branch, committing, pushing,',
34
+ ' opening or updating a pull request, answering review comments, or reporting CI status',
35
+ ' in the ProhostAI repositories.',
36
+ '---',
37
+ '',
38
+ '# ProhostAI pull-request protocol',
39
+ '',
40
+ `Read \`${protocolPath}\` in full before you open or update a pull request, and follow it exactly.`,
41
+ 'That file is the single source of truth; this skill only points at it, so never work from a',
42
+ 'remembered or summarised copy — re-read it each time.',
43
+ '',
44
+ `A \`gh pr create\` gate (\`${gatePath}\`) runs before every shell command. If it refuses, fix`,
45
+ 'what it names (usually the PR body headings, or a missing test) and try again. Never work',
46
+ 'around it.',
47
+ '',
48
+ ].join('\n');
49
+ }
50
+ /**
51
+ * Write the developer skill and PR gate hook into ``workdir``.
52
+ *
53
+ * @throws WorkspaceInitError when ``repo`` is not a ProhostAI checkout, or the
54
+ * existing settings file is not JSON (it is never overwritten blind).
55
+ */
56
+ export function initDeveloperWorkspace(options) {
57
+ const repo = path.resolve(options.repo);
58
+ const protocolPath = path.join(repo, PROTOCOL_RELATIVE_PATH);
59
+ const gatePath = path.join(repo, GATE_RELATIVE_PATH);
60
+ for (const required of [protocolPath, gatePath]) {
61
+ if (!existsSync(required)) {
62
+ throw new WorkspaceInitError(`${required} does not exist — pass --repo <path to a ProhostAI backend-service checkout>.`);
63
+ }
64
+ }
65
+ const claudeDir = path.join(path.resolve(options.workdir), '.claude');
66
+ const skillDir = path.join(claudeDir, 'skills', SKILL_NAME);
67
+ mkdirSync(skillDir, { recursive: true, mode: 0o700 });
68
+ const skillPath = path.join(skillDir, 'SKILL.md');
69
+ writeFileSync(skillPath, skillBody(protocolPath, gatePath));
70
+ const settingsPath = path.join(claudeDir, 'settings.json');
71
+ let settings = {};
72
+ if (existsSync(settingsPath)) {
73
+ try {
74
+ const parsed = JSON.parse(readFileSync(settingsPath, 'utf8'));
75
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
76
+ throw new Error('not an object');
77
+ settings = parsed;
78
+ }
79
+ catch {
80
+ throw new WorkspaceInitError(`${settingsPath} is not a JSON object; fix or remove it and run this again.`);
81
+ }
82
+ }
83
+ const hooks = settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)
84
+ ? settings.hooks
85
+ : {};
86
+ const preToolUse = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : [];
87
+ // Claude runs a hook command through a shell, so a checkout path with a
88
+ // space or metacharacter must be quoted — unquoted, the gate would fail to
89
+ // run (or run as something else) and `gh pr create` would go ungated.
90
+ const gateCommand = shellQuote(gatePath);
91
+ const present = preToolUse.some((group) => (group.hooks ?? []).some((hook) => hook.command === gateCommand || hook.command === gatePath));
92
+ if (!present)
93
+ preToolUse.push({ matcher: '.*', hooks: [{ type: 'command', command: gateCommand }] });
94
+ settings.hooks = { ...hooks, PreToolUse: preToolUse };
95
+ writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
96
+ return { skillPath, settingsPath, hookAdded: !present };
97
+ }
package/dist/index.d.ts CHANGED
@@ -16,6 +16,8 @@ interface ParsedArgs {
16
16
  command: string | undefined;
17
17
  /** Second positional, e.g. the ``pair`` in ``prohost agent pair``. */
18
18
  subcommand: string | undefined;
19
+ /** Any further positionals, e.g. ``add work`` in ``prohost agent account add work``. */
20
+ args: string[];
19
21
  flags: Record<string, string | boolean>;
20
22
  }
21
23
  export declare function parseArgs(argv: readonly string[]): ParsedArgs;
package/dist/index.js CHANGED
@@ -24,10 +24,14 @@ function printHelp() {
24
24
  '',
25
25
  'Usage:',
26
26
  ' prohost listen --forward-to <local-url> [--api-key <key>] [--url <ws-url>] [--machine <name>] [--subscription-id <id>]',
27
- ' prohost agent pair --code <pairing-code> [--webhook-url <https-url>] [--base-url <url>]',
27
+ ' prohost agent pair --code <pairing-code> [--account <label>] [--webhook-url <https-url>] [--base-url <url>]',
28
28
  " prohost agent run --exec '<command>' [--idle-timeout <seconds>] [--timeout <seconds>] [--dry-run] [--workdir <dir>]",
29
- " prohost agent install-daemon --exec '<command>' [--workdir <dir>] Keep the agent running",
29
+ " prohost agent install-daemon --exec '<command>' [--workdir <dir>] [--account <label>] Keep the agent running",
30
30
  ' prohost agent uninstall-daemon',
31
+ ' prohost agent account add <label> --runtime claude|codex [--billing subscription|api_key]',
32
+ ' prohost agent account list | remove <label>',
33
+ ' prohost agent list Every paired agent on this machine, and its accounts',
34
+ ' prohost agent workspace init --developer --repo <checkout> [--workdir <dir>]',
31
35
  '',
32
36
  'listen flags:',
33
37
  ' --forward-to Local URL to POST each webhook payload to (required)',
@@ -72,6 +76,20 @@ function printHelp() {
72
76
  ' will go. The name "prohost" is reserved; a malformed file is a warning and a run',
73
77
  ' without extras. Which servers loaded is printed to agent.log at start-up.',
74
78
  '',
79
+ 'Accounts:',
80
+ ' An account is one Claude Code or Codex login on this machine, in its own config directory',
81
+ ' (~/.prohost-accounts, or $PROHOST_ACCOUNTS_ROOT). `account add` prints the sign-in command',
82
+ ' (CLAUDE_CONFIG_DIR=… claude auth login / CODEX_HOME=… codex login) and waits for it; the CLI',
83
+ ' never reads the login itself. An agent uses one account — `default` is ~/.claude or ~/.codex —',
84
+ ' set with --account or picked in ProhostAI, and applied from its next run. Accounts never switch',
85
+ ' on their own: an agent whose account is used up rests until the limit resets.',
86
+ ' --no-wait `account add`: print the sign-in command and return.',
87
+ ' --wait-timeout `account add`: seconds to wait for the sign-in (default 600).',
88
+ '',
89
+ 'agent workspace init --developer:',
90
+ ' Adds the ProhostAI PR-protocol skill and the `gh pr create` gate hook from --repo (a',
91
+ ' backend-service checkout) to the agent workspace\'s .claude/. Opt-in; nothing else changes.',
92
+ '',
75
93
  'agent install-daemon flags: (macOS: launchd; Linux: prints a systemd-user unit)',
76
94
  ' --exec The same command you would pass to `agent run` (required).',
77
95
  ' --workdir, --idle-timeout, --timeout, --machine, --url, --allow-unverified, --safe-tools',
@@ -86,6 +104,7 @@ function printHelp() {
86
104
  ' PROHOST_WS_URL Used when --url is not provided.',
87
105
  ' PROHOST_PAIRING_CODE Used by `agent pair` when --code is not provided.',
88
106
  ' PROHOST_HOME Credential, ledger, workspace and log directory. Defaults to ~/.prohost.',
107
+ ' PROHOST_ACCOUNTS_ROOT Machine-wide account registry shared by every agent. Defaults to ~/.prohost-accounts.',
89
108
  ];
90
109
  process.stdout.write(`${lines.join('\n')}\n`);
91
110
  }
@@ -93,6 +112,7 @@ export function parseArgs(argv) {
93
112
  const flags = {};
94
113
  let command;
95
114
  let subcommand;
115
+ const args = [];
96
116
  let i = 0;
97
117
  while (i < argv.length) {
98
118
  const token = argv[i];
@@ -101,6 +121,8 @@ export function parseArgs(argv) {
101
121
  command = token;
102
122
  else if (!subcommand)
103
123
  subcommand = token;
124
+ else
125
+ args.push(token);
104
126
  i += 1;
105
127
  continue;
106
128
  }
@@ -125,10 +147,10 @@ export function parseArgs(argv) {
125
147
  i += 2;
126
148
  }
127
149
  }
128
- return { command, subcommand, flags };
150
+ return { command, subcommand, args, flags };
129
151
  }
130
152
  async function main(argv) {
131
- const { command, subcommand, flags } = parseArgs(argv);
153
+ const { command, subcommand, args, flags } = parseArgs(argv);
132
154
  // Checked before the help/no-command branch: `prohost --version` carries no
133
155
  // command, which would otherwise print help and exit 1.
134
156
  if (flags.version) {
@@ -148,7 +170,7 @@ async function main(argv) {
148
170
  return 1;
149
171
  }
150
172
  if (command === 'agent') {
151
- return runAgent(subcommand, flags, printHelp);
173
+ return runAgent(subcommand, flags, printHelp, args);
152
174
  }
153
175
  if (command !== 'listen') {
154
176
  process.stderr.write(`Unknown command: ${command}\n\n`);
package/dist/version.d.ts CHANGED
@@ -7,6 +7,6 @@
7
7
  * `package.json`, so a release bump that forgets this file fails the suite
8
8
  * instead of shipping a `User-Agent` that lies about which build is calling.
9
9
  */
10
- export declare const CLI_VERSION = "0.6.0";
10
+ export declare const CLI_VERSION = "0.8.0";
11
11
  /** Sent on every HTTP request the CLI makes back into ProhostAI. */
12
- export declare const USER_AGENT = "prohost-cli/0.6.0";
12
+ export declare const USER_AGENT = "prohost-cli/0.8.0";
package/dist/version.js CHANGED
@@ -7,6 +7,6 @@
7
7
  * `package.json`, so a release bump that forgets this file fails the suite
8
8
  * instead of shipping a `User-Agent` that lies about which build is calling.
9
9
  */
10
- export const CLI_VERSION = '0.6.0';
10
+ export const CLI_VERSION = '0.8.0';
11
11
  /** Sent on every HTTP request the CLI makes back into ProhostAI. */
12
12
  export const USER_AGENT = `prohost-cli/${CLI_VERSION}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prohost/cli",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Run your own AI agent as a ProhostAI teammate, and stream your account's webhooks to your laptop.",
5
5
  "type": "module",
6
6
  "license": "MIT",