@phnx-labs/agents-cli 1.17.6 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ **Plugins**
6
+
7
+ - `~/.agents/plugins/` is now a first-class user-resource location, alongside `skills/`, `commands/`, `hooks/`, etc. — git-tracked as source of truth. Previously, `migrateRuntimeToCache` moved `~/.agents/plugins/` into `~/.agents/.cache/plugins/` on every CLI version bump, silently destroying user-authored plugins in the working tree. Fixed by (1) removing the destructive move, (2) restoring discovery to the user-root, (3) a one-shot reverse migration that moves any cached plugins back to the user-root without overwriting an existing user-root copy, and (4) decoupling the migration sentinel from the binary version so migrations only re-run on real schema bumps. ([#20](https://github.com/phnx-labs/agents-cli/issues/20))
8
+ - `agents view <agent>@<version>` gains a `Plugins` section listing each plugin that supports the agent, with a `(N skills, N commands, …)` content summary and an OSC 8 hyperlink to the plugin source.
9
+
10
+ **Hooks**
11
+
12
+ - `getAvailableResources` and the version-home sync now treat only executable files in `hooks/` as hooks. Docs (`README.md`) and data files (`promptcuts.yaml`) that live alongside hooks no longer get synced into version homes as hooks, and the orphan-pruner trusts the manifest's declared hook list rather than re-scanning every source dir.
13
+
3
14
  ## 1.17.6
4
15
 
5
16
  **Workflows**
@@ -22,6 +22,7 @@ import { readBundle, resolveBundleEnv, describeBundle } from '../lib/secrets/bun
22
22
  import { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, RUN_STRATEGIES, } from '../lib/rotate.js';
23
23
  import { getGlobalDefault, getVersionHomePath, resolveVersionAlias } from '../lib/versions.js';
24
24
  import { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion } from '../lib/plugins.js';
25
+ import { parseWorkflowFrontmatter } from '../lib/workflows.js';
25
26
  import * as fs from 'fs';
26
27
  import * as path from 'path';
27
28
  const VALID_AGENTS = Object.keys(AGENT_COMMANDS);
@@ -46,6 +47,7 @@ export function registerRunCommand(program) {
46
47
  .option('--model <model>', 'Override the model directly (e.g., claude-opus-4-6)')
47
48
  .option('--env <key=value>', 'Pass environment variable to the agent (repeatable, e.g., --env DEBUG=1 --env API_KEY=xyz)', (val, prev) => [...prev, val], [])
48
49
  .option('--secrets <bundle>', 'Inject a secrets bundle (repeatable). Values resolve from macOS Keychain at run time. See `agents secrets`.', (val, prev) => [...prev, val], [])
50
+ .option('--no-auto-secrets', 'Skip auto-injection of secrets declared by a workflow\'s frontmatter `secrets:` field. Has no effect on bare-agent runs.')
49
51
  .option('--cwd <dir>', 'Working directory for the agent (defaults to current directory)')
50
52
  .option('--add-dir <dir>', 'Grant access to an additional directory outside the project (Claude only, repeatable)', (val, prev) => [...prev, val], [])
51
53
  .option('--json', 'Stream events as JSON lines (for parsing by other tools)')
@@ -186,6 +188,26 @@ Examples:
186
188
  syncPluginToVersion(buildDiscoveredPlugin(pluginRoot, manifest), 'claude', versionHome);
187
189
  }
188
190
  }
191
+ // Auto-inject secrets bundles declared in the workflow's frontmatter `secrets:` field.
192
+ // Union with any --secrets flags the user passed; dedupe. Skip when --no-auto-secrets is set.
193
+ if (!options.noAutoSecrets) {
194
+ const fm = parseWorkflowFrontmatter(workflowDir);
195
+ const declared = fm?.secrets ?? [];
196
+ if (declared.length > 0) {
197
+ const existing = new Set(options.secrets);
198
+ const added = [];
199
+ for (const b of declared) {
200
+ if (!existing.has(b)) {
201
+ options.secrets.push(b);
202
+ existing.add(b);
203
+ added.push(b);
204
+ }
205
+ }
206
+ if (added.length > 0) {
207
+ process.stderr.write(chalk.gray(`[workflow] auto-injecting secrets from ${rawAgent}: ${added.join(', ')}\n`));
208
+ }
209
+ }
210
+ }
189
211
  const subagentCount = fs.existsSync(subagentsDir)
190
212
  ? fs.readdirSync(subagentsDir).filter(f => f.endsWith('.md')).length
191
213
  : 0;
@@ -9,6 +9,8 @@ import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getV
9
9
  import { getShimsDir, isShimsInPath, ensureVersionedAliasCurrent, removeShim, } from '../lib/shims.js';
10
10
  import { getAgentResources } from '../lib/resources.js';
11
11
  import { WORKFLOW_CAPABLE_AGENTS } from '../lib/workflows.js';
12
+ import { discoverPlugins, pluginSupportsAgent } from '../lib/plugins.js';
13
+ import { PLUGINS_CAPABLE_AGENTS } from '../lib/agents.js';
12
14
  import { getAgentsDir, getUserAgentsDir, getEffectivePromptcutsPath, readMergedPromptcuts } from '../lib/state.js';
13
15
  import { isGitRepo, getGitSyncStatus } from '../lib/git.js';
14
16
  import { getCentralRulesFileName } from '../lib/rules/rules.js';
@@ -20,6 +22,27 @@ function termLink(text, filePath) {
20
22
  const url = `file://${filePath}`;
21
23
  return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`;
22
24
  }
25
+ /**
26
+ * Resolve a resource path to something the IDE can open inline. When `p` is a
27
+ * directory, OSC 8 file:// links cause IDEs (Cursor/VS Code) to open it as a
28
+ * new workspace window; pointing at the bundle's marker file (SKILL.md /
29
+ * WORKFLOW.md / AGENT.md) opens in the current window instead.
30
+ */
31
+ function linkTarget(p) {
32
+ try {
33
+ if (!fs.statSync(p).isDirectory())
34
+ return p;
35
+ }
36
+ catch {
37
+ return p;
38
+ }
39
+ for (const marker of ['SKILL.md', 'WORKFLOW.md', 'AGENT.md']) {
40
+ const candidate = path.join(p, marker);
41
+ if (fs.existsSync(candidate))
42
+ return candidate;
43
+ }
44
+ return p;
45
+ }
23
46
  function formatLastActive(date) {
24
47
  if (!date)
25
48
  return '';
@@ -508,7 +531,7 @@ async function showAgentResources(agentId, requestedVersion) {
508
531
  nameColor = chalk.yellow;
509
532
  else if (r.syncState === 'deleted')
510
533
  nameColor = chalk.red;
511
- const linkedName = r.path ? termLink(r.name, r.path) : r.name;
534
+ const linkedName = r.path ? termLink(r.name, linkTarget(r.path)) : r.name;
512
535
  let display = nameColor(linkedName);
513
536
  if (r.ruleCount !== undefined)
514
537
  display += chalk.gray(` (${r.ruleCount} rules)`);
@@ -571,6 +594,34 @@ async function showAgentResources(agentId, requestedVersion) {
571
594
  if (WORKFLOW_CAPABLE_AGENTS.includes(agentId)) {
572
595
  renderSection('Workflows', agentData.workflows);
573
596
  }
597
+ if (PLUGINS_CAPABLE_AGENTS.includes(agentId)) {
598
+ const plugins = discoverPlugins().filter(p => pluginSupportsAgent(p, agentId));
599
+ console.log(chalk.bold('\nPlugins\n'));
600
+ if (plugins.length === 0) {
601
+ console.log(` ${chalk.gray('none')}`);
602
+ }
603
+ else {
604
+ const versionStr = agentData.version ? ` (${agentData.version})` : '';
605
+ const agentHeader = home ? termLink(agentData.agentName, home) : agentData.agentName;
606
+ console.log(` ${chalk.bold(agentHeader)}${chalk.gray(versionStr)}:`);
607
+ for (const p of plugins) {
608
+ const linkedName = termLink(p.name, linkTarget(p.root));
609
+ const parts = [];
610
+ if (p.skills.length > 0)
611
+ parts.push(`${p.skills.length} skill${p.skills.length === 1 ? '' : 's'}`);
612
+ if (p.commands.length > 0)
613
+ parts.push(`${p.commands.length} command${p.commands.length === 1 ? '' : 's'}`);
614
+ if (p.hooks.length > 0)
615
+ parts.push(`${p.hooks.length} hook${p.hooks.length === 1 ? '' : 's'}`);
616
+ if (p.agentDefs.length > 0)
617
+ parts.push(`${p.agentDefs.length} subagent${p.agentDefs.length === 1 ? '' : 's'}`);
618
+ if (p.hasMcp)
619
+ parts.push('mcp');
620
+ const contents = parts.length > 0 ? chalk.gray(` (${parts.join(', ')})`) : '';
621
+ console.log(` ${chalk.cyan(linkedName)}${contents} ${chalk.cyan('[user]')}`);
622
+ }
623
+ }
624
+ }
574
625
  // Rules section with subrules breakdown
575
626
  function renderRulesSection() {
576
627
  console.log(chalk.bold('\nRules\n'));
@@ -600,7 +651,7 @@ async function showAgentResources(agentId, requestedVersion) {
600
651
  nameColor = chalk.yellow;
601
652
  else if (r.syncState === 'deleted')
602
653
  nameColor = chalk.red;
603
- const linkedName = r.path ? termLink(r.name, r.path) : r.name;
654
+ const linkedName = r.path ? termLink(r.name, linkTarget(r.path)) : r.name;
604
655
  let display = nameColor(linkedName);
605
656
  if (r.ruleCount !== undefined)
606
657
  display += chalk.gray(` (${r.ruleCount} rules)`);
@@ -394,14 +394,16 @@ Examples:
394
394
  lines.push(` ${fm.description}`);
395
395
  lines.push('');
396
396
  if (fm.model)
397
- lines.push(` Model: ${fm.model}`);
397
+ lines.push(` Model: ${fm.model}`);
398
398
  if (fm.tools?.length)
399
- lines.push(` Tools: ${fm.tools.join(', ')}`);
399
+ lines.push(` Tools: ${fm.tools.join(', ')}`);
400
400
  if (fm.mcpServers?.length)
401
- lines.push(` MCP: ${fm.mcpServers.join(', ')}`);
401
+ lines.push(` MCP: ${fm.mcpServers.join(', ')}`);
402
402
  if (fm.skills?.length)
403
- lines.push(` Skills: ${fm.skills.join(', ')}`);
404
- lines.push(` Path: ${workflow.path}`);
403
+ lines.push(` Skills: ${fm.skills.join(', ')}`);
404
+ if (fm.secrets?.length)
405
+ lines.push(` Secrets: ${fm.secrets.join(', ')} ${chalk.gray('(auto-injected at run time — pass --no-auto-secrets to skip)')}`);
406
+ lines.push(` Path: ${workflow.path}`);
405
407
  if (fm.allowedAgents?.length) {
406
408
  lines.push(chalk.bold(`\n Subagents (${workflow.subagentCount}):`));
407
409
  for (const a of fm.allowedAgents) {
package/dist/index.js CHANGED
@@ -666,7 +666,11 @@ if (process.env.AGENTS_SKIP_MIGRATION !== '1') {
666
666
  try {
667
667
  const { runMigration } = await import('./lib/migrate.js');
668
668
  const sentinel = getMigratedSentinelPath();
669
- const sentinelValue = `${VERSION}-v8`;
669
+ // Sentinel is keyed to the migration SCHEMA version, not the binary version.
670
+ // Bumping the suffix re-runs migrations for every user; binary releases that
671
+ // don't change the schema must NOT re-run (they would destroy user content
672
+ // when migration steps overlap with user-authored paths). See issue #20.
673
+ const sentinelValue = 'v9';
670
674
  let needRun = true;
671
675
  try {
672
676
  if (fs.existsSync(sentinel) && fs.readFileSync(sentinel, 'utf-8').trim() === sentinelValue) {
@@ -740,6 +740,62 @@ function migrateRuntimeToHistory() {
740
740
  catch { /* best-effort */ }
741
741
  }
742
742
  }
743
+ /**
744
+ * Restore plugins from the cache bucket back to the user-root.
745
+ *
746
+ * Earlier releases moved ~/.agents/plugins/ → ~/.agents/.cache/plugins/ as part
747
+ * of `migrateRuntimeToCache`. That was wrong: plugins are user-authored
748
+ * resources (alongside skills/, commands/, hooks/) and belong at the user-root
749
+ * so they're git-tracked. See issue #20.
750
+ *
751
+ * For each ~/.agents/.cache/plugins/<name>/ that the user already has at
752
+ * ~/.agents/plugins/<name>/, the cache copy is left alone — the user-root copy
753
+ * wins. For plugins only present in the cache, the directory is moved back to
754
+ * the user-root. Idempotent.
755
+ */
756
+ function migratePluginsBackToUserRoot() {
757
+ const cachePlugins = path.join(CACHE_DIR, 'plugins');
758
+ const userPlugins = path.join(USER_DIR, 'plugins');
759
+ if (!fs.existsSync(cachePlugins))
760
+ return;
761
+ let entries;
762
+ try {
763
+ entries = fs.readdirSync(cachePlugins, { withFileTypes: true });
764
+ }
765
+ catch {
766
+ return;
767
+ }
768
+ try {
769
+ fs.mkdirSync(userPlugins, { recursive: true, mode: 0o700 });
770
+ }
771
+ catch {
772
+ return;
773
+ }
774
+ for (const entry of entries) {
775
+ if (!entry.isDirectory())
776
+ continue;
777
+ const src = path.join(cachePlugins, entry.name);
778
+ const dest = path.join(userPlugins, entry.name);
779
+ if (fs.existsSync(dest))
780
+ continue;
781
+ try {
782
+ fs.renameSync(src, dest);
783
+ }
784
+ catch {
785
+ try {
786
+ copyDirSkipExisting(src, dest);
787
+ fs.rmSync(src, { recursive: true, force: true });
788
+ }
789
+ catch { /* best-effort */ }
790
+ }
791
+ }
792
+ // Drop the cache plugins dir if we emptied it.
793
+ try {
794
+ if (fs.readdirSync(cachePlugins).length === 0)
795
+ fs.rmdirSync(cachePlugins);
796
+ }
797
+ catch { /* best-effort */ }
798
+ }
743
799
  /**
744
800
  * Move regenerable runtime data into ~/.agents/.cache/.
745
801
  *
@@ -747,7 +803,6 @@ function migrateRuntimeToHistory() {
747
803
  * ~/.agents/shims/ -> ~/.agents/.cache/shims/
748
804
  * ~/.agents/bin/ -> ~/.agents/.cache/bin/
749
805
  * ~/.agents/packages/ -> ~/.agents/.cache/packages/
750
- * ~/.agents/plugins/ -> ~/.agents/.cache/plugins/
751
806
  * ~/.agents/cloud/ -> ~/.agents/.cache/cloud/
752
807
  * ~/.agents/drive/ -> ~/.agents/.cache/drive/
753
808
  * ~/.agents/terminals/ -> ~/.agents/.cache/terminals/
@@ -772,7 +827,10 @@ function migrateRuntimeToCache() {
772
827
  moveDirOnce(path.join(USER_DIR, 'shims'), path.join(CACHE_DIR, 'shims'));
773
828
  moveDirOnce(path.join(USER_DIR, 'bin'), path.join(CACHE_DIR, 'bin'));
774
829
  moveDirOnce(path.join(USER_DIR, 'packages'), path.join(CACHE_DIR, 'packages'));
775
- moveDirOnce(path.join(USER_DIR, 'plugins'), path.join(CACHE_DIR, 'plugins'));
830
+ // ~/.agents/plugins/ is intentionally NOT migrated — it is user-authored
831
+ // content (git-tracked), alongside skills/, commands/, hooks/. The reverse
832
+ // migration `migratePluginsBackToUserRoot` reclaims any plugins/ that prior
833
+ // releases moved into ~/.agents/.cache/plugins/. See issue #20.
776
834
  moveDirOnce(path.join(USER_DIR, 'cloud'), path.join(CACHE_DIR, 'cloud'));
777
835
  moveDirOnce(path.join(USER_DIR, 'drive'), path.join(CACHE_DIR, 'drive'));
778
836
  // terminals/ stays at the top level: the agents-cli IDE extension publishes
@@ -1405,6 +1463,10 @@ export async function runMigration() {
1405
1463
  // Bucket moves: collapse runtime state into ~/.agents/.history and ~/.agents/.cache.
1406
1464
  migrateRuntimeToHistory();
1407
1465
  migrateRuntimeToCache();
1466
+ // Restore plugins (user-authored) from cache back to user-root. Runs AFTER
1467
+ // migrateRuntimeToCache so any legacy plugins/ still at the user-root from
1468
+ // very-old layouts have already been handled.
1469
+ migratePluginsBackToUserRoot();
1408
1470
  // System-repo sweep: move every remaining operational dir into its canonical
1409
1471
  // user-bucket location, then drop known-dead artifacts and warn about
1410
1472
  // anything we don't recognize. Order: durable (sessions/teams/trash/repos/
@@ -1,14 +1,16 @@
1
1
  /**
2
2
  * Plugin discovery, validation, and syncing.
3
3
  *
4
- * Plugins are bundles in ~/.agents/.cache/plugins/ that package skills, hooks,
4
+ * Plugins are bundles in ~/.agents/plugins/ that package skills, hooks,
5
5
  * commands, agents, bin scripts, MCP servers, and settings under a single
6
- * manifest (plugin.json). This module discovers plugins, validates their
7
- * manifests, and syncs their contents into agent version homes.
6
+ * manifest (plugin.json). They are user-authored resources, sitting alongside
7
+ * skills/, commands/, hooks/, etc. git-tracked as source of truth. This
8
+ * module discovers plugins, validates their manifests, and syncs their
9
+ * contents into agent version homes.
8
10
  */
9
11
  import type { AgentId, DiscoveredPlugin, PluginManifest } from './types.js';
10
12
  /**
11
- * Discover all plugins in ~/.agents/.cache/plugins/.
13
+ * Discover all plugins in ~/.agents/plugins/.
12
14
  * A valid plugin has a .claude-plugin/plugin.json manifest.
13
15
  */
14
16
  export declare function discoverPlugins(): DiscoveredPlugin[];
@@ -131,7 +133,7 @@ export declare function parseInstallSpec(spec: string): {
131
133
  };
132
134
  /**
133
135
  * Install a plugin from a git URL or local path.
134
- * Clones/copies to ~/.agents/.cache/plugins/<name>/.
136
+ * Clones/copies to ~/.agents/plugins/<name>/.
135
137
  * Returns the installed plugin name and root path.
136
138
  */
137
139
  export declare function installPlugin(spec: string): Promise<{
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * Plugin discovery, validation, and syncing.
3
3
  *
4
- * Plugins are bundles in ~/.agents/.cache/plugins/ that package skills, hooks,
4
+ * Plugins are bundles in ~/.agents/plugins/ that package skills, hooks,
5
5
  * commands, agents, bin scripts, MCP servers, and settings under a single
6
- * manifest (plugin.json). This module discovers plugins, validates their
7
- * manifests, and syncs their contents into agent version homes.
6
+ * manifest (plugin.json). They are user-authored resources, sitting alongside
7
+ * skills/, commands/, hooks/, etc. git-tracked as source of truth. This
8
+ * module discovers plugins, validates their manifests, and syncs their
9
+ * contents into agent version homes.
8
10
  */
9
11
  import * as fs from 'fs';
10
12
  import * as path from 'path';
@@ -17,7 +19,7 @@ const PLUGIN_MANIFEST_FILE = 'plugin.json';
17
19
  const USER_CONFIG_FILE = '.user-config.json';
18
20
  const SOURCE_FILE = '.source';
19
21
  /**
20
- * Discover all plugins in ~/.agents/.cache/plugins/.
22
+ * Discover all plugins in ~/.agents/plugins/.
21
23
  * A valid plugin has a .claude-plugin/plugin.json manifest.
22
24
  */
23
25
  export function discoverPlugins() {
@@ -1034,7 +1036,7 @@ export function parseInstallSpec(spec) {
1034
1036
  }
1035
1037
  /**
1036
1038
  * Install a plugin from a git URL or local path.
1037
- * Clones/copies to ~/.agents/.cache/plugins/<name>/.
1039
+ * Clones/copies to ~/.agents/plugins/<name>/.
1038
1040
  * Returns the installed plugin name and root path.
1039
1041
  */
1040
1042
  export async function installPlugin(spec) {
@@ -111,7 +111,7 @@ export declare function getShimsDir(): string;
111
111
  export declare function getBinDir(): string;
112
112
  /** Path to config backups (~/.agents/.history/backups/). */
113
113
  export declare function getBackupsDir(): string;
114
- /** Path to plugin bundles (~/.agents/.cache/plugins/). */
114
+ /** Path to plugin bundles (~/.agents/plugins/) — user-authored resource. */
115
115
  export declare function getPluginsDir(): string;
116
116
  /** Path to synced remote session data (~/.agents/.cache/drive/). */
117
117
  export declare function getDriveDir(): string;
package/dist/lib/state.js CHANGED
@@ -65,7 +65,9 @@ const TRASH_DIR = path.join(HISTORY_DIR, 'trash');
65
65
  const SHIMS_DIR = path.join(CACHE_DIR, 'shims');
66
66
  const BIN_DIR = path.join(CACHE_DIR, 'bin');
67
67
  const PACKAGES_DIR = path.join(CACHE_DIR, 'packages');
68
- const PLUGINS_DIR = path.join(CACHE_DIR, 'plugins');
68
+ // Plugins are user-authored resources, alongside skills/, commands/, hooks/.
69
+ // They live at the user-root so they're git-tracked as source of truth.
70
+ const PLUGINS_DIR = path.join(USER_AGENTS_DIR, 'plugins');
69
71
  const CLOUD_DIR = path.join(CACHE_DIR, 'cloud');
70
72
  const DRIVE_DIR = path.join(CACHE_DIR, 'drive');
71
73
  const TERMINALS_DIR = path.join(CACHE_DIR, 'terminals');
@@ -266,7 +268,7 @@ export function getShimsDir() { return SHIMS_DIR; }
266
268
  export function getBinDir() { return BIN_DIR; }
267
269
  /** Path to config backups (~/.agents/.history/backups/). */
268
270
  export function getBackupsDir() { return BACKUPS_DIR; }
269
- /** Path to plugin bundles (~/.agents/.cache/plugins/). */
271
+ /** Path to plugin bundles (~/.agents/plugins/) — user-authored resource. */
270
272
  export function getPluginsDir() { return PLUGINS_DIR; }
271
273
  /** Path to synced remote session data (~/.agents/.cache/drive/). */
272
274
  export function getDriveDir() { return DRIVE_DIR; }
@@ -109,15 +109,24 @@ export function getAvailableResources(cwd = process.cwd()) {
109
109
  }
110
110
  }
111
111
  result.skills = Array.from(skillNames);
112
- // Hooks (files)
112
+ // Hooks (files). Only executable files in hooks/ count as hooks. Auxiliary
113
+ // files like README.md (docs) or promptcuts.yaml (data read directly by a
114
+ // hook script) live alongside hooks but are not hooks themselves and must
115
+ // not be synced as such.
113
116
  const hookNames = new Set();
114
117
  for (const { base } of resourceBases) {
115
118
  const hooksDir = path.join(base, 'hooks');
116
119
  if (!fs.existsSync(hooksDir))
117
120
  continue;
118
- const names = fs.readdirSync(hooksDir).filter(f => !f.startsWith('.'));
119
- for (const name of names) {
120
- hookNames.add(name);
121
+ for (const name of fs.readdirSync(hooksDir)) {
122
+ if (name.startsWith('.'))
123
+ continue;
124
+ try {
125
+ const stat = fs.statSync(path.join(hooksDir, name));
126
+ if (stat.isFile() && (stat.mode & 0o111) !== 0)
127
+ hookNames.add(name);
128
+ }
129
+ catch { /* ignore unreadable */ }
121
130
  }
122
131
  }
123
132
  result.hooks = Array.from(hookNames);
@@ -1674,29 +1683,15 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
1674
1683
  fs.chmodSync(destFile, 0o755);
1675
1684
  syncedHooks.push(hook);
1676
1685
  }
1677
- // Remove orphan hook files that exist in version home but not in any trusted source.
1678
- const centralHookNames = new Set(fs.existsSync(getHooksDir())
1679
- ? fs.readdirSync(getHooksDir()).filter(f => !f.startsWith('.'))
1680
- : []);
1681
- {
1682
- const hooksDir = path.join(userAgentsDir, 'hooks');
1683
- if (fs.existsSync(hooksDir)) {
1684
- for (const file of fs.readdirSync(hooksDir).filter(f => !f.startsWith('.'))) {
1685
- centralHookNames.add(file);
1686
- }
1687
- }
1688
- }
1689
- for (const extra of extraRepos) {
1690
- const hooksDir = path.join(extra.dir, 'hooks');
1691
- if (fs.existsSync(hooksDir)) {
1692
- for (const file of fs.readdirSync(hooksDir).filter(f => !f.startsWith('.'))) {
1693
- centralHookNames.add(file);
1694
- }
1695
- }
1696
- }
1686
+ // Remove orphan files from version home. The trusted set is the
1687
+ // manifest-declared hook list (`available.hooks`) — auxiliary files
1688
+ // like README.md or promptcuts.yaml may exist alongside hooks at the
1689
+ // source but are not hooks and must not linger in version homes from
1690
+ // older syncs.
1691
+ const trustedHookNames = new Set(available.hooks);
1697
1692
  if (fs.existsSync(hooksTarget)) {
1698
1693
  for (const file of fs.readdirSync(hooksTarget).filter(f => !f.startsWith('.'))) {
1699
- if (!centralHookNames.has(file)) {
1694
+ if (!trustedHookNames.has(file)) {
1700
1695
  removePath(safeJoin(hooksTarget, file));
1701
1696
  }
1702
1697
  }
@@ -17,6 +17,13 @@ export interface WorkflowFrontmatter {
17
17
  skills?: string[];
18
18
  mcpServers?: string[];
19
19
  allowedAgents?: string[];
20
+ /**
21
+ * Secrets bundle names this workflow needs (e.g. `linear.app`, `github.com`).
22
+ * When `agents run <workflow>` resolves a workflow, these are unioned into the
23
+ * effective `--secrets` list and resolved from the macOS Keychain before spawn.
24
+ * Pass `--no-auto-secrets` to skip this injection.
25
+ */
26
+ secrets?: string[];
20
27
  }
21
28
  /** A workflow found during repo discovery. */
22
29
  export interface DiscoveredWorkflow {
@@ -37,6 +37,7 @@ export function parseWorkflowFrontmatter(workflowDir) {
37
37
  skills: parsed.skills,
38
38
  mcpServers: parsed.mcpServers,
39
39
  allowedAgents: parsed.allowedAgents,
40
+ secrets: parsed.secrets,
40
41
  };
41
42
  }
42
43
  catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.17.6",
3
+ "version": "1.18.0",
4
4
  "description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",