@phnx-labs/agents-cli 1.20.71 → 1.20.73
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 +325 -1
- package/README.md +40 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/activity.d.ts +12 -0
- package/dist/commands/activity.js +68 -0
- package/dist/commands/apply.d.ts +13 -0
- package/dist/commands/apply.js +28 -3
- package/dist/commands/browser.js +1 -1
- package/dist/commands/cloud.js +21 -11
- package/dist/commands/commands.js +2 -0
- package/dist/commands/doctor.js +8 -4
- package/dist/commands/events.d.ts +11 -10
- package/dist/commands/events.js +30 -21
- package/dist/commands/exec.d.ts +34 -0
- package/dist/commands/exec.js +337 -39
- package/dist/commands/feed.d.ts +13 -0
- package/dist/commands/feed.js +130 -29
- package/dist/commands/hq.d.ts +2 -0
- package/dist/commands/hq.js +58 -0
- package/dist/commands/lease.d.ts +19 -0
- package/dist/commands/lease.js +138 -11
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +123 -0
- package/dist/commands/logs.js +39 -10
- package/dist/commands/mcp.js +2 -0
- package/dist/commands/mine.d.ts +27 -0
- package/dist/commands/mine.js +207 -0
- package/dist/commands/monitors.js +79 -49
- package/dist/commands/plugins.js +1 -0
- package/dist/commands/profiles.js +158 -0
- package/dist/commands/repo.js +2 -60
- package/dist/commands/resource-view.d.ts +2 -0
- package/dist/commands/resource-view.js +8 -0
- package/dist/commands/resources.d.ts +5 -0
- package/dist/commands/resources.js +95 -0
- package/dist/commands/routines.d.ts +2 -0
- package/dist/commands/routines.js +139 -37
- package/dist/commands/secrets.js +234 -47
- package/dist/commands/send.d.ts +15 -0
- package/dist/commands/send.js +62 -0
- package/dist/commands/sessions-tail.d.ts +1 -0
- package/dist/commands/sessions-tail.js +20 -9
- package/dist/commands/sessions.d.ts +6 -0
- package/dist/commands/sessions.js +26 -3
- package/dist/commands/setup-mine.d.ts +18 -0
- package/dist/commands/setup-mine.js +106 -0
- package/dist/commands/setup-share.js +2 -2
- package/dist/commands/setup.js +3 -1
- package/dist/commands/share.d.ts +8 -1
- package/dist/commands/share.js +122 -37
- package/dist/commands/skills.js +2 -0
- package/dist/commands/ssh.d.ts +9 -0
- package/dist/commands/ssh.js +196 -26
- package/dist/commands/status.js +8 -2
- package/dist/commands/subagents.js +3 -1
- package/dist/commands/sync.js +1 -1
- package/dist/commands/teams.js +11 -2
- package/dist/commands/uninstall.d.ts +11 -0
- package/dist/commands/uninstall.js +158 -0
- package/dist/commands/utils.d.ts +29 -0
- package/dist/commands/utils.js +24 -0
- package/dist/commands/versions.js +120 -11
- package/dist/index.js +83 -12
- package/dist/lib/activity.d.ts +124 -0
- package/dist/lib/activity.js +542 -0
- package/dist/lib/agents.js +16 -5
- package/dist/lib/artifact-actions.d.ts +1 -1
- package/dist/lib/artifact-actions.js +1 -1
- package/dist/lib/ask-classifier.d.ts +2 -1
- package/dist/lib/ask-classifier.js +3 -3
- package/dist/lib/auth-health.d.ts +32 -4
- package/dist/lib/auth-health.js +40 -1
- package/dist/lib/auto-pull.js +8 -1
- package/dist/lib/brand.d.ts +40 -0
- package/dist/lib/brand.js +122 -0
- package/dist/lib/browser/drivers/ssh.js +4 -7
- package/dist/lib/browser/service.js +6 -8
- package/dist/lib/channels/providers/index.d.ts +2 -0
- package/dist/lib/channels/providers/index.js +20 -0
- package/dist/lib/channels/providers/mailbox.d.ts +2 -0
- package/dist/lib/channels/providers/mailbox.js +26 -0
- package/dist/lib/channels/providers/openclaw-telegram.d.ts +2 -0
- package/dist/lib/channels/providers/openclaw-telegram.js +34 -0
- package/dist/lib/channels/providers/rush.d.ts +6 -0
- package/dist/lib/channels/providers/rush.js +55 -0
- package/dist/lib/channels/registry.d.ts +42 -0
- package/dist/lib/channels/registry.js +20 -0
- package/dist/lib/channels/resolve.d.ts +11 -0
- package/dist/lib/channels/resolve.js +12 -0
- package/dist/lib/cli-resources.d.ts +18 -0
- package/dist/lib/cli-resources.js +53 -4
- package/dist/lib/cloud/codex.js +6 -3
- package/dist/lib/cloud/factory.d.ts +1 -0
- package/dist/lib/cloud/factory.js +10 -4
- package/dist/lib/cloud/rush.d.ts +3 -1
- package/dist/lib/cloud/rush.js +6 -1
- package/dist/lib/cloud/types.d.ts +2 -0
- package/dist/lib/codex-home.d.ts +35 -0
- package/dist/lib/codex-home.js +136 -0
- package/dist/lib/crabbox/cli.d.ts +44 -0
- package/dist/lib/crabbox/cli.js +120 -20
- package/dist/lib/crabbox/lease.d.ts +52 -1
- package/dist/lib/crabbox/lease.js +117 -16
- package/dist/lib/crabbox/progress.d.ts +31 -0
- package/dist/lib/crabbox/progress.js +76 -0
- package/dist/lib/crabbox/runtimes.d.ts +4 -0
- package/dist/lib/crabbox/runtimes.js +33 -8
- package/dist/lib/crabbox/setup-copy.d.ts +87 -0
- package/dist/lib/crabbox/setup-copy.js +127 -0
- package/dist/lib/daemon.d.ts +14 -5
- package/dist/lib/daemon.js +52 -9
- package/dist/lib/drive-sync.js +6 -3
- package/dist/lib/event-stream.d.ts +36 -0
- package/dist/lib/event-stream.js +68 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +11 -0
- package/dist/lib/exec.js +23 -2
- package/dist/lib/feed-policy.d.ts +1 -1
- package/dist/lib/feed-policy.js +14 -9
- package/dist/lib/feed-ranking.d.ts +32 -0
- package/dist/lib/feed-ranking.js +224 -0
- package/dist/lib/feed.d.ts +35 -2
- package/dist/lib/feed.js +66 -3
- package/dist/lib/fleet/auth-sync.d.ts +53 -0
- package/dist/lib/fleet/auth-sync.js +92 -0
- package/dist/lib/fleet/remote-login.d.ts +187 -0
- package/dist/lib/fleet/remote-login.js +556 -0
- package/dist/lib/format.d.ts +30 -3
- package/dist/lib/format.js +34 -5
- package/dist/lib/fs-atomic.d.ts +7 -2
- package/dist/lib/fs-atomic.js +8 -12
- package/dist/lib/git.d.ts +16 -0
- package/dist/lib/git.js +79 -2
- package/dist/lib/heal.js +11 -3
- package/dist/lib/hosts/logs.d.ts +12 -0
- package/dist/lib/hosts/logs.js +18 -0
- package/dist/lib/hosts/progress.d.ts +28 -10
- package/dist/lib/hosts/progress.js +79 -22
- package/dist/lib/hosts/remote-cmd.js +4 -0
- package/dist/lib/hq/floor.d.ts +87 -0
- package/dist/lib/hq/floor.js +226 -0
- package/dist/lib/menubar/install-menubar.js +14 -20
- package/dist/lib/notify.d.ts +1 -0
- package/dist/lib/notify.js +3 -3
- package/dist/lib/open-url.d.ts +2 -0
- package/dist/lib/open-url.js +19 -0
- package/dist/lib/openclaw-keychain.d.ts +56 -0
- package/dist/lib/openclaw-keychain.js +236 -0
- package/dist/lib/overdue.js +10 -0
- package/dist/lib/permissions.d.ts +44 -1
- package/dist/lib/permissions.js +284 -7
- package/dist/lib/project-launch.d.ts +6 -12
- package/dist/lib/project-launch.js +13 -228
- package/dist/lib/project-resources.d.ts +7 -0
- package/dist/lib/project-resources.js +291 -0
- package/dist/lib/pty-client.d.ts +27 -0
- package/dist/lib/pty-client.js +136 -10
- package/dist/lib/refresh.js +14 -10
- package/dist/lib/resource-profiles.d.ts +26 -0
- package/dist/lib/resource-profiles.js +157 -0
- package/dist/lib/resources/permissions.js +7 -1
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/resources.d.ts +1 -1
- package/dist/lib/resources.js +32 -3
- package/dist/lib/routines.d.ts +11 -0
- package/dist/lib/routines.js +56 -15
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.js +66 -5
- package/dist/lib/secrets/bundles.d.ts +16 -3
- package/dist/lib/secrets/bundles.js +206 -37
- package/dist/lib/secrets/icloud-import.d.ts +2 -2
- package/dist/lib/secrets/icloud-import.js +9 -6
- package/dist/lib/secrets/mcp.js +1 -1
- package/dist/lib/secrets/vault-age-helper.d.ts +1 -0
- package/dist/lib/secrets/vault-age-helper.js +34 -0
- package/dist/lib/secrets/vault.d.ts +49 -0
- package/dist/lib/secrets/vault.js +397 -0
- package/dist/lib/self-heal/checks/path.js +3 -1
- package/dist/lib/self-heal/checks/shadowing.js +10 -2
- package/dist/lib/self-heal/checks/shims.js +19 -11
- package/dist/lib/session/cloud.d.ts +2 -2
- package/dist/lib/session/cloud.js +2 -2
- package/dist/lib/session/db.d.ts +4 -0
- package/dist/lib/session/db.js +44 -7
- package/dist/lib/session/discover.d.ts +2 -0
- package/dist/lib/session/discover.js +114 -3
- package/dist/lib/session/stream-render.d.ts +3 -0
- package/dist/lib/session/stream-render.js +130 -0
- package/dist/lib/session/types.d.ts +6 -0
- package/dist/lib/share/analytics.d.ts +13 -0
- package/dist/lib/share/analytics.js +45 -0
- package/dist/lib/share/config.d.ts +19 -5
- package/dist/lib/share/config.js +44 -8
- package/dist/lib/share/provision.d.ts +58 -6
- package/dist/lib/share/provision.js +97 -22
- package/dist/lib/share/publish.d.ts +36 -13
- package/dist/lib/share/publish.js +55 -8
- package/dist/lib/share/worker-template.js +83 -17
- package/dist/lib/shims.d.ts +36 -0
- package/dist/lib/shims.js +120 -16
- package/dist/lib/ssh-exec.d.ts +14 -0
- package/dist/lib/ssh-exec.js +57 -0
- package/dist/lib/staleness/detectors/hooks.js +25 -1
- package/dist/lib/staleness/detectors/permissions.js +66 -0
- package/dist/lib/staleness/detectors/workflows.js +20 -0
- package/dist/lib/staleness/writers/hooks.js +58 -4
- package/dist/lib/staleness/writers/permissions.js +2 -2
- package/dist/lib/staleness/writers/skills.js +1 -1
- package/dist/lib/staleness/writers/sources.d.ts +10 -1
- package/dist/lib/staleness/writers/sources.js +68 -1
- package/dist/lib/star-nudge.d.ts +45 -0
- package/dist/lib/star-nudge.js +91 -0
- package/dist/lib/startup/command-registry.d.ts +7 -1
- package/dist/lib/startup/command-registry.js +19 -3
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +3 -0
- package/dist/lib/subagents-registry.d.ts +2 -0
- package/dist/lib/subagents-registry.js +3 -0
- package/dist/lib/teams/parsers.js +214 -6
- package/dist/lib/teams/supervisor.d.ts +7 -0
- package/dist/lib/teams/supervisor.js +2 -1
- package/dist/lib/template.d.ts +1 -1
- package/dist/lib/template.js +1 -1
- package/dist/lib/triggers/webhook.js +36 -3
- package/dist/lib/types.d.ts +73 -0
- package/dist/lib/uninstall.d.ts +84 -0
- package/dist/lib/uninstall.js +347 -0
- package/dist/lib/usage.d.ts +13 -1
- package/dist/lib/usage.js +32 -14
- package/dist/lib/version.d.ts +40 -0
- package/dist/lib/version.js +94 -16
- package/dist/lib/versions.d.ts +27 -0
- package/dist/lib/versions.js +248 -79
- package/dist/lib/workflows.d.ts +2 -0
- package/dist/lib/workflows.js +88 -0
- package/package.json +2 -1
- package/dist/commands/daemon.d.ts +0 -10
- package/dist/commands/daemon.js +0 -121
package/dist/lib/shims.js
CHANGED
|
@@ -17,6 +17,7 @@ import { IS_WINDOWS, prependToWindowsUserPath } from './platform/index.js';
|
|
|
17
17
|
import { getShimsDir, getVersionsDir, getBackupsDir, getHistoryDir, ensureAgentsDir } from './state.js';
|
|
18
18
|
export { getShimsDir };
|
|
19
19
|
import { AGENTS, agentConfigDirName, readAuthAccountIdentity } from './agents.js';
|
|
20
|
+
import { codexHomeShimBash } from './codex-home.js';
|
|
20
21
|
/**
|
|
21
22
|
* Files and directories to always skip during conflict detection and migration.
|
|
22
23
|
* These are never user config that should be migrated.
|
|
@@ -261,12 +262,7 @@ if [ "\$(uname -s)" = "Linux" ] && [ -z "\${CLAUDE_CODE_OAUTH_TOKEN:-}" ] && [ -
|
|
|
261
262
|
fi
|
|
262
263
|
`
|
|
263
264
|
: agent === 'codex'
|
|
264
|
-
? `
|
|
265
|
-
# Codex reads its config (approval_policy, sandbox_mode, MCP servers, rules)
|
|
266
|
-
# from CODEX_HOME. Point it at the versioned home so permissions/rules
|
|
267
|
-
# written by agents-cli actually take effect.
|
|
268
|
-
export CODEX_HOME="$VERSION_DIR/home/${configDirName}"
|
|
269
|
-
`
|
|
265
|
+
? codexHomeShimBash(`$VERSION_DIR/home/${configDirName}`, `$AGENTS_USER_DIR/.codex-homes/$VERSION`)
|
|
270
266
|
: agent === 'copilot'
|
|
271
267
|
? `
|
|
272
268
|
# GitHub Copilot CLI honors COPILOT_HOME to relocate its config and state
|
|
@@ -682,6 +678,75 @@ export function createShim(agent) {
|
|
|
682
678
|
}
|
|
683
679
|
return shimPath;
|
|
684
680
|
}
|
|
681
|
+
// ─── White-label brand shims ──────────────────────────────────────────────────
|
|
682
|
+
// A brand shim is a PURE pass-through: unlike an agent shim (which resolves a
|
|
683
|
+
// version and execs the agent binary) or an alias shim (which injects a fixed
|
|
684
|
+
// subcommand), it forwards argv verbatim to the agents-cli entrypoint with
|
|
685
|
+
// `AGENTS_BRAND` set, so `<brand> <any-verb>` behaves exactly like
|
|
686
|
+
// `agents <any-verb>` but presents under the brand's name. See lib/brand.ts.
|
|
687
|
+
const BRAND_SHIM_MARKER = '# Brand shim:';
|
|
688
|
+
/** The POSIX pass-through shim for a brand. */
|
|
689
|
+
export function generateBrandShim(name) {
|
|
690
|
+
const agentsBin = shellQuote(getAgentsBinForGeneratedShim());
|
|
691
|
+
return `#!/bin/sh
|
|
692
|
+
# Auto-generated by agents-cli - do not edit
|
|
693
|
+
${BRAND_SHIM_MARKER} ${name} -> agents-cli (white-label)
|
|
694
|
+
AGENTS_BIN=${agentsBin}
|
|
695
|
+
if [ -z "$AGENTS_BIN" ] || [ ! -x "$AGENTS_BIN" ]; then
|
|
696
|
+
echo "${name}: agents-cli entrypoint missing or not executable: $AGENTS_BIN" >&2
|
|
697
|
+
exit 127
|
|
698
|
+
fi
|
|
699
|
+
export AGENTS_BRAND=${name}
|
|
700
|
+
exec "$AGENTS_BIN" "$@"
|
|
701
|
+
`;
|
|
702
|
+
}
|
|
703
|
+
/** Write a brand's pass-through shim(s) onto PATH; returns the shim path. */
|
|
704
|
+
export function createBrandShim(name) {
|
|
705
|
+
ensureAgentsDir();
|
|
706
|
+
const shimsDir = getShimsDir();
|
|
707
|
+
const shimPath = path.join(shimsDir, name);
|
|
708
|
+
const targets = shimTargetsFor(process.platform);
|
|
709
|
+
if (targets.bash) {
|
|
710
|
+
fs.writeFileSync(shimPath, generateBrandShim(name), { mode: 0o755 });
|
|
711
|
+
}
|
|
712
|
+
if (targets.cmd) {
|
|
713
|
+
writeWindowsBrandShim(shimPath + '.cmd', name);
|
|
714
|
+
}
|
|
715
|
+
return shimPath;
|
|
716
|
+
}
|
|
717
|
+
/** Windows `.cmd` pass-through: set AGENTS_BRAND then forward argv to the entrypoint. */
|
|
718
|
+
function writeWindowsBrandShim(cmdPath, name) {
|
|
719
|
+
const indexJs = getAgentsBinForGeneratedShim();
|
|
720
|
+
const content = `@echo off\r\n` +
|
|
721
|
+
`rem Auto-generated by agents-cli - do not edit\r\n` +
|
|
722
|
+
`rem Brand shim: ${name}\r\n` +
|
|
723
|
+
`set AGENTS_BRAND=${name}\r\n` +
|
|
724
|
+
`node "${indexJs}" %*\r\n`;
|
|
725
|
+
fs.writeFileSync(cmdPath, content);
|
|
726
|
+
}
|
|
727
|
+
/** True when the file at the given path is an agents-cli brand shim. */
|
|
728
|
+
export function isBrandShim(filePath) {
|
|
729
|
+
try {
|
|
730
|
+
const head = fs.readFileSync(filePath, 'utf-8').slice(0, 300);
|
|
731
|
+
return head.includes(BRAND_SHIM_MARKER) || head.includes('rem Brand shim:');
|
|
732
|
+
}
|
|
733
|
+
catch {
|
|
734
|
+
return false;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
/** Remove a brand's shim companions. Returns true if anything was removed. */
|
|
738
|
+
export function removeBrandShim(name) {
|
|
739
|
+
const shimsDir = getShimsDir();
|
|
740
|
+
const shimPath = path.join(shimsDir, name);
|
|
741
|
+
let removed = false;
|
|
742
|
+
for (const p of [shimPath, shimPath + '.cmd']) {
|
|
743
|
+
if (fs.existsSync(p) && isBrandShim(p)) {
|
|
744
|
+
fs.unlinkSync(p);
|
|
745
|
+
removed = true;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return removed;
|
|
749
|
+
}
|
|
685
750
|
/**
|
|
686
751
|
* Generate a Windows `.cmd` launcher that delegates to `agents __shim <spec>`.
|
|
687
752
|
* `spec` is the agent's cliCommand for the default-version shim, or
|
|
@@ -785,6 +850,29 @@ function assertSafeVersion(version) {
|
|
|
785
850
|
throw new Error(`Refusing to generate shim for unsafe version: ${JSON.stringify(version)}`);
|
|
786
851
|
}
|
|
787
852
|
}
|
|
853
|
+
/**
|
|
854
|
+
* Agents whose config directory can be relocated by an environment variable
|
|
855
|
+
* (the per-agent `managedEnv` block in `generateVersionedAliasScript` below).
|
|
856
|
+
*
|
|
857
|
+
* Only these can be installed with `agents add --isolated`: the versioned alias
|
|
858
|
+
* points that env var at the copy's private home, so the copy never reads or
|
|
859
|
+
* writes the user's real `~/.<agent>`. Every other agent isolates ONLY by
|
|
860
|
+
* adopting `~/.<agent>` via a symlink — which an isolated install deliberately
|
|
861
|
+
* skips — so an isolated copy would silently fall back to (and mutate) the real
|
|
862
|
+
* config dir. For those agents `--isolated` is refused up front.
|
|
863
|
+
*
|
|
864
|
+
* KEEP IN SYNC with the `managedEnv` switch in `generateVersionedAliasScript`.
|
|
865
|
+
* The colocated test `shims.isolation-capability.test.ts` enforces this.
|
|
866
|
+
*/
|
|
867
|
+
export const CONFIG_ENV_ISOLATED_AGENTS = ['claude', 'codex', 'copilot', 'grok', 'kimi'];
|
|
868
|
+
/**
|
|
869
|
+
* Whether an agent supports a clean `--isolated` install — i.e. its config
|
|
870
|
+
* location can be redirected by an env var so the isolated copy stays fully
|
|
871
|
+
* separate from the user's real `~/.<agent>`. See {@link CONFIG_ENV_ISOLATED_AGENTS}.
|
|
872
|
+
*/
|
|
873
|
+
export function supportsIsolatedInstall(agent) {
|
|
874
|
+
return CONFIG_ENV_ISOLATED_AGENTS.includes(agent);
|
|
875
|
+
}
|
|
788
876
|
/**
|
|
789
877
|
* Generate a versioned alias script that directly execs a specific version.
|
|
790
878
|
* e.g., claude@2.0.65 -> directly runs that version's binary
|
|
@@ -806,12 +894,7 @@ export CLAUDE_CONFIG_DIR="$HOME/.agents/.history/versions/${agent}/${version}/ho
|
|
|
806
894
|
export DISABLE_AUTOUPDATER="\${DISABLE_AUTOUPDATER:-1}"
|
|
807
895
|
`
|
|
808
896
|
: agent === 'codex'
|
|
809
|
-
? `
|
|
810
|
-
# Codex reads its config (approval_policy, sandbox_mode, MCP servers, rules)
|
|
811
|
-
# from CODEX_HOME. Point direct aliases at the versioned home so permissions
|
|
812
|
-
# and rules written by agents-cli actually take effect.
|
|
813
|
-
export CODEX_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}"
|
|
814
|
-
`
|
|
897
|
+
? codexHomeShimBash(`$HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}`, `$HOME/.agents/.codex-homes/${version}`)
|
|
815
898
|
: agent === 'copilot'
|
|
816
899
|
? `
|
|
817
900
|
# Copilot honors COPILOT_HOME to relocate ~/.copilot (settings, mcp-config.json,
|
|
@@ -1039,7 +1122,7 @@ export function versionedAliasExists(agent, version) {
|
|
|
1039
1122
|
* Get the path to the agent's config directory in HOME.
|
|
1040
1123
|
* e.g., ~/.claude for claude, ~/.codex for codex
|
|
1041
1124
|
*/
|
|
1042
|
-
function getAgentConfigPath(agent) {
|
|
1125
|
+
export function getAgentConfigPath(agent) {
|
|
1043
1126
|
const agentConfig = AGENTS[agent];
|
|
1044
1127
|
const home = process.env.AGENTS_REAL_HOME || os.homedir();
|
|
1045
1128
|
return agentConfig.configDir.replace(os.homedir(), home);
|
|
@@ -1567,7 +1650,10 @@ export function getConfigSymlinkVersion(agent) {
|
|
|
1567
1650
|
if (!stat.isSymbolicLink()) {
|
|
1568
1651
|
return null;
|
|
1569
1652
|
}
|
|
1570
|
-
|
|
1653
|
+
// Normalize separators so this matches on Windows too — readlinkSync there
|
|
1654
|
+
// returns backslash paths, which the forward-slash-only regex never matched
|
|
1655
|
+
// (misclassifying an owned symlink as foreign, e.g. in `agents uninstall`).
|
|
1656
|
+
const target = fs.readlinkSync(configPath).replace(/\\/g, '/');
|
|
1571
1657
|
// Extract version from path like ~/.agents/versions/claude/2.0.65/home/.claude
|
|
1572
1658
|
const match = target.match(/versions\/[^/]+\/([^/]+)\/home/);
|
|
1573
1659
|
return match ? match[1] : null;
|
|
@@ -2194,7 +2280,7 @@ function isShimPathCommandLine(line, shimsDir) {
|
|
|
2194
2280
|
}
|
|
2195
2281
|
return trimmed.startsWith('export PATH=') || trimmed.startsWith('fish_add_path ');
|
|
2196
2282
|
}
|
|
2197
|
-
function stripShimPathLines(content, shimsDir) {
|
|
2283
|
+
export function stripShimPathLines(content, shimsDir) {
|
|
2198
2284
|
const lines = content.split('\n');
|
|
2199
2285
|
const kept = [];
|
|
2200
2286
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -2342,6 +2428,24 @@ export function listAgentsWithInstalledVersions() {
|
|
|
2342
2428
|
.map((entry) => entry.name)
|
|
2343
2429
|
.filter((agent) => fs.readdirSync(path.join(versionsDir, agent), { withFileTypes: true }).some((entry) => entry.isDirectory()));
|
|
2344
2430
|
}
|
|
2431
|
+
function isInstalledVersionIsolated(agent, version) {
|
|
2432
|
+
return fs.existsSync(path.join(getVersionsDir(), agent, version, '.isolated'));
|
|
2433
|
+
}
|
|
2434
|
+
export function listAgentsWithNonIsolatedInstalledVersions() {
|
|
2435
|
+
const versionsDir = getVersionsDir();
|
|
2436
|
+
if (!fs.existsSync(versionsDir)) {
|
|
2437
|
+
return [];
|
|
2438
|
+
}
|
|
2439
|
+
const entries = fs.readdirSync(versionsDir, { withFileTypes: true });
|
|
2440
|
+
return entries
|
|
2441
|
+
.filter((entry) => entry.isDirectory() && AGENTS[entry.name])
|
|
2442
|
+
.map((entry) => entry.name)
|
|
2443
|
+
.filter((agent) => {
|
|
2444
|
+
const agentVersionsDir = path.join(versionsDir, agent);
|
|
2445
|
+
return fs.readdirSync(agentVersionsDir, { withFileTypes: true })
|
|
2446
|
+
.some((entry) => entry.isDirectory() && !isInstalledVersionIsolated(agent, entry.name));
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2345
2449
|
/**
|
|
2346
2450
|
* Create shims for all installed agents.
|
|
2347
2451
|
*/
|
|
@@ -2356,7 +2460,7 @@ function ensureAllShims() {
|
|
|
2356
2460
|
const agent = entry.name;
|
|
2357
2461
|
const agentVersionsDir = path.join(versionsDir, agent);
|
|
2358
2462
|
const versions = fs.readdirSync(agentVersionsDir, { withFileTypes: true })
|
|
2359
|
-
.filter((e) => e.isDirectory());
|
|
2463
|
+
.filter((e) => e.isDirectory() && !isInstalledVersionIsolated(agent, e.name));
|
|
2360
2464
|
if (versions.length > 0 && !shimExists(agent)) {
|
|
2361
2465
|
createShim(agent);
|
|
2362
2466
|
}
|
package/dist/lib/ssh-exec.d.ts
CHANGED
|
@@ -89,6 +89,20 @@ export interface SshExecRawResult {
|
|
|
89
89
|
* U+FFFD replacement char (which would desync a byte offset from the wire).
|
|
90
90
|
*/
|
|
91
91
|
export declare function sshExecRaw(target: string, remoteCmd: string, opts?: SshExecOptions): SshExecRawResult;
|
|
92
|
+
export interface SshExecRawStreamOptions extends SshExecOptions {
|
|
93
|
+
onStdout: (chunk: Buffer) => void;
|
|
94
|
+
onStderr?: (chunk: Buffer) => void;
|
|
95
|
+
signal?: AbortSignal;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Stream raw stdout from a long-lived remote command over ssh.
|
|
99
|
+
*
|
|
100
|
+
* Unlike {@link sshStream}, this does not inherit local stdio: callers receive
|
|
101
|
+
* byte-exact stdout chunks and decide how to account for them. That matters for
|
|
102
|
+
* offset-resumed log following where a UTF-8 decode boundary must not shift the
|
|
103
|
+
* byte cursor.
|
|
104
|
+
*/
|
|
105
|
+
export declare function sshExecRawStream(target: string, remoteCmd: string, opts: SshExecRawStreamOptions): Promise<Omit<SshExecRawResult, 'stdout'>>;
|
|
92
106
|
/** True if `target` is reachable over ssh (a passwordless `true` succeeds quickly). */
|
|
93
107
|
export declare function sshReachable(target: string, timeoutMs?: number): boolean;
|
|
94
108
|
export interface SshStreamOptions {
|
package/dist/lib/ssh-exec.js
CHANGED
|
@@ -205,6 +205,63 @@ export function sshExecRaw(target, remoteCmd, opts = {}) {
|
|
|
205
205
|
timedOut,
|
|
206
206
|
};
|
|
207
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Stream raw stdout from a long-lived remote command over ssh.
|
|
210
|
+
*
|
|
211
|
+
* Unlike {@link sshStream}, this does not inherit local stdio: callers receive
|
|
212
|
+
* byte-exact stdout chunks and decide how to account for them. That matters for
|
|
213
|
+
* offset-resumed log following where a UTF-8 decode boundary must not shift the
|
|
214
|
+
* byte cursor.
|
|
215
|
+
*/
|
|
216
|
+
export function sshExecRawStream(target, remoteCmd, opts) {
|
|
217
|
+
assertValidSshTarget(target);
|
|
218
|
+
const mux = opts.multiplex === false ? [] : controlOpts();
|
|
219
|
+
const args = [...sshConnectOpts(mux, opts.hostKeyOpts), ...(opts.extraSshArgs ?? []), target, remoteCmd];
|
|
220
|
+
return new Promise((resolve) => {
|
|
221
|
+
const child = spawn('ssh', args, {
|
|
222
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
223
|
+
windowsHide: true,
|
|
224
|
+
});
|
|
225
|
+
const stderr = [];
|
|
226
|
+
let settled = false;
|
|
227
|
+
let timedOut = false;
|
|
228
|
+
let aborted = false;
|
|
229
|
+
const finish = (code) => {
|
|
230
|
+
if (settled)
|
|
231
|
+
return;
|
|
232
|
+
settled = true;
|
|
233
|
+
if (timer)
|
|
234
|
+
clearTimeout(timer);
|
|
235
|
+
opts.signal?.removeEventListener('abort', abort);
|
|
236
|
+
resolve({ code, stderr: Buffer.concat(stderr), timedOut });
|
|
237
|
+
};
|
|
238
|
+
const abort = () => {
|
|
239
|
+
aborted = true;
|
|
240
|
+
child.kill('SIGTERM');
|
|
241
|
+
};
|
|
242
|
+
const timer = opts.timeoutMs
|
|
243
|
+
? setTimeout(() => {
|
|
244
|
+
timedOut = true;
|
|
245
|
+
child.kill('SIGTERM');
|
|
246
|
+
}, opts.timeoutMs)
|
|
247
|
+
: null;
|
|
248
|
+
child.stdout.on('data', (chunk) => { opts.onStdout(chunk); });
|
|
249
|
+
child.stderr.on('data', (chunk) => {
|
|
250
|
+
stderr.push(chunk);
|
|
251
|
+
opts.onStderr?.(chunk);
|
|
252
|
+
});
|
|
253
|
+
child.stdin.on('error', () => { });
|
|
254
|
+
child.stdin.end(opts.input ?? '');
|
|
255
|
+
opts.signal?.addEventListener('abort', abort, { once: true });
|
|
256
|
+
child.on('error', (err) => {
|
|
257
|
+
stderr.push(Buffer.from(err.message));
|
|
258
|
+
finish(null);
|
|
259
|
+
});
|
|
260
|
+
child.on('close', (code) => {
|
|
261
|
+
finish(aborted ? null : code);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
}
|
|
208
265
|
/** True if `target` is reachable over ssh (a passwordless `true` succeeds quickly). */
|
|
209
266
|
export function sshReachable(target, timeoutMs = 10000) {
|
|
210
267
|
return sshExec(target, 'true', { timeoutMs, multiplex: true }).code === 0;
|
|
@@ -8,6 +8,30 @@ import * as path from 'path';
|
|
|
8
8
|
import { capableAgents } from '../../capabilities.js';
|
|
9
9
|
import { resolveHookSource } from '../writers/sources.js';
|
|
10
10
|
import { lazyAgentMap } from '../writers/lazy-map.js';
|
|
11
|
+
function hookSourcesMatch(src, dest) {
|
|
12
|
+
const srcStat = fs.lstatSync(src);
|
|
13
|
+
if (srcStat.isSymbolicLink())
|
|
14
|
+
return false;
|
|
15
|
+
if (srcStat.isDirectory()) {
|
|
16
|
+
if (!fs.existsSync(dest) || !fs.lstatSync(dest).isDirectory())
|
|
17
|
+
return false;
|
|
18
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
if (entry.isSymbolicLink())
|
|
21
|
+
continue;
|
|
22
|
+
const srcPath = path.join(src, entry.name);
|
|
23
|
+
const destPath = path.join(dest, entry.name);
|
|
24
|
+
if (!hookSourcesMatch(srcPath, destPath))
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
if (!srcStat.isFile())
|
|
30
|
+
return false;
|
|
31
|
+
if (!fs.existsSync(dest) || !fs.lstatSync(dest).isFile())
|
|
32
|
+
return false;
|
|
33
|
+
return fs.readFileSync(src, 'utf-8') === fs.readFileSync(dest, 'utf-8');
|
|
34
|
+
}
|
|
11
35
|
function buildHooksDetector(agent) {
|
|
12
36
|
return {
|
|
13
37
|
kind: 'hooks',
|
|
@@ -26,7 +50,7 @@ function buildHooksDetector(agent) {
|
|
|
26
50
|
continue;
|
|
27
51
|
}
|
|
28
52
|
try {
|
|
29
|
-
if (
|
|
53
|
+
if (hookSourcesMatch(src, path.join(hooksDir, hook))) {
|
|
30
54
|
synced.push(hook);
|
|
31
55
|
}
|
|
32
56
|
}
|
|
@@ -288,6 +288,69 @@ function buildOpenClawDetector() {
|
|
|
288
288
|
},
|
|
289
289
|
};
|
|
290
290
|
}
|
|
291
|
+
function buildCopilotDetector() {
|
|
292
|
+
return {
|
|
293
|
+
kind: 'permissions',
|
|
294
|
+
agent: 'copilot',
|
|
295
|
+
list({ versionHome, cwd }) {
|
|
296
|
+
const configPath = path.join(versionHome, '.copilot', 'permissions-config.json');
|
|
297
|
+
if (!fs.existsSync(configPath))
|
|
298
|
+
return [];
|
|
299
|
+
try {
|
|
300
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
301
|
+
const locations = config.locations && typeof config.locations === 'object' && !Array.isArray(config.locations)
|
|
302
|
+
? config.locations
|
|
303
|
+
: {};
|
|
304
|
+
const location = locations[path.resolve(cwd)];
|
|
305
|
+
const hasApprovals = Array.isArray(location?.tool_approvals) && location.tool_approvals.length > 0;
|
|
306
|
+
const hasDirectories = Array.isArray(location?.allowed_directories) && location.allowed_directories.length > 0;
|
|
307
|
+
if (hasApprovals || hasDirectories)
|
|
308
|
+
return discoverPermissionGroups().map(g => g.name);
|
|
309
|
+
}
|
|
310
|
+
catch { /* parse fail */ }
|
|
311
|
+
return [];
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function buildForgeDetector() {
|
|
316
|
+
return {
|
|
317
|
+
kind: 'permissions',
|
|
318
|
+
agent: 'forge',
|
|
319
|
+
list({ versionHome }) {
|
|
320
|
+
const permissionsPath = path.join(versionHome, '.forge', 'permissions.yaml');
|
|
321
|
+
if (!fs.existsSync(permissionsPath))
|
|
322
|
+
return [];
|
|
323
|
+
try {
|
|
324
|
+
const config = yaml.parse(fs.readFileSync(permissionsPath, 'utf-8'));
|
|
325
|
+
if (config && Array.isArray(config.policies) && config.policies.length > 0) {
|
|
326
|
+
return discoverPermissionGroups().map(g => g.name);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
catch { /* parse fail */ }
|
|
330
|
+
return [];
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function buildHermesDetector() {
|
|
335
|
+
return {
|
|
336
|
+
kind: 'permissions',
|
|
337
|
+
agent: 'hermes',
|
|
338
|
+
list({ versionHome }) {
|
|
339
|
+
const configPath = path.join(versionHome, '.hermes', 'config.yaml');
|
|
340
|
+
if (!fs.existsSync(configPath))
|
|
341
|
+
return [];
|
|
342
|
+
try {
|
|
343
|
+
const config = yaml.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
344
|
+
const hasAllow = Array.isArray(config?.command_allowlist) && config.command_allowlist.length > 0;
|
|
345
|
+
const hasDeny = Array.isArray(config?.approvals?.deny) && config.approvals.deny.length > 0;
|
|
346
|
+
if (hasAllow || hasDeny)
|
|
347
|
+
return discoverPermissionGroups().map(g => g.name);
|
|
348
|
+
}
|
|
349
|
+
catch { /* parse fail */ }
|
|
350
|
+
return [];
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
}
|
|
291
354
|
const handlers = {
|
|
292
355
|
claude: buildClaudeDetector,
|
|
293
356
|
codex: buildCodexDetector,
|
|
@@ -301,6 +364,9 @@ const handlers = {
|
|
|
301
364
|
droid: buildDroidDetector,
|
|
302
365
|
kiro: buildKiroDetector,
|
|
303
366
|
openclaw: buildOpenClawDetector,
|
|
367
|
+
copilot: buildCopilotDetector,
|
|
368
|
+
forge: buildForgeDetector,
|
|
369
|
+
hermes: buildHermesDetector,
|
|
304
370
|
};
|
|
305
371
|
export const permissionsDetectors = lazyAgentMap(() => {
|
|
306
372
|
const m = {};
|
|
@@ -73,6 +73,26 @@ function buildWorkflowsDetector(agent) {
|
|
|
73
73
|
.filter(d => d.isFile() && d.name.endsWith('.yaml') && !d.name.startsWith('.'))
|
|
74
74
|
.map(d => d.name.slice(0, -'.yaml'.length));
|
|
75
75
|
}
|
|
76
|
+
if (agent === 'openclaw') {
|
|
77
|
+
const dir = path.join(versionHome, '.openclaw', 'workflows');
|
|
78
|
+
if (!fs.existsSync(dir))
|
|
79
|
+
return [];
|
|
80
|
+
return fs.readdirSync(dir, { withFileTypes: true })
|
|
81
|
+
.filter(d => d.isFile() && d.name.endsWith('.lobster') && !d.name.startsWith('.'))
|
|
82
|
+
.filter(d => {
|
|
83
|
+
try {
|
|
84
|
+
const parsed = yaml.parse(fs.readFileSync(path.join(dir, d.name), 'utf-8'));
|
|
85
|
+
const env = parsed && typeof parsed === 'object' && parsed.env && typeof parsed.env === 'object' && !Array.isArray(parsed.env)
|
|
86
|
+
? parsed.env
|
|
87
|
+
: {};
|
|
88
|
+
return env.AGENTS_CLI_WORKFLOW === d.name.slice(0, -'.lobster'.length);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
.map(d => d.name.slice(0, -'.lobster'.length));
|
|
95
|
+
}
|
|
76
96
|
const workflowsDir = path.join(versionHome, 'workflows');
|
|
77
97
|
if (!fs.existsSync(workflowsDir))
|
|
78
98
|
return [];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Hooks writer — copies trusted hook
|
|
2
|
+
* Hooks writer — copies trusted hook sources into `{agentDir}/hooks/`.
|
|
3
3
|
* Caller filters by `supports(agent, 'hooks', version)` before invoking.
|
|
4
4
|
* Orphan sweep (delete hooks in version-home that aren't in `availableNames`)
|
|
5
5
|
* stays in the orchestrator since it depends on the broader available set.
|
|
@@ -12,6 +12,60 @@ import { safeJoin } from '../../paths.js';
|
|
|
12
12
|
import { registerHooksToSettings } from '../../hooks.js';
|
|
13
13
|
import { resolveHookSource } from './sources.js';
|
|
14
14
|
import { lazyAgentMap } from './lazy-map.js';
|
|
15
|
+
function removePath(target) {
|
|
16
|
+
try {
|
|
17
|
+
const stat = fs.lstatSync(target);
|
|
18
|
+
if (stat.isSymbolicLink() || stat.isFile()) {
|
|
19
|
+
fs.unlinkSync(target);
|
|
20
|
+
}
|
|
21
|
+
else if (stat.isDirectory()) {
|
|
22
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
/* already gone */
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function copyDir(src, dest) {
|
|
30
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
31
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
if (entry.isSymbolicLink())
|
|
34
|
+
continue;
|
|
35
|
+
const srcPath = safeJoin(src, entry.name);
|
|
36
|
+
const destPath = safeJoin(dest, entry.name);
|
|
37
|
+
if (entry.isDirectory()) {
|
|
38
|
+
copyDir(srcPath, destPath);
|
|
39
|
+
}
|
|
40
|
+
else if (entry.isFile()) {
|
|
41
|
+
fs.copyFileSync(srcPath, destPath);
|
|
42
|
+
fs.chmodSync(destPath, fs.statSync(srcPath).mode);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function copyHookSource(src, dest) {
|
|
47
|
+
let stat;
|
|
48
|
+
try {
|
|
49
|
+
stat = fs.lstatSync(src);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
if (stat.isSymbolicLink())
|
|
55
|
+
return false;
|
|
56
|
+
removePath(dest);
|
|
57
|
+
if (stat.isDirectory()) {
|
|
58
|
+
copyDir(src, dest);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
if (stat.isFile()) {
|
|
62
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
63
|
+
fs.copyFileSync(src, dest);
|
|
64
|
+
fs.chmodSync(dest, 0o755);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
15
69
|
function buildHooksWriter(agent) {
|
|
16
70
|
return {
|
|
17
71
|
kind: 'hooks',
|
|
@@ -26,9 +80,9 @@ function buildHooksWriter(agent) {
|
|
|
26
80
|
if (!srcFile)
|
|
27
81
|
continue;
|
|
28
82
|
const destFile = safeJoin(hooksTarget, hook);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
83
|
+
if (copyHookSource(srcFile, destFile)) {
|
|
84
|
+
synced.push(hook);
|
|
85
|
+
}
|
|
32
86
|
}
|
|
33
87
|
// Native hook registration in settings.json/hooks.json. Grok is included
|
|
34
88
|
// so subrule-bundled guards (absolute paths outside the central hooks/
|
|
@@ -5,7 +5,7 @@ function buildPermissionsWriter(agent) {
|
|
|
5
5
|
return {
|
|
6
6
|
kind: 'permissions',
|
|
7
7
|
agent,
|
|
8
|
-
write({ versionHome, selection }) {
|
|
8
|
+
write({ versionHome, selection, cwd }) {
|
|
9
9
|
if (selection.length === 0)
|
|
10
10
|
return { synced: [] };
|
|
11
11
|
const built = buildPermissionsFromGroups(selection);
|
|
@@ -13,7 +13,7 @@ function buildPermissionsWriter(agent) {
|
|
|
13
13
|
const hasDeny = (built.deny?.length ?? 0) > 0;
|
|
14
14
|
if (!hasAllow && !hasDeny)
|
|
15
15
|
return { synced: [] };
|
|
16
|
-
const r = applyPermsToVersion(agent, built, versionHome, true);
|
|
16
|
+
const r = applyPermsToVersion(agent, built, versionHome, true, cwd);
|
|
17
17
|
return { synced: r.success ? selection : [] };
|
|
18
18
|
},
|
|
19
19
|
};
|
|
@@ -56,7 +56,7 @@ function buildSkillsWriter(agent) {
|
|
|
56
56
|
fs.mkdirSync(skillsTarget, { recursive: true });
|
|
57
57
|
const synced = [];
|
|
58
58
|
for (const skill of selection) {
|
|
59
|
-
const srcDir = resolveSkillSource(skill);
|
|
59
|
+
const srcDir = resolveSkillSource(skill, { agent });
|
|
60
60
|
if (!srcDir)
|
|
61
61
|
continue;
|
|
62
62
|
const destDir = safeJoin(skillsTarget, skill);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AgentId } from '../../types.js';
|
|
1
2
|
export type EnabledExtra = {
|
|
2
3
|
alias: string;
|
|
3
4
|
dir: string;
|
|
@@ -9,7 +10,15 @@ export declare function trustedSourceBases(): {
|
|
|
9
10
|
/** Find the trusted source for a command markdown by name. */
|
|
10
11
|
export declare function resolveCommandSource(name: string): string | null;
|
|
11
12
|
/** Find the trusted source directory for a skill by name. */
|
|
12
|
-
export declare function resolveSkillSource(name: string
|
|
13
|
+
export declare function resolveSkillSource(name: string, options?: {
|
|
14
|
+
agent?: AgentId;
|
|
15
|
+
plugins?: Set<string>;
|
|
16
|
+
}): string | null;
|
|
17
|
+
/** List trusted plugin-bundled skill names, filtered to an optional plugin/agent scope. */
|
|
18
|
+
export declare function listPluginSkillNames(options?: {
|
|
19
|
+
agent?: AgentId;
|
|
20
|
+
plugins?: Set<string>;
|
|
21
|
+
}): string[];
|
|
13
22
|
/** Find the trusted source file for a hook by name. */
|
|
14
23
|
export declare function resolveHookSource(name: string): string | null;
|
|
15
24
|
/** All trusted command-skill source roots, used to dedup name collisions for commands-as-skills writes. */
|
|
@@ -35,6 +35,50 @@ function isLiveDir(p) {
|
|
|
35
35
|
return false;
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
+
function readPluginManifest(pluginRoot) {
|
|
39
|
+
const manifestPath = path.join(pluginRoot, '.claude-plugin', 'plugin.json');
|
|
40
|
+
if (!isLiveFile(manifestPath))
|
|
41
|
+
return null;
|
|
42
|
+
try {
|
|
43
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
44
|
+
if (!manifest.name || !manifest.version)
|
|
45
|
+
return null;
|
|
46
|
+
return manifest;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function pluginSupportsAgent(manifest, agent) {
|
|
53
|
+
if (!agent)
|
|
54
|
+
return true;
|
|
55
|
+
return !manifest.agents || manifest.agents.length === 0 || manifest.agents.includes(agent);
|
|
56
|
+
}
|
|
57
|
+
function pluginSkillDirs(options = {}) {
|
|
58
|
+
const dirs = [];
|
|
59
|
+
for (const base of trustedSourceBases()) {
|
|
60
|
+
const pluginsDir = path.join(base.dir, 'plugins');
|
|
61
|
+
let entries;
|
|
62
|
+
try {
|
|
63
|
+
entries = fs.readdirSync(pluginsDir, { withFileTypes: true });
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
if (!entry.isDirectory() || entry.name.startsWith('.'))
|
|
70
|
+
continue;
|
|
71
|
+
if (options.plugins && !options.plugins.has(entry.name))
|
|
72
|
+
continue;
|
|
73
|
+
const pluginRoot = path.join(pluginsDir, entry.name);
|
|
74
|
+
const manifest = readPluginManifest(pluginRoot);
|
|
75
|
+
if (!manifest || !pluginSupportsAgent(manifest, options.agent))
|
|
76
|
+
continue;
|
|
77
|
+
dirs.push(path.join(pluginRoot, 'skills'));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return dirs;
|
|
81
|
+
}
|
|
38
82
|
/** Find the trusted source for a command markdown by name. */
|
|
39
83
|
export function resolveCommandSource(name) {
|
|
40
84
|
const candidates = [
|
|
@@ -45,14 +89,36 @@ export function resolveCommandSource(name) {
|
|
|
45
89
|
return candidates.find(isLiveFile) ?? null;
|
|
46
90
|
}
|
|
47
91
|
/** Find the trusted source directory for a skill by name. */
|
|
48
|
-
export function resolveSkillSource(name) {
|
|
92
|
+
export function resolveSkillSource(name, options = {}) {
|
|
49
93
|
const candidates = [
|
|
50
94
|
safeJoin(path.join(getUserAgentsDir(), 'skills'), name),
|
|
51
95
|
safeJoin(getSkillsDir(), name),
|
|
52
96
|
...getEnabledExtraRepos().map((e) => safeJoin(path.join(e.dir, 'skills'), name)),
|
|
97
|
+
...pluginSkillDirs(options).map((skillsDir) => safeJoin(skillsDir, name)),
|
|
53
98
|
];
|
|
54
99
|
return candidates.find(isLiveDir) ?? null;
|
|
55
100
|
}
|
|
101
|
+
/** List trusted plugin-bundled skill names, filtered to an optional plugin/agent scope. */
|
|
102
|
+
export function listPluginSkillNames(options = {}) {
|
|
103
|
+
const names = new Set();
|
|
104
|
+
for (const skillsDir of pluginSkillDirs(options)) {
|
|
105
|
+
let entries;
|
|
106
|
+
try {
|
|
107
|
+
entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
for (const entry of entries) {
|
|
113
|
+
if (!entry.isDirectory() || entry.name.startsWith('.'))
|
|
114
|
+
continue;
|
|
115
|
+
if (isLiveFile(path.join(skillsDir, entry.name, 'SKILL.md'))) {
|
|
116
|
+
names.add(entry.name);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return Array.from(names);
|
|
121
|
+
}
|
|
56
122
|
/** Find the trusted source file for a hook by name. */
|
|
57
123
|
export function resolveHookSource(name) {
|
|
58
124
|
const candidates = [
|
|
@@ -68,5 +134,6 @@ export function trustedSkillRoots() {
|
|
|
68
134
|
path.join(getUserAgentsDir(), 'skills'),
|
|
69
135
|
getSkillsDir(),
|
|
70
136
|
...getEnabledExtraRepos().map((e) => path.join(e.dir, 'skills')),
|
|
137
|
+
...pluginSkillDirs(),
|
|
71
138
|
];
|
|
72
139
|
}
|