@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.
@@ -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, isSyncEnabled, setSyncEnabled, SYNC_BUNDLE, } from '../lib/session/sync/config.js';
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 actions short-circuit before any network cycle.
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
- setSyncEnabled(false);
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. Re-enable: agents sessions sync --enable'));
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
- setSyncEnabled(true);
20
- console.log(chalk.green('Automatic session sync enabled') + chalk.dim(' — the daemon resumes on its next cycle.'));
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 = isSyncEnabled();
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', 'Turn ON automatic background sync on this machine (persisted)')
81
- .option('--disable', 'Turn OFF automatic background sync on this machine (persisted)')
82
- .option('--status', 'Show whether automatic sync is enabled and configured');
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
- - --disable/--enable persist a machine-local switch (~/.agents/.history) that gates the
103
- daemon's automatic sync; a bare 'agents sessions sync' still forces a manual cycle.
104
- The AGENTS_SESSIONS_SYNC env var (on/off) overrides the switch for one invocation.
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
- * Render the unified active-session view, grouped by machine. Local sessions
606
- * come from `getActiveSessions()`; unless `--local`, sessions from other
607
- * machines are folded in over SSH (explicit `--host` targets, else the
608
- * registered online devices from `ag devices`). A tip is shown when there are
609
- * no other machines to include.
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
- const local = await getActiveSessions();
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 remote = await gatherRemoteActive(opts.hosts);
621
- remoteDeviceCount = remote.deviceCount;
622
- merged = dedupeByMachineSession([...local, ...remote.sessions]);
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.
@@ -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
  }
@@ -343,10 +343,14 @@ export async function runDaemon() {
343
343
  return;
344
344
  syncing = true;
345
345
  try {
346
- const { isSyncConfigured, isSyncEnabled } = await import('./session/sync/config.js');
347
- // isSyncEnabled() first: a machine the operator turned off must skip the
348
- // keychain read entirely, not just the network cycle.
349
- if (!isSyncEnabled() || !isSyncConfigured())
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
@@ -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): string | null;
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() exactly like the daemon
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() exactly like the daemon
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: an off switch or a missing r2.backups bundle
115
- // is a clean no-op, not an error that fails the whole sync.
116
- const { isSyncConfigured, isSyncEnabled } = await import('./session/sync/config.js');
117
- if (isSyncEnabled() && isSyncConfigured()) {
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 };
@@ -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.38",
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",