@phnx-labs/agents-cli 1.20.57 → 1.20.59
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 +30 -0
- package/README.md +40 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/defaults.js +24 -0
- package/dist/commands/exec.js +29 -5
- package/dist/commands/output.d.ts +19 -0
- package/dist/commands/output.js +333 -0
- package/dist/commands/secrets.js +34 -25
- package/dist/commands/versions.js +11 -3
- package/dist/commands/view.js +19 -4
- package/dist/index.js +2 -1
- package/dist/lib/agents.d.ts +21 -0
- package/dist/lib/agents.js +42 -14
- package/dist/lib/daemon.d.ts +5 -5
- package/dist/lib/daemon.js +65 -17
- package/dist/lib/git.d.ts +9 -0
- package/dist/lib/git.js +12 -0
- package/dist/lib/hosts/dispatch.d.ts +21 -0
- package/dist/lib/hosts/dispatch.js +88 -5
- package/dist/lib/hosts/passthrough.js +1 -0
- package/dist/lib/mcp.js +1 -1
- package/dist/lib/output/git-output.d.ts +74 -0
- package/dist/lib/output/git-output.js +213 -0
- package/dist/lib/permissions.d.ts +31 -1
- package/dist/lib/permissions.js +210 -9
- package/dist/lib/project-root.d.ts +65 -0
- package/dist/lib/project-root.js +134 -0
- package/dist/lib/resources/mcp.js +1 -1
- package/dist/lib/resources/permissions.js +5 -1
- package/dist/lib/resources/skills.js +6 -1
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/secrets/agent.d.ts +26 -23
- package/dist/lib/secrets/agent.js +196 -216
- package/dist/lib/secrets/remote.d.ts +7 -2
- package/dist/lib/secrets/remote.js +12 -10
- package/dist/lib/session/active.d.ts +3 -0
- package/dist/lib/session/active.js +1 -0
- package/dist/lib/session/db.d.ts +3 -0
- package/dist/lib/session/db.js +20 -4
- package/dist/lib/session/discover.d.ts +2 -0
- package/dist/lib/session/discover.js +40 -4
- package/dist/lib/session/parse.js +38 -15
- package/dist/lib/session/state.d.ts +4 -1
- package/dist/lib/session/state.js +18 -1
- package/dist/lib/session/types.d.ts +10 -0
- package/dist/lib/staleness/detectors/permissions.js +64 -1
- package/dist/lib/staleness/detectors/subagents.js +30 -0
- package/dist/lib/staleness/writers/commands.js +3 -3
- package/dist/lib/staleness/writers/subagents.js +13 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/subagents.d.ts +23 -0
- package/dist/lib/subagents.js +161 -12
- package/dist/lib/teams/agents.d.ts +14 -0
- package/dist/lib/teams/agents.js +158 -22
- package/dist/lib/types.d.ts +13 -0
- package/dist/lib/versions.d.ts +39 -0
- package/dist/lib/versions.js +199 -12
- package/package.json +1 -1
- package/scripts/postinstall.js +26 -11
package/dist/lib/types.d.ts
CHANGED
|
@@ -643,6 +643,12 @@ export interface Meta {
|
|
|
643
643
|
registries?: Record<RegistryType, Record<string, RegistryConfig>>;
|
|
644
644
|
versions?: Partial<Record<AgentId, Record<string, VersionResources>>>;
|
|
645
645
|
source?: string;
|
|
646
|
+
/**
|
|
647
|
+
* Projects root for the `agents run --project <slug>` shorthand, e.g.
|
|
648
|
+
* `~/src/github.com/<user>`. Auto-inferred from the repo you launch inside and
|
|
649
|
+
* cached here; stored home-relative (`~/…`) so it resolves on remote hosts too.
|
|
650
|
+
*/
|
|
651
|
+
projectRoot?: string;
|
|
646
652
|
/**
|
|
647
653
|
* Extra DotAgent repos merged after ~/.agents/. Managed clones live as peer
|
|
648
654
|
* dirs at ~/.agents-<alias>/; user-owned repos can point at arbitrary paths
|
|
@@ -771,6 +777,13 @@ export interface ClaudePermissions {
|
|
|
771
777
|
additionalDirectories?: string[];
|
|
772
778
|
};
|
|
773
779
|
}
|
|
780
|
+
/** Cursor CLI native format in ~/.cursor/cli-config.json (Shell/Read/Write/WebFetch/Mcp). */
|
|
781
|
+
export interface CursorPermissions {
|
|
782
|
+
permissions: {
|
|
783
|
+
allow: string[];
|
|
784
|
+
deny: string[];
|
|
785
|
+
};
|
|
786
|
+
}
|
|
774
787
|
/** OpenCode's native permission format (per-command allow/deny/ask). */
|
|
775
788
|
export interface OpenCodePermissions {
|
|
776
789
|
permission: {
|
package/dist/lib/versions.d.ts
CHANGED
|
@@ -114,6 +114,41 @@ export declare function getVersionDir(agent: AgentId, version: string): string;
|
|
|
114
114
|
* Get the binary path for a specific agent version.
|
|
115
115
|
*/
|
|
116
116
|
export declare function getBinaryPath(agent: AgentId, version: string): string;
|
|
117
|
+
/**
|
|
118
|
+
* Does this agent resolve to ONE global binary that is the same file regardless
|
|
119
|
+
* of the `version` argument? (droid → always `~/.local/bin/droid`.) Computed
|
|
120
|
+
* generically by probing `getBinaryPath` with two distinct versions rather than
|
|
121
|
+
* hardcoding an agent id, so it stays correct if another global-binary agent is
|
|
122
|
+
* added.
|
|
123
|
+
*
|
|
124
|
+
* This is the narrower cousin of `isSelfUpdatingAgent`: every global-binary
|
|
125
|
+
* agent is self-updating, but grok is self-updating WITHOUT a global binary — it
|
|
126
|
+
* stores a real per-version binary copy under each version-home
|
|
127
|
+
* (`versions/grok/<v>/home/.grok/downloads/grok-<v>`), so its version-homes are
|
|
128
|
+
* genuinely distinct and must NOT be collapsed. Gate the single-binary
|
|
129
|
+
* collapse/live-version logic on THIS predicate; gate pin-refusal / "switch
|
|
130
|
+
* profile" copy on `isSelfUpdatingAgent`.
|
|
131
|
+
*/
|
|
132
|
+
export declare function isGlobalBinaryAgent(agent: AgentId): boolean;
|
|
133
|
+
/** Drop the live-version cache (call after an install/remove that changes the
|
|
134
|
+
* running binary, e.g. `agents add droid@latest`). */
|
|
135
|
+
export declare function invalidateLiveVersionCache(agent?: AgentId): void;
|
|
136
|
+
/**
|
|
137
|
+
* Resolve the version the ONE globally-installed binary actually reports via
|
|
138
|
+
* `<cli> --version`, cached for {@link LIVE_VERSION_TTL_MS}. For a self-updating
|
|
139
|
+
* global-binary agent (droid) this is the single source of truth for "which
|
|
140
|
+
* version is installed" — the on-disk version-dir NAMES are just stale labels
|
|
141
|
+
* left behind by successive `agents add`/self-update cycles. Returns null when
|
|
142
|
+
* the binary isn't on PATH or the probe fails.
|
|
143
|
+
*/
|
|
144
|
+
export declare function getLiveVersion(agent: AgentId): Promise<string | null>;
|
|
145
|
+
/**
|
|
146
|
+
* Synchronous, non-blocking read of the live-version cache — returns the value
|
|
147
|
+
* only if a recent {@link getLiveVersion} call already warmed it, else null.
|
|
148
|
+
* `listInstalledVersions` is sync and must not shell out, so it prefers this
|
|
149
|
+
* warm value (accurate) and otherwise falls back to the newest on-disk dir.
|
|
150
|
+
*/
|
|
151
|
+
export declare function getCachedLiveVersion(agent: AgentId): string | null;
|
|
117
152
|
/**
|
|
118
153
|
* Get the isolated HOME directory for a specific agent version.
|
|
119
154
|
* Each version has its own config isolation (like jobs sandbox).
|
|
@@ -155,6 +190,10 @@ export declare function isOldestInstalled(agent: AgentId): Promise<{
|
|
|
155
190
|
export declare function invalidateInstalledVersionsCache(agent?: AgentId): void;
|
|
156
191
|
/**
|
|
157
192
|
* List all installed versions for an agent (cached by versions-dir mtime).
|
|
193
|
+
*
|
|
194
|
+
* For a self-updating global-binary agent (droid) every version dir resolves to
|
|
195
|
+
* the SAME binary, so this collapses them to a single canonical entry — one
|
|
196
|
+
* install, one row in `agents view`, never the phantom set of semver dir names.
|
|
158
197
|
*/
|
|
159
198
|
export declare function listInstalledVersions(agent: AgentId): string[];
|
|
160
199
|
/**
|
package/dist/lib/versions.js
CHANGED
|
@@ -28,7 +28,7 @@ import { listResources } from './resources.js';
|
|
|
28
28
|
// (single source of truth). Re-exported below so existing importers of
|
|
29
29
|
// `compareVersions` from './versions.js' keep working.
|
|
30
30
|
import { VERSION_RE, compareVersions } from './agent-spec/primitives.js';
|
|
31
|
-
import { AGENTS, agentConfigDirName, getAccountEmail, resolveAgentName, formatAgentError, findInPath } from './agents.js';
|
|
31
|
+
import { AGENTS, agentConfigDirName, getAccountEmail, resolveAgentName, formatAgentError, findInPath, isSelfUpdatingAgent } from './agents.js';
|
|
32
32
|
import { discoverPermissionGroups, getActivePermissionPresetName, readPermissionPresetRecipe, PERMISSION_PRESET_ENV_VAR } from './permissions.js';
|
|
33
33
|
import { parseMcpServerConfig } from './mcp.js';
|
|
34
34
|
import { createVersionedAlias, removeVersionedAlias, getConfigSymlinkVersion, ensureClaudeInsideSymlink } from './shims.js';
|
|
@@ -835,6 +835,65 @@ export function getBinaryPath(agent, version) {
|
|
|
835
835
|
const versionDir = getVersionDir(agent, version);
|
|
836
836
|
return path.join(versionDir, 'node_modules', '.bin', agentConfig.cliCommand);
|
|
837
837
|
}
|
|
838
|
+
/**
|
|
839
|
+
* Does this agent resolve to ONE global binary that is the same file regardless
|
|
840
|
+
* of the `version` argument? (droid → always `~/.local/bin/droid`.) Computed
|
|
841
|
+
* generically by probing `getBinaryPath` with two distinct versions rather than
|
|
842
|
+
* hardcoding an agent id, so it stays correct if another global-binary agent is
|
|
843
|
+
* added.
|
|
844
|
+
*
|
|
845
|
+
* This is the narrower cousin of `isSelfUpdatingAgent`: every global-binary
|
|
846
|
+
* agent is self-updating, but grok is self-updating WITHOUT a global binary — it
|
|
847
|
+
* stores a real per-version binary copy under each version-home
|
|
848
|
+
* (`versions/grok/<v>/home/.grok/downloads/grok-<v>`), so its version-homes are
|
|
849
|
+
* genuinely distinct and must NOT be collapsed. Gate the single-binary
|
|
850
|
+
* collapse/live-version logic on THIS predicate; gate pin-refusal / "switch
|
|
851
|
+
* profile" copy on `isSelfUpdatingAgent`.
|
|
852
|
+
*/
|
|
853
|
+
export function isGlobalBinaryAgent(agent) {
|
|
854
|
+
return getBinaryPath(agent, '0.0.0-probe-a') === getBinaryPath(agent, '0.0.0-probe-b');
|
|
855
|
+
}
|
|
856
|
+
// Live-version cache for self-updating global binaries. `<cli> --version` is a
|
|
857
|
+
// ~real shell-out, so hold the result briefly: the same `agents view` render
|
|
858
|
+
// asks for it from both listInstalledVersions (sync) and the label path (async).
|
|
859
|
+
const LIVE_VERSION_TTL_MS = 5000;
|
|
860
|
+
const liveVersionCache = new Map();
|
|
861
|
+
/** Drop the live-version cache (call after an install/remove that changes the
|
|
862
|
+
* running binary, e.g. `agents add droid@latest`). */
|
|
863
|
+
export function invalidateLiveVersionCache(agent) {
|
|
864
|
+
if (agent)
|
|
865
|
+
liveVersionCache.delete(agent);
|
|
866
|
+
else
|
|
867
|
+
liveVersionCache.clear();
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Resolve the version the ONE globally-installed binary actually reports via
|
|
871
|
+
* `<cli> --version`, cached for {@link LIVE_VERSION_TTL_MS}. For a self-updating
|
|
872
|
+
* global-binary agent (droid) this is the single source of truth for "which
|
|
873
|
+
* version is installed" — the on-disk version-dir NAMES are just stale labels
|
|
874
|
+
* left behind by successive `agents add`/self-update cycles. Returns null when
|
|
875
|
+
* the binary isn't on PATH or the probe fails.
|
|
876
|
+
*/
|
|
877
|
+
export async function getLiveVersion(agent) {
|
|
878
|
+
const cached = liveVersionCache.get(agent);
|
|
879
|
+
if (cached && Date.now() - cached.at < LIVE_VERSION_TTL_MS)
|
|
880
|
+
return cached.version;
|
|
881
|
+
const version = await getCliVersionFromPath(agent);
|
|
882
|
+
liveVersionCache.set(agent, { at: Date.now(), version });
|
|
883
|
+
return version;
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Synchronous, non-blocking read of the live-version cache — returns the value
|
|
887
|
+
* only if a recent {@link getLiveVersion} call already warmed it, else null.
|
|
888
|
+
* `listInstalledVersions` is sync and must not shell out, so it prefers this
|
|
889
|
+
* warm value (accurate) and otherwise falls back to the newest on-disk dir.
|
|
890
|
+
*/
|
|
891
|
+
export function getCachedLiveVersion(agent) {
|
|
892
|
+
const cached = liveVersionCache.get(agent);
|
|
893
|
+
if (cached && Date.now() - cached.at < LIVE_VERSION_TTL_MS)
|
|
894
|
+
return cached.version;
|
|
895
|
+
return null;
|
|
896
|
+
}
|
|
838
897
|
/**
|
|
839
898
|
* Get the isolated HOME directory for a specific agent version.
|
|
840
899
|
* Each version has its own config isolation (like jobs sandbox).
|
|
@@ -970,8 +1029,43 @@ export function invalidateInstalledVersionsCache(agent) {
|
|
|
970
1029
|
else
|
|
971
1030
|
installedVersionsCache.clear();
|
|
972
1031
|
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Choose the single canonical version-dir to represent a self-updating
|
|
1034
|
+
* global-binary agent (droid). All its version dirs map to ONE binary, so
|
|
1035
|
+
* exactly one is real; the rest are stale labels. Prefer, in order: the dir the
|
|
1036
|
+
* live config symlink points at (what actually runs), the recorded global
|
|
1037
|
+
* default, the live `--version` (when the cache is warm), else the newest dir.
|
|
1038
|
+
* `versions` MUST be sorted ascending. Callers guarantee it is non-empty.
|
|
1039
|
+
*/
|
|
1040
|
+
function pickCanonicalGlobalBinaryVersion(agent, versions) {
|
|
1041
|
+
const symlinkVersion = getConfigSymlinkVersion(agent);
|
|
1042
|
+
if (symlinkVersion && versions.includes(symlinkVersion))
|
|
1043
|
+
return symlinkVersion;
|
|
1044
|
+
const globalDefault = getGlobalDefault(agent);
|
|
1045
|
+
if (globalDefault && versions.includes(globalDefault))
|
|
1046
|
+
return globalDefault;
|
|
1047
|
+
const live = getCachedLiveVersion(agent);
|
|
1048
|
+
if (live && versions.includes(live))
|
|
1049
|
+
return live;
|
|
1050
|
+
return versions[versions.length - 1];
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Collapse a global-binary agent's phantom version dirs to the single canonical
|
|
1054
|
+
* one (see {@link pickCanonicalGlobalBinaryVersion}). No-op for npm-packaged and
|
|
1055
|
+
* per-version agents (claude/codex/grok/…), whose version dirs are genuinely
|
|
1056
|
+
* distinct installs.
|
|
1057
|
+
*/
|
|
1058
|
+
function collapseGlobalBinaryVersions(agent, versions) {
|
|
1059
|
+
if (!isGlobalBinaryAgent(agent) || versions.length === 0)
|
|
1060
|
+
return versions;
|
|
1061
|
+
return [pickCanonicalGlobalBinaryVersion(agent, versions)];
|
|
1062
|
+
}
|
|
973
1063
|
/**
|
|
974
1064
|
* List all installed versions for an agent (cached by versions-dir mtime).
|
|
1065
|
+
*
|
|
1066
|
+
* For a self-updating global-binary agent (droid) every version dir resolves to
|
|
1067
|
+
* the SAME binary, so this collapses them to a single canonical entry — one
|
|
1068
|
+
* install, one row in `agents view`, never the phantom set of semver dir names.
|
|
975
1069
|
*/
|
|
976
1070
|
export function listInstalledVersions(agent) {
|
|
977
1071
|
const agentVersionsDir = path.join(getVersionsDir(), agent);
|
|
@@ -985,7 +1079,9 @@ export function listInstalledVersions(agent) {
|
|
|
985
1079
|
}
|
|
986
1080
|
const cached = installedVersionsCache.get(agent);
|
|
987
1081
|
if (cached && cached.stamp === stamp) {
|
|
988
|
-
|
|
1082
|
+
// Collapse is applied per-call (not cached): it depends on the live-version
|
|
1083
|
+
// cache + config symlink, which can change without the versions-dir mtime.
|
|
1084
|
+
return collapseGlobalBinaryVersions(agent, cached.versions);
|
|
989
1085
|
}
|
|
990
1086
|
const entries = fs.readdirSync(agentVersionsDir, { withFileTypes: true });
|
|
991
1087
|
const versions = [];
|
|
@@ -1001,7 +1097,7 @@ export function listInstalledVersions(agent) {
|
|
|
1001
1097
|
}
|
|
1002
1098
|
versions.sort(compareVersions);
|
|
1003
1099
|
installedVersionsCache.set(agent, { stamp, versions });
|
|
1004
|
-
return versions;
|
|
1100
|
+
return collapseGlobalBinaryVersions(agent, versions);
|
|
1005
1101
|
}
|
|
1006
1102
|
/**
|
|
1007
1103
|
* List every version directory for an agent, including ones missing the
|
|
@@ -1067,18 +1163,31 @@ export async function installVersion(agent, version, onProgress, opts) {
|
|
|
1067
1163
|
if (!agentConfig.installScript) {
|
|
1068
1164
|
return { success: false, installedVersion: version, error: 'Agent has no npm package' };
|
|
1069
1165
|
}
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1166
|
+
// A self-updating agent (droid, grok, …) is a single global binary whose
|
|
1167
|
+
// installer only ever fetches the CURRENT release — there is no semver to
|
|
1168
|
+
// pin. Rather than hard-refuse `<agent>@1.2.3` (the old behavior):
|
|
1169
|
+
// - if the binary is already installed, a pin is a no-op — it self-updates
|
|
1170
|
+
// in place, so skip the installer and just refresh our bookkeeping;
|
|
1171
|
+
// - otherwise redirect the pin to a current-release install.
|
|
1172
|
+
let runInstaller = true;
|
|
1173
|
+
if (version !== 'latest' && isSelfUpdatingAgent(agent)) {
|
|
1174
|
+
const liveVersion = await getLiveVersion(agent);
|
|
1175
|
+
if (liveVersion) {
|
|
1176
|
+
onProgress?.(`${agentConfig.name} is a single self-updating binary — @${version} maps to the already-installed current release (${liveVersion}); nothing to install.`);
|
|
1177
|
+
runInstaller = false;
|
|
1178
|
+
}
|
|
1179
|
+
else {
|
|
1180
|
+
onProgress?.(`${agentConfig.name} is a single self-updating binary with no pinnable versions — installing the current release (ignoring @${version}).`);
|
|
1181
|
+
}
|
|
1182
|
+
version = 'latest';
|
|
1076
1183
|
}
|
|
1077
1184
|
let installedVersion = version;
|
|
1078
1185
|
try {
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1186
|
+
if (runInstaller) {
|
|
1187
|
+
const script = agentConfig.installScript.replaceAll('VERSION', version);
|
|
1188
|
+
onProgress?.(`Installing ${agentConfig.name}@${version} via official installer...`);
|
|
1189
|
+
await execAsync(script, { timeout: 120000 });
|
|
1190
|
+
}
|
|
1082
1191
|
if (version === 'latest') {
|
|
1083
1192
|
installedVersion = await getCliVersionFromPath(agent) || version;
|
|
1084
1193
|
// Fold any stale literal `latest` dir from an earlier probe-failed
|
|
@@ -1125,6 +1234,9 @@ export async function installVersion(agent, version, onProgress, opts) {
|
|
|
1125
1234
|
correctly reports it uninstalled. */
|
|
1126
1235
|
}
|
|
1127
1236
|
createVersionedAlias(agent, installedVersion);
|
|
1237
|
+
// The self-updating binary just changed on disk — drop the cached
|
|
1238
|
+
// `--version` so `agents view` reflects the freshly-installed release.
|
|
1239
|
+
invalidateLiveVersionCache(agent);
|
|
1128
1240
|
emit('version.install', { agent, version: installedVersion });
|
|
1129
1241
|
return { success: true, installedVersion };
|
|
1130
1242
|
}
|
|
@@ -1319,6 +1431,13 @@ function removeInstallArtifacts(versionDir) {
|
|
|
1319
1431
|
* renaming that dir would dangle the live symlink.
|
|
1320
1432
|
*/
|
|
1321
1433
|
export async function reconcileStaleLatestForAgent(agent) {
|
|
1434
|
+
// Global-binary agents (droid) can accumulate MANY stale semver dirs — not
|
|
1435
|
+
// just a literal `latest` — because every `agents add` after an in-place
|
|
1436
|
+
// self-update creates a fresh dir for the same one binary. Fold them all.
|
|
1437
|
+
if (isGlobalBinaryAgent(agent)) {
|
|
1438
|
+
await reconcileGlobalBinaryVersions(agent);
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1322
1441
|
if (!fs.existsSync(getVersionDir(agent, 'latest')))
|
|
1323
1442
|
return;
|
|
1324
1443
|
if (getConfigSymlinkVersion(agent) === 'latest')
|
|
@@ -1328,6 +1447,74 @@ export async function reconcileStaleLatestForAgent(agent) {
|
|
|
1328
1447
|
await reconcileStaleLatestDir(agent, concrete);
|
|
1329
1448
|
}
|
|
1330
1449
|
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Fold every stale version-dir of a global-binary agent (droid) into the single
|
|
1452
|
+
* canonical survivor. All its dirs point at ONE binary, so the extras — left by
|
|
1453
|
+
* successive `agents add` + in-place self-updates, and by probe-failed installs
|
|
1454
|
+
* that created a literal `latest` dir — are phantom labels. Soft-delete each
|
|
1455
|
+
* non-survivor dir (its `home/` stays recoverable via `agents restore`) so disk
|
|
1456
|
+
* converges to a single install matching what `agents view` shows.
|
|
1457
|
+
*
|
|
1458
|
+
* Survivor selection matches listInstalledVersions' canonical choice
|
|
1459
|
+
* (config-symlink target → global default → live version → newest). The survivor
|
|
1460
|
+
* is NOT renamed: droid's ~/.factory symlink points inside its home, and the
|
|
1461
|
+
* live version label is surfaced by the view renderer via getLiveVersion.
|
|
1462
|
+
*/
|
|
1463
|
+
async function reconcileGlobalBinaryVersions(agent) {
|
|
1464
|
+
// Step 1 — preserve the original literal-`latest` reconcile: a probe-failed
|
|
1465
|
+
// install leaves a `versions/<agent>/latest/` dir; fold it onto the concrete
|
|
1466
|
+
// live version (rename if that dir is absent, else trash). Skipped while the
|
|
1467
|
+
// config symlink still points at `latest` (renaming would dangle it).
|
|
1468
|
+
if (fs.existsSync(getVersionDir(agent, 'latest')) && getConfigSymlinkVersion(agent) !== 'latest') {
|
|
1469
|
+
const concrete = await getCliVersionFromPath(agent);
|
|
1470
|
+
if (concrete && concrete !== 'latest') {
|
|
1471
|
+
await reconcileStaleLatestDir(agent, concrete);
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
// Step 2 — collapse any remaining phantom SEMVER dirs (successive `agents add`
|
|
1475
|
+
// after in-place self-updates) into a single survivor.
|
|
1476
|
+
const agentVersionsDir = path.join(getVersionsDir(), agent);
|
|
1477
|
+
let entries;
|
|
1478
|
+
try {
|
|
1479
|
+
entries = fs.readdirSync(agentVersionsDir, { withFileTypes: true });
|
|
1480
|
+
}
|
|
1481
|
+
catch {
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name).sort(compareVersions);
|
|
1485
|
+
if (dirs.length <= 1)
|
|
1486
|
+
return;
|
|
1487
|
+
// Warm the live-version cache so the survivor pick (and later view labels) can
|
|
1488
|
+
// prefer the version the binary actually reports.
|
|
1489
|
+
await getLiveVersion(agent);
|
|
1490
|
+
const survivor = pickCanonicalGlobalBinaryVersion(agent, dirs);
|
|
1491
|
+
const symlinkVersion = getConfigSymlinkVersion(agent);
|
|
1492
|
+
let foldedAny = false;
|
|
1493
|
+
for (const version of dirs) {
|
|
1494
|
+
if (version === survivor)
|
|
1495
|
+
continue;
|
|
1496
|
+
// Never trash the dir the live config symlink points at — that would dangle
|
|
1497
|
+
// the symlink. pickCanonical already prefers it as survivor; this guards the
|
|
1498
|
+
// rare mismatch.
|
|
1499
|
+
if (version === symlinkVersion)
|
|
1500
|
+
continue;
|
|
1501
|
+
const staleDir = getVersionDir(agent, version);
|
|
1502
|
+
const trashPath = softDeleteVersionDir(agent, version);
|
|
1503
|
+
if (trashPath) {
|
|
1504
|
+
const { updateSessionFilePaths } = await import('./session/db.js');
|
|
1505
|
+
updateSessionFilePaths(staleDir, trashPath);
|
|
1506
|
+
foldedAny = true;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
if (foldedAny) {
|
|
1510
|
+
invalidateInstalledVersionsCache(agent);
|
|
1511
|
+
// Keep the recorded default pointing at a dir that still exists on disk.
|
|
1512
|
+
const def = getGlobalDefault(agent);
|
|
1513
|
+
if (!def || !fs.existsSync(getVersionDir(agent, def))) {
|
|
1514
|
+
setGlobalDefault(agent, survivor);
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1331
1518
|
export async function reconcileStaleLatestDir(agent, installedVersion) {
|
|
1332
1519
|
if (installedVersion === 'latest')
|
|
1333
1520
|
return 'none';
|
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.59",
|
|
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",
|
package/scripts/postinstall.js
CHANGED
|
@@ -386,23 +386,38 @@ function retargetManagedLinksToNativeBin() {
|
|
|
386
386
|
async function healLongRunningProcesses() {
|
|
387
387
|
if (process.platform !== 'darwin') return;
|
|
388
388
|
if (process.env.CI || process.env.AGENTS_NO_HEAL === '1') return;
|
|
389
|
+
|
|
390
|
+
// Retire the legacy standalone secrets-agent service FIRST (#416 step 2) so
|
|
391
|
+
// that when the daemon (re)starts below its coexistence guard sees no
|
|
392
|
+
// standalone broker and hosts the broker socket itself. No-op if no legacy
|
|
393
|
+
// plist is present; never blocks.
|
|
394
|
+
let retiredLegacyBroker = false;
|
|
395
|
+
try {
|
|
396
|
+
const a = await import('../dist/lib/secrets/agent.js');
|
|
397
|
+
if (a.secretsAgentServiceInstalled?.()) {
|
|
398
|
+
a.retireLegacySecretsAgentService?.();
|
|
399
|
+
retiredLegacyBroker = true;
|
|
400
|
+
console.log(' Retired the standalone secrets-agent service — the daemon now hosts the broker.');
|
|
401
|
+
}
|
|
402
|
+
} catch { /* best effort */ }
|
|
403
|
+
|
|
389
404
|
// Routines daemon: restart so it reloads new code (e.g. picks up keychain
|
|
390
|
-
// read-memoization / broker fast-path that a stale daemon wouldn't have)
|
|
405
|
+
// read-memoization / broker fast-path that a stale daemon wouldn't have) and
|
|
406
|
+
// (re)hosts the broker socket now the legacy service is gone. If it wasn't
|
|
407
|
+
// running but we just retired a broker the user relied on, start it so the
|
|
408
|
+
// socket has a host.
|
|
391
409
|
try {
|
|
392
410
|
const d = await import('../dist/lib/daemon.js');
|
|
393
411
|
if (d.isDaemonRunning?.()) {
|
|
394
412
|
d.stopDaemon?.();
|
|
395
|
-
|
|
413
|
+
// This lifecycle script is process.argv[1] while npm is installing. Pin
|
|
414
|
+
// the restart to the installed CLI entrypoint so the service manifest
|
|
415
|
+
// never records scripts/postinstall.js as the daemon command.
|
|
416
|
+
d.startDaemon?.(AGENTS_BIN);
|
|
396
417
|
console.log(' Restarted the routines daemon onto this version.');
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
// new code. No-op if the service isn't installed; never blocks.
|
|
401
|
-
try {
|
|
402
|
-
const a = await import('../dist/lib/secrets/agent.js');
|
|
403
|
-
if (a.secretsAgentServiceInstalled?.()) {
|
|
404
|
-
a.kickstartSecretsAgentService?.();
|
|
405
|
-
console.log(' Reloaded the secrets-agent service onto this version.');
|
|
418
|
+
} else if (retiredLegacyBroker) {
|
|
419
|
+
d.startDaemon?.(AGENTS_BIN);
|
|
420
|
+
console.log(' Started the daemon to host the secrets broker.');
|
|
406
421
|
}
|
|
407
422
|
} catch { /* best effort */ }
|
|
408
423
|
}
|