@phnx-labs/agents-cli 1.20.26 → 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 +26 -0
- package/dist/commands/doctor.d.ts +5 -2
- package/dist/commands/doctor.js +126 -27
- 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/heal.d.ts +107 -0
- package/dist/lib/heal.js +279 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- 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/session/remote.d.ts +33 -0
- package/dist/lib/session/remote.js +114 -0
- 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 +7 -0
- package/dist/lib/versions.d.ts +3 -1
- package/dist/lib/versions.js +35 -3
- package/package.json +3 -3
package/dist/lib/plugins.js
CHANGED
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
import * as fs from 'fs';
|
|
12
12
|
import * as path from 'path';
|
|
13
13
|
import { execFileSync } from 'child_process';
|
|
14
|
-
import { getPluginsDir, getTrashPluginsDir, getExtraPluginsDir, getProjectPluginsDir } from './state.js';
|
|
14
|
+
import { getPluginsDir, getTrashPluginsDir, getExtraPluginsDir, getProjectPluginsDir, getSystemPluginsDir } from './state.js';
|
|
15
15
|
import { IS_WINDOWS, isWindowsAbsolutePath, homeDir } from './platform/index.js';
|
|
16
16
|
import { assertSafeGitTransport } from './git.js';
|
|
17
17
|
import { listInstalledVersions, getVersionHomePath } from './versions.js';
|
|
18
18
|
import { AGENTS, agentConfigDirName } from './agents.js';
|
|
19
19
|
import { capableAgents, isCapable } from './capabilities.js';
|
|
20
20
|
import { shouldInstallCommandAsSkill, installCommandSkillToVersion } from './command-skills.js';
|
|
21
|
-
import { copyPluginToMarketplace, syncMarketplaceManifest, registerMarketplace, unregisterMarketplace, addPluginToSettings, removePluginFromSettings, removePluginFromMarketplace, marketplaceIsEmpty, removeEmptyMarketplaceDir, isInstalledInMarketplace, marketplaceRoot, discoverMarketplaces, marketplaceNameFor, MARKETPLACE_NAME, PROJECT_MARKETPLACE_NAME, } from './plugin-marketplace.js';
|
|
21
|
+
import { copyPluginToMarketplace, syncMarketplaceManifest, registerMarketplace, unregisterMarketplace, addPluginToSettings, removePluginFromSettings, removePluginFromMarketplace, marketplaceIsEmpty, removeEmptyMarketplaceDir, isInstalledInMarketplace, marketplaceRoot, discoverMarketplaces, marketplaceNameFor, MARKETPLACE_NAME, PROJECT_MARKETPLACE_NAME, SYSTEM_MARKETPLACE_NAME, } from './plugin-marketplace.js';
|
|
22
22
|
const PLUGIN_MANIFEST_DIR = '.claude-plugin';
|
|
23
23
|
const PLUGIN_MANIFEST_FILE = 'plugin.json';
|
|
24
24
|
const USER_CONFIG_FILE = '.user-config.json';
|
|
@@ -186,11 +186,20 @@ export function assertPluginTargetContained(targetRoot, pluginsDir) {
|
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
/**
|
|
189
|
-
* Get a specific plugin by name.
|
|
189
|
+
* Get a specific plugin by name. On a cross-marketplace name collision the
|
|
190
|
+
* highest-precedence scope wins (project > extra > user > system) — the same
|
|
191
|
+
* resolution the sync writer's Map(last-wins) dedupe and collectPluginScopes()
|
|
192
|
+
* use. discoverPlugins() yields low→high precedence order, so the LAST match is
|
|
193
|
+
* the winner; returning the first match would resolve to the lowest scope (e.g.
|
|
194
|
+
* a system plugin over the user's same-named one), which is exactly backwards.
|
|
190
195
|
*/
|
|
191
196
|
export function getPlugin(name) {
|
|
192
197
|
const plugins = discoverPlugins();
|
|
193
|
-
|
|
198
|
+
for (let i = plugins.length - 1; i >= 0; i--) {
|
|
199
|
+
if (plugins[i].name === name)
|
|
200
|
+
return plugins[i];
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
194
203
|
}
|
|
195
204
|
/**
|
|
196
205
|
* Check if an agent supports a specific plugin.
|
|
@@ -382,13 +391,18 @@ export function checkPluginDependencies(manifest) {
|
|
|
382
391
|
/**
|
|
383
392
|
* Reconstruct a MarketplaceSpec from a marketplace name. The inverse of
|
|
384
393
|
* marketplaceNameFor(): "agents-cli" → user, "agents-project" → project,
|
|
385
|
-
* "agents-<alias>" → extra. The per-version
|
|
386
|
-
* the name (never spec.root), but we
|
|
387
|
-
* spec is honest for any caller that
|
|
394
|
+
* "agents-system" → system, "agents-<alias>" → extra. The per-version
|
|
395
|
+
* marketplace operations only key off the name (never spec.root), but we
|
|
396
|
+
* resolve the real source root anyway so the spec is honest for any caller that
|
|
397
|
+
* inspects it (e.g. descriptionFor, which would otherwise label the system
|
|
398
|
+
* marketplace as an extra repo named "system").
|
|
388
399
|
*/
|
|
389
400
|
function marketplaceSpecForName(name, cwd = process.cwd()) {
|
|
390
401
|
if (!name || name === MARKETPLACE_NAME)
|
|
391
402
|
return { kind: 'user' };
|
|
403
|
+
if (name === SYSTEM_MARKETPLACE_NAME) {
|
|
404
|
+
return { kind: 'system', root: getSystemPluginsDir() };
|
|
405
|
+
}
|
|
392
406
|
if (name === PROJECT_MARKETPLACE_NAME) {
|
|
393
407
|
return { kind: 'project', root: getProjectPluginsDir(cwd) ?? '' };
|
|
394
408
|
}
|
|
@@ -1096,10 +1110,38 @@ export async function installPlugin(spec) {
|
|
|
1096
1110
|
throw new Error(`Installed source has no valid .claude-plugin/plugin.json`);
|
|
1097
1111
|
}
|
|
1098
1112
|
const capabilities = inspectPluginCapabilities(targetRoot);
|
|
1099
|
-
// Persist source for future updates
|
|
1100
|
-
|
|
1113
|
+
// Persist source for future updates. `version` records the manifest version
|
|
1114
|
+
// at pull time — a baseline that lets the heal path tell "central is an
|
|
1115
|
+
// untouched copy of upstream" (safe to fast-forward) from "the user edited
|
|
1116
|
+
// it" (leave alone) without hashing the whole tree.
|
|
1117
|
+
fs.writeFileSync(path.join(targetRoot, SOURCE_FILE), JSON.stringify({ source, isGit: !isLocalPath, version: manifest.version }), 'utf-8');
|
|
1101
1118
|
return { name: manifest.name, root: targetRoot, isNew, capabilities };
|
|
1102
1119
|
}
|
|
1120
|
+
/** Read a plugin's `.source` provenance, or null when absent/unreadable. */
|
|
1121
|
+
export function readPluginSourceInfo(root) {
|
|
1122
|
+
const f = path.join(root, SOURCE_FILE);
|
|
1123
|
+
if (!fs.existsSync(f))
|
|
1124
|
+
return null;
|
|
1125
|
+
try {
|
|
1126
|
+
return JSON.parse(fs.readFileSync(f, 'utf-8'));
|
|
1127
|
+
}
|
|
1128
|
+
catch {
|
|
1129
|
+
return null;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Resolve the CURRENT upstream manifest version for a local-sourced plugin
|
|
1134
|
+
* (the `.system`/local-path case). Returns null for git sources — reading their
|
|
1135
|
+
* upstream version would need a network fetch, so git plugins are refreshed only
|
|
1136
|
+
* via the explicit `agents plugins update`.
|
|
1137
|
+
*/
|
|
1138
|
+
export function getUpstreamManifestVersion(info) {
|
|
1139
|
+
if (info.isGit)
|
|
1140
|
+
return null;
|
|
1141
|
+
const resolved = info.source.replace(/^~/, homeDir());
|
|
1142
|
+
const m = loadPluginManifest(resolved);
|
|
1143
|
+
return m?.version ?? null;
|
|
1144
|
+
}
|
|
1103
1145
|
/**
|
|
1104
1146
|
* Update an installed plugin by re-pulling from its original source.
|
|
1105
1147
|
* Returns true if the update succeeded.
|
|
@@ -1136,11 +1178,14 @@ export async function updatePlugin(name) {
|
|
|
1136
1178
|
: null;
|
|
1137
1179
|
fs.rmSync(plugin.root, { recursive: true, force: true });
|
|
1138
1180
|
fs.cpSync(resolvedSource, plugin.root, { recursive: true });
|
|
1139
|
-
fs.writeFileSync(path.join(plugin.root, SOURCE_FILE), JSON.stringify(sourceInfo), 'utf-8');
|
|
1140
1181
|
if (userConfigBackup !== null) {
|
|
1141
1182
|
fs.writeFileSync(userConfigPath, userConfigBackup, 'utf-8');
|
|
1142
1183
|
}
|
|
1143
1184
|
}
|
|
1185
|
+
// Re-stamp .source with the freshly pulled manifest version so the baseline
|
|
1186
|
+
// tracks what's now on disk (keeps the heal "unmodified?" check honest).
|
|
1187
|
+
const freshVersion = loadPluginManifest(plugin.root)?.version;
|
|
1188
|
+
fs.writeFileSync(path.join(plugin.root, SOURCE_FILE), JSON.stringify({ ...sourceInfo, version: freshVersion }), 'utf-8');
|
|
1144
1189
|
}
|
|
1145
1190
|
catch (err) {
|
|
1146
1191
|
return { success: false, error: err.message };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSH target: a bare ssh-config host alias (e.g. `yosemite-s1`) or `user@host`.
|
|
3
|
+
* The strict allowlist blocks shell metacharacters and a leading `-`, so a target
|
|
4
|
+
* can never be smuggled in as an ssh argv flag.
|
|
5
|
+
*/
|
|
6
|
+
export declare const SSH_TARGET_RE: RegExp;
|
|
7
|
+
export declare function assertValidSshTarget(host: string): void;
|
|
8
|
+
/** POSIX single-quote a string for safe interpolation into a remote shell command. */
|
|
9
|
+
export declare function shellQuote(s: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Strip the `--host`/`-H` flag (and its value) from a raw `agents sessions` argv,
|
|
12
|
+
* leaving the args to forward to the remote unchanged. The remote runs the same
|
|
13
|
+
* binary, so every other flag (`--since`, `--last`, `--json`, query, …) carries
|
|
14
|
+
* over for free. Handles every form commander accepts: `--host h`, `--host=h`,
|
|
15
|
+
* `-H h`, `-H=h`, and the glued short form `-Hh`.
|
|
16
|
+
*
|
|
17
|
+
* @param argv full process argv; the sessions args begin at index 2
|
|
18
|
+
* (`[runtime, script, 'sessions', ...]`).
|
|
19
|
+
*/
|
|
20
|
+
export declare function buildForwardedArgs(argv: string[], hosts?: Set<string>): string[];
|
|
21
|
+
/**
|
|
22
|
+
* Build the single remote command string for `ssh <host> <cmd>`. Forwarded args
|
|
23
|
+
* are quoted for the inner login shell, then the whole `agents …` invocation is
|
|
24
|
+
* quoted again so it survives `bash -lc <...>`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function buildRemoteCommand(forwardedArgs: string[]): string;
|
|
27
|
+
/**
|
|
28
|
+
* Run the current `agents sessions` invocation on one or more remote machines over
|
|
29
|
+
* SSH, streaming each remote's output to the terminal. Sets `process.exitCode = 1`
|
|
30
|
+
* if any host fails. Reads the invocation from `process.argv` (override via
|
|
31
|
+
* `argv` for testing).
|
|
32
|
+
*/
|
|
33
|
+
export declare function runRemoteSessions(hosts: string[], argv?: string[]): void;
|
|
@@ -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
|
+
}
|
|
@@ -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;
|
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": {
|