@phnx-labs/agents-cli 1.20.37 → 1.20.39
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/computer-actions.d.ts +10 -0
- package/dist/commands/computer-actions.js +47 -17
- package/dist/commands/doctor.js +48 -1
- package/dist/commands/go.d.ts +28 -0
- package/dist/commands/go.js +238 -0
- package/dist/commands/sessions-picker.d.ts +2 -0
- package/dist/commands/sessions-picker.js +10 -1
- package/dist/commands/sessions-sync.js +23 -15
- package/dist/commands/sessions.d.ts +19 -1
- package/dist/commands/sessions.js +57 -10
- package/dist/index.js +59 -68
- 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/active.d.ts +6 -0
- package/dist/lib/session/active.js +1 -1
- 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 +69 -2
- package/dist/lib/shims.js +260 -23
- 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
|
@@ -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
|
|
@@ -164,7 +182,7 @@ export declare function formatPickerLabel(s: SessionMeta, query: string, cols?:
|
|
|
164
182
|
* so it stays fixed across the picker's re-renders within a single run.
|
|
165
183
|
*/
|
|
166
184
|
export declare function formatPickerTip(sessions: SessionMeta[]): string;
|
|
167
|
-
export declare function pickSessionInteractive(sessions: SessionMeta[], message?: string, initialSearch?: string, hiddenCount?: number): Promise<PickedSession | null>;
|
|
185
|
+
export declare function pickSessionInteractive(sessions: SessionMeta[], message?: string, initialSearch?: string, hiddenCount?: number, enterHint?: string): Promise<PickedSession | null>;
|
|
168
186
|
/**
|
|
169
187
|
* Resume a session in the current terminal — a foreground takeover of this
|
|
170
188
|
* process. Used by the single-select picker and by `sessions resume` when the
|
|
@@ -39,6 +39,7 @@ import { setHelpSections } from '../lib/help.js';
|
|
|
39
39
|
import { registerSessionsTailCommand } from './sessions-tail.js';
|
|
40
40
|
import { registerSessionsSyncCommand } from './sessions-sync.js';
|
|
41
41
|
import { registerSessionsResumeCommand } from './sessions-resume.js';
|
|
42
|
+
import { registerGoCommand } from './go.js';
|
|
42
43
|
import { registerSessionsInjectCommand } from './sessions-inject.js';
|
|
43
44
|
const SESSION_AGENT_FILTER_HELP = `Filter by agent, e.g. claude, codex, claude@2.0.65`;
|
|
44
45
|
/**
|
|
@@ -600,25 +601,65 @@ async function enrichLocalLocators(local) {
|
|
|
600
601
|
}
|
|
601
602
|
catch { /* non-fatal */ }
|
|
602
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
|
+
}
|
|
603
609
|
/**
|
|
604
|
-
*
|
|
605
|
-
*
|
|
606
|
-
* machines
|
|
607
|
-
*
|
|
608
|
-
*
|
|
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.
|
|
609
643
|
*/
|
|
610
644
|
async function renderActiveSessions(asJson, waitingOnly = false, opts = {}) {
|
|
611
645
|
const self = machineId();
|
|
612
|
-
|
|
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() : [];
|
|
613
649
|
for (const s of local)
|
|
614
650
|
if (!s.machine)
|
|
615
651
|
s.machine = self;
|
|
616
652
|
let remoteDeviceCount = 0;
|
|
617
653
|
let merged = local;
|
|
618
654
|
if (!opts.local) {
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
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
|
+
}
|
|
622
663
|
}
|
|
623
664
|
// --waiting: only sessions blocked on the user. Exits non-zero when any are
|
|
624
665
|
// present so a supervising agent or hook can poll it as a gate.
|
|
@@ -664,6 +705,9 @@ function printCrossMachineTip() {
|
|
|
664
705
|
}
|
|
665
706
|
/** Main action handler for `agents sessions`. Routes to picker, table, or single-session render. */
|
|
666
707
|
async function sessionsAction(query, options) {
|
|
708
|
+
// Explicit --query is interchangeable with the positional; it's how you search
|
|
709
|
+
// for text that collides with a subcommand name (e.g. `sessions --query go`).
|
|
710
|
+
query = query ?? options.query;
|
|
667
711
|
// Normalize convenience flags before any routing reads them: per-agent
|
|
668
712
|
// shorthands fold into --agent, and --device is an alias for --host (both
|
|
669
713
|
// resolve against the same device registry).
|
|
@@ -1365,7 +1409,7 @@ const PICKER_TIPS = [
|
|
|
1365
1409
|
export function formatPickerTip(sessions) {
|
|
1366
1410
|
return chalk.gray(PICKER_TIPS[sessions.length % PICKER_TIPS.length]);
|
|
1367
1411
|
}
|
|
1368
|
-
export async function pickSessionInteractive(sessions, message = 'Search sessions:', initialSearch, hiddenCount = 0) {
|
|
1412
|
+
export async function pickSessionInteractive(sessions, message = 'Search sessions:', initialSearch, hiddenCount = 0, enterHint) {
|
|
1369
1413
|
if (hiddenCount > 0) {
|
|
1370
1414
|
console.log(chalk.gray(formatTeamHiddenFooter(hiddenCount)));
|
|
1371
1415
|
}
|
|
@@ -1385,6 +1429,7 @@ export async function pickSessionInteractive(sessions, message = 'Search session
|
|
|
1385
1429
|
labelFor: (s, query) => formatPickerLabel(s, query, cols),
|
|
1386
1430
|
pageSize: PICKER_RECENT_COUNT,
|
|
1387
1431
|
initialSearch,
|
|
1432
|
+
enterHint,
|
|
1388
1433
|
});
|
|
1389
1434
|
}
|
|
1390
1435
|
catch (err) {
|
|
@@ -1888,6 +1933,7 @@ export function registerSessionsCommands(program) {
|
|
|
1888
1933
|
const sessionsCmd = program
|
|
1889
1934
|
.command('sessions')
|
|
1890
1935
|
.argument('[query]', 'Session ID, search query, or path (., ../, /path) to filter by project')
|
|
1936
|
+
.option('--query <text>', 'Search text — use when the term collides with a subcommand name (e.g. "go")')
|
|
1891
1937
|
.description('Find, browse, and read agent conversation transcripts across Claude, Codex, Gemini, and OpenCode.')
|
|
1892
1938
|
.option('-a, --agent <agent>', 'Filter by agent type and version (e.g., claude, codex@0.116.0)')
|
|
1893
1939
|
.option('--claude', 'Shorthand for --agent claude')
|
|
@@ -1967,6 +2013,7 @@ export function registerSessionsCommands(program) {
|
|
|
1967
2013
|
registerSessionsTailCommand(sessionsCmd);
|
|
1968
2014
|
registerSessionsSyncCommand(sessionsCmd);
|
|
1969
2015
|
registerSessionsResumeCommand(sessionsCmd);
|
|
2016
|
+
registerGoCommand(sessionsCmd);
|
|
1970
2017
|
registerSessionsInjectCommand(sessionsCmd);
|
|
1971
2018
|
}
|
|
1972
2019
|
function formatNoSessionsMessage(showAll, project) {
|
package/dist/index.js
CHANGED
|
@@ -483,7 +483,7 @@ async function maybeBootstrapShimIntegration(requestedCommand, helpOrVersionRequ
|
|
|
483
483
|
const { confirm } = await import('@inquirer/prompts');
|
|
484
484
|
const { AGENTS } = await import('./lib/agents.js');
|
|
485
485
|
const { getGlobalDefault, listInstalledVersions } = await import('./lib/versions.js');
|
|
486
|
-
const { addShimsToPath, ensureShimCurrent, ensureVersionedAliasCurrent, getPathShadowingExecutable, getPathSetupInstructions, getShimsDir, isShimsInPath, listAgentsWithInstalledVersions, removeLegacyUserShim, } = await import('./lib/shims.js');
|
|
486
|
+
const { addShimsToPath, adoptShadowingLauncher, ensureShimCurrent, ensureVersionedAliasCurrent, getPathShadowingExecutable, getPathSetupInstructions, getShimsDir, isShimsInPath, listAgentsWithInstalledVersions, removeLegacyUserShim, } = await import('./lib/shims.js');
|
|
487
487
|
const installedAgents = listAgentsWithInstalledVersions();
|
|
488
488
|
if (installedAgents.length === 0) {
|
|
489
489
|
return;
|
|
@@ -519,87 +519,78 @@ async function maybeBootstrapShimIntegration(requestedCommand, helpOrVersionRequ
|
|
|
519
519
|
return;
|
|
520
520
|
}
|
|
521
521
|
const defaultAgents = installedAgents.filter((agent) => getGlobalDefault(agent));
|
|
522
|
+
// Auto-adopt any harness launcher that shadows our shim. PATH-order repair
|
|
523
|
+
// (below) cannot win against `~/.local/bin` — it's prepended in .zshenv for
|
|
524
|
+
// every shell while our prepend only lands in .zshrc — so for symlink
|
|
525
|
+
// launchers we *become* the launcher instead. Detection keys on the launcher
|
|
526
|
+
// symlink EXISTING (via adoptShadowingLauncher's own fallback), not on this
|
|
527
|
+
// shell's PATH order, so it also heals the GUI/non-interactive shadow an
|
|
528
|
+
// interactive run can't see. Reversible; only ever rewrites a symlink.
|
|
529
|
+
for (const agent of defaultAgents) {
|
|
530
|
+
const result = adoptShadowingLauncher(agent);
|
|
531
|
+
if (result.adopted) {
|
|
532
|
+
console.log(chalk.green(`Adopted ${AGENTS[agent].cliCommand} launcher (${result.launcher}) — version management now wins regardless of PATH order.`));
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
// Recompute AFTER adoption so anything we just took over drops out. What
|
|
536
|
+
// remains is a real binary we deliberately don't touch (adoption is
|
|
537
|
+
// symlink-only) — those get an honest one-time note, never a looping prompt.
|
|
522
538
|
const shadowed = defaultAgents
|
|
523
539
|
.map((agent) => ({ agent, shadowedBy: getPathShadowingExecutable(agent) }))
|
|
524
540
|
.filter((item) => Boolean(item.shadowedBy));
|
|
525
|
-
//
|
|
526
|
-
//
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
// contradicted the comment below and surfaced false positives (e.g. an
|
|
530
|
-
// earlier `alias codex=...` cancelled by a later `unalias codex` was
|
|
531
|
-
// reported because the detector did a static rc-file regex).
|
|
532
|
-
if (shadowed.length === 0 && isShimsInPath()) {
|
|
541
|
+
// After adoption, the only things left are (a) real-binary shadows we won't
|
|
542
|
+
// touch, and (b) a genuinely missing PATH entry. Nothing else needs the user.
|
|
543
|
+
const pathMissing = !isShimsInPath();
|
|
544
|
+
if (shadowed.length === 0 && !pathMissing) {
|
|
533
545
|
return;
|
|
534
546
|
}
|
|
535
|
-
// Suppress repeated
|
|
536
|
-
//
|
|
537
|
-
//
|
|
538
|
-
//
|
|
539
|
-
// PPID changes and the prompt is allowed again.
|
|
547
|
+
// Suppress repeated notices within the same shell. A successful rc-file edit
|
|
548
|
+
// doesn't reload the parent shell, so the next invocation re-fires detection.
|
|
549
|
+
// The sentinel survives only as long as the parent shell process — a new
|
|
550
|
+
// terminal (new PPID) is allowed to surface it again.
|
|
540
551
|
const sentinelPath = path.join(os.tmpdir(), `agents-shim-prompted-${process.ppid}`);
|
|
541
552
|
if (fs.existsSync(sentinelPath)) {
|
|
542
553
|
return;
|
|
543
554
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
default: true,
|
|
557
|
-
});
|
|
558
|
-
if (!shouldRepair) {
|
|
559
|
-
console.log(chalk.yellow('Shim integration still needs attention.'));
|
|
560
|
-
console.log(chalk.gray(getPathSetupInstructions()));
|
|
561
|
-
try {
|
|
562
|
-
fs.writeFileSync(sentinelPath, '1');
|
|
563
|
-
}
|
|
564
|
-
catch { /* best-effort */ }
|
|
565
|
-
return;
|
|
566
|
-
}
|
|
567
|
-
const pathResult = addShimsToPath();
|
|
568
|
-
if (!pathResult.success) {
|
|
569
|
-
console.log(chalk.yellow('Could not repair shim PATH setup automatically.'));
|
|
570
|
-
console.log(chalk.gray(pathResult.error || getPathSetupInstructions()));
|
|
571
|
-
// Write the sentinel even on failure — otherwise an unwritable rc file
|
|
572
|
-
// re-prompts every invocation in the same shell. The user opens a new
|
|
573
|
-
// terminal (new PPID) to retry.
|
|
574
|
-
try {
|
|
575
|
-
fs.writeFileSync(sentinelPath, '1');
|
|
576
|
-
}
|
|
577
|
-
catch { /* best-effort */ }
|
|
578
|
-
return;
|
|
555
|
+
// Real-binary shadows: adoption is symlink-only (we never rename a real native
|
|
556
|
+
// binary), and `addShimsToPath` provably can't outrank an early-PATH dir like
|
|
557
|
+
// ~/.local/bin across zsh's whole sourcing chain. So DON'T offer a "Repair?"
|
|
558
|
+
// prompt here — that was the infinite-loop bug (Yes was always a no-op).
|
|
559
|
+
// Inform once and point at the real levers.
|
|
560
|
+
if (shadowed.length > 0) {
|
|
561
|
+
const targets = shadowed
|
|
562
|
+
.map(({ agent, shadowedBy }) => ` ${AGENTS[agent].cliCommand}: ${shadowedBy}`)
|
|
563
|
+
.join('\n');
|
|
564
|
+
console.log(chalk.yellow('These agent commands run a native binary instead of the version-managed shim:'));
|
|
565
|
+
console.log(chalk.gray(targets));
|
|
566
|
+
console.log(chalk.gray(`It's a real binary (not a symlink), so agents-cli won't move it. To hand it to agents-cli, remove/reorder it, or put ${getShimsDir()} earlier in PATH.`));
|
|
579
567
|
}
|
|
580
|
-
//
|
|
581
|
-
//
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
.join('\n');
|
|
590
|
-
console.log(chalk.yellow('Repair could not change anything — the shim is shadowed by another binary on PATH:'));
|
|
591
|
-
console.log(chalk.gray(targets));
|
|
592
|
-
console.log(chalk.gray(`Fix it by removing or reordering that binary, or making sure ${getShimsDir()} appears earlier in PATH than its parent dir.`));
|
|
568
|
+
// Genuinely-missing PATH entry is the one thing addShimsToPath actually fixes,
|
|
569
|
+
// so it's the only case that still earns an interactive prompt.
|
|
570
|
+
if (pathMissing) {
|
|
571
|
+
const shouldRepair = await confirm({
|
|
572
|
+
message: 'Add the agents-cli shims directory to your PATH now?',
|
|
573
|
+
default: true,
|
|
574
|
+
});
|
|
575
|
+
if (!shouldRepair) {
|
|
576
|
+
console.log(chalk.gray(getPathSetupInstructions()));
|
|
593
577
|
}
|
|
594
578
|
else {
|
|
595
|
-
|
|
596
|
-
|
|
579
|
+
const pathResult = addShimsToPath();
|
|
580
|
+
if (!pathResult.success) {
|
|
581
|
+
console.log(chalk.yellow('Could not update PATH automatically.'));
|
|
582
|
+
console.log(chalk.gray(pathResult.error || getPathSetupInstructions()));
|
|
583
|
+
}
|
|
584
|
+
else if (pathResult.alreadyPresent) {
|
|
585
|
+
console.log(chalk.yellow(`Shim PATH entry is already in ~/${pathResult.rcFile} — this shell just needs to reload it.`));
|
|
586
|
+
console.log(chalk.gray(`Run: source ~/${pathResult.rcFile} (or open a new terminal)`));
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
console.log(chalk.green(`Added shims to PATH in ~/${pathResult.rcFile}`));
|
|
590
|
+
console.log(chalk.gray(getPathSetupInstructions()));
|
|
591
|
+
}
|
|
597
592
|
}
|
|
598
593
|
}
|
|
599
|
-
else {
|
|
600
|
-
console.log(chalk.green(`Repaired shim PATH setup in ~/${pathResult.rcFile}`));
|
|
601
|
-
console.log(chalk.gray(getPathSetupInstructions()));
|
|
602
|
-
}
|
|
603
594
|
try {
|
|
604
595
|
fs.writeFileSync(sentinelPath, '1');
|
|
605
596
|
}
|
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();
|
|
@@ -89,6 +89,12 @@ export interface ActiveQueryOptions {
|
|
|
89
89
|
/** Skip the `ps` scan for ad-hoc headless agents. */
|
|
90
90
|
skipHeadless?: boolean;
|
|
91
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Locate the live transcript for an agent process. Claude files are keyed by
|
|
94
|
+
* cwd (+ optional session uuid); Codex files are date-partitioned, so we resolve
|
|
95
|
+
* the newest indexed Codex session for the cwd instead.
|
|
96
|
+
*/
|
|
97
|
+
export declare function findSessionFileForKind(kind: string, cwd?: string, sessionId?: string): string | undefined;
|
|
92
98
|
/** Live teams teammates. Reuses AgentManager which already polls PIDs via `kill -0`. */
|
|
93
99
|
export declare function listTeamsActive(): Promise<ActiveSession[]>;
|
|
94
100
|
/** Live editor-terminal agents across every IDE window. */
|
|
@@ -161,7 +161,7 @@ function classifyActivity(sessionFile) {
|
|
|
161
161
|
* cwd (+ optional session uuid); Codex files are date-partitioned, so we resolve
|
|
162
162
|
* the newest indexed Codex session for the cwd instead.
|
|
163
163
|
*/
|
|
164
|
-
function findSessionFileForKind(kind, cwd, sessionId) {
|
|
164
|
+
export function findSessionFileForKind(kind, cwd, sessionId) {
|
|
165
165
|
if (!cwd)
|
|
166
166
|
return undefined;
|
|
167
167
|
if (kind === 'claude')
|
|
@@ -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
|
@@ -77,7 +77,7 @@ export interface ConflictInfo {
|
|
|
77
77
|
* top-level entry add/remove — deep edits to plugin contents won't
|
|
78
78
|
* trigger auto-resync, run `agents sync` for that.
|
|
79
79
|
*/
|
|
80
|
-
export declare const SHIM_SCHEMA_VERSION =
|
|
80
|
+
export declare const SHIM_SCHEMA_VERSION = 23;
|
|
81
81
|
/**
|
|
82
82
|
* Generate the full bash shim script for the given agent. The returned string
|
|
83
83
|
* is written to ~/.agents/shims/{cliCommand} and made executable.
|
|
@@ -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/;
|
|
@@ -311,6 +314,70 @@ export declare function getPathShadowingExecutable(agent: AgentId): string | nul
|
|
|
311
314
|
export declare function removeLegacyUserShim(agent: AgentId, overrides?: {
|
|
312
315
|
homeDir?: string;
|
|
313
316
|
}): boolean;
|
|
317
|
+
/**
|
|
318
|
+
* Where an adopted launcher's provenance is recorded. Lives under durable
|
|
319
|
+
* `.history` (NOT the regenerable `.cache`) so the reverse pointer to the native
|
|
320
|
+
* binary survives a cache wipe — the shim reads it to fall through to the native
|
|
321
|
+
* binary by absolute path when no managed version resolves. Two lines:
|
|
322
|
+
* line 1 = original binary, line 2 = launcher path (for `--release`).
|
|
323
|
+
*/
|
|
324
|
+
export declare function getAdoptedRecordPath(agent: AgentId, historyDir?: string): string;
|
|
325
|
+
/**
|
|
326
|
+
* The launcher a harness's own installer drops in an early-PATH dir. Detection
|
|
327
|
+
* for adoption keys on the launcher *existing as a symlink resolving outside our
|
|
328
|
+
* shims dir* — NOT on current PATH order. That's deliberate: the shim only loses
|
|
329
|
+
* PATH races in non-interactive / GUI-launched shells, which an interactive
|
|
330
|
+
* `agents` run can't observe via its own PATH. Keying on the durable symlink lets
|
|
331
|
+
* auto-adoption fire for those users too. Returns the launcher path or null.
|
|
332
|
+
*/
|
|
333
|
+
export declare function findAdoptableLauncher(agent: AgentId, overrides?: {
|
|
334
|
+
homeDir?: string;
|
|
335
|
+
shimsDir?: string;
|
|
336
|
+
}): string | null;
|
|
337
|
+
export type AdoptResult = {
|
|
338
|
+
adopted: true;
|
|
339
|
+
launcher: string;
|
|
340
|
+
original: string;
|
|
341
|
+
} | {
|
|
342
|
+
adopted: false;
|
|
343
|
+
reason: 'no-shadow' | 'already-adopted' | 'not-a-symlink' | 'unsafe-target' | 'error';
|
|
344
|
+
launcher?: string;
|
|
345
|
+
};
|
|
346
|
+
/**
|
|
347
|
+
* Adopt the harness's own launcher that shadows our shim on PATH.
|
|
348
|
+
*
|
|
349
|
+
* PATH-ordering fixes (editing rc files) can never reliably win: `~/.local/bin`
|
|
350
|
+
* (where grok/droid/etc. self-install) is prepended in `.zshenv`/`.zprofile`
|
|
351
|
+
* for *every* shell, while our shims prepend only lands in `.zshrc`
|
|
352
|
+
* (interactive). No single rc file guarantees "last prepend wins" across zsh's
|
|
353
|
+
* whole sourcing chain, so the shim loses in non-interactive / GUI-launched
|
|
354
|
+
* contexts. Instead of fighting PATH order, we *become* the launcher: replace
|
|
355
|
+
* the shadowing symlink with one pointing at our shim, and record the real
|
|
356
|
+
* original so the shim falls through to it when no managed version is selected.
|
|
357
|
+
*
|
|
358
|
+
* Regression bounds:
|
|
359
|
+
* - Only ever touches a **symlink** (never renames/deletes a real binary).
|
|
360
|
+
* - Records the resolved original + launcher path for lossless restore
|
|
361
|
+
* (`releaseAdoptedLauncher`), in durable `.history` so a cache wipe can't
|
|
362
|
+
* orphan the reverse pointer.
|
|
363
|
+
* - Idempotent: a no-op once the launcher already points at our shim.
|
|
364
|
+
* - Never records our own shim as the "original" (would loop).
|
|
365
|
+
*/
|
|
366
|
+
export declare function adoptShadowingLauncher(agent: AgentId, overrides?: {
|
|
367
|
+
shadowedBy?: string;
|
|
368
|
+
shimsDir?: string;
|
|
369
|
+
historyDir?: string;
|
|
370
|
+
}): AdoptResult;
|
|
371
|
+
/**
|
|
372
|
+
* Undo `adoptShadowingLauncher`: repoint the launcher back at the recorded
|
|
373
|
+
* original and drop the record. Reversible escape hatch for users who want the
|
|
374
|
+
* native launcher to win. Returns the restored original path, or null if there
|
|
375
|
+
* was nothing to release.
|
|
376
|
+
*/
|
|
377
|
+
export declare function releaseAdoptedLauncher(agent: AgentId, overrides?: {
|
|
378
|
+
shimsDir?: string;
|
|
379
|
+
historyDir?: string;
|
|
380
|
+
}): string | null;
|
|
314
381
|
export declare function hasAliasShadowingShim(agent: AgentId, overrides?: {
|
|
315
382
|
homeDir?: string;
|
|
316
383
|
}): boolean;
|