@phnx-labs/agents-cli 1.20.8 → 1.20.10

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +1 -1
  3. package/dist/commands/daemon.js +6 -6
  4. package/dist/commands/import.js +3 -6
  5. package/dist/commands/inspect.d.ts +2 -0
  6. package/dist/commands/inspect.js +75 -28
  7. package/dist/commands/models.js +2 -1
  8. package/dist/commands/plugins.js +3 -2
  9. package/dist/commands/refresh-rules.js +4 -4
  10. package/dist/commands/routines.js +8 -7
  11. package/dist/commands/sessions.js +17 -2
  12. package/dist/commands/subagents.js +2 -1
  13. package/dist/commands/usage.js +11 -3
  14. package/dist/index.js +69 -47
  15. package/dist/lib/agents.d.ts +18 -1
  16. package/dist/lib/agents.js +89 -23
  17. package/dist/lib/browser/chrome.d.ts +4 -3
  18. package/dist/lib/browser/chrome.js +87 -12
  19. package/dist/lib/browser/ipc.js +59 -13
  20. package/dist/lib/daemon.js +20 -8
  21. package/dist/lib/fs-walk.d.ts +7 -1
  22. package/dist/lib/fs-walk.js +45 -11
  23. package/dist/lib/git.js +5 -2
  24. package/dist/lib/log-follow.d.ts +7 -0
  25. package/dist/lib/log-follow.js +65 -0
  26. package/dist/lib/platform/index.d.ts +1 -0
  27. package/dist/lib/platform/index.js +1 -0
  28. package/dist/lib/platform/ipc.d.ts +11 -0
  29. package/dist/lib/platform/ipc.js +21 -0
  30. package/dist/lib/platform/paths.d.ts +7 -0
  31. package/dist/lib/platform/paths.js +9 -0
  32. package/dist/lib/platform/process.d.ts +9 -1
  33. package/dist/lib/platform/process.js +27 -0
  34. package/dist/lib/plugins.js +5 -3
  35. package/dist/lib/self-update.d.ts +86 -0
  36. package/dist/lib/self-update.js +178 -0
  37. package/dist/lib/versions.js +3 -3
  38. package/package.json +1 -1
  39. package/scripts/postinstall.js +29 -19
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Cross-platform `tail -f`.
3
+ *
4
+ * Replaces spawning the POSIX `tail` binary (absent on Windows) with a poll-based
5
+ * follower that behaves identically on every platform and needs no external
6
+ * dependency. Reads bytes appended since the last position every `intervalMs`;
7
+ * resets to 0 if the file shrinks (truncation / log rotation). The active timer
8
+ * keeps the event loop alive, so callers just register a SIGINT handler that
9
+ * calls the returned stop().
10
+ */
11
+ import * as fs from 'fs';
12
+ export function followFile(filePath, onChunk, opts = {}) {
13
+ const intervalMs = opts.intervalMs ?? 500;
14
+ let pos = 0;
15
+ if (opts.fromEnd) {
16
+ try {
17
+ pos = fs.statSync(filePath).size;
18
+ }
19
+ catch {
20
+ pos = 0;
21
+ }
22
+ }
23
+ else {
24
+ try {
25
+ const initial = fs.readFileSync(filePath);
26
+ if (initial.length > 0)
27
+ onChunk(initial.toString('utf-8'));
28
+ pos = initial.length;
29
+ }
30
+ catch { /* file may not exist yet — start at 0 and wait for it to appear */ }
31
+ }
32
+ const poll = () => {
33
+ let size;
34
+ try {
35
+ size = fs.statSync(filePath).size;
36
+ }
37
+ catch {
38
+ return; /* gone / not yet created */
39
+ }
40
+ if (size < pos)
41
+ pos = 0; // truncated or rotated — re-read from the top
42
+ if (size <= pos)
43
+ return;
44
+ let fd;
45
+ try {
46
+ fd = fs.openSync(filePath, 'r');
47
+ const buf = Buffer.alloc(size - pos);
48
+ const bytes = fs.readSync(fd, buf, 0, buf.length, pos);
49
+ pos += bytes;
50
+ if (bytes > 0)
51
+ onChunk(buf.subarray(0, bytes).toString('utf-8'));
52
+ }
53
+ catch { /* transient read error — retry next tick */ }
54
+ finally {
55
+ if (fd !== undefined) {
56
+ try {
57
+ fs.closeSync(fd);
58
+ }
59
+ catch { /* noop */ }
60
+ }
61
+ }
62
+ };
63
+ const timer = setInterval(poll, intervalMs);
64
+ return () => clearInterval(timer);
65
+ }
@@ -18,3 +18,4 @@ export declare const IS_LINUX: boolean;
18
18
  export * from './paths.js';
19
19
  export * from './exec.js';
20
20
  export * from './process.js';
21
+ export * from './ipc.js';
@@ -18,3 +18,4 @@ export const IS_LINUX = process.platform === 'linux';
18
18
  export * from './paths.js';
19
19
  export * from './exec.js';
20
20
  export * from './process.js';
21
+ export * from './ipc.js';
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Resolve the address a local daemon listens on / clients connect to.
3
+ *
4
+ * POSIX: the AF_UNIX socket file path itself.
5
+ * Windows: a named pipe (`\\.\pipe\agents-<hash>`). Filesystem socket files
6
+ * aren't supported there, and named pipes are NOT filesystem objects — derive a
7
+ * stable name from a hash of the socket path so client and server agree without
8
+ * touching disk, and never probe the result with fs.existsSync (it always reports
9
+ * false). Both forms are accepted by net.createServer / net.createConnection.
10
+ */
11
+ export declare function ipcEndpoint(socketPath: string, platform?: NodeJS.Platform): string;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Local-daemon IPC endpoint, platform-aware.
3
+ */
4
+ import * as crypto from 'crypto';
5
+ /**
6
+ * Resolve the address a local daemon listens on / clients connect to.
7
+ *
8
+ * POSIX: the AF_UNIX socket file path itself.
9
+ * Windows: a named pipe (`\\.\pipe\agents-<hash>`). Filesystem socket files
10
+ * aren't supported there, and named pipes are NOT filesystem objects — derive a
11
+ * stable name from a hash of the socket path so client and server agree without
12
+ * touching disk, and never probe the result with fs.existsSync (it always reports
13
+ * false). Both forms are accepted by net.createServer / net.createConnection.
14
+ */
15
+ export function ipcEndpoint(socketPath, platform = process.platform) {
16
+ if (platform === 'win32') {
17
+ const hash = crypto.createHash('sha1').update(socketPath).digest('hex').slice(0, 16);
18
+ return `\\\\.\\pipe\\agents-${hash}`;
19
+ }
20
+ return socketPath;
21
+ }
@@ -20,3 +20,10 @@ export declare function toComparablePath(p: string, platform?: NodeJS.Platform):
20
20
  * correctly on all three platforms.
21
21
  */
22
22
  export declare function homeDir(): string;
23
+ /**
24
+ * Is this a Windows absolute path — a drive-letter root (`C:\`, `C:/`) or a UNC
25
+ * share (`\\server\share`)? Used by local-source parsing to recognize a native
26
+ * Windows path that the POSIX `/`, `./`, `../` prefixes miss. Caller decides
27
+ * whether to apply it (typically gated on win32).
28
+ */
29
+ export declare function isWindowsAbsolutePath(p: string): boolean;
@@ -47,3 +47,12 @@ export function toComparablePath(p, platform = process.platform) {
47
47
  export function homeDir() {
48
48
  return os.homedir();
49
49
  }
50
+ /**
51
+ * Is this a Windows absolute path — a drive-letter root (`C:\`, `C:/`) or a UNC
52
+ * share (`\\server\share`)? Used by local-source parsing to recognize a native
53
+ * Windows path that the POSIX `/`, `./`, `../` prefixes miss. Caller decides
54
+ * whether to apply it (typically gated on win32).
55
+ */
56
+ export function isWindowsAbsolutePath(p) {
57
+ return WIN_DRIVE_RE.test(p) || p.startsWith('\\\\');
58
+ }
@@ -1,6 +1,14 @@
1
1
  /**
2
- * Process liveness / control, platform-aware.
2
+ * Forcefully terminate a process AND its descendant tree.
3
+ *
4
+ * Windows: `taskkill /F /T /PID` — the only reliable way to take down the whole
5
+ * tree (a bare TerminateProcess leaves children orphaned, which is exactly the
6
+ * "stop reported success but the tree is still alive" bug). POSIX: SIGKILL to the
7
+ * pid (matching the existing hard-kill behavior; callers that own a process group
8
+ * can pass the negative pid). Best-effort — never throws; an already-exited
9
+ * process counts as success.
3
10
  */
11
+ export declare function killTree(pid: number): void;
4
12
  /**
5
13
  * Is a process with this PID currently alive?
6
14
  *
@@ -1,6 +1,33 @@
1
1
  /**
2
2
  * Process liveness / control, platform-aware.
3
3
  */
4
+ import { execFileSync } from 'child_process';
5
+ /**
6
+ * Forcefully terminate a process AND its descendant tree.
7
+ *
8
+ * Windows: `taskkill /F /T /PID` — the only reliable way to take down the whole
9
+ * tree (a bare TerminateProcess leaves children orphaned, which is exactly the
10
+ * "stop reported success but the tree is still alive" bug). POSIX: SIGKILL to the
11
+ * pid (matching the existing hard-kill behavior; callers that own a process group
12
+ * can pass the negative pid). Best-effort — never throws; an already-exited
13
+ * process counts as success.
14
+ */
15
+ export function killTree(pid) {
16
+ if (!pid || pid <= 0)
17
+ return;
18
+ if (process.platform === 'win32') {
19
+ try {
20
+ execFileSync('taskkill', ['/F', '/T', '/PID', String(pid)], { stdio: 'ignore' });
21
+ }
22
+ catch { /* already gone, or no such pid */ }
23
+ }
24
+ else {
25
+ try {
26
+ process.kill(pid, 'SIGKILL');
27
+ }
28
+ catch { /* already gone */ }
29
+ }
30
+ }
4
31
  /**
5
32
  * Is a process with this PID currently alive?
6
33
  *
@@ -12,6 +12,7 @@ import * as fs from 'fs';
12
12
  import * as path from 'path';
13
13
  import { execFileSync } from 'child_process';
14
14
  import { getPluginsDir, getTrashPluginsDir, getExtraPluginsDir, getProjectPluginsDir } from './state.js';
15
+ import { IS_WINDOWS, isWindowsAbsolutePath, homeDir } from './platform/index.js';
15
16
  import { listInstalledVersions, getVersionHomePath } from './versions.js';
16
17
  import { AGENTS, agentConfigDirName } from './agents.js';
17
18
  import { capableAgents, isCapable } from './capabilities.js';
@@ -1007,9 +1008,10 @@ export function parseInstallSpec(spec) {
1007
1008
  export async function installPlugin(spec) {
1008
1009
  const { name: specName, source } = parseInstallSpec(spec);
1009
1010
  // Resolve local path (handle ~)
1010
- const isLocalPath = source.startsWith('/') || source.startsWith('./') || source.startsWith('../') || source.startsWith('~');
1011
+ const isLocalPath = source.startsWith('/') || source.startsWith('./') || source.startsWith('../') || source.startsWith('~')
1012
+ || (IS_WINDOWS && isWindowsAbsolutePath(source));
1011
1013
  const resolvedSource = isLocalPath
1012
- ? source.replace(/^~/, process.env.HOME || '~')
1014
+ ? source.replace(/^~/, homeDir())
1013
1015
  : source;
1014
1016
  const pluginsDir = getPluginsDir();
1015
1017
  fs.mkdirSync(pluginsDir, { recursive: true });
@@ -1085,7 +1087,7 @@ export async function updatePlugin(name) {
1085
1087
  execFileSync('git', ['-C', plugin.root, 'pull', '--ff-only'], { stdio: 'pipe' });
1086
1088
  }
1087
1089
  else {
1088
- const resolvedSource = sourceInfo.source.replace(/^~/, process.env.HOME || '~');
1090
+ const resolvedSource = sourceInfo.source.replace(/^~/, homeDir());
1089
1091
  if (!fs.existsSync(resolvedSource)) {
1090
1092
  return { success: false, error: `Source path no longer exists: ${resolvedSource}` };
1091
1093
  }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Self-update install plumbing.
3
+ *
4
+ * The hard requirement: an upgrade must replace the copy that is currently
5
+ * running. A bare `npm install -g` writes into the global prefix of whatever
6
+ * `npm` PATH happens to resolve — on machines with more than one node
7
+ * installation (nvm + Homebrew + vendored runtimes) that prefix can belong to
8
+ * a different node than the one this copy lives under. The install then
9
+ * "succeeds" while the running copy stays stale and re-prompts forever.
10
+ *
11
+ * So every step here is anchored to the running package root on disk, never
12
+ * to PATH resolution.
13
+ */
14
+ export declare const NPM_PACKAGE_NAME = "@phnx-labs/agents-cli";
15
+ export interface UpdateCheckCache {
16
+ lastCheck: number;
17
+ latestVersion: string;
18
+ dismissed?: string;
19
+ }
20
+ /** Read the cached update-check state from disk. Returns null if the file is missing or corrupt. */
21
+ export declare function readUpdateCache(file: string): UpdateCheckCache | null;
22
+ /**
23
+ * Persist the latest known version and current timestamp. Preserves an
24
+ * existing `dismissed` marker — the background refresh must not erase a
25
+ * user's "Skip this version" choice, or they get re-prompted for the exact
26
+ * version they dismissed.
27
+ */
28
+ export declare function saveUpdateCheck(file: string, latestVersion: string): void;
29
+ /** Record that the user chose to skip `version`; suppresses prompts until a newer version appears. */
30
+ export declare function dismissUpdateVersion(file: string, version: string): void;
31
+ /** Whether the cached state warrants an upgrade prompt for a copy running `currentVersion`. */
32
+ export declare function shouldPromptUpgrade(cache: UpdateCheckCache | null, currentVersion: string): boolean;
33
+ /**
34
+ * Derive the npm global prefix that owns the install at `packageRoot`.
35
+ *
36
+ * npm's global layout for a scoped package:
37
+ * POSIX: <prefix>/lib/node_modules/@phnx-labs/agents-cli
38
+ * Windows: <prefix>/node_modules/@phnx-labs/agents-cli
39
+ *
40
+ * Throws when `packageRoot` is not inside a node_modules tree (e.g. running
41
+ * from a source checkout) — there is no prefix to install into, and guessing
42
+ * one is exactly the bug this module exists to prevent.
43
+ */
44
+ export declare function deriveGlobalPrefix(packageRoot: string): string;
45
+ /**
46
+ * Install `spec` into an explicit global prefix. `--prefix` pins the
47
+ * destination no matter which npm binary PATH resolves. `--ignore-scripts`
48
+ * skips lifecycle scripts; the caller refreshes alias shims afterwards via
49
+ * refreshAliasShims().
50
+ */
51
+ export declare function installPackageIntoPrefix(spec: string, prefix: string): Promise<void>;
52
+ /** Read the version field of the package.json at `packageRoot`, fresh from disk. */
53
+ export declare function readInstalledVersion(packageRoot: string): string;
54
+ /**
55
+ * Assert that the install at `packageRoot` now carries `expectedVersion`.
56
+ * npm exiting 0 only proves it wrote *somewhere*; this proves it wrote *here*.
57
+ */
58
+ export declare function verifyInstalledVersion(packageRoot: string, expectedVersion: string): void;
59
+ /**
60
+ * Re-run the freshly installed copy's postinstall in shims-only mode so the
61
+ * bare-command aliases (secrets, sessions, ...) pick up the new entrypoint
62
+ * and any aliases added in the new version. Best-effort: a failure here
63
+ * leaves the previous shims in place, which still point at the (now
64
+ * upgraded) package root.
65
+ */
66
+ export declare function refreshAliasShims(packageRoot: string): void;
67
+ export interface AgentsCliInstall {
68
+ /** The PATH entry (`<dir>/agents`) that resolves to this install. */
69
+ binPath: string;
70
+ /** Package root containing package.json and dist/. */
71
+ packageRoot: string;
72
+ version: string;
73
+ }
74
+ /**
75
+ * Scan PATH for `agents` entrypoints and resolve each to the agents-cli
76
+ * package root it executes. More than one distinct root means upgrades,
77
+ * shims, and the command the user types can act on different copies — the
78
+ * divergence behind silently-failing self-updates.
79
+ *
80
+ * npm bin entries are symlinks that resolve to `<packageRoot>/dist/index.js`
81
+ * (the dev install's `~/.local/bin/agents` chains through the dev prefix to
82
+ * the same shape). Anything that doesn't resolve to a dist/index.js inside a
83
+ * package named @phnx-labs/agents-cli is some other tool and is skipped.
84
+ * POSIX-only: Windows npm bins are .cmd wrappers, not symlinks.
85
+ */
86
+ export declare function findAgentsCliInstalls(pathEnv: string): AgentsCliInstall[];
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Self-update install plumbing.
3
+ *
4
+ * The hard requirement: an upgrade must replace the copy that is currently
5
+ * running. A bare `npm install -g` writes into the global prefix of whatever
6
+ * `npm` PATH happens to resolve — on machines with more than one node
7
+ * installation (nvm + Homebrew + vendored runtimes) that prefix can belong to
8
+ * a different node than the one this copy lives under. The install then
9
+ * "succeeds" while the running copy stays stale and re-prompts forever.
10
+ *
11
+ * So every step here is anchored to the running package root on disk, never
12
+ * to PATH resolution.
13
+ */
14
+ import * as fs from 'fs';
15
+ import * as path from 'path';
16
+ import { spawnSync } from 'child_process';
17
+ import { compareVersions } from './versions.js';
18
+ export const NPM_PACKAGE_NAME = '@phnx-labs/agents-cli';
19
+ /** Read the cached update-check state from disk. Returns null if the file is missing or corrupt. */
20
+ export function readUpdateCache(file) {
21
+ try {
22
+ return JSON.parse(fs.readFileSync(file, 'utf-8'));
23
+ }
24
+ catch {
25
+ /* cache file missing or corrupt */
26
+ return null;
27
+ }
28
+ }
29
+ /**
30
+ * Persist the latest known version and current timestamp. Preserves an
31
+ * existing `dismissed` marker — the background refresh must not erase a
32
+ * user's "Skip this version" choice, or they get re-prompted for the exact
33
+ * version they dismissed.
34
+ */
35
+ export function saveUpdateCheck(file, latestVersion) {
36
+ try {
37
+ const dir = path.dirname(file);
38
+ if (!fs.existsSync(dir))
39
+ fs.mkdirSync(dir, { recursive: true });
40
+ const dismissed = readUpdateCache(file)?.dismissed;
41
+ fs.writeFileSync(file, JSON.stringify({ lastCheck: Date.now(), latestVersion, ...(dismissed ? { dismissed } : {}) }));
42
+ }
43
+ catch {
44
+ /* best-effort cache update */
45
+ }
46
+ }
47
+ /** Record that the user chose to skip `version`; suppresses prompts until a newer version appears. */
48
+ export function dismissUpdateVersion(file, version) {
49
+ try {
50
+ const dir = path.dirname(file);
51
+ if (!fs.existsSync(dir))
52
+ fs.mkdirSync(dir, { recursive: true });
53
+ const existing = readUpdateCache(file);
54
+ fs.writeFileSync(file, JSON.stringify({
55
+ lastCheck: existing?.lastCheck ?? Date.now(),
56
+ latestVersion: version,
57
+ dismissed: version,
58
+ }));
59
+ }
60
+ catch {
61
+ /* best-effort */
62
+ }
63
+ }
64
+ /** Whether the cached state warrants an upgrade prompt for a copy running `currentVersion`. */
65
+ export function shouldPromptUpgrade(cache, currentVersion) {
66
+ if (!cache?.latestVersion)
67
+ return false;
68
+ return (cache.latestVersion !== currentVersion &&
69
+ compareVersions(cache.latestVersion, currentVersion) > 0 &&
70
+ cache.latestVersion !== cache.dismissed);
71
+ }
72
+ /**
73
+ * Derive the npm global prefix that owns the install at `packageRoot`.
74
+ *
75
+ * npm's global layout for a scoped package:
76
+ * POSIX: <prefix>/lib/node_modules/@phnx-labs/agents-cli
77
+ * Windows: <prefix>/node_modules/@phnx-labs/agents-cli
78
+ *
79
+ * Throws when `packageRoot` is not inside a node_modules tree (e.g. running
80
+ * from a source checkout) — there is no prefix to install into, and guessing
81
+ * one is exactly the bug this module exists to prevent.
82
+ */
83
+ export function deriveGlobalPrefix(packageRoot) {
84
+ const resolved = path.resolve(packageRoot);
85
+ // Two levels up from the package root: the scope dir, then node_modules.
86
+ const nodeModulesDir = path.dirname(path.dirname(resolved));
87
+ if (path.basename(nodeModulesDir) !== 'node_modules') {
88
+ throw new Error(`${resolved} is not an npm-managed install; reinstall with: npm install -g ${NPM_PACKAGE_NAME}`);
89
+ }
90
+ const parent = path.dirname(nodeModulesDir);
91
+ return path.basename(parent) === 'lib' ? path.dirname(parent) : parent;
92
+ }
93
+ /**
94
+ * Install `spec` into an explicit global prefix. `--prefix` pins the
95
+ * destination no matter which npm binary PATH resolves. `--ignore-scripts`
96
+ * skips lifecycle scripts; the caller refreshes alias shims afterwards via
97
+ * refreshAliasShims().
98
+ */
99
+ export async function installPackageIntoPrefix(spec, prefix) {
100
+ const { execFile } = await import('child_process');
101
+ const { promisify } = await import('util');
102
+ const execFileAsync = promisify(execFile);
103
+ await execFileAsync('npm', ['install', '-g', '--prefix', prefix, spec, '--ignore-scripts']);
104
+ }
105
+ /** Read the version field of the package.json at `packageRoot`, fresh from disk. */
106
+ export function readInstalledVersion(packageRoot) {
107
+ return JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf-8')).version;
108
+ }
109
+ /**
110
+ * Assert that the install at `packageRoot` now carries `expectedVersion`.
111
+ * npm exiting 0 only proves it wrote *somewhere*; this proves it wrote *here*.
112
+ */
113
+ export function verifyInstalledVersion(packageRoot, expectedVersion) {
114
+ const actual = readInstalledVersion(packageRoot);
115
+ if (actual !== expectedVersion) {
116
+ throw new Error(`npm reported success but ${packageRoot} is still ${actual} (expected ${expectedVersion}). ` +
117
+ `Run manually: npm install -g --prefix ${deriveGlobalPrefix(packageRoot)} ${NPM_PACKAGE_NAME}@${expectedVersion}`);
118
+ }
119
+ }
120
+ /**
121
+ * Re-run the freshly installed copy's postinstall in shims-only mode so the
122
+ * bare-command aliases (secrets, sessions, ...) pick up the new entrypoint
123
+ * and any aliases added in the new version. Best-effort: a failure here
124
+ * leaves the previous shims in place, which still point at the (now
125
+ * upgraded) package root.
126
+ */
127
+ export function refreshAliasShims(packageRoot) {
128
+ spawnSync(process.execPath, [path.join(packageRoot, 'scripts', 'postinstall.js')], {
129
+ env: { ...process.env, AGENTS_POSTINSTALL_SHIMS_ONLY: '1' },
130
+ stdio: 'ignore',
131
+ });
132
+ }
133
+ /**
134
+ * Scan PATH for `agents` entrypoints and resolve each to the agents-cli
135
+ * package root it executes. More than one distinct root means upgrades,
136
+ * shims, and the command the user types can act on different copies — the
137
+ * divergence behind silently-failing self-updates.
138
+ *
139
+ * npm bin entries are symlinks that resolve to `<packageRoot>/dist/index.js`
140
+ * (the dev install's `~/.local/bin/agents` chains through the dev prefix to
141
+ * the same shape). Anything that doesn't resolve to a dist/index.js inside a
142
+ * package named @phnx-labs/agents-cli is some other tool and is skipped.
143
+ * POSIX-only: Windows npm bins are .cmd wrappers, not symlinks.
144
+ */
145
+ export function findAgentsCliInstalls(pathEnv) {
146
+ if (process.platform === 'win32')
147
+ return [];
148
+ const installs = [];
149
+ const seenRoots = new Set();
150
+ for (const dir of pathEnv.split(path.delimiter).filter(Boolean)) {
151
+ const candidate = path.join(dir, 'agents');
152
+ let real;
153
+ try {
154
+ real = fs.realpathSync(candidate);
155
+ }
156
+ catch {
157
+ continue; // missing or dangling symlink
158
+ }
159
+ if (path.basename(real) !== 'index.js' || path.basename(path.dirname(real)) !== 'dist') {
160
+ continue;
161
+ }
162
+ const packageRoot = path.dirname(path.dirname(real));
163
+ if (seenRoots.has(packageRoot))
164
+ continue;
165
+ let pkg;
166
+ try {
167
+ pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf-8'));
168
+ }
169
+ catch {
170
+ continue;
171
+ }
172
+ if (pkg.name !== NPM_PACKAGE_NAME || typeof pkg.version !== 'string')
173
+ continue;
174
+ seenRoots.add(packageRoot);
175
+ installs.push({ binPath: candidate, packageRoot, version: pkg.version });
176
+ }
177
+ return installs;
178
+ }
@@ -777,9 +777,9 @@ export function parseAgentSpec(spec) {
777
777
  if (parts.length > 2) {
778
778
  return null;
779
779
  }
780
- const agentName = parts[0].toLowerCase();
781
780
  const version = parts[1] || 'latest';
782
- if (!AGENTS[agentName]) {
781
+ const agent = resolveAgentName(parts[0]);
782
+ if (!agent) {
783
783
  return null;
784
784
  }
785
785
  // Reject any version string that could escape an exec context or a
@@ -788,7 +788,7 @@ export function parseAgentSpec(spec) {
788
788
  return null;
789
789
  }
790
790
  return {
791
- agent: agentName,
791
+ agent,
792
792
  version,
793
793
  };
794
794
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.20.8",
3
+ "version": "1.20.10",
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",
@@ -32,6 +32,35 @@ function shellQuote(value) {
32
32
  return `'${value.replace(/'/g, `'\\''`)}'`;
33
33
  }
34
34
 
35
+ // Shorthands that delegate to the installed agents-cli entrypoint.
36
+ const ALIASES = ['sessions', 'secrets', 'browser', 'pty', 'teams'];
37
+
38
+ function writeAliasShims() {
39
+ const written = [];
40
+ for (const name of ALIASES) {
41
+ const target = path.join(SHIMS_DIR, name);
42
+ const script = `#!/bin/sh\nAGENTS_BIN=${shellQuote(AGENTS_BIN)}\nif [ -z "$AGENTS_BIN" ] || [ ! -x "$AGENTS_BIN" ]; then\n echo "agents: agents-cli entrypoint missing or not executable: $AGENTS_BIN" >&2\n exit 127\nfi\nexec "$AGENTS_BIN" ${name} "$@"\n`;
43
+ fs.writeFileSync(target, script, { mode: 0o755 });
44
+ // Windows can't run the POSIX shim; drop a `.cmd` companion that invokes the
45
+ // entrypoint via node so the bare shorthand works in a Windows shell.
46
+ if (process.platform === 'win32') {
47
+ fs.writeFileSync(target + '.cmd', `@echo off\r\nnode "${AGENTS_BIN}" ${name} %*\r\n`);
48
+ }
49
+ written.push(name);
50
+ }
51
+ return written;
52
+ }
53
+
54
+ // Self-updater entry: the upgrade installs with --ignore-scripts (skipping
55
+ // this script as an npm lifecycle hook), then re-invokes it with this env var
56
+ // so the alias shims are refreshed from the newly installed copy. Shims only —
57
+ // no prompts, no rc-file edits, no output.
58
+ if (process.env.AGENTS_POSTINSTALL_SHIMS_ONLY === '1') {
59
+ fs.mkdirSync(SHIMS_DIR, { recursive: true });
60
+ writeAliasShims();
61
+ process.exit(0);
62
+ }
63
+
35
64
  // For local installs, create directories and show a message
36
65
  const isGlobalInstall = process.env.npm_config_global || process.argv.includes('-g');
37
66
  if (!isGlobalInstall) {
@@ -86,25 +115,6 @@ const exportLine = shellName === 'fish'
86
115
  ? `fish_add_path ${SHIMS_DIR}`
87
116
  : `export PATH="${SHIMS_DIR}:$PATH"`;
88
117
 
89
- // Shorthands that delegate to the installed agents-cli entrypoint.
90
- const ALIASES = ['sessions', 'secrets', 'browser', 'pty', 'teams'];
91
-
92
- function writeAliasShims() {
93
- const written = [];
94
- for (const name of ALIASES) {
95
- const target = path.join(SHIMS_DIR, name);
96
- const script = `#!/bin/sh\nAGENTS_BIN=${shellQuote(AGENTS_BIN)}\nif [ -z "$AGENTS_BIN" ] || [ ! -x "$AGENTS_BIN" ]; then\n echo "agents: agents-cli entrypoint missing or not executable: $AGENTS_BIN" >&2\n exit 127\nfi\nexec "$AGENTS_BIN" ${name} "$@"\n`;
97
- fs.writeFileSync(target, script, { mode: 0o755 });
98
- // Windows can't run the POSIX shim; drop a `.cmd` companion that invokes the
99
- // entrypoint via node so the bare shorthand works in a Windows shell.
100
- if (process.platform === 'win32') {
101
- fs.writeFileSync(target + '.cmd', `@echo off\r\nnode "${AGENTS_BIN}" ${name} %*\r\n`);
102
- }
103
- written.push(name);
104
- }
105
- return written;
106
- }
107
-
108
118
  function getVersion() {
109
119
  const pkgPath = new URL('../package.json', import.meta.url).pathname;
110
120
  try {