@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 +19 -0
- package/README.md +52 -0
- package/dist/commands/exec.js +22 -0
- package/dist/commands/view.js +53 -2
- package/dist/commands/workflows.js +35 -12
- package/dist/index.js +5 -1
- package/dist/lib/migrate.js +64 -2
- package/dist/lib/plugins.d.ts +7 -5
- package/dist/lib/plugins.js +7 -5
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +4 -2
- package/dist/lib/versions.js +20 -25
- package/dist/lib/workflows.d.ts +7 -0
- package/dist/lib/workflows.js +1 -0
- package/package.json +1 -1
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.
|
package/dist/commands/exec.js
CHANGED
|
@@ -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;
|
package/dist/commands/view.js
CHANGED
|
@@ -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
|
|
21
|
-
|
|
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
|
-
#
|
|
31
|
-
agents workflows
|
|
45
|
+
# Inspect a workflow's frontmatter and subagents
|
|
46
|
+
agents workflows view code-review
|
|
32
47
|
|
|
33
|
-
#
|
|
34
|
-
agents
|
|
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:
|
|
397
|
+
lines.push(` Model: ${fm.model}`);
|
|
377
398
|
if (fm.tools?.length)
|
|
378
|
-
lines.push(` Tools:
|
|
399
|
+
lines.push(` Tools: ${fm.tools.join(', ')}`);
|
|
379
400
|
if (fm.mcpServers?.length)
|
|
380
|
-
lines.push(` MCP:
|
|
401
|
+
lines.push(` MCP: ${fm.mcpServers.join(', ')}`);
|
|
381
402
|
if (fm.skills?.length)
|
|
382
|
-
lines.push(` Skills:
|
|
383
|
-
|
|
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
|
-
|
|
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) {
|
package/dist/lib/migrate.js
CHANGED
|
@@ -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
|
-
|
|
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/
|
package/dist/lib/plugins.d.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Plugin discovery, validation, and syncing.
|
|
3
3
|
*
|
|
4
|
-
* Plugins are bundles in ~/.agents
|
|
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).
|
|
7
|
-
*
|
|
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
|
|
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
|
|
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<{
|
package/dist/lib/plugins.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Plugin discovery, validation, and syncing.
|
|
3
3
|
*
|
|
4
|
-
* Plugins are bundles in ~/.agents
|
|
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).
|
|
7
|
-
*
|
|
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
|
|
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
|
|
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) {
|
package/dist/lib/state.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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; }
|
package/dist/lib/versions.js
CHANGED
|
@@ -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
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
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 (!
|
|
1694
|
+
if (!trustedHookNames.has(file)) {
|
|
1700
1695
|
removePath(safeJoin(hooksTarget, file));
|
|
1701
1696
|
}
|
|
1702
1697
|
}
|
package/dist/lib/workflows.d.ts
CHANGED
|
@@ -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 {
|
package/dist/lib/workflows.js
CHANGED
package/package.json
CHANGED