@phnx-labs/agents-cli 1.17.0 → 1.17.1

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.17.1
4
+
5
+ **Agent management**
6
+
7
+ - `agents import <agent>` — adopt an existing global npm/homebrew install into agents-cli management without reinstalling. Supports `--version`, `--from-path`, `--yes`. The imported version is wired in as the global default with shim + versioned alias so it behaves the same as a freshly `agents add`'d install.
8
+
3
9
  ## 1.17.0
4
10
 
5
11
  **Workflows: a new first-class resource**
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `agents import` — adopt an existing unmanaged agent install into agents-cli.
3
+ *
4
+ * Three forms:
5
+ *
6
+ * agents import openclaw
7
+ * Auto-detect via the binary on PATH. Resolves the npm package directory,
8
+ * reads its version, and registers it under
9
+ * ~/.agents/.history/versions/<agent>/<version>/.
10
+ *
11
+ * agents import openclaw --version 2026.3.8
12
+ * Same auto-detect, but pin the version label rather than reading it from
13
+ * the package. Useful when the package metadata is stale or you want a
14
+ * canonical name.
15
+ *
16
+ * agents import openclaw --from-path /opt/homebrew/lib/node_modules/openclaw
17
+ * Skip detection entirely. The given path must be a directory containing
18
+ * a valid package.json with a `bin` entry.
19
+ *
20
+ * In all forms, the agent's config dir (e.g. ~/.openclaw) is also moved under
21
+ * management — same behavior as the first-run `agents setup` import flow.
22
+ */
23
+ import type { Command } from 'commander';
24
+ export declare function registerImportCommand(program: Command): void;
@@ -0,0 +1,203 @@
1
+ /**
2
+ * `agents import` — adopt an existing unmanaged agent install into agents-cli.
3
+ *
4
+ * Three forms:
5
+ *
6
+ * agents import openclaw
7
+ * Auto-detect via the binary on PATH. Resolves the npm package directory,
8
+ * reads its version, and registers it under
9
+ * ~/.agents/.history/versions/<agent>/<version>/.
10
+ *
11
+ * agents import openclaw --version 2026.3.8
12
+ * Same auto-detect, but pin the version label rather than reading it from
13
+ * the package. Useful when the package metadata is stale or you want a
14
+ * canonical name.
15
+ *
16
+ * agents import openclaw --from-path /opt/homebrew/lib/node_modules/openclaw
17
+ * Skip detection entirely. The given path must be a directory containing
18
+ * a valid package.json with a `bin` entry.
19
+ *
20
+ * In all forms, the agent's config dir (e.g. ~/.openclaw) is also moved under
21
+ * management — same behavior as the first-run `agents setup` import flow.
22
+ */
23
+ import chalk from 'chalk';
24
+ import ora from 'ora';
25
+ import * as fs from 'fs';
26
+ import * as path from 'path';
27
+ import { confirm } from '@inquirer/prompts';
28
+ import { ALL_AGENT_IDS } from '../lib/agents.js';
29
+ import { AGENTS, getCliPath, getCliVersion, agentLabel } from '../lib/agents.js';
30
+ import { getVersionDir } from '../lib/versions.js';
31
+ import { finalizeImport, importAgentBinary, importAgentConfig, resolvePackageDirFromBinary, } from '../lib/import.js';
32
+ import { isPromptCancelled, isInteractiveTerminal } from './utils.js';
33
+ function isValidAgentId(value) {
34
+ return ALL_AGENT_IDS.includes(value);
35
+ }
36
+ async function runImport(agentArg, opts) {
37
+ if (!isValidAgentId(agentArg)) {
38
+ console.error(chalk.red(`Unknown agent: ${agentArg}`));
39
+ console.error(chalk.gray(`Known agents: ${ALL_AGENT_IDS.join(', ')}`));
40
+ process.exit(1);
41
+ }
42
+ const agentId = agentArg;
43
+ const agent = AGENTS[agentId];
44
+ // Reject agents that don't ship via npm before we spin up PATH lookups and
45
+ // prompts. cursor/kiro/goose/roo all have npmPackage='' and use custom
46
+ // install scripts — the symlink-farm import doesn't apply to them.
47
+ if (!agent.npmPackage) {
48
+ console.error(chalk.red(`${agentLabel(agentId)} doesn't install via npm — \`agents import\` only handles npm-style packages.`));
49
+ console.error(chalk.gray(`Use \`agents add ${agentId}\` to install via its native script.`));
50
+ process.exit(1);
51
+ }
52
+ let globalPath = null;
53
+ if (opts.fromPath) {
54
+ globalPath = path.resolve(opts.fromPath);
55
+ if (!fs.existsSync(globalPath)) {
56
+ console.error(chalk.red(`Path does not exist: ${globalPath}`));
57
+ process.exit(1);
58
+ }
59
+ }
60
+ else {
61
+ const binary = await getCliPath(agentId);
62
+ if (!binary) {
63
+ // Use || (not ??) so empty-string npmPackage falls back to cliCommand.
64
+ // Defensive: agents with empty npmPackage are rejected above, but keep
65
+ // the operator correct in case that early check is ever relaxed.
66
+ const installName = agent.npmPackage || agent.cliCommand;
67
+ console.error(chalk.red(`No "${agent.cliCommand}" found on PATH.`));
68
+ console.error(chalk.gray(`Install it first (e.g. \`npm i -g ${installName}\`) or pass --from-path.`));
69
+ process.exit(1);
70
+ }
71
+ globalPath = resolvePackageDirFromBinary(binary);
72
+ if (!globalPath) {
73
+ console.error(chalk.red(`Could not resolve npm package for binary: ${binary}`));
74
+ console.error(chalk.gray('Pass --from-path <dir> with the package directory explicitly.'));
75
+ process.exit(1);
76
+ }
77
+ }
78
+ let version = opts.version;
79
+ if (!version) {
80
+ try {
81
+ const pkg = JSON.parse(fs.readFileSync(path.join(globalPath, 'package.json'), 'utf8'));
82
+ version = typeof pkg.version === 'string' ? pkg.version : undefined;
83
+ }
84
+ catch {
85
+ /* fall through */
86
+ }
87
+ // Only fall back to running the PATH binary's --version when we're
88
+ // auto-detecting. With --from-path, the PATH binary may belong to a
89
+ // different install entirely; reporting its version here would silently
90
+ // mis-attribute the imported version.
91
+ if (!version && !opts.fromPath) {
92
+ const detected = await getCliVersion(agentId);
93
+ version = detected ?? undefined;
94
+ }
95
+ }
96
+ if (!version) {
97
+ console.error(chalk.red(`Could not determine version for ${agentLabel(agentId)}.`));
98
+ console.error(chalk.gray('Pass --version <version> explicitly.'));
99
+ process.exit(1);
100
+ }
101
+ const versionDir = getVersionDir(agentId, version);
102
+ console.log(chalk.bold(`\nImport ${agentLabel(agentId)} v${version}`));
103
+ console.log(` from: ${chalk.gray(globalPath)}`);
104
+ console.log(` into: ${chalk.gray(versionDir)}`);
105
+ const configDirExists = fs.existsSync(agent.configDir);
106
+ let configAlreadyManaged = false;
107
+ if (configDirExists) {
108
+ const stat = fs.lstatSync(agent.configDir);
109
+ if (stat.isSymbolicLink()) {
110
+ configAlreadyManaged = true;
111
+ console.log(` config: ${chalk.gray(`${agent.configDir} (already managed — will skip)`)}`);
112
+ }
113
+ else {
114
+ console.log(` config: ${chalk.gray(`${agent.configDir} (will be moved into version home)`)}`);
115
+ }
116
+ }
117
+ else {
118
+ console.log(` config: ${chalk.gray(`${agent.configDir} (does not exist — will skip)`)}`);
119
+ }
120
+ if (!opts.yes && isInteractiveTerminal()) {
121
+ console.log();
122
+ const proceed = await confirm({
123
+ message: `Import ${agentLabel(agentId)} v${version} into agents-cli?`,
124
+ default: true,
125
+ }).catch((err) => {
126
+ if (isPromptCancelled(err))
127
+ return false;
128
+ throw err;
129
+ });
130
+ if (!proceed) {
131
+ console.log(chalk.gray('Aborted.'));
132
+ return;
133
+ }
134
+ }
135
+ // Order: config first, then binary, then finalize. Config does the
136
+ // user-visible side effect (renaming ~/.<agent>/), so if it fails we don't
137
+ // want a stranded symlink farm. Binary registration is cheap and reversible
138
+ // — if it fails after config, the next `agents import` call retries cleanly.
139
+ const willImportConfig = configDirExists && !configAlreadyManaged;
140
+ if (willImportConfig) {
141
+ const cfgSpinner = ora(`Importing config dir for ${agentLabel(agentId)} v${version}...`).start();
142
+ const cfgResult = await importAgentConfig(agentId, version);
143
+ if (cfgResult.success) {
144
+ cfgSpinner.succeed(`Config imported (${agent.configDir} -> ${versionDir}/home/.${agentId})`);
145
+ }
146
+ else if (cfgResult.skipped) {
147
+ cfgSpinner.warn(`Config: ${cfgResult.error}`);
148
+ }
149
+ else {
150
+ cfgSpinner.fail(`Config: ${cfgResult.error}`);
151
+ process.exit(1);
152
+ }
153
+ }
154
+ const binSpinner = ora(`Registering ${agentLabel(agentId)} v${version} binary...`).start();
155
+ const binResult = importAgentBinary({ agentId, npmPackage: agent.npmPackage, cliCommand: agent.cliCommand }, version, globalPath, versionDir);
156
+ if (binResult.success) {
157
+ binSpinner.succeed(`Binary registered (${agent.cliCommand} -> ${globalPath})`);
158
+ }
159
+ else if (binResult.skipped) {
160
+ binSpinner.warn(`Binary: ${binResult.error}`);
161
+ }
162
+ else {
163
+ binSpinner.fail(`Binary: ${binResult.error}`);
164
+ process.exit(1);
165
+ }
166
+ // Wire the imported version into the resolver: global default, main shim,
167
+ // versioned alias, home-file symlinks. Idempotent — safe to call even if
168
+ // importAgentConfig already set the global default.
169
+ const finalizeSpinner = ora(`Wiring ${agentLabel(agentId)} v${version} as the active version...`).start();
170
+ try {
171
+ finalizeImport(agentId, version);
172
+ finalizeSpinner.succeed(`${agentLabel(agentId)} v${version} set as default with shim + alias`);
173
+ }
174
+ catch (err) {
175
+ finalizeSpinner.fail(`Finalize: ${err.message}`);
176
+ process.exit(1);
177
+ }
178
+ console.log();
179
+ console.log(chalk.green(`${agentLabel(agentId)} v${version} is now managed.`));
180
+ console.log(chalk.gray(`Verify: agents view ${agentId}`));
181
+ }
182
+ export function registerImportCommand(program) {
183
+ program
184
+ .command('import')
185
+ .argument('<agent>', 'Agent id (e.g. openclaw, claude, codex)')
186
+ .description('Import an existing unmanaged agent install into agents-cli')
187
+ .option('--version <version>', 'Pin a version label (otherwise read from package.json)')
188
+ .option('--from-path <path>', 'Path to the npm package dir (otherwise auto-detected from PATH)')
189
+ .option('-y, --yes', 'Skip the confirmation prompt')
190
+ .addHelpText('after', `
191
+ Examples:
192
+ $ agents import openclaw Auto-detect via PATH
193
+ $ agents import openclaw --version 2026.3.8 Pin a version label
194
+ $ agents import openclaw --from-path /opt/homebrew/lib/node_modules/openclaw
195
+
196
+ When to use:
197
+ When an agent CLI is already installed globally via npm or homebrew and you
198
+ want to bring it under agents-cli management without reinstalling. Creates a
199
+ symlink farm pointing at the existing install — nothing is copied or moved
200
+ (except the agent's config dir, which is moved into the version's home).
201
+ `)
202
+ .action(runImport);
203
+ }
package/dist/index.js CHANGED
@@ -36,6 +36,7 @@ import { registerRulesCommands } from './commands/rules.js';
36
36
  import { registerPermissionsCommands } from './commands/permissions.js';
37
37
  import { registerMcpCommands } from './commands/mcp.js';
38
38
  import { registerVersionsCommands } from './commands/versions.js';
39
+ import { registerImportCommand } from './commands/import.js';
39
40
  import { registerPackagesCommands } from './commands/packages.js';
40
41
  import { registerDaemonCommands } from './commands/daemon.js';
41
42
  import { registerRoutinesCommands } from './commands/routines.js';
@@ -87,6 +88,7 @@ Quick start:
87
88
 
88
89
  Agent versions:
89
90
  add <agent>[@version] Install an agent CLI (e.g. agents add codex)
91
+ import <agent> Adopt an existing global install (npm/homebrew) into agents-cli
90
92
  remove <agent>[@version] Uninstall a version
91
93
  use <agent>@<version> Set the default version
92
94
  prune [target] Remove orphan resources and older duplicate version installs (targets: commands, sessions, runs, trash)
@@ -460,6 +462,7 @@ registerSubagentsCommands(program);
460
462
  registerPluginsCommands(program);
461
463
  registerWorkflowsCommands(program);
462
464
  registerVersionsCommands(program);
465
+ registerImportCommand(program);
463
466
  registerPackagesCommands(program);
464
467
  registerDaemonCommands(program);
465
468
  registerRoutinesCommands(program);
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Import existing unmanaged agent installations into agents-cli.
3
+ *
4
+ * Two flavors:
5
+ *
6
+ * 1. Config-only import — moves an agent's config dir (e.g. ~/.openclaw)
7
+ * into the version structure and symlinks it back. Used by `agents setup`
8
+ * on first-run when an agent was previously installed via npm/homebrew.
9
+ *
10
+ * 2. Full import — also registers an existing binary install (e.g. a global
11
+ * `npm i -g openclaw`) under the managed version path so the shim
12
+ * resolver can find it. This is what `agents import <agent>` does.
13
+ *
14
+ * The binary side never moves files. It creates a thin symlink farm under
15
+ * `~/.agents/.history/versions/<agent>/<version>/` pointing at the original
16
+ * global install, plus a package.json marker so `isVersionInstalled` returns
17
+ * true.
18
+ */
19
+ import type { AgentId } from './types.js';
20
+ export interface ImportConfigResult {
21
+ success: boolean;
22
+ skipped?: boolean;
23
+ error?: string;
24
+ }
25
+ export interface ImportBinaryResult {
26
+ success: boolean;
27
+ skipped?: boolean;
28
+ error?: string;
29
+ resolvedFromPath?: string;
30
+ }
31
+ /**
32
+ * Move an agent's config dir into the managed version structure and symlink it
33
+ * back to its original location. Sets the imported version as the global
34
+ * default and refreshes the shim so the user's PATH lookup hits the managed
35
+ * version.
36
+ *
37
+ * No-op (returns skipped=true) if the version's config dir is already created.
38
+ */
39
+ export declare function importAgentConfig(agentId: AgentId, version: string): Promise<ImportConfigResult>;
40
+ /**
41
+ * Wire an imported version into the rest of the system so it behaves the same
42
+ * as a freshly installed version:
43
+ *
44
+ * - registered as the global default in agents.yaml (so `agents view`
45
+ * reports it correctly and resolvers find it),
46
+ * - main shim refreshed (`~/.agents/.cache/shims/<cli>`),
47
+ * - versioned alias created (`~/.agents/.cache/shims/<cli>@<version>`),
48
+ * - home-file symlinks (CLAUDE.md / AGENTS.md / etc.) repointed at this
49
+ * version's home dir.
50
+ *
51
+ * Without this, the binary-only import path would leave the version stranded:
52
+ * isVersionInstalled returns true, but the resolver never picks it. Safe to
53
+ * call multiple times — each underlying function is idempotent.
54
+ */
55
+ export declare function finalizeImport(agentId: AgentId, version: string): void;
56
+ /**
57
+ * Agent metadata needed by importAgentBinary. Taking these as explicit
58
+ * inputs (rather than looking up AGENTS internally) decouples the symlink
59
+ * farm from the AGENTS registry, which keeps the function pure and avoids
60
+ * fragile coupling in test setups that stub `lib/agents.ts`.
61
+ */
62
+ export interface AgentBinarySpec {
63
+ /** Agent id used in the marker package.json (`agents-{agentId}-{version}`). */
64
+ agentId: string;
65
+ /** npm package name (e.g. `openclaw`) — used as the `node_modules/<name>` dir. */
66
+ npmPackage: string;
67
+ /** Binary name on PATH (e.g. `openclaw`) — used as the `.bin/<name>` entry. */
68
+ cliCommand: string;
69
+ }
70
+ /**
71
+ * Register an existing global npm package install under the managed version
72
+ * path so the shim resolver finds it.
73
+ *
74
+ * Layout produced (everything is a symlink, nothing is copied):
75
+ *
76
+ * {versionDir}/
77
+ * package.json # marker so isVersionInstalled() is true
78
+ * home/ # empty isolated $HOME for this version
79
+ * node_modules/{npmPackage} -> {globalPath}
80
+ * node_modules/.bin/{cliCommand} -> {binaryEntry}
81
+ */
82
+ export declare function importAgentBinary(spec: AgentBinarySpec, version: string, globalPath: string, versionDir: string): ImportBinaryResult;
83
+ /**
84
+ * Resolve the on-disk npm package directory for an agent's CLI binary by
85
+ * walking up from the binary, following any symlinks. Returns null if the
86
+ * package can't be identified.
87
+ *
88
+ * Handles the homebrew/global-npm pattern where:
89
+ * /opt/homebrew/bin/{cli} -> ../lib/node_modules/{pkg}/dist/index.js
90
+ */
91
+ export declare function resolvePackageDirFromBinary(binaryPath: string): string | null;
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Import existing unmanaged agent installations into agents-cli.
3
+ *
4
+ * Two flavors:
5
+ *
6
+ * 1. Config-only import — moves an agent's config dir (e.g. ~/.openclaw)
7
+ * into the version structure and symlinks it back. Used by `agents setup`
8
+ * on first-run when an agent was previously installed via npm/homebrew.
9
+ *
10
+ * 2. Full import — also registers an existing binary install (e.g. a global
11
+ * `npm i -g openclaw`) under the managed version path so the shim
12
+ * resolver can find it. This is what `agents import <agent>` does.
13
+ *
14
+ * The binary side never moves files. It creates a thin symlink farm under
15
+ * `~/.agents/.history/versions/<agent>/<version>/` pointing at the original
16
+ * global install, plus a package.json marker so `isVersionInstalled` returns
17
+ * true.
18
+ */
19
+ import * as fs from 'fs';
20
+ import * as path from 'path';
21
+ import { AGENTS } from './agents.js';
22
+ import { getVersionsDir } from './state.js';
23
+ import { setGlobalDefault } from './versions.js';
24
+ import { createShim, createVersionedAlias, ensureShimCurrent, switchHomeFileSymlinks } from './shims.js';
25
+ /**
26
+ * Move an agent's config dir into the managed version structure and symlink it
27
+ * back to its original location. Sets the imported version as the global
28
+ * default and refreshes the shim so the user's PATH lookup hits the managed
29
+ * version.
30
+ *
31
+ * No-op (returns skipped=true) if the version's config dir is already created.
32
+ */
33
+ export async function importAgentConfig(agentId, version) {
34
+ const agent = AGENTS[agentId];
35
+ const configDir = agent.configDir;
36
+ const versionsDir = getVersionsDir();
37
+ const versionHome = path.join(versionsDir, agentId, version, 'home');
38
+ const versionConfigDir = path.join(versionHome, `.${agentId}`);
39
+ if (fs.existsSync(versionConfigDir)) {
40
+ return { success: false, skipped: true, error: `${version} already installed` };
41
+ }
42
+ try {
43
+ fs.mkdirSync(versionHome, { recursive: true });
44
+ fs.renameSync(configDir, versionConfigDir);
45
+ fs.symlinkSync(versionConfigDir, configDir);
46
+ setGlobalDefault(agentId, version);
47
+ switchHomeFileSymlinks(agentId, version);
48
+ ensureShimCurrent(agentId);
49
+ return { success: true };
50
+ }
51
+ catch (err) {
52
+ return { success: false, error: err.message };
53
+ }
54
+ }
55
+ /**
56
+ * Wire an imported version into the rest of the system so it behaves the same
57
+ * as a freshly installed version:
58
+ *
59
+ * - registered as the global default in agents.yaml (so `agents view`
60
+ * reports it correctly and resolvers find it),
61
+ * - main shim refreshed (`~/.agents/.cache/shims/<cli>`),
62
+ * - versioned alias created (`~/.agents/.cache/shims/<cli>@<version>`),
63
+ * - home-file symlinks (CLAUDE.md / AGENTS.md / etc.) repointed at this
64
+ * version's home dir.
65
+ *
66
+ * Without this, the binary-only import path would leave the version stranded:
67
+ * isVersionInstalled returns true, but the resolver never picks it. Safe to
68
+ * call multiple times — each underlying function is idempotent.
69
+ */
70
+ export function finalizeImport(agentId, version) {
71
+ setGlobalDefault(agentId, version);
72
+ createShim(agentId);
73
+ createVersionedAlias(agentId, version);
74
+ switchHomeFileSymlinks(agentId, version);
75
+ ensureShimCurrent(agentId);
76
+ }
77
+ /**
78
+ * Register an existing global npm package install under the managed version
79
+ * path so the shim resolver finds it.
80
+ *
81
+ * Layout produced (everything is a symlink, nothing is copied):
82
+ *
83
+ * {versionDir}/
84
+ * package.json # marker so isVersionInstalled() is true
85
+ * home/ # empty isolated $HOME for this version
86
+ * node_modules/{npmPackage} -> {globalPath}
87
+ * node_modules/.bin/{cliCommand} -> {binaryEntry}
88
+ */
89
+ export function importAgentBinary(spec, version, globalPath, versionDir) {
90
+ const binaryLink = path.join(versionDir, 'node_modules', '.bin', spec.cliCommand);
91
+ // lstat — we want to detect the symlink itself, not follow it. fs.existsSync
92
+ // can return false on dangling symlinks, which would incorrectly let us
93
+ // proceed to symlinkSync below and throw EEXIST.
94
+ let alreadyExists = false;
95
+ try {
96
+ fs.lstatSync(binaryLink);
97
+ alreadyExists = true;
98
+ }
99
+ catch {
100
+ /* not present */
101
+ }
102
+ if (alreadyExists) {
103
+ return { success: false, skipped: true, error: `${version} already installed`, resolvedFromPath: globalPath };
104
+ }
105
+ if (!fs.existsSync(globalPath)) {
106
+ return { success: false, error: `Path does not exist: ${globalPath}` };
107
+ }
108
+ const globalPkgJson = path.join(globalPath, 'package.json');
109
+ if (!fs.existsSync(globalPkgJson)) {
110
+ return { success: false, error: `Not an npm package (no package.json): ${globalPath}` };
111
+ }
112
+ let pkgBinEntry;
113
+ try {
114
+ const pkg = JSON.parse(fs.readFileSync(globalPkgJson, 'utf8'));
115
+ if (typeof pkg.bin === 'string') {
116
+ pkgBinEntry = pkg.bin;
117
+ }
118
+ else if (pkg.bin && typeof pkg.bin === 'object') {
119
+ // Strict: only accept the exact cliCommand key. Multi-bin packages
120
+ // (e.g. @anthropic-ai/claude-code ships several bins) would otherwise
121
+ // silently get a wrong binary chosen by Object.values() ordering.
122
+ pkgBinEntry = pkg.bin[spec.cliCommand];
123
+ }
124
+ }
125
+ catch (err) {
126
+ return { success: false, error: `Failed to read package.json: ${err.message}` };
127
+ }
128
+ if (!pkgBinEntry) {
129
+ return { success: false, error: `package.json has no bin entry for "${spec.cliCommand}" — pass --from-path to a package that ships it` };
130
+ }
131
+ const binaryTarget = path.resolve(globalPath, pkgBinEntry);
132
+ if (!fs.existsSync(binaryTarget)) {
133
+ return { success: false, error: `Binary entry missing: ${binaryTarget}` };
134
+ }
135
+ try {
136
+ fs.mkdirSync(path.join(versionDir, 'home'), { recursive: true });
137
+ fs.mkdirSync(path.join(versionDir, 'node_modules', '.bin'), { recursive: true });
138
+ fs.writeFileSync(path.join(versionDir, 'package.json'), JSON.stringify({ name: `agents-${spec.agentId}-${version}`, version: '1.0.0', private: true, imported: true, from: globalPath }, null, 2));
139
+ const pkgLink = path.join(versionDir, 'node_modules', spec.npmPackage);
140
+ fs.mkdirSync(path.dirname(pkgLink), { recursive: true });
141
+ if (!fs.existsSync(pkgLink)) {
142
+ fs.symlinkSync(globalPath, pkgLink);
143
+ }
144
+ fs.symlinkSync(binaryTarget, binaryLink);
145
+ return { success: true, resolvedFromPath: globalPath };
146
+ }
147
+ catch (err) {
148
+ return { success: false, error: err.message };
149
+ }
150
+ }
151
+ /**
152
+ * Resolve the on-disk npm package directory for an agent's CLI binary by
153
+ * walking up from the binary, following any symlinks. Returns null if the
154
+ * package can't be identified.
155
+ *
156
+ * Handles the homebrew/global-npm pattern where:
157
+ * /opt/homebrew/bin/{cli} -> ../lib/node_modules/{pkg}/dist/index.js
158
+ */
159
+ export function resolvePackageDirFromBinary(binaryPath) {
160
+ try {
161
+ let real = fs.realpathSync(binaryPath);
162
+ let dir = path.dirname(real);
163
+ // Walk up looking for the nearest package.json
164
+ for (let i = 0; i < 6; i++) {
165
+ const pkg = path.join(dir, 'package.json');
166
+ if (fs.existsSync(pkg)) {
167
+ return dir;
168
+ }
169
+ const parent = path.dirname(dir);
170
+ if (parent === dir)
171
+ break;
172
+ dir = parent;
173
+ }
174
+ return null;
175
+ }
176
+ catch {
177
+ return null;
178
+ }
179
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.17.0",
3
+ "version": "1.17.1",
4
4
  "description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",