@phnx-labs/agents-cli 1.17.5 → 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,24 @@
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
+
14
+ ## 1.17.6
15
+
16
+ **Workflows**
17
+
18
+ - New `workflows` skill — author-and-run guide for workflow bundles (`WORKFLOW.md` frontmatter, `subagents/` directory for multi-agent pipelines, scoped `skills/` and `plugins/`, sharing via `agents repo push` or GitHub install). Calls out the `--mode plan` deadlock that bites workflows which need to post comments or edit files.
19
+ - `agents workflows --help` rewritten with a structure diagram, project > user > system resolution order, and an explicit note that workflows mutating state need `--mode edit` or `--mode full` to avoid a headless deadlock at `ExitPlanMode`.
20
+ - README gains a `Workflows` section between Teams and Browser covering the bundle layout, frontmatter, subagents/skills/plugins, and the `--mode` requirement.
21
+
3
22
  ## 1.17.4
4
23
 
5
24
  **Browser**
package/README.md CHANGED
@@ -47,6 +47,7 @@ Also available as `ag` -- all commands work with both `agents` and `ag`.
47
47
  - [Sessions across agents](#sessions-across-agents)
48
48
  - [Run open models through Claude Code](#run-open-models-through-claude-code)
49
49
  - [Teams](#teams)
50
+ - [Workflows](#workflows)
50
51
  - [Browser](#browser)
51
52
  - [Secrets](#secrets)
52
53
  - [Routines](#routines)
@@ -244,6 +245,57 @@ Team state is observable via `agents teams list --json` / `agents teams status -
244
245
 
245
246
  ---
246
247
 
248
+ ## Workflows
249
+
250
+ Bundle an orchestrator prompt with optional subagents, skills, and plugins into a named, reusable pipeline. One bundle, one invocation.
251
+
252
+ ```bash
253
+ # Use a workflow — workflow name goes in the agent slot
254
+ agents run code-review "review PR #42 on acme/api"
255
+
256
+ # List + inspect
257
+ agents workflows list
258
+ agents workflows view code-review
259
+
260
+ # Install from GitHub or local
261
+ agents workflows add gh:yourteam/code-review
262
+ agents workflows add ./my-workflow
263
+ ```
264
+
265
+ A workflow is a directory:
266
+
267
+ ```
268
+ ~/.agents/workflows/code-review/
269
+ WORKFLOW.md # YAML frontmatter + orchestrator system prompt
270
+ subagents/ # optional: *.md files exposed to the orchestrator
271
+ security.md
272
+ style.md
273
+ skills/ # optional: knowledge packs scoped to this workflow
274
+ plugins/ # optional: plugin bundles
275
+ ```
276
+
277
+ `WORKFLOW.md`'s Markdown body is the orchestrator's system prompt. Files under `subagents/` get copied to `~/.claude/agents/` at run time so the built-in Agent tool can dispatch to them by name — including in parallel. `skills/` and `plugins/` sync into the version home just for the run.
278
+
279
+ ```yaml
280
+ # WORKFLOW.md frontmatter
281
+ ---
282
+ name: Code Review
283
+ description: Evidence-grounded PR review with file:line citations.
284
+ model: claude-opus-4-7
285
+ tools:
286
+ - Read
287
+ - Grep
288
+ - Bash
289
+ - WebFetch
290
+ ---
291
+ ```
292
+
293
+ Workflows that need to write — post PR comments, edit files, send Slack — should run with `--mode edit` or `--mode full`. `agents run` defaults to `--mode plan` (read-only), which deadlocks at `ExitPlanMode` in headless runs.
294
+
295
+ Resolution is project > user > system: a `<repo>/.agents/workflows/<name>/` overrides a same-named workflow in `~/.agents/workflows/`. Commit project workflows with your repo so teammates get the same pipeline.
296
+
297
+ ---
298
+
247
299
  ## Browser
248
300
 
249
301
  Give agents access to a real browser — no relay extension, no cloud service, no Playwright getting blocked.
@@ -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)`);
@@ -17,21 +17,42 @@ export function registerWorkflowsCommands(program) {
17
17
  .command('workflows')
18
18
  .description('Manage multi-agent pipeline workflows (WORKFLOW.md bundles)')
19
19
  .addHelpText('after', `
20
- Workflows are directory bundles (WORKFLOW.md + subagents/) that define multi-agent pipelines run via:
21
- agents run <workflow-name>
20
+ Workflows are directory bundles that define reusable named agent pipelines.
21
+ Run a workflow with:
22
+ agents run <workflow-name> [prompt]
23
+
24
+ Structure:
25
+ ~/.agents/workflows/<name>/
26
+ WORKFLOW.md required: YAML frontmatter + orchestrator system prompt
27
+ subagents/*.md optional: subagents the orchestrator can dispatch to
28
+ skills/ optional: knowledge packs scoped to this workflow
29
+ plugins/ optional: plugin bundles scoped to this workflow
30
+
31
+ Resolution: project (.agents/workflows/) > user (~/.agents/workflows/) > system.
32
+
33
+ Note: agents run defaults to --mode plan (read-only). For workflows that
34
+ write files, post comments, or otherwise mutate state, pass --mode edit or
35
+ --mode full or the run will deadlock at ExitPlanMode.
22
36
 
23
37
  Examples:
24
38
  # See what workflows are available
25
39
  agents workflows list
26
40
 
27
- # Install from GitHub
41
+ # Install from GitHub or a local directory
28
42
  agents workflows add gh:user/workflows
43
+ agents workflows add ./code-review
29
44
 
30
- # Install a local workflow directory
31
- agents workflows add ./rdev
45
+ # Inspect a workflow's frontmatter and subagents
46
+ agents workflows view code-review
32
47
 
33
- # Remove a workflow
34
- agents workflows remove rdev
48
+ # Run it (workflow name goes in the agent slot)
49
+ agents run code-review "review PR #42"
50
+
51
+ # Run a workflow that posts comments / edits files
52
+ agents run code-review --mode full "review PR #42 and post the review"
53
+
54
+ # Remove from version homes (and central storage on second run)
55
+ agents workflows remove code-review
35
56
  `);
36
57
  workflowsCmd
37
58
  .command('list [agent]')
@@ -373,14 +394,16 @@ Examples:
373
394
  lines.push(` ${fm.description}`);
374
395
  lines.push('');
375
396
  if (fm.model)
376
- lines.push(` Model: ${fm.model}`);
397
+ lines.push(` Model: ${fm.model}`);
377
398
  if (fm.tools?.length)
378
- lines.push(` Tools: ${fm.tools.join(', ')}`);
399
+ lines.push(` Tools: ${fm.tools.join(', ')}`);
379
400
  if (fm.mcpServers?.length)
380
- lines.push(` MCP: ${fm.mcpServers.join(', ')}`);
401
+ lines.push(` MCP: ${fm.mcpServers.join(', ')}`);
381
402
  if (fm.skills?.length)
382
- lines.push(` Skills: ${fm.skills.join(', ')}`);
383
- 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}`);
384
407
  if (fm.allowedAgents?.length) {
385
408
  lines.push(chalk.bold(`\n Subagents (${workflow.subagentCount}):`));
386
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.5",
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",