@phnx-labs/agents-cli 1.15.0 → 1.17.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.
Files changed (111) hide show
  1. package/CHANGELOG.md +143 -39
  2. package/README.md +6 -6
  3. package/dist/commands/alias.js +2 -2
  4. package/dist/commands/browser-picker.d.ts +21 -0
  5. package/dist/commands/browser-picker.js +114 -0
  6. package/dist/commands/browser.js +793 -83
  7. package/dist/commands/cloud.js +8 -0
  8. package/dist/commands/commands.js +72 -22
  9. package/dist/commands/daemon.js +2 -2
  10. package/dist/commands/exec.js +70 -1
  11. package/dist/commands/hooks.js +71 -26
  12. package/dist/commands/mcp.js +81 -39
  13. package/dist/commands/plugins.js +224 -17
  14. package/dist/commands/prune.js +29 -1
  15. package/dist/commands/pull.js +3 -3
  16. package/dist/commands/repo.js +1 -1
  17. package/dist/commands/routines.js +2 -2
  18. package/dist/commands/secrets.js +154 -20
  19. package/dist/commands/sessions.js +62 -19
  20. package/dist/commands/{init.d.ts → setup.d.ts} +7 -6
  21. package/dist/commands/{init.js → setup.js} +22 -21
  22. package/dist/commands/skills.js +60 -19
  23. package/dist/commands/subagents.js +41 -13
  24. package/dist/commands/utils.d.ts +16 -0
  25. package/dist/commands/utils.js +32 -0
  26. package/dist/commands/view.js +78 -20
  27. package/dist/commands/workflows.d.ts +10 -0
  28. package/dist/commands/workflows.js +457 -0
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.js +48 -36
  31. package/dist/lib/agents.js +2 -2
  32. package/dist/lib/auto-pull-worker.js +2 -3
  33. package/dist/lib/auto-pull.js +2 -2
  34. package/dist/lib/browser/cdp.d.ts +7 -1
  35. package/dist/lib/browser/cdp.js +32 -1
  36. package/dist/lib/browser/chrome.d.ts +10 -0
  37. package/dist/lib/browser/chrome.js +41 -3
  38. package/dist/lib/browser/devices.d.ts +4 -0
  39. package/dist/lib/browser/devices.js +27 -0
  40. package/dist/lib/browser/drivers/local.js +22 -6
  41. package/dist/lib/browser/drivers/ssh.js +9 -2
  42. package/dist/lib/browser/input.d.ts +1 -0
  43. package/dist/lib/browser/input.js +3 -0
  44. package/dist/lib/browser/ipc.js +158 -23
  45. package/dist/lib/browser/profiles.d.ts +10 -2
  46. package/dist/lib/browser/profiles.js +122 -37
  47. package/dist/lib/browser/service.d.ts +91 -13
  48. package/dist/lib/browser/service.js +767 -132
  49. package/dist/lib/browser/types.d.ts +91 -3
  50. package/dist/lib/browser/types.js +16 -0
  51. package/dist/lib/cloud/rush.d.ts +28 -1
  52. package/dist/lib/cloud/rush.js +69 -14
  53. package/dist/lib/cloud/store.js +2 -2
  54. package/dist/lib/commands.d.ts +1 -15
  55. package/dist/lib/commands.js +11 -7
  56. package/dist/lib/daemon.js +2 -3
  57. package/dist/lib/doctor-diff.js +4 -4
  58. package/dist/lib/events.js +2 -2
  59. package/dist/lib/hooks.d.ts +11 -7
  60. package/dist/lib/hooks.js +138 -49
  61. package/dist/lib/migrate.d.ts +1 -1
  62. package/dist/lib/migrate.js +1237 -22
  63. package/dist/lib/models.js +2 -2
  64. package/dist/lib/permissions.d.ts +8 -66
  65. package/dist/lib/permissions.js +18 -18
  66. package/dist/lib/plugins.d.ts +94 -24
  67. package/dist/lib/plugins.js +702 -123
  68. package/dist/lib/pty-server.js +9 -10
  69. package/dist/lib/resource-patterns.d.ts +41 -0
  70. package/dist/lib/resource-patterns.js +82 -0
  71. package/dist/lib/resources/hooks.d.ts +5 -1
  72. package/dist/lib/resources/hooks.js +21 -4
  73. package/dist/lib/resources/index.d.ts +17 -0
  74. package/dist/lib/resources/index.js +7 -0
  75. package/dist/lib/resources/types.d.ts +1 -1
  76. package/dist/lib/resources/workflows.d.ts +24 -0
  77. package/dist/lib/resources/workflows.js +110 -0
  78. package/dist/lib/resources.d.ts +6 -1
  79. package/dist/lib/resources.js +12 -2
  80. package/dist/lib/rotate.js +3 -4
  81. package/dist/lib/session/active.d.ts +3 -0
  82. package/dist/lib/session/active.js +92 -6
  83. package/dist/lib/session/cloud.js +2 -2
  84. package/dist/lib/session/db.d.ts +18 -0
  85. package/dist/lib/session/db.js +109 -5
  86. package/dist/lib/session/discover.d.ts +6 -0
  87. package/dist/lib/session/discover.js +55 -29
  88. package/dist/lib/session/team-filter.js +2 -2
  89. package/dist/lib/shims.d.ts +4 -52
  90. package/dist/lib/shims.js +23 -15
  91. package/dist/lib/skills.js +6 -2
  92. package/dist/lib/sqlite.js +10 -4
  93. package/dist/lib/state.d.ts +101 -16
  94. package/dist/lib/state.js +179 -31
  95. package/dist/lib/subagents.d.ts +28 -0
  96. package/dist/lib/subagents.js +98 -1
  97. package/dist/lib/sync-manifest.d.ts +1 -1
  98. package/dist/lib/sync-manifest.js +3 -3
  99. package/dist/lib/teams/persistence.js +15 -5
  100. package/dist/lib/teams/registry.js +2 -2
  101. package/dist/lib/types.d.ts +75 -17
  102. package/dist/lib/types.js +3 -3
  103. package/dist/lib/usage.js +2 -2
  104. package/dist/lib/versions.d.ts +3 -0
  105. package/dist/lib/versions.js +158 -47
  106. package/dist/lib/workflows.d.ts +79 -0
  107. package/dist/lib/workflows.js +233 -0
  108. package/package.json +1 -5
  109. package/scripts/postinstall.js +60 -59
  110. package/dist/commands/fork.d.ts +0 -10
  111. package/dist/commands/fork.js +0 -146
@@ -14,7 +14,7 @@ import * as path from 'path';
14
14
  import * as crypto from 'crypto';
15
15
  import { execFileSync } from 'child_process';
16
16
  import { fileURLToPath } from 'url';
17
- import { getAgentsDir } from './state.js';
17
+ import { getPtyDir as getPtyDirRoot } from './state.js';
18
18
  /**
19
19
  * Capture a stable identifier for a process at the moment it was started.
20
20
  * Used to defeat PID reuse: a kill(pid, ...) is only safe when the process
@@ -53,7 +53,6 @@ export function captureProcessStartTime(pid) {
53
53
  }
54
54
  // --- Constants ---
55
55
  const SENTINEL = '__AGENTS_PTY_DONE__';
56
- const PTY_DIR = 'helpers/pty';
57
56
  const SOCKET_NAME = 'pty.sock';
58
57
  const PID_FILE = 'pty.pid';
59
58
  const LOG_FILE = 'logs.jsonl';
@@ -84,7 +83,7 @@ function buildPtyEnv() {
84
83
  }
85
84
  /** Get the PTY helper directory, creating it if needed. */
86
85
  function getPtyDir() {
87
- const dir = path.join(getAgentsDir(), PTY_DIR);
86
+ const dir = getPtyDirRoot();
88
87
  fs.mkdirSync(dir, { recursive: true });
89
88
  return dir;
90
89
  }
@@ -178,7 +177,7 @@ export async function runPtyServer() {
178
177
  }
179
178
  catch (err) {
180
179
  console.error('node-pty is required for PTY support.');
181
- console.error('Install: cd ' + getAgentsDir() + '/../agents-cli && bun add node-pty');
180
+ console.error('Install: cd ' + '~/agents-cli && bun add node-pty');
182
181
  process.exit(1);
183
182
  }
184
183
  try {
@@ -188,7 +187,7 @@ export async function runPtyServer() {
188
187
  }
189
188
  catch {
190
189
  console.error('@xterm/headless is required for PTY support.');
191
- console.error('Install: cd ' + getAgentsDir() + '/../agents-cli && bun add @xterm/headless');
190
+ console.error('Install: cd ' + '~/agents-cli && bun add @xterm/headless');
192
191
  process.exit(1);
193
192
  }
194
193
  const sessions = new Map();
@@ -493,11 +492,11 @@ export async function runPtyServer() {
493
492
  });
494
493
  conn.on('error', () => { });
495
494
  });
496
- // Lock down ~/.agents-system/ before opening the socket — without this, any local
497
- // user with execute on the parent dir could connect to the socket during
498
- // the listen()-to-chmod() window. macOS BSD AF_UNIX semantics make socket
499
- // mode advisory only, so the parent dir is the real boundary.
500
- const agentsDir = getAgentsDir();
495
+ // Lock down the PTY scratch dir before opening the socket — without this,
496
+ // any local user with execute on the parent dir could connect to the socket
497
+ // during the listen()-to-chmod() window. macOS BSD AF_UNIX semantics make
498
+ // socket mode advisory only, so the parent dir is the real boundary.
499
+ const agentsDir = getPtyDirRoot();
501
500
  fs.mkdirSync(agentsDir, { recursive: true });
502
501
  fs.chmodSync(agentsDir, 0o700);
503
502
  // umask covers any inherited group/other bits while listen() is creating
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Resource selection patterns for agents.yaml versions: entries.
3
+ *
4
+ * Pattern syntax: [!]source:name
5
+ * "system:*" — all resources from ~/.agents-system/
6
+ * "user:*" — all resources from ~/.agents/
7
+ * "rush:*" — all resources from ~/.agents-rush/ (extra repo alias)
8
+ * "project:*" — all resources from .agents/ in the project root
9
+ * "user:foo" — specifically the resource named "foo" from ~/.agents/
10
+ * "!user:temp" — exclude "temp" from the user repo
11
+ *
12
+ * Evaluation rule: union all inclusions, then subtract all exclusions.
13
+ */
14
+ export interface ParsedPattern {
15
+ negate: boolean;
16
+ source: string;
17
+ name: string;
18
+ }
19
+ export declare function parsePattern(p: string): ParsedPattern;
20
+ /** Returns true if the string is a legacy plain name with no source: prefix. */
21
+ export declare function isLegacyName(p: string): boolean;
22
+ /**
23
+ * Expand a list of patterns against an available name→source map.
24
+ * Returns the union of matching names with exclusions subtracted.
25
+ *
26
+ * Supports comma-grouped names to avoid repeating the source prefix:
27
+ * "system:brain-scan,mq" → includes brain-scan and mq from system
28
+ * "!user:temp,draft" → excludes temp and draft from user
29
+ *
30
+ * Note: in YAML flow sequences ([...]) a comma inside a pattern requires
31
+ * quoting ("system:brain-scan,mq"). Block-style items and yaml.stringify
32
+ * output handle this automatically.
33
+ */
34
+ export declare function expandPatterns(patterns: string[], available: Map<string, string>): string[];
35
+ /**
36
+ * Build the default pattern list for a resource type.
37
+ * Order: system → user → alias1 → alias2 → ... → project (base-to-override).
38
+ * @param extraAliases Alias names of enabled extra repos, in insertion order.
39
+ * @param includeProject Whether to append "project:*". False for hooks (security).
40
+ */
41
+ export declare function defaultPatterns(extraAliases?: string[], includeProject?: boolean): string[];
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Resource selection patterns for agents.yaml versions: entries.
3
+ *
4
+ * Pattern syntax: [!]source:name
5
+ * "system:*" — all resources from ~/.agents-system/
6
+ * "user:*" — all resources from ~/.agents/
7
+ * "rush:*" — all resources from ~/.agents-rush/ (extra repo alias)
8
+ * "project:*" — all resources from .agents/ in the project root
9
+ * "user:foo" — specifically the resource named "foo" from ~/.agents/
10
+ * "!user:temp" — exclude "temp" from the user repo
11
+ *
12
+ * Evaluation rule: union all inclusions, then subtract all exclusions.
13
+ */
14
+ export function parsePattern(p) {
15
+ const negate = p.startsWith('!');
16
+ const raw = negate ? p.slice(1) : p;
17
+ const colon = raw.indexOf(':');
18
+ if (colon === -1) {
19
+ throw new Error(`Invalid resource pattern "${p}": expected "source:name" format`);
20
+ }
21
+ return { negate, source: raw.slice(0, colon), name: raw.slice(colon + 1) };
22
+ }
23
+ /** Returns true if the string is a legacy plain name with no source: prefix. */
24
+ export function isLegacyName(p) {
25
+ return !p.startsWith('!') && !p.includes(':');
26
+ }
27
+ /**
28
+ * Expand a list of patterns against an available name→source map.
29
+ * Returns the union of matching names with exclusions subtracted.
30
+ *
31
+ * Supports comma-grouped names to avoid repeating the source prefix:
32
+ * "system:brain-scan,mq" → includes brain-scan and mq from system
33
+ * "!user:temp,draft" → excludes temp and draft from user
34
+ *
35
+ * Note: in YAML flow sequences ([...]) a comma inside a pattern requires
36
+ * quoting ("system:brain-scan,mq"). Block-style items and yaml.stringify
37
+ * output handle this automatically.
38
+ */
39
+ export function expandPatterns(patterns, available) {
40
+ const included = new Set();
41
+ const excluded = new Set();
42
+ for (const p of patterns) {
43
+ try {
44
+ const { negate, source, name } = parsePattern(p);
45
+ const target = negate ? excluded : included;
46
+ // Comma-grouped names: "system:brain-scan,mq" → ['brain-scan', 'mq']
47
+ const names = name === '*' ? ['*'] : name.split(',').map(n => n.trim()).filter(Boolean);
48
+ for (const n of names) {
49
+ if (n === '*') {
50
+ for (const [rn, rs] of available) {
51
+ if (rs === source)
52
+ target.add(rn);
53
+ }
54
+ }
55
+ else {
56
+ if (available.has(n))
57
+ target.add(n);
58
+ }
59
+ }
60
+ }
61
+ catch {
62
+ // Skip malformed patterns
63
+ }
64
+ }
65
+ return [...included].filter(n => !excluded.has(n));
66
+ }
67
+ /**
68
+ * Build the default pattern list for a resource type.
69
+ * Order: system → user → alias1 → alias2 → ... → project (base-to-override).
70
+ * @param extraAliases Alias names of enabled extra repos, in insertion order.
71
+ * @param includeProject Whether to append "project:*". False for hooks (security).
72
+ */
73
+ export function defaultPatterns(extraAliases = [], includeProject = true) {
74
+ const patterns = ['system:*', 'user:*'];
75
+ for (const alias of extraAliases) {
76
+ patterns.push(`${alias}:*`);
77
+ }
78
+ if (includeProject) {
79
+ patterns.push('project:*');
80
+ }
81
+ return patterns;
82
+ }
@@ -1,7 +1,11 @@
1
1
  /**
2
2
  * HooksHandler - ResourceHandler implementation for hooks.
3
3
  *
4
- * Hooks are declared in hooks.yaml at each layer (system, user, project).
4
+ * Hook declarations live in:
5
+ * - System: ~/.agents-system/hooks.yaml (npm-shipped defaults)
6
+ * - User: `hooks:` section of ~/.agents/agents.yaml
7
+ * - Project: <project>/.agents/hooks.yaml
8
+ *
5
9
  * Resolution: project > user > system (higher layer wins on name conflict).
6
10
  * Non-conflicting hooks from all layers are unioned together.
7
11
  */
@@ -1,7 +1,11 @@
1
1
  /**
2
2
  * HooksHandler - ResourceHandler implementation for hooks.
3
3
  *
4
- * Hooks are declared in hooks.yaml at each layer (system, user, project).
4
+ * Hook declarations live in:
5
+ * - System: ~/.agents-system/hooks.yaml (npm-shipped defaults)
6
+ * - User: `hooks:` section of ~/.agents/agents.yaml
7
+ * - Project: <project>/.agents/hooks.yaml
8
+ *
5
9
  * Resolution: project > user > system (higher layer wins on name conflict).
6
10
  * Non-conflicting hooks from all layers are unioned together.
7
11
  */
@@ -10,13 +14,20 @@ import * as path from 'path';
10
14
  import * as yaml from 'yaml';
11
15
  import { getSystemAgentsDir, getUserAgentsDir, getProjectAgentsDir, } from '../state.js';
12
16
  /**
13
- * Get the hooks.yaml path for a given layer directory.
17
+ * Get the hook manifest path for a layer dir. The user layer reads from
18
+ * agents.yaml (hooks: section) since that's where user hooks now live.
19
+ * Other layers continue to use a standalone hooks.yaml.
14
20
  */
15
21
  function getHooksYamlPath(layerDir) {
22
+ if (layerDir === getUserAgentsDir()) {
23
+ return path.join(layerDir, 'agents.yaml');
24
+ }
16
25
  return path.join(layerDir, 'hooks.yaml');
17
26
  }
18
27
  /**
19
- * Parse hooks.yaml from a directory.
28
+ * Parse hooks for a layer directory.
29
+ * - User layer: read `hooks:` section from agents.yaml.
30
+ * - System / project layer: read top-level map from hooks.yaml.
20
31
  * Returns empty object if file doesn't exist or is invalid.
21
32
  */
22
33
  function parseHooksYaml(dir) {
@@ -27,7 +38,13 @@ function parseHooksYaml(dir) {
27
38
  try {
28
39
  const content = fs.readFileSync(manifestPath, 'utf-8');
29
40
  const parsed = yaml.parse(content);
30
- return parsed || {};
41
+ if (!parsed)
42
+ return {};
43
+ if (dir === getUserAgentsDir()) {
44
+ const hooks = parsed.hooks;
45
+ return (hooks && typeof hooks === 'object') ? hooks : {};
46
+ }
47
+ return parsed;
31
48
  }
32
49
  catch {
33
50
  return {};
@@ -13,6 +13,7 @@ export { RulesHandler, type RuleItem } from './rules.js';
13
13
  export { McpHandler, getMcpConfigPath, type McpItem } from './mcp.js';
14
14
  export { PermissionsHandler, type PermissionItem } from './permissions.js';
15
15
  export { SubagentsHandler, subagentsHandler, type SubagentItem } from './subagents.js';
16
+ export { WorkflowsHandler, type WorkflowItem } from './workflows.js';
16
17
  import type { ResourceKind, ResourceHandler } from './types.js';
17
18
  /** All resource handlers keyed by kind. */
18
19
  export declare const handlers: {
@@ -29,6 +30,22 @@ export declare const handlers: {
29
30
  readonly permissions: ResourceHandler<import("../types.js").PermissionSet>;
30
31
  readonly subagent: import("./subagents.js").SubagentsHandler;
31
32
  readonly subagents: import("./subagents.js").SubagentsHandler;
33
+ readonly workflow: {
34
+ readonly kind: "workflow";
35
+ listAll(_agent: import("./types.js").AgentId, cwd?: string): import("./types.js").ResolvedItem<import("./workflows.js").WorkflowItem>[];
36
+ resolve(_agent: import("./types.js").AgentId, name: string, cwd?: string): import("./types.js").ResolvedItem<import("./workflows.js").WorkflowItem> | null;
37
+ sync(_agent: import("./types.js").AgentId, _versionHome: string, _cwd?: string): void;
38
+ format(_agent: import("./types.js").AgentId): "md";
39
+ targetDir(_agent: import("./types.js").AgentId): string;
40
+ };
41
+ readonly workflows: {
42
+ readonly kind: "workflow";
43
+ listAll(_agent: import("./types.js").AgentId, cwd?: string): import("./types.js").ResolvedItem<import("./workflows.js").WorkflowItem>[];
44
+ resolve(_agent: import("./types.js").AgentId, name: string, cwd?: string): import("./types.js").ResolvedItem<import("./workflows.js").WorkflowItem> | null;
45
+ sync(_agent: import("./types.js").AgentId, _versionHome: string, _cwd?: string): void;
46
+ format(_agent: import("./types.js").AgentId): "md";
47
+ targetDir(_agent: import("./types.js").AgentId): string;
48
+ };
32
49
  };
33
50
  /** Get a handler by resource kind. */
34
51
  export declare function getHandler(kind: ResourceKind): ResourceHandler<unknown> | null;
@@ -13,6 +13,7 @@ export { RulesHandler } from './rules.js';
13
13
  export { McpHandler, getMcpConfigPath } from './mcp.js';
14
14
  export { PermissionsHandler } from './permissions.js';
15
15
  export { SubagentsHandler, subagentsHandler } from './subagents.js';
16
+ export { WorkflowsHandler } from './workflows.js';
16
17
  import { commandsHandler } from './commands.js';
17
18
  import { HooksHandler } from './hooks.js';
18
19
  import { SkillsHandler } from './skills.js';
@@ -20,6 +21,7 @@ import { RulesHandler } from './rules.js';
20
21
  import { McpHandler } from './mcp.js';
21
22
  import { PermissionsHandler } from './permissions.js';
22
23
  import { subagentsHandler } from './subagents.js';
24
+ import { WorkflowsHandler } from './workflows.js';
23
25
  /** All resource handlers keyed by kind. */
24
26
  export const handlers = {
25
27
  command: commandsHandler,
@@ -35,6 +37,8 @@ export const handlers = {
35
37
  permissions: PermissionsHandler,
36
38
  subagent: subagentsHandler,
37
39
  subagents: subagentsHandler,
40
+ workflow: WorkflowsHandler,
41
+ workflows: WorkflowsHandler,
38
42
  };
39
43
  /** Get a handler by resource kind. */
40
44
  export function getHandler(kind) {
@@ -53,6 +57,8 @@ export function getHandler(kind) {
53
57
  return PermissionsHandler;
54
58
  case 'subagent':
55
59
  return subagentsHandler;
60
+ case 'workflow':
61
+ return WorkflowsHandler;
56
62
  default:
57
63
  return null;
58
64
  }
@@ -66,4 +72,5 @@ export const RESOURCE_KINDS = [
66
72
  'mcp',
67
73
  'permission',
68
74
  'subagent',
75
+ 'workflow',
69
76
  ];
@@ -7,7 +7,7 @@
7
7
  */
8
8
  export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw';
9
9
  export type Layer = 'system' | 'user' | 'project';
10
- export type ResourceKind = 'command' | 'hook' | 'skill' | 'rule' | 'mcp' | 'permission' | 'subagent';
10
+ export type ResourceKind = 'command' | 'hook' | 'skill' | 'rule' | 'mcp' | 'permission' | 'subagent' | 'workflow';
11
11
  /** A resolved resource with its origin layer. */
12
12
  export interface ResolvedItem<T> {
13
13
  name: string;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Workflows resource handler.
3
+ *
4
+ * Workflows are directory bundles with a WORKFLOW.md containing YAML frontmatter.
5
+ * They optionally contain subagents/, skills/, and plugins/ subdirectories.
6
+ * Resolution order: project > user > system.
7
+ */
8
+ import type { AgentId, ResolvedItem, ResourceHandler } from './types.js';
9
+ export interface WorkflowItem {
10
+ name: string;
11
+ description: string;
12
+ model?: string;
13
+ subagentCount: number;
14
+ }
15
+ declare class WorkflowsHandlerImpl implements ResourceHandler<WorkflowItem> {
16
+ readonly kind: "workflow";
17
+ listAll(_agent: AgentId, cwd?: string): ResolvedItem<WorkflowItem>[];
18
+ resolve(_agent: AgentId, name: string, cwd?: string): ResolvedItem<WorkflowItem> | null;
19
+ sync(_agent: AgentId, _versionHome: string, _cwd?: string): void;
20
+ format(_agent: AgentId): 'md';
21
+ targetDir(_agent: AgentId): string;
22
+ }
23
+ export declare const WorkflowsHandler: WorkflowsHandlerImpl;
24
+ export {};
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Workflows resource handler.
3
+ *
4
+ * Workflows are directory bundles with a WORKFLOW.md containing YAML frontmatter.
5
+ * They optionally contain subagents/, skills/, and plugins/ subdirectories.
6
+ * Resolution order: project > user > system.
7
+ */
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+ import { getProjectAgentsDir, getUserWorkflowsDir, getSystemWorkflowsDir, getEnabledExtraRepos, } from '../state.js';
11
+ import { parseWorkflowFrontmatter, countWorkflowSubagents } from '../workflows.js';
12
+ function getLayerDirs(cwd) {
13
+ const projectDir = getProjectAgentsDir(cwd);
14
+ const extraRepos = getEnabledExtraRepos();
15
+ return {
16
+ system: getSystemWorkflowsDir(),
17
+ user: getUserWorkflowsDir(),
18
+ project: projectDir ? path.join(projectDir, 'workflows') : null,
19
+ extra: extraRepos.map(e => path.join(e.dir, 'workflows')),
20
+ };
21
+ }
22
+ function listWorkflowsInDir(dir) {
23
+ if (!fs.existsSync(dir))
24
+ return [];
25
+ try {
26
+ return fs.readdirSync(dir, { withFileTypes: true })
27
+ .filter(e => e.isDirectory() && !e.name.startsWith('.') &&
28
+ fs.existsSync(path.join(dir, e.name, 'WORKFLOW.md')))
29
+ .map(e => ({ name: e.name, path: path.join(dir, e.name) }));
30
+ }
31
+ catch {
32
+ return [];
33
+ }
34
+ }
35
+ class WorkflowsHandlerImpl {
36
+ kind = 'workflow';
37
+ listAll(_agent, cwd) {
38
+ const dirs = getLayerDirs(cwd);
39
+ const seen = new Set();
40
+ const results = [];
41
+ const layerDirs = [];
42
+ if (dirs.project)
43
+ layerDirs.push({ dir: dirs.project, layer: 'project' });
44
+ layerDirs.push({ dir: dirs.user, layer: 'user' });
45
+ layerDirs.push({ dir: dirs.system, layer: 'system' });
46
+ for (const extraDir of dirs.extra)
47
+ layerDirs.push({ dir: extraDir, layer: 'system' });
48
+ for (const { dir, layer } of layerDirs) {
49
+ for (const { name, path: workflowPath } of listWorkflowsInDir(dir)) {
50
+ if (seen.has(name))
51
+ continue;
52
+ const fm = parseWorkflowFrontmatter(workflowPath);
53
+ if (!fm)
54
+ continue;
55
+ seen.add(name);
56
+ results.push({
57
+ name,
58
+ item: {
59
+ name: fm.name || name,
60
+ description: fm.description,
61
+ model: fm.model,
62
+ subagentCount: countWorkflowSubagents(workflowPath),
63
+ },
64
+ layer,
65
+ path: workflowPath,
66
+ });
67
+ }
68
+ }
69
+ return results.sort((a, b) => a.name.localeCompare(b.name));
70
+ }
71
+ resolve(_agent, name, cwd) {
72
+ const dirs = getLayerDirs(cwd);
73
+ const searchDirs = [];
74
+ if (dirs.project)
75
+ searchDirs.push({ dir: dirs.project, layer: 'project' });
76
+ searchDirs.push({ dir: dirs.user, layer: 'user' });
77
+ searchDirs.push({ dir: dirs.system, layer: 'system' });
78
+ for (const extraDir of dirs.extra)
79
+ searchDirs.push({ dir: extraDir, layer: 'system' });
80
+ for (const { dir, layer } of searchDirs) {
81
+ const workflowPath = path.join(dir, name);
82
+ const fm = parseWorkflowFrontmatter(workflowPath);
83
+ if (fm) {
84
+ return {
85
+ name,
86
+ item: {
87
+ name: fm.name || name,
88
+ description: fm.description,
89
+ model: fm.model,
90
+ subagentCount: countWorkflowSubagents(workflowPath),
91
+ },
92
+ layer,
93
+ path: workflowPath,
94
+ };
95
+ }
96
+ }
97
+ return null;
98
+ }
99
+ sync(_agent, _versionHome, _cwd) {
100
+ // Version-home copies are written by syncResourcesToVersion in versions.ts.
101
+ // exec.ts resolves workflows at run time from source dirs directly.
102
+ }
103
+ format(_agent) {
104
+ return 'md';
105
+ }
106
+ targetDir(_agent) {
107
+ return 'workflows';
108
+ }
109
+ }
110
+ export const WorkflowsHandler = new WorkflowsHandlerImpl();
@@ -11,7 +11,11 @@ export interface ResolvedResource {
11
11
  name: string;
12
12
  /** Absolute path to the resource file or directory. */
13
13
  path: string;
14
- source: 'project' | 'user' | 'system';
14
+ /**
15
+ * Source layer: 'project' | 'user' | 'system' for built-in layers,
16
+ * or the alias name (e.g. 'rush') for extra repos registered in agents.yaml.
17
+ */
18
+ source: string;
15
19
  }
16
20
  /**
17
21
  * Resolve a single resource by kind + name using project > user > system precedence.
@@ -52,6 +56,7 @@ export interface AgentResources {
52
56
  mcp: McpResourceEntry[];
53
57
  memory: ResourceEntry[];
54
58
  hooks: ResourceEntry[];
59
+ workflows: ResourceEntry[];
55
60
  }
56
61
  /** Options for resource discovery. */
57
62
  export interface GetAgentResourcesOptions {
@@ -11,6 +11,8 @@ import { listInstalledHooksWithScope } from './hooks.js';
11
11
  import { listInstalledInstructionsWithScope } from './rules/rules.js';
12
12
  import { getEffectiveHome } from './versions.js';
13
13
  import { listMcpServerConfigs } from './mcp.js';
14
+ import { WorkflowsHandler } from './resources/workflows.js';
15
+ import { WORKFLOW_CAPABLE_AGENTS } from './workflows.js';
14
16
  import { getProjectAgentsDir, getUserAgentsDir, getSystemAgentsDir, getEnabledExtraRepos, } from './state.js';
15
17
  /**
16
18
  * Resolve a single resource by kind + name using project > user > system precedence.
@@ -26,7 +28,7 @@ export function resolveResource(kind, name, cwd) {
26
28
  ...(projectDir ? [[path.join(projectDir, kind), 'project']] : []),
27
29
  [path.join(getUserAgentsDir(), kind), 'user'],
28
30
  [path.join(getSystemAgentsDir(), kind), 'system'],
29
- ...extraRepos.map((e) => [path.join(e.dir, kind), 'system']),
31
+ ...extraRepos.map((e) => [path.join(e.dir, kind), e.alias]),
30
32
  ];
31
33
  for (const [dir, source] of candidates) {
32
34
  if (!fs.existsSync(dir))
@@ -60,7 +62,7 @@ export function listResources(kind, cwd) {
60
62
  ...(projectDir ? [[path.join(projectDir, kind), 'project']] : []),
61
63
  [path.join(getUserAgentsDir(), kind), 'user'],
62
64
  [path.join(getSystemAgentsDir(), kind), 'system'],
63
- ...extraRepos.map((e) => [path.join(e.dir, kind), 'system']),
65
+ ...extraRepos.map((e) => [path.join(e.dir, kind), e.alias]),
64
66
  ];
65
67
  for (const [dir, source] of roots) {
66
68
  if (!fs.existsSync(dir))
@@ -159,6 +161,13 @@ export function getAgentResources(agentId, options = {}) {
159
161
  hooks.push({ name: hook.name, path: hook.path, scope: hook.scope });
160
162
  }
161
163
  }
164
+ // Workflows (claude only)
165
+ const workflows = [];
166
+ if (WORKFLOW_CAPABLE_AGENTS.includes(agentId)) {
167
+ for (const w of WorkflowsHandler.listAll(agentId, cwd)) {
168
+ workflows.push({ name: w.name, path: w.path, scope: w.layer === 'project' ? 'project' : 'user' });
169
+ }
170
+ }
162
171
  return {
163
172
  agentId,
164
173
  commands,
@@ -167,6 +176,7 @@ export function getAgentResources(agentId, options = {}) {
167
176
  mcp,
168
177
  memory,
169
178
  hooks,
179
+ workflows,
170
180
  };
171
181
  }
172
182
  /**
@@ -8,12 +8,11 @@ import * as fs from 'fs';
8
8
  import * as path from 'path';
9
9
  import * as yaml from 'yaml';
10
10
  import { getAccountInfo } from './agents.js';
11
- import { readMeta, writeMeta, getAgentsDir } from './state.js';
11
+ import { readMeta, writeMeta, getHelpersDir, getUserAgentsDir } from './state.js';
12
12
  import { listInstalledVersions, getVersionHomePath, resolveVersion } from './versions.js';
13
13
  import { getUsageInfoByIdentity, getUsageLookupKey, isClaudeAuthValid, } from './usage.js';
14
- const ROTATE_DIR = 'helpers/rotate';
15
14
  function getRotateDir() {
16
- const dir = path.join(getAgentsDir(), ROTATE_DIR);
15
+ const dir = path.join(getHelpersDir(), 'rotate');
17
16
  fs.mkdirSync(dir, { recursive: true });
18
17
  return dir;
19
18
  }
@@ -35,7 +34,7 @@ export function normalizeRunStrategy(value) {
35
34
  /** Read project-local run strategy from the nearest agents.yaml, if present. */
36
35
  export function getProjectRunStrategy(agent, startPath) {
37
36
  let dir = path.resolve(startPath);
38
- const userAgentsYaml = path.join(getAgentsDir(), 'agents.yaml');
37
+ const userAgentsYaml = path.join(getUserAgentsDir(), 'agents.yaml');
39
38
  while (dir !== path.dirname(dir)) {
40
39
  const manifestPath = path.join(dir, 'agents.yaml');
41
40
  if (manifestPath !== userAgentsYaml && fs.existsSync(manifestPath)) {
@@ -8,7 +8,10 @@ export interface ActiveSession {
8
8
  pid?: number;
9
9
  sessionId?: string;
10
10
  cwd?: string;
11
+ /** User-given name from /rename command. */
11
12
  label?: string;
13
+ /** First meaningful line of the initial prompt (extracted topic). */
14
+ topic?: string;
12
15
  sessionFile?: string;
13
16
  startedAtMs?: number;
14
17
  status: ActiveStatus;