@phnx-labs/agents-cli 1.22.32 → 1.22.34
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 +55 -0
- package/README.md +2 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/accounts.js +3 -3
- package/dist/commands/browser-sessions-picker.d.ts +16 -0
- package/dist/commands/browser-sessions-picker.js +179 -0
- package/dist/commands/browser.js +9 -4
- package/dist/commands/hosts.js +1 -5
- package/dist/commands/inspect.js +174 -41
- package/dist/commands/message.d.ts +6 -1
- package/dist/commands/message.js +60 -3
- package/dist/commands/sessions-browser.js +2 -2
- package/dist/commands/sessions.d.ts +54 -4
- package/dist/commands/sessions.js +274 -58
- package/dist/commands/ssh.js +17 -5
- package/dist/commands/teams.d.ts +28 -0
- package/dist/commands/teams.js +148 -13
- package/dist/commands/upgrade.d.ts +7 -0
- package/dist/commands/upgrade.js +10 -0
- package/dist/commands/watchdog.d.ts +2 -0
- package/dist/commands/watchdog.js +112 -27
- package/dist/index.js +51 -59
- package/dist/lib/agents.js +6 -0
- package/dist/lib/browser/sessions-list.d.ts +81 -0
- package/dist/lib/browser/sessions-list.js +179 -4
- package/dist/lib/daemon.js +45 -5
- package/dist/lib/devices/connect.d.ts +33 -0
- package/dist/lib/devices/connect.js +61 -3
- package/dist/lib/devices/doctor-findings.d.ts +4 -2
- package/dist/lib/devices/doctor-findings.js +4 -2
- package/dist/lib/exec.js +63 -6
- package/dist/lib/help.d.ts +3 -2
- package/dist/lib/help.js +4 -0
- package/dist/lib/hosts/dispatch.d.ts +2 -32
- package/dist/lib/hosts/dispatch.js +6 -61
- package/dist/lib/hosts/tasks.d.ts +7 -0
- package/dist/lib/hosts/tasks.js +9 -0
- package/dist/lib/mailbox-target.d.ts +27 -0
- package/dist/lib/mailbox-target.js +21 -0
- package/dist/lib/mcp.d.ts +10 -0
- package/dist/lib/mcp.js +21 -2
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/migrate.d.ts +11 -0
- package/dist/lib/migrate.js +40 -0
- package/dist/lib/project-root.d.ts +47 -0
- package/dist/lib/project-root.js +68 -0
- 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 +9 -2
- package/dist/lib/secrets/agent.js +52 -7
- package/dist/lib/secrets/reaper.d.ts +24 -3
- package/dist/lib/secrets/reaper.js +55 -6
- package/dist/lib/session/discover.js +12 -0
- package/dist/lib/session/render.d.ts +2 -0
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/session/team-filter.d.ts +11 -0
- package/dist/lib/session/team-filter.js +10 -0
- package/dist/lib/shims.js +9 -2
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +8 -3
- package/dist/lib/startup/root-command.d.ts +3 -0
- package/dist/lib/startup/root-command.js +10 -0
- package/dist/lib/teams/agents.d.ts +136 -6
- package/dist/lib/teams/agents.js +324 -58
- package/dist/lib/teams/worktree.d.ts +39 -2
- package/dist/lib/teams/worktree.js +60 -4
- package/dist/lib/types.d.ts +11 -0
- package/dist/lib/versions.js +2 -2
- package/dist/lib/watchdog/history.d.ts +20 -0
- package/dist/lib/watchdog/history.js +46 -0
- package/dist/lib/watchdog/log.d.ts +16 -1
- package/dist/lib/watchdog/log.js +82 -2
- package/dist/lib/watchdog/runner.d.ts +12 -0
- package/dist/lib/watchdog/runner.js +20 -0
- package/package.json +1 -1
package/dist/lib/exec.js
CHANGED
|
@@ -292,10 +292,37 @@ export function buildExecEnv(options) {
|
|
|
292
292
|
result.CLAUDE_CONFIG_DIR = path.join(versionHome, '.claude');
|
|
293
293
|
const setupToken = resolveClaudeSetupToken(versionHome);
|
|
294
294
|
if (setupToken) {
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
|
|
295
|
+
// The `auth` bundle's setup-token exists so a run with NO human present
|
|
296
|
+
// authenticates without the Touch-ID-gated login item — usage probes,
|
|
297
|
+
// routines, dispatched runs (claude-account-token.ts). An interactive run
|
|
298
|
+
// has a human at the TTY, and their own per-version login is the credential
|
|
299
|
+
// they established and expect; overriding it made `/status` report
|
|
300
|
+
// `Auth token: CLAUDE_CODE_OAUTH_TOKEN` on a personal machine and took every
|
|
301
|
+
// hand-driven session off that login. macOS cannot cheaply confirm a home's
|
|
302
|
+
// login first (probing the Keychain raises an authorization sheet per
|
|
303
|
+
// installed version on the `agents run` hot path — agents.ts
|
|
304
|
+
// `isClaudeCredentialFileBlank`), so interactive simply defers to Claude
|
|
305
|
+
// Code, which prompts a present human to log in if the login is missing.
|
|
306
|
+
if (resolveInteractive(options)) {
|
|
307
|
+
// Drop an INHERITED copy of the same token too: an interactive launch from
|
|
308
|
+
// inside a headless agent's shell inherits that agent's injected value via
|
|
309
|
+
// sanitizeProcessEnv(process.env) and would keep authenticating as it.
|
|
310
|
+
// Matched by VALUE, so a token the user exported deliberately is a
|
|
311
|
+
// different string and is left alone. This is NARROWER than the routines
|
|
312
|
+
// path, which overwrites-or-deletes unconditionally and never inspects the
|
|
313
|
+
// inherited value (`runner.ts:1020-1021`) — the gap between the two is
|
|
314
|
+
// what RUSH-2360 tracks, including a DIFFERENT account's inherited token,
|
|
315
|
+
// which this equality check lets through.
|
|
316
|
+
if (result.CLAUDE_CODE_OAUTH_TOKEN === setupToken) {
|
|
317
|
+
delete result.CLAUDE_CODE_OAUTH_TOKEN;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
// A token keyed to this version home's own account replaces any ambient
|
|
322
|
+
// shared value inherited from the launcher. options.env still wins below
|
|
323
|
+
// for explicit caller overrides.
|
|
324
|
+
result.CLAUDE_CODE_OAUTH_TOKEN = setupToken;
|
|
325
|
+
}
|
|
299
326
|
}
|
|
300
327
|
// A managed pin lives in a per-version dir; Claude Code's own background
|
|
301
328
|
// auto-updater would rewrite that pinned binary in place (and has left it
|
|
@@ -381,6 +408,29 @@ export function buildExecEnv(options) {
|
|
|
381
408
|
delete result.COPILOT_HOME;
|
|
382
409
|
delete result.KIMI_CODE_HOME;
|
|
383
410
|
}
|
|
411
|
+
else if (options.agent === 'cursor') {
|
|
412
|
+
// Cursor has no config-dir env var (only CURSOR_API_KEY / CURSOR_API_ENDPOINT).
|
|
413
|
+
// Its OAuth token — the login gate — lives at $XDG_CONFIG_HOME/cursor/auth.json
|
|
414
|
+
// (verified empirically: relocating XDG_CONFIG_HOME relocates the login;
|
|
415
|
+
// ~/.cursor/cli-config.json holds only account metadata, not the token). Pin
|
|
416
|
+
// XDG_CONFIG_HOME into the version home so each installed Cursor account
|
|
417
|
+
// authenticates from its own token, isolated per run — no global ~/.cursor
|
|
418
|
+
// symlink swap, so concurrent runs on different accounts never clobber one
|
|
419
|
+
// another. cli-config.json (HOME-relative) has no override and stays on the
|
|
420
|
+
// shared home; only the token is per-account, which is what gates the login.
|
|
421
|
+
const cwd = options.cwd || process.cwd();
|
|
422
|
+
const resolvedVersion = options.version ?? resolveVersion('cursor', cwd);
|
|
423
|
+
const version = options.version
|
|
424
|
+
? resolvedVersion
|
|
425
|
+
: (resolvedVersion && isVersionInstalled('cursor', resolvedVersion) ? resolvedVersion : null);
|
|
426
|
+
if (version) {
|
|
427
|
+
result.XDG_CONFIG_HOME = path.join(getVersionHomePath('cursor', version), '.config');
|
|
428
|
+
}
|
|
429
|
+
delete result.CLAUDE_CONFIG_DIR;
|
|
430
|
+
delete result.CODEX_HOME;
|
|
431
|
+
delete result.COPILOT_HOME;
|
|
432
|
+
delete result.KIMI_CODE_HOME;
|
|
433
|
+
}
|
|
384
434
|
else {
|
|
385
435
|
delete result.CLAUDE_CONFIG_DIR;
|
|
386
436
|
delete result.CODEX_HOME;
|
|
@@ -1139,8 +1189,15 @@ export async function execShimPassthrough(agent, rawArgs, cwd, pinnedVersion) {
|
|
|
1139
1189
|
// a shell, while the hook runs under the agent descendant. This is the primary
|
|
1140
1190
|
// attribution path on Windows (no lsof), so the launchId join matters most here.
|
|
1141
1191
|
const launchId = randomUUID();
|
|
1142
|
-
// mode/effort are required by ExecOptions but
|
|
1143
|
-
// derives
|
|
1192
|
+
// mode/effort are required by ExecOptions but do not affect the env buildExecEnv
|
|
1193
|
+
// derives; pass the agent's default to satisfy the type. Passing no prompt makes
|
|
1194
|
+
// this resolve INTERACTIVE, so the launch authenticates from the per-version
|
|
1195
|
+
// login rather than the headless `auth` setup-token (EXEC-2a) — right for the
|
|
1196
|
+
// common case, someone invoking the harness binary directly. Known limit: we do
|
|
1197
|
+
// not parse `rawArgs`, so a `-p "task"` passthrough is headless in fact and
|
|
1198
|
+
// interactive by this classification, and loses the token. Windows-only in
|
|
1199
|
+
// practice (POSIX uses the bash shim, which execs the binary and never reaches
|
|
1200
|
+
// buildExecEnv — `shims.ts:749`, `shims.ts:761-764`).
|
|
1144
1201
|
const env = buildExecEnv({ agent, version, cwd, mode: defaultModeFor(agent), effort: 'auto', env: { AGENT_LAUNCH_ID: launchId } });
|
|
1145
1202
|
const { command, args, shell } = resolveShimSpawn(process.platform, binary, [...launchArgs, ...rawArgs]);
|
|
1146
1203
|
return new Promise((resolve) => {
|
package/dist/lib/help.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export interface CommandGroup {
|
|
|
19
19
|
*/
|
|
20
20
|
export declare function registerCommandGroups(parent: Command, groups: readonly CommandGroup[]): void;
|
|
21
21
|
/** Examples + Notes blocks attached to a command via setHelpSections. */
|
|
22
|
-
interface HelpSections {
|
|
22
|
+
export interface HelpSections {
|
|
23
23
|
examples?: string;
|
|
24
24
|
notes?: string;
|
|
25
25
|
}
|
|
@@ -32,6 +32,7 @@ interface HelpSections {
|
|
|
32
32
|
* literals without babysitting whitespace.
|
|
33
33
|
*/
|
|
34
34
|
export declare function setHelpSections(cmd: Command, sections: HelpSections): void;
|
|
35
|
+
/** Read the workflow-first sections attached to a command for generated references. */
|
|
36
|
+
export declare function getHelpSections(cmd: Command): Readonly<HelpSections>;
|
|
35
37
|
/** Apply standardized help formatting to the root command and all subcommands. */
|
|
36
38
|
export declare function applyGlobalHelpConventions(root: Command): void;
|
|
37
|
-
export {};
|
package/dist/lib/help.js
CHANGED
|
@@ -19,6 +19,10 @@ const helpSectionRegistry = new WeakMap();
|
|
|
19
19
|
export function setHelpSections(cmd, sections) {
|
|
20
20
|
helpSectionRegistry.set(cmd, sections);
|
|
21
21
|
}
|
|
22
|
+
/** Read the workflow-first sections attached to a command for generated references. */
|
|
23
|
+
export function getHelpSections(cmd) {
|
|
24
|
+
return helpSectionRegistry.get(cmd) ?? {};
|
|
25
|
+
}
|
|
22
26
|
/** Strip a uniform leading indent from a block and trim surrounding blank lines. */
|
|
23
27
|
function dedent(body) {
|
|
24
28
|
const lines = body.replace(/^\n+/, '').replace(/\s+$/, '').split('\n');
|
|
@@ -11,38 +11,8 @@
|
|
|
11
11
|
import type { Host } from './types.js';
|
|
12
12
|
import { type HostTask } from './tasks.js';
|
|
13
13
|
import { type HostCredentials } from './credentials.js';
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
* caller gave no `--cwd`/`--remote-cwd`.
|
|
17
|
-
*
|
|
18
|
-
* Without this a `--host` run lands in the remote `$HOME`, so an agent launched
|
|
19
|
-
* from a repo starts with no project context and the user has to `cd` by hand.
|
|
20
|
-
* Only a cwd under the LOCAL home is mirrored — that is the part with a
|
|
21
|
-
* meaningful remote analogue (`~/src/x` re-roots onto the remote home). A path
|
|
22
|
-
* outside home returns undefined: `/opt/thing` on this box says nothing about
|
|
23
|
-
* the target's filesystem, so the run keeps the remote home.
|
|
24
|
-
*/
|
|
25
|
-
export declare function deriveMirroredCwd(localCwd: string): string | undefined;
|
|
26
|
-
/**
|
|
27
|
-
* Build a `cd <dir> && ` prefix that resolves on the REMOTE host.
|
|
28
|
-
*
|
|
29
|
-
* A `~`/`$HOME`-anchored path must resolve against the REMOTE user's home, not
|
|
30
|
-
* the local one (`/home/<me>` vs `/Users/<me>`). We emit an unquoted `"$HOME"`
|
|
31
|
-
* for that segment — the remote login shell expands it — and shell-quote the
|
|
32
|
-
* remainder. Any other path (absolute or relative) is quoted verbatim.
|
|
33
|
-
*
|
|
34
|
-
* `mirror` marks a directory the caller DERIVED from the local cwd rather than
|
|
35
|
-
* one the user asked for (see `deriveMirroredCwd`). The same repo checked out at
|
|
36
|
-
* the same home-relative path on both boxes is the common fleet layout, so
|
|
37
|
-
* mirroring lands the remote agent in the project instead of `$HOME`. It is a
|
|
38
|
-
* best-effort mirror by definition — the host may simply not have that checkout
|
|
39
|
-
* — so a missing directory falls back to the remote home instead of failing the
|
|
40
|
-
* run. An explicit `--cwd`/`--remote-cwd` is never mirrored: the user named that
|
|
41
|
-
* directory, so a missing one must surface as a `cd` error.
|
|
42
|
-
*/
|
|
43
|
-
export declare function remoteCdPrefix(remoteCwd?: string, opts?: {
|
|
44
|
-
mirror?: boolean;
|
|
45
|
-
}): string;
|
|
14
|
+
import { deriveMirroredCwd, homeRemainder, remoteCdPrefix } from '../project-root.js';
|
|
15
|
+
export { deriveMirroredCwd, homeRemainder, remoteCdPrefix };
|
|
46
16
|
/**
|
|
47
17
|
* Merge the resolved actor's provenance env UNDER a caller-supplied env, so every
|
|
48
18
|
* remote `agents …` invocation forwards `AGENTS_ACTOR*` / `GIT_*` across the SSH
|
|
@@ -19,72 +19,17 @@ import { saveTask, updateTask, terminalPatch } from './tasks.js';
|
|
|
19
19
|
import { followHostTask } from './progress.js';
|
|
20
20
|
import { wrapHostCommandWithCredentials } from './credentials.js';
|
|
21
21
|
import { hostKeyCheckingOpts } from '../devices/known-hosts.js';
|
|
22
|
-
import {
|
|
22
|
+
import { deriveMirroredCwd, homeRemainder, remoteCdPrefix } from '../project-root.js';
|
|
23
23
|
import { RUN_AUTO_KEYWORD, RUN_AUTO_HOST_RESOLVED_ENV } from '../types.js';
|
|
24
|
+
// The home-relative portability helpers live in project-root.js (the canonical
|
|
25
|
+
// home-relative conversion home), reused by the interactive `agents ssh` login
|
|
26
|
+
// shell builder (devices/connect.ts) as well as this dispatch layer. Re-exported
|
|
27
|
+
// here so existing `hosts/dispatch.js` importers and tests keep resolving them.
|
|
28
|
+
export { deriveMirroredCwd, homeRemainder, remoteCdPrefix };
|
|
24
29
|
// Use $HOME (not ~) so the path is correct whether or not it's quoted and
|
|
25
30
|
// regardless of the run's cwd. Task ids are 8 hex chars, so these paths are
|
|
26
31
|
// injection-safe to interpolate unquoted into remote commands.
|
|
27
32
|
const REMOTE_DIR = '$HOME/.agents/.cache/hosts';
|
|
28
|
-
/**
|
|
29
|
-
* If `p` is anchored at the home dir — a leading `~` or `$HOME` — return the
|
|
30
|
-
* remainder (no leading slash), else null. Callers that want a local-home
|
|
31
|
-
* absolute (`/Users/<me>/x`, from a shell-expanded `--cwd ~/x`) re-rooted at the
|
|
32
|
-
* remote home normalize it to `~/x` first (`toRemotePortable`); explicit
|
|
33
|
-
* `--remote-cwd` is left literal and so is never re-rooted here.
|
|
34
|
-
*/
|
|
35
|
-
function homeRemainder(p) {
|
|
36
|
-
if (p === '~' || p === '$HOME')
|
|
37
|
-
return '';
|
|
38
|
-
if (p.startsWith('~/'))
|
|
39
|
-
return p.slice(2);
|
|
40
|
-
if (p.startsWith('$HOME/'))
|
|
41
|
-
return p.slice(6);
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Derive the remote directory to mirror from the local cwd, for a host run the
|
|
46
|
-
* caller gave no `--cwd`/`--remote-cwd`.
|
|
47
|
-
*
|
|
48
|
-
* Without this a `--host` run lands in the remote `$HOME`, so an agent launched
|
|
49
|
-
* from a repo starts with no project context and the user has to `cd` by hand.
|
|
50
|
-
* Only a cwd under the LOCAL home is mirrored — that is the part with a
|
|
51
|
-
* meaningful remote analogue (`~/src/x` re-roots onto the remote home). A path
|
|
52
|
-
* outside home returns undefined: `/opt/thing` on this box says nothing about
|
|
53
|
-
* the target's filesystem, so the run keeps the remote home.
|
|
54
|
-
*/
|
|
55
|
-
export function deriveMirroredCwd(localCwd) {
|
|
56
|
-
const portable = toRemotePortable(localCwd);
|
|
57
|
-
return homeRemainder(portable) === null ? undefined : portable;
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Build a `cd <dir> && ` prefix that resolves on the REMOTE host.
|
|
61
|
-
*
|
|
62
|
-
* A `~`/`$HOME`-anchored path must resolve against the REMOTE user's home, not
|
|
63
|
-
* the local one (`/home/<me>` vs `/Users/<me>`). We emit an unquoted `"$HOME"`
|
|
64
|
-
* for that segment — the remote login shell expands it — and shell-quote the
|
|
65
|
-
* remainder. Any other path (absolute or relative) is quoted verbatim.
|
|
66
|
-
*
|
|
67
|
-
* `mirror` marks a directory the caller DERIVED from the local cwd rather than
|
|
68
|
-
* one the user asked for (see `deriveMirroredCwd`). The same repo checked out at
|
|
69
|
-
* the same home-relative path on both boxes is the common fleet layout, so
|
|
70
|
-
* mirroring lands the remote agent in the project instead of `$HOME`. It is a
|
|
71
|
-
* best-effort mirror by definition — the host may simply not have that checkout
|
|
72
|
-
* — so a missing directory falls back to the remote home instead of failing the
|
|
73
|
-
* run. An explicit `--cwd`/`--remote-cwd` is never mirrored: the user named that
|
|
74
|
-
* directory, so a missing one must surface as a `cd` error.
|
|
75
|
-
*/
|
|
76
|
-
export function remoteCdPrefix(remoteCwd, opts = {}) {
|
|
77
|
-
if (!remoteCwd)
|
|
78
|
-
return '';
|
|
79
|
-
const rest = homeRemainder(remoteCwd);
|
|
80
|
-
if (rest === '')
|
|
81
|
-
return 'cd "$HOME" && ';
|
|
82
|
-
if (rest !== null) {
|
|
83
|
-
const dir = `"$HOME"/${shellQuote(rest)}`;
|
|
84
|
-
return opts.mirror ? `{ cd ${dir} || cd "$HOME"; } && ` : `cd ${dir} && `;
|
|
85
|
-
}
|
|
86
|
-
return `cd ${shellQuote(remoteCwd)} && `;
|
|
87
|
-
}
|
|
88
33
|
/**
|
|
89
34
|
* Merge the resolved actor's provenance env UNDER a caller-supplied env, so every
|
|
90
35
|
* remote `agents …` invocation forwards `AGENTS_ACTOR*` / `GIT_*` across the SSH
|
|
@@ -74,3 +74,10 @@ export declare function findTaskBySessionId(sessionId: string): HostTask | null;
|
|
|
74
74
|
* reused across dispatches.
|
|
75
75
|
*/
|
|
76
76
|
export declare function findTaskByName(name: string): HostTask | null;
|
|
77
|
+
/**
|
|
78
|
+
* Resolve a host-task reference the way `agents hosts ps/logs/stop` do: a raw
|
|
79
|
+
* dispatch id first, then a `--name` handle, then the remote agent/session id.
|
|
80
|
+
* Shared so any other caller resolving "does this ref name a detached --device
|
|
81
|
+
* dispatch?" (e.g. `agents message`) uses the identical three-way lookup.
|
|
82
|
+
*/
|
|
83
|
+
export declare function resolveTaskRef(ref: string): HostTask | null;
|
package/dist/lib/hosts/tasks.js
CHANGED
|
@@ -101,3 +101,12 @@ export function findTaskByName(name) {
|
|
|
101
101
|
}
|
|
102
102
|
return null;
|
|
103
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Resolve a host-task reference the way `agents hosts ps/logs/stop` do: a raw
|
|
106
|
+
* dispatch id first, then a `--name` handle, then the remote agent/session id.
|
|
107
|
+
* Shared so any other caller resolving "does this ref name a detached --device
|
|
108
|
+
* dispatch?" (e.g. `agents message`) uses the identical three-way lookup.
|
|
109
|
+
*/
|
|
110
|
+
export function resolveTaskRef(ref) {
|
|
111
|
+
return loadTask(ref) ?? findTaskByName(ref) ?? findTaskBySessionId(ref);
|
|
112
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* NEVER guessed; the caller reports it. Pure (no I/O) so it is unit-testable.
|
|
6
6
|
*/
|
|
7
7
|
import type { ActiveSession } from './session/active.js';
|
|
8
|
+
import type { HostTask } from './hosts/tasks.js';
|
|
8
9
|
export type MessageResolution = {
|
|
9
10
|
kind: 'cloud';
|
|
10
11
|
id: string;
|
|
@@ -34,3 +35,29 @@ export declare function mailboxIdForActiveSession(s: ActiveSession): string | un
|
|
|
34
35
|
* cloud provider.
|
|
35
36
|
*/
|
|
36
37
|
export declare function resolveMessageTarget(target: string, sessions: ActiveSession[], isCloudTask: (id: string) => boolean): MessageResolution;
|
|
38
|
+
export type HostTaskRoute = {
|
|
39
|
+
kind: 'reroute';
|
|
40
|
+
remoteRef: string;
|
|
41
|
+
host: string;
|
|
42
|
+
} | {
|
|
43
|
+
kind: 'finished';
|
|
44
|
+
host: string;
|
|
45
|
+
status: string;
|
|
46
|
+
exitCode?: number;
|
|
47
|
+
} | {
|
|
48
|
+
kind: 'not-found';
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Decide how `agents message <target>` should handle a target that matched no
|
|
52
|
+
* local/cloud session (RUSH-2366 follow-up): `getActiveSessions()` has no
|
|
53
|
+
* visibility into a detached `agents run --device <host> --no-follow`
|
|
54
|
+
* dispatch, whose only local record is the `~/.agents/.cache/hosts/<id>.json`
|
|
55
|
+
* sidecar `agents hosts ps` reads. Pure — the caller does the actual lookup
|
|
56
|
+
* (`resolveTaskRef`) and I/O (the ssh reroute).
|
|
57
|
+
*
|
|
58
|
+
* `remoteRef` prefers the remote agent's OWN identity (its captured session id
|
|
59
|
+
* or `--name` handle) over the LOCAL dispatch-record id the user typed here —
|
|
60
|
+
* the live process on the host registers itself under the former, never the
|
|
61
|
+
* latter.
|
|
62
|
+
*/
|
|
63
|
+
export declare function decideHostTaskRoute(task: HostTask | null, target: string): HostTaskRoute;
|
|
@@ -43,3 +43,24 @@ export function resolveMessageTarget(target, sessions, isCloudTask) {
|
|
|
43
43
|
candidates: [...byId.entries()].map(([id, s]) => ({ id, label: labelFor(s) })),
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Decide how `agents message <target>` should handle a target that matched no
|
|
48
|
+
* local/cloud session (RUSH-2366 follow-up): `getActiveSessions()` has no
|
|
49
|
+
* visibility into a detached `agents run --device <host> --no-follow`
|
|
50
|
+
* dispatch, whose only local record is the `~/.agents/.cache/hosts/<id>.json`
|
|
51
|
+
* sidecar `agents hosts ps` reads. Pure — the caller does the actual lookup
|
|
52
|
+
* (`resolveTaskRef`) and I/O (the ssh reroute).
|
|
53
|
+
*
|
|
54
|
+
* `remoteRef` prefers the remote agent's OWN identity (its captured session id
|
|
55
|
+
* or `--name` handle) over the LOCAL dispatch-record id the user typed here —
|
|
56
|
+
* the live process on the host registers itself under the former, never the
|
|
57
|
+
* latter.
|
|
58
|
+
*/
|
|
59
|
+
export function decideHostTaskRoute(task, target) {
|
|
60
|
+
if (!task)
|
|
61
|
+
return { kind: 'not-found' };
|
|
62
|
+
if (task.status === 'running') {
|
|
63
|
+
return { kind: 'reroute', remoteRef: task.sessionId ?? task.name ?? target, host: task.host };
|
|
64
|
+
}
|
|
65
|
+
return { kind: 'finished', host: task.host, status: task.status, exitCode: task.exitCode };
|
|
66
|
+
}
|
package/dist/lib/mcp.d.ts
CHANGED
|
@@ -73,6 +73,16 @@ export declare function untrustProjectMcp(cwd?: string): boolean;
|
|
|
73
73
|
export declare function listMcpServerConfigs(cwd?: string, options?: {
|
|
74
74
|
enforceProjectTrust?: boolean;
|
|
75
75
|
}): InstalledMcpServer[];
|
|
76
|
+
/**
|
|
77
|
+
* Parse one config during a directory SCAN. `validateMcpYamlConfig` returns null
|
|
78
|
+
* for some malformed shapes but *throws* for others (`args` not a string array,
|
|
79
|
+
* `command`/`env` of the wrong type), and a scan must not be all-or-nothing: a
|
|
80
|
+
* single bad file under `<repo>/mcp/` took down the whole of
|
|
81
|
+
* `agents inspect <repo>` with an unhandled stack trace. Skip the file and name
|
|
82
|
+
* it, exactly as a null return is already skipped. Explicit single-file
|
|
83
|
+
* operations call `parseMcpServerConfig` directly and still throw loudly.
|
|
84
|
+
*/
|
|
85
|
+
export declare function parseMcpConfigForScan(filePath: string): McpYamlConfig | null;
|
|
76
86
|
/**
|
|
77
87
|
* Scan a repository for MCP server YAML configs.
|
|
78
88
|
* Looks under <repoPath>/mcp/*.yaml — same on-disk layout as ~/.agents/mcp/.
|
package/dist/lib/mcp.js
CHANGED
|
@@ -219,7 +219,7 @@ export function listMcpServerConfigs(cwd = process.cwd(), options = {}) {
|
|
|
219
219
|
if (!entry.name.endsWith('.yaml') && !entry.name.endsWith('.yml'))
|
|
220
220
|
continue;
|
|
221
221
|
const filePath = path.join(dir, entry.name);
|
|
222
|
-
const config =
|
|
222
|
+
const config = parseMcpConfigForScan(filePath);
|
|
223
223
|
if (config && !results.has(config.name)) {
|
|
224
224
|
results.set(config.name, {
|
|
225
225
|
name: config.name,
|
|
@@ -232,6 +232,25 @@ export function listMcpServerConfigs(cwd = process.cwd(), options = {}) {
|
|
|
232
232
|
}
|
|
233
233
|
return Array.from(results.values());
|
|
234
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* Parse one config during a directory SCAN. `validateMcpYamlConfig` returns null
|
|
237
|
+
* for some malformed shapes but *throws* for others (`args` not a string array,
|
|
238
|
+
* `command`/`env` of the wrong type), and a scan must not be all-or-nothing: a
|
|
239
|
+
* single bad file under `<repo>/mcp/` took down the whole of
|
|
240
|
+
* `agents inspect <repo>` with an unhandled stack trace. Skip the file and name
|
|
241
|
+
* it, exactly as a null return is already skipped. Explicit single-file
|
|
242
|
+
* operations call `parseMcpServerConfig` directly and still throw loudly.
|
|
243
|
+
*/
|
|
244
|
+
export function parseMcpConfigForScan(filePath) {
|
|
245
|
+
try {
|
|
246
|
+
return parseMcpServerConfig(filePath);
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
250
|
+
console.error(`agents-cli: skipping ${filePath} — ${reason}`);
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
235
254
|
/**
|
|
236
255
|
* Scan a repository for MCP server YAML configs.
|
|
237
256
|
* Looks under <repoPath>/mcp/*.yaml — same on-disk layout as ~/.agents/mcp/.
|
|
@@ -247,7 +266,7 @@ export function discoverMcpConfigsFromRepo(repoPath) {
|
|
|
247
266
|
if (!entry.name.endsWith('.yaml') && !entry.name.endsWith('.yml'))
|
|
248
267
|
continue;
|
|
249
268
|
const filePath = path.join(dir, entry.name);
|
|
250
|
-
const config =
|
|
269
|
+
const config = parseMcpConfigForScan(filePath);
|
|
251
270
|
if (config) {
|
|
252
271
|
results.push({ name: config.name, path: filePath, config, scope: 'user' });
|
|
253
272
|
}
|
|
Binary file
|
|
Binary file
|
package/dist/lib/migrate.d.ts
CHANGED
|
@@ -168,5 +168,16 @@ export declare function migrateWatchdogSentinelToRoutine(sentinelPath?: string,
|
|
|
168
168
|
* manually before proceeding. Exported for unit-testing with temp dirs.
|
|
169
169
|
*/
|
|
170
170
|
export declare function migrateCliDirToClis(agentsDirs: string[]): void;
|
|
171
|
+
/**
|
|
172
|
+
* Cursor's OAuth token historically lived only in the global
|
|
173
|
+
* ~/.config/cursor/auth.json, shared across every version home. Cursor runs now
|
|
174
|
+
* pin XDG_CONFIG_HOME per version home (real per-account isolation — see
|
|
175
|
+
* buildExecEnv), so the current login must be copied into the active account's
|
|
176
|
+
* version home or it would read as logged out after upgrade. The active account
|
|
177
|
+
* is the home the ~/.cursor symlink currently targets. Idempotent: skips when
|
|
178
|
+
* the home already has its own token, and a no-op for unmanaged Cursor installs
|
|
179
|
+
* (where ~/.cursor is a real dir, not a symlink into a version home).
|
|
180
|
+
*/
|
|
181
|
+
export declare function seedActiveCursorLoginPerVersion(): void;
|
|
171
182
|
/** Run all idempotent migrations. Safe to call multiple times. */
|
|
172
183
|
export declare function runMigration(): Promise<void>;
|
package/dist/lib/migrate.js
CHANGED
|
@@ -2250,6 +2250,43 @@ function migrateHumans() {
|
|
|
2250
2250
|
catch { /* best-effort — leave the old key if we can't rewrite */ }
|
|
2251
2251
|
}
|
|
2252
2252
|
}
|
|
2253
|
+
/**
|
|
2254
|
+
* Cursor's OAuth token historically lived only in the global
|
|
2255
|
+
* ~/.config/cursor/auth.json, shared across every version home. Cursor runs now
|
|
2256
|
+
* pin XDG_CONFIG_HOME per version home (real per-account isolation — see
|
|
2257
|
+
* buildExecEnv), so the current login must be copied into the active account's
|
|
2258
|
+
* version home or it would read as logged out after upgrade. The active account
|
|
2259
|
+
* is the home the ~/.cursor symlink currently targets. Idempotent: skips when
|
|
2260
|
+
* the home already has its own token, and a no-op for unmanaged Cursor installs
|
|
2261
|
+
* (where ~/.cursor is a real dir, not a symlink into a version home).
|
|
2262
|
+
*/
|
|
2263
|
+
export function seedActiveCursorLoginPerVersion() {
|
|
2264
|
+
const realHome = process.env.AGENTS_REAL_HOME || os.homedir();
|
|
2265
|
+
const globalAuth = path.join(realHome, '.config', 'cursor', 'auth.json');
|
|
2266
|
+
let versionHome;
|
|
2267
|
+
try {
|
|
2268
|
+
if (!fs.existsSync(globalAuth))
|
|
2269
|
+
return;
|
|
2270
|
+
// ~/.cursor -> .../versions/cursor/<version>/home/.cursor ; the version home
|
|
2271
|
+
// is that link's parent directory.
|
|
2272
|
+
const link = fs.readlinkSync(path.join(realHome, '.cursor'));
|
|
2273
|
+
const resolved = path.isAbsolute(link) ? link : path.resolve(realHome, link);
|
|
2274
|
+
versionHome = path.dirname(resolved);
|
|
2275
|
+
}
|
|
2276
|
+
catch {
|
|
2277
|
+
return; // not a symlink (unmanaged install) or unreadable — nothing to seed
|
|
2278
|
+
}
|
|
2279
|
+
if (!versionHome.includes(path.join('versions', 'cursor')))
|
|
2280
|
+
return;
|
|
2281
|
+
const dest = path.join(versionHome, '.config', 'cursor', 'auth.json');
|
|
2282
|
+
try {
|
|
2283
|
+
if (fs.existsSync(dest))
|
|
2284
|
+
return;
|
|
2285
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
2286
|
+
fs.copyFileSync(globalAuth, dest);
|
|
2287
|
+
}
|
|
2288
|
+
catch { /* best-effort — a failed seed just means one re-login */ }
|
|
2289
|
+
}
|
|
2253
2290
|
/** Run all idempotent migrations. Safe to call multiple times. */
|
|
2254
2291
|
export async function runMigration() {
|
|
2255
2292
|
// MUST run first: every other migrator reads SYSTEM_DIR (the new path).
|
|
@@ -2266,6 +2303,9 @@ export async function runMigration() {
|
|
|
2266
2303
|
migratePromptcutsIntoHooks();
|
|
2267
2304
|
migrateSystemVersionsToUser();
|
|
2268
2305
|
mergeOverlappingVersionHomes();
|
|
2306
|
+
// Cursor runs now isolate the login per version home; preserve the current
|
|
2307
|
+
// login by seeding the active home's token from the legacy global copy.
|
|
2308
|
+
seedActiveCursorLoginPerVersion();
|
|
2269
2309
|
migrateRunsIntoRoutines();
|
|
2270
2310
|
migrateTrashToHidden();
|
|
2271
2311
|
migrateBackupsToHidden();
|
|
@@ -24,6 +24,53 @@ export declare function expandLocalHome(p: string): string;
|
|
|
24
24
|
* it is a literal remote path by contract.
|
|
25
25
|
*/
|
|
26
26
|
export declare function toRemotePortable(p: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* If `p` is anchored at the home dir — a leading `~` or `$HOME` — return the
|
|
29
|
+
* remainder (no leading slash), else null. Callers that want a local-home
|
|
30
|
+
* absolute (`/Users/<me>/x`, from a shell-expanded `--cwd ~/x`) re-rooted at the
|
|
31
|
+
* remote home normalize it to `~/x` first (`toRemotePortable`); explicit
|
|
32
|
+
* `--remote-cwd` is left literal and so is never re-rooted here.
|
|
33
|
+
*
|
|
34
|
+
* The canonical home-anchor stripper — shared by `remoteCdPrefix`,
|
|
35
|
+
* `deriveMirroredCwd`, and the interactive-login shell builder
|
|
36
|
+
* (`devices/connect.ts`), so there is exactly one notion of "the part below the
|
|
37
|
+
* home dir".
|
|
38
|
+
*/
|
|
39
|
+
export declare function homeRemainder(p: string): string | null;
|
|
40
|
+
/**
|
|
41
|
+
* Derive the remote directory to mirror from the local cwd, for a host run the
|
|
42
|
+
* caller gave no `--cwd`/`--remote-cwd` (and for an interactive `agents ssh`
|
|
43
|
+
* login with no command).
|
|
44
|
+
*
|
|
45
|
+
* Without this a `--host` run — or an `agents ssh <device>` login — lands in the
|
|
46
|
+
* remote `$HOME`, so an agent launched from a repo starts with no project
|
|
47
|
+
* context and the user has to `cd` by hand. Only a cwd under the LOCAL home is
|
|
48
|
+
* mirrored — that is the part with a meaningful remote analogue (`~/src/x`
|
|
49
|
+
* re-roots onto the remote home). A path outside home returns undefined:
|
|
50
|
+
* `/opt/thing` on this box says nothing about the target's filesystem, so the
|
|
51
|
+
* run keeps the remote home.
|
|
52
|
+
*/
|
|
53
|
+
export declare function deriveMirroredCwd(localCwd: string): string | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* Build a `cd <dir> && ` prefix that resolves on the REMOTE host.
|
|
56
|
+
*
|
|
57
|
+
* A `~`/`$HOME`-anchored path must resolve against the REMOTE user's home, not
|
|
58
|
+
* the local one (`/home/<me>` vs `/Users/<me>`). We emit an unquoted `"$HOME"`
|
|
59
|
+
* for that segment — the remote login shell expands it — and shell-quote the
|
|
60
|
+
* remainder. Any other path (absolute or relative) is quoted verbatim.
|
|
61
|
+
*
|
|
62
|
+
* `mirror` marks a directory the caller DERIVED from the local cwd rather than
|
|
63
|
+
* one the user asked for (see `deriveMirroredCwd`). The same repo checked out at
|
|
64
|
+
* the same home-relative path on both boxes is the common fleet layout, so
|
|
65
|
+
* mirroring lands the remote agent in the project instead of `$HOME`. It is a
|
|
66
|
+
* best-effort mirror by definition — the host may simply not have that checkout
|
|
67
|
+
* — so a missing directory falls back to the remote home instead of failing the
|
|
68
|
+
* run. An explicit `--cwd`/`--remote-cwd` is never mirrored: the user named that
|
|
69
|
+
* directory, so a missing one must surface as a `cd` error.
|
|
70
|
+
*/
|
|
71
|
+
export declare function remoteCdPrefix(remoteCwd?: string, opts?: {
|
|
72
|
+
mirror?: boolean;
|
|
73
|
+
}): string;
|
|
27
74
|
/** The configured projects root (home-relative or absolute), or undefined when unset. */
|
|
28
75
|
export declare function getProjectRoot(): string | undefined;
|
|
29
76
|
/** Set (override) the cached projects root. Stored home-relative when under `$HOME`. */
|
package/dist/lib/project-root.js
CHANGED
|
@@ -18,6 +18,7 @@ import { readMeta, updateMeta } from './state.js';
|
|
|
18
18
|
import { getMainRepoRoot } from './git.js';
|
|
19
19
|
import { toPosix } from './platform/index.js';
|
|
20
20
|
import { loadProjectDef, resolveDefinedProjectPath } from './projects.js';
|
|
21
|
+
import { shellQuote } from './ssh-exec.js';
|
|
21
22
|
const HOME = process.env.HOME ?? os.homedir();
|
|
22
23
|
/** Rewrite an absolute path under the local home to a `~/`-relative string; pass others through. */
|
|
23
24
|
export function toHomeRelative(abs) {
|
|
@@ -53,6 +54,73 @@ export function toRemotePortable(p) {
|
|
|
53
54
|
return toHomeRelative(p);
|
|
54
55
|
return p;
|
|
55
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* If `p` is anchored at the home dir — a leading `~` or `$HOME` — return the
|
|
59
|
+
* remainder (no leading slash), else null. Callers that want a local-home
|
|
60
|
+
* absolute (`/Users/<me>/x`, from a shell-expanded `--cwd ~/x`) re-rooted at the
|
|
61
|
+
* remote home normalize it to `~/x` first (`toRemotePortable`); explicit
|
|
62
|
+
* `--remote-cwd` is left literal and so is never re-rooted here.
|
|
63
|
+
*
|
|
64
|
+
* The canonical home-anchor stripper — shared by `remoteCdPrefix`,
|
|
65
|
+
* `deriveMirroredCwd`, and the interactive-login shell builder
|
|
66
|
+
* (`devices/connect.ts`), so there is exactly one notion of "the part below the
|
|
67
|
+
* home dir".
|
|
68
|
+
*/
|
|
69
|
+
export function homeRemainder(p) {
|
|
70
|
+
if (p === '~' || p === '$HOME')
|
|
71
|
+
return '';
|
|
72
|
+
if (p.startsWith('~/'))
|
|
73
|
+
return p.slice(2);
|
|
74
|
+
if (p.startsWith('$HOME/'))
|
|
75
|
+
return p.slice(6);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Derive the remote directory to mirror from the local cwd, for a host run the
|
|
80
|
+
* caller gave no `--cwd`/`--remote-cwd` (and for an interactive `agents ssh`
|
|
81
|
+
* login with no command).
|
|
82
|
+
*
|
|
83
|
+
* Without this a `--host` run — or an `agents ssh <device>` login — lands in the
|
|
84
|
+
* remote `$HOME`, so an agent launched from a repo starts with no project
|
|
85
|
+
* context and the user has to `cd` by hand. Only a cwd under the LOCAL home is
|
|
86
|
+
* mirrored — that is the part with a meaningful remote analogue (`~/src/x`
|
|
87
|
+
* re-roots onto the remote home). A path outside home returns undefined:
|
|
88
|
+
* `/opt/thing` on this box says nothing about the target's filesystem, so the
|
|
89
|
+
* run keeps the remote home.
|
|
90
|
+
*/
|
|
91
|
+
export function deriveMirroredCwd(localCwd) {
|
|
92
|
+
const portable = toRemotePortable(localCwd);
|
|
93
|
+
return homeRemainder(portable) === null ? undefined : portable;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Build a `cd <dir> && ` prefix that resolves on the REMOTE host.
|
|
97
|
+
*
|
|
98
|
+
* A `~`/`$HOME`-anchored path must resolve against the REMOTE user's home, not
|
|
99
|
+
* the local one (`/home/<me>` vs `/Users/<me>`). We emit an unquoted `"$HOME"`
|
|
100
|
+
* for that segment — the remote login shell expands it — and shell-quote the
|
|
101
|
+
* remainder. Any other path (absolute or relative) is quoted verbatim.
|
|
102
|
+
*
|
|
103
|
+
* `mirror` marks a directory the caller DERIVED from the local cwd rather than
|
|
104
|
+
* one the user asked for (see `deriveMirroredCwd`). The same repo checked out at
|
|
105
|
+
* the same home-relative path on both boxes is the common fleet layout, so
|
|
106
|
+
* mirroring lands the remote agent in the project instead of `$HOME`. It is a
|
|
107
|
+
* best-effort mirror by definition — the host may simply not have that checkout
|
|
108
|
+
* — so a missing directory falls back to the remote home instead of failing the
|
|
109
|
+
* run. An explicit `--cwd`/`--remote-cwd` is never mirrored: the user named that
|
|
110
|
+
* directory, so a missing one must surface as a `cd` error.
|
|
111
|
+
*/
|
|
112
|
+
export function remoteCdPrefix(remoteCwd, opts = {}) {
|
|
113
|
+
if (!remoteCwd)
|
|
114
|
+
return '';
|
|
115
|
+
const rest = homeRemainder(remoteCwd);
|
|
116
|
+
if (rest === '')
|
|
117
|
+
return 'cd "$HOME" && ';
|
|
118
|
+
if (rest !== null) {
|
|
119
|
+
const dir = `"$HOME"/${shellQuote(rest)}`;
|
|
120
|
+
return opts.mirror ? `{ cd ${dir} || cd "$HOME"; } && ` : `cd ${dir} && `;
|
|
121
|
+
}
|
|
122
|
+
return `cd ${shellQuote(remoteCwd)} && `;
|
|
123
|
+
}
|
|
56
124
|
/** The configured projects root (home-relative or absolute), or undefined when unset. */
|
|
57
125
|
export function getProjectRoot() {
|
|
58
126
|
return readMeta().projectRoot;
|
|
Binary file
|
|
Binary file
|
|
@@ -263,7 +263,7 @@ export declare function brokerPidAlive(): boolean;
|
|
|
263
263
|
export declare function runSecretsAgent(opts?: {
|
|
264
264
|
service?: boolean;
|
|
265
265
|
}): Promise<{
|
|
266
|
-
close(): void
|
|
266
|
+
close(): void | Promise<void>;
|
|
267
267
|
} | null>;
|
|
268
268
|
/**
|
|
269
269
|
* Host the secrets broker inside the always-on daemon (#416).
|
|
@@ -284,8 +284,15 @@ export declare function runSecretsAgent(opts?: {
|
|
|
284
284
|
* or null off-darwin (nothing to broker without biometry).
|
|
285
285
|
*/
|
|
286
286
|
export declare function startHostedBroker(): Promise<{
|
|
287
|
-
close(): void
|
|
287
|
+
close(): void | Promise<void>;
|
|
288
288
|
} | null>;
|
|
289
|
+
/**
|
|
290
|
+
* Call Node's net.Server.close() and wait for the 'close' event (or a bounded
|
|
291
|
+
* timeout). Without this, close() returned while the listen socket could still
|
|
292
|
+
* be held — a successor bind could race EADDRINUSE against a half-closed server
|
|
293
|
+
* (RUSH-2421). Pure side-effect helper; never throws.
|
|
294
|
+
*/
|
|
295
|
+
export declare function closeServerBounded(server: net.Server, timeoutMs?: number): Promise<void>;
|
|
289
296
|
/** True if a broker socket exists at all. Cheap; gates the sync read so the
|
|
290
297
|
* never-unlocked path stays a single stat. */
|
|
291
298
|
export declare function agentSocketExists(): boolean;
|