@phnx-labs/agents-cli 1.20.25 → 1.20.27
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 +37 -0
- package/dist/commands/doctor.d.ts +5 -2
- package/dist/commands/doctor.js +126 -27
- package/dist/commands/inspect.d.ts +2 -1
- package/dist/commands/inspect.js +1 -1
- package/dist/commands/menubar.js +6 -1
- package/dist/commands/repo.js +40 -0
- package/dist/commands/secrets.js +16 -12
- package/dist/commands/sessions.js +20 -1
- package/dist/index.js +2 -12
- package/dist/lib/agent-spec.d.ts +36 -0
- package/dist/lib/agent-spec.js +157 -0
- package/dist/lib/agents.js +1 -0
- package/dist/lib/daemon.js +32 -0
- package/dist/lib/doctor-diff.d.ts +7 -0
- package/dist/lib/doctor-diff.js +18 -13
- package/dist/lib/fs-atomic.d.ts +3 -2
- package/dist/lib/fs-atomic.js +22 -7
- package/dist/lib/heal.d.ts +107 -0
- package/dist/lib/heal.js +279 -0
- package/dist/lib/hooks.js +36 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +27 -3
- package/dist/lib/menubar/install-menubar.js +74 -9
- package/dist/lib/plugin-marketplace.d.ts +18 -0
- package/dist/lib/plugin-marketplace.js +67 -1
- package/dist/lib/plugins.d.ts +23 -1
- package/dist/lib/plugins.js +55 -10
- package/dist/lib/resources/rules.d.ts +5 -2
- package/dist/lib/resources/rules.js +39 -10
- package/dist/lib/rules/compose.d.ts +22 -0
- package/dist/lib/rules/compose.js +114 -9
- package/dist/lib/secrets/agent.d.ts +4 -2
- package/dist/lib/secrets/agent.js +6 -4
- package/dist/lib/secrets/bundles.d.ts +20 -14
- package/dist/lib/secrets/bundles.js +31 -10
- package/dist/lib/session/remote.d.ts +33 -0
- package/dist/lib/session/remote.js +114 -0
- package/dist/lib/staleness/checkers/rules.js +13 -1
- package/dist/lib/staleness/detectors/commands.js +7 -6
- package/dist/lib/staleness/writers/commands.js +7 -12
- package/dist/lib/startup/dev-build.d.ts +22 -0
- package/dist/lib/startup/dev-build.js +41 -0
- package/dist/lib/types.d.ts +13 -2
- package/dist/lib/versions.d.ts +3 -1
- package/dist/lib/versions.js +35 -3
- package/package.json +3 -3
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents sessions --host <target>` — run the session query on a remote machine
|
|
3
|
+
* over SSH and stream its output back. Session transcripts and the index DB live
|
|
4
|
+
* on the machine that produced them (see `discover.ts`, all `os.homedir()`-rooted),
|
|
5
|
+
* so instead of syncing the bytes here we invoke the *remote's own* `agents
|
|
6
|
+
* sessions` against its already-built index and forward stdout verbatim.
|
|
7
|
+
*
|
|
8
|
+
* This is the live counterpart to `agents sessions sync` (R2/CRDT, eventual): no
|
|
9
|
+
* upfront copy, always current, but the peer must be reachable. SSH access is the
|
|
10
|
+
* only auth — if you can `ssh <host>`, you own the box (no identity layer by design).
|
|
11
|
+
*
|
|
12
|
+
* Mirrors the transport already used by `agents secrets export --to-ssh`
|
|
13
|
+
* (`src/commands/secrets.ts`): `ssh -o BatchMode=yes <host> bash -lc '<cmd>'`,
|
|
14
|
+
* with `bash -lc` so the remote login PATH resolves `agents`.
|
|
15
|
+
*/
|
|
16
|
+
import { spawnSync } from 'child_process';
|
|
17
|
+
import chalk from 'chalk';
|
|
18
|
+
/**
|
|
19
|
+
* SSH target: a bare ssh-config host alias (e.g. `yosemite-s1`) or `user@host`.
|
|
20
|
+
* The strict allowlist blocks shell metacharacters and a leading `-`, so a target
|
|
21
|
+
* can never be smuggled in as an ssh argv flag.
|
|
22
|
+
*/
|
|
23
|
+
export const SSH_TARGET_RE = /^[a-zA-Z0-9._-]+(@[a-zA-Z0-9._-]+)?$/;
|
|
24
|
+
export function assertValidSshTarget(host) {
|
|
25
|
+
if (!SSH_TARGET_RE.test(host)) {
|
|
26
|
+
throw new Error(`Invalid SSH target ${JSON.stringify(host)}. Expected a host alias or user@host ` +
|
|
27
|
+
`(letters, digits, '.', '_', '-').`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** POSIX single-quote a string for safe interpolation into a remote shell command. */
|
|
31
|
+
export function shellQuote(s) {
|
|
32
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Strip the `--host`/`-H` flag (and its value) from a raw `agents sessions` argv,
|
|
36
|
+
* leaving the args to forward to the remote unchanged. The remote runs the same
|
|
37
|
+
* binary, so every other flag (`--since`, `--last`, `--json`, query, …) carries
|
|
38
|
+
* over for free. Handles every form commander accepts: `--host h`, `--host=h`,
|
|
39
|
+
* `-H h`, `-H=h`, and the glued short form `-Hh`.
|
|
40
|
+
*
|
|
41
|
+
* @param argv full process argv; the sessions args begin at index 2
|
|
42
|
+
* (`[runtime, script, 'sessions', ...]`).
|
|
43
|
+
*/
|
|
44
|
+
export function buildForwardedArgs(argv, hosts = new Set()) {
|
|
45
|
+
const args = argv.slice(2);
|
|
46
|
+
const out = [];
|
|
47
|
+
for (let i = 0; i < args.length; i++) {
|
|
48
|
+
const a = args[i];
|
|
49
|
+
if (a === '--host' || a === '-H') {
|
|
50
|
+
// Commander's `<target...>` variadic accepts both `--host a --host b` and
|
|
51
|
+
// `--host a b` — consume every consecutive token that is a known host so
|
|
52
|
+
// the variadic form doesn't leak the extra hosts into the remote argv.
|
|
53
|
+
// Fall back to consuming the single next token when we have no host set
|
|
54
|
+
// (e.g. malformed input) so the flag value never leaks either way.
|
|
55
|
+
if (hosts.size > 0) {
|
|
56
|
+
while (i + 1 < args.length && hosts.has(args[i + 1]))
|
|
57
|
+
i++;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
i++; // also consume the separate value token
|
|
61
|
+
}
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (a.startsWith('--host=') || a.startsWith('-H='))
|
|
65
|
+
continue;
|
|
66
|
+
if (/^-H.+/.test(a))
|
|
67
|
+
continue; // glued short form: -Hyosemite-s1
|
|
68
|
+
out.push(a);
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Build the single remote command string for `ssh <host> <cmd>`. Forwarded args
|
|
74
|
+
* are quoted for the inner login shell, then the whole `agents …` invocation is
|
|
75
|
+
* quoted again so it survives `bash -lc <...>`.
|
|
76
|
+
*/
|
|
77
|
+
export function buildRemoteCommand(forwardedArgs) {
|
|
78
|
+
const inner = ['agents', ...forwardedArgs].map(shellQuote).join(' ');
|
|
79
|
+
return `bash -lc ${shellQuote(inner)}`;
|
|
80
|
+
}
|
|
81
|
+
const SSH_OPTS = [
|
|
82
|
+
'-o', 'BatchMode=yes',
|
|
83
|
+
'-o', 'StrictHostKeyChecking=accept-new',
|
|
84
|
+
'-o', 'ConnectTimeout=10',
|
|
85
|
+
];
|
|
86
|
+
/**
|
|
87
|
+
* Run the current `agents sessions` invocation on one or more remote machines over
|
|
88
|
+
* SSH, streaming each remote's output to the terminal. Sets `process.exitCode = 1`
|
|
89
|
+
* if any host fails. Reads the invocation from `process.argv` (override via
|
|
90
|
+
* `argv` for testing).
|
|
91
|
+
*/
|
|
92
|
+
export function runRemoteSessions(hosts, argv = process.argv) {
|
|
93
|
+
for (const host of hosts)
|
|
94
|
+
assertValidSshTarget(host); // fail fast on any bad target
|
|
95
|
+
const remoteCmd = buildRemoteCommand(buildForwardedArgs(argv, new Set(hosts)));
|
|
96
|
+
const multi = hosts.length > 1;
|
|
97
|
+
let failures = 0;
|
|
98
|
+
for (const host of hosts) {
|
|
99
|
+
if (multi)
|
|
100
|
+
process.stdout.write(chalk.cyan(`\n── ${host} ──\n`));
|
|
101
|
+
const res = spawnSync('ssh', [...SSH_OPTS, host, remoteCmd], { stdio: 'inherit' });
|
|
102
|
+
if (res.error) {
|
|
103
|
+
failures++;
|
|
104
|
+
console.error(chalk.red(`${host}: ${res.error.message}`));
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (res.status !== 0) {
|
|
108
|
+
failures++;
|
|
109
|
+
console.error(chalk.red(`${host}: remote query failed (exit ${res.status ?? 'signal'}).`));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (failures > 0)
|
|
113
|
+
process.exitCode = 1;
|
|
114
|
+
}
|
|
@@ -54,7 +54,19 @@ function activeSources(agent, version, cwd) {
|
|
|
54
54
|
result['rules.yaml'] = yamlPath;
|
|
55
55
|
}
|
|
56
56
|
for (const sub of compose.subrules) {
|
|
57
|
-
|
|
57
|
+
// Key off the actual source path so dir-form subrules (subrules/<name>/rule.md)
|
|
58
|
+
// and flat ones (subrules/<name>.md) both fingerprint the file that really
|
|
59
|
+
// contributes to the output, not a hard-coded `.md` that may not exist.
|
|
60
|
+
if (sub.subruleDir) {
|
|
61
|
+
result[`subrules/${sub.name}/rule.md`] = sub.sourcePath;
|
|
62
|
+
// Fingerprint hooks.yaml too so editing a hook re-syncs the rules section.
|
|
63
|
+
const hooksFile = path.join(sub.subruleDir, 'hooks.yaml');
|
|
64
|
+
if (fs.existsSync(hooksFile))
|
|
65
|
+
result[`subrules/${sub.name}/hooks.yaml`] = hooksFile;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
result[`subrules/${sub.name}.md`] = sub.sourcePath;
|
|
69
|
+
}
|
|
58
70
|
}
|
|
59
71
|
return result;
|
|
60
72
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Commands detector — mirrors versions.ts:343-357. Inspects the version home,
|
|
3
|
-
* returns command names. Honors the commands-as-skills marker for
|
|
4
|
-
* Codex >= 0.117.0; falls back to scanning
|
|
5
|
-
* the native path.
|
|
3
|
+
* returns command names. Honors the commands-as-skills marker for skills-only
|
|
4
|
+
* agents (grok, kimi, Codex >= 0.117.0, …); falls back to scanning
|
|
5
|
+
* `{agentDir}/<commandsSubdir>/` for the native path.
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from 'fs';
|
|
8
8
|
import * as path from 'path';
|
|
@@ -29,13 +29,14 @@ function buildCommandsDetector(agent) {
|
|
|
29
29
|
},
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
|
-
// Detector registration mirrors writers/commands.ts —
|
|
33
|
-
//
|
|
32
|
+
// Detector registration mirrors writers/commands.ts — skills-capable agents
|
|
33
|
+
// with no native command-file dir convert commands to skills by default; only
|
|
34
|
+
// agents with their own slash-command runtime (nativeCommandRuntime) opt out.
|
|
34
35
|
export const commandsDetectors = lazyAgentMap(() => {
|
|
35
36
|
const m = {};
|
|
36
37
|
for (const id of Object.keys(AGENTS)) {
|
|
37
38
|
const cfg = AGENTS[id];
|
|
38
|
-
if (cfg.capabilities.commands === false && (!cfg.commandsSubdir || cfg.commandsSubdir === '') &&
|
|
39
|
+
if (cfg.capabilities.commands === false && (!cfg.commandsSubdir || cfg.commandsSubdir === '') && cfg.nativeCommandRuntime)
|
|
39
40
|
continue;
|
|
40
41
|
const hasCommands = cfg.capabilities.commands !== false;
|
|
41
42
|
const hasSkills = cfg.capabilities.skills !== false;
|
|
@@ -80,25 +80,20 @@ function buildCommandsWriter(agent) {
|
|
|
80
80
|
// - commands-as-skills (grok, codex >= 0.117.0)
|
|
81
81
|
//
|
|
82
82
|
// Agents that have skills but use a NATIVE non-file slash-command system
|
|
83
|
-
// (openclaw → Gateway-based commands) are NOT registered.
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
// resolver).
|
|
83
|
+
// (openclaw → Gateway-based commands) are NOT registered. They declare
|
|
84
|
+
// `nativeCommandRuntime: true` to opt out — their own runtime resolves slash
|
|
85
|
+
// commands, so there's nothing to write and nothing to convert.
|
|
87
86
|
export const commandsWriters = lazyAgentMap(() => {
|
|
88
87
|
const m = {};
|
|
89
88
|
for (const id of Object.keys(AGENTS)) {
|
|
90
89
|
const cfg = AGENTS[id];
|
|
91
90
|
if (cfg.capabilities.commands === false && (!cfg.skillsDir || cfg.skillsDir === ''))
|
|
92
91
|
continue;
|
|
93
|
-
//
|
|
92
|
+
// Skills-capable agent with no native command-file dir: convert commands to
|
|
93
|
+
// skills by default (grok, kimi, …). Opt out only agents with their own
|
|
94
|
+
// slash-command runtime (openclaw).
|
|
94
95
|
if (cfg.capabilities.commands === false && (!cfg.commandsSubdir || cfg.commandsSubdir === '')) {
|
|
95
|
-
|
|
96
|
-
// Distinguish: grok has skillsDir set; openclaw also has skillsDir, so we
|
|
97
|
-
// can't use that. The cleanest signal is the agent's `cliCommand` set —
|
|
98
|
-
// openclaw flags `commands: false` AND has its Gateway runtime, while
|
|
99
|
-
// grok flags `commands: false` because grok's slash commands are skills.
|
|
100
|
-
// We opt in explicitly: only grok takes commands-as-skills today.
|
|
101
|
-
if (id !== 'grok')
|
|
96
|
+
if (cfg.nativeCommandRuntime)
|
|
102
97
|
continue;
|
|
103
98
|
}
|
|
104
99
|
const hasCommands = cfg.capabilities.commands !== false;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide whether the CLI is running from a source checkout (a "dev build") vs an
|
|
3
|
+
* installed package. Dev builds suppress autopull / migrations / auto-update so
|
|
4
|
+
* iterating on the repo never mutates the user's real setup.
|
|
5
|
+
*
|
|
6
|
+
* Two signals:
|
|
7
|
+
* 1. A `0.0.0-dev*` version stamp (scripts/install.sh dev installs).
|
|
8
|
+
* 2. Running out of an actual agents-cli git checkout.
|
|
9
|
+
*
|
|
10
|
+
* Signal 2 must be precise. The naive check —
|
|
11
|
+
* `existsSync(dirname(dirname(argv[1])) + '/.git')` — false-positives badly:
|
|
12
|
+
* - npm-global bins are symlinks. `/opt/homebrew/bin/agents` →
|
|
13
|
+
* `…/node_modules/@phnx-labs/agents-cli/dist/index.js`. Without resolving the
|
|
14
|
+
* symlink, `dirname(dirname())` walks to `/opt/homebrew`, which is **itself a
|
|
15
|
+
* git repo** (Homebrew). So every Homebrew-node user looked like a dev build
|
|
16
|
+
* and had migrations + the menu-bar self-heal silently disabled.
|
|
17
|
+
*
|
|
18
|
+
* Fix: resolve the symlink with realpath, then require the `.git`'s repo root to
|
|
19
|
+
* actually be the agents-cli package (its package.json `name`), not some
|
|
20
|
+
* unrelated ancestor that happens to be version-controlled.
|
|
21
|
+
*/
|
|
22
|
+
export declare function detectDevBuild(argv1: string, version: string): boolean;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Decide whether the CLI is running from a source checkout (a "dev build") vs an
|
|
5
|
+
* installed package. Dev builds suppress autopull / migrations / auto-update so
|
|
6
|
+
* iterating on the repo never mutates the user's real setup.
|
|
7
|
+
*
|
|
8
|
+
* Two signals:
|
|
9
|
+
* 1. A `0.0.0-dev*` version stamp (scripts/install.sh dev installs).
|
|
10
|
+
* 2. Running out of an actual agents-cli git checkout.
|
|
11
|
+
*
|
|
12
|
+
* Signal 2 must be precise. The naive check —
|
|
13
|
+
* `existsSync(dirname(dirname(argv[1])) + '/.git')` — false-positives badly:
|
|
14
|
+
* - npm-global bins are symlinks. `/opt/homebrew/bin/agents` →
|
|
15
|
+
* `…/node_modules/@phnx-labs/agents-cli/dist/index.js`. Without resolving the
|
|
16
|
+
* symlink, `dirname(dirname())` walks to `/opt/homebrew`, which is **itself a
|
|
17
|
+
* git repo** (Homebrew). So every Homebrew-node user looked like a dev build
|
|
18
|
+
* and had migrations + the menu-bar self-heal silently disabled.
|
|
19
|
+
*
|
|
20
|
+
* Fix: resolve the symlink with realpath, then require the `.git`'s repo root to
|
|
21
|
+
* actually be the agents-cli package (its package.json `name`), not some
|
|
22
|
+
* unrelated ancestor that happens to be version-controlled.
|
|
23
|
+
*/
|
|
24
|
+
export function detectDevBuild(argv1, version) {
|
|
25
|
+
if (version.startsWith('0.0.0-dev'))
|
|
26
|
+
return true;
|
|
27
|
+
try {
|
|
28
|
+
const cliPath = fs.realpathSync(argv1 || '');
|
|
29
|
+
const repoRoot = path.dirname(path.dirname(cliPath));
|
|
30
|
+
if (!fs.existsSync(path.join(repoRoot, '.git')))
|
|
31
|
+
return false;
|
|
32
|
+
const pkgPath = path.join(repoRoot, 'package.json');
|
|
33
|
+
if (!fs.existsSync(pkgPath))
|
|
34
|
+
return false;
|
|
35
|
+
const name = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))?.name;
|
|
36
|
+
return name === '@phnx-labs/agents-cli';
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -77,6 +77,13 @@ export interface AgentConfig {
|
|
|
77
77
|
commandsDir: string;
|
|
78
78
|
commandsSubdir: string;
|
|
79
79
|
skillsDir: string;
|
|
80
|
+
/**
|
|
81
|
+
* Agent resolves slash-commands through its own runtime (e.g. openclaw's
|
|
82
|
+
* Gateway), so agents-cli commands must NOT be converted into skills for it.
|
|
83
|
+
* Skills-capable agents WITHOUT a native command-file dir convert commands to
|
|
84
|
+
* skills by default; set this to opt such an agent out of that conversion.
|
|
85
|
+
*/
|
|
86
|
+
nativeCommandRuntime?: boolean;
|
|
80
87
|
hooksDir: string;
|
|
81
88
|
pluginManifestDir?: string;
|
|
82
89
|
instructionsFile: string;
|
|
@@ -563,9 +570,13 @@ export interface ExtraRepoConfig {
|
|
|
563
570
|
export interface Meta {
|
|
564
571
|
agents?: Partial<Record<AgentId, string>>;
|
|
565
572
|
run?: RunConfig;
|
|
566
|
-
/** macOS secrets-agent config. `
|
|
567
|
-
*
|
|
573
|
+
/** macOS secrets-agent config. `policy` is the default prompt policy for
|
|
574
|
+
* bundles without an explicit per-bundle policy: `daily` (the default) asks
|
|
575
|
+
* once per ~24h, `always` asks every time. `auto` (default on) lets the first
|
|
576
|
+
* real keychain read of a `daily` bundle populate the broker so concurrent
|
|
577
|
+
* runs read silently — set it `false` to force a prompt on every read. */
|
|
568
578
|
secrets?: {
|
|
579
|
+
policy?: 'always' | 'daily';
|
|
569
580
|
agent?: {
|
|
570
581
|
auto?: boolean;
|
|
571
582
|
};
|
package/dist/lib/versions.d.ts
CHANGED
|
@@ -144,8 +144,10 @@ export declare function isOldestInstalled(agent: AgentId): Promise<{
|
|
|
144
144
|
installed: boolean;
|
|
145
145
|
version: string | null;
|
|
146
146
|
}>;
|
|
147
|
+
/** Drop the installed-versions cache (call after install/remove mutations). */
|
|
148
|
+
export declare function invalidateInstalledVersionsCache(agent?: AgentId): void;
|
|
147
149
|
/**
|
|
148
|
-
* List all installed versions for an agent.
|
|
150
|
+
* List all installed versions for an agent (cached by versions-dir mtime).
|
|
149
151
|
*/
|
|
150
152
|
export declare function listInstalledVersions(agent: AgentId): string[];
|
|
151
153
|
/**
|
package/dist/lib/versions.js
CHANGED
|
@@ -898,14 +898,36 @@ export async function isOldestInstalled(agent) {
|
|
|
898
898
|
}
|
|
899
899
|
return { installed: isVersionInstalled(agent, oldestVersion), version: oldestVersion };
|
|
900
900
|
}
|
|
901
|
+
// Per-process cache for listInstalledVersions. The agent's versions dir mtime
|
|
902
|
+
// changes whenever a version dir is added or removed (install/remove), so a
|
|
903
|
+
// stamp match means the installed set is unchanged and we skip the readdir +
|
|
904
|
+
// N binary stats. Mirrors the readMeta() cache in state.ts. Hot path:
|
|
905
|
+
// resolveAgentTargets and every enumerate-style consumer hit this.
|
|
906
|
+
const installedVersionsCache = new Map();
|
|
907
|
+
/** Drop the installed-versions cache (call after install/remove mutations). */
|
|
908
|
+
export function invalidateInstalledVersionsCache(agent) {
|
|
909
|
+
if (agent)
|
|
910
|
+
installedVersionsCache.delete(agent);
|
|
911
|
+
else
|
|
912
|
+
installedVersionsCache.clear();
|
|
913
|
+
}
|
|
901
914
|
/**
|
|
902
|
-
* List all installed versions for an agent.
|
|
915
|
+
* List all installed versions for an agent (cached by versions-dir mtime).
|
|
903
916
|
*/
|
|
904
917
|
export function listInstalledVersions(agent) {
|
|
905
918
|
const agentVersionsDir = path.join(getVersionsDir(), agent);
|
|
906
|
-
|
|
919
|
+
let stamp;
|
|
920
|
+
try {
|
|
921
|
+
stamp = fs.statSync(agentVersionsDir).mtimeMs;
|
|
922
|
+
}
|
|
923
|
+
catch {
|
|
924
|
+
installedVersionsCache.set(agent, { stamp: 0, versions: [] });
|
|
907
925
|
return [];
|
|
908
926
|
}
|
|
927
|
+
const cached = installedVersionsCache.get(agent);
|
|
928
|
+
if (cached && cached.stamp === stamp) {
|
|
929
|
+
return cached.versions;
|
|
930
|
+
}
|
|
909
931
|
const entries = fs.readdirSync(agentVersionsDir, { withFileTypes: true });
|
|
910
932
|
const versions = [];
|
|
911
933
|
for (const entry of entries) {
|
|
@@ -916,7 +938,9 @@ export function listInstalledVersions(agent) {
|
|
|
916
938
|
}
|
|
917
939
|
}
|
|
918
940
|
}
|
|
919
|
-
|
|
941
|
+
versions.sort(compareVersions);
|
|
942
|
+
installedVersionsCache.set(agent, { stamp, versions });
|
|
943
|
+
return versions;
|
|
920
944
|
}
|
|
921
945
|
/**
|
|
922
946
|
* List every version directory for an agent, including ones missing the
|
|
@@ -1831,7 +1855,15 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1831
1855
|
// dot-dirs to keep plugin-managed subtrees (.plugins/, .promptcuts) intact.
|
|
1832
1856
|
const skillsTargetSweep = path.join(agentDir, 'skills');
|
|
1833
1857
|
if (!userPassedSelection && fs.existsSync(skillsTargetSweep) && !fs.lstatSync(skillsTargetSweep).isSymbolicLink()) {
|
|
1858
|
+
// Trust real skills AND command-skills: when commandsAsSkills, the
|
|
1859
|
+
// commands writer (above) materialized each command as a skill dir under
|
|
1860
|
+
// skills/. Those names are not in skillsToSync, so without this they'd be
|
|
1861
|
+
// swept as orphans — silently deleting every converted command (e.g.
|
|
1862
|
+
// /recap on kimi/grok).
|
|
1834
1863
|
const trustedSkills = new Set(skillsToSync);
|
|
1864
|
+
if (commandsAsSkills)
|
|
1865
|
+
for (const cmd of commandsToSync)
|
|
1866
|
+
trustedSkills.add(cmd);
|
|
1835
1867
|
for (const entry of fs.readdirSync(skillsTargetSweep, { withFileTypes: true })) {
|
|
1836
1868
|
if (!entry.isDirectory() || entry.name.startsWith('.'))
|
|
1837
1869
|
continue;
|
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.27",
|
|
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",
|
|
@@ -92,10 +92,10 @@
|
|
|
92
92
|
"diff": "9.0.0",
|
|
93
93
|
"marked": "15.0.12",
|
|
94
94
|
"marked-terminal": "7.3.0",
|
|
95
|
-
"ora": "9.4.
|
|
95
|
+
"ora": "9.4.1",
|
|
96
96
|
"proper-lockfile": "4.1.2",
|
|
97
97
|
"simple-git": "3.36.0",
|
|
98
|
-
"smol-toml": "1.
|
|
98
|
+
"smol-toml": "1.7.0",
|
|
99
99
|
"yaml": "2.9.0"
|
|
100
100
|
},
|
|
101
101
|
"devDependencies": {
|