@prohost/cli 0.6.0 → 0.7.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 +75 -0
- package/README.md +47 -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 +11 -0
- package/dist/agent/api.js +13 -1
- 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 +41 -0
- package/dist/agent/contract.js +11 -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/run.d.ts +8 -2
- package/dist/agent/run.js +135 -16
- 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
|
@@ -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
|
|
105
|
-
const used = percent(
|
|
106
|
-
const at = isoTime(
|
|
107
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
10
|
+
export declare const CLI_VERSION = "0.7.0";
|
|
11
11
|
/** Sent on every HTTP request the CLI makes back into ProhostAI. */
|
|
12
|
-
export declare const USER_AGENT = "prohost-cli/0.
|
|
12
|
+
export declare const USER_AGENT = "prohost-cli/0.7.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.
|
|
10
|
+
export const CLI_VERSION = '0.7.0';
|
|
11
11
|
/** Sent on every HTTP request the CLI makes back into ProhostAI. */
|
|
12
12
|
export const USER_AGENT = `prohost-cli/${CLI_VERSION}`;
|