@phnx-labs/agents-cli 1.20.68 → 1.20.70
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 +66 -0
- package/README.md +30 -7
- package/dist/bin/agents +0 -0
- package/dist/commands/computer.d.ts +26 -0
- package/dist/commands/computer.js +173 -149
- package/dist/commands/doctor.js +5 -0
- package/dist/commands/exec.d.ts +18 -0
- package/dist/commands/exec.js +88 -6
- package/dist/commands/fork.d.ts +9 -0
- package/dist/commands/fork.js +67 -0
- package/dist/commands/run-account-picker.d.ts +14 -0
- package/dist/commands/run-account-picker.js +131 -0
- package/dist/commands/setup-browser.d.ts +18 -0
- package/dist/commands/setup-browser.js +142 -0
- package/dist/commands/setup-computer.d.ts +19 -0
- package/dist/commands/setup-computer.js +133 -0
- package/dist/commands/setup-share.d.ts +17 -0
- package/dist/commands/setup-share.js +85 -0
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +58 -1
- package/dist/commands/share.d.ts +15 -0
- package/dist/commands/share.js +10 -4
- package/dist/commands/ssh.js +202 -9
- package/dist/commands/view.d.ts +4 -14
- package/dist/commands/view.js +32 -30
- package/dist/index.js +2 -1
- package/dist/lib/agents.d.ts +10 -0
- package/dist/lib/agents.js +32 -0
- package/dist/lib/auth-health.d.ts +141 -0
- package/dist/lib/auth-health.js +277 -0
- package/dist/lib/browser/chrome.d.ts +9 -0
- package/dist/lib/browser/chrome.js +22 -0
- package/dist/lib/computer/download.d.ts +54 -0
- package/dist/lib/computer/download.js +146 -0
- package/dist/lib/computer-rpc.js +9 -3
- package/dist/lib/daemon.js +38 -0
- package/dist/lib/devices/connect.d.ts +15 -0
- package/dist/lib/devices/connect.js +17 -5
- package/dist/lib/devices/health-report.d.ts +11 -0
- package/dist/lib/devices/health-report.js +73 -4
- package/dist/lib/devices/stats-cache.d.ts +36 -0
- package/dist/lib/devices/stats-cache.js +115 -0
- package/dist/lib/devices/terminfo.d.ts +44 -0
- package/dist/lib/devices/terminfo.js +167 -0
- package/dist/lib/rotate.d.ts +12 -8
- package/dist/lib/rotate.js +22 -23
- package/dist/lib/session/fork.d.ts +32 -0
- package/dist/lib/session/fork.js +101 -0
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/usage.d.ts +23 -0
- package/dist/lib/usage.js +84 -0
- package/package.json +1 -1
package/dist/lib/rotate.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ import { type UsageSnapshot } from './usage.js';
|
|
|
11
11
|
export interface RotateCandidate {
|
|
12
12
|
agent: AgentId;
|
|
13
13
|
version: string;
|
|
14
|
+
accountKey: string | null;
|
|
15
|
+
accountLabel: string;
|
|
14
16
|
email: string | null;
|
|
15
17
|
/**
|
|
16
18
|
* Per-org usage/quota key (e.g. `claude:org=<orgUuid>`) — the unit rate
|
|
@@ -21,7 +23,9 @@ export interface RotateCandidate {
|
|
|
21
23
|
usageKey: string | null;
|
|
22
24
|
usageStatus: AccountInfo['usageStatus'];
|
|
23
25
|
usageSnapshot: UsageSnapshot | null;
|
|
24
|
-
|
|
26
|
+
usageError: string | null;
|
|
27
|
+
plan: string | null;
|
|
28
|
+
signedIn: boolean;
|
|
25
29
|
lastActive: Date | null;
|
|
26
30
|
}
|
|
27
31
|
export interface RotateResult {
|
|
@@ -58,7 +62,7 @@ export declare function getConfiguredRunStrategy(agent: AgentId, startPath?: str
|
|
|
58
62
|
export declare function setGlobalRunStrategy(agent: AgentId, strategy: RunStrategy): void;
|
|
59
63
|
/**
|
|
60
64
|
* Whether a specific account can serve a run right now, and — when it can't —
|
|
61
|
-
* why. `signed_out` covers
|
|
65
|
+
* why. `signed_out` covers a missing usable credential; `rate_limited` and
|
|
62
66
|
* `out_of_credits` name the throttle. Used to pre-warn on a version-pinned
|
|
63
67
|
* teammate whose account rotation won't route around (a pin IS the target).
|
|
64
68
|
*/
|
|
@@ -71,7 +75,7 @@ export type AccountReadiness = {
|
|
|
71
75
|
};
|
|
72
76
|
/**
|
|
73
77
|
* Pure decision reusing the router's own eligibility gate (`hasUsageAvailable`
|
|
74
|
-
* +
|
|
78
|
+
* + canonical signed-in state, i.e. `isRotationEligible`), so a pre-flight warning can NEVER
|
|
75
79
|
* disagree with what rotation would actually do. The `reason` combines the two
|
|
76
80
|
* signals `hasUsageAvailable` reads: the live snapshot (session-inclusive
|
|
77
81
|
* rate-limit) and the coarse cached `usageStatus` (out-of-credits, which a
|
|
@@ -100,17 +104,16 @@ export declare function checkRunAccountReadiness(agent: AgentId, version: string
|
|
|
100
104
|
* headroom, with no stampede on the lowest-usage one. Stateless — parallel
|
|
101
105
|
* callers naturally fan out via the random roll.
|
|
102
106
|
*
|
|
103
|
-
* Eligibility: signed in
|
|
107
|
+
* Eligibility: signed in according to AccountInfo and not currently
|
|
104
108
|
* rate-limited — no blocking window (session OR weekly) at 100%, matching the
|
|
105
109
|
* `agents view` badge; or the local cached status is usable when no live
|
|
106
110
|
* snapshot exists. Note the split: eligibility considers the session window
|
|
107
111
|
* (a session-maxed account can't run now), but the capacity *weight* above is
|
|
108
112
|
* driven by weekly headroom so a brief session spike doesn't distort routing.
|
|
109
113
|
*
|
|
110
|
-
* Dedupe: when multiple versions share
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* account and both 429ing.
|
|
114
|
+
* Dedupe: when multiple versions share a usage/account identity, collapse to
|
|
115
|
+
* one candidate (the least-recently-active version). The org-scoped usage key
|
|
116
|
+
* wins over email so same-email personal and Team accounts remain distinct.
|
|
114
117
|
*
|
|
115
118
|
* Returns null if no candidate is eligible — callers fall back to the pinned
|
|
116
119
|
* version so behavior stays predictable.
|
|
@@ -122,6 +125,7 @@ export declare function pickBalancedCandidate(candidates: RotateCandidate[]): Ro
|
|
|
122
125
|
* usage headroom.
|
|
123
126
|
*/
|
|
124
127
|
export declare function pickAvailableCandidate(candidates: RotateCandidate[], preferredVersion?: string | null): RotateResult | null;
|
|
128
|
+
export declare function collectRunCandidates(agent: AgentId): Promise<RotateCandidate[]>;
|
|
125
129
|
/**
|
|
126
130
|
* Pick a healthy version for `agent` using weighted random by remaining
|
|
127
131
|
* capacity. See `pickBalancedCandidate` for algorithm details.
|
package/dist/lib/rotate.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from 'fs';
|
|
8
8
|
import * as path from 'path';
|
|
9
|
-
import { getAccountInfo } from './agents.js';
|
|
9
|
+
import { accountDisplayLabel, getAccountInfo } from './agents.js';
|
|
10
10
|
import { readMeta, writeMeta, getHelpersDir } from './state.js';
|
|
11
11
|
import { listInstalledVersions, getVersionHomePath, resolveVersion } from './versions.js';
|
|
12
12
|
import { getProjectRunConfigs } from './run-config.js';
|
|
@@ -65,14 +65,10 @@ export function setGlobalRunStrategy(agent, strategy) {
|
|
|
65
65
|
writeMeta(meta);
|
|
66
66
|
}
|
|
67
67
|
function isRotationEligible(candidate) {
|
|
68
|
-
return
|
|
69
|
-
&& candidate.authValid
|
|
70
|
-
&& hasUsageAvailable(candidate);
|
|
68
|
+
return candidate.signedIn && hasUsageAvailable(candidate);
|
|
71
69
|
}
|
|
72
70
|
function isAvailableEligible(candidate) {
|
|
73
|
-
return
|
|
74
|
-
&& candidate.authValid
|
|
75
|
-
&& hasUsageAvailable(candidate);
|
|
71
|
+
return isRotationEligible(candidate);
|
|
76
72
|
}
|
|
77
73
|
function hasUsageAvailable(candidate) {
|
|
78
74
|
const snapshot = candidate.usageSnapshot;
|
|
@@ -95,7 +91,7 @@ function hasUsageAvailable(candidate) {
|
|
|
95
91
|
}
|
|
96
92
|
/**
|
|
97
93
|
* Pure decision reusing the router's own eligibility gate (`hasUsageAvailable`
|
|
98
|
-
* +
|
|
94
|
+
* + canonical signed-in state, i.e. `isRotationEligible`), so a pre-flight warning can NEVER
|
|
99
95
|
* disagree with what rotation would actually do. The `reason` combines the two
|
|
100
96
|
* signals `hasUsageAvailable` reads: the live snapshot (session-inclusive
|
|
101
97
|
* rate-limit) and the coarse cached `usageStatus` (out-of-credits, which a
|
|
@@ -104,7 +100,7 @@ function hasUsageAvailable(candidate) {
|
|
|
104
100
|
* reported while the account is actually serving requests.
|
|
105
101
|
*/
|
|
106
102
|
export function readinessFromCandidate(candidate) {
|
|
107
|
-
if (!candidate.
|
|
103
|
+
if (!candidate.signedIn) {
|
|
108
104
|
return { ready: false, reason: 'signed_out', email: candidate.email };
|
|
109
105
|
}
|
|
110
106
|
if (hasUsageAvailable(candidate)) {
|
|
@@ -162,7 +158,7 @@ function compareCandidates(a, b) {
|
|
|
162
158
|
* key; fall back to email only when no usage identity is available.
|
|
163
159
|
*/
|
|
164
160
|
function candidateIdentity(c) {
|
|
165
|
-
return c.usageKey ?? c.email
|
|
161
|
+
return c.usageKey ?? c.accountKey ?? c.email ?? `${c.agent}@${c.version}`;
|
|
166
162
|
}
|
|
167
163
|
function dedupeAndSortCandidates(candidates) {
|
|
168
164
|
const byIdentity = new Map();
|
|
@@ -189,17 +185,16 @@ function dedupeAndSortCandidates(candidates) {
|
|
|
189
185
|
* headroom, with no stampede on the lowest-usage one. Stateless — parallel
|
|
190
186
|
* callers naturally fan out via the random roll.
|
|
191
187
|
*
|
|
192
|
-
* Eligibility: signed in
|
|
188
|
+
* Eligibility: signed in according to AccountInfo and not currently
|
|
193
189
|
* rate-limited — no blocking window (session OR weekly) at 100%, matching the
|
|
194
190
|
* `agents view` badge; or the local cached status is usable when no live
|
|
195
191
|
* snapshot exists. Note the split: eligibility considers the session window
|
|
196
192
|
* (a session-maxed account can't run now), but the capacity *weight* above is
|
|
197
193
|
* driven by weekly headroom so a brief session spike doesn't distort routing.
|
|
198
194
|
*
|
|
199
|
-
* Dedupe: when multiple versions share
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
* account and both 429ing.
|
|
195
|
+
* Dedupe: when multiple versions share a usage/account identity, collapse to
|
|
196
|
+
* one candidate (the least-recently-active version). The org-scoped usage key
|
|
197
|
+
* wins over email so same-email personal and Team accounts remain distinct.
|
|
203
198
|
*
|
|
204
199
|
* Returns null if no candidate is eligible — callers fall back to the pinned
|
|
205
200
|
* version so behavior stays predictable.
|
|
@@ -278,12 +273,11 @@ export function pickAvailableCandidate(candidates, preferredVersion) {
|
|
|
278
273
|
: undefined;
|
|
279
274
|
return { picked: preferred ?? sorted[0], healthy: sorted, excluded };
|
|
280
275
|
}
|
|
281
|
-
async function collectRunCandidates(agent) {
|
|
276
|
+
export async function collectRunCandidates(agent) {
|
|
282
277
|
const versions = listInstalledVersions(agent);
|
|
283
278
|
const rows = await Promise.all(versions.map(async (version) => {
|
|
284
279
|
const home = getVersionHomePath(agent, version);
|
|
285
280
|
const info = await getAccountInfo(agent, home);
|
|
286
|
-
// `info.email` (from .claude.json's oauthAccount) is the auth heuristic.
|
|
287
281
|
// We used to additionally call isClaudeAuthValid(home), which reads
|
|
288
282
|
// "Claude Code-credentials-<hash>" from the system keychain. That item is
|
|
289
283
|
// written by Claude Code itself with its own process in the ACL, so our
|
|
@@ -291,15 +285,17 @@ async function collectRunCandidates(agent) {
|
|
|
291
285
|
// one per installed version, every time `agents run` cold-starts. If
|
|
292
286
|
// claude's stored token has actually expired, the spawned agent detects
|
|
293
287
|
// it at its own startup and re-auths; that's the correct UX.
|
|
294
|
-
const authValid = info.email != null;
|
|
295
288
|
return {
|
|
296
289
|
agent,
|
|
297
290
|
version,
|
|
298
291
|
home,
|
|
299
292
|
info,
|
|
293
|
+
accountKey: info.accountKey,
|
|
294
|
+
accountLabel: accountDisplayLabel(info),
|
|
300
295
|
email: info.email,
|
|
301
296
|
usageStatus: info.usageStatus,
|
|
302
|
-
|
|
297
|
+
plan: info.plan,
|
|
298
|
+
signedIn: info.signedIn,
|
|
303
299
|
lastActive: info.lastActive,
|
|
304
300
|
};
|
|
305
301
|
}));
|
|
@@ -311,10 +307,13 @@ async function collectRunCandidates(agent) {
|
|
|
311
307
|
})));
|
|
312
308
|
return rows.map(({ home: _home, info, ...candidate }) => {
|
|
313
309
|
const usageKey = getUsageLookupKey(info);
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
310
|
+
const usage = usageKey ? usageByKey.get(usageKey) : undefined;
|
|
311
|
+
return {
|
|
312
|
+
...candidate,
|
|
313
|
+
usageKey,
|
|
314
|
+
usageSnapshot: usage?.snapshot ?? null,
|
|
315
|
+
usageError: usage?.error ?? null,
|
|
316
|
+
};
|
|
318
317
|
});
|
|
319
318
|
}
|
|
320
319
|
/**
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { SessionMeta } from './types.js';
|
|
2
|
+
/** Agents that `fork` can branch today (see the module doc for why). */
|
|
3
|
+
export declare const FORKABLE_AGENTS: readonly ["claude"];
|
|
4
|
+
/** Whether a session's agent can be forked by {@link forkSession}. */
|
|
5
|
+
export declare function isForkableAgent(agent: string): boolean;
|
|
6
|
+
/** Outcome of a successful fork. */
|
|
7
|
+
export interface ForkResult {
|
|
8
|
+
/** The new session's full id. */
|
|
9
|
+
newId: string;
|
|
10
|
+
/** The new session's short id (first 8 chars), for display/resume. */
|
|
11
|
+
shortId: string;
|
|
12
|
+
/** Absolute path of the copied transcript. */
|
|
13
|
+
filePath: string;
|
|
14
|
+
/** The label applied to the fork. */
|
|
15
|
+
label: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Fork a Claude session into a new, independent one.
|
|
19
|
+
*
|
|
20
|
+
* Copies `source.filePath` to a new `<uuid>.jsonl` beside it, rewrites the
|
|
21
|
+
* embedded session id, registers the new session in the index, and records a
|
|
22
|
+
* `--name`-style label. Returns the new ids/path. Throws if the source
|
|
23
|
+
* transcript is missing.
|
|
24
|
+
*
|
|
25
|
+
* @param source The resolved metadata of the session being forked.
|
|
26
|
+
* @param opts.name Optional explicit label; defaults to `fork of <original>`.
|
|
27
|
+
* @param now ISO timestamp to stamp the fork with (injectable for tests).
|
|
28
|
+
*/
|
|
29
|
+
export declare function forkSession(source: SessionMeta, opts?: {
|
|
30
|
+
name?: string;
|
|
31
|
+
now?: string;
|
|
32
|
+
}): ForkResult;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session forking — branch an existing conversation into a new, independent
|
|
3
|
+
* session that can be continued separately, leaving the original untouched.
|
|
4
|
+
*
|
|
5
|
+
* `resume` continues the SAME conversation (same id, same file — it appends).
|
|
6
|
+
* `fork` copies the transcript under a FRESH session id, so continuing the fork
|
|
7
|
+
* diverges from the original instead of mutating it. This is the "git branch"
|
|
8
|
+
* of conversations.
|
|
9
|
+
*
|
|
10
|
+
* v1 supports Claude, whose session id IS its `<id>.jsonl` filename and which
|
|
11
|
+
* resumes natively via `--resume`. A fork is therefore: copy the transcript to
|
|
12
|
+
* a new-uuid filename in the same directory, rewrite the embedded `sessionId`
|
|
13
|
+
* on each line, register the new session in the index, and label it. Other
|
|
14
|
+
* agents (codex single-file; grok/kimi multi-file; opencode DB-only) are a
|
|
15
|
+
* natural follow-up and are refused up front for now.
|
|
16
|
+
*/
|
|
17
|
+
import { randomUUID } from 'crypto';
|
|
18
|
+
import * as fs from 'fs';
|
|
19
|
+
import * as path from 'path';
|
|
20
|
+
import { upsertSession } from './db.js';
|
|
21
|
+
import { recordRunName } from './run-names.js';
|
|
22
|
+
/** Agents that `fork` can branch today (see the module doc for why). */
|
|
23
|
+
export const FORKABLE_AGENTS = ['claude'];
|
|
24
|
+
/** Whether a session's agent can be forked by {@link forkSession}. */
|
|
25
|
+
export function isForkableAgent(agent) {
|
|
26
|
+
return FORKABLE_AGENTS.includes(agent);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Rewrite the per-line `sessionId` field of a Claude JSONL transcript to a new
|
|
30
|
+
* id. Claude resolves a conversation by its filename, so this is belt-and-braces
|
|
31
|
+
* (keeps the in-file id consistent with the new filename); malformed lines are
|
|
32
|
+
* passed through untouched.
|
|
33
|
+
*/
|
|
34
|
+
function rewriteSessionId(transcript, newId) {
|
|
35
|
+
return transcript
|
|
36
|
+
.split('\n')
|
|
37
|
+
.map((line) => {
|
|
38
|
+
if (!line.trim())
|
|
39
|
+
return line;
|
|
40
|
+
try {
|
|
41
|
+
const obj = JSON.parse(line);
|
|
42
|
+
if (typeof obj.sessionId === 'string') {
|
|
43
|
+
obj.sessionId = newId;
|
|
44
|
+
return JSON.stringify(obj);
|
|
45
|
+
}
|
|
46
|
+
return line;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return line;
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
.join('\n');
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Fork a Claude session into a new, independent one.
|
|
56
|
+
*
|
|
57
|
+
* Copies `source.filePath` to a new `<uuid>.jsonl` beside it, rewrites the
|
|
58
|
+
* embedded session id, registers the new session in the index, and records a
|
|
59
|
+
* `--name`-style label. Returns the new ids/path. Throws if the source
|
|
60
|
+
* transcript is missing.
|
|
61
|
+
*
|
|
62
|
+
* @param source The resolved metadata of the session being forked.
|
|
63
|
+
* @param opts.name Optional explicit label; defaults to `fork of <original>`.
|
|
64
|
+
* @param now ISO timestamp to stamp the fork with (injectable for tests).
|
|
65
|
+
*/
|
|
66
|
+
export function forkSession(source, opts = {}) {
|
|
67
|
+
if (!fs.existsSync(source.filePath)) {
|
|
68
|
+
throw new Error(`transcript not found for session ${source.shortId}: ${source.filePath}`);
|
|
69
|
+
}
|
|
70
|
+
const newId = randomUUID();
|
|
71
|
+
const shortId = newId.slice(0, 8);
|
|
72
|
+
const dir = path.dirname(source.filePath);
|
|
73
|
+
const filePath = path.join(dir, `${newId}.jsonl`);
|
|
74
|
+
const transcript = fs.readFileSync(source.filePath, 'utf-8');
|
|
75
|
+
const rewritten = rewriteSessionId(transcript, newId);
|
|
76
|
+
fs.writeFileSync(filePath, rewritten);
|
|
77
|
+
const original = source.label || source.topic || source.shortId;
|
|
78
|
+
const label = opts.name || `fork of ${original}`;
|
|
79
|
+
// Label sidecar (seeds the DB label; survives rescans until an agent title
|
|
80
|
+
// supersedes it), mirroring `agents run --name`.
|
|
81
|
+
recordRunName({ sessionId: newId, name: label, agent: source.agent, cwd: source.cwd });
|
|
82
|
+
// Register the new session so it resolves immediately (by `agents resume`,
|
|
83
|
+
// `agents sessions`, etc.) without waiting for the next scan.
|
|
84
|
+
const stamp = opts.now ?? new Date().toISOString();
|
|
85
|
+
const meta = {
|
|
86
|
+
...source,
|
|
87
|
+
id: newId,
|
|
88
|
+
shortId,
|
|
89
|
+
filePath,
|
|
90
|
+
label,
|
|
91
|
+
timestamp: stamp,
|
|
92
|
+
lastActivity: stamp,
|
|
93
|
+
// The fork has not opened its own PR / team; drop origin-specific refs.
|
|
94
|
+
prUrl: undefined,
|
|
95
|
+
prNumber: undefined,
|
|
96
|
+
teamOrigin: undefined,
|
|
97
|
+
spawnedTeam: undefined,
|
|
98
|
+
};
|
|
99
|
+
upsertSession(meta, rewritten);
|
|
100
|
+
return { newId, shortId, filePath, label };
|
|
101
|
+
}
|
|
@@ -45,6 +45,7 @@ export declare const loadDaemon: ModuleLoader;
|
|
|
45
45
|
export declare const loadRoutines: ModuleLoader;
|
|
46
46
|
export declare const loadMonitors: ModuleLoader;
|
|
47
47
|
export declare const loadRun: ModuleLoader;
|
|
48
|
+
export declare const loadFork: ModuleLoader;
|
|
48
49
|
export declare const loadDefaults: ModuleLoader;
|
|
49
50
|
export declare const loadModels: ModuleLoader;
|
|
50
51
|
export declare const loadPrune: ModuleLoader;
|
|
@@ -23,6 +23,7 @@ export const loadDaemon = async () => (await import('../../commands/daemon.js'))
|
|
|
23
23
|
export const loadRoutines = async () => (await import('../../commands/routines.js')).registerRoutinesCommands;
|
|
24
24
|
export const loadMonitors = async () => (await import('../../commands/monitors.js')).registerMonitorsCommands;
|
|
25
25
|
export const loadRun = async () => (await import('../../commands/exec.js')).registerRunCommand;
|
|
26
|
+
export const loadFork = async () => (await import('../../commands/fork.js')).registerForkCommand;
|
|
26
27
|
export const loadDefaults = async () => (await import('../../commands/defaults.js')).registerDefaultsCommands;
|
|
27
28
|
export const loadModels = async () => (await import('../../commands/models.js')).registerModelsCommand;
|
|
28
29
|
export const loadPrune = async () => (await import('../../commands/prune.js')).registerPruneCommand;
|
|
@@ -126,6 +127,7 @@ export const COMMAND_LOADERS = {
|
|
|
126
127
|
routines: [loadRoutines],
|
|
127
128
|
monitors: [loadMonitors],
|
|
128
129
|
run: [loadRun],
|
|
130
|
+
fork: [loadFork],
|
|
129
131
|
defaults: [loadDefaults],
|
|
130
132
|
models: [loadModels],
|
|
131
133
|
trash: [loadTrash],
|
package/dist/lib/usage.d.ts
CHANGED
|
@@ -184,6 +184,29 @@ export interface DroidBillingLimitsResponse {
|
|
|
184
184
|
} | null;
|
|
185
185
|
} | null;
|
|
186
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Live auth probes — the same authenticated GET the usage fetchers above do,
|
|
189
|
+
* but surfacing the raw HTTP status instead of swallowing 401/expired to null.
|
|
190
|
+
* These back `agents fleet ping` and the fleet auth-health cache: completing a
|
|
191
|
+
* real request is the only proof a token is accepted. The local "signed in"
|
|
192
|
+
* flag cannot tell a revoked-but-unexpired token from a good one. Classification
|
|
193
|
+
* of the returned status into a verdict lives in lib/auth-health.ts (kept there
|
|
194
|
+
* so it stays pure/testable and to avoid an import cycle).
|
|
195
|
+
*/
|
|
196
|
+
export interface ProviderProbe {
|
|
197
|
+
/** HTTP status of the probe request, or null when no request was made (missing/expired token) or the request threw. */
|
|
198
|
+
status: number | null;
|
|
199
|
+
/** Local credential state observed before the request. */
|
|
200
|
+
token: 'present' | 'missing' | 'expired';
|
|
201
|
+
/** Network/parse error message when status is null but a token was present. */
|
|
202
|
+
error?: string;
|
|
203
|
+
}
|
|
204
|
+
/** Probe Claude's OAuth token against the usage endpoint. Refreshes an expired access token (safe for Claude). */
|
|
205
|
+
export declare function probeClaudeStatus(home?: string, cliVersion?: string | null): Promise<ProviderProbe>;
|
|
206
|
+
/** Probe Kimi's OAuth token against the /usages endpoint. Never refreshes (single-use rotation — see getKimiUsageInfo). */
|
|
207
|
+
export declare function probeKimiStatus(home?: string): Promise<ProviderProbe>;
|
|
208
|
+
/** Probe Droid's WorkOS token against the billing-limits endpoint. Never refreshes (single-use rotation — see getDroidUsageInfo). */
|
|
209
|
+
export declare function probeDroidStatus(home?: string): Promise<ProviderProbe>;
|
|
187
210
|
/**
|
|
188
211
|
* Normalize the Factory billing-limits payload into the common UsageWindow
|
|
189
212
|
* shape. Orgs on the legacy (non token-rate-limit) billing model have no
|
package/dist/lib/usage.js
CHANGED
|
@@ -609,6 +609,90 @@ async function getDroidUsageInfo(options) {
|
|
|
609
609
|
return { snapshot: null, error: null };
|
|
610
610
|
}
|
|
611
611
|
}
|
|
612
|
+
/** Probe Claude's OAuth token against the usage endpoint. Refreshes an expired access token (safe for Claude). */
|
|
613
|
+
export async function probeClaudeStatus(home, cliVersion) {
|
|
614
|
+
const oauth = await loadClaudeOauth(home);
|
|
615
|
+
if (!oauth?.accessToken)
|
|
616
|
+
return { status: null, token: 'missing' };
|
|
617
|
+
let access = null;
|
|
618
|
+
try {
|
|
619
|
+
access = await getClaudeAccessToken(oauth, home);
|
|
620
|
+
}
|
|
621
|
+
catch {
|
|
622
|
+
access = null;
|
|
623
|
+
}
|
|
624
|
+
// Fall back to the stored (possibly stale) token so the server returns the
|
|
625
|
+
// real verdict — a 401 here IS the revoked/expired signal we want to surface.
|
|
626
|
+
const bearer = access || oauth.accessToken.trim();
|
|
627
|
+
try {
|
|
628
|
+
const response = await fetch(CLAUDE_USAGE_URL, {
|
|
629
|
+
method: 'GET',
|
|
630
|
+
headers: {
|
|
631
|
+
Authorization: `Bearer ${bearer}`,
|
|
632
|
+
'Content-Type': 'application/json',
|
|
633
|
+
'anthropic-beta': CLAUDE_OAUTH_BETA_HEADER,
|
|
634
|
+
'User-Agent': getClaudeUserAgent(cliVersion),
|
|
635
|
+
},
|
|
636
|
+
signal: AbortSignal.timeout(8000),
|
|
637
|
+
});
|
|
638
|
+
return { status: response.status, token: 'present' };
|
|
639
|
+
}
|
|
640
|
+
catch (err) {
|
|
641
|
+
return { status: null, token: 'present', error: err instanceof Error ? err.message : String(err) };
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
/** Probe Kimi's OAuth token against the /usages endpoint. Never refreshes (single-use rotation — see getKimiUsageInfo). */
|
|
645
|
+
export async function probeKimiStatus(home) {
|
|
646
|
+
const credPath = resolveKimiCredentialPath(home);
|
|
647
|
+
if (!credPath)
|
|
648
|
+
return { status: null, token: 'missing' };
|
|
649
|
+
let accessToken;
|
|
650
|
+
let expiresAt = null;
|
|
651
|
+
try {
|
|
652
|
+
const cred = JSON.parse(fs.readFileSync(credPath, 'utf-8'));
|
|
653
|
+
accessToken = typeof cred?.access_token === 'string' ? cred.access_token : undefined;
|
|
654
|
+
expiresAt = typeof cred?.expires_at === 'number' ? cred.expires_at : null;
|
|
655
|
+
}
|
|
656
|
+
catch {
|
|
657
|
+
return { status: null, token: 'missing' };
|
|
658
|
+
}
|
|
659
|
+
if (!accessToken)
|
|
660
|
+
return { status: null, token: 'missing' };
|
|
661
|
+
if (expiresAt !== null && Date.now() / 1000 >= expiresAt)
|
|
662
|
+
return { status: null, token: 'expired' };
|
|
663
|
+
try {
|
|
664
|
+
const response = await fetch(KIMI_USAGES_URL, {
|
|
665
|
+
method: 'GET',
|
|
666
|
+
headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/json' },
|
|
667
|
+
signal: AbortSignal.timeout(8000),
|
|
668
|
+
});
|
|
669
|
+
return { status: response.status, token: 'present' };
|
|
670
|
+
}
|
|
671
|
+
catch (err) {
|
|
672
|
+
return { status: null, token: 'present', error: err instanceof Error ? err.message : String(err) };
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
/** Probe Droid's WorkOS token against the billing-limits endpoint. Never refreshes (single-use rotation — see getDroidUsageInfo). */
|
|
676
|
+
export async function probeDroidStatus(home) {
|
|
677
|
+
const cred = decryptDroidAuthPayload(home || os.homedir());
|
|
678
|
+
const accessToken = cred?.access_token;
|
|
679
|
+
if (typeof accessToken !== 'string' || !accessToken)
|
|
680
|
+
return { status: null, token: 'missing' };
|
|
681
|
+
const exp = decodeJwtPayload(accessToken)?.exp;
|
|
682
|
+
if (typeof exp === 'number' && Date.now() / 1000 >= exp)
|
|
683
|
+
return { status: null, token: 'expired' };
|
|
684
|
+
try {
|
|
685
|
+
const response = await fetch(DROID_USAGE_URL, {
|
|
686
|
+
method: 'GET',
|
|
687
|
+
headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/json' },
|
|
688
|
+
signal: AbortSignal.timeout(8000),
|
|
689
|
+
});
|
|
690
|
+
return { status: response.status, token: 'present' };
|
|
691
|
+
}
|
|
692
|
+
catch (err) {
|
|
693
|
+
return { status: null, token: 'present', error: err instanceof Error ? err.message : String(err) };
|
|
694
|
+
}
|
|
695
|
+
}
|
|
612
696
|
/**
|
|
613
697
|
* Normalize the Factory billing-limits payload into the common UsageWindow
|
|
614
698
|
* shape. Orgs on the legacy (non token-rate-limit) billing model have no
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.70",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|