@phnx-labs/agents-cli 1.20.73 → 1.20.74
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 +96 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/doctor.js +13 -1
- package/dist/commands/export.d.ts +11 -0
- package/dist/commands/export.js +215 -0
- package/dist/commands/import.js +51 -3
- package/dist/commands/setup.js +6 -1
- package/dist/commands/versions.js +36 -5
- package/dist/commands/view.js +75 -27
- package/dist/index.js +2 -1
- package/dist/lib/agents.d.ts +35 -0
- package/dist/lib/agents.js +63 -1
- package/dist/lib/config-transfer.d.ts +16 -0
- package/dist/lib/config-transfer.js +56 -0
- package/dist/lib/export.d.ts +72 -0
- package/dist/lib/export.js +269 -0
- package/dist/lib/import.d.ts +23 -0
- package/dist/lib/import.js +74 -2
- package/dist/lib/isolation-boundary-report.d.ts +10 -0
- package/dist/lib/isolation-boundary-report.js +35 -0
- package/dist/lib/project-resources.js +20 -5
- package/dist/lib/shims.d.ts +26 -0
- package/dist/lib/shims.js +68 -0
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/state.js +14 -4
- package/dist/lib/types.d.ts +11 -0
- package/dist/lib/uninstall.js +1 -45
- package/dist/lib/versions.d.ts +13 -0
- package/dist/lib/versions.js +61 -2
- package/package.json +1 -1
package/dist/lib/state.js
CHANGED
|
@@ -606,18 +606,25 @@ function writeIfChanged(filePath, content) {
|
|
|
606
606
|
* `agents:` / `versions:` are not written (no empty committed files).
|
|
607
607
|
*/
|
|
608
608
|
function writeMetaUnlocked(meta) {
|
|
609
|
-
const { agents, versions, defaultBrowserProfile, ...central } = meta;
|
|
609
|
+
const { agents, isolatedAgents, versions, defaultBrowserProfile, ...central } = meta;
|
|
610
610
|
// Write the machine-local files FIRST, then strip central — so a crash mid-write
|
|
611
611
|
// never removes pins/versions from central before they're persisted elsewhere.
|
|
612
612
|
const devicePath = getDeviceMetaPath();
|
|
613
613
|
const hasAgents = !!agents && Object.keys(agents).length > 0;
|
|
614
|
+
// The isolated pointer names a version installed on THIS machine, exactly like a
|
|
615
|
+
// global pin, so it belongs beside `agents:` in the device file rather than in the
|
|
616
|
+
// central doc that syncs — otherwise another machine inherits a pointer to a copy
|
|
617
|
+
// it does not have.
|
|
618
|
+
const hasIsolatedAgents = !!isolatedAgents && Object.keys(isolatedAgents).length > 0;
|
|
614
619
|
const hasDefaultBrowser = !!defaultBrowserProfile;
|
|
615
|
-
if (hasAgents || hasDefaultBrowser) {
|
|
620
|
+
if (hasAgents || hasIsolatedAgents || hasDefaultBrowser) {
|
|
616
621
|
// Device-local doc carries `agents:` pins and `defaultBrowserProfile:` — both
|
|
617
622
|
// are per-machine and must never land in central agents.yaml (which syncs).
|
|
618
623
|
const deviceDoc = {};
|
|
619
624
|
if (hasAgents)
|
|
620
625
|
deviceDoc.agents = agents;
|
|
626
|
+
if (hasIsolatedAgents)
|
|
627
|
+
deviceDoc.isolatedAgents = isolatedAgents;
|
|
621
628
|
if (hasDefaultBrowser)
|
|
622
629
|
deviceDoc.defaultBrowserProfile = defaultBrowserProfile;
|
|
623
630
|
fs.mkdirSync(path.dirname(devicePath), { recursive: true });
|
|
@@ -640,8 +647,9 @@ function writeMetaUnlocked(meta) {
|
|
|
640
647
|
}
|
|
641
648
|
/**
|
|
642
649
|
* Overlay this machine's local state onto a central-portable Meta:
|
|
643
|
-
* - `agents:` from the device file (device wins; the union
|
|
644
|
-
* one-level merge and self-heals a pre-migration central that
|
|
650
|
+
* - `agents:` and `isolatedAgents:` from the device file (device wins; the union
|
|
651
|
+
* both preserves the one-level merge and self-heals a pre-migration central that
|
|
652
|
+
* still has pins)
|
|
645
653
|
* - `defaultBrowserProfile:` from the device file (device is the sole source;
|
|
646
654
|
* the field is stripped from central on write, so nothing to merge against)
|
|
647
655
|
* - `versions:` from the history JSON (wholesale replace; falls back to
|
|
@@ -654,6 +662,8 @@ function overlayMachineLocal(meta) {
|
|
|
654
662
|
const dm = yaml.parse(fs.readFileSync(devicePath, 'utf-8'));
|
|
655
663
|
if (dm?.agents)
|
|
656
664
|
meta.agents = { ...meta.agents, ...dm.agents };
|
|
665
|
+
if (dm?.isolatedAgents)
|
|
666
|
+
meta.isolatedAgents = { ...meta.isolatedAgents, ...dm.isolatedAgents };
|
|
657
667
|
if (dm?.defaultBrowserProfile)
|
|
658
668
|
meta.defaultBrowserProfile = dm.defaultBrowserProfile;
|
|
659
669
|
}
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -692,6 +692,17 @@ export interface BrandConfig {
|
|
|
692
692
|
/** Top-level structure of ~/.agents/.system/agents.yaml -- the CLI's persistent state. */
|
|
693
693
|
export interface Meta {
|
|
694
694
|
agents?: Partial<Record<AgentId, string>>;
|
|
695
|
+
/**
|
|
696
|
+
* Per-agent preferred ISOLATED version — which copy a bare `agents run <agent>`
|
|
697
|
+
* resolves to when the agent has no global default.
|
|
698
|
+
*
|
|
699
|
+
* Kept separate from `agents` on purpose. An entry there is the global default,
|
|
700
|
+
* which owns the launcher, the bare shim and the real `~/.<agent>` config
|
|
701
|
+
* symlink, and arms the self-heal `shadowing` check. An isolated copy must never
|
|
702
|
+
* acquire any of that, so it cannot be recorded in the same place — the
|
|
703
|
+
* separation is what keeps `getGlobalDefault` incapable of returning one.
|
|
704
|
+
*/
|
|
705
|
+
isolatedAgents?: Partial<Record<AgentId, string>>;
|
|
695
706
|
run?: RunConfig;
|
|
696
707
|
/**
|
|
697
708
|
* `agents run --lease` config. `secretsBundle` names the keychain secrets bundle
|
package/dist/lib/uninstall.js
CHANGED
|
@@ -20,6 +20,7 @@ import * as os from 'os';
|
|
|
20
20
|
import * as path from 'path';
|
|
21
21
|
import { AGENTS, ALL_AGENT_IDS } from './agents.js';
|
|
22
22
|
import { getAgentConfigPath, getConfigSymlinkVersion, stripShimPathLines, releaseAdoptedLauncher, } from './shims.js';
|
|
23
|
+
import { moveDirCrossDevice, copyDirStrippingAgentsSymlinks } from './config-transfer.js';
|
|
23
24
|
import { getUserAgentsDir, getBackupsDir, getHistoryDir, getShimsDir, getLegacySystemAgentsDir, } from './state.js';
|
|
24
25
|
/** Home dir honoring the AGENTS_REAL_HOME test/override, mirroring shims.ts. */
|
|
25
26
|
function realHome() {
|
|
@@ -60,51 +61,6 @@ function symlinkTarget(p) {
|
|
|
60
61
|
function removeLink(p) {
|
|
61
62
|
fs.unlinkSync(p);
|
|
62
63
|
}
|
|
63
|
-
/**
|
|
64
|
-
* Move `source` onto `dest` across possibly-different volumes. `renameSync` is
|
|
65
|
-
* atomic but throws EXDEV when `~/.agents` lives on a different filesystem than
|
|
66
|
-
* `$HOME`; fall back to copy-then-remove so the restore still completes. The
|
|
67
|
-
* source (a backup inside `~/.agents`) is removed only after the copy succeeds,
|
|
68
|
-
* so a mid-copy failure never destroys the sole surviving copy.
|
|
69
|
-
*/
|
|
70
|
-
function moveDirCrossDevice(source, dest) {
|
|
71
|
-
try {
|
|
72
|
-
fs.renameSync(source, dest);
|
|
73
|
-
}
|
|
74
|
-
catch (err) {
|
|
75
|
-
if (err.code !== 'EXDEV')
|
|
76
|
-
throw err;
|
|
77
|
-
fs.cpSync(source, dest, { recursive: true });
|
|
78
|
-
fs.rmSync(source, { recursive: true, force: true });
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Copy `source` to `dest`, dropping any symlink whose target resolves back into
|
|
83
|
-
* `~/.agents`. Adoption syncs managed resources (skills/commands) into the
|
|
84
|
-
* version home as symlinks into `~/.agents`; copying them verbatim would leave
|
|
85
|
-
* the restored config full of links that dangle the moment `~/.agents` is
|
|
86
|
-
* disposed. Stripping them yields a clean, self-contained restore.
|
|
87
|
-
*/
|
|
88
|
-
function copyDirStrippingAgentsSymlinks(source, dest, agentsDir) {
|
|
89
|
-
const inside = agentsDir + path.sep;
|
|
90
|
-
fs.cpSync(source, dest, {
|
|
91
|
-
recursive: true,
|
|
92
|
-
filter: (src) => {
|
|
93
|
-
try {
|
|
94
|
-
const st = fs.lstatSync(src);
|
|
95
|
-
if (st.isSymbolicLink()) {
|
|
96
|
-
const tgt = path.resolve(path.dirname(src), fs.readlinkSync(src));
|
|
97
|
-
if (tgt === agentsDir || tgt.startsWith(inside))
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
catch {
|
|
102
|
-
/* unreadable entry — let cpSync surface it on the real copy */
|
|
103
|
-
}
|
|
104
|
-
return true;
|
|
105
|
-
},
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
64
|
/** Classify one agent's config dir without mutating anything. */
|
|
109
65
|
function planConfig(agent) {
|
|
110
66
|
const realPath = getAgentConfigPath(agent);
|
package/dist/lib/versions.d.ts
CHANGED
|
@@ -216,6 +216,19 @@ export declare function getGlobalDefault(agent: AgentId): string | null;
|
|
|
216
216
|
* Set the global default version for an agent.
|
|
217
217
|
*/
|
|
218
218
|
export declare function setGlobalDefault(agent: AgentId, version: string | undefined): void;
|
|
219
|
+
/**
|
|
220
|
+
* Get the preferred ISOLATED version for an agent — the copy a bare
|
|
221
|
+
* `agents run <agent>` falls back to when there is no global default.
|
|
222
|
+
*/
|
|
223
|
+
export declare function getIsolatedDefault(agent: AgentId): string | null;
|
|
224
|
+
/**
|
|
225
|
+
* Set (or clear, with `undefined`) the preferred isolated version.
|
|
226
|
+
*
|
|
227
|
+
* Deliberately does NOT touch the launcher, the bare shim, the `~/.<agent>` config
|
|
228
|
+
* symlink or the global default — the five things `setDefaultVersion` does. This is
|
|
229
|
+
* a pointer inside the sandbox, so it stays inside the sandbox.
|
|
230
|
+
*/
|
|
231
|
+
export declare function setIsolatedDefault(agent: AgentId, version: string | undefined): void;
|
|
219
232
|
/**
|
|
220
233
|
* Mark an installed version as an isolated install (`agents add --isolated`).
|
|
221
234
|
*
|
package/dist/lib/versions.js
CHANGED
|
@@ -32,7 +32,7 @@ import { VERSION_RE, compareVersions } from './agent-spec/primitives.js';
|
|
|
32
32
|
import { AGENTS, agentConfigDirName, getAccountEmail, resolveAgentName, formatAgentError, findInPath, isSelfUpdatingAgent } from './agents.js';
|
|
33
33
|
import { discoverPermissionGroups, getActivePermissionPresetName, readPermissionPresetRecipe, PERMISSION_PRESET_ENV_VAR } from './permissions.js';
|
|
34
34
|
import { parseMcpServerConfig, isProjectMcpTrusted } from './mcp.js';
|
|
35
|
-
import { createVersionedAlias, removeVersionedAlias, getConfigSymlinkVersion, ensureClaudeInsideSymlink } from './shims.js';
|
|
35
|
+
import { createVersionedAlias, removeVersionedAlias, getConfigSymlinkVersion, ensureClaudeInsideSymlink, assertIsolationBoundary, } from './shims.js';
|
|
36
36
|
import { importInstallScriptBinary } from './import.js';
|
|
37
37
|
import { IS_WINDOWS, composeWin32CommandLine } from './platform/index.js';
|
|
38
38
|
import { listInstalledSubagents } from './subagents.js';
|
|
@@ -1186,6 +1186,14 @@ export function getGlobalDefault(agent) {
|
|
|
1186
1186
|
* Set the global default version for an agent.
|
|
1187
1187
|
*/
|
|
1188
1188
|
export function setGlobalDefault(agent, version) {
|
|
1189
|
+
// A global default is what owns the launcher and arms the self-heal `shadowing`
|
|
1190
|
+
// check, so recording one for an isolated-only agent is the root of the original
|
|
1191
|
+
// breach. Clearing is always allowed — `removeVersion` legitimately clears a
|
|
1192
|
+
// default as the last non-isolated version goes away, which is the very moment
|
|
1193
|
+
// an agent BECOMES isolated-only.
|
|
1194
|
+
if (version !== undefined) {
|
|
1195
|
+
assertIsolationBoundary(agent, 'set a global default');
|
|
1196
|
+
}
|
|
1189
1197
|
const meta = readMeta();
|
|
1190
1198
|
if (!meta.agents) {
|
|
1191
1199
|
meta.agents = {};
|
|
@@ -1199,6 +1207,34 @@ export function setGlobalDefault(agent, version) {
|
|
|
1199
1207
|
}
|
|
1200
1208
|
writeMeta(meta);
|
|
1201
1209
|
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Get the preferred ISOLATED version for an agent — the copy a bare
|
|
1212
|
+
* `agents run <agent>` falls back to when there is no global default.
|
|
1213
|
+
*/
|
|
1214
|
+
export function getIsolatedDefault(agent) {
|
|
1215
|
+
const meta = readMeta();
|
|
1216
|
+
return meta.isolatedAgents?.[agent] || null;
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Set (or clear, with `undefined`) the preferred isolated version.
|
|
1220
|
+
*
|
|
1221
|
+
* Deliberately does NOT touch the launcher, the bare shim, the `~/.<agent>` config
|
|
1222
|
+
* symlink or the global default — the five things `setDefaultVersion` does. This is
|
|
1223
|
+
* a pointer inside the sandbox, so it stays inside the sandbox.
|
|
1224
|
+
*/
|
|
1225
|
+
export function setIsolatedDefault(agent, version) {
|
|
1226
|
+
const meta = readMeta();
|
|
1227
|
+
if (!meta.isolatedAgents) {
|
|
1228
|
+
meta.isolatedAgents = {};
|
|
1229
|
+
}
|
|
1230
|
+
if (version === undefined) {
|
|
1231
|
+
delete meta.isolatedAgents[agent];
|
|
1232
|
+
}
|
|
1233
|
+
else {
|
|
1234
|
+
meta.isolatedAgents[agent] = version;
|
|
1235
|
+
}
|
|
1236
|
+
writeMeta(meta);
|
|
1237
|
+
}
|
|
1202
1238
|
/**
|
|
1203
1239
|
* Path to the sentinel file that marks a version as an isolated install.
|
|
1204
1240
|
*
|
|
@@ -1784,6 +1820,13 @@ export function removeVersion(agent, version) {
|
|
|
1784
1820
|
console.log(chalk.yellow(`Removed the last non-isolated ${agent} version and cleared its default. Reinstall with: agents add ${agent}, then set one with: agents use ${agent}@<version>`));
|
|
1785
1821
|
}
|
|
1786
1822
|
}
|
|
1823
|
+
// Same for the isolated pointer: a removed version must not stay the answer to a
|
|
1824
|
+
// bare `agents run <agent>`. Prefer the newest remaining isolated copy so removing
|
|
1825
|
+
// one of several does not silently drop the user back to their PATH binary.
|
|
1826
|
+
if (getIsolatedDefault(agent) === version) {
|
|
1827
|
+
const survivors = listInstalledVersions(agent).filter((v) => isVersionIsolated(agent, v));
|
|
1828
|
+
setIsolatedDefault(agent, survivors.length > 0 ? survivors[survivors.length - 1] : undefined);
|
|
1829
|
+
}
|
|
1787
1830
|
// Clean up dangling config symlink if it pointed to the removed version
|
|
1788
1831
|
const symlinkVersion = getConfigSymlinkVersion(agent);
|
|
1789
1832
|
if (symlinkVersion === version) {
|
|
@@ -1855,7 +1898,23 @@ export function resolveVersion(agent, projectPath) {
|
|
|
1855
1898
|
}
|
|
1856
1899
|
}
|
|
1857
1900
|
// Fall back to global default
|
|
1858
|
-
|
|
1901
|
+
const globalDefault = getGlobalDefault(agent);
|
|
1902
|
+
if (globalDefault)
|
|
1903
|
+
return globalDefault;
|
|
1904
|
+
// Last resort: the preferred isolated copy. Strictly a fallback — a global
|
|
1905
|
+
// default always wins, so nothing changes for anyone who has one. Without this,
|
|
1906
|
+
// an isolated-only user cannot reach their installs by bare name at all: the
|
|
1907
|
+
// resolution chain ended here, so `agents run codex` fell through to whatever
|
|
1908
|
+
// `codex` meant on PATH, and only `agents run codex@<version>` worked.
|
|
1909
|
+
//
|
|
1910
|
+
// The pointer is verified on read. It survives in agents.yaml across a trash +
|
|
1911
|
+
// restore cycle, but a version removed for good would otherwise leave a dangling
|
|
1912
|
+
// pin that resolves to a directory that is not there.
|
|
1913
|
+
const isolated = getIsolatedDefault(agent);
|
|
1914
|
+
if (isolated && isVersionInstalled(agent, isolated) && isVersionIsolated(agent, isolated)) {
|
|
1915
|
+
return isolated;
|
|
1916
|
+
}
|
|
1917
|
+
return null;
|
|
1859
1918
|
}
|
|
1860
1919
|
/**
|
|
1861
1920
|
* Normalize a user-supplied @version token across CLI subcommands.
|
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.74",
|
|
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",
|