@phnx-labs/agents-cli 1.20.38 → 1.20.40
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/dist/commands/beta.js +1 -0
- package/dist/commands/sessions-sync.js +23 -15
- package/dist/commands/sessions.d.ts +18 -0
- package/dist/commands/sessions.js +49 -9
- package/dist/lib/beta.d.ts +1 -1
- package/dist/lib/beta.js +1 -1
- package/dist/lib/daemon.js +8 -4
- package/dist/lib/session/sync/config.d.ts +0 -13
- package/dist/lib/session/sync/config.js +0 -56
- package/dist/lib/shims.d.ts +4 -1
- package/dist/lib/shims.js +23 -3
- package/dist/lib/sync-umbrella.d.ts +1 -1
- package/dist/lib/sync-umbrella.js +7 -5
- package/dist/lib/types.d.ts +1 -1
- package/package.json +1 -1
package/dist/commands/beta.js
CHANGED
|
@@ -3,6 +3,7 @@ import { ALL_BETA_FEATURES, getBetaConfigLocation, getEnabledBetaFeatures, setBe
|
|
|
3
3
|
const BETA_DESCRIPTIONS = {
|
|
4
4
|
drive: 'Google Drive integration for reading and writing files',
|
|
5
5
|
factory: 'Cloud-based agent dispatch via Rush Factory',
|
|
6
|
+
'session-sync': 'Cross-machine session transcript sync via R2 (daemon push/pull)',
|
|
6
7
|
};
|
|
7
8
|
function parseFeatures(values) {
|
|
8
9
|
const valid = new Set(ALL_BETA_FEATURES);
|
|
@@ -5,31 +5,39 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import chalk from 'chalk';
|
|
7
7
|
import { setHelpSections } from '../lib/help.js';
|
|
8
|
-
import { isSyncConfigured,
|
|
8
|
+
import { isSyncConfigured, SYNC_BUNDLE } from '../lib/session/sync/config.js';
|
|
9
|
+
import { isBetaEnabled, setBetaEnabled, betaEnableHint } from '../lib/beta.js';
|
|
9
10
|
import { syncSessions } from '../lib/session/sync/sync.js';
|
|
11
|
+
/** The daemon's automatic session sync is gated by this beta feature. */
|
|
12
|
+
const SYNC_BETA = 'session-sync';
|
|
10
13
|
export async function runSessionsSync(options) {
|
|
11
|
-
// Toggle / status
|
|
14
|
+
// Toggle / status delegate to the `session-sync` beta feature — the single
|
|
15
|
+
// source of truth for whether the daemon auto-syncs (opt-in, off by default).
|
|
16
|
+
// These short-circuit before any network cycle.
|
|
12
17
|
if (options.disable) {
|
|
13
|
-
|
|
18
|
+
setBetaEnabled([SYNC_BETA], false);
|
|
14
19
|
console.log(chalk.yellow('Automatic session sync disabled') +
|
|
15
|
-
chalk.dim(' — the daemon stops pushing/pulling within ~90s.
|
|
20
|
+
chalk.dim(' — the daemon stops pushing/pulling within ~90s. (Same as: agents beta disable session-sync)'));
|
|
16
21
|
return;
|
|
17
22
|
}
|
|
18
23
|
if (options.enable) {
|
|
19
|
-
|
|
20
|
-
console.log(chalk.green('Automatic session sync enabled') +
|
|
24
|
+
setBetaEnabled([SYNC_BETA], true);
|
|
25
|
+
console.log(chalk.green('Automatic session sync enabled') +
|
|
26
|
+
chalk.dim(' — the daemon resumes on its next cycle. (Same as: agents beta enable session-sync)'));
|
|
21
27
|
return;
|
|
22
28
|
}
|
|
23
29
|
if (options.status) {
|
|
24
|
-
const enabled =
|
|
30
|
+
const enabled = isBetaEnabled(SYNC_BETA);
|
|
25
31
|
const configured = isSyncConfigured();
|
|
26
32
|
if (options.json) {
|
|
27
33
|
console.log(JSON.stringify({ enabled, configured }, null, 2));
|
|
28
34
|
}
|
|
29
35
|
else {
|
|
30
|
-
console.log(`automatic sync: ${enabled ? chalk.green('enabled') : chalk.yellow('disabled')}` +
|
|
36
|
+
console.log(`automatic sync: ${enabled ? chalk.green('enabled (beta)') : chalk.yellow('disabled')}` +
|
|
31
37
|
chalk.dim(' · ') +
|
|
32
38
|
`credentials: ${configured ? chalk.green('configured') : chalk.yellow(`missing (${SYNC_BUNDLE})`)}`);
|
|
39
|
+
if (!enabled)
|
|
40
|
+
console.log(chalk.dim(` ${betaEnableHint(SYNC_BETA)}`));
|
|
33
41
|
}
|
|
34
42
|
return;
|
|
35
43
|
}
|
|
@@ -77,9 +85,9 @@ export function registerSessionsSyncCommand(sessionsCmd) {
|
|
|
77
85
|
.description('Sync session transcripts across machines via R2 (CRDT merge). Claude and Codex.')
|
|
78
86
|
.option('-v, --verbose', 'Log each pushed and pulled session')
|
|
79
87
|
.option('--json', 'Output the sync result as JSON')
|
|
80
|
-
.option('--enable', '
|
|
81
|
-
.option('--disable', '
|
|
82
|
-
.option('--status', 'Show whether automatic sync is
|
|
88
|
+
.option('--enable', 'Opt in to automatic background sync (beta; alias for: agents beta enable session-sync)')
|
|
89
|
+
.option('--disable', 'Opt out of automatic background sync (alias for: agents beta disable session-sync)')
|
|
90
|
+
.option('--status', 'Show whether automatic sync is opted-in (beta) and configured');
|
|
83
91
|
setHelpSections(syncCmd, {
|
|
84
92
|
examples: `
|
|
85
93
|
# One sync cycle (push local changes, pull + merge from other machines)
|
|
@@ -97,11 +105,11 @@ export function registerSessionsSyncCommand(sessionsCmd) {
|
|
|
97
105
|
notes: `
|
|
98
106
|
- Credentials come from the '${SYNC_BUNDLE}' secrets bundle (R2 S3 API, read+write).
|
|
99
107
|
- Each machine writes only its own prefix; conflicts are impossible by construction.
|
|
100
|
-
- The daemon runs this automatically (~90s); this command forces an immediate cycle.
|
|
101
108
|
- Sessions present locally always win; synced-in copies fill in other machines' sessions.
|
|
102
|
-
-
|
|
103
|
-
|
|
104
|
-
|
|
109
|
+
- Automatic background sync is an opt-in BETA feature, OFF by default. The daemon only
|
|
110
|
+
syncs (~90s) once you opt in via 'agents beta enable session-sync' (or the --enable
|
|
111
|
+
alias here). A bare 'agents sessions sync' always forces a manual one-shot cycle
|
|
112
|
+
regardless of the beta opt-in.
|
|
105
113
|
`,
|
|
106
114
|
});
|
|
107
115
|
// `--json` is also declared on the parent `sessions` command, so a bare
|
|
@@ -98,6 +98,24 @@ export declare function dedupeByMachineSession(sessions: ActiveSession[]): Activ
|
|
|
98
98
|
* `localMachine` is injected so the ordering is testable without os.hostname().
|
|
99
99
|
*/
|
|
100
100
|
export declare function mergeLocalFirst(sessions: SessionMeta[], localMachine: string): SessionMeta[];
|
|
101
|
+
/**
|
|
102
|
+
* Whether the local machine's sessions belong in an `--active` view. Local is
|
|
103
|
+
* included by default; an explicit `--host`/`--device` list scopes the view to
|
|
104
|
+
* exactly those machines, so local is dropped unless it is itself named (by
|
|
105
|
+
* alias or `user@host`, matched on the normalized machine id). Exported for
|
|
106
|
+
* unit testing without touching SSH or the live process table.
|
|
107
|
+
*/
|
|
108
|
+
export declare function shouldIncludeLocal(hosts: string[] | undefined, self: string): boolean;
|
|
109
|
+
/**
|
|
110
|
+
* The peers to dial for an `--active` view. No `--host` → `undefined`, which
|
|
111
|
+
* tells `gatherRemoteActive` to sweep the registered online devices. An
|
|
112
|
+
* explicit list → exactly those, minus this machine (its sessions come from the
|
|
113
|
+
* local seed, so dialing self would be a wasted SSH and a spurious "unreachable"
|
|
114
|
+
* note). Returns `[]` when the only named host is self — the caller then skips
|
|
115
|
+
* the remote fan-out entirely rather than letting `[]` trigger the sweep.
|
|
116
|
+
* Exported for unit testing.
|
|
117
|
+
*/
|
|
118
|
+
export declare function remoteHostsToDial(hosts: string[] | undefined, self: string): string[] | undefined;
|
|
101
119
|
/**
|
|
102
120
|
* Group key for the overview: prefer the indexed project name; else fold the cwd
|
|
103
121
|
* to its repo — a worktree (`.../<repo>/.agents/worktrees/<slug>`) folds to the
|
|
@@ -601,25 +601,65 @@ async function enrichLocalLocators(local) {
|
|
|
601
601
|
}
|
|
602
602
|
catch { /* non-fatal */ }
|
|
603
603
|
}
|
|
604
|
+
/** Normalize a `--host`/`--device` token (`alias`, `user@host`, `host.domain`)
|
|
605
|
+
* to the machine id the fan-out and registry key off. */
|
|
606
|
+
function hostToken(h) {
|
|
607
|
+
return normalizeHost(h.split('@').pop() || h);
|
|
608
|
+
}
|
|
604
609
|
/**
|
|
605
|
-
*
|
|
606
|
-
*
|
|
607
|
-
* machines
|
|
608
|
-
*
|
|
609
|
-
*
|
|
610
|
+
* Whether the local machine's sessions belong in an `--active` view. Local is
|
|
611
|
+
* included by default; an explicit `--host`/`--device` list scopes the view to
|
|
612
|
+
* exactly those machines, so local is dropped unless it is itself named (by
|
|
613
|
+
* alias or `user@host`, matched on the normalized machine id). Exported for
|
|
614
|
+
* unit testing without touching SSH or the live process table.
|
|
615
|
+
*/
|
|
616
|
+
export function shouldIncludeLocal(hosts, self) {
|
|
617
|
+
if (!hosts || hosts.length === 0)
|
|
618
|
+
return true;
|
|
619
|
+
return hosts.some(h => hostToken(h) === self);
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* The peers to dial for an `--active` view. No `--host` → `undefined`, which
|
|
623
|
+
* tells `gatherRemoteActive` to sweep the registered online devices. An
|
|
624
|
+
* explicit list → exactly those, minus this machine (its sessions come from the
|
|
625
|
+
* local seed, so dialing self would be a wasted SSH and a spurious "unreachable"
|
|
626
|
+
* note). Returns `[]` when the only named host is self — the caller then skips
|
|
627
|
+
* the remote fan-out entirely rather than letting `[]` trigger the sweep.
|
|
628
|
+
* Exported for unit testing.
|
|
629
|
+
*/
|
|
630
|
+
export function remoteHostsToDial(hosts, self) {
|
|
631
|
+
if (!hosts || hosts.length === 0)
|
|
632
|
+
return undefined;
|
|
633
|
+
return hosts.filter(h => hostToken(h) !== self);
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Render the unified active-session view, grouped by machine. With no `--host`,
|
|
637
|
+
* local sessions come from `getActiveSessions()` and (unless `--local`) the
|
|
638
|
+
* registered online devices from `ag devices` are folded in over SSH. An
|
|
639
|
+
* explicit `--host`/`--device` list SCOPES the view to exactly those machines —
|
|
640
|
+
* the local machine is included only when it is itself named — so `--host` is a
|
|
641
|
+
* filter, not an addition (matching the non-`--active` listing path). A tip is
|
|
642
|
+
* shown when there are no other machines to include.
|
|
610
643
|
*/
|
|
611
644
|
async function renderActiveSessions(asJson, waitingOnly = false, opts = {}) {
|
|
612
645
|
const self = machineId();
|
|
613
|
-
|
|
646
|
+
// An explicit --host/--device list scopes the view: seed local sessions only
|
|
647
|
+
// when no hosts are named, or when this machine is one of the named targets.
|
|
648
|
+
const local = shouldIncludeLocal(opts.hosts, self) ? await getActiveSessions() : [];
|
|
614
649
|
for (const s of local)
|
|
615
650
|
if (!s.machine)
|
|
616
651
|
s.machine = self;
|
|
617
652
|
let remoteDeviceCount = 0;
|
|
618
653
|
let merged = local;
|
|
619
654
|
if (!opts.local) {
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
|
|
655
|
+
const remoteHosts = remoteHostsToDial(opts.hosts, self);
|
|
656
|
+
// An explicit list naming only self leaves nothing remote to dial — skip the
|
|
657
|
+
// fan-out rather than let an empty list fall through to the device sweep.
|
|
658
|
+
if (!opts.hosts?.length || (remoteHosts && remoteHosts.length > 0)) {
|
|
659
|
+
const remote = await gatherRemoteActive(remoteHosts);
|
|
660
|
+
remoteDeviceCount = remote.deviceCount;
|
|
661
|
+
merged = dedupeByMachineSession([...local, ...remote.sessions]);
|
|
662
|
+
}
|
|
623
663
|
}
|
|
624
664
|
// --waiting: only sessions blocked on the user. Exits non-zero when any are
|
|
625
665
|
// present so a supervising agent or hook can poll it as a gate.
|
package/dist/lib/beta.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* checks.
|
|
9
9
|
*/
|
|
10
10
|
import type { BetaFeatureName } from './types.js';
|
|
11
|
-
export declare const ALL_BETA_FEATURES: readonly ["drive", "factory"];
|
|
11
|
+
export declare const ALL_BETA_FEATURES: readonly ["drive", "factory", "session-sync"];
|
|
12
12
|
export declare function getEnabledBetaFeatures(): BetaFeatureName[];
|
|
13
13
|
export declare function isBetaEnabled(feature: BetaFeatureName): boolean;
|
|
14
14
|
export declare function getBetaConfigLocation(): {
|
package/dist/lib/beta.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import { getAgentsDir, getOptionalUserAgentsDir, readMeta, writeMeta } from './state.js';
|
|
12
12
|
import { readManifest, writeManifest } from './manifest.js';
|
|
13
|
-
export const ALL_BETA_FEATURES = ['drive', 'factory'];
|
|
13
|
+
export const ALL_BETA_FEATURES = ['drive', 'factory', 'session-sync'];
|
|
14
14
|
function isBetaFeatureName(value) {
|
|
15
15
|
return typeof value === 'string' && ALL_BETA_FEATURES.includes(value);
|
|
16
16
|
}
|
package/dist/lib/daemon.js
CHANGED
|
@@ -343,10 +343,14 @@ export async function runDaemon() {
|
|
|
343
343
|
return;
|
|
344
344
|
syncing = true;
|
|
345
345
|
try {
|
|
346
|
-
const {
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
|
|
346
|
+
const { isBetaEnabled } = await import('./beta.js');
|
|
347
|
+
// Off by default: session sync is an opt-in beta feature. Check the beta
|
|
348
|
+
// flag FIRST so a machine that hasn't opted in skips the keychain read
|
|
349
|
+
// (isSyncConfigured) entirely, not just the network cycle.
|
|
350
|
+
if (!isBetaEnabled('session-sync'))
|
|
351
|
+
return;
|
|
352
|
+
const { isSyncConfigured } = await import('./session/sync/config.js');
|
|
353
|
+
if (!isSyncConfigured())
|
|
350
354
|
return;
|
|
351
355
|
const { syncSessions } = await import('./session/sync/sync.js');
|
|
352
356
|
const r = await syncSessions();
|
|
@@ -5,19 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
/** Secrets bundle holding the R2 credentials. */
|
|
7
7
|
export declare const SYNC_BUNDLE = "r2.backups";
|
|
8
|
-
/** Env var that overrides the persisted enable flag (on/off/true/false/1/0/yes/no). */
|
|
9
|
-
export declare const SYNC_ENABLED_ENV = "AGENTS_SESSIONS_SYNC";
|
|
10
|
-
/** Durable, machine-local path holding the sync enable flag. */
|
|
11
|
-
export declare function syncStateFilePath(): string;
|
|
12
|
-
/**
|
|
13
|
-
* Whether automatic session sync is enabled on this machine. Defaults to true;
|
|
14
|
-
* an unrecognized env value falls through to the file; an absent/unreadable file
|
|
15
|
-
* falls through to the default. Read fresh every call (no memoization) so a
|
|
16
|
-
* `--disable` takes effect on the daemon's next ~90s cycle without a restart.
|
|
17
|
-
*/
|
|
18
|
-
export declare function isSyncEnabled(): boolean;
|
|
19
|
-
/** Persist the machine-local sync enable flag (durable across cache wipes). */
|
|
20
|
-
export declare function setSyncEnabled(enabled: boolean): void;
|
|
21
8
|
export interface R2Config {
|
|
22
9
|
accountId: string;
|
|
23
10
|
bucket: string;
|
|
@@ -3,65 +3,9 @@
|
|
|
3
3
|
* machine's stable identity. Credentials come from the `r2.backups` secrets
|
|
4
4
|
* bundle (OS keychain on macOS, libsecret on Linux) — never from env or disk.
|
|
5
5
|
*/
|
|
6
|
-
import * as fs from 'fs';
|
|
7
|
-
import * as path from 'path';
|
|
8
6
|
import { readAndResolveBundleEnv } from '../../secrets/bundles.js';
|
|
9
|
-
import { getHistoryDir } from '../../state.js';
|
|
10
7
|
/** Secrets bundle holding the R2 credentials. */
|
|
11
8
|
export const SYNC_BUNDLE = 'r2.backups';
|
|
12
|
-
// ── Enable / disable switch ─────────────────────────────────────────────────
|
|
13
|
-
// Whether the daemon's automatic cross-machine sync (and `agents sync
|
|
14
|
-
// --sessions`) may run on THIS machine. Independent of credential presence
|
|
15
|
-
// (isSyncConfigured): a machine can hold valid R2 creds yet still opt out of the
|
|
16
|
-
// background push/pull — e.g. when on-demand `agents sessions --host` is
|
|
17
|
-
// preferred over the ad-hoc R2 mirror. Manual `agents sessions sync` is an
|
|
18
|
-
// explicit user action and is deliberately NOT gated by this switch.
|
|
19
|
-
//
|
|
20
|
-
// Resolution order: the AGENTS_SESSIONS_SYNC env var (a recognized on/off value
|
|
21
|
-
// wins outright, for ad-hoc overrides and tests), then a durable machine-local
|
|
22
|
-
// flag file, then the default (enabled). The flag lives in the durable
|
|
23
|
-
// ~/.agents/.history tree — NOT .cache — so a cache wipe can never silently
|
|
24
|
-
// re-enable a sync the operator turned off.
|
|
25
|
-
/** Env var that overrides the persisted enable flag (on/off/true/false/1/0/yes/no). */
|
|
26
|
-
export const SYNC_ENABLED_ENV = 'AGENTS_SESSIONS_SYNC';
|
|
27
|
-
const SYNC_ENABLED_FILE = 'sessions-sync.json';
|
|
28
|
-
const OFF_VALUES = new Set(['0', 'off', 'false', 'no', 'disabled']);
|
|
29
|
-
const ON_VALUES = new Set(['1', 'on', 'true', 'yes', 'enabled']);
|
|
30
|
-
/** Durable, machine-local path holding the sync enable flag. */
|
|
31
|
-
export function syncStateFilePath() {
|
|
32
|
-
return path.join(getHistoryDir(), SYNC_ENABLED_FILE);
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Whether automatic session sync is enabled on this machine. Defaults to true;
|
|
36
|
-
* an unrecognized env value falls through to the file; an absent/unreadable file
|
|
37
|
-
* falls through to the default. Read fresh every call (no memoization) so a
|
|
38
|
-
* `--disable` takes effect on the daemon's next ~90s cycle without a restart.
|
|
39
|
-
*/
|
|
40
|
-
export function isSyncEnabled() {
|
|
41
|
-
const envRaw = process.env[SYNC_ENABLED_ENV]?.trim().toLowerCase();
|
|
42
|
-
if (envRaw) {
|
|
43
|
-
if (OFF_VALUES.has(envRaw))
|
|
44
|
-
return false;
|
|
45
|
-
if (ON_VALUES.has(envRaw))
|
|
46
|
-
return true;
|
|
47
|
-
// Unrecognized value: ignore and consult the persisted flag.
|
|
48
|
-
}
|
|
49
|
-
try {
|
|
50
|
-
const parsed = JSON.parse(fs.readFileSync(syncStateFilePath(), 'utf-8'));
|
|
51
|
-
if (parsed && typeof parsed.enabled === 'boolean')
|
|
52
|
-
return parsed.enabled;
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
// Absent or unreadable → default enabled.
|
|
56
|
-
}
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
/** Persist the machine-local sync enable flag (durable across cache wipes). */
|
|
60
|
-
export function setSyncEnabled(enabled) {
|
|
61
|
-
const p = syncStateFilePath();
|
|
62
|
-
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
63
|
-
fs.writeFileSync(p, JSON.stringify({ enabled }, null, 2) + '\n', 'utf-8');
|
|
64
|
-
}
|
|
65
9
|
/**
|
|
66
10
|
* Resolve R2 credentials from the `r2.backups` bundle. Throws a clear,
|
|
67
11
|
* actionable error if the bundle or any key is missing — sync cannot proceed
|
package/dist/lib/shims.d.ts
CHANGED
|
@@ -299,7 +299,10 @@ export declare function getShimPath(agent: AgentId): string;
|
|
|
299
299
|
* loop because addShimsToPath() only edits the rc file, never the legacy
|
|
300
300
|
* shim file itself.
|
|
301
301
|
*/
|
|
302
|
-
export declare function getPathShadowingExecutable(agent: AgentId
|
|
302
|
+
export declare function getPathShadowingExecutable(agent: AgentId, overrides?: {
|
|
303
|
+
pathDirs?: string[];
|
|
304
|
+
shimPath?: string;
|
|
305
|
+
}): string | null;
|
|
303
306
|
/**
|
|
304
307
|
* Delete the legacy ~/.agents/shims/<cli> file if it exists, returning whether
|
|
305
308
|
* anything was removed. Pre-split installs put shims under ~/.agents/shims/;
|
package/dist/lib/shims.js
CHANGED
|
@@ -1616,12 +1616,16 @@ export function getShimPath(agent) {
|
|
|
1616
1616
|
* loop because addShimsToPath() only edits the rc file, never the legacy
|
|
1617
1617
|
* shim file itself.
|
|
1618
1618
|
*/
|
|
1619
|
-
export function getPathShadowingExecutable(agent) {
|
|
1620
|
-
const pathDirs = (process.env.PATH || '').split(path.delimiter).filter(Boolean);
|
|
1621
|
-
const shimPath = path.resolve(getShimPath(agent));
|
|
1619
|
+
export function getPathShadowingExecutable(agent, overrides) {
|
|
1620
|
+
const pathDirs = overrides?.pathDirs ?? (process.env.PATH || '').split(path.delimiter).filter(Boolean);
|
|
1621
|
+
const shimPath = path.resolve(overrides?.shimPath ?? getShimPath(agent));
|
|
1622
1622
|
const cliCommand = AGENTS[agent].cliCommand;
|
|
1623
1623
|
const legacyUserShim = path.resolve(path.join(os.homedir(), '.agents', 'shims', cliCommand));
|
|
1624
1624
|
const managedShimExists = fs.existsSync(shimPath);
|
|
1625
|
+
// The shim's own realpath — an adopted launcher is a symlink at a DIFFERENT
|
|
1626
|
+
// path that resolves here, so identity must be by resolved target, not the
|
|
1627
|
+
// literal path string.
|
|
1628
|
+
const shimReal = managedShimExists ? canonicalOrNull(shimPath) : null;
|
|
1625
1629
|
for (const dir of pathDirs) {
|
|
1626
1630
|
const candidate = path.resolve(dir, cliCommand);
|
|
1627
1631
|
if (!fs.existsSync(candidate)) {
|
|
@@ -1629,6 +1633,12 @@ export function getPathShadowingExecutable(agent) {
|
|
|
1629
1633
|
}
|
|
1630
1634
|
if (candidate === shimPath)
|
|
1631
1635
|
return null;
|
|
1636
|
+
// Adopted launcher: a symlink we repointed at our shim. Its path differs
|
|
1637
|
+
// from shimPath but it resolves to the same file, so it is NOT a shadow —
|
|
1638
|
+
// otherwise every adopted default would be re-flagged forever, resurfacing
|
|
1639
|
+
// the false "runs a native binary" note this whole feature set out to kill.
|
|
1640
|
+
if (shimReal && canonicalOrNull(candidate) === shimReal)
|
|
1641
|
+
return null;
|
|
1632
1642
|
if (candidate === legacyUserShim && managedShimExists) {
|
|
1633
1643
|
// Legacy file from the pre-split layout. Don't treat as shadow — the
|
|
1634
1644
|
// repair flow deletes it via removeLegacyUserShim instead. Continue
|
|
@@ -1729,6 +1739,16 @@ function canonical(p) {
|
|
|
1729
1739
|
return path.resolve(p);
|
|
1730
1740
|
}
|
|
1731
1741
|
}
|
|
1742
|
+
/** Like canonical(), but null when the path can't be resolved (broken/racy
|
|
1743
|
+
* symlink) — used where a failed resolve must NOT collapse to the input path. */
|
|
1744
|
+
function canonicalOrNull(p) {
|
|
1745
|
+
try {
|
|
1746
|
+
return fs.realpathSync(p);
|
|
1747
|
+
}
|
|
1748
|
+
catch {
|
|
1749
|
+
return null;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1732
1752
|
/**
|
|
1733
1753
|
* Adopt the harness's own launcher that shadows our shim on PATH.
|
|
1734
1754
|
*
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* secrets -> listRemoteBundles + pullBundle (needs a passphrase; skipped
|
|
14
14
|
* cleanly when none is available — tokenized non-interactive auth
|
|
15
15
|
* arrives with `agents login`, #366/#367)
|
|
16
|
-
* sessions -> syncSessions(), gated by isSyncConfigured()
|
|
16
|
+
* sessions -> syncSessions(), gated by the session-sync beta opt-in + isSyncConfigured(), like the daemon
|
|
17
17
|
* reconcile-> refresh({ skipPrompts }) — re-materialize resources into homes
|
|
18
18
|
*/
|
|
19
19
|
/** The five umbrella flags off `agents sync`. */
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* secrets -> listRemoteBundles + pullBundle (needs a passphrase; skipped
|
|
14
14
|
* cleanly when none is available — tokenized non-interactive auth
|
|
15
15
|
* arrives with `agents login`, #366/#367)
|
|
16
|
-
* sessions -> syncSessions(), gated by isSyncConfigured()
|
|
16
|
+
* sessions -> syncSessions(), gated by the session-sync beta opt-in + isSyncConfigured(), like the daemon
|
|
17
17
|
* reconcile-> refresh({ skipPrompts }) — re-materialize resources into homes
|
|
18
18
|
*/
|
|
19
19
|
import { pullRepo } from './git.js';
|
|
@@ -111,10 +111,12 @@ export async function runUmbrellaSync(args) {
|
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
if (plan.fetchSessions) {
|
|
114
|
-
// Gate exactly like the daemon:
|
|
115
|
-
// is a clean no-op, not an error that
|
|
116
|
-
|
|
117
|
-
|
|
114
|
+
// Gate exactly like the daemon: without the `session-sync` beta opt-in (or
|
|
115
|
+
// with a missing r2.backups bundle) this is a clean no-op, not an error that
|
|
116
|
+
// fails the whole sync.
|
|
117
|
+
const { isBetaEnabled } = await import('./beta.js');
|
|
118
|
+
const { isSyncConfigured } = await import('./session/sync/config.js');
|
|
119
|
+
if (isBetaEnabled('session-sync') && isSyncConfigured()) {
|
|
118
120
|
const { syncSessions } = await import('./session/sync/sync.js');
|
|
119
121
|
const r = await syncSessions();
|
|
120
122
|
result.sessions = { ran: true, pushed: r.pushed, pulled: r.pulled, merged: r.merged };
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -61,7 +61,7 @@ export interface BudgetConfig {
|
|
|
61
61
|
require_confirm_over?: number;
|
|
62
62
|
}
|
|
63
63
|
/** Preview features that users can opt into via `agents beta`. */
|
|
64
|
-
export type BetaFeatureName = 'drive' | 'factory';
|
|
64
|
+
export type BetaFeatureName = 'drive' | 'factory' | 'session-sync';
|
|
65
65
|
/** Subset of chalk color names used for agent-specific terminal output. */
|
|
66
66
|
export type ChalkColor = 'magenta' | 'green' | 'blue' | 'cyan' | 'yellowBright' | 'redBright' | 'whiteBright' | 'blueBright' | 'greenBright' | 'magentaBright' | 'cyanBright';
|
|
67
67
|
/** Static configuration for a single agent -- paths, capabilities, and format conventions. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.40",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|