@phnx-labs/agents-cli 1.20.3 → 1.20.4
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 +8 -0
- package/dist/commands/import.js +90 -37
- package/dist/commands/view.js +5 -0
- package/dist/lib/agents.js +23 -3
- package/dist/lib/import.d.ts +21 -0
- package/dist/lib/import.js +55 -2
- package/dist/lib/plugin-marketplace.d.ts +10 -0
- package/dist/lib/plugin-marketplace.js +47 -1
- package/dist/lib/pty-server.js +27 -3
- package/dist/lib/versions.js +21 -0
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.20.4
|
|
4
|
+
|
|
5
|
+
**Plugin marketplace sync (skip outside-pointing symlinks)**
|
|
6
|
+
|
|
7
|
+
- `copyPluginToMarketplace` used `fs.cpSync(plugin.root, dest, { recursive: true, dereference: false })`, which faithfully preserved every symlink — including the ones plugin authors put at the top of their plugin source for prompt-side references (the rush plugin's `app -> ../../../rush/app`, `web -> rush/web`, `widgets -> rush/widgets`). Those targets resolve to the rush monorepo (~8.7 GB of `app/` including node_modules + .next builds, 782 MB of `web/`, plus 463 MB brand-assets). Every claude version got a full set of those symlinks in `~/.claude/plugins/marketplaces/agents-cli/plugins/rush/`. When the consumer (Claude Code, OpenClaw) discovers plugins, it walks the marketplace tree and follows those symlinks — producing multi-minute startup hangs.
|
|
8
|
+
- The copy now walks the source tree and drops symlinks whose `realpath` escapes the plugin root, leaving internal symlinks intact (cpSync rewrites internal targets to absolute paths into the source tree, which the consumer still resolves correctly). One informational line per plugin lists the skipped names so plugin authors notice.
|
|
9
|
+
- Existing per-version marketplace directories still hold the bloat from prior syncs; clean up with `rm` against `~/.claude/plugins/marketplaces/agents-cli/plugins/*/{app,web,widgets,*-symlinks-that-escaped}` then re-run `agents pull` or any plugin sync to re-copy with the filter.
|
|
10
|
+
|
|
3
11
|
## 1.20.3
|
|
4
12
|
|
|
5
13
|
**`agents run` startup latency (stale-while-revalidate the usage probe + memoize agents.yaml)**
|
package/dist/commands/import.js
CHANGED
|
@@ -23,12 +23,13 @@
|
|
|
23
23
|
import chalk from 'chalk';
|
|
24
24
|
import ora from 'ora';
|
|
25
25
|
import * as fs from 'fs';
|
|
26
|
+
import * as os from 'os';
|
|
26
27
|
import * as path from 'path';
|
|
27
28
|
import { confirm } from '@inquirer/prompts';
|
|
28
29
|
import { ALL_AGENT_IDS } from '../lib/agents.js';
|
|
29
30
|
import { AGENTS, getCliPath, getCliVersion, agentLabel } from '../lib/agents.js';
|
|
30
31
|
import { getVersionDir } from '../lib/versions.js';
|
|
31
|
-
import { finalizeImport, importAgentBinary, importAgentConfig, isValidImportVersion, resolvePackageDirFromBinary, } from '../lib/import.js';
|
|
32
|
+
import { finalizeImport, importAgentBinary, importAgentConfig, importInstallScriptBinary, isValidImportVersion, resolvePackageDirFromBinary, } from '../lib/import.js';
|
|
32
33
|
import { isPromptCancelled, isInteractiveTerminal } from './utils.js';
|
|
33
34
|
function isValidAgentId(value) {
|
|
34
35
|
return ALL_AGENT_IDS.includes(value);
|
|
@@ -41,54 +42,94 @@ async function runImport(agentArg, opts) {
|
|
|
41
42
|
}
|
|
42
43
|
const agentId = agentArg;
|
|
43
44
|
const agent = AGENTS[agentId];
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
console.error(chalk.gray(`Use \`agents add ${agentId}\` to install via its native script.`));
|
|
50
|
-
process.exit(1);
|
|
51
|
-
}
|
|
45
|
+
// installScript-based agents (Grok, Antigravity, Cursor, Kiro, Goose, Roo)
|
|
46
|
+
// don't have an npm package; their binary lives wherever the curl/brew
|
|
47
|
+
// installer dropped it. We adopt by symlinking that PATH binary directly
|
|
48
|
+
// into the version's `node_modules/.bin/`. No package.json walk.
|
|
49
|
+
const isInstallScriptAgent = !agent.npmPackage;
|
|
52
50
|
let globalPath = null;
|
|
51
|
+
let installScriptBinary = null;
|
|
53
52
|
if (opts.fromPath) {
|
|
54
53
|
globalPath = path.resolve(opts.fromPath);
|
|
55
54
|
if (!fs.existsSync(globalPath)) {
|
|
56
55
|
console.error(chalk.red(`Path does not exist: ${globalPath}`));
|
|
57
56
|
process.exit(1);
|
|
58
57
|
}
|
|
58
|
+
if (isInstallScriptAgent) {
|
|
59
|
+
// With --from-path on an installScript agent, the path is the binary
|
|
60
|
+
// itself (or a directory containing it). Accept either.
|
|
61
|
+
if (fs.statSync(globalPath).isDirectory()) {
|
|
62
|
+
const candidate = path.join(globalPath, agent.cliCommand);
|
|
63
|
+
if (!fs.existsSync(candidate)) {
|
|
64
|
+
console.error(chalk.red(`No "${agent.cliCommand}" in ${globalPath}`));
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
installScriptBinary = candidate;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
installScriptBinary = globalPath;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
59
73
|
}
|
|
60
74
|
else {
|
|
61
75
|
const binary = await getCliPath(agentId);
|
|
62
76
|
if (!binary) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const installName = agent.npmPackage || agent.cliCommand;
|
|
77
|
+
const installHint = isInstallScriptAgent
|
|
78
|
+
? `Run \`agents add ${agentId}\` to install via the official script, or pass --from-path.`
|
|
79
|
+
: `Install it first (e.g. \`npm i -g ${agent.npmPackage || agent.cliCommand}\`) or pass --from-path.`;
|
|
67
80
|
console.error(chalk.red(`No "${agent.cliCommand}" found on PATH.`));
|
|
68
|
-
console.error(chalk.gray(
|
|
81
|
+
console.error(chalk.gray(installHint));
|
|
69
82
|
process.exit(1);
|
|
70
83
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
84
|
+
if (isInstallScriptAgent) {
|
|
85
|
+
installScriptBinary = binary;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
globalPath = resolvePackageDirFromBinary(binary);
|
|
89
|
+
if (!globalPath) {
|
|
90
|
+
console.error(chalk.red(`Could not resolve npm package for binary: ${binary}`));
|
|
91
|
+
console.error(chalk.gray('Pass --from-path <dir> with the package directory explicitly.'));
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// For Grok, the binary on PATH is typically `~/.grok/bin/grok` (a moving
|
|
97
|
+
// pointer to the latest install). Prefer the exact versioned file in
|
|
98
|
+
// `~/.grok/downloads/` so the v<x.y.z> alias is pinned to that file and
|
|
99
|
+
// doesn't drift when the user upgrades externally.
|
|
100
|
+
if (isInstallScriptAgent && agentId === 'grok' && !opts.fromPath) {
|
|
101
|
+
const detected = await getCliVersion(agentId);
|
|
102
|
+
if (detected) {
|
|
103
|
+
const downloads = path.join(os.homedir(), '.grok', 'downloads');
|
|
104
|
+
try {
|
|
105
|
+
const entries = fs.readdirSync(downloads);
|
|
106
|
+
const exact = entries.find((e) => e.startsWith('grok-') && e.includes(detected));
|
|
107
|
+
if (exact) {
|
|
108
|
+
installScriptBinary = path.join(downloads, exact);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
/* fall back to PATH binary already set above */
|
|
113
|
+
}
|
|
76
114
|
}
|
|
77
115
|
}
|
|
78
116
|
let version = opts.version;
|
|
79
117
|
if (!version) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
118
|
+
if (!isInstallScriptAgent && globalPath) {
|
|
119
|
+
try {
|
|
120
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(globalPath, 'package.json'), 'utf8'));
|
|
121
|
+
version = typeof pkg.version === 'string' ? pkg.version : undefined;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
/* fall through */
|
|
125
|
+
}
|
|
86
126
|
}
|
|
87
127
|
// Only fall back to running the PATH binary's --version when we're
|
|
88
|
-
// auto-detecting. With --from-path, the PATH binary may
|
|
89
|
-
// different install entirely; reporting its version here
|
|
90
|
-
// mis-attribute the imported version.
|
|
91
|
-
|
|
128
|
+
// auto-detecting. With --from-path on an npm agent, the PATH binary may
|
|
129
|
+
// belong to a different install entirely; reporting its version here
|
|
130
|
+
// would silently mis-attribute the imported version. installScript agents
|
|
131
|
+
// always use `<bin> --version` since they have no package.json to read.
|
|
132
|
+
if (!version && (isInstallScriptAgent || !opts.fromPath)) {
|
|
92
133
|
const detected = await getCliVersion(agentId);
|
|
93
134
|
version = detected ?? undefined;
|
|
94
135
|
}
|
|
@@ -104,8 +145,9 @@ async function runImport(agentArg, opts) {
|
|
|
104
145
|
process.exit(1);
|
|
105
146
|
}
|
|
106
147
|
const versionDir = getVersionDir(agentId, version);
|
|
148
|
+
const fromLabel = isInstallScriptAgent ? installScriptBinary : globalPath;
|
|
107
149
|
console.log(chalk.bold(`\nImport ${agentLabel(agentId)} v${version}`));
|
|
108
|
-
console.log(` from: ${chalk.gray(
|
|
150
|
+
console.log(` from: ${chalk.gray(fromLabel)}`);
|
|
109
151
|
console.log(` into: ${chalk.gray(versionDir)}`);
|
|
110
152
|
const configDirExists = fs.existsSync(agent.configDir);
|
|
111
153
|
let configAlreadyManaged = false;
|
|
@@ -146,7 +188,8 @@ async function runImport(agentArg, opts) {
|
|
|
146
188
|
const cfgSpinner = ora(`Importing config dir for ${agentLabel(agentId)} v${version}...`).start();
|
|
147
189
|
const cfgResult = await importAgentConfig(agentId, version);
|
|
148
190
|
if (cfgResult.success) {
|
|
149
|
-
|
|
191
|
+
const relConfig = path.relative(os.homedir(), agent.configDir);
|
|
192
|
+
cfgSpinner.succeed(`Config imported (${agent.configDir} -> ${versionDir}/home/${relConfig})`);
|
|
150
193
|
}
|
|
151
194
|
else if (cfgResult.skipped) {
|
|
152
195
|
cfgSpinner.warn(`Config: ${cfgResult.error}`);
|
|
@@ -157,9 +200,11 @@ async function runImport(agentArg, opts) {
|
|
|
157
200
|
}
|
|
158
201
|
}
|
|
159
202
|
const binSpinner = ora(`Registering ${agentLabel(agentId)} v${version} binary...`).start();
|
|
160
|
-
const binResult =
|
|
203
|
+
const binResult = isInstallScriptAgent
|
|
204
|
+
? importInstallScriptBinary({ agentId, npmPackage: agent.npmPackage, cliCommand: agent.cliCommand }, version, installScriptBinary, versionDir)
|
|
205
|
+
: importAgentBinary({ agentId, npmPackage: agent.npmPackage, cliCommand: agent.cliCommand }, version, globalPath, versionDir);
|
|
161
206
|
if (binResult.success) {
|
|
162
|
-
binSpinner.succeed(`Binary registered (${agent.cliCommand} -> ${
|
|
207
|
+
binSpinner.succeed(`Binary registered (${agent.cliCommand} -> ${binResult.resolvedFromPath})`);
|
|
163
208
|
}
|
|
164
209
|
else if (binResult.skipped) {
|
|
165
210
|
binSpinner.warn(`Binary: ${binResult.error}`);
|
|
@@ -198,11 +243,19 @@ Examples:
|
|
|
198
243
|
$ agents import openclaw --version 2026.3.8 Pin a version label
|
|
199
244
|
$ agents import openclaw --from-path /opt/homebrew/lib/node_modules/openclaw
|
|
200
245
|
|
|
246
|
+
# installScript-based agents (curl/brew installers, no npm package):
|
|
247
|
+
$ agents import grok Adopt ~/.grok/downloads/grok-<ver>
|
|
248
|
+
$ agents import antigravity Adopt ~/.local/bin/agy
|
|
249
|
+
$ agents import cursor Adopt ~/.local/bin/cursor-agent
|
|
250
|
+
$ agents import antigravity --from-path ~/.local/bin/agy
|
|
251
|
+
|
|
201
252
|
When to use:
|
|
202
|
-
When an agent CLI is already installed globally
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
253
|
+
When an agent CLI is already installed globally and you want to bring it
|
|
254
|
+
under agents-cli management without reinstalling. Creates a symlink farm
|
|
255
|
+
pointing at the existing install — nothing is copied or moved (except the
|
|
256
|
+
agent's config dir, which is moved into the version's home). Works for both
|
|
257
|
+
npm-style packages (claude, codex, gemini, opencode, openclaw) and
|
|
258
|
+
installScript-based agents (grok, antigravity, cursor, kiro, goose, roo).
|
|
206
259
|
`)
|
|
207
260
|
.action(runImport);
|
|
208
261
|
}
|
package/dist/commands/view.js
CHANGED
|
@@ -455,6 +455,11 @@ async function showInstalledVersions(filterAgentId) {
|
|
|
455
455
|
if (agent.npmPackage && cliState?.version) {
|
|
456
456
|
console.log(chalk.gray(` Manage: agents add ${agentId}@${cliState.version} -y`));
|
|
457
457
|
}
|
|
458
|
+
else if (!agent.npmPackage && cliState?.installed) {
|
|
459
|
+
// installScript-based agent already on PATH — direct users to adopt the
|
|
460
|
+
// existing install with `agents import` instead of re-running curl.
|
|
461
|
+
console.log(chalk.gray(` Adopt: agents import ${agentId}`));
|
|
462
|
+
}
|
|
458
463
|
console.log();
|
|
459
464
|
}
|
|
460
465
|
}
|
package/dist/lib/agents.js
CHANGED
|
@@ -55,13 +55,25 @@ function saveCliVersionCache() {
|
|
|
55
55
|
/* best-effort cache persist */
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* Synchronous PATH search -- no subprocess. Returns first matching binary path.
|
|
60
|
+
*
|
|
61
|
+
* Skips our own shims dir (`~/.agents/.cache/shims/`) — those shims are
|
|
62
|
+
* dispatch helpers, not real installs. Counting them as installed produced a
|
|
63
|
+
* false positive where agents with NO real binary on the host (e.g. a
|
|
64
|
+
* never-installed Cursor whose only PATH entry was our `cursor-agent` shim
|
|
65
|
+
* dispatcher) showed up under `agents view`'s "Not Managed by Agents CLI"
|
|
66
|
+
* section, even though the user had nothing to import.
|
|
67
|
+
*/
|
|
59
68
|
function findInPath(command) {
|
|
60
69
|
const pathEnv = process.env.PATH || '';
|
|
61
70
|
const pathExt = process.platform === 'win32' ? (process.env.PATHEXT || '').split(';') : [''];
|
|
71
|
+
const shimsDir = getShimsDir();
|
|
62
72
|
for (const dir of pathEnv.split(path.delimiter)) {
|
|
63
73
|
if (!dir)
|
|
64
74
|
continue;
|
|
75
|
+
if (path.resolve(dir) === path.resolve(shimsDir))
|
|
76
|
+
continue;
|
|
65
77
|
for (const ext of pathExt) {
|
|
66
78
|
const full = path.join(dir, command + ext);
|
|
67
79
|
try {
|
|
@@ -507,8 +519,16 @@ async function getCachedVersionForBinary(agentId, binaryPath) {
|
|
|
507
519
|
/* version command failed */
|
|
508
520
|
version = null;
|
|
509
521
|
}
|
|
510
|
-
|
|
511
|
-
|
|
522
|
+
// Skip persisting null results — the most common cause is a transient
|
|
523
|
+
// `--version` failure (slow startup, stdout race, etc.). A sticky-null
|
|
524
|
+
// entry kept users in a broken state where every subsequent
|
|
525
|
+
// `getCachedVersionForBinary` short-circuited to null forever, even
|
|
526
|
+
// after the binary started working. Re-probing on the next call costs
|
|
527
|
+
// one execFile; persisting null costs the whole feature.
|
|
528
|
+
if (version !== null) {
|
|
529
|
+
cache[agentId] = { binaryPath, mtime, version };
|
|
530
|
+
saveCliVersionCache();
|
|
531
|
+
}
|
|
512
532
|
return version;
|
|
513
533
|
}
|
|
514
534
|
/**
|
package/dist/lib/import.d.ts
CHANGED
|
@@ -81,6 +81,27 @@ export interface AgentBinarySpec {
|
|
|
81
81
|
* node_modules/.bin/{cliCommand} -> {binaryEntry}
|
|
82
82
|
*/
|
|
83
83
|
export declare function importAgentBinary(spec: AgentBinarySpec, version: string, globalPath: string, versionDir: string): ImportBinaryResult;
|
|
84
|
+
/**
|
|
85
|
+
* Register an existing installScript-based binary (Grok, Antigravity, Cursor,
|
|
86
|
+
* etc. — anything with `npmPackage: ''` and a curl/brew installer) under the
|
|
87
|
+
* managed version path. Unlike `importAgentBinary` this skips the npm
|
|
88
|
+
* package.json walk and just symlinks the resolved PATH binary directly into
|
|
89
|
+
* `{versionDir}/node_modules/.bin/{cliCommand}`. The symlink is what makes
|
|
90
|
+
* `listInstalledVersions` consider the version Managed.
|
|
91
|
+
*
|
|
92
|
+
* Layout produced:
|
|
93
|
+
*
|
|
94
|
+
* {versionDir}/
|
|
95
|
+
* package.json # marker (private, imported, from)
|
|
96
|
+
* home/ # empty isolated $HOME
|
|
97
|
+
* node_modules/.bin/{cliCommand} -> {binaryPath}
|
|
98
|
+
*
|
|
99
|
+
* For agents whose binary lookup is special-cased elsewhere (e.g. Grok's
|
|
100
|
+
* `~/.grok/downloads/`), the symlink is still created — `getBinaryPath` won't
|
|
101
|
+
* read it for those agents, but it documents provenance and lets a future
|
|
102
|
+
* refactor consolidate the binary-resolution registry.
|
|
103
|
+
*/
|
|
104
|
+
export declare function importInstallScriptBinary(spec: AgentBinarySpec, version: string, binaryPath: string, versionDir: string): ImportBinaryResult;
|
|
84
105
|
/**
|
|
85
106
|
* Resolve the on-disk npm package directory for an agent's CLI binary by
|
|
86
107
|
* walking up from the binary, following any symlinks. Returns null if the
|
package/dist/lib/import.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* true.
|
|
18
18
|
*/
|
|
19
19
|
import * as fs from 'fs';
|
|
20
|
+
import * as os from 'os';
|
|
20
21
|
import * as path from 'path';
|
|
21
22
|
import { AGENTS } from './agents.js';
|
|
22
23
|
import { getVersionsDir } from './state.js';
|
|
@@ -42,12 +43,17 @@ export async function importAgentConfig(agentId, version) {
|
|
|
42
43
|
const configDir = agent.configDir;
|
|
43
44
|
const versionsDir = getVersionsDir();
|
|
44
45
|
const versionHome = path.join(versionsDir, agentId, version, 'home');
|
|
45
|
-
|
|
46
|
+
// Match the shim's derivation in generateShimScript: the per-version config
|
|
47
|
+
// path mirrors the original configDir's path relative to $HOME. Hardcoding
|
|
48
|
+
// `.${agentId}` broke for nested configDirs like Antigravity
|
|
49
|
+
// (`~/.gemini/antigravity-cli`) — the destination would be `.antigravity`,
|
|
50
|
+
// mismatching the shim's expectation of `.gemini/antigravity-cli`.
|
|
51
|
+
const versionConfigDir = path.join(versionHome, path.relative(os.homedir(), configDir));
|
|
46
52
|
if (fs.existsSync(versionConfigDir)) {
|
|
47
53
|
return { success: false, skipped: true, error: `${version} already installed` };
|
|
48
54
|
}
|
|
49
55
|
try {
|
|
50
|
-
fs.mkdirSync(
|
|
56
|
+
fs.mkdirSync(path.dirname(versionConfigDir), { recursive: true });
|
|
51
57
|
fs.renameSync(configDir, versionConfigDir);
|
|
52
58
|
fs.symlinkSync(versionConfigDir, configDir);
|
|
53
59
|
setGlobalDefault(agentId, version);
|
|
@@ -155,6 +161,53 @@ export function importAgentBinary(spec, version, globalPath, versionDir) {
|
|
|
155
161
|
return { success: false, error: err.message };
|
|
156
162
|
}
|
|
157
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Register an existing installScript-based binary (Grok, Antigravity, Cursor,
|
|
166
|
+
* etc. — anything with `npmPackage: ''` and a curl/brew installer) under the
|
|
167
|
+
* managed version path. Unlike `importAgentBinary` this skips the npm
|
|
168
|
+
* package.json walk and just symlinks the resolved PATH binary directly into
|
|
169
|
+
* `{versionDir}/node_modules/.bin/{cliCommand}`. The symlink is what makes
|
|
170
|
+
* `listInstalledVersions` consider the version Managed.
|
|
171
|
+
*
|
|
172
|
+
* Layout produced:
|
|
173
|
+
*
|
|
174
|
+
* {versionDir}/
|
|
175
|
+
* package.json # marker (private, imported, from)
|
|
176
|
+
* home/ # empty isolated $HOME
|
|
177
|
+
* node_modules/.bin/{cliCommand} -> {binaryPath}
|
|
178
|
+
*
|
|
179
|
+
* For agents whose binary lookup is special-cased elsewhere (e.g. Grok's
|
|
180
|
+
* `~/.grok/downloads/`), the symlink is still created — `getBinaryPath` won't
|
|
181
|
+
* read it for those agents, but it documents provenance and lets a future
|
|
182
|
+
* refactor consolidate the binary-resolution registry.
|
|
183
|
+
*/
|
|
184
|
+
export function importInstallScriptBinary(spec, version, binaryPath, versionDir) {
|
|
185
|
+
const binaryLink = path.join(versionDir, 'node_modules', '.bin', spec.cliCommand);
|
|
186
|
+
let alreadyExists = false;
|
|
187
|
+
try {
|
|
188
|
+
fs.lstatSync(binaryLink);
|
|
189
|
+
alreadyExists = true;
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
/* not present */
|
|
193
|
+
}
|
|
194
|
+
if (alreadyExists) {
|
|
195
|
+
return { success: false, skipped: true, error: `${version} already installed`, resolvedFromPath: binaryPath };
|
|
196
|
+
}
|
|
197
|
+
if (!fs.existsSync(binaryPath)) {
|
|
198
|
+
return { success: false, error: `Binary does not exist: ${binaryPath}` };
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
fs.mkdirSync(path.join(versionDir, 'home'), { recursive: true });
|
|
202
|
+
fs.mkdirSync(path.join(versionDir, 'node_modules', '.bin'), { recursive: true });
|
|
203
|
+
fs.writeFileSync(path.join(versionDir, 'package.json'), JSON.stringify({ name: `agents-${spec.agentId}-${version}`, version: '1.0.0', private: true, imported: true, from: binaryPath, installScriptBased: true }, null, 2));
|
|
204
|
+
fs.symlinkSync(binaryPath, binaryLink);
|
|
205
|
+
return { success: true, resolvedFromPath: binaryPath };
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
return { success: false, error: err.message };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
158
211
|
/**
|
|
159
212
|
* Resolve the on-disk npm package directory for an agent's CLI binary by
|
|
160
213
|
* walking up from the binary, following any symlinks. Returns null if the
|
|
@@ -45,6 +45,16 @@ export declare function knownMarketplacesPath(agent: AgentId, versionHome: strin
|
|
|
45
45
|
/**
|
|
46
46
|
* Copy plugin source into marketplace install dir.
|
|
47
47
|
* Source of truth remains ~/.agents/plugins/<name>/ — this is a per-version snapshot.
|
|
48
|
+
*
|
|
49
|
+
* Symlinks pointing OUTSIDE the plugin source root are dropped. They show up
|
|
50
|
+
* when plugin authors (legitimately) link prompt-side references to sibling
|
|
51
|
+
* codebases — e.g. the rush plugin's `app -> ../../../rush/app` for @app/...
|
|
52
|
+
* autocomplete in user prompts. Faithfully copying those symlinks pollutes
|
|
53
|
+
* the marketplace with gigabytes of node_modules / .next / brand-asset video
|
|
54
|
+
* that the consumer (Claude Code, OpenClaw) then walks during plugin
|
|
55
|
+
* discovery — which is the documented cause of multi-minute startup hangs.
|
|
56
|
+
*
|
|
57
|
+
* Internal symlinks (target stays inside the plugin root) are preserved.
|
|
48
58
|
*/
|
|
49
59
|
export declare function copyPluginToMarketplace(plugin: DiscoveredPlugin, agent: AgentId, versionHome: string): string;
|
|
50
60
|
/**
|
|
@@ -40,6 +40,16 @@ function settingsPath(agent, versionHome) {
|
|
|
40
40
|
/**
|
|
41
41
|
* Copy plugin source into marketplace install dir.
|
|
42
42
|
* Source of truth remains ~/.agents/plugins/<name>/ — this is a per-version snapshot.
|
|
43
|
+
*
|
|
44
|
+
* Symlinks pointing OUTSIDE the plugin source root are dropped. They show up
|
|
45
|
+
* when plugin authors (legitimately) link prompt-side references to sibling
|
|
46
|
+
* codebases — e.g. the rush plugin's `app -> ../../../rush/app` for @app/...
|
|
47
|
+
* autocomplete in user prompts. Faithfully copying those symlinks pollutes
|
|
48
|
+
* the marketplace with gigabytes of node_modules / .next / brand-asset video
|
|
49
|
+
* that the consumer (Claude Code, OpenClaw) then walks during plugin
|
|
50
|
+
* discovery — which is the documented cause of multi-minute startup hangs.
|
|
51
|
+
*
|
|
52
|
+
* Internal symlinks (target stays inside the plugin root) are preserved.
|
|
43
53
|
*/
|
|
44
54
|
export function copyPluginToMarketplace(plugin, agent, versionHome) {
|
|
45
55
|
const dest = pluginInstallDir(plugin, agent, versionHome);
|
|
@@ -47,7 +57,43 @@ export function copyPluginToMarketplace(plugin, agent, versionHome) {
|
|
|
47
57
|
if (fs.existsSync(dest)) {
|
|
48
58
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
49
59
|
}
|
|
50
|
-
|
|
60
|
+
const sourceRealRoot = (() => {
|
|
61
|
+
try {
|
|
62
|
+
return fs.realpathSync(plugin.root);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return plugin.root;
|
|
66
|
+
}
|
|
67
|
+
})();
|
|
68
|
+
const skipped = [];
|
|
69
|
+
fs.cpSync(plugin.root, dest, {
|
|
70
|
+
recursive: true,
|
|
71
|
+
dereference: false,
|
|
72
|
+
filter: (src) => {
|
|
73
|
+
try {
|
|
74
|
+
const stat = fs.lstatSync(src);
|
|
75
|
+
if (!stat.isSymbolicLink())
|
|
76
|
+
return true;
|
|
77
|
+
const target = fs.realpathSync(src);
|
|
78
|
+
if (target === sourceRealRoot || target.startsWith(sourceRealRoot + path.sep)) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
skipped.push(path.relative(plugin.root, src) || path.basename(src));
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Dangling symlink or stat failure — drop it; it can't be useful in
|
|
86
|
+
// the marketplace and would error the consumer's walk anyway.
|
|
87
|
+
skipped.push(path.relative(plugin.root, src) || path.basename(src));
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
if (skipped.length > 0) {
|
|
93
|
+
process.stderr.write(`agents-cli: plugin '${plugin.name}' has ${skipped.length} symlink(s) ` +
|
|
94
|
+
`pointing outside its source root; not copied to marketplace ` +
|
|
95
|
+
`(would bloat consumer startup): ${skipped.join(', ')}\n`);
|
|
96
|
+
}
|
|
51
97
|
return dest;
|
|
52
98
|
}
|
|
53
99
|
/**
|
package/dist/lib/pty-server.js
CHANGED
|
@@ -192,7 +192,33 @@ export async function runPtyServer() {
|
|
|
192
192
|
}
|
|
193
193
|
const sessions = new Map();
|
|
194
194
|
const socketPath = getSocketPath();
|
|
195
|
-
|
|
195
|
+
const pidPath = getPtyPidPath();
|
|
196
|
+
// Race resolution must happen BEFORE touching the socket file. Two clients
|
|
197
|
+
// racing ensureServer() in pty-client.ts can both observe
|
|
198
|
+
// isPtyServerRunning()=false and spawn parallel servers; without the
|
|
199
|
+
// O_EXCL claim below, the second spawn would unlink the first's socket
|
|
200
|
+
// inode and overwrite its PID file, orphaning the first server with its
|
|
201
|
+
// kernel socket binding intact but unreachable via the filesystem.
|
|
202
|
+
if (isPtyServerRunning()) {
|
|
203
|
+
log('INFO', 'PTY server already running; duplicate spawn exits cleanly');
|
|
204
|
+
process.exit(0);
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
fs.writeFileSync(pidPath, String(process.pid), { flag: 'wx', encoding: 'utf-8' });
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
if (err && err.code === 'EEXIST') {
|
|
211
|
+
log('INFO', 'PID slot claimed by a concurrent server; exiting cleanly');
|
|
212
|
+
process.exit(0);
|
|
213
|
+
}
|
|
214
|
+
throw err;
|
|
215
|
+
}
|
|
216
|
+
// We own the PID slot; ensure it's released on any exit path, not just SIGTERM/SIGINT.
|
|
217
|
+
process.on('exit', () => { try {
|
|
218
|
+
fs.unlinkSync(pidPath);
|
|
219
|
+
}
|
|
220
|
+
catch { } });
|
|
221
|
+
// Remove stale socket from a prior crashed server. Safe now that we hold the PID slot.
|
|
196
222
|
if (fs.existsSync(socketPath)) {
|
|
197
223
|
try {
|
|
198
224
|
fs.unlinkSync(socketPath);
|
|
@@ -510,8 +536,6 @@ export async function runPtyServer() {
|
|
|
510
536
|
// assumption, not a nice-to-have. If we can't lock it down, refuse to
|
|
511
537
|
// start so the caller learns immediately.
|
|
512
538
|
fs.chmodSync(socketPath, 0o600);
|
|
513
|
-
// Write PID
|
|
514
|
-
fs.writeFileSync(getPtyPidPath(), String(process.pid), 'utf-8');
|
|
515
539
|
log('INFO', `PTY server started (PID: ${process.pid}, socket: ${socketPath})`);
|
|
516
540
|
// Shutdown handler
|
|
517
541
|
function shutdown() {
|
package/dist/lib/versions.js
CHANGED
|
@@ -31,6 +31,7 @@ import { applyPermissionsToVersion as applyPermsToVersion, PERMISSIONS_CAPABLE_A
|
|
|
31
31
|
import { installMcpServers, parseMcpServerConfig } from './mcp.js';
|
|
32
32
|
import { markdownToToml } from './convert.js';
|
|
33
33
|
import { createVersionedAlias, removeVersionedAlias, getConfigSymlinkVersion, ensureClaudeInsideSymlink } from './shims.js';
|
|
34
|
+
import { importInstallScriptBinary } from './import.js';
|
|
34
35
|
import { listInstalledSubagents, transformSubagentForClaude, syncSubagentToOpenclaw, SUBAGENT_CAPABLE_AGENTS } from './subagents.js';
|
|
35
36
|
import { WORKFLOW_CAPABLE_AGENTS, listInstalledWorkflows, syncWorkflowToVersion } from './workflows.js';
|
|
36
37
|
import { registerHooksToSettings } from './hooks.js';
|
|
@@ -1176,6 +1177,26 @@ export async function installVersion(agent, version, onProgress) {
|
|
|
1176
1177
|
const versionDir = getVersionDir(agent, installedVersion);
|
|
1177
1178
|
fs.mkdirSync(versionDir, { recursive: true });
|
|
1178
1179
|
fs.mkdirSync(path.join(versionDir, 'home'), { recursive: true });
|
|
1180
|
+
// Symlink the installed binary into the version's node_modules/.bin so
|
|
1181
|
+
// listInstalledVersions (which checks getBinaryPath) sees this version as
|
|
1182
|
+
// installed. Without this, `agents add antigravity@latest` succeeds
|
|
1183
|
+
// but `agents view` shows the agent under "Not Managed" because
|
|
1184
|
+
// listInstalledVersions returns [] — the installer drops the binary in
|
|
1185
|
+
// ~/.local/bin (or similar) rather than the version's node_modules/.bin.
|
|
1186
|
+
// Grok is special-cased in getBinaryPath itself (binary lives in
|
|
1187
|
+
// ~/.grok/downloads), so we skip the symlink there.
|
|
1188
|
+
if (agent !== 'grok') {
|
|
1189
|
+
try {
|
|
1190
|
+
const { stdout: whichOut } = await execFileAsync('which', [agentConfig.cliCommand]);
|
|
1191
|
+
const installedBinary = whichOut.trim();
|
|
1192
|
+
if (installedBinary && fs.existsSync(installedBinary)) {
|
|
1193
|
+
importInstallScriptBinary({ agentId: agent, npmPackage: agentConfig.npmPackage, cliCommand: agentConfig.cliCommand }, installedVersion, installedBinary, versionDir);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
catch {
|
|
1197
|
+
/* binary missing from PATH — install script failed silently; surface via the existing version.install error path below isn't possible here since the script returned 0. Leave the version dir empty so getBinaryPath check correctly reports it uninstalled. */
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1179
1200
|
createVersionedAlias(agent, installedVersion);
|
|
1180
1201
|
emit('version.install', { agent, version: installedVersion });
|
|
1181
1202
|
return { success: true, installedVersion };
|
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.4",
|
|
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",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"npm": ">=9"
|
|
75
75
|
},
|
|
76
76
|
"dependencies": {
|
|
77
|
-
"@inquirer/prompts": "8.5.
|
|
77
|
+
"@inquirer/prompts": "8.5.2",
|
|
78
78
|
"@types/proper-lockfile": "4.1.4",
|
|
79
79
|
"@xterm/headless": "6.0.0",
|
|
80
80
|
"@zed-industries/agent-client-protocol": "0.4.5",
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
"marked": "15.0.12",
|
|
86
86
|
"marked-terminal": "7.3.0",
|
|
87
87
|
"node-pty": "1.1.0",
|
|
88
|
-
"ora": "
|
|
88
|
+
"ora": "9.4.0",
|
|
89
89
|
"proper-lockfile": "4.1.2",
|
|
90
90
|
"simple-git": "3.36.0",
|
|
91
91
|
"smol-toml": "1.6.1",
|
|
@@ -94,9 +94,9 @@
|
|
|
94
94
|
"devDependencies": {
|
|
95
95
|
"@types/diff": "8.0.0",
|
|
96
96
|
"@types/marked-terminal": "6.1.1",
|
|
97
|
-
"@types/node": "25.9.
|
|
98
|
-
"tsx": "4.22.
|
|
97
|
+
"@types/node": "25.9.2",
|
|
98
|
+
"tsx": "4.22.4",
|
|
99
99
|
"typescript": "6.0.3",
|
|
100
|
-
"vitest": "4.1.
|
|
100
|
+
"vitest": "4.1.8"
|
|
101
101
|
}
|
|
102
102
|
}
|