@phnx-labs/agents-cli 1.20.63 → 1.20.64
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 +17 -0
- package/README.md +9 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/exec.js +55 -28
- package/dist/commands/feed.d.ts +4 -0
- package/dist/commands/feed.js +27 -8
- package/dist/commands/lease.d.ts +23 -0
- package/dist/commands/lease.js +201 -0
- package/dist/commands/mailboxes.d.ts +20 -0
- package/dist/commands/mailboxes.js +390 -0
- package/dist/commands/routines.js +20 -14
- package/dist/commands/sessions-export.d.ts +2 -0
- package/dist/commands/sessions-export.js +279 -0
- package/dist/commands/sessions-import.d.ts +2 -0
- package/dist/commands/sessions-import.js +230 -0
- package/dist/commands/sessions.js +4 -0
- package/dist/commands/ssh.js +98 -3
- package/dist/commands/usage.d.ts +2 -0
- package/dist/commands/usage.js +7 -2
- package/dist/commands/view.d.ts +1 -1
- package/dist/index.js +2 -1
- package/dist/lib/agents.d.ts +18 -0
- package/dist/lib/agents.js +27 -17
- package/dist/lib/browser/drivers/ssh.js +19 -2
- package/dist/lib/comms-render.d.ts +37 -0
- package/dist/lib/comms-render.js +89 -0
- package/dist/lib/crabbox/cli.d.ts +72 -0
- package/dist/lib/crabbox/cli.js +158 -9
- package/dist/lib/crabbox/runtimes.d.ts +13 -0
- package/dist/lib/crabbox/runtimes.js +24 -0
- package/dist/lib/daemon.js +6 -1
- package/dist/lib/devices/health.d.ts +77 -0
- package/dist/lib/devices/health.js +186 -0
- package/dist/lib/mailbox.d.ts +39 -0
- package/dist/lib/mailbox.js +112 -0
- package/dist/lib/paths.d.ts +13 -0
- package/dist/lib/paths.js +26 -4
- package/dist/lib/routines.d.ts +21 -2
- package/dist/lib/routines.js +35 -12
- package/dist/lib/runner.js +255 -13
- package/dist/lib/sandbox.d.ts +9 -1
- package/dist/lib/sandbox.js +11 -2
- package/dist/lib/session/bundle.d.ts +150 -0
- package/dist/lib/session/bundle.js +189 -0
- package/dist/lib/session/remote-bundle.d.ts +12 -0
- package/dist/lib/session/remote-bundle.js +61 -0
- package/dist/lib/session/sync/agents.d.ts +54 -6
- package/dist/lib/session/sync/agents.js +0 -0
- package/dist/lib/session/sync/manifest.d.ts +14 -3
- package/dist/lib/session/sync/manifest.js +4 -0
- package/dist/lib/session/sync/sync.d.ts +23 -2
- package/dist/lib/session/sync/sync.js +177 -74
- package/dist/lib/ssh-tunnel.js +13 -1
- package/dist/lib/staleness/detectors/subagents.d.ts +5 -0
- package/dist/lib/staleness/detectors/subagents.js +5 -192
- package/dist/lib/staleness/writers/subagents.d.ts +10 -0
- package/dist/lib/staleness/writers/subagents.js +11 -102
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +5 -0
- package/dist/lib/subagents-registry.d.ts +85 -0
- package/dist/lib/subagents-registry.js +393 -0
- package/dist/lib/subagents.d.ts +8 -8
- package/dist/lib/subagents.js +32 -663
- package/dist/lib/sync-umbrella.d.ts +1 -0
- package/dist/lib/sync-umbrella.js +14 -3
- package/dist/lib/types.d.ts +9 -0
- package/dist/lib/usage.d.ts +42 -3
- package/dist/lib/usage.js +162 -22
- package/package.json +1 -1
|
@@ -118,9 +118,20 @@ export async function runUmbrellaSync(args) {
|
|
|
118
118
|
const { isSyncConfigured } = await import('./session/sync/config.js');
|
|
119
119
|
if (isBetaEnabled('session-sync') && isSyncConfigured()) {
|
|
120
120
|
const { syncSessions } = await import('./session/sync/sync.js');
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
// Record a failure here instead of letting it throw: like the fetchSecrets
|
|
122
|
+
// block above, a failure in one fetch stage must not abort the others or
|
|
123
|
+
// the reconcile below (see this function's doc comment). Without this,
|
|
124
|
+
// syncSessions() throwing — e.g. via the C1 containment reject on a
|
|
125
|
+
// malicious peer manifest — would skip `plan.reconcile` entirely.
|
|
126
|
+
try {
|
|
127
|
+
const r = await syncSessions();
|
|
128
|
+
result.sessions = { ran: true, pushed: r.pushed, pulled: r.pulled, merged: r.merged };
|
|
129
|
+
log(`sessions: pushed ${r.pushed}, pulled ${r.pulled}, merged ${r.merged}`);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
result.sessions = { ran: false, pushed: 0, pulled: 0, merged: 0, error: err.message };
|
|
133
|
+
log(`sessions: failed — ${err.message}`);
|
|
134
|
+
}
|
|
124
135
|
}
|
|
125
136
|
else {
|
|
126
137
|
result.sessions = { ran: false, pushed: 0, pulled: 0, merged: 0 };
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -624,6 +624,15 @@ export interface ExtraRepoConfig {
|
|
|
624
624
|
export interface Meta {
|
|
625
625
|
agents?: Partial<Record<AgentId, string>>;
|
|
626
626
|
run?: RunConfig;
|
|
627
|
+
/**
|
|
628
|
+
* `agents run --lease` config. `secretsBundle` names the keychain secrets bundle
|
|
629
|
+
* whose provider token (e.g. `HCLOUD_TOKEN`) crabbox uses to reach the cloud API.
|
|
630
|
+
* When unset, the bundle is resolved by env (`AGENTS_LEASE_SECRETS_BUNDLE`) then
|
|
631
|
+
* auto-detected (the first bundle that declares a provider token key).
|
|
632
|
+
*/
|
|
633
|
+
lease?: {
|
|
634
|
+
secretsBundle?: string;
|
|
635
|
+
};
|
|
627
636
|
/** macOS secrets-agent config. `policy` is the default prompt policy for
|
|
628
637
|
* bundles without an explicit per-bundle policy: `daily` (the default) asks
|
|
629
638
|
* once per ~7 days, `always` asks every time. `auto` (default on) lets the
|
package/dist/lib/usage.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AccountInfo } from './agents.js';
|
|
2
2
|
import type { AgentId } from './types.js';
|
|
3
3
|
/** Discriminator for usage window types. */
|
|
4
|
-
export type UsageWindowKey = 'session' | 'week' | 'sonnet_week';
|
|
4
|
+
export type UsageWindowKey = 'session' | 'week' | 'sonnet_week' | 'month';
|
|
5
5
|
/** A single rate-limit window with utilization percentage and reset time. */
|
|
6
6
|
export interface UsageWindow {
|
|
7
7
|
key: UsageWindowKey;
|
|
@@ -154,7 +154,46 @@ export interface KimiUsagesResponse {
|
|
|
154
154
|
export declare function normalizeKimiWindows(data: KimiUsagesResponse): UsageWindow[];
|
|
155
155
|
/** Derive a display plan label from Kimi's membership tier or subscription type. */
|
|
156
156
|
export declare function formatKimiPlan(data: KimiUsagesResponse): string | null;
|
|
157
|
-
/**
|
|
157
|
+
/** A single Droid token-rate-limit window from /api/billing/limits. */
|
|
158
|
+
interface DroidLimitWindow {
|
|
159
|
+
usedPercent?: number | null;
|
|
160
|
+
windowEnd?: string | null;
|
|
161
|
+
}
|
|
162
|
+
/** Response shape from Factory's billing limits endpoint (subset we render). */
|
|
163
|
+
export interface DroidBillingLimitsResponse {
|
|
164
|
+
usesTokenRateLimitsBilling?: boolean | null;
|
|
165
|
+
limits?: {
|
|
166
|
+
standard?: {
|
|
167
|
+
fiveHour?: DroidLimitWindow | null;
|
|
168
|
+
weekly?: DroidLimitWindow | null;
|
|
169
|
+
monthly?: DroidLimitWindow | null;
|
|
170
|
+
} | null;
|
|
171
|
+
} | null;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Normalize the Factory billing-limits payload into the common UsageWindow
|
|
175
|
+
* shape. Orgs on the legacy (non token-rate-limit) billing model have no
|
|
176
|
+
* meaningful windows, so they render nothing — mirrors droid's own gate on
|
|
177
|
+
* `usesTokenRateLimitsBilling` before it reads `limits.standard`.
|
|
178
|
+
*/
|
|
179
|
+
export declare function normalizeDroidWindows(data: DroidBillingLimitsResponse): UsageWindow[];
|
|
180
|
+
/**
|
|
181
|
+
* Load a version home's Claude OAuth credential from the two stores Claude Code
|
|
182
|
+
* uses, tried in order:
|
|
183
|
+
*
|
|
184
|
+
* 1. The OS keychain (`getKeychainToken`). Canonical on macOS — Claude Code
|
|
185
|
+
* writes the token to the login keychain and we read it via `/usr/bin/security`.
|
|
186
|
+
* 2. `<home>/.claude/.credentials.json`. On a headless Linux box (the
|
|
187
|
+
* `agents view --host <linux>` case) there is no reachable Secret Service, so
|
|
188
|
+
* the Claude CLI stores its OAuth token in this plaintext file instead. The
|
|
189
|
+
* keychain read above finds nothing on that platform, so we fall back to the
|
|
190
|
+
* file. Same wrapped `{ claudeAiOauth }` shape, so one parser handles both.
|
|
191
|
+
* Mirrors `readClaudeCredentialsBlob` (cloud/rush.ts), the proven pattern.
|
|
192
|
+
*
|
|
193
|
+
* Without step 2 the live usage fetch got no token on Linux, so `agents view`
|
|
194
|
+
* (run remotely over SSH by `--host`) rendered no usage bars even though the
|
|
195
|
+
* account + plan — read from the plaintext `.claude.json` — showed fine.
|
|
196
|
+
*/
|
|
158
197
|
export declare function loadClaudeOauth(home?: string): Promise<ClaudeOauthCredentials | null>;
|
|
159
198
|
/**
|
|
160
199
|
* Derive the Keychain service name for a Claude home directory.
|
package/dist/lib/usage.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Usage and rate-limit tracking for Claude and
|
|
2
|
+
* Usage and rate-limit tracking for Claude, Codex, Kimi, and Droid agents.
|
|
3
3
|
*
|
|
4
|
-
* Fetches live usage data from
|
|
4
|
+
* Fetches live usage data from each agent's usage API (Anthropic OAuth for
|
|
5
|
+
* Claude, Kimi Code /usages, Factory billing limits for Droid) or parses
|
|
5
6
|
* rate-limit events from Codex session logs. Results are normalized into a
|
|
6
7
|
* common UsageSnapshot shape, cached to disk, and rendered as terminal
|
|
7
8
|
* progress bars for the `agents view` command.
|
|
@@ -14,6 +15,7 @@ import * as path from 'path';
|
|
|
14
15
|
import * as readline from 'readline';
|
|
15
16
|
import { promisify } from 'util';
|
|
16
17
|
import chalk from 'chalk';
|
|
18
|
+
import { decodeJwtPayload, decryptDroidAuthPayload } from './agents.js';
|
|
17
19
|
import { walkForFiles } from './fs-walk.js';
|
|
18
20
|
import { getKeychainToken, setKeychainToken, deleteKeychainToken, } from './secrets/index.js';
|
|
19
21
|
import { getCacheDir } from './state.js';
|
|
@@ -34,6 +36,7 @@ const CLAUDE_KEYCHAIN_SERVICE = 'Claude Code-credentials';
|
|
|
34
36
|
const getClaudeUsageCachePath = () => path.join(getCacheDir(), 'claude-usage.json');
|
|
35
37
|
const CACHED_CLAUDE_USAGE_SOURCE_LABEL = 'last seen live account data';
|
|
36
38
|
const KIMI_USAGES_URL = 'https://api.kimi.com/coding/v1/usages';
|
|
39
|
+
const DROID_USAGE_URL = 'https://api.factory.ai/api/billing/limits';
|
|
37
40
|
const COMPACT_BAR_LEN = 5;
|
|
38
41
|
const USAGE_BAR_LEN = 10;
|
|
39
42
|
const FULL = '\u2588';
|
|
@@ -47,6 +50,8 @@ export async function getUsageInfo(agentId, options) {
|
|
|
47
50
|
return getCodexUsageInfo(options);
|
|
48
51
|
case 'kimi':
|
|
49
52
|
return getKimiUsageInfo(options);
|
|
53
|
+
case 'droid':
|
|
54
|
+
return getDroidUsageInfo(options);
|
|
50
55
|
default:
|
|
51
56
|
return { snapshot: null, error: null };
|
|
52
57
|
}
|
|
@@ -117,13 +122,14 @@ const inFlightRefreshes = new Map();
|
|
|
117
122
|
*/
|
|
118
123
|
export async function getUsageInfoForIdentity(input) {
|
|
119
124
|
const usageKey = getUsageLookupKey(input.info);
|
|
120
|
-
// Agents whose usage comes from a live network call (Claude, Kimi) go
|
|
121
|
-
// the stale-while-revalidate cache below so `agents run`/`agents view`
|
|
122
|
-
// the network on the hot path. Everything else (Codex reads local
|
|
123
|
-
// logs) takes the legacy blocking path. The on-disk cache is shared and
|
|
124
|
-
// by usageKey, which is namespaced per agent (`claude:org=…`,
|
|
125
|
-
// so one cache file holds every account without
|
|
126
|
-
|
|
125
|
+
// Agents whose usage comes from a live network call (Claude, Kimi, Droid) go
|
|
126
|
+
// through the stale-while-revalidate cache below so `agents run`/`agents view`
|
|
127
|
+
// stay off the network on the hot path. Everything else (Codex reads local
|
|
128
|
+
// session logs) takes the legacy blocking path. The on-disk cache is shared and
|
|
129
|
+
// keyed by usageKey, which is namespaced per agent (`claude:org=…`,
|
|
130
|
+
// `kimi:user=…`, `droid:org=…`), so one cache file holds every account without
|
|
131
|
+
// collision.
|
|
132
|
+
const usesNetworkUsage = input.agentId === 'claude' || input.agentId === 'kimi' || input.agentId === 'droid';
|
|
127
133
|
if (!usesNetworkUsage || !usageKey) {
|
|
128
134
|
return getUsageInfo(input.agentId, {
|
|
129
135
|
home: input.home,
|
|
@@ -201,8 +207,12 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3) {
|
|
|
201
207
|
parts.push(chalk.gray(plan.padEnd(planWidth)));
|
|
202
208
|
}
|
|
203
209
|
if (snapshot) {
|
|
210
|
+
// Compact rows show the two windows every agent shares (session + week);
|
|
211
|
+
// extra windows (Claude's Sonnet week, Droid's month) render only in the
|
|
212
|
+
// full per-version usage section. An exhausted month window still surfaces
|
|
213
|
+
// here as the rate-limited badge via deriveUsageStatusFromSnapshot.
|
|
204
214
|
const windows = snapshot.windows
|
|
205
|
-
.filter((window) => window.key
|
|
215
|
+
.filter((window) => window.key === 'session' || window.key === 'week')
|
|
206
216
|
.map((window) => `${chalk.gray(`${window.shortLabel}:`)} ${renderCompactUsageBar(window.usedPercent)}`);
|
|
207
217
|
if (windows.length > 0) {
|
|
208
218
|
parts.push(windows.join(' '));
|
|
@@ -513,6 +523,95 @@ export function formatKimiPlan(data) {
|
|
|
513
523
|
return null;
|
|
514
524
|
return tail.charAt(0).toUpperCase() + tail.slice(1).toLowerCase();
|
|
515
525
|
}
|
|
526
|
+
/**
|
|
527
|
+
* Fetch Droid usage via Factory's billing limits API — the same endpoint the
|
|
528
|
+
* droid CLI polls for its token-limit banner. The WorkOS access token comes
|
|
529
|
+
* from the locally decrypted ~/.factory/auth.v2.file (the same credential
|
|
530
|
+
* account identity in agents.ts reads).
|
|
531
|
+
*
|
|
532
|
+
* Deliberately NO token refresh, for a sharper reason than Kimi's: WorkOS
|
|
533
|
+
* refresh tokens are single-use and rotate on every exchange, so refreshing
|
|
534
|
+
* here would race a concurrently running droid session and can permanently
|
|
535
|
+
* invalidate the user's login chain. Droid refreshes its own credential when
|
|
536
|
+
* it runs; if the stored token is expired we skip the live fetch and let the
|
|
537
|
+
* SWR cache serve the last-seen snapshot.
|
|
538
|
+
*/
|
|
539
|
+
async function getDroidUsageInfo(options) {
|
|
540
|
+
try {
|
|
541
|
+
const cred = decryptDroidAuthPayload(options?.home || os.homedir());
|
|
542
|
+
const accessToken = cred?.access_token;
|
|
543
|
+
if (typeof accessToken !== 'string' || !accessToken) {
|
|
544
|
+
return { snapshot: null, error: null };
|
|
545
|
+
}
|
|
546
|
+
const exp = decodeJwtPayload(accessToken)?.exp;
|
|
547
|
+
if (typeof exp === 'number' && Date.now() / 1000 >= exp) {
|
|
548
|
+
return { snapshot: null, error: null };
|
|
549
|
+
}
|
|
550
|
+
const response = await fetch(DROID_USAGE_URL, {
|
|
551
|
+
method: 'GET',
|
|
552
|
+
headers: {
|
|
553
|
+
Authorization: `Bearer ${accessToken}`,
|
|
554
|
+
Accept: 'application/json',
|
|
555
|
+
},
|
|
556
|
+
signal: AbortSignal.timeout(5000),
|
|
557
|
+
});
|
|
558
|
+
// 401 => revoked/expired token; render nothing rather than a misleading
|
|
559
|
+
// empty bar.
|
|
560
|
+
if (!response.ok) {
|
|
561
|
+
return { snapshot: null, error: null };
|
|
562
|
+
}
|
|
563
|
+
const data = await response.json();
|
|
564
|
+
const windows = normalizeDroidWindows(data);
|
|
565
|
+
if (windows.length === 0) {
|
|
566
|
+
return { snapshot: null, error: null };
|
|
567
|
+
}
|
|
568
|
+
return {
|
|
569
|
+
snapshot: {
|
|
570
|
+
source: 'live',
|
|
571
|
+
sourceLabel: 'live account data',
|
|
572
|
+
capturedAt: new Date(),
|
|
573
|
+
windows,
|
|
574
|
+
},
|
|
575
|
+
error: null,
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
return { snapshot: null, error: null };
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Normalize the Factory billing-limits payload into the common UsageWindow
|
|
584
|
+
* shape. Orgs on the legacy (non token-rate-limit) billing model have no
|
|
585
|
+
* meaningful windows, so they render nothing — mirrors droid's own gate on
|
|
586
|
+
* `usesTokenRateLimitsBilling` before it reads `limits.standard`.
|
|
587
|
+
*/
|
|
588
|
+
export function normalizeDroidWindows(data) {
|
|
589
|
+
if (data.usesTokenRateLimitsBilling !== true)
|
|
590
|
+
return [];
|
|
591
|
+
const standard = data.limits?.standard;
|
|
592
|
+
if (!standard)
|
|
593
|
+
return [];
|
|
594
|
+
const windows = [
|
|
595
|
+
normalizeDroidWindow(standard.fiveHour, 'session', 'Current session', 'S'),
|
|
596
|
+
normalizeDroidWindow(standard.weekly, 'week', 'Current week', 'W'),
|
|
597
|
+
normalizeDroidWindow(standard.monthly, 'month', 'Current month', 'M'),
|
|
598
|
+
];
|
|
599
|
+
return windows.filter((window) => window !== null);
|
|
600
|
+
}
|
|
601
|
+
/** Normalize a single Droid billing-limits window. */
|
|
602
|
+
function normalizeDroidWindow(window, key, label, shortLabel) {
|
|
603
|
+
const usedPercent = normalizePercent(window?.usedPercent);
|
|
604
|
+
if (usedPercent === null)
|
|
605
|
+
return null;
|
|
606
|
+
return {
|
|
607
|
+
key,
|
|
608
|
+
label,
|
|
609
|
+
shortLabel,
|
|
610
|
+
usedPercent,
|
|
611
|
+
resetsAt: parseDateValue(window?.windowEnd),
|
|
612
|
+
windowMinutes: inferWindowMinutes(key),
|
|
613
|
+
};
|
|
614
|
+
}
|
|
516
615
|
/** Collect Codex JSONL session files sorted newest-first. */
|
|
517
616
|
function collectCodexSessionFiles(home) {
|
|
518
617
|
const base = home || os.homedir();
|
|
@@ -609,19 +708,16 @@ function normalizeClaudeWindow(window, key, label, shortLabel) {
|
|
|
609
708
|
windowMinutes: inferWindowMinutes(key),
|
|
610
709
|
};
|
|
611
710
|
}
|
|
612
|
-
/**
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
711
|
+
/**
|
|
712
|
+
* Parse a wrapped Claude OAuth payload — the `{ claudeAiOauth, organizationUuid }`
|
|
713
|
+
* shape written by BOTH the macOS Keychain item and the Linux `.credentials.json`
|
|
714
|
+
* file — into our credential struct. Returns null when there is no usable access
|
|
715
|
+
* token. Never throws (malformed JSON => null).
|
|
716
|
+
*/
|
|
717
|
+
function parseClaudeOauthPayload(raw) {
|
|
618
718
|
try {
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
const payload = JSON.parse(stdout.trim());
|
|
622
|
-
if (typeof payload?.claudeAiOauth?.accessToken !== 'string')
|
|
623
|
-
return null;
|
|
624
|
-
if (!payload.claudeAiOauth) {
|
|
719
|
+
const payload = JSON.parse(raw.trim());
|
|
720
|
+
if (!payload?.claudeAiOauth || typeof payload.claudeAiOauth.accessToken !== 'string') {
|
|
625
721
|
return null;
|
|
626
722
|
}
|
|
627
723
|
return {
|
|
@@ -633,6 +729,48 @@ export async function loadClaudeOauth(home) {
|
|
|
633
729
|
return null;
|
|
634
730
|
}
|
|
635
731
|
}
|
|
732
|
+
/**
|
|
733
|
+
* Load a version home's Claude OAuth credential from the two stores Claude Code
|
|
734
|
+
* uses, tried in order:
|
|
735
|
+
*
|
|
736
|
+
* 1. The OS keychain (`getKeychainToken`). Canonical on macOS — Claude Code
|
|
737
|
+
* writes the token to the login keychain and we read it via `/usr/bin/security`.
|
|
738
|
+
* 2. `<home>/.claude/.credentials.json`. On a headless Linux box (the
|
|
739
|
+
* `agents view --host <linux>` case) there is no reachable Secret Service, so
|
|
740
|
+
* the Claude CLI stores its OAuth token in this plaintext file instead. The
|
|
741
|
+
* keychain read above finds nothing on that platform, so we fall back to the
|
|
742
|
+
* file. Same wrapped `{ claudeAiOauth }` shape, so one parser handles both.
|
|
743
|
+
* Mirrors `readClaudeCredentialsBlob` (cloud/rush.ts), the proven pattern.
|
|
744
|
+
*
|
|
745
|
+
* Without step 2 the live usage fetch got no token on Linux, so `agents view`
|
|
746
|
+
* (run remotely over SSH by `--host`) rendered no usage bars even though the
|
|
747
|
+
* account + plan — read from the plaintext `.claude.json` — showed fine.
|
|
748
|
+
*/
|
|
749
|
+
export async function loadClaudeOauth(home) {
|
|
750
|
+
// The OS keychain/keyring step is macOS/Linux-only. Windows skips straight
|
|
751
|
+
// to the .credentials.json fallback — the Claude CLI has no keychain
|
|
752
|
+
// integration there and stores its OAuth token in that file too.
|
|
753
|
+
if (process.platform === 'darwin' || process.platform === 'linux') {
|
|
754
|
+
try {
|
|
755
|
+
const fromKeychain = parseClaudeOauthPayload(getKeychainToken(getClaudeKeychainService(home)));
|
|
756
|
+
if (fromKeychain)
|
|
757
|
+
return fromKeychain;
|
|
758
|
+
}
|
|
759
|
+
catch {
|
|
760
|
+
// No keychain item, or no reachable keyring (headless Linux) — fall through.
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
const credsPath = path.join(home ?? os.homedir(), '.claude', '.credentials.json');
|
|
764
|
+
try {
|
|
765
|
+
if (fs.existsSync(credsPath)) {
|
|
766
|
+
return parseClaudeOauthPayload(fs.readFileSync(credsPath, 'utf-8'));
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
catch {
|
|
770
|
+
// Unreadable file — treat as not signed in.
|
|
771
|
+
}
|
|
772
|
+
return null;
|
|
773
|
+
}
|
|
636
774
|
/**
|
|
637
775
|
* Save Claude OAuth credentials to the system keychain/keyring.
|
|
638
776
|
* Reads the existing payload, merges the new OAuth fields, and writes back.
|
|
@@ -895,6 +1033,8 @@ function inferWindowMinutes(key) {
|
|
|
895
1033
|
case 'week':
|
|
896
1034
|
case 'sonnet_week':
|
|
897
1035
|
return 10080;
|
|
1036
|
+
case 'month':
|
|
1037
|
+
return 43200;
|
|
898
1038
|
}
|
|
899
1039
|
}
|
|
900
1040
|
/** Parse a date value from a number (epoch seconds or ms) or ISO string. */
|
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.64",
|
|
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",
|