@phnx-labs/agents-cli 1.20.50 → 1.20.52
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 +40 -0
- package/dist/commands/browser-picker.js +1 -18
- package/dist/commands/browser.js +215 -7
- package/dist/commands/cloud.js +7 -25
- package/dist/commands/computer.d.ts +1 -0
- package/dist/commands/computer.js +129 -8
- package/dist/commands/events.d.ts +1 -1
- package/dist/commands/events.js +2 -3
- package/dist/commands/exec.js +66 -8
- package/dist/commands/factory.js +9 -4
- package/dist/commands/feed.d.ts +9 -0
- package/dist/commands/feed.js +69 -0
- package/dist/commands/inspect.js +1 -11
- package/dist/commands/logs.d.ts +5 -1
- package/dist/commands/logs.js +248 -3
- package/dist/commands/mcp.js +9 -6
- package/dist/commands/message.js +1 -4
- package/dist/commands/profiles.js +1 -18
- package/dist/commands/repo.js +33 -14
- package/dist/commands/resource-view.d.ts +1 -0
- package/dist/commands/resource-view.js +5 -17
- package/dist/commands/secrets.d.ts +23 -0
- package/dist/commands/secrets.js +174 -70
- package/dist/commands/sessions-picker.js +1 -18
- package/dist/commands/sessions.js +6 -8
- package/dist/commands/teams-picker.js +1 -32
- package/dist/commands/teams.js +5 -27
- package/dist/commands/tmux.js +1 -3
- package/dist/commands/view.js +1 -9
- package/dist/commands/worktree.js +1 -4
- package/dist/index.js +6 -2
- package/dist/lib/agents.d.ts +0 -4
- package/dist/lib/agents.js +20 -33
- package/dist/lib/auto-dispatch-linear.d.ts +18 -0
- package/dist/lib/auto-dispatch-linear.js +107 -0
- package/dist/lib/auto-dispatch-provider.d.ts +10 -0
- package/dist/lib/auto-dispatch-provider.js +25 -0
- package/dist/lib/auto-dispatch.d.ts +87 -0
- package/dist/lib/auto-dispatch.js +142 -0
- package/dist/lib/browser/cdp.js +11 -2
- package/dist/lib/browser/drivers/ssh.d.ts +28 -10
- package/dist/lib/browser/drivers/ssh.js +57 -18
- package/dist/lib/browser/login-detection.d.ts +94 -0
- package/dist/lib/browser/login-detection.js +274 -0
- package/dist/lib/browser/profiles.d.ts +17 -8
- package/dist/lib/browser/profiles.js +27 -8
- package/dist/lib/browser/refs.js +1 -5
- package/dist/lib/browser/secret-ref.d.ts +10 -0
- package/dist/lib/browser/secret-ref.js +14 -0
- package/dist/lib/browser/service.js +14 -12
- package/dist/lib/cli-resources.d.ts +0 -2
- package/dist/lib/cli-resources.js +30 -13
- package/dist/lib/cloud/rush.d.ts +15 -24
- package/dist/lib/cloud/rush.js +7 -32
- package/dist/lib/crabbox/cli.js +4 -1
- package/dist/lib/crabbox/lease.d.ts +6 -0
- package/dist/lib/crabbox/lease.js +40 -10
- package/dist/lib/crabbox/runtimes.d.ts +38 -1
- package/dist/lib/crabbox/runtimes.js +98 -5
- package/dist/lib/daemon.d.ts +12 -9
- package/dist/lib/daemon.js +73 -17
- package/dist/lib/events.d.ts +31 -5
- package/dist/lib/events.js +288 -101
- package/dist/lib/exec.js +32 -14
- package/dist/lib/feed.d.ts +56 -0
- package/dist/lib/feed.js +251 -0
- package/dist/lib/format.d.ts +38 -0
- package/dist/lib/format.js +108 -0
- package/dist/lib/git.d.ts +21 -0
- package/dist/lib/git.js +92 -0
- package/dist/lib/hooks/cache.d.ts +9 -2
- package/dist/lib/hooks/cache.js +220 -8
- package/dist/lib/hooks.js +24 -10
- package/dist/lib/hosts/passthrough.js +1 -0
- package/dist/lib/platform/exec.d.ts +4 -1
- package/dist/lib/platform/exec.js +8 -2
- package/dist/lib/resources.d.ts +0 -8
- package/dist/lib/resources.js +0 -10
- package/dist/lib/rotate.js +2 -0
- package/dist/lib/runner.js +10 -2
- 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.d.ts +21 -0
- package/dist/lib/secrets/agent.js +63 -1
- package/dist/lib/secrets/bundles.d.ts +33 -1
- package/dist/lib/secrets/bundles.js +38 -8
- package/dist/lib/secrets/icloud-import.d.ts +70 -0
- package/dist/lib/secrets/icloud-import.js +173 -0
- package/dist/lib/secrets/index.d.ts +36 -0
- package/dist/lib/secrets/index.js +99 -9
- package/dist/lib/secrets/remote.js +1 -1
- package/dist/lib/secrets/sync.js +1 -1
- package/dist/lib/session/active.d.ts +11 -1
- package/dist/lib/session/active.js +3 -0
- package/dist/lib/session/db.d.ts +1 -4
- package/dist/lib/session/db.js +20 -25
- package/dist/lib/session/discover.d.ts +2 -2
- package/dist/lib/session/discover.js +60 -48
- package/dist/lib/session/parse.js +35 -34
- package/dist/lib/session/render.d.ts +7 -3
- package/dist/lib/session/render.js +15 -9
- package/dist/lib/session/state.d.ts +55 -0
- package/dist/lib/session/state.js +100 -11
- package/dist/lib/session/types.d.ts +9 -0
- package/dist/lib/shims.d.ts +9 -3
- package/dist/lib/shims.js +44 -8
- package/dist/lib/ssh-tunnel.d.ts +33 -2
- package/dist/lib/ssh-tunnel.js +94 -7
- package/dist/lib/staleness/types.d.ts +0 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +25 -8
- package/dist/lib/teams/agents.js +6 -3
- package/dist/lib/types.d.ts +24 -1
- package/dist/lib/versions.d.ts +0 -26
- package/dist/lib/versions.js +2 -145
- package/dist/lib/warn-unpushed.d.ts +40 -0
- package/dist/lib/warn-unpushed.js +128 -0
- package/dist/lib/whats-new.d.ts +5 -3
- package/dist/lib/whats-new.js +25 -5
- package/package.json +3 -1
- package/dist/lib/resources/index.d.ts +0 -53
- package/dist/lib/resources/index.js +0 -76
package/dist/lib/browser/refs.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { truncate } from '../format.js';
|
|
1
2
|
/** Snapshot the stable descriptor for every ref in a freshly-built node map. */
|
|
2
3
|
export function describeRefs(nodeMap) {
|
|
3
4
|
return Array.from(nodeMap.values()).map((n) => ({
|
|
@@ -175,11 +176,6 @@ export async function getRefs(cdp, sessionId, opts = {}) {
|
|
|
175
176
|
}
|
|
176
177
|
return { refs: lines.join('\n'), nodeMap, opts: { interactive, limit } };
|
|
177
178
|
}
|
|
178
|
-
function truncate(s, max) {
|
|
179
|
-
if (s.length <= max)
|
|
180
|
-
return s;
|
|
181
|
-
return s.slice(0, max - 1) + '…';
|
|
182
|
-
}
|
|
183
179
|
export async function resolveRefToCoords(cdp, sessionId, nodeMap, ref) {
|
|
184
180
|
const node = nodeMap.get(ref);
|
|
185
181
|
if (!node)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a `<bundle>/<KEY>` reference (optionally `secret:<bundle>/<KEY>`) used by
|
|
3
|
+
* `agents browser type --secret` to feed a credential from an `agents secrets`
|
|
4
|
+
* bundle into a page WITHOUT the value ever crossing stdout or the transcript.
|
|
5
|
+
* Returns null on a malformed ref.
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseSecretRef(ref: string): {
|
|
8
|
+
bundle: string;
|
|
9
|
+
key: string;
|
|
10
|
+
} | null;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a `<bundle>/<KEY>` reference (optionally `secret:<bundle>/<KEY>`) used by
|
|
3
|
+
* `agents browser type --secret` to feed a credential from an `agents secrets`
|
|
4
|
+
* bundle into a page WITHOUT the value ever crossing stdout or the transcript.
|
|
5
|
+
* Returns null on a malformed ref.
|
|
6
|
+
*/
|
|
7
|
+
export function parseSecretRef(ref) {
|
|
8
|
+
const body = ref.startsWith('secret:') ? ref.slice('secret:'.length) : ref;
|
|
9
|
+
const slash = body.indexOf('/');
|
|
10
|
+
// Need a non-empty bundle before the slash and a non-empty key after it.
|
|
11
|
+
if (slash <= 0 || slash >= body.length - 1)
|
|
12
|
+
return null;
|
|
13
|
+
return { bundle: body.slice(0, slash), key: body.slice(slash + 1) };
|
|
14
|
+
}
|
|
@@ -8,7 +8,7 @@ import { getProfile, getProfileRuntimeDir, getBrowserRuntimeDir, listProfiles, e
|
|
|
8
8
|
import { killChrome, getRunningChromeInfo, launchBrowser, allocatePort } from './chrome.js';
|
|
9
9
|
import { connectLocal } from './drivers/local.js';
|
|
10
10
|
import { connectSSH, shellQuote } from './drivers/ssh.js';
|
|
11
|
-
import { clearProfileRuntime } from './runtime-state.js';
|
|
11
|
+
import { clearProfileRuntime, listProfileCacheDirs, readProfileRuntimeMeta, isProcessAlive } from './runtime-state.js';
|
|
12
12
|
import { resolveDomainSkill } from './domain-skills.js';
|
|
13
13
|
import { generateTaskId, generateShortId, generateTaskName, } from './types.js';
|
|
14
14
|
import { getRefs, resolveRefToCoords, describeRefs, healRef } from './refs.js';
|
|
@@ -415,6 +415,7 @@ export class BrowserService {
|
|
|
415
415
|
killChrome(conn.pid);
|
|
416
416
|
conn.cleanup?.();
|
|
417
417
|
this.connections.delete(profileName);
|
|
418
|
+
clearProfileRuntime(profileName);
|
|
418
419
|
}
|
|
419
420
|
return { ok: true, profile: profileName };
|
|
420
421
|
}
|
|
@@ -442,17 +443,18 @@ export class BrowserService {
|
|
|
442
443
|
killChrome(conn.pid);
|
|
443
444
|
conn.cleanup?.();
|
|
444
445
|
this.connections.delete(key);
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
446
|
+
clearProfileRuntime(key);
|
|
447
|
+
}
|
|
448
|
+
// Kill stale processes and clean runtime dirs for every composite and
|
|
449
|
+
// fork entry belonging to this profile (including `.N` forks left by
|
|
450
|
+
// earlier daemon sessions that the connection loop above didn't cover).
|
|
451
|
+
for (const dir of listProfileCacheDirs(profileName)) {
|
|
452
|
+
const dirName = path.basename(dir);
|
|
453
|
+
const meta = readProfileRuntimeMeta(dirName);
|
|
454
|
+
if (meta?.pid && meta.pid !== 0 && isProcessAlive(meta.pid, meta.command)) {
|
|
455
|
+
killChrome(meta.pid);
|
|
456
|
+
}
|
|
457
|
+
clearProfileRuntime(dirName);
|
|
456
458
|
}
|
|
457
459
|
}
|
|
458
460
|
async navigate(taskId, url, profileName) {
|
|
@@ -133,5 +133,3 @@ export declare function listCliStatus(cwd?: string): {
|
|
|
133
133
|
statuses: CliStatus[];
|
|
134
134
|
errors: CliManifestError[];
|
|
135
135
|
};
|
|
136
|
-
/** Names of CLIs that are declared but not currently installed on the host. */
|
|
137
|
-
export declare function getMissingClis(cwd?: string): CliManifest[];
|
|
@@ -21,6 +21,7 @@ import * as path from 'path';
|
|
|
21
21
|
import { spawnSync } from 'child_process';
|
|
22
22
|
import * as yaml from 'yaml';
|
|
23
23
|
import { listResources, resolveResource } from './resources.js';
|
|
24
|
+
import { composeWin32CommandLine } from './platform/index.js';
|
|
24
25
|
// ─── Validation primitives ───────────────────────────────────────────────────
|
|
25
26
|
/** Token allowed inside `check:` strings — letters, digits, underscore, dot, slash, dash. */
|
|
26
27
|
const SAFE_CHECK_TOKEN = /^[a-zA-Z0-9_./-]+$/;
|
|
@@ -253,19 +254,27 @@ export function resolveCliManifest(name, cwd) {
|
|
|
253
254
|
// ─── Host detection ──────────────────────────────────────────────────────────
|
|
254
255
|
/**
|
|
255
256
|
* Return true if a command resolves on the current PATH. Uses POSIX `command -v`
|
|
256
|
-
* via spawn argv (no shell); results are cached for the
|
|
257
|
+
* (or `where` on Windows) via spawn argv (no shell); results are cached for the
|
|
258
|
+
* lifetime of the process.
|
|
257
259
|
*/
|
|
258
260
|
const cmdExistsCache = new Map();
|
|
259
261
|
export function hasCommand(cmd) {
|
|
260
262
|
if (cmdExistsCache.has(cmd))
|
|
261
263
|
return cmdExistsCache.get(cmd);
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
stdio: 'ignore'
|
|
267
|
-
}
|
|
268
|
-
|
|
264
|
+
let ok;
|
|
265
|
+
if (process.platform === 'win32') {
|
|
266
|
+
// `sh` only exists when Git Bash is installed; `where` is the native PATH
|
|
267
|
+
// probe (resolves .exe/.cmd/.bat via PATHEXT). Argv keeps `cmd` uninterpolated.
|
|
268
|
+
ok = spawnSync('where', [cmd], { stdio: 'ignore' }).status === 0;
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
// `command` is a shell builtin on most POSIX shells; invoking `sh -c 'command -v X'`
|
|
272
|
+
// with X as an *argument* (not interpolated) is the safe path. `cmd` may be passed
|
|
273
|
+
// by callers that haven't validated it, so we route via argv to neutralize metas.
|
|
274
|
+
ok = spawnSync('sh', ['-c', 'command -v "$1" >/dev/null 2>&1', '_', cmd], {
|
|
275
|
+
stdio: 'ignore',
|
|
276
|
+
}).status === 0;
|
|
277
|
+
}
|
|
269
278
|
cmdExistsCache.set(cmd, ok);
|
|
270
279
|
return ok;
|
|
271
280
|
}
|
|
@@ -280,7 +289,19 @@ export function isCliInstalled(manifest) {
|
|
|
280
289
|
return hasCommand(c.cmd);
|
|
281
290
|
}
|
|
282
291
|
const result = spawnSync(c.cmd, c.args, { stdio: 'ignore', timeout: 10_000 });
|
|
283
|
-
|
|
292
|
+
if (result.status === 0)
|
|
293
|
+
return true;
|
|
294
|
+
// On Windows the PATH entry point is often a `.cmd`/`.bat` shim (npm installs,
|
|
295
|
+
// script installers), which Node refuses to spawn without a shell (ENOENT /
|
|
296
|
+
// EINVAL). A failed *spawn* — not a failed run — gets one retry through the
|
|
297
|
+
// shell, as a single pre-composed line (DEP0190-safe). Check tokens are
|
|
298
|
+
// SAFE_CHECK_TOKEN-validated at parse time, so the line cannot inject.
|
|
299
|
+
if (process.platform === 'win32' && result.error) {
|
|
300
|
+
const line = composeWin32CommandLine(c.cmd, c.args);
|
|
301
|
+
const retry = spawnSync(line, { stdio: 'ignore', timeout: 10_000, shell: true });
|
|
302
|
+
return retry.status === 0;
|
|
303
|
+
}
|
|
304
|
+
return false;
|
|
284
305
|
}
|
|
285
306
|
// ─── Method selection ────────────────────────────────────────────────────────
|
|
286
307
|
/**
|
|
@@ -480,7 +501,3 @@ export function listCliStatus(cwd) {
|
|
|
480
501
|
}));
|
|
481
502
|
return { statuses, errors };
|
|
482
503
|
}
|
|
483
|
-
/** Names of CLIs that are declared but not currently installed on the host. */
|
|
484
|
-
export function getMissingClis(cwd) {
|
|
485
|
-
return listCliStatus(cwd).statuses.filter((s) => !s.installed).map((s) => s.manifest);
|
|
486
|
-
}
|
package/dist/lib/cloud/rush.d.ts
CHANGED
|
@@ -29,6 +29,21 @@ export interface AccountTokenEntry {
|
|
|
29
29
|
/** Stringified OAuth credentials JSON (Mac: keychain blob; Linux: .credentials.json). */
|
|
30
30
|
credentials_json: string;
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Read the raw OAuth credentials for one Claude version. On Mac, prefer the
|
|
34
|
+
* Keychain blob (canonical). On Linux/CI, fall back to `.claude/.credentials.json`
|
|
35
|
+
* inside the version home (where the Linux Claude CLI stores its OAuth).
|
|
36
|
+
*
|
|
37
|
+
* Returns null when no credentials are findable — caller treats as "version
|
|
38
|
+
* is installed but not signed in" and skips it from the manifest.
|
|
39
|
+
*
|
|
40
|
+
* NOTE: the darwin branch returns the UNWRAPPED oauth sub-object (what the Rush
|
|
41
|
+
* server re-wraps). It is NOT the shape Claude Code reads back from
|
|
42
|
+
* `.credentials.json`. The lease exporter (crabbox/runtimes.ts) therefore only
|
|
43
|
+
* reuses the Linux `.credentials.json` branch here and reads the wrapped raw
|
|
44
|
+
* Keychain payload itself on darwin.
|
|
45
|
+
*/
|
|
46
|
+
export declare function readClaudeCredentialsBlob(home: string): Promise<string | null>;
|
|
32
47
|
/**
|
|
33
48
|
* Build a manifest of the user's local Claude installations to send on every
|
|
34
49
|
* cloud dispatch. The manifest is the contract the server uses to detect when
|
|
@@ -75,30 +90,6 @@ export declare function buildDispatchBody(input: {
|
|
|
75
90
|
*/
|
|
76
91
|
images?: ImageAttachment[] | null;
|
|
77
92
|
}): Record<string, unknown>;
|
|
78
|
-
/** A single account registered in Rush Cloud's multi-account rotation pool. */
|
|
79
|
-
export interface RemoteAccount {
|
|
80
|
-
id: string;
|
|
81
|
-
provider: string;
|
|
82
|
-
email: string | null;
|
|
83
|
-
subscription_type: string | null;
|
|
84
|
-
five_hour_pct: number | null;
|
|
85
|
-
seven_day_pct: number | null;
|
|
86
|
-
usage_fetched_at: string | null;
|
|
87
|
-
created_at: string;
|
|
88
|
-
}
|
|
89
|
-
/** Fetch all Claude accounts in this user's Rush Cloud rotation pool (no tokens). */
|
|
90
|
-
export declare function listRemoteAccounts(): Promise<RemoteAccount[]>;
|
|
91
|
-
/**
|
|
92
|
-
* Register a CLAUDE_CODE_OAUTH_TOKEN with Rush Cloud's rotation pool.
|
|
93
|
-
* The server validates the token against the Anthropic usage API and stores it
|
|
94
|
-
* encrypted in Vault. Returns the account metadata (no token).
|
|
95
|
-
*/
|
|
96
|
-
export declare function addRemoteAccount(provider: string, pastedToken: string): Promise<RemoteAccount & {
|
|
97
|
-
five_hour_pct: number | null;
|
|
98
|
-
seven_day_pct: number | null;
|
|
99
|
-
}>;
|
|
100
|
-
/** Remove a Claude account from Rush Cloud's rotation pool by its ID. */
|
|
101
|
-
export declare function removeRemoteAccount(id: string): Promise<void>;
|
|
102
93
|
export declare class RushCloudProvider implements CloudProvider {
|
|
103
94
|
id: "rush";
|
|
104
95
|
name: string;
|
package/dist/lib/cloud/rush.js
CHANGED
|
@@ -170,8 +170,14 @@ function parsePromptCode(body) {
|
|
|
170
170
|
*
|
|
171
171
|
* Returns null when no credentials are findable — caller treats as "version
|
|
172
172
|
* is installed but not signed in" and skips it from the manifest.
|
|
173
|
+
*
|
|
174
|
+
* NOTE: the darwin branch returns the UNWRAPPED oauth sub-object (what the Rush
|
|
175
|
+
* server re-wraps). It is NOT the shape Claude Code reads back from
|
|
176
|
+
* `.credentials.json`. The lease exporter (crabbox/runtimes.ts) therefore only
|
|
177
|
+
* reuses the Linux `.credentials.json` branch here and reads the wrapped raw
|
|
178
|
+
* Keychain payload itself on darwin.
|
|
173
179
|
*/
|
|
174
|
-
async function readClaudeCredentialsBlob(home) {
|
|
180
|
+
export async function readClaudeCredentialsBlob(home) {
|
|
175
181
|
if (process.platform === 'darwin') {
|
|
176
182
|
const oauth = await loadClaudeOauth(home);
|
|
177
183
|
if (oauth && oauth.accessToken) {
|
|
@@ -294,37 +300,6 @@ export function buildDispatchBody(input) {
|
|
|
294
300
|
}
|
|
295
301
|
return body;
|
|
296
302
|
}
|
|
297
|
-
/** Fetch all Claude accounts in this user's Rush Cloud rotation pool (no tokens). */
|
|
298
|
-
export async function listRemoteAccounts() {
|
|
299
|
-
const token = readToken();
|
|
300
|
-
const res = await api('GET', '/api/v1/cloud-accounts', token);
|
|
301
|
-
if (!res.ok) {
|
|
302
|
-
throw new Error(`Failed to list accounts (${res.status}): ${sanitizeErrorBody(await res.text())}`);
|
|
303
|
-
}
|
|
304
|
-
const data = await res.json();
|
|
305
|
-
return data.accounts ?? [];
|
|
306
|
-
}
|
|
307
|
-
/**
|
|
308
|
-
* Register a CLAUDE_CODE_OAUTH_TOKEN with Rush Cloud's rotation pool.
|
|
309
|
-
* The server validates the token against the Anthropic usage API and stores it
|
|
310
|
-
* encrypted in Vault. Returns the account metadata (no token).
|
|
311
|
-
*/
|
|
312
|
-
export async function addRemoteAccount(provider, pastedToken) {
|
|
313
|
-
const token = readToken();
|
|
314
|
-
const res = await api('POST', '/api/v1/cloud-accounts', token, { provider, token: pastedToken });
|
|
315
|
-
if (!res.ok) {
|
|
316
|
-
throw new Error(`Failed to add account (${res.status}): ${sanitizeErrorBody(await res.text())}`);
|
|
317
|
-
}
|
|
318
|
-
return await res.json();
|
|
319
|
-
}
|
|
320
|
-
/** Remove a Claude account from Rush Cloud's rotation pool by its ID. */
|
|
321
|
-
export async function removeRemoteAccount(id) {
|
|
322
|
-
const token = readToken();
|
|
323
|
-
const res = await api('DELETE', `/api/v1/cloud-accounts/${encodeURIComponent(id)}`, token);
|
|
324
|
-
if (!res.ok) {
|
|
325
|
-
throw new Error(`Failed to remove account (${res.status}): ${sanitizeErrorBody(await res.text())}`);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
303
|
export class RushCloudProvider {
|
|
329
304
|
id = 'rush';
|
|
330
305
|
name = 'Rush Cloud';
|
package/dist/lib/crabbox/cli.js
CHANGED
|
@@ -209,7 +209,10 @@ export function crabboxRunScript(slug, script, opts = {}) {
|
|
|
209
209
|
/** Release the lease / delete the box. Best-effort; never throws. */
|
|
210
210
|
export function crabboxStop(slug, opts = {}) {
|
|
211
211
|
try {
|
|
212
|
-
|
|
212
|
+
// Positional target: crabbox's stop subcommand has no --id flag (unlike
|
|
213
|
+
// status/run/ssh) — `stop --id <slug>` dies with "flag provided but not
|
|
214
|
+
// defined: -id" and the box leaks past the run it was leased for.
|
|
215
|
+
const r = spawnSync('crabbox', ['stop', slug], { encoding: 'utf-8', env: crabboxEnv(opts) });
|
|
213
216
|
return r.status === 0;
|
|
214
217
|
}
|
|
215
218
|
catch {
|
|
@@ -26,6 +26,12 @@ export interface LeaseRunOptions {
|
|
|
26
26
|
onData?: (s: string) => void;
|
|
27
27
|
/** Keep the box after the run instead of stopping it. */
|
|
28
28
|
keep?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Raw wrapped Claude OAuth payload (from `resolveClaudeCredentialsBlob`), written
|
|
31
|
+
* to `~/.claude/.credentials.json` on the box. The command layer resolves it
|
|
32
|
+
* (after consent) so this module stays free of Keychain I/O and unit-testable.
|
|
33
|
+
*/
|
|
34
|
+
claudeCredentialsJson?: string | null;
|
|
29
35
|
}
|
|
30
36
|
export interface LeaseRunResult {
|
|
31
37
|
box: CrabboxBox;
|
|
@@ -7,35 +7,65 @@
|
|
|
7
7
|
* `--script-stdin` body so the token contents never touch argv.
|
|
8
8
|
*/
|
|
9
9
|
import { crabboxWarmup, crabboxWaitReady, crabboxRunScript, crabboxStop } from './cli.js';
|
|
10
|
-
import { buildCredentialScript } from './runtimes.js';
|
|
10
|
+
import { buildCredentialScript, CLAUDE_TOKEN_REMOTE } from './runtimes.js';
|
|
11
11
|
/** POSIX single-quote for safe embedding in the generated bootstrap script. */
|
|
12
12
|
function q(s) {
|
|
13
13
|
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Bash snippet that guarantees `agents` is runnable on the box. Fresh crabbox
|
|
17
|
+
* images ship without node, and the box user may not own the global npm prefix,
|
|
18
|
+
* so everything installs user-level under ~/.local (node from the official
|
|
19
|
+
* latest-v22.x tarball to satisfy engines.node >=22.5.0). Exits 96 with a
|
|
20
|
+
* diagnostic when the CLI still isn't runnable — a silent `|| true` here used
|
|
21
|
+
* to surface only as `agents: command not found` deep in the script.
|
|
22
|
+
*/
|
|
23
|
+
const ENSURE_AGENTS_CLI = [
|
|
24
|
+
'export PATH="$HOME/.local/bin:$PATH"',
|
|
25
|
+
'if ! command -v node >/dev/null 2>&1; then',
|
|
26
|
+
' case "$(uname -m)" in aarch64|arm64) narch=arm64;; *) narch=x64;; esac',
|
|
27
|
+
' nver=$(curl -fsSL https://nodejs.org/dist/latest-v22.x/ | grep -oE "v22\\.[0-9]+\\.[0-9]+" | head -1)',
|
|
28
|
+
' mkdir -p "$HOME/.local"',
|
|
29
|
+
' curl -fsSL "https://nodejs.org/dist/latest-v22.x/node-$nver-linux-$narch.tar.xz" | tar -xJ -C "$HOME/.local" --strip-components=1',
|
|
30
|
+
'fi',
|
|
31
|
+
'if ! command -v agents >/dev/null 2>&1; then',
|
|
32
|
+
' npm config set prefix "$HOME/.local" >/dev/null 2>&1 || true',
|
|
33
|
+
' npm install -g @phnx-labs/agents-cli >/dev/null 2>&1',
|
|
34
|
+
'fi',
|
|
35
|
+
'if ! command -v agents >/dev/null 2>&1; then',
|
|
36
|
+
' echo "lease bootstrap: agents-cli install failed (node: $(command -v node || echo missing))" >&2',
|
|
37
|
+
' exit 96',
|
|
38
|
+
'fi',
|
|
39
|
+
// Same first-run guard the hosts bootstrap uses (hosts/ready.ts) — a fresh
|
|
40
|
+
// install refuses `agents run` with "agents-cli is not set up" until setup ran.
|
|
41
|
+
'if [ ! -d "$HOME/.agents/.system" ]; then agents setup >/dev/null 2>&1 || true; fi',
|
|
42
|
+
].join('\n');
|
|
15
43
|
/**
|
|
16
44
|
* Build the single bootstrap script run on the box: ensure agents-cli, install
|
|
17
45
|
* the picked runtime CLIs, write their credentials, run the agent, then shred
|
|
18
46
|
* the credential files. Best-effort install steps never abort the run.
|
|
19
47
|
*/
|
|
20
48
|
export function buildBootstrapScript(opts) {
|
|
21
|
-
const credScript = buildCredentialScript(opts.runtimes, opts.detected
|
|
49
|
+
const credScript = buildCredentialScript(opts.runtimes, opts.detected, {
|
|
50
|
+
claudeCredentialsJson: opts.claudeCredentialsJson,
|
|
51
|
+
});
|
|
22
52
|
const runParts = ['agents', 'run', q(opts.agent), q(opts.prompt), '--quiet'];
|
|
23
53
|
if (opts.mode)
|
|
24
54
|
runParts.push('--mode', q(opts.mode));
|
|
25
55
|
if (opts.model)
|
|
26
56
|
runParts.push('--model', q(opts.model));
|
|
27
57
|
// Credential files to shred after the run (home-level paths written above).
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
58
|
+
// Runs regardless of --keep-box (it's in the box body, not teardown), so a kept
|
|
59
|
+
// box still loses the token after the run — minimizing the credential window.
|
|
60
|
+
const shredPaths = opts.runtimes.flatMap((id) => {
|
|
61
|
+
const paths = { claude: ['.claude.json', CLAUDE_TOKEN_REMOTE], codex: ['.codex/auth.json'], gemini: ['.gemini/google_accounts.json'], grok: ['.grok/auth.json'] }[id];
|
|
62
|
+
return paths ?? [];
|
|
63
|
+
});
|
|
64
|
+
const shred = shredPaths.map((p) => `rm -f "$HOME/${p}" 2>/dev/null || true`).join('\n');
|
|
35
65
|
const installRuntimes = opts.runtimes.map((id) => `agents add ${q(id)} >/dev/null 2>&1 || true`).join('\n');
|
|
36
66
|
return [
|
|
37
67
|
'set -uo pipefail',
|
|
38
|
-
|
|
68
|
+
ENSURE_AGENTS_CLI,
|
|
39
69
|
installRuntimes,
|
|
40
70
|
credScript,
|
|
41
71
|
`${runParts.join(' ')}`,
|
|
@@ -47,11 +47,48 @@ export declare function pickRuntimes(detected: DetectedRuntime[], prompt?: (choi
|
|
|
47
47
|
checked: boolean;
|
|
48
48
|
disabled: boolean | string;
|
|
49
49
|
}[]) => Promise<AgentId[]>): Promise<AgentId[]>;
|
|
50
|
+
/**
|
|
51
|
+
* Where Claude Code reads its OAuth token on the box. `.claude.json` (the file
|
|
52
|
+
* LEASE_RUNTIMES copies) is config/account-metadata ONLY — the actual token
|
|
53
|
+
* lives here, so without it the box boots "Not logged in".
|
|
54
|
+
*/
|
|
55
|
+
export declare const CLAUDE_TOKEN_REMOTE = ".claude/.credentials.json";
|
|
56
|
+
/**
|
|
57
|
+
* The RAW wrapped Claude credential payload (`{"claudeAiOauth":{…}}`) to write to
|
|
58
|
+
* the box's `~/.claude/.credentials.json`, or null if no signed-in token is found.
|
|
59
|
+
*
|
|
60
|
+
* On macOS the token is in the login Keychain, read SILENTLY via
|
|
61
|
+
* `getKeychainToken` (the `/usr/bin/security … -w` path — Claude's item trusts it,
|
|
62
|
+
* no Touch ID). A default native install uses the bare `Claude Code-credentials`
|
|
63
|
+
* service; an agents-cli managed install (where `~/.claude` symlinks into a
|
|
64
|
+
* versioned home) uses a hash-suffixed service, so we try the bare service first,
|
|
65
|
+
* then enumerate installed version homes (preferring the account whose email
|
|
66
|
+
* matches `preferEmail`, so the token matches the `.claude.json` config we copy).
|
|
67
|
+
* Off macOS the local Claude CLI stores the token in `.credentials.json` already —
|
|
68
|
+
* reuse the rush.ts Linux branch verbatim.
|
|
69
|
+
*
|
|
70
|
+
* The reader/service/version helpers are injected so unit tests never touch the
|
|
71
|
+
* real Keychain.
|
|
72
|
+
*/
|
|
73
|
+
export declare function resolveClaudeCredentialsBlob(opts?: {
|
|
74
|
+
preferEmail?: string | null;
|
|
75
|
+
readItem?: (service: string) => string;
|
|
76
|
+
service?: (home?: string) => string;
|
|
77
|
+
listVersions?: () => string[];
|
|
78
|
+
versionHome?: (version: string) => string;
|
|
79
|
+
accountEmail?: (home: string) => Promise<string | null>;
|
|
80
|
+
}): Promise<string | null>;
|
|
50
81
|
/**
|
|
51
82
|
* Build a bash snippet that writes each picked runtime's token file to the box's
|
|
52
83
|
* home-level config path (0600), from the token contents read locally. Returns
|
|
53
84
|
* `''` when no runtimes were selected. The snippet is meant to be embedded in
|
|
54
85
|
* the `--script-stdin` body (never argv).
|
|
86
|
+
*
|
|
87
|
+
* `extras.claudeCredentialsJson` (the raw wrapped payload from
|
|
88
|
+
* `resolveClaudeCredentialsBlob`) is written to `~/.claude/.credentials.json` in
|
|
89
|
+
* ADDITION to claude's `.claude.json` config — without it the box is "Not logged in".
|
|
55
90
|
*/
|
|
56
|
-
export declare function buildCredentialScript(picked: AgentId[], detected: DetectedRuntime[]
|
|
91
|
+
export declare function buildCredentialScript(picked: AgentId[], detected: DetectedRuntime[], extras?: {
|
|
92
|
+
claudeCredentialsJson?: string | null;
|
|
93
|
+
}): string;
|
|
57
94
|
export {};
|
|
@@ -16,6 +16,10 @@ import * as os from 'os';
|
|
|
16
16
|
import * as path from 'path';
|
|
17
17
|
import * as fs from 'fs';
|
|
18
18
|
import { getAccountInfo } from '../agents.js';
|
|
19
|
+
import { getKeychainToken } from '../secrets/index.js';
|
|
20
|
+
import { getClaudeKeychainService } from '../usage.js';
|
|
21
|
+
import { listInstalledVersions, getVersionHomePath } from '../versions.js';
|
|
22
|
+
import { readClaudeCredentialsBlob } from '../cloud/rush.js';
|
|
19
23
|
export const LEASE_RUNTIMES = [
|
|
20
24
|
{ id: 'claude', label: 'Claude Code', localCandidates: ['.claude/.claude.json', '.claude.json'], remote: '.claude.json' },
|
|
21
25
|
{ id: 'codex', label: 'Codex CLI', localCandidates: ['.codex/auth.json'], remote: '.codex/auth.json' },
|
|
@@ -79,15 +83,102 @@ export async function pickRuntimes(detected, prompt) {
|
|
|
79
83
|
// token's contents effectively impossible, so the quoted heredoc can never be
|
|
80
84
|
// closed early by the credential body.
|
|
81
85
|
const CRED_EOF = 'AGENTS_LEASE_CRED_EOF_9f3c1a7b5e2d4068';
|
|
86
|
+
/**
|
|
87
|
+
* Where Claude Code reads its OAuth token on the box. `.claude.json` (the file
|
|
88
|
+
* LEASE_RUNTIMES copies) is config/account-metadata ONLY — the actual token
|
|
89
|
+
* lives here, so without it the box boots "Not logged in".
|
|
90
|
+
*/
|
|
91
|
+
export const CLAUDE_TOKEN_REMOTE = '.claude/.credentials.json';
|
|
92
|
+
/** True when `s` parses to a Claude keychain payload with an OAuth access token. */
|
|
93
|
+
function isClaudeCredentialsBlob(s) {
|
|
94
|
+
try {
|
|
95
|
+
const p = JSON.parse(s);
|
|
96
|
+
return typeof p?.claudeAiOauth?.accessToken === 'string';
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The RAW wrapped Claude credential payload (`{"claudeAiOauth":{…}}`) to write to
|
|
104
|
+
* the box's `~/.claude/.credentials.json`, or null if no signed-in token is found.
|
|
105
|
+
*
|
|
106
|
+
* On macOS the token is in the login Keychain, read SILENTLY via
|
|
107
|
+
* `getKeychainToken` (the `/usr/bin/security … -w` path — Claude's item trusts it,
|
|
108
|
+
* no Touch ID). A default native install uses the bare `Claude Code-credentials`
|
|
109
|
+
* service; an agents-cli managed install (where `~/.claude` symlinks into a
|
|
110
|
+
* versioned home) uses a hash-suffixed service, so we try the bare service first,
|
|
111
|
+
* then enumerate installed version homes (preferring the account whose email
|
|
112
|
+
* matches `preferEmail`, so the token matches the `.claude.json` config we copy).
|
|
113
|
+
* Off macOS the local Claude CLI stores the token in `.credentials.json` already —
|
|
114
|
+
* reuse the rush.ts Linux branch verbatim.
|
|
115
|
+
*
|
|
116
|
+
* The reader/service/version helpers are injected so unit tests never touch the
|
|
117
|
+
* real Keychain.
|
|
118
|
+
*/
|
|
119
|
+
export async function resolveClaudeCredentialsBlob(opts) {
|
|
120
|
+
const readItem = opts?.readItem ?? getKeychainToken;
|
|
121
|
+
const service = opts?.service ?? getClaudeKeychainService;
|
|
122
|
+
const listVersions = opts?.listVersions ?? (() => listInstalledVersions('claude'));
|
|
123
|
+
const versionHome = opts?.versionHome ?? ((v) => getVersionHomePath('claude', v));
|
|
124
|
+
const accountEmail = opts?.accountEmail ?? (async (home) => (await getAccountInfo('claude', home)).email);
|
|
125
|
+
const tryRead = (svc) => {
|
|
126
|
+
try {
|
|
127
|
+
const raw = readItem(svc).trim();
|
|
128
|
+
return isClaudeCredentialsBlob(raw) ? raw : null;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
if (process.platform === 'darwin') {
|
|
135
|
+
// 1) Bare service — the default native (non-managed) install.
|
|
136
|
+
const bare = tryRead(service(undefined));
|
|
137
|
+
if (bare)
|
|
138
|
+
return bare;
|
|
139
|
+
// 2) Managed installs — hash-suffixed service keyed to each version home.
|
|
140
|
+
// Prefer the version whose account email matches the copied config.
|
|
141
|
+
let homes;
|
|
142
|
+
try {
|
|
143
|
+
homes = listVersions().map(versionHome);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
homes = [];
|
|
147
|
+
}
|
|
148
|
+
if (opts?.preferEmail) {
|
|
149
|
+
const scored = await Promise.all(homes.map(async (home) => ({ home, match: (await accountEmail(home).catch(() => null)) === opts.preferEmail })));
|
|
150
|
+
homes = [...scored.filter((s) => s.match), ...scored.filter((s) => !s.match)].map((s) => s.home);
|
|
151
|
+
}
|
|
152
|
+
for (const home of homes) {
|
|
153
|
+
const hit = tryRead(service(home));
|
|
154
|
+
if (hit)
|
|
155
|
+
return hit;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
// Off darwin: the local Claude CLI already stores the wrapped blob on disk.
|
|
160
|
+
const home = process.env.AGENTS_REAL_HOME || os.homedir();
|
|
161
|
+
return readClaudeCredentialsBlob(home);
|
|
162
|
+
}
|
|
82
163
|
/**
|
|
83
164
|
* Build a bash snippet that writes each picked runtime's token file to the box's
|
|
84
165
|
* home-level config path (0600), from the token contents read locally. Returns
|
|
85
166
|
* `''` when no runtimes were selected. The snippet is meant to be embedded in
|
|
86
167
|
* the `--script-stdin` body (never argv).
|
|
168
|
+
*
|
|
169
|
+
* `extras.claudeCredentialsJson` (the raw wrapped payload from
|
|
170
|
+
* `resolveClaudeCredentialsBlob`) is written to `~/.claude/.credentials.json` in
|
|
171
|
+
* ADDITION to claude's `.claude.json` config — without it the box is "Not logged in".
|
|
87
172
|
*/
|
|
88
|
-
export function buildCredentialScript(picked, detected) {
|
|
173
|
+
export function buildCredentialScript(picked, detected, extras) {
|
|
89
174
|
const byId = new Map(detected.map((d) => [d.id, d]));
|
|
90
175
|
const parts = [];
|
|
176
|
+
const writeFile = (remote, contents) => {
|
|
177
|
+
const dir = path.posix.dirname(remote);
|
|
178
|
+
const mkdir = dir && dir !== '.' ? `mkdir -p "$HOME/${dir}"\n` : '';
|
|
179
|
+
return (`${mkdir}cat > "$HOME/${remote}" <<'${CRED_EOF}'\n${contents}${contents.endsWith('\n') ? '' : '\n'}${CRED_EOF}\n` +
|
|
180
|
+
`chmod 600 "$HOME/${remote}"`);
|
|
181
|
+
};
|
|
91
182
|
for (const id of picked) {
|
|
92
183
|
const d = byId.get(id);
|
|
93
184
|
const cred = LEASE_RUNTIMES.find((c) => c.id === id);
|
|
@@ -100,10 +191,12 @@ export function buildCredentialScript(picked, detected) {
|
|
|
100
191
|
catch {
|
|
101
192
|
continue;
|
|
102
193
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
194
|
+
parts.push(writeFile(cred.remote, contents));
|
|
195
|
+
// For claude the file above is config/state only; the OAuth token is a
|
|
196
|
+
// second artifact — without it the box comes up "Not logged in".
|
|
197
|
+
if (id === 'claude' && extras?.claudeCredentialsJson) {
|
|
198
|
+
parts.push(writeFile(CLAUDE_TOKEN_REMOTE, extras.claudeCredentialsJson));
|
|
199
|
+
}
|
|
107
200
|
}
|
|
108
201
|
return parts.join('\n');
|
|
109
202
|
}
|
package/dist/lib/daemon.d.ts
CHANGED
|
@@ -54,13 +54,15 @@ export declare function runDaemon(): Promise<void>;
|
|
|
54
54
|
/**
|
|
55
55
|
* Read the long-lived Claude OAuth token (from `claude setup-token`) that the
|
|
56
56
|
* user stored under the `claude` secrets bundle. Resolves the bundle the same
|
|
57
|
-
* way `agents run --secrets` does
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
57
|
+
* way `agents run --secrets` does. Interactive starts may prompt Keychain;
|
|
58
|
+
* headless auto-starts are broker-only and return null unless the user already
|
|
59
|
+
* unlocked the bundle in the secrets agent. That keeps a background browser
|
|
60
|
+
* command from hanging on an unseen biometric prompt. Never throws: an absent
|
|
61
|
+
* token leaves the daemon on its existing interactive OAuth session.
|
|
62
62
|
*/
|
|
63
|
-
export declare function readDaemonClaudeOAuthToken(
|
|
63
|
+
export declare function readDaemonClaudeOAuthToken(opts?: {
|
|
64
|
+
allowPrompt?: boolean;
|
|
65
|
+
}): string | null;
|
|
64
66
|
/**
|
|
65
67
|
* Write a launchd plist or systemd unit with owner-only permissions atomically.
|
|
66
68
|
*
|
|
@@ -72,9 +74,10 @@ export declare function readDaemonClaudeOAuthToken(): string | null;
|
|
|
72
74
|
*/
|
|
73
75
|
export declare function writeOwnerOnlyServiceManifest(filePath: string, content: string): void;
|
|
74
76
|
/** Generate a macOS launchd plist for auto-starting the daemon. */
|
|
75
|
-
export declare function generateLaunchdPlist(): string;
|
|
77
|
+
export declare function generateLaunchdPlist(oauthToken?: string | null): string;
|
|
76
78
|
/** Generate a Linux systemd user unit for auto-starting the daemon. */
|
|
77
|
-
export declare function generateSystemdUnit(): string;
|
|
79
|
+
export declare function generateSystemdUnit(oauthToken?: string | null): string;
|
|
80
|
+
export declare function getAgentsBinPath(): string;
|
|
78
81
|
/** Start the daemon via launchd, systemd, or as a detached process. */
|
|
79
82
|
export declare function startDaemon(): {
|
|
80
83
|
pid: number | null;
|
|
@@ -102,7 +105,7 @@ export declare function ensureDaemonStarted(): {
|
|
|
102
105
|
* the daemon then passes it to every routine run it spawns. An already-set
|
|
103
106
|
* value (e.g. inherited from launchd) is left untouched.
|
|
104
107
|
*/
|
|
105
|
-
export declare function buildDetachedDaemonEnv(baseEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
108
|
+
export declare function buildDetachedDaemonEnv(baseEnv?: NodeJS.ProcessEnv, oauthToken?: string | null): NodeJS.ProcessEnv;
|
|
106
109
|
/**
|
|
107
110
|
* Resolve how to launch the daemon: `node <entry> daemon _run`, matching the
|
|
108
111
|
* exact form that works under a direct `daemon _run`.
|