@phnx-labs/agents-cli 1.22.41 → 1.22.43
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 +86 -0
- package/README.md +10 -4
- package/dist/bin/agents +0 -0
- package/dist/bootstrap.js +25 -7
- package/dist/cli/command-registry.d.ts +2 -3
- package/dist/cli/command-registry.js +4 -8
- package/dist/commands/accounts.d.ts +29 -0
- package/dist/commands/accounts.js +220 -29
- package/dist/commands/alias.d.ts +7 -4
- package/dist/commands/alias.js +34 -9
- package/dist/commands/auth.d.ts +9 -0
- package/dist/commands/auth.js +108 -0
- package/dist/commands/browser-sessions-picker.js +42 -62
- package/dist/commands/browser.js +30 -38
- package/dist/commands/computer-actions.d.ts +1 -1
- package/dist/commands/computer-actions.js +1 -1
- package/dist/commands/computer-sessions-picker.js +32 -55
- package/dist/commands/computer.d.ts +1 -1
- package/dist/commands/computer.js +22 -8
- package/dist/commands/daemon.js +1 -1
- package/dist/commands/doctor.d.ts +5 -3
- package/dist/commands/doctor.js +23 -8
- package/dist/commands/exec.js +3 -2
- package/dist/commands/feed.js +2 -31
- package/dist/commands/harness.js +3 -3
- package/dist/commands/hooks.js +1 -1
- package/dist/commands/import.js +1 -1
- package/dist/commands/insights.js +163 -85
- package/dist/commands/inspect.js +2 -2
- package/dist/commands/mine.js +1 -1
- package/dist/commands/monitors.js +1 -1
- package/dist/commands/org.d.ts +6 -0
- package/dist/commands/org.js +175 -0
- package/dist/commands/packages.js +1 -1
- package/dist/commands/profiles.js +2 -2
- package/dist/commands/prune.js +1 -1
- package/dist/commands/routines.js +44 -26
- package/dist/commands/run-account-picker.d.ts +19 -0
- package/dist/commands/run-account-picker.js +81 -0
- package/dist/commands/sessions-picker-factory.d.ts +40 -0
- package/dist/commands/sessions-picker-factory.js +67 -0
- package/dist/commands/sessions-picker.js +124 -122
- package/dist/commands/setup-browser.js +4 -4
- package/dist/commands/setup-mine.js +1 -1
- package/dist/commands/setup-preferences.js +2 -2
- package/dist/commands/setup.js +10 -4
- package/dist/commands/snapshot.d.ts +1 -1
- package/dist/commands/snapshot.js +4 -4
- package/dist/commands/status.d.ts +4 -2
- package/dist/commands/status.js +10 -7
- package/dist/commands/sync.d.ts +1 -0
- package/dist/commands/sync.js +30 -3
- package/dist/commands/versions.js +1 -1
- package/dist/commands/view.js +7 -1
- package/dist/commands/webhook.js +1 -1
- package/dist/lib/account-capabilities.d.ts +10 -0
- package/dist/lib/account-capabilities.js +34 -0
- package/dist/lib/accounting/rotate.d.ts +5 -3
- package/dist/lib/accounting/rotate.js +36 -7
- package/dist/lib/accounting/usage.d.ts +18 -4
- package/dist/lib/accounting/usage.js +138 -27
- package/dist/lib/agent-spec/agents.d.ts +584 -0
- package/dist/lib/agent-spec/agents.js +3127 -0
- package/dist/lib/agent-spec/provider.js +1 -1
- package/dist/lib/agents.d.ts +2 -584
- package/dist/lib/agents.js +2 -3127
- package/dist/lib/browser/drivers/local.d.ts +8 -2
- package/dist/lib/browser/drivers/local.js +8 -2
- package/dist/lib/browser/drivers/ssh.d.ts +8 -2
- package/dist/lib/browser/drivers/ssh.js +10 -4
- package/dist/lib/browser/hygiene.d.ts +1 -1
- package/dist/lib/browser/hygiene.js +5 -1
- package/dist/lib/browser/profiles.d.ts +74 -1
- package/dist/lib/browser/profiles.js +114 -5
- package/dist/lib/browser/runtime-state.d.ts +11 -5
- package/dist/lib/browser/runtime-state.js +13 -9
- package/dist/lib/browser/service.d.ts +68 -30
- package/dist/lib/browser/service.js +305 -194
- package/dist/lib/browser/types.d.ts +95 -3
- package/dist/lib/browser/types.js +51 -0
- package/dist/lib/catchup.js +1 -1
- package/dist/lib/cloud/rush.d.ts +0 -15
- package/dist/lib/cloud/rush.js +0 -35
- package/dist/lib/computer/actions.d.ts +1 -1
- package/dist/lib/computer/actions.js +1 -1
- package/dist/lib/{computer-rpc.js → computer/computer-rpc.js} +5 -5
- package/dist/lib/computer/dispatch.d.ts +1 -1
- package/dist/lib/computer/download.d.ts +1 -1
- package/dist/lib/computer/download.js +3 -3
- package/dist/lib/{ssh-tunnel.d.ts → computer/ssh-tunnel.d.ts} +1 -1
- package/dist/lib/{ssh-tunnel.js → computer/ssh-tunnel.js} +12 -12
- package/dist/lib/crabbox/runtimes.d.ts +6 -2
- package/dist/lib/crabbox/runtimes.js +21 -5
- package/dist/lib/daemon/daemon.js +87 -59
- package/dist/lib/{runner.d.ts → daemon/runner.d.ts} +28 -11
- package/dist/lib/{runner.js → daemon/runner.js} +143 -123
- package/dist/lib/devices/doctor-findings.d.ts +1 -1
- package/dist/lib/devices/doctor-findings.js +4 -4
- package/dist/lib/devices/fleet-inventory.js +1 -1
- package/dist/lib/devices/harness-inventory.js +1 -1
- package/dist/lib/devices/health-report.js +1 -1
- package/dist/lib/devices/registry.js +4 -22
- package/dist/lib/doctor-diff.d.ts +1 -1
- package/dist/lib/doctor-diff.js +1 -1
- package/dist/lib/drift-sync.d.ts +1 -1
- package/dist/lib/drift-sync.js +2 -2
- package/dist/lib/drift.d.ts +1 -1
- package/dist/lib/drift.js +1 -1
- package/dist/lib/entitlement.d.ts +31 -0
- package/dist/lib/entitlement.js +137 -0
- package/dist/lib/exec.js +52 -205
- package/dist/lib/feed/feed.js +2 -8
- package/dist/lib/format.d.ts +9 -0
- package/dist/lib/format.js +16 -0
- package/dist/lib/fs-atomic.d.ts +7 -0
- package/dist/lib/fs-atomic.js +9 -0
- package/dist/lib/github/pr-mergeable.d.ts +49 -0
- package/dist/lib/github/pr-mergeable.js +157 -0
- package/dist/lib/github/pr-verdict.d.ts +67 -0
- package/dist/lib/github/pr-verdict.js +72 -0
- package/dist/lib/harness/adapter.d.ts +161 -0
- package/dist/lib/harness/adapter.js +47 -0
- package/dist/lib/harness/adapters/claude.d.ts +2 -0
- package/dist/lib/harness/adapters/claude.js +110 -0
- package/dist/lib/harness/adapters/codex.d.ts +2 -0
- package/dist/lib/harness/adapters/codex.js +83 -0
- package/dist/lib/harness/adapters/copilot.d.ts +2 -0
- package/dist/lib/harness/adapters/copilot.js +23 -0
- package/dist/lib/harness/adapters/cursor.d.ts +2 -0
- package/dist/lib/harness/adapters/cursor.js +38 -0
- package/dist/lib/harness/adapters/droid.d.ts +2 -0
- package/dist/lib/harness/adapters/droid.js +17 -0
- package/dist/lib/harness/adapters/grok.d.ts +2 -0
- package/dist/lib/harness/adapters/grok.js +14 -0
- package/dist/lib/harness/adapters/kimi.d.ts +2 -0
- package/dist/lib/harness/adapters/kimi.js +49 -0
- package/dist/lib/harness/adapters/muse.d.ts +2 -0
- package/dist/lib/harness/adapters/muse.js +44 -0
- package/dist/lib/harness/adapters/opencode.d.ts +2 -0
- package/dist/lib/harness/adapters/opencode.js +14 -0
- package/dist/lib/harness/exec-config-version.d.ts +8 -0
- package/dist/lib/harness/exec-config-version.js +22 -0
- package/dist/lib/harness/index.d.ts +1 -0
- package/dist/lib/harness/index.js +34 -0
- package/dist/lib/{hooks.d.ts → hooks/install.d.ts} +1 -1
- package/dist/lib/{hooks.js → hooks/install.js} +11 -11
- package/dist/lib/hosts/passthrough.d.ts +10 -0
- package/dist/lib/hosts/passthrough.js +28 -6
- package/dist/lib/hosts/session-index.js +1 -1
- package/dist/lib/import.js +1 -1
- package/dist/lib/installations/index.d.ts +4 -4
- package/dist/lib/installations/index.js +3 -4
- package/dist/lib/installations/migrate.js +6 -6
- package/dist/lib/{shims.d.ts → installations/shims.d.ts} +2 -2
- package/dist/lib/{shims.js → installations/shims.js} +13 -115
- package/dist/lib/installations/store.d.ts +141 -3
- package/dist/lib/installations/store.js +475 -5
- package/dist/lib/installations/versions.d.ts +40 -125
- package/dist/lib/installations/versions.js +57 -462
- package/dist/lib/isolation-boundary-report.d.ts +1 -1
- package/dist/lib/isolation-boundary-report.js +1 -1
- package/dist/lib/mailbox-gc.js +2 -3
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.js +19 -8
- package/dist/lib/monitors/config.d.ts +2 -2
- package/dist/lib/monitors/config.js +3 -2
- package/dist/lib/monitors/dispatch.d.ts +1 -1
- package/dist/lib/monitors/dispatch.js +2 -2
- package/dist/lib/monitors/state.d.ts +1 -1
- package/dist/lib/origin-machine.d.ts +18 -0
- package/dist/lib/origin-machine.js +34 -0
- package/dist/lib/perf/db.js +1 -1
- package/dist/lib/perf/spool.js +1 -1
- package/dist/lib/plugins/plugins.js +1 -1
- package/dist/lib/plugins/skills.js +1 -1
- package/dist/lib/prix-account.d.ts +158 -0
- package/dist/lib/prix-account.js +214 -0
- package/dist/lib/profiles.d.ts +7 -0
- package/dist/lib/profiles.js +26 -1
- package/dist/lib/project-key.js +2 -2
- package/dist/lib/project-launch.js +1 -1
- package/dist/lib/refresh.js +2 -2
- package/dist/lib/resource-inventory.d.ts +1 -1
- package/dist/lib/resource-inventory.js +1 -1
- package/dist/lib/rules/rules.js +1 -1
- package/dist/lib/sandbox.d.ts +24 -0
- package/dist/lib/sandbox.js +101 -0
- package/dist/lib/scheduling/routines.d.ts +18 -9
- package/dist/lib/scheduling/routines.js +17 -9
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.js +9 -1
- package/dist/lib/secrets/index.js +1 -1
- package/dist/lib/secrets/list-filter.js +1 -1
- package/dist/lib/self-heal/checks/hook-runtime.js +1 -1
- package/dist/lib/self-heal/checks/path.js +1 -1
- package/dist/lib/self-heal/checks/shadowing.js +1 -1
- package/dist/lib/self-heal/checks/shims.js +1 -1
- package/dist/lib/service-manifest.d.ts +20 -0
- package/dist/lib/service-manifest.js +34 -0
- package/dist/lib/session/cloud.js +1 -1
- package/dist/lib/session/db.js +1 -1
- package/dist/lib/session/discover.d.ts +39 -0
- package/dist/lib/session/discover.js +3 -3
- package/dist/lib/session/fork.js +1 -1
- package/dist/lib/session/live-metadata.js +1 -1
- package/dist/lib/session/origin-machine.d.ts +1 -18
- package/dist/lib/session/origin-machine.js +4 -34
- package/dist/lib/session/parse.js +24 -42
- package/dist/lib/session/recovery.js +1 -1
- package/dist/lib/session/relative-time.d.ts +1 -44
- package/dist/lib/session/relative-time.js +4 -120
- package/dist/lib/session/remote-list.js +1 -1
- package/dist/lib/session/remote.js +2 -2
- package/dist/lib/session/short-id.d.ts +1 -17
- package/dist/lib/session/short-id.js +4 -20
- package/dist/lib/session/width.d.ts +1 -29
- package/dist/lib/session/width.js +4 -101
- package/dist/lib/snapshot.d.ts +2 -2
- package/dist/lib/snapshot.js +1 -1
- package/dist/lib/staleness/prune.d.ts +1 -1
- package/dist/lib/staleness/prune.js +1 -1
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +4 -1
- package/dist/lib/startup/command-registry.js +12 -5
- package/dist/lib/teams/agents.js +2 -19
- package/dist/lib/teams/parsers.js +20 -26
- package/dist/lib/teams/placement-probe.js +1 -1
- package/dist/lib/teams/registry.js +2 -21
- package/dist/lib/teams/scheduler.js +1 -1
- package/dist/lib/text/relative-time.d.ts +44 -0
- package/dist/lib/text/relative-time.js +120 -0
- package/dist/lib/text/short-id.d.ts +17 -0
- package/dist/lib/text/short-id.js +20 -0
- package/dist/lib/text/width.d.ts +29 -0
- package/dist/lib/text/width.js +101 -0
- package/dist/lib/triggers/handlers.d.ts +7 -0
- package/dist/lib/triggers/handlers.js +9 -1
- package/dist/lib/triggers/webhook.d.ts +1 -0
- package/dist/lib/triggers/webhook.js +3 -2
- package/dist/lib/uninstall.js +1 -1
- package/dist/lib/version.d.ts +9 -2
- package/dist/lib/version.js +11 -9
- package/dist/lib/view-types.d.ts +4 -0
- package/package.json +1 -1
- package/dist/commands/tickets.d.ts +0 -2
- package/dist/commands/tickets.js +0 -43
- package/dist/lib/observe-aliases.d.ts +0 -30
- package/dist/lib/observe-aliases.js +0 -38
- package/dist/lib/tickets/list.d.ts +0 -53
- package/dist/lib/tickets/list.js +0 -153
- /package/dist/lib/{computer-rpc.d.ts → computer/computer-rpc.d.ts} +0 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** How long a cached tier is trusted before the next call re-fetches. */
|
|
2
|
+
export declare const ENTITLEMENT_CACHE_TTL_MS: number;
|
|
3
|
+
export type EntitlementSource = 'live' | 'cache' | 'offline' | 'no-session';
|
|
4
|
+
export interface EntitlementTier {
|
|
5
|
+
/** The raw tier name from the backend ('free', 'admin', or a paid tier name). */
|
|
6
|
+
tierName: string;
|
|
7
|
+
/** true for 'admin' or any tier name other than 'free'. */
|
|
8
|
+
isPaid: boolean;
|
|
9
|
+
/** Where this value came from — mainly for diagnostics/tests. */
|
|
10
|
+
source: EntitlementSource;
|
|
11
|
+
}
|
|
12
|
+
export declare function setEntitlementUserYamlPathForTest(value: string | null): void;
|
|
13
|
+
export declare function setEntitlementCachePathForTest(value: string | null): void;
|
|
14
|
+
export declare function setEntitlementFetchForTest(fn: typeof globalThis.fetch): void;
|
|
15
|
+
export declare function entitlementCachePath(): string;
|
|
16
|
+
/**
|
|
17
|
+
* The live plan tier, cache-first with a TTL.
|
|
18
|
+
*
|
|
19
|
+
* - No `~/.rush/user.yaml` at all → free, no network call.
|
|
20
|
+
* - A fresh cache entry (within {@link ENTITLEMENT_CACHE_TTL_MS}) is returned
|
|
21
|
+
* with no network call.
|
|
22
|
+
* - A stale or missing cache triggers a live fetch; on success the result is
|
|
23
|
+
* cached and returned.
|
|
24
|
+
* - A failed fetch (offline, timeout, non-2xx) falls back to a stale cache if
|
|
25
|
+
* one exists — never silently drops a known-paid account to free just
|
|
26
|
+
* because the network hiccuped — and only falls back to free when there is
|
|
27
|
+
* no cache at all to fall back to.
|
|
28
|
+
*/
|
|
29
|
+
export declare function getTier(): Promise<EntitlementTier>;
|
|
30
|
+
/** Per-harness account cap for a tier: 3 on free, 10 on paid/admin. */
|
|
31
|
+
export declare function accountCapForTier(tier: Pick<EntitlementTier, 'isPaid'>): number;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan-tier entitlement — the one place agents-cli reads a user's live billing
|
|
3
|
+
* tier for plan gates (the `agents accounts` per-harness cap, the `agents
|
|
4
|
+
* insights` paid split). Fetches `GET /api/v1/billing/subscription?agent=agi-cli`
|
|
5
|
+
* using the Rush session token from `~/.rush/user.yaml` (same read pattern as
|
|
6
|
+
* `lib/cloud/rush.ts` / `lib/secrets/drivers/rush.ts` — this module does not
|
|
7
|
+
* import either, since neither exports its token reader), caches the result on
|
|
8
|
+
* disk with a TTL, and stays offline-tolerant: a stale cache is honored over a
|
|
9
|
+
* failed network call rather than silently dropping a paid account to free, and
|
|
10
|
+
* no session file at all resolves straight to the free tier with no request.
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from 'node:fs';
|
|
13
|
+
import * as os from 'node:os';
|
|
14
|
+
import * as path from 'node:path';
|
|
15
|
+
import * as yaml from 'yaml';
|
|
16
|
+
import { getCacheDir } from './state.js';
|
|
17
|
+
import { atomicWriteFileSync, ensureLockTarget, withFileLock } from './fs-atomic.js';
|
|
18
|
+
const PROXY_BASE = process.env.RUSH_PROXY_BASE ?? 'https://api.prix.dev';
|
|
19
|
+
const SUBSCRIPTION_PATH = '/api/v1/billing/subscription?agent=agi-cli';
|
|
20
|
+
/** How long a cached tier is trusted before the next call re-fetches. */
|
|
21
|
+
export const ENTITLEMENT_CACHE_TTL_MS = 15 * 60_000;
|
|
22
|
+
/** Same shape as bootstrap.ts's fetchNpmPackageMetadata — a hung connection
|
|
23
|
+
* (dead peer, a firewall dropping packets silently) must not block every
|
|
24
|
+
* `accounts add`/`insights` call indefinitely once the cache goes stale. */
|
|
25
|
+
const FETCH_TIMEOUT_MS = 5000;
|
|
26
|
+
const FREE_TIER = { tierName: 'free', isPaid: false };
|
|
27
|
+
// ─── Injectable seams (tests only — the lib's own real read/write paths) ────
|
|
28
|
+
let userYamlPathOverride = null;
|
|
29
|
+
let cachePathOverride = null;
|
|
30
|
+
let fetchImpl = globalThis.fetch;
|
|
31
|
+
export function setEntitlementUserYamlPathForTest(value) {
|
|
32
|
+
userYamlPathOverride = value;
|
|
33
|
+
}
|
|
34
|
+
export function setEntitlementCachePathForTest(value) {
|
|
35
|
+
cachePathOverride = value;
|
|
36
|
+
}
|
|
37
|
+
export function setEntitlementFetchForTest(fn) {
|
|
38
|
+
fetchImpl = fn;
|
|
39
|
+
}
|
|
40
|
+
function userYamlPath() {
|
|
41
|
+
return userYamlPathOverride ?? path.join(os.homedir(), '.rush', 'user.yaml');
|
|
42
|
+
}
|
|
43
|
+
export function entitlementCachePath() {
|
|
44
|
+
return cachePathOverride ?? path.join(getCacheDir(), '.entitlement-cache.json');
|
|
45
|
+
}
|
|
46
|
+
function readRushToken() {
|
|
47
|
+
const file = userYamlPath();
|
|
48
|
+
if (!fs.existsSync(file))
|
|
49
|
+
return null;
|
|
50
|
+
try {
|
|
51
|
+
const data = yaml.parse(fs.readFileSync(file, 'utf-8'));
|
|
52
|
+
return data?.session?.access_token || null;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function readCache() {
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(fs.readFileSync(entitlementCachePath(), 'utf-8'));
|
|
61
|
+
if (parsed?.version === 1 && typeof parsed.tierName === 'string' && typeof parsed.fetchedAt === 'number')
|
|
62
|
+
return parsed;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// missing or corrupt — treat as no cache
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
/** Best-effort write; a failed cache write just means the next call re-fetches. */
|
|
70
|
+
function writeCache(tierName, isPaid) {
|
|
71
|
+
try {
|
|
72
|
+
const target = entitlementCachePath();
|
|
73
|
+
ensureLockTarget(target);
|
|
74
|
+
withFileLock(target, () => {
|
|
75
|
+
const entry = { version: 1, tierName, isPaid, fetchedAt: Date.now() };
|
|
76
|
+
atomicWriteFileSync(target, JSON.stringify(entry, null, 2));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// best-effort
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Only `tierName: "free"` (or an absent tier name) reads as free — every other
|
|
85
|
+
* name (`admin`, or a real paid-tier name once agi-cli has a pricing manifest,
|
|
86
|
+
* see the integration spec §4.4 G3) is treated as paid. Backend tier config for
|
|
87
|
+
* agi-cli doesn't exist yet, so this deliberately does not lean on
|
|
88
|
+
* `hasSubscription`/`needsUpgrade` nuance the backend isn't populating meaningfully
|
|
89
|
+
* for this agent today.
|
|
90
|
+
*/
|
|
91
|
+
function classifySubscription(sub) {
|
|
92
|
+
const tierName = typeof sub.tierName === 'string' && sub.tierName ? sub.tierName : 'free';
|
|
93
|
+
return { tierName, isPaid: tierName !== 'free' };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The live plan tier, cache-first with a TTL.
|
|
97
|
+
*
|
|
98
|
+
* - No `~/.rush/user.yaml` at all → free, no network call.
|
|
99
|
+
* - A fresh cache entry (within {@link ENTITLEMENT_CACHE_TTL_MS}) is returned
|
|
100
|
+
* with no network call.
|
|
101
|
+
* - A stale or missing cache triggers a live fetch; on success the result is
|
|
102
|
+
* cached and returned.
|
|
103
|
+
* - A failed fetch (offline, timeout, non-2xx) falls back to a stale cache if
|
|
104
|
+
* one exists — never silently drops a known-paid account to free just
|
|
105
|
+
* because the network hiccuped — and only falls back to free when there is
|
|
106
|
+
* no cache at all to fall back to.
|
|
107
|
+
*/
|
|
108
|
+
export async function getTier() {
|
|
109
|
+
const token = readRushToken();
|
|
110
|
+
if (!token)
|
|
111
|
+
return { ...FREE_TIER, source: 'no-session' };
|
|
112
|
+
const cached = readCache();
|
|
113
|
+
if (cached && Date.now() - cached.fetchedAt < ENTITLEMENT_CACHE_TTL_MS) {
|
|
114
|
+
return { tierName: cached.tierName, isPaid: cached.isPaid, source: 'cache' };
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const res = await fetchImpl(`${PROXY_BASE}${SUBSCRIPTION_PATH}`, {
|
|
118
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
119
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
120
|
+
});
|
|
121
|
+
if (!res.ok)
|
|
122
|
+
throw new Error(`billing/subscription responded ${res.status}`);
|
|
123
|
+
const sub = (await res.json());
|
|
124
|
+
const { tierName, isPaid } = classifySubscription(sub);
|
|
125
|
+
writeCache(tierName, isPaid);
|
|
126
|
+
return { tierName, isPaid, source: 'live' };
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
if (cached)
|
|
130
|
+
return { tierName: cached.tierName, isPaid: cached.isPaid, source: 'offline' };
|
|
131
|
+
return { ...FREE_TIER, source: 'offline' };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** Per-harness account cap for a tier: 3 on free, 10 on paid/admin. */
|
|
135
|
+
export function accountCapForTier(tier) {
|
|
136
|
+
return tier.isPaid ? 10 : 3;
|
|
137
|
+
}
|
package/dist/lib/exec.js
CHANGED
|
@@ -19,8 +19,7 @@ import { sanitizeProcessEnv } from './secrets/bundles.js';
|
|
|
19
19
|
import { resolveActor, actorEnv } from './actor.js';
|
|
20
20
|
import { expandLocalHome } from './project-root.js';
|
|
21
21
|
import { getShimsDir, getHistoryDir, getRuntimeStateDir } from './state.js';
|
|
22
|
-
import {
|
|
23
|
-
import { readCodexConfiguredModel } from './shims.js';
|
|
22
|
+
import { readCodexConfiguredModel } from './installations/shims.js';
|
|
24
23
|
import { writePidSessionEntry, extractSessionIdArg } from './session/pid-registry.js';
|
|
25
24
|
import { writeSessionActorRecord, writeSessionAliasRecord } from './session/actor-sidecar.js';
|
|
26
25
|
import { loadHookSessionIndex, resolveHookSessionId } from './session/hook-sessions.js';
|
|
@@ -32,10 +31,14 @@ import { isTmuxInstalled } from './tmux/binary.js';
|
|
|
32
31
|
import { isTmuxEnabled } from './device-config.js';
|
|
33
32
|
import { machineId } from './machine-id.js';
|
|
34
33
|
import { shellQuote } from './ssh-exec.js';
|
|
35
|
-
import { resolveClaudeSetupToken } from './claude-account-token.js';
|
|
36
34
|
import { codexEditWritableRoots, codexPolicyArgs } from './codex-policy.js';
|
|
35
|
+
import { resolveClaudeSetupToken } from './claude-account-token.js';
|
|
37
36
|
import { applyAddDirs } from './add-dir.js';
|
|
38
37
|
import { applyActiveRulesPresetAtRun } from './rules/run-sync.js';
|
|
38
|
+
import { resolveHarnessAdapter, stripForeignConfigDir } from './harness/index.js';
|
|
39
|
+
import { resolveConfigVersion } from './harness/exec-config-version.js';
|
|
40
|
+
import { getAccountInfo } from './agents.js';
|
|
41
|
+
import { getUsageLookupKey, noteClaudeSessionLimit, parseClaudeSessionLimitReset } from './accounting/usage.js';
|
|
39
42
|
/**
|
|
40
43
|
* Map a raw mode string (CLI flag, YAML field, env var) to the canonical Mode.
|
|
41
44
|
*
|
|
@@ -282,180 +285,25 @@ export function buildExecEnv(options) {
|
|
|
282
285
|
// Config-dir env vars are agent-specific. When the caller is running inside
|
|
283
286
|
// an agent-managed shell, process.env already carries one; spreading into a
|
|
284
287
|
// different agent's env would leak a config pointer the target CLI doesn't
|
|
285
|
-
// understand.
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
//
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
if (versionHome) {
|
|
302
|
-
result.CLAUDE_CONFIG_DIR = path.join(versionHome, '.claude');
|
|
303
|
-
// A managed pin lives in a per-version dir; Claude Code's own background
|
|
304
|
-
// auto-updater would rewrite that pinned binary in place (and has left it
|
|
305
|
-
// half-swapped and broken). Disable it so a pin stays a pin. Honor an
|
|
306
|
-
// explicit user value — from process.env (already in result) or from
|
|
307
|
-
// options.env (spread over result below).
|
|
308
|
-
if (result.DISABLE_AUTOUPDATER === undefined) {
|
|
309
|
-
result.DISABLE_AUTOUPDATER = '1';
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
// The `auth` bundle's setup-token exists so a run with NO human present
|
|
313
|
-
// authenticates without the Touch-ID-gated login item — usage probes,
|
|
314
|
-
// routines, dispatched runs (claude-account-token.ts). An interactive run
|
|
315
|
-
// has a human at the TTY, and their own per-version login is the credential
|
|
316
|
-
// they established and expect; overriding it made `/status` report
|
|
317
|
-
// `Auth token: CLAUDE_CODE_OAUTH_TOKEN` on a personal machine and took every
|
|
318
|
-
// hand-driven session off that login. macOS cannot cheaply confirm a home's
|
|
319
|
-
// login first (probing the Keychain raises an authorization sheet per
|
|
320
|
-
// installed version on the `agents run` hot path — agents.ts
|
|
321
|
-
// `isClaudeCredentialFileBlank`), so interactive simply defers to Claude
|
|
322
|
-
// Code, which prompts a present human to log in if the login is missing.
|
|
323
|
-
if (resolveInteractive(options)) {
|
|
324
|
-
// Drop an INHERITED copy of OUR OWN setup-token: an interactive launch from
|
|
325
|
-
// inside a headless agent's shell inherits that agent's injected value via
|
|
326
|
-
// sanitizeProcessEnv(process.env) and would keep authenticating as it.
|
|
327
|
-
// Matched by VALUE, so a token the user exported deliberately is a different
|
|
328
|
-
// string and is left alone (#2383). This is NARROWER than the non-interactive
|
|
329
|
-
// path below, which overwrites-or-deletes unconditionally and never inspects
|
|
330
|
-
// the inherited value — a DIFFERENT account's inherited setup-token passing
|
|
331
|
-
// through this equality check is the adjacent hole RUSH-2360 leaves as
|
|
332
|
-
// follow-up (it does not silently run on a *shared, rotating* token, which is
|
|
333
|
-
// what caused the RUSH-1822 logout storm).
|
|
334
|
-
if (setupToken && result.CLAUDE_CODE_OAUTH_TOKEN === setupToken) {
|
|
335
|
-
delete result.CLAUDE_CODE_OAUTH_TOKEN;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
else {
|
|
339
|
-
// Non-interactive (routines, dispatched, provisioned box): mirror the routines
|
|
340
|
-
// path (`runner.ts:1017-1021`) UNCONDITIONALLY. Inject the per-account
|
|
341
|
-
// setup-token when one resolves — it replaces any ambient shared value
|
|
342
|
-
// inherited from the launcher. When NONE resolves, STRIP the ambient
|
|
343
|
-
// CLAUDE_CODE_OAUTH_TOKEN so a run on a provisioned box can never silently
|
|
344
|
-
// authenticate as the shared, rotating token an earlier version of this path
|
|
345
|
-
// let through — the RUSH-1822 fleet-wide-logout hazard, tracked by RUSH-2360.
|
|
346
|
-
// A missing login then fails loud (401) against this home's own credential
|
|
347
|
-
// instead of quietly borrowing another's. options.env still wins below for an
|
|
348
|
-
// explicit caller override.
|
|
349
|
-
if (setupToken) {
|
|
350
|
-
result.CLAUDE_CODE_OAUTH_TOKEN = setupToken;
|
|
351
|
-
}
|
|
352
|
-
else {
|
|
353
|
-
delete result.CLAUDE_CODE_OAUTH_TOKEN;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
delete result.CODEX_HOME;
|
|
357
|
-
delete result.COPILOT_HOME;
|
|
358
|
-
delete result.KIMI_CODE_HOME;
|
|
359
|
-
}
|
|
360
|
-
else if (options.agent === 'codex') {
|
|
361
|
-
const cwd = options.cwd || process.cwd();
|
|
362
|
-
const resolvedVersion = options.version ?? resolveVersion('codex', cwd);
|
|
363
|
-
const version = options.version
|
|
364
|
-
? resolvedVersion
|
|
365
|
-
: (resolvedVersion && isVersionInstalled('codex', resolvedVersion) ? resolvedVersion : null);
|
|
366
|
-
if (version) {
|
|
367
|
-
// On macOS the deep versioned home overflows the Unix-socket SUN_LEN
|
|
368
|
-
// limit for codex's app-server control socket; resolve to a short,
|
|
369
|
-
// SUN_LEN-safe home (migrating once if needed). See codex-home.ts.
|
|
370
|
-
const versionedHome = path.join(getVersionHomePath('codex', version), '.codex');
|
|
371
|
-
const agentsUserDir = path.dirname(getHistoryDir());
|
|
372
|
-
result.CODEX_HOME = resolveCodexHome(versionedHome, agentsUserDir, version);
|
|
373
|
-
}
|
|
374
|
-
delete result.CLAUDE_CONFIG_DIR;
|
|
375
|
-
delete result.COPILOT_HOME;
|
|
376
|
-
delete result.KIMI_CODE_HOME;
|
|
377
|
-
}
|
|
378
|
-
else if (options.agent === 'copilot') {
|
|
379
|
-
// Copilot honors COPILOT_HOME (relocates ~/.copilot, including settings,
|
|
380
|
-
// mcp-config.json, sessions, logs). Pin it at the per-version home so
|
|
381
|
-
// version switches isolate MCP servers, auth, and session history.
|
|
382
|
-
const cwd = options.cwd || process.cwd();
|
|
383
|
-
const resolvedVersion = options.version ?? resolveVersion('copilot', cwd);
|
|
384
|
-
const version = options.version
|
|
385
|
-
? resolvedVersion
|
|
386
|
-
: (resolvedVersion && isVersionInstalled('copilot', resolvedVersion) ? resolvedVersion : null);
|
|
387
|
-
if (version) {
|
|
388
|
-
result.COPILOT_HOME = path.join(getVersionHomePath('copilot', version), '.copilot');
|
|
389
|
-
}
|
|
390
|
-
delete result.CLAUDE_CONFIG_DIR;
|
|
391
|
-
delete result.CODEX_HOME;
|
|
392
|
-
delete result.KIMI_CODE_HOME;
|
|
393
|
-
}
|
|
394
|
-
else if (options.agent === 'kimi') {
|
|
395
|
-
// Kimi honors KIMI_CODE_HOME (relocates ~/.kimi-code, including config,
|
|
396
|
-
// skills, hooks, sessions). Pin it at the per-version home.
|
|
397
|
-
const cwd = options.cwd || process.cwd();
|
|
398
|
-
const resolvedVersion = options.version ?? resolveVersion('kimi', cwd);
|
|
399
|
-
const version = options.version
|
|
400
|
-
? resolvedVersion
|
|
401
|
-
: (resolvedVersion && isVersionInstalled('kimi', resolvedVersion) ? resolvedVersion : null);
|
|
402
|
-
if (version) {
|
|
403
|
-
result.KIMI_CODE_HOME = path.join(getVersionHomePath('kimi', version), '.kimi-code');
|
|
404
|
-
}
|
|
405
|
-
delete result.CLAUDE_CONFIG_DIR;
|
|
406
|
-
delete result.CODEX_HOME;
|
|
407
|
-
delete result.COPILOT_HOME;
|
|
408
|
-
}
|
|
409
|
-
else if (options.agent === 'muse') {
|
|
410
|
-
// Muse has no MUSE_CONFIG_DIR. Config is XDG-based:
|
|
411
|
-
// $XDG_CONFIG_HOME/muse (settings, skills, hooks, auth)
|
|
412
|
-
// $XDG_DATA_HOME/muse (sessions, plugins)
|
|
413
|
-
// Pin both into the version home so multi-version isolation matches
|
|
414
|
-
// Claude's CLAUDE_CONFIG_DIR / Codex's CODEX_HOME, and so Muse never
|
|
415
|
-
// resolves through the adopt-time ~/.config/muse symlink (SymlinkOrReparse).
|
|
416
|
-
const cwd = options.cwd || process.cwd();
|
|
417
|
-
const resolvedVersion = options.version ?? resolveVersion('muse', cwd);
|
|
418
|
-
const version = options.version
|
|
419
|
-
? resolvedVersion
|
|
420
|
-
: (resolvedVersion && isVersionInstalled('muse', resolvedVersion) ? resolvedVersion : null);
|
|
421
|
-
if (version) {
|
|
422
|
-
const versionHome = getVersionHomePath('muse', version);
|
|
423
|
-
result.XDG_CONFIG_HOME = path.join(versionHome, '.config');
|
|
424
|
-
result.XDG_DATA_HOME = path.join(versionHome, '.local', 'share');
|
|
425
|
-
}
|
|
426
|
-
delete result.CLAUDE_CONFIG_DIR;
|
|
427
|
-
delete result.CODEX_HOME;
|
|
428
|
-
delete result.COPILOT_HOME;
|
|
429
|
-
delete result.KIMI_CODE_HOME;
|
|
430
|
-
}
|
|
431
|
-
else if (options.agent === 'cursor') {
|
|
432
|
-
// Cursor has no config-dir env var (only CURSOR_API_KEY / CURSOR_API_ENDPOINT).
|
|
433
|
-
// Its OAuth token — the login gate — lives at $XDG_CONFIG_HOME/cursor/auth.json
|
|
434
|
-
// (verified empirically: relocating XDG_CONFIG_HOME relocates the login;
|
|
435
|
-
// ~/.cursor/cli-config.json holds only account metadata, not the token). Pin
|
|
436
|
-
// XDG_CONFIG_HOME into the version home so each installed Cursor account
|
|
437
|
-
// authenticates from its own token, isolated per run — no global ~/.cursor
|
|
438
|
-
// symlink swap, so concurrent runs on different accounts never clobber one
|
|
439
|
-
// another. cli-config.json (HOME-relative) has no override and stays on the
|
|
440
|
-
// shared home; only the token is per-account, which is what gates the login.
|
|
441
|
-
const cwd = options.cwd || process.cwd();
|
|
442
|
-
const resolvedVersion = options.version ?? resolveVersion('cursor', cwd);
|
|
443
|
-
const version = options.version
|
|
444
|
-
? resolvedVersion
|
|
445
|
-
: (resolvedVersion && isVersionInstalled('cursor', resolvedVersion) ? resolvedVersion : null);
|
|
446
|
-
if (version) {
|
|
447
|
-
result.XDG_CONFIG_HOME = path.join(getVersionHomePath('cursor', version), '.config');
|
|
448
|
-
}
|
|
449
|
-
delete result.CLAUDE_CONFIG_DIR;
|
|
450
|
-
delete result.CODEX_HOME;
|
|
451
|
-
delete result.COPILOT_HOME;
|
|
452
|
-
delete result.KIMI_CODE_HOME;
|
|
288
|
+
// understand. Each harness adapter pins its own config-dir var to the versioned
|
|
289
|
+
// home and strips the foreign ones; a harness with no config-dir env (the old
|
|
290
|
+
// `else`) has no adapter override, so all four are stripped here.
|
|
291
|
+
const configAdapter = resolveHarnessAdapter(options.agent);
|
|
292
|
+
if (configAdapter.applyExecConfigEnv) {
|
|
293
|
+
// Resolve version/versionHome here (this module already imports
|
|
294
|
+
// installations/versions); the adapters must not import it themselves or they
|
|
295
|
+
// close a versions -> shims -> harness -> adapter import cycle.
|
|
296
|
+
const { version, versionHome } = resolveConfigVersion(options.agent, options.cwd || process.cwd(), options.version);
|
|
297
|
+
configAdapter.applyExecConfigEnv(result, {
|
|
298
|
+
agent: options.agent,
|
|
299
|
+
version,
|
|
300
|
+
versionHome,
|
|
301
|
+
interactive: resolveInteractive(options),
|
|
302
|
+
resolveClaudeSetupToken,
|
|
303
|
+
});
|
|
453
304
|
}
|
|
454
305
|
else {
|
|
455
|
-
|
|
456
|
-
delete result.CODEX_HOME;
|
|
457
|
-
delete result.COPILOT_HOME;
|
|
458
|
-
delete result.KIMI_CODE_HOME;
|
|
306
|
+
stripForeignConfigDir(result);
|
|
459
307
|
}
|
|
460
308
|
// Point the agent at its own mailbox so the PreToolUse `mailbox-inject` hook
|
|
461
309
|
// knows which box to drain and inject mid-run. Keyed by the session id — the
|
|
@@ -952,18 +800,24 @@ export function buildExecCommand(options) {
|
|
|
952
800
|
// AGENT_COMMANDS.modeFlags drifted apart. Tests assert they agree.
|
|
953
801
|
throw new Error(`Internal error: ${options.agent} declares '${resolvedMode}' in capabilities.modes but has no entry in AGENT_COMMANDS.modeFlags.${resolvedMode}.`);
|
|
954
802
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
803
|
+
// Launch-arg quirks (the harness axis of Move 3): cursor's additive `--trust`
|
|
804
|
+
// and the codex/kimi mode-flag overrides move to the harness adapter, so the
|
|
805
|
+
// per-agent name-chain collapses to one dispatch. The generic resume-subcommand
|
|
806
|
+
// and modeFlags paths stay here — they are not per-harness.
|
|
807
|
+
const launchAdapter = resolveHarnessAdapter(options.agent);
|
|
808
|
+
const launchArgsCtx = {
|
|
809
|
+
resolvedMode,
|
|
810
|
+
interactive,
|
|
811
|
+
cwd: options.cwd ?? process.cwd(),
|
|
812
|
+
addDirs: (options.addDirs ?? []).map(expandLocalHome),
|
|
813
|
+
};
|
|
814
|
+
const preModeArgs = launchAdapter.execPreModeArgs?.(launchArgsCtx);
|
|
815
|
+
if (preModeArgs) {
|
|
816
|
+
cmd.push(...preModeArgs);
|
|
959
817
|
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
...codexEditWritableRoots(options.cwd ?? process.cwd()),
|
|
964
|
-
...(options.addDirs ?? []).map(expandLocalHome),
|
|
965
|
-
];
|
|
966
|
-
cmd.push(...codexPolicyArgs(policyMode, writableRoots));
|
|
818
|
+
const modeArgsOverride = launchAdapter.execModeArgs?.(launchArgsCtx);
|
|
819
|
+
if (modeArgsOverride !== undefined) {
|
|
820
|
+
cmd.push(...modeArgsOverride);
|
|
967
821
|
}
|
|
968
822
|
else if (resumeSpec && 'subcommand' in resumeSpec) {
|
|
969
823
|
if (resolvedMode === 'skip') {
|
|
@@ -982,21 +836,6 @@ export function buildExecCommand(options) {
|
|
|
982
836
|
cmd.push(...modeFlags);
|
|
983
837
|
}
|
|
984
838
|
}
|
|
985
|
-
else if (options.agent === 'kimi' && !interactive) {
|
|
986
|
-
// kimi's headless prompt mode (`-p`/`--prompt`) is self-contained and REFUSES
|
|
987
|
-
// to be combined with any startup-mode flag: `--plan`, `--auto`, and `--yolo`
|
|
988
|
-
// all abort with "Cannot combine --prompt with --X" (verified against the live
|
|
989
|
-
// kimi CLI). The write-capable modes (edit/auto/skip) all collapse to kimi's
|
|
990
|
-
// default `-p` behavior, which already auto-approves tool calls, so we emit no
|
|
991
|
-
// mode flag. Plan can't reach here headless — resolveHeadlessMode already
|
|
992
|
-
// downgraded it to auto (kimi's headlessPlan:false); this asserts that
|
|
993
|
-
// invariant so a plan-mode run can never silently mutate the workspace.
|
|
994
|
-
if (resolvedMode === 'plan') {
|
|
995
|
-
throw new Error(`Internal error: kimi reached headless command build with resolved mode 'plan'; ` +
|
|
996
|
-
`resolveHeadlessMode should have downgraded it to auto (capabilities.headlessPlan is false).`);
|
|
997
|
-
}
|
|
998
|
-
// edit/auto/skip: emit no mode flag — `kimi -p` auto-runs.
|
|
999
|
-
}
|
|
1000
839
|
else {
|
|
1001
840
|
cmd.push(...modeFlags);
|
|
1002
841
|
}
|
|
@@ -1228,7 +1067,7 @@ export async function execShimPassthrough(agent, rawArgs, cwd, pinnedVersion) {
|
|
|
1228
1067
|
// not parse `rawArgs`, so a `-p "task"` passthrough is headless in fact and
|
|
1229
1068
|
// interactive by this classification, and loses the token. Windows-only in
|
|
1230
1069
|
// practice (POSIX uses the bash shim, which execs the binary and never reaches
|
|
1231
|
-
// buildExecEnv — `shims.ts
|
|
1070
|
+
// buildExecEnv — `installations/shims.ts` generateShimScript).
|
|
1232
1071
|
const env = buildExecEnv({ agent, version, cwd, mode: defaultModeFor(agent), effort: 'auto', env: { AGENT_LAUNCH_ID: launchId } });
|
|
1233
1072
|
const { command, args, shell } = resolveShimSpawn(process.platform, binary, [...launchArgs, ...rawArgs]);
|
|
1234
1073
|
return new Promise((resolve) => {
|
|
@@ -2246,12 +2085,20 @@ export async function runWithFallback(options) {
|
|
|
2246
2085
|
}
|
|
2247
2086
|
throw err;
|
|
2248
2087
|
}
|
|
2249
|
-
|
|
2088
|
+
const output = `${result.stderr}\n${result.stdout}`;
|
|
2089
|
+
const sessionLimitReset = agent === 'claude' ? parseClaudeSessionLimitReset(output) : null;
|
|
2090
|
+
if (sessionLimitReset && version) {
|
|
2091
|
+
const account = await getAccountInfo(agent, getVersionHomePath(agent, version));
|
|
2092
|
+
const usageKey = getUsageLookupKey(account);
|
|
2093
|
+
if (usageKey)
|
|
2094
|
+
noteClaudeSessionLimit(usageKey, sessionLimitReset);
|
|
2095
|
+
}
|
|
2096
|
+
if (result.exitCode === 0 && !sessionLimitReset)
|
|
2250
2097
|
return 0;
|
|
2251
2098
|
const isLast = i === chain.length - 1;
|
|
2252
2099
|
if (isLast)
|
|
2253
|
-
return result.exitCode;
|
|
2254
|
-
if (!detectRateLimit(result.stderr) && !detectRateLimit(result.stdout)) {
|
|
2100
|
+
return result.exitCode || 1;
|
|
2101
|
+
if (!sessionLimitReset && !detectRateLimit(result.stderr) && !detectRateLimit(result.stdout)) {
|
|
2255
2102
|
return result.exitCode;
|
|
2256
2103
|
}
|
|
2257
2104
|
const next = chain[i + 1];
|
package/dist/lib/feed/feed.js
CHANGED
|
@@ -28,6 +28,7 @@ import { stringifyDoc } from '../yaml-io.js';
|
|
|
28
28
|
import { getFeedDir, getUserAgentsDir } from '../state.js';
|
|
29
29
|
import { isAdmin, isHighConsequenceAllowed, isKnownOperator } from '../operator.js';
|
|
30
30
|
import { projectKeyFromCwd } from '../project-key.js';
|
|
31
|
+
import { atomicWriteJsonSync } from '../fs-atomic.js';
|
|
31
32
|
/**
|
|
32
33
|
* Stable block id for a session. One block per session -- a new question
|
|
33
34
|
* replaces the previous one (the agent can only ask one question at a time).
|
|
@@ -56,11 +57,6 @@ function safeReadJson(file) {
|
|
|
56
57
|
return undefined;
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
|
-
function atomicWriteJson(file, value) {
|
|
60
|
-
const tmp = `${file}.${process.pid}.tmp`;
|
|
61
|
-
fs.writeFileSync(tmp, JSON.stringify(value, null, 2), 'utf-8');
|
|
62
|
-
fs.renameSync(tmp, file);
|
|
63
|
-
}
|
|
64
60
|
/** Read one block record. Returns undefined when missing or corrupt. */
|
|
65
61
|
export function readBlock(blockId, root) {
|
|
66
62
|
const parsed = safeReadJson(blockPath(root ?? getFeedDir(), blockId));
|
|
@@ -285,9 +281,7 @@ export function publishBlock(block, root) {
|
|
|
285
281
|
const dir = root ?? getFeedDir();
|
|
286
282
|
fs.mkdirSync(dir, { recursive: true });
|
|
287
283
|
const target = blockPath(dir, block.blockId);
|
|
288
|
-
|
|
289
|
-
fs.writeFileSync(tmp, JSON.stringify(block, null, 2), 'utf-8');
|
|
290
|
-
fs.renameSync(tmp, target);
|
|
284
|
+
atomicWriteJsonSync(target, block);
|
|
291
285
|
}
|
|
292
286
|
/** Read all block records. Returns them sorted by stable block filename. */
|
|
293
287
|
export function listBlocks(root) {
|
package/dist/lib/format.d.ts
CHANGED
|
@@ -27,6 +27,15 @@ export declare function formatDie(msg: string, opts?: DieOptions): {
|
|
|
27
27
|
* keep the original red-stderr behavior.
|
|
28
28
|
*/
|
|
29
29
|
export declare function die(msg: string, code?: number, opts?: DieOptions): never;
|
|
30
|
+
/**
|
|
31
|
+
* Await a command action and turn a thrown Error into a clean `die(message)`
|
|
32
|
+
* instead of Node's raw stack dump — bootstrap's parseAsync catch deliberately
|
|
33
|
+
* rethrows plain Errors as engineering bugs, so a command whose helpers throw
|
|
34
|
+
* user-actionable errors (auth, org) wraps its `.action(...)` call site with
|
|
35
|
+
* this. The helpers keep throwing so tests can assert on them. Pass `json`
|
|
36
|
+
* so a `--json` caller still gets the structured `{"error"}` payload.
|
|
37
|
+
*/
|
|
38
|
+
export declare function runOrDie(fn: () => void | Promise<void>, opts?: DieOptions): Promise<void>;
|
|
30
39
|
/**
|
|
31
40
|
* `die()` with a structured friction event attached. Use this at CLI error
|
|
32
41
|
* chokepoints so the nightly routine can classify and rank recurring failures
|
package/dist/lib/format.js
CHANGED
|
@@ -44,6 +44,22 @@ export function die(msg, code = 1, opts = {}) {
|
|
|
44
44
|
console.error(text);
|
|
45
45
|
process.exit(code);
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Await a command action and turn a thrown Error into a clean `die(message)`
|
|
49
|
+
* instead of Node's raw stack dump — bootstrap's parseAsync catch deliberately
|
|
50
|
+
* rethrows plain Errors as engineering bugs, so a command whose helpers throw
|
|
51
|
+
* user-actionable errors (auth, org) wraps its `.action(...)` call site with
|
|
52
|
+
* this. The helpers keep throwing so tests can assert on them. Pass `json`
|
|
53
|
+
* so a `--json` caller still gets the structured `{"error"}` payload.
|
|
54
|
+
*/
|
|
55
|
+
export async function runOrDie(fn, opts = {}) {
|
|
56
|
+
try {
|
|
57
|
+
await fn();
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
die(err instanceof Error ? err.message : String(err), 1, opts);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
47
63
|
/**
|
|
48
64
|
* `die()` with a structured friction event attached. Use this at CLI error
|
|
49
65
|
* chokepoints so the nightly routine can classify and rank recurring failures
|
package/dist/lib/fs-atomic.d.ts
CHANGED
|
@@ -11,6 +11,13 @@ export declare function ensureLockTarget(filePath: string, initialContent?: stri
|
|
|
11
11
|
* partial write. On POSIX, rename(2) is atomic.
|
|
12
12
|
*/
|
|
13
13
|
export declare function atomicWriteFileSync(filePath: string, content: string, options?: fs.WriteFileOptions): void;
|
|
14
|
+
/**
|
|
15
|
+
* Convenience wrapper around {@link atomicWriteFileSync} for the common case of
|
|
16
|
+
* writing pretty-printed JSON (RUSH-2840). Same tmp-then-rename mechanics, same
|
|
17
|
+
* caller responsibility to ensure the parent directory exists first — this adds
|
|
18
|
+
* only the `JSON.stringify`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function atomicWriteJsonSync(filePath: string, data: unknown): void;
|
|
14
21
|
/**
|
|
15
22
|
* Acquires an exclusive proper-lockfile lock on filePath, runs fn, then
|
|
16
23
|
* releases the lock. Retries with capped linear back-off until either the lock
|
package/dist/lib/fs-atomic.js
CHANGED
|
@@ -55,6 +55,15 @@ export function atomicWriteFileSync(filePath, content, options = 'utf-8') {
|
|
|
55
55
|
throw err;
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Convenience wrapper around {@link atomicWriteFileSync} for the common case of
|
|
60
|
+
* writing pretty-printed JSON (RUSH-2840). Same tmp-then-rename mechanics, same
|
|
61
|
+
* caller responsibility to ensure the parent directory exists first — this adds
|
|
62
|
+
* only the `JSON.stringify`.
|
|
63
|
+
*/
|
|
64
|
+
export function atomicWriteJsonSync(filePath, data) {
|
|
65
|
+
atomicWriteFileSync(filePath, JSON.stringify(data, null, 2));
|
|
66
|
+
}
|
|
58
67
|
export function withFileLock(filePath, fn, opts = {}) {
|
|
59
68
|
let release = null;
|
|
60
69
|
let lastError;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List this user's mergeable PRs across registered project repos.
|
|
3
|
+
*
|
|
4
|
+
* The built-in `pr-merge-on-green` monitor used to run `gh pr list --author @me`
|
|
5
|
+
* with no `--repo`. `gh` then infers the repository from the working directory,
|
|
6
|
+
* and the daemon's cwd is not a git repo, so every poll returned empty.
|
|
7
|
+
*
|
|
8
|
+
* This helper:
|
|
9
|
+
* 1. Collects GitHub slugs from `~/.agents/projects/*.yaml` (`repo` / `repos[].slug`).
|
|
10
|
+
* 2. Canonicalizes each slug (`gh repo view` — `phnx-labs/agents-cli` lists
|
|
11
|
+
* nothing; the live name is `phnx-labs/agi-cli`).
|
|
12
|
+
* 3. Runs `gh pr list --repo <slug> --author @me` (cwd-independent).
|
|
13
|
+
* 4. Keeps CI-green PRs with a merge-guard verdict (formal APPROVED review
|
|
14
|
+
* OR an APPROVE comment on THIS PR).
|
|
15
|
+
*
|
|
16
|
+
* Stdout is a single line of `owner/repo#n` refs (or empty). Empty is a silent
|
|
17
|
+
* observation under `condition.mode: every` — do not write to stderr on the
|
|
18
|
+
* no-match path, or command.ts will treat stderr as the observation and fire.
|
|
19
|
+
*/
|
|
20
|
+
import { type ProjectDef } from '../projects.js';
|
|
21
|
+
import { type MergeablePrInput, type StatusCheck } from './pr-verdict.js';
|
|
22
|
+
export type GhExec = (args: string[]) => Promise<string>;
|
|
23
|
+
/** Default runner: `gh` with a 30s timeout. Non-zero exit throws. */
|
|
24
|
+
export declare function ghExec(args: string[]): Promise<string>;
|
|
25
|
+
/** Unique GitHub slugs declared on project definitions. */
|
|
26
|
+
export declare function projectRepoSlugs(defs: readonly ProjectDef[]): string[];
|
|
27
|
+
/**
|
|
28
|
+
* Resolve a slug to GitHub's current `nameWithOwner`. A renamed repo
|
|
29
|
+
* (`phnx-labs/agents-cli` → `phnx-labs/agi-cli`) lists zero PRs under the old
|
|
30
|
+
* name; `gh repo view` returns the live one. On error, keep the input slug.
|
|
31
|
+
*/
|
|
32
|
+
export declare function canonicalizeRepo(slug: string, gh: GhExec): Promise<string>;
|
|
33
|
+
interface ListedPr {
|
|
34
|
+
number: number;
|
|
35
|
+
reviewDecision?: string | null;
|
|
36
|
+
statusCheckRollup?: StatusCheck[] | null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Given PRs already listed for one repo, attach verdict payloads only for the
|
|
40
|
+
* CI-green ones that lack a formal APPROVED reviewDecision, then select.
|
|
41
|
+
*/
|
|
42
|
+
export declare function selectListedMergeable(repo: string, listed: readonly ListedPr[], gh: GhExec): Promise<MergeablePrInput[]>;
|
|
43
|
+
/** List mergeable `owner/repo#n` refs, space-separated. Empty string if none. */
|
|
44
|
+
export declare function listMergeableRefs(opts?: {
|
|
45
|
+
gh?: GhExec;
|
|
46
|
+
defs?: ProjectDef[];
|
|
47
|
+
repos?: string[];
|
|
48
|
+
}): Promise<string>;
|
|
49
|
+
export {};
|