@phnx-labs/agents-cli 1.14.4 → 1.14.6
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 +19 -0
- package/dist/lib/browser/chrome.d.ts +2 -2
- package/dist/lib/browser/chrome.js +15 -3
- package/dist/lib/browser/drivers/local.js +1 -1
- package/dist/lib/browser/drivers/ssh.js +14 -5
- package/dist/lib/browser/service.js +24 -3
- package/dist/lib/browser/types.d.ts +3 -1
- package/dist/lib/usage.js +83 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.14.6
|
|
4
|
+
|
|
5
|
+
**Fix: OAuth token refresh now persists to Keychain**
|
|
6
|
+
|
|
7
|
+
- Fixed bug where refreshed Claude OAuth tokens were used but never saved back to macOS Keychain
|
|
8
|
+
- Previously, agents-cli would refresh expired tokens on each run but discard them, eventually exhausting the refresh token
|
|
9
|
+
- Now refreshed `accessToken`, `refreshToken`, and `expiresAt` are written back to Keychain after successful refresh
|
|
10
|
+
- Accounts will stay healthy across runs without requiring re-login
|
|
11
|
+
|
|
12
|
+
## 1.14.5
|
|
13
|
+
|
|
14
|
+
**Browser: custom binary and Electron app support**
|
|
15
|
+
|
|
16
|
+
- Added `binary` field to browser profiles for specifying custom executable paths (e.g., Electron apps like Rush)
|
|
17
|
+
- Added `electron` field to browser profiles — when true, uses existing windows instead of creating new ones (Electron doesn't support `Target.createTarget`)
|
|
18
|
+
- New `custom` browser type that requires a binary path
|
|
19
|
+
- Works with both local and SSH-based browser connections
|
|
20
|
+
- Example profile for Rush: `agents browser profiles edit rush --browser custom --binary "/Applications/Rush.app/Contents/MacOS/Rush" --electron`
|
|
21
|
+
|
|
3
22
|
## Unreleased
|
|
4
23
|
|
|
5
24
|
**System repo moved to `~/.agents-system`; `~/.agents` is now free for user-owned repos**
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { ChromeOptions } from './types.js';
|
|
2
2
|
import type { BrowserType } from './types.js';
|
|
3
|
-
export declare function findBrowserPath(browserType: BrowserType): string;
|
|
3
|
+
export declare function findBrowserPath(browserType: BrowserType, customBinary?: string): string;
|
|
4
4
|
export interface LaunchResult {
|
|
5
5
|
pid: number;
|
|
6
6
|
port: number;
|
|
7
7
|
wsUrl: string;
|
|
8
8
|
}
|
|
9
|
-
export declare function launchBrowser(profileName: string, browserType: BrowserType, port: number, options?: ChromeOptions, secrets?: string): Promise<LaunchResult>;
|
|
9
|
+
export declare function launchBrowser(profileName: string, browserType: BrowserType, port: number, options?: ChromeOptions, secrets?: string, customBinary?: string): Promise<LaunchResult>;
|
|
10
10
|
export declare function attachToChrome(port: number): Promise<string>;
|
|
11
11
|
export declare function killChrome(pid: number): void;
|
|
12
12
|
export declare function getRunningChromeInfo(profileName: string): {
|
|
@@ -15,6 +15,7 @@ const BROWSER_PATHS = {
|
|
|
15
15
|
chromium: ['/Applications/Chromium.app/Contents/MacOS/Chromium'],
|
|
16
16
|
brave: ['/Applications/Brave Browser.app/Contents/MacOS/Brave Browser'],
|
|
17
17
|
edge: ['/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge'],
|
|
18
|
+
custom: [],
|
|
18
19
|
},
|
|
19
20
|
linux: {
|
|
20
21
|
chrome: ['/usr/bin/google-chrome', '/usr/bin/google-chrome-stable'],
|
|
@@ -22,6 +23,7 @@ const BROWSER_PATHS = {
|
|
|
22
23
|
chromium: ['/usr/bin/chromium', '/usr/bin/chromium-browser', '/snap/bin/chromium'],
|
|
23
24
|
brave: ['/usr/bin/brave-browser', '/usr/bin/brave'],
|
|
24
25
|
edge: ['/usr/bin/microsoft-edge'],
|
|
26
|
+
custom: [],
|
|
25
27
|
},
|
|
26
28
|
win32: {
|
|
27
29
|
chrome: [
|
|
@@ -36,9 +38,19 @@ const BROWSER_PATHS = {
|
|
|
36
38
|
edge: [
|
|
37
39
|
'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
38
40
|
],
|
|
41
|
+
custom: [],
|
|
39
42
|
},
|
|
40
43
|
};
|
|
41
|
-
export function findBrowserPath(browserType) {
|
|
44
|
+
export function findBrowserPath(browserType, customBinary) {
|
|
45
|
+
if (customBinary) {
|
|
46
|
+
if (!fs.existsSync(customBinary)) {
|
|
47
|
+
throw new Error(`Custom binary not found: ${customBinary}`);
|
|
48
|
+
}
|
|
49
|
+
return customBinary;
|
|
50
|
+
}
|
|
51
|
+
if (browserType === 'custom') {
|
|
52
|
+
throw new Error('browser: custom requires a binary path in the profile');
|
|
53
|
+
}
|
|
42
54
|
const platform = os.platform();
|
|
43
55
|
const platformPaths = BROWSER_PATHS[platform];
|
|
44
56
|
if (!platformPaths) {
|
|
@@ -52,8 +64,8 @@ export function findBrowserPath(browserType) {
|
|
|
52
64
|
}
|
|
53
65
|
throw new Error(`Browser "${browserType}" not found. Install it first.`);
|
|
54
66
|
}
|
|
55
|
-
export async function launchBrowser(profileName, browserType, port, options = {}, secrets) {
|
|
56
|
-
const browserPath = findBrowserPath(browserType);
|
|
67
|
+
export async function launchBrowser(profileName, browserType, port, options = {}, secrets, customBinary) {
|
|
68
|
+
const browserPath = findBrowserPath(browserType, customBinary);
|
|
57
69
|
const runtimeDir = getProfileRuntimeDir(profileName);
|
|
58
70
|
const userDataDir = path.join(runtimeDir, 'chrome-data');
|
|
59
71
|
fs.mkdirSync(userDataDir, { recursive: true });
|
|
@@ -14,7 +14,7 @@ export async function connectLocal(endpoint, profile) {
|
|
|
14
14
|
}
|
|
15
15
|
catch {
|
|
16
16
|
const newPort = allocatePort();
|
|
17
|
-
const { pid, wsUrl } = await launchBrowser(profile.name, profile.browser, newPort, profile.chrome, profile.secrets);
|
|
17
|
+
const { pid, wsUrl } = await launchBrowser(profile.name, profile.browser, newPort, profile.chrome, profile.secrets, profile.binary);
|
|
18
18
|
const cdp = new CDPClient();
|
|
19
19
|
await cdp.connect(wsUrl);
|
|
20
20
|
return { cdp, port: newPort, pid };
|
|
@@ -12,7 +12,7 @@ export async function connectSSH(endpoint, profile) {
|
|
|
12
12
|
const remotePort = parseInt(url.searchParams.get('port') || '9222', 10);
|
|
13
13
|
const localPort = allocatePort();
|
|
14
14
|
try {
|
|
15
|
-
await ensureRemoteBrowser(user, host, profile.browser, remotePort);
|
|
15
|
+
await ensureRemoteBrowser(user, host, profile.browser, remotePort, profile.binary);
|
|
16
16
|
}
|
|
17
17
|
catch {
|
|
18
18
|
// Browser may already be running, continue
|
|
@@ -96,7 +96,7 @@ function tryConnect(port) {
|
|
|
96
96
|
socket.on('error', reject);
|
|
97
97
|
});
|
|
98
98
|
}
|
|
99
|
-
async function ensureRemoteBrowser(user, host, browserType, port) {
|
|
99
|
+
async function ensureRemoteBrowser(user, host, browserType, port, customBinary) {
|
|
100
100
|
const browserPaths = {
|
|
101
101
|
chrome: '/Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome',
|
|
102
102
|
comet: '/Applications/Comet.app/Contents/MacOS/Comet',
|
|
@@ -104,9 +104,18 @@ async function ensureRemoteBrowser(user, host, browserType, port) {
|
|
|
104
104
|
brave: '/Applications/Brave\\ Browser.app/Contents/MacOS/Brave\\ Browser',
|
|
105
105
|
edge: '/Applications/Microsoft\\ Edge.app/Contents/MacOS/Microsoft\\ Edge',
|
|
106
106
|
};
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
|
|
107
|
+
let browserPath;
|
|
108
|
+
if (customBinary) {
|
|
109
|
+
browserPath = customBinary.replace(/ /g, '\\ ');
|
|
110
|
+
}
|
|
111
|
+
else if (browserType === 'custom') {
|
|
112
|
+
throw new Error('browser: custom requires a binary path in the profile');
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
browserPath = browserPaths[browserType];
|
|
116
|
+
if (!browserPath) {
|
|
117
|
+
throw new Error(`Unknown browser type: ${browserType}`);
|
|
118
|
+
}
|
|
110
119
|
}
|
|
111
120
|
const remoteCmd = `${browserPath} --remote-debugging-port=${port} '--remote-allow-origins=*' --disable-background-timer-throttling --user-data-dir=/tmp/agents-browser-${port} </dev/null >/dev/null 2>&1 &`;
|
|
112
121
|
return new Promise((resolve, reject) => {
|
|
@@ -28,7 +28,7 @@ export class BrowserService {
|
|
|
28
28
|
const task = conn.tasks.get(finalTaskId);
|
|
29
29
|
return { task: finalTaskId, windowTargetId: task.windowTargetId };
|
|
30
30
|
}
|
|
31
|
-
const { windowTargetId } = await this.createTaskWindow(conn
|
|
31
|
+
const { windowTargetId } = await this.createTaskWindow(conn, finalTaskId);
|
|
32
32
|
const task = {
|
|
33
33
|
id: finalTaskId,
|
|
34
34
|
profile: profileName,
|
|
@@ -88,6 +88,19 @@ export class BrowserService {
|
|
|
88
88
|
}
|
|
89
89
|
async navigate(taskId, url, profileName) {
|
|
90
90
|
const { conn, task } = await this.findTask(taskId, profileName);
|
|
91
|
+
if (conn.electron) {
|
|
92
|
+
const tabId = task.windowTargetId || task.tabIds[0];
|
|
93
|
+
if (!tabId) {
|
|
94
|
+
throw new Error('No existing tab to navigate in Electron app');
|
|
95
|
+
}
|
|
96
|
+
const sessionId = await this.getSessionId(conn, tabId);
|
|
97
|
+
await conn.cdp.send('Page.navigate', { url }, sessionId);
|
|
98
|
+
if (!task.tabIds.includes(tabId)) {
|
|
99
|
+
task.tabIds.push(tabId);
|
|
100
|
+
}
|
|
101
|
+
await this.saveTaskState(task.profile, conn.tasks);
|
|
102
|
+
return { tabId, url };
|
|
103
|
+
}
|
|
91
104
|
const result = (await conn.cdp.send('Target.createTarget', {
|
|
92
105
|
url,
|
|
93
106
|
}));
|
|
@@ -255,6 +268,7 @@ export class BrowserService {
|
|
|
255
268
|
cdp,
|
|
256
269
|
port: existingInfo.port,
|
|
257
270
|
pid: existingInfo.pid,
|
|
271
|
+
electron: profile.electron,
|
|
258
272
|
tasks,
|
|
259
273
|
sessionCache: new Map(),
|
|
260
274
|
};
|
|
@@ -280,6 +294,7 @@ export class BrowserService {
|
|
|
280
294
|
cdp: conn.cdp,
|
|
281
295
|
port: conn.port,
|
|
282
296
|
pid: conn.pid,
|
|
297
|
+
electron: profile.electron,
|
|
283
298
|
tasks: conn.pid === 0 ? this.loadTaskState(profile.name) : new Map(),
|
|
284
299
|
sessionCache: new Map(),
|
|
285
300
|
};
|
|
@@ -291,6 +306,7 @@ export class BrowserService {
|
|
|
291
306
|
cdp: conn.cdp,
|
|
292
307
|
port: conn.port,
|
|
293
308
|
pid: conn.pid,
|
|
309
|
+
electron: profile.electron,
|
|
294
310
|
tasks: new Map(),
|
|
295
311
|
sessionCache: new Map(),
|
|
296
312
|
};
|
|
@@ -300,8 +316,13 @@ export class BrowserService {
|
|
|
300
316
|
async enableDomains(cdp) {
|
|
301
317
|
await cdp.send('Target.setDiscoverTargets', { discover: true });
|
|
302
318
|
}
|
|
303
|
-
async createTaskWindow(
|
|
304
|
-
|
|
319
|
+
async createTaskWindow(conn, _taskId) {
|
|
320
|
+
if (conn.electron) {
|
|
321
|
+
const { targetInfos } = (await conn.cdp.send('Target.getTargets'));
|
|
322
|
+
const pageTarget = targetInfos.find((t) => t.type === 'page');
|
|
323
|
+
return { windowTargetId: pageTarget?.targetId };
|
|
324
|
+
}
|
|
325
|
+
const result = (await conn.cdp.send('Target.createTarget', {
|
|
305
326
|
url: 'about:blank',
|
|
306
327
|
newWindow: true,
|
|
307
328
|
}));
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
export type BrowserType = 'chrome' | 'comet' | 'chromium' | 'brave' | 'edge';
|
|
1
|
+
export type BrowserType = 'chrome' | 'comet' | 'chromium' | 'brave' | 'edge' | 'custom';
|
|
2
2
|
export interface BrowserProfile {
|
|
3
3
|
name: string;
|
|
4
4
|
description?: string;
|
|
5
5
|
browser: BrowserType;
|
|
6
|
+
binary?: string;
|
|
7
|
+
electron?: boolean;
|
|
6
8
|
endpoints: string[];
|
|
7
9
|
chrome?: ChromeOptions;
|
|
8
10
|
secrets?: string;
|
package/dist/lib/usage.js
CHANGED
|
@@ -211,7 +211,7 @@ async function getClaudeUsageInfo(options) {
|
|
|
211
211
|
if (!isClaudeUsageOrgMatch(requestedOrgId, liveOrgId)) {
|
|
212
212
|
return { snapshot: null, error: null };
|
|
213
213
|
}
|
|
214
|
-
const accessToken = await getClaudeAccessToken(oauth);
|
|
214
|
+
const accessToken = await getClaudeAccessToken(oauth, options?.home);
|
|
215
215
|
if (!accessToken) {
|
|
216
216
|
return { snapshot: null, error: null };
|
|
217
217
|
}
|
|
@@ -343,9 +343,14 @@ function normalizeClaudeWindow(window, key, label, shortLabel) {
|
|
|
343
343
|
windowMinutes: inferWindowMinutes(key),
|
|
344
344
|
};
|
|
345
345
|
}
|
|
346
|
+
let warnedNonDarwin = false;
|
|
346
347
|
/** Load Claude OAuth credentials from the macOS Keychain. */
|
|
347
348
|
export async function loadClaudeOauth(home) {
|
|
348
349
|
if (process.platform !== 'darwin') {
|
|
350
|
+
if (!warnedNonDarwin && process.stderr.isTTY) {
|
|
351
|
+
process.stderr.write('[agents] Usage tracking requires macOS Keychain. Skipped on this platform.\n');
|
|
352
|
+
warnedNonDarwin = true;
|
|
353
|
+
}
|
|
349
354
|
return null;
|
|
350
355
|
}
|
|
351
356
|
try {
|
|
@@ -374,6 +379,74 @@ export async function loadClaudeOauth(home) {
|
|
|
374
379
|
return null;
|
|
375
380
|
}
|
|
376
381
|
}
|
|
382
|
+
/**
|
|
383
|
+
* Save Claude OAuth credentials to the macOS Keychain.
|
|
384
|
+
* Reads the existing payload, merges the new OAuth fields, and writes back.
|
|
385
|
+
*/
|
|
386
|
+
async function saveClaudeOauth(home, credentials) {
|
|
387
|
+
if (process.platform !== 'darwin') {
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
try {
|
|
391
|
+
const account = os.userInfo().username;
|
|
392
|
+
const service = getClaudeKeychainService(home);
|
|
393
|
+
// Read existing payload to preserve other fields
|
|
394
|
+
let existingPayload = {};
|
|
395
|
+
try {
|
|
396
|
+
const { stdout } = await execFileAsync('security', [
|
|
397
|
+
'find-generic-password',
|
|
398
|
+
'-a',
|
|
399
|
+
account,
|
|
400
|
+
'-s',
|
|
401
|
+
service,
|
|
402
|
+
'-w',
|
|
403
|
+
]);
|
|
404
|
+
existingPayload = JSON.parse(stdout.trim());
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
// No existing entry, start fresh
|
|
408
|
+
}
|
|
409
|
+
// Merge new credentials into existing payload
|
|
410
|
+
const newPayload = {
|
|
411
|
+
...existingPayload,
|
|
412
|
+
claudeAiOauth: {
|
|
413
|
+
...existingPayload.claudeAiOauth,
|
|
414
|
+
accessToken: credentials.accessToken,
|
|
415
|
+
refreshToken: credentials.refreshToken,
|
|
416
|
+
expiresAt: credentials.expiresAt,
|
|
417
|
+
scopes: credentials.scopes ?? existingPayload.claudeAiOauth?.scopes,
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
const payloadJson = JSON.stringify(newPayload);
|
|
421
|
+
// Delete existing entry first (security add-generic-password -U can fail)
|
|
422
|
+
try {
|
|
423
|
+
await execFileAsync('security', [
|
|
424
|
+
'delete-generic-password',
|
|
425
|
+
'-a',
|
|
426
|
+
account,
|
|
427
|
+
'-s',
|
|
428
|
+
service,
|
|
429
|
+
]);
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
// Entry might not exist, ignore
|
|
433
|
+
}
|
|
434
|
+
// Add updated entry
|
|
435
|
+
await execFileAsync('security', [
|
|
436
|
+
'add-generic-password',
|
|
437
|
+
'-a',
|
|
438
|
+
account,
|
|
439
|
+
'-s',
|
|
440
|
+
service,
|
|
441
|
+
'-w',
|
|
442
|
+
payloadJson,
|
|
443
|
+
]);
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
377
450
|
/**
|
|
378
451
|
* Derive the Keychain service name for a Claude home directory.
|
|
379
452
|
* Managed (non-default) homes get a hash suffix for isolation.
|
|
@@ -493,8 +566,8 @@ function isCachedUsageWindowFresh(window, capturedAt, now) {
|
|
|
493
566
|
}
|
|
494
567
|
return true;
|
|
495
568
|
}
|
|
496
|
-
/** Obtain a valid access token, refreshing if expired. */
|
|
497
|
-
async function getClaudeAccessToken(oauth) {
|
|
569
|
+
/** Obtain a valid access token, refreshing if expired. Saves refreshed tokens to Keychain. */
|
|
570
|
+
async function getClaudeAccessToken(oauth, home) {
|
|
498
571
|
const accessToken = oauth.accessToken?.trim();
|
|
499
572
|
if (!accessToken) {
|
|
500
573
|
return null;
|
|
@@ -507,7 +580,12 @@ async function getClaudeAccessToken(oauth) {
|
|
|
507
580
|
return null;
|
|
508
581
|
}
|
|
509
582
|
const refreshed = await refreshClaudeToken(oauth);
|
|
510
|
-
|
|
583
|
+
if (!refreshed?.accessToken) {
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
// Persist refreshed credentials to Keychain so they survive across runs
|
|
587
|
+
await saveClaudeOauth(home, refreshed);
|
|
588
|
+
return refreshed.accessToken.trim();
|
|
511
589
|
}
|
|
512
590
|
/** Refresh an expired Claude OAuth access token using the refresh token. */
|
|
513
591
|
async function refreshClaudeToken(oauth) {
|
|
@@ -547,7 +625,7 @@ export async function isClaudeAuthValid(home) {
|
|
|
547
625
|
const oauth = await loadClaudeOauth(home);
|
|
548
626
|
if (!oauth)
|
|
549
627
|
return false;
|
|
550
|
-
const token = await getClaudeAccessToken(oauth);
|
|
628
|
+
const token = await getClaudeAccessToken(oauth, home);
|
|
551
629
|
return token !== null;
|
|
552
630
|
}
|
|
553
631
|
/** Build a User-Agent string for Claude API requests. */
|
package/package.json
CHANGED