@phnx-labs/agents-cli 1.20.5 → 1.20.7

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.
Files changed (70) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +1 -1
  3. package/dist/commands/browser.js +31 -4
  4. package/dist/commands/computer-actions.d.ts +36 -0
  5. package/dist/commands/computer-actions.js +328 -0
  6. package/dist/commands/computer.js +74 -55
  7. package/dist/commands/defaults.d.ts +7 -0
  8. package/dist/commands/defaults.js +89 -0
  9. package/dist/commands/exec.js +24 -6
  10. package/dist/commands/inspect.d.ts +38 -7
  11. package/dist/commands/inspect.js +194 -24
  12. package/dist/commands/rules.js +3 -3
  13. package/dist/commands/secrets.js +46 -9
  14. package/dist/commands/sessions.js +9 -12
  15. package/dist/commands/setup.js +2 -2
  16. package/dist/commands/teams.js +108 -11
  17. package/dist/commands/view.d.ts +12 -1
  18. package/dist/commands/view.js +121 -38
  19. package/dist/index.js +61 -22
  20. package/dist/lib/agents.d.ts +10 -6
  21. package/dist/lib/agents.js +23 -14
  22. package/dist/lib/browser/chrome.d.ts +10 -0
  23. package/dist/lib/browser/chrome.js +84 -3
  24. package/dist/lib/daemon.js +4 -7
  25. package/dist/lib/exec.d.ts +9 -0
  26. package/dist/lib/exec.js +85 -9
  27. package/dist/lib/migrate.js +6 -4
  28. package/dist/lib/permissions.d.ts +23 -0
  29. package/dist/lib/permissions.js +89 -7
  30. package/dist/lib/platform/exec.d.ts +9 -0
  31. package/dist/lib/platform/exec.js +24 -0
  32. package/dist/lib/platform/index.d.ts +20 -0
  33. package/dist/lib/platform/index.js +20 -0
  34. package/dist/lib/platform/paths.d.ts +22 -0
  35. package/dist/lib/platform/paths.js +49 -0
  36. package/dist/lib/platform/process.d.ts +12 -0
  37. package/dist/lib/platform/process.js +22 -0
  38. package/dist/lib/plugin-marketplace.js +1 -1
  39. package/dist/lib/project-launch.d.ts +5 -0
  40. package/dist/lib/project-launch.js +37 -0
  41. package/dist/lib/pty-client.js +13 -5
  42. package/dist/lib/pty-server.d.ts +24 -1
  43. package/dist/lib/pty-server.js +109 -29
  44. package/dist/lib/resources/rules.js +1 -1
  45. package/dist/lib/resources/skills.js +1 -1
  46. package/dist/lib/resources.d.ts +2 -0
  47. package/dist/lib/resources.js +2 -1
  48. package/dist/lib/rotate.js +6 -18
  49. package/dist/lib/run-config.d.ts +9 -0
  50. package/dist/lib/run-config.js +35 -0
  51. package/dist/lib/run-defaults.d.ts +42 -0
  52. package/dist/lib/run-defaults.js +180 -0
  53. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  54. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  55. package/dist/lib/secrets/install-helper.d.ts +11 -3
  56. package/dist/lib/secrets/install-helper.js +48 -6
  57. package/dist/lib/secrets/linux.d.ts +12 -0
  58. package/dist/lib/secrets/linux.js +30 -16
  59. package/dist/lib/session/artifacts.js +8 -2
  60. package/dist/lib/shims.d.ts +9 -1
  61. package/dist/lib/shims.js +80 -3
  62. package/dist/lib/staleness/detectors/hooks.js +1 -1
  63. package/dist/lib/staleness/writers/hooks.js +1 -1
  64. package/dist/lib/teams/agents.js +5 -7
  65. package/dist/lib/teams/api.d.ts +67 -0
  66. package/dist/lib/teams/api.js +78 -0
  67. package/dist/lib/types.d.ts +15 -6
  68. package/dist/lib/versions.js +4 -4
  69. package/package.json +5 -2
  70. package/scripts/postinstall.js +18 -1
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Defaults command tree.
3
+ *
4
+ * `agents defaults run ...` manages selector-based defaults for `agents run`.
5
+ */
6
+ import type { Command } from 'commander';
7
+ export declare function registerDefaultsCommands(program: Command): void;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Defaults command tree.
3
+ *
4
+ * `agents defaults run ...` manages selector-based defaults for `agents run`.
5
+ */
6
+ import chalk from 'chalk';
7
+ import { setHelpSections } from '../lib/help.js';
8
+ import { listRunDefaults, setRunDefault, unsetRunDefault, } from '../lib/run-defaults.js';
9
+ function formatRunDefault(entry) {
10
+ const parts = [];
11
+ if (entry.defaults.mode)
12
+ parts.push(`mode ${chalk.white(entry.defaults.mode)}`);
13
+ if (entry.defaults.model)
14
+ parts.push(`model ${chalk.white(entry.defaults.model)}`);
15
+ return `${chalk.cyan(entry.selector.padEnd(22))} ${parts.join(' ')}`;
16
+ }
17
+ export function registerDefaultsCommands(program) {
18
+ const defaults = program
19
+ .command('defaults')
20
+ .description('Manage default options for agents-cli commands');
21
+ const run = defaults
22
+ .command('run')
23
+ .description('Manage selector-based defaults for `agents run`');
24
+ setHelpSections(run, {
25
+ examples: `
26
+ agents defaults run list
27
+ agents defaults run set 'claude:*' --mode auto --model opus
28
+ agents defaults run set claude@2.1.45 --mode plan --model sonnet
29
+ agents defaults run unset 'claude:*'
30
+ `,
31
+ notes: `
32
+ Selectors use <agent>:<version>. Use * for all versions of an agent.
33
+ Exact selectors override wildcard selectors field by field.
34
+ Explicit flags on agents run always win over configured defaults.
35
+ `,
36
+ });
37
+ run
38
+ .command('list')
39
+ .description('List configured run defaults')
40
+ .action(() => {
41
+ const entries = listRunDefaults();
42
+ if (entries.length === 0) {
43
+ console.log(chalk.gray('No run defaults configured.'));
44
+ console.log(chalk.gray("Set one with: agents defaults run set 'claude:*' --mode auto --model opus"));
45
+ return;
46
+ }
47
+ console.log(chalk.bold('Run Defaults\n'));
48
+ for (const entry of entries) {
49
+ console.log(` ${formatRunDefault(entry)}`);
50
+ }
51
+ });
52
+ run
53
+ .command('set <selector>')
54
+ .description('Set defaults for an agent/version selector')
55
+ .option('--mode <mode>', "Default mode: plan, edit, auto, skip. 'full' accepted as alias for skip.")
56
+ .option('--model <model>', 'Default model or model alias, forwarded via --model')
57
+ .action((selector, options) => {
58
+ try {
59
+ const entry = setRunDefault(selector, {
60
+ ...(options.mode !== undefined ? { mode: options.mode } : {}),
61
+ ...(options.model !== undefined ? { model: options.model } : {}),
62
+ });
63
+ console.log(chalk.green('Set run default:'));
64
+ console.log(` ${formatRunDefault(entry)}`);
65
+ }
66
+ catch (err) {
67
+ console.error(chalk.red(err.message));
68
+ process.exit(1);
69
+ }
70
+ });
71
+ run
72
+ .command('unset <selector>')
73
+ .description('Remove defaults for an agent/version selector')
74
+ .action((selector) => {
75
+ try {
76
+ const removed = unsetRunDefault(selector);
77
+ if (removed) {
78
+ console.log(chalk.green(`Removed run default ${selector}`));
79
+ }
80
+ else {
81
+ console.log(chalk.gray(`No run default matched ${selector}`));
82
+ }
83
+ }
84
+ catch (err) {
85
+ console.error(chalk.red(err.message));
86
+ process.exit(1);
87
+ }
88
+ });
89
+ }
@@ -85,7 +85,7 @@ export function registerRunCommand(program) {
85
85
  `,
86
86
  });
87
87
  runCmd.action(async (agentSpec, prompt, options) => {
88
- const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, defaultModeFor }, { ALL_AGENT_IDS }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, RUN_STRATEGIES }, { getGlobalDefault, getVersionHomePath, resolveVersionAlias }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef },] = await Promise.all([
88
+ const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, defaultModeFor }, { ALL_AGENT_IDS }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, RUN_STRATEGIES }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef }, { resolveRunDefaults },] = await Promise.all([
89
89
  import('../lib/exec.js'),
90
90
  import('../lib/agents.js'),
91
91
  import('../lib/profiles.js'),
@@ -94,6 +94,7 @@ export function registerRunCommand(program) {
94
94
  import('../lib/versions.js'),
95
95
  import('../lib/plugins.js'),
96
96
  import('../lib/workflows.js'),
97
+ import('../lib/run-defaults.js'),
97
98
  ]);
98
99
  const isValidAgent = (agent) => ALL_AGENT_IDS.includes(agent);
99
100
  // Parse agent@version
@@ -102,6 +103,7 @@ export function registerRunCommand(program) {
102
103
  let version = rawVersion || undefined;
103
104
  let profileEnv;
104
105
  let fromProfile = false;
106
+ let workflowModel;
105
107
  const cwd = options.cwd ?? process.cwd();
106
108
  if (isValidAgent(rawAgent)) {
107
109
  agent = rawAgent;
@@ -133,6 +135,10 @@ export function registerRunCommand(program) {
133
135
  // subagents/*.md ← flat .md files copied to ~/.claude/agents/ for Agent tool discovery
134
136
  const workflowDir = resolveWorkflowRef(rawAgent, cwd);
135
137
  agent = 'claude';
138
+ const workflowFrontmatter = parseWorkflowFrontmatter(workflowDir);
139
+ if (typeof workflowFrontmatter?.model === 'string' && workflowFrontmatter.model.trim() !== '') {
140
+ workflowModel = workflowFrontmatter.model.trim();
141
+ }
136
142
  const resolvedVersion = resolveVersionAlias('claude', version);
137
143
  const versionHome = getVersionHomePath('claude', resolvedVersion ?? getGlobalDefault('claude') ?? '');
138
144
  const claudeAgentsDir = path.join(versionHome, '.claude', 'agents');
@@ -179,8 +185,7 @@ export function registerRunCommand(program) {
179
185
  // Auto-inject secrets bundles declared in the workflow's frontmatter `secrets:` field.
180
186
  // Union with any --secrets flags the user passed; dedupe. Skip when --no-auto-secrets is set.
181
187
  if (!options.noAutoSecrets) {
182
- const fm = parseWorkflowFrontmatter(workflowDir);
183
- const declared = fm?.secrets ?? [];
188
+ const declared = workflowFrontmatter?.secrets ?? [];
184
189
  if (declared.length > 0) {
185
190
  const existing = new Set(options.secrets);
186
191
  const added = [];
@@ -262,9 +267,18 @@ export function registerRunCommand(program) {
262
267
  }
263
268
  }
264
269
  }
270
+ const defaultVersion = version ?? resolveVersion(agent, cwd);
271
+ const runDefaults = fromProfile
272
+ ? { sources: {} }
273
+ : resolveRunDefaults(agent, defaultVersion, cwd);
265
274
  // Accept the four canonical modes plus 'full' as a permanent silent
266
275
  // alias for 'skip' (rewritten downstream by normalizeMode in exec.ts).
267
276
  let mode = options.mode;
277
+ const modeSource = runCmd.getOptionValueSource('mode');
278
+ const modeFromRunDefault = modeSource === 'default' && !!runDefaults.mode;
279
+ if (modeFromRunDefault) {
280
+ mode = runDefaults.mode;
281
+ }
268
282
  if (!['plan', 'edit', 'auto', 'skip', 'full'].includes(mode)) {
269
283
  console.error(chalk.red(`Invalid mode: ${mode}. Use plan, edit, auto, or skip ('full' accepted as alias for skip).`));
270
284
  process.exit(1);
@@ -276,13 +290,12 @@ export function registerRunCommand(program) {
276
290
  // user did not ask for read-only, they asked for "just run it." An
277
291
  // explicit --mode plan still throws (see resolveMode), because silently
278
292
  // elevating an explicit read-only request to edit is unsafe.
279
- const modeSource = runCmd.getOptionValueSource('mode');
280
293
  const modeIsDefault = modeSource === 'default';
281
294
  try {
282
295
  resolveMode(agent, normalizeMode(mode));
283
296
  }
284
297
  catch (err) {
285
- if (modeIsDefault) {
298
+ if (modeIsDefault && !modeFromRunDefault) {
286
299
  mode = defaultModeFor(agent);
287
300
  if (!options.quiet) {
288
301
  process.stderr.write(chalk.gray(`[agents] ${agent} has no '${options.mode}' mode; using '${mode}'\n`));
@@ -334,6 +347,11 @@ export function registerRunCommand(program) {
334
347
  const env = hasOverrides
335
348
  ? { ...(profileEnv ?? {}), ...secretsEnv, ...(userEnv ?? {}) }
336
349
  : undefined;
350
+ const modelSource = runCmd.getOptionValueSource('model');
351
+ const model = options.model
352
+ ?? (!fromProfile && modelSource === undefined
353
+ ? (workflowModel ?? (options.fallback ? undefined : runDefaults.model))
354
+ : undefined);
337
355
  const execOptions = {
338
356
  agent,
339
357
  version,
@@ -342,7 +360,7 @@ export function registerRunCommand(program) {
342
360
  mode,
343
361
  effort,
344
362
  cwd: options.cwd,
345
- model: options.model,
363
+ model,
346
364
  addDirs: options.addDir,
347
365
  json: options.json,
348
366
  headless: options.headless ?? true,
@@ -1,14 +1,30 @@
1
1
  /**
2
- * `agents inspect <agent>[@version]`single-agent, single-version detail view.
2
+ * `agents inspect <target>`detail view for one agent+version or one DotAgents repo.
3
3
  *
4
- * Summary mode shows the per-version header (paths, shim, capabilities, resource
5
- * counts, sessions). Drill-down flags (`--skills`, `--hooks`, `--mcp`, ...) list
6
- * one resource kind; passing a positional query to the same flag fuzzy-searches
7
- * for a single resource and prints its detail. Resource names render as OSC-8
8
- * hyperlinks to the marker file (SKILL.md / WORKFLOW.md / AGENT.md / the file
9
- * itself) so users can click straight to the source.
4
+ * Agent targets (`claude`, `claude@2.1.170`) show the per-version header (paths,
5
+ * shim, capabilities, resource counts, sessions). Repo targets (`user`, `system`,
6
+ * `project`, a registered extra-repo alias, or a filesystem path to a repo with a
7
+ * `.agents/` dir or to a DotAgents root itself) show the repo root, git state, and
8
+ * per-kind resource counts. Drill-down flags (`--skills`, `--hooks`, `--mcp`, ...)
9
+ * list one resource kind for either target form; passing a positional query to the
10
+ * same flag fuzzy-searches for a single resource and prints its detail. Resource
11
+ * names render as OSC-8 hyperlinks to the marker file (SKILL.md / WORKFLOW.md /
12
+ * AGENT.md / the file itself) so users can click straight to the source.
10
13
  */
11
14
  import { Command } from 'commander';
15
+ /** Resource kinds the inspect command can drill into. */
16
+ declare const DRILLABLE_KINDS: readonly ["commands", "skills", "hooks", "mcp", "rules", "plugins", "workflows", "subagents"];
17
+ type DrillableKind = typeof DRILLABLE_KINDS[number];
18
+ interface ResourceItem {
19
+ name: string;
20
+ source: string;
21
+ /** Absolute path to the resource entry (file or directory). */
22
+ path: string;
23
+ /** Path the OSC-8 link should point at — marker file inside bundles, else `path`. */
24
+ linkTarget: string;
25
+ /** One-line description (frontmatter `description:` or first non-frontmatter line). */
26
+ description: string;
27
+ }
12
28
  interface InspectOptions {
13
29
  brief?: boolean;
14
30
  json?: boolean;
@@ -23,4 +39,19 @@ interface InspectOptions {
23
39
  }
24
40
  export declare function registerInspectCommand(program: Command): void;
25
41
  export declare function inspectAction(target: string, options: InspectOptions): Promise<void>;
42
+ export interface RepoTarget {
43
+ /** Display label: 'user' | 'system' | 'project', an extra-repo alias, or a path-derived name. */
44
+ label: string;
45
+ /** Absolute path to the DotAgents root (the dir holding commands/, skills/, ...). */
46
+ root: string;
47
+ }
48
+ /**
49
+ * Resolve a non-agent target as a DotAgents repo: the built-in layer names,
50
+ * a registered extra-repo alias, or a filesystem path. Paths accept either a
51
+ * DotAgents root itself or a repo whose `.agents/` dir should be inspected.
52
+ * Returns null when the target is none of these.
53
+ */
54
+ export declare function resolveRepoTarget(target: string, cwd?: string): RepoTarget | null;
55
+ /** List one resource kind from a single repo root — no layering, no overrides. */
56
+ export declare function collectRepoKind(repo: RepoTarget, kind: DrillableKind): ResourceItem[];
26
57
  export {};
@@ -1,13 +1,17 @@
1
1
  /**
2
- * `agents inspect <agent>[@version]`single-agent, single-version detail view.
2
+ * `agents inspect <target>`detail view for one agent+version or one DotAgents repo.
3
3
  *
4
- * Summary mode shows the per-version header (paths, shim, capabilities, resource
5
- * counts, sessions). Drill-down flags (`--skills`, `--hooks`, `--mcp`, ...) list
6
- * one resource kind; passing a positional query to the same flag fuzzy-searches
7
- * for a single resource and prints its detail. Resource names render as OSC-8
8
- * hyperlinks to the marker file (SKILL.md / WORKFLOW.md / AGENT.md / the file
9
- * itself) so users can click straight to the source.
4
+ * Agent targets (`claude`, `claude@2.1.170`) show the per-version header (paths,
5
+ * shim, capabilities, resource counts, sessions). Repo targets (`user`, `system`,
6
+ * `project`, a registered extra-repo alias, or a filesystem path to a repo with a
7
+ * `.agents/` dir or to a DotAgents root itself) show the repo root, git state, and
8
+ * per-kind resource counts. Drill-down flags (`--skills`, `--hooks`, `--mcp`, ...)
9
+ * list one resource kind for either target form; passing a positional query to the
10
+ * same flag fuzzy-searches for a single resource and prints its detail. Resource
11
+ * names render as OSC-8 hyperlinks to the marker file (SKILL.md / WORKFLOW.md /
12
+ * AGENT.md / the file itself) so users can click straight to the source.
10
13
  */
14
+ import { execSync } from 'child_process';
11
15
  import * as fs from 'fs';
12
16
  import * as os from 'os';
13
17
  import * as path from 'path';
@@ -15,7 +19,7 @@ import chalk from 'chalk';
15
19
  import * as yaml from 'yaml';
16
20
  import { AGENTS, getCliState } from '../lib/agents.js';
17
21
  import { supports } from '../lib/capabilities.js';
18
- import { readMeta } from '../lib/state.js';
22
+ import { readMeta, getUserAgentsDir, getSystemAgentsDir, getProjectAgentsDir, getEnabledExtraRepos, } from '../lib/state.js';
19
23
  import { getVersionHomePath } from '../lib/versions.js';
20
24
  import { getShimsDir, getVersionedAliasPath } from '../lib/shims.js';
21
25
  import { getAgentResources, listResources, } from '../lib/resources.js';
@@ -39,8 +43,8 @@ const CAPABILITY_NAMES = [
39
43
  // ─── Command registration ────────────────────────────────────────────────────
40
44
  export function registerInspectCommand(program) {
41
45
  const cmd = program
42
- .command('inspect <agent>')
43
- .description('Inspect one installed agent at one version — paths, capabilities, resources, drill into any kind.')
46
+ .command('inspect <target>')
47
+ .description('Inspect one installed agent at one version, or a DotAgents repo (user|system|project|alias|path) — paths, capabilities, resources, drill into any kind.')
44
48
  .option('--brief', 'header + capabilities only; skip resources/sessions')
45
49
  .option('--json', 'machine-readable JSON output');
46
50
  for (const kind of DRILLABLE_KINDS) {
@@ -52,6 +56,20 @@ export function registerInspectCommand(program) {
52
56
  }
53
57
  // ─── Main dispatcher ─────────────────────────────────────────────────────────
54
58
  export async function inspectAction(target, options) {
59
+ const agentKey = target.split('@')[0].toLowerCase();
60
+ if (!(agentKey in AGENTS)) {
61
+ const repo = resolveRepoTarget(target);
62
+ if (repo) {
63
+ await inspectRepo(repo, options);
64
+ return;
65
+ }
66
+ const extras = getEnabledExtraRepos();
67
+ console.error(chalk.red(`Unknown target: ${target}`));
68
+ console.error(chalk.gray(`Agents: ${Object.keys(AGENTS).join(', ')}`));
69
+ const aliases = extras.length > 0 ? `, ${extras.map(e => e.alias).join(', ')}` : '';
70
+ console.error(chalk.gray(`Repos: user, system, project${aliases} — or a path to a repo with a .agents/ dir`));
71
+ process.exit(1);
72
+ }
55
73
  const { agent, version } = parseTarget(target);
56
74
  const versionHome = getVersionHomePath(agent, version);
57
75
  if (!fs.existsSync(versionHome)) {
@@ -75,13 +93,7 @@ export async function inspectAction(target, options) {
75
93
  }
76
94
  function parseTarget(target) {
77
95
  const [rawAgent, rawVersion] = target.split('@');
78
- const agentKey = (rawAgent || '').toLowerCase();
79
- if (!(agentKey in AGENTS)) {
80
- console.error(chalk.red(`Unknown agent: ${rawAgent}`));
81
- console.error(chalk.gray(`Known agents: ${Object.keys(AGENTS).join(', ')}`));
82
- process.exit(1);
83
- }
84
- const agent = agentKey;
96
+ const agent = (rawAgent || '').toLowerCase();
85
97
  let version = rawVersion;
86
98
  if (!version || version === 'default') {
87
99
  const meta = readMeta();
@@ -110,6 +122,160 @@ function pickDrillKind(options) {
110
122
  }
111
123
  return active[0];
112
124
  }
125
+ /** Files at a DotAgents root that mark it as one, beyond the per-kind dirs. */
126
+ const REPO_MARKER_FILES = ['agents.yaml', 'hooks.yaml'];
127
+ /**
128
+ * Resolve a non-agent target as a DotAgents repo: the built-in layer names,
129
+ * a registered extra-repo alias, or a filesystem path. Paths accept either a
130
+ * DotAgents root itself or a repo whose `.agents/` dir should be inspected.
131
+ * Returns null when the target is none of these.
132
+ */
133
+ export function resolveRepoTarget(target, cwd) {
134
+ if (target === 'user')
135
+ return { label: 'user', root: getUserAgentsDir() };
136
+ if (target === 'system')
137
+ return { label: 'system', root: getSystemAgentsDir() };
138
+ if (target === 'project') {
139
+ const dir = getProjectAgentsDir(cwd);
140
+ if (!dir) {
141
+ console.error(chalk.red('No project .agents/ directory found from the current directory.'));
142
+ process.exit(1);
143
+ }
144
+ return { label: 'project', root: dir };
145
+ }
146
+ for (const extra of getEnabledExtraRepos()) {
147
+ if (extra.alias === target)
148
+ return { label: extra.alias, root: extra.dir };
149
+ }
150
+ const expanded = target.startsWith('~/') ? path.join(os.homedir(), target.slice(2)) : target;
151
+ const abs = path.resolve(cwd ?? process.cwd(), expanded);
152
+ const stat = safeStat(abs);
153
+ if (!stat || !stat.isDirectory())
154
+ return null;
155
+ // A dir that is itself a DotAgents root wins over its nested .agents/ —
156
+ // extra repos like ~/.agents-extras keep resources at the top level and use
157
+ // .agents/ only for worktrees.
158
+ if (isDotAgentsRoot(abs)) {
159
+ const label = path.basename(abs) === '.agents' ? path.basename(path.dirname(abs)) : path.basename(abs);
160
+ return { label, root: abs };
161
+ }
162
+ if (path.basename(abs) !== '.agents') {
163
+ const nested = path.join(abs, '.agents');
164
+ if (safeStat(nested)?.isDirectory()) {
165
+ return { label: path.basename(abs), root: nested };
166
+ }
167
+ }
168
+ return null;
169
+ }
170
+ function isDotAgentsRoot(dir) {
171
+ for (const marker of REPO_MARKER_FILES) {
172
+ if (fs.existsSync(path.join(dir, marker)))
173
+ return true;
174
+ }
175
+ for (const kind of DRILLABLE_KINDS) {
176
+ if (safeStat(path.join(dir, kind))?.isDirectory())
177
+ return true;
178
+ }
179
+ return false;
180
+ }
181
+ async function inspectRepo(repo, options) {
182
+ const drill = pickDrillKind(options);
183
+ const jsonHead = { repo: repo.label, root: repo.root };
184
+ if (drill) {
185
+ const items = collectRepoKind(repo, drill.kind);
186
+ if (drill.query === true || drill.query === undefined) {
187
+ renderItemList(repo.label, jsonHead, drill.kind, items, options);
188
+ }
189
+ else {
190
+ renderItemDetail(repo.label, jsonHead, drill.kind, String(drill.query), items, options);
191
+ }
192
+ return;
193
+ }
194
+ renderRepoSummary(repo, options);
195
+ }
196
+ /** List one resource kind from a single repo root — no layering, no overrides. */
197
+ export function collectRepoKind(repo, kind) {
198
+ const dir = path.join(repo.root, kind);
199
+ let entries;
200
+ try {
201
+ entries = fs.readdirSync(dir, { withFileTypes: true });
202
+ }
203
+ catch {
204
+ return [];
205
+ }
206
+ const items = [];
207
+ for (const entry of entries) {
208
+ if (entry.name.startsWith('.'))
209
+ continue;
210
+ const p = path.join(dir, entry.name);
211
+ items.push({
212
+ name: entry.name.replace(/\.(md|yaml|yml|toml|json)$/, ''),
213
+ source: repo.label,
214
+ path: p,
215
+ linkTarget: linkTarget(p),
216
+ description: readDescription(p),
217
+ });
218
+ }
219
+ return items.sort((a, b) => a.name.localeCompare(b.name));
220
+ }
221
+ function renderRepoSummary(repo, options) {
222
+ const git = repoGitInfo(repo.root);
223
+ const manifests = REPO_MARKER_FILES.filter(m => fs.existsSync(path.join(repo.root, m)));
224
+ const counts = {};
225
+ if (!options.brief) {
226
+ for (const kind of DRILLABLE_KINDS) {
227
+ const items = collectRepoKind(repo, kind);
228
+ counts[kind] = { total: items.length, bySource: { [repo.label]: items.length } };
229
+ }
230
+ }
231
+ if (options.json) {
232
+ console.log(JSON.stringify({
233
+ repo: repo.label,
234
+ root: repo.root,
235
+ git,
236
+ manifests,
237
+ resources: options.brief ? null : Object.fromEntries(DRILLABLE_KINDS.map(kind => [kind, counts[kind].total])),
238
+ }, null, 2));
239
+ return;
240
+ }
241
+ console.log('\n' + chalk.bold(repo.label) + ' ' + chalk.gray('[dotagents repo]') + '\n');
242
+ const rows = [['root', termLink(repo.root, repo.root)]];
243
+ if (git) {
244
+ const dirty = git.dirty > 0 ? ` ${chalk.gray('·')} ${chalk.yellow(`${git.dirty} dirty`)}` : '';
245
+ const url = git.url ? ` ${chalk.gray('·')} ${chalk.gray(git.url)}` : '';
246
+ rows.push(['git', `${git.branch}${dirty}${url}`]);
247
+ }
248
+ if (manifests.length > 0)
249
+ rows.push(['manifests', manifests.join(', ')]);
250
+ for (const [k, v] of rows)
251
+ console.log(` ${k.padEnd(10)} ${v}`);
252
+ if (!options.brief) {
253
+ console.log('\n' + chalk.bold('Resources'));
254
+ for (const kind of DRILLABLE_KINDS) {
255
+ console.log(` ${kind.padEnd(10)} ${String(counts[kind].total).padStart(4)}`);
256
+ }
257
+ }
258
+ console.log('');
259
+ console.log(chalk.gray(`Drill in: agents inspect ${repo.label} --skills <query>`));
260
+ console.log('');
261
+ }
262
+ function repoGitInfo(root) {
263
+ const git = (args) => {
264
+ try {
265
+ return execSync(`git -C ${JSON.stringify(root)} ${args}`, { stdio: ['ignore', 'pipe', 'ignore'] })
266
+ .toString().trim();
267
+ }
268
+ catch {
269
+ return null;
270
+ }
271
+ };
272
+ const branch = git('rev-parse --abbrev-ref HEAD');
273
+ if (branch === null)
274
+ return null;
275
+ const status = git('status --porcelain');
276
+ const dirty = status ? status.split('\n').filter(Boolean).length : 0;
277
+ return { branch, dirty, url: git('remote get-url origin') };
278
+ }
113
279
  // ─── Summary mode ────────────────────────────────────────────────────────────
114
280
  async function renderSummary(agent, version, versionHome, options) {
115
281
  const meta = readMeta();
@@ -184,17 +350,19 @@ async function renderSummary(agent, version, versionHome, options) {
184
350
  // ─── List mode ───────────────────────────────────────────────────────────────
185
351
  async function renderList(agent, version, versionHome, kind, options) {
186
352
  const items = collectKind(agent, versionHome, kind);
353
+ renderItemList(`${agent}@${version}`, { agent, version }, kind, items, options);
354
+ }
355
+ function renderItemList(header, jsonHead, kind, items, options) {
187
356
  if (options.json) {
188
357
  console.log(JSON.stringify({
189
- agent,
190
- version,
358
+ ...jsonHead,
191
359
  kind,
192
360
  count: items.length,
193
361
  items: items.map(i => ({ name: i.name, source: i.source, path: i.path, description: i.description })),
194
362
  }, null, 2));
195
363
  return;
196
364
  }
197
- console.log('\n' + chalk.bold(`${agent}@${version}`) + ' ' + chalk.gray(`${kind} (${items.length})`) + '\n');
365
+ console.log('\n' + chalk.bold(header) + ' ' + chalk.gray(`${kind} (${items.length})`) + '\n');
198
366
  if (items.length === 0) {
199
367
  console.log(chalk.gray(` (none installed)`));
200
368
  console.log('');
@@ -212,11 +380,14 @@ async function renderList(agent, version, versionHome, kind, options) {
212
380
  // ─── Detail mode (fuzzy) ─────────────────────────────────────────────────────
213
381
  async function renderDetail(agent, version, versionHome, kind, query, options) {
214
382
  const items = collectKind(agent, versionHome, kind);
383
+ renderItemDetail(`${agent}@${version}`, { agent, version }, kind, query, items, options);
384
+ }
385
+ function renderItemDetail(header, jsonHead, kind, query, items, options) {
215
386
  const matches = findMatches(items, query);
216
387
  if (matches.length === 0) {
217
388
  const suggestions = suggestClosest(items, query, 3);
218
389
  if (options.json) {
219
- console.log(JSON.stringify({ agent, version, kind, query, match: null, suggestions: suggestions.map(s => s.name) }, null, 2));
390
+ console.log(JSON.stringify({ ...jsonHead, kind, query, match: null, suggestions: suggestions.map(s => s.name) }, null, 2));
220
391
  }
221
392
  else {
222
393
  console.error(chalk.red(`No ${kind} matching '${query}'.`));
@@ -231,8 +402,7 @@ async function renderDetail(agent, version, versionHome, kind, query, options) {
231
402
  if (options.json) {
232
403
  const detail = buildDetail(best.item, kind);
233
404
  console.log(JSON.stringify({
234
- agent,
235
- version,
405
+ ...jsonHead,
236
406
  kind,
237
407
  query,
238
408
  match: { ...detail, matchKind: best.matchKind },
@@ -240,7 +410,7 @@ async function renderDetail(agent, version, versionHome, kind, query, options) {
240
410
  }, null, 2));
241
411
  return;
242
412
  }
243
- console.log('\n' + chalk.bold(`${agent}@${version}`) + ' ' + chalk.gray(`${kind} matching "${query}"`) + '\n');
413
+ console.log('\n' + chalk.bold(header) + ' ' + chalk.gray(`${kind} matching "${query}"`) + '\n');
244
414
  const matchTag = best.matchKind === 'exact' ? 'exact' : best.matchKind === 'substring' ? 'substring' : `~${best.distance}`;
245
415
  console.log(` ${chalk.green('✓')} ${termLink(chalk.bold.cyan(best.item.name), best.item.linkTarget)} ${chalk.gray(`[${matchTag}, ${best.item.source}]`)}`);
246
416
  if (best.item.description) {
@@ -4,7 +4,7 @@ import * as fs from 'fs';
4
4
  import * as os from 'os';
5
5
  import * as path from 'path';
6
6
  import { select, checkbox } from '@inquirer/prompts';
7
- import { AGENTS, ALL_AGENT_IDS, resolveAgentName, formatAgentError, agentLabel, } from '../lib/agents.js';
7
+ import { AGENTS, agentConfigDirName, ALL_AGENT_IDS, resolveAgentName, formatAgentError, agentLabel, } from '../lib/agents.js';
8
8
  import { cloneRepo } from '../lib/git.js';
9
9
  import { discoverInstructionsFromRepo, discoverRuleFilesFromRepo, installInstructionsCentrally, uninstallInstructions, listInstalledInstructionsWithScope, instructionsExists, getInstructionsContent, listCentralRules, } from '../lib/rules/rules.js';
10
10
  import { listInstalledVersions, getGlobalDefault, resolveVersionAlias, syncResourcesToVersion, promptAgentVersionSelection, getVersionHomePath, } from '../lib/versions.js';
@@ -426,7 +426,7 @@ Examples:
426
426
  return;
427
427
  }
428
428
  const home = getVersionHomePath(agentId, requestedVersion);
429
- const filePath = path.join(home, `.${agentId}`, AGENTS[agentId].instructionsFile);
429
+ const filePath = path.join(home, agentConfigDirName(agentId), AGENTS[agentId].instructionsFile);
430
430
  if (!fs.existsSync(filePath)) {
431
431
  console.log(chalk.yellow(`No user rules found for ${agentLabel(agentId)}@${requestedVersion}`));
432
432
  return;
@@ -486,7 +486,7 @@ Examples:
486
486
  }
487
487
  const home = getVersionHomePath(agentId, requestedVersion);
488
488
  const agent = AGENTS[agentId];
489
- const filePath = path.join(home, `.${agentId}`, agent.instructionsFile);
489
+ const filePath = path.join(home, agentConfigDirName(agentId), agent.instructionsFile);
490
490
  if (fs.existsSync(filePath)) {
491
491
  fs.unlinkSync(filePath);
492
492
  console.log(chalk.green(`Removed ${agent.instructionsFile} from ${agentLabel(agent.id)}@${requestedVersion}`));
@@ -1052,15 +1052,15 @@ Examples:
1052
1052
  password += charClass[charIndex];
1053
1053
  }
1054
1054
  if (opts.copy) {
1055
- const { spawn } = await import('child_process');
1056
- const proc = spawn('pbcopy', [], { stdio: ['pipe', 'inherit', 'inherit'] });
1057
- proc.stdin.write(password);
1058
- proc.stdin.end();
1059
- await new Promise((resolve, reject) => {
1060
- proc.on('close', (code) => code === 0 ? resolve() : reject(new Error('pbcopy failed')));
1061
- proc.on('error', reject);
1062
- });
1063
- console.log(chalk.green(`Password copied to clipboard (${length} chars)`));
1055
+ try {
1056
+ await copyToClipboard(password);
1057
+ console.log(chalk.green(`Password copied to clipboard (${length} chars)`));
1058
+ }
1059
+ catch (err) {
1060
+ console.error(chalk.red(`Clipboard copy failed: ${err.message}`));
1061
+ console.error(chalk.gray('Re-run without --copy to print the password instead.'));
1062
+ process.exitCode = 1;
1063
+ }
1064
1064
  }
1065
1065
  else {
1066
1066
  console.log(password);
@@ -1069,3 +1069,40 @@ Examples:
1069
1069
  registerSecretsSyncCommands(cmd);
1070
1070
  registerSecretsMigrateAclCommand(cmd);
1071
1071
  }
1072
+ /**
1073
+ * Copy text to the system clipboard, cross-platform.
1074
+ * macOS: `pbcopy`. Windows: `clip`. Linux: tries `wl-copy` (Wayland), then
1075
+ * `xclip`, then `xsel` (X11). Throws with an install hint if none are present.
1076
+ */
1077
+ async function copyToClipboard(text) {
1078
+ const { spawn } = await import('child_process');
1079
+ const candidates = process.platform === 'darwin'
1080
+ ? [['pbcopy', []]]
1081
+ : process.platform === 'win32'
1082
+ ? [['clip', []]]
1083
+ : [
1084
+ ['wl-copy', []],
1085
+ ['xclip', ['-selection', 'clipboard']],
1086
+ ['xsel', ['--clipboard', '--input']],
1087
+ ];
1088
+ let lastErr = null;
1089
+ for (const [cmd, args] of candidates) {
1090
+ try {
1091
+ await new Promise((resolve, reject) => {
1092
+ const proc = spawn(cmd, args, { stdio: ['pipe', 'ignore', 'ignore'] });
1093
+ proc.on('error', reject);
1094
+ proc.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`${cmd} exited ${code}`))));
1095
+ proc.stdin.write(text);
1096
+ proc.stdin.end();
1097
+ });
1098
+ return;
1099
+ }
1100
+ catch (err) {
1101
+ lastErr = err;
1102
+ }
1103
+ }
1104
+ const hint = process.platform === 'linux'
1105
+ ? ' Install one: wl-clipboard (Wayland) or xclip / xsel (X11).'
1106
+ : '';
1107
+ throw new Error(`no clipboard tool available (${lastErr?.message ?? 'none found'}).${hint}`);
1108
+ }