@phnx-labs/agents-cli 1.14.2 → 1.14.4

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 (121) hide show
  1. package/README.md +17 -7
  2. package/dist/browser.d.ts +2 -0
  3. package/dist/browser.js +7 -0
  4. package/dist/commands/browser.d.ts +3 -0
  5. package/dist/commands/browser.js +392 -0
  6. package/dist/commands/daemon.js +1 -1
  7. package/dist/commands/doctor.d.ts +16 -9
  8. package/dist/commands/doctor.js +248 -12
  9. package/dist/commands/prune.js +9 -3
  10. package/dist/commands/refresh-rules.d.ts +15 -0
  11. package/dist/commands/{refresh-memory.js → refresh-rules.js} +14 -14
  12. package/dist/commands/routines.js +1 -1
  13. package/dist/commands/rules.js +100 -4
  14. package/dist/commands/secrets.js +198 -11
  15. package/dist/commands/sync.js +19 -0
  16. package/dist/commands/teams.js +184 -22
  17. package/dist/commands/trash.d.ts +10 -0
  18. package/dist/commands/trash.js +187 -0
  19. package/dist/commands/view.js +47 -14
  20. package/dist/index.js +62 -4
  21. package/dist/lib/agents.js +2 -2
  22. package/dist/lib/browser/cdp.d.ts +24 -0
  23. package/dist/lib/browser/cdp.js +94 -0
  24. package/dist/lib/browser/chrome.d.ts +16 -0
  25. package/dist/lib/browser/chrome.js +157 -0
  26. package/dist/lib/browser/drivers/local.d.ts +8 -0
  27. package/dist/lib/browser/drivers/local.js +22 -0
  28. package/dist/lib/browser/drivers/ssh.d.ts +9 -0
  29. package/dist/lib/browser/drivers/ssh.js +129 -0
  30. package/dist/lib/browser/index.d.ts +5 -0
  31. package/dist/lib/browser/index.js +5 -0
  32. package/dist/lib/browser/input.d.ts +6 -0
  33. package/dist/lib/browser/input.js +52 -0
  34. package/dist/lib/browser/ipc.d.ts +12 -0
  35. package/dist/lib/browser/ipc.js +223 -0
  36. package/dist/lib/browser/profiles.d.ts +11 -0
  37. package/dist/lib/browser/profiles.js +61 -0
  38. package/dist/lib/browser/refs.d.ts +21 -0
  39. package/dist/lib/browser/refs.js +88 -0
  40. package/dist/lib/browser/service.d.ts +45 -0
  41. package/dist/lib/browser/service.js +404 -0
  42. package/dist/lib/browser/types.d.ts +73 -0
  43. package/dist/lib/browser/types.js +7 -0
  44. package/dist/lib/cloud/codex.js +1 -1
  45. package/dist/lib/cloud/registry.js +2 -2
  46. package/dist/lib/cloud/rush.js +2 -2
  47. package/dist/lib/cloud/store.js +2 -2
  48. package/dist/lib/daemon.d.ts +1 -1
  49. package/dist/lib/daemon.js +47 -11
  50. package/dist/lib/diff-text.d.ts +25 -0
  51. package/dist/lib/diff-text.js +47 -0
  52. package/dist/lib/doctor-diff.d.ts +64 -0
  53. package/dist/lib/doctor-diff.js +497 -0
  54. package/dist/lib/git.js +3 -3
  55. package/dist/lib/hooks.d.ts +6 -0
  56. package/dist/lib/hooks.js +6 -1
  57. package/dist/lib/migrate.js +123 -0
  58. package/dist/lib/pty-client.js +3 -3
  59. package/dist/lib/pty-server.js +36 -7
  60. package/dist/lib/resources/commands.d.ts +46 -0
  61. package/dist/lib/resources/commands.js +208 -0
  62. package/dist/lib/resources/hooks.d.ts +12 -0
  63. package/dist/lib/resources/hooks.js +136 -0
  64. package/dist/lib/resources/index.d.ts +36 -0
  65. package/dist/lib/resources/index.js +69 -0
  66. package/dist/lib/resources/mcp.d.ts +34 -0
  67. package/dist/lib/resources/mcp.js +483 -0
  68. package/dist/lib/resources/permissions.d.ts +13 -0
  69. package/dist/lib/resources/permissions.js +184 -0
  70. package/dist/lib/resources/rules.d.ts +43 -0
  71. package/dist/lib/resources/rules.js +146 -0
  72. package/dist/lib/resources/skills.d.ts +37 -0
  73. package/dist/lib/resources/skills.js +238 -0
  74. package/dist/lib/resources/subagents.d.ts +46 -0
  75. package/dist/lib/resources/subagents.js +198 -0
  76. package/dist/lib/resources/types.d.ts +82 -0
  77. package/dist/lib/resources/types.js +8 -0
  78. package/dist/lib/resources.js +1 -1
  79. package/dist/lib/rotate.d.ts +8 -1
  80. package/dist/lib/rotate.js +17 -4
  81. package/dist/lib/rules/compile.d.ts +104 -0
  82. package/dist/lib/{memory-compile.js → rules/compile.js} +160 -21
  83. package/dist/lib/rules/compose.d.ts +78 -0
  84. package/dist/lib/rules/compose.js +170 -0
  85. package/dist/lib/{memory.d.ts → rules/rules.d.ts} +5 -5
  86. package/dist/lib/{memory.js → rules/rules.js} +10 -10
  87. package/dist/lib/secrets/AgentsKeychain.app/Contents/CodeResources +0 -0
  88. package/dist/lib/secrets/AgentsKeychain.app/Contents/MacOS/AgentsKeychain +0 -0
  89. package/dist/lib/secrets/bundles.d.ts +61 -4
  90. package/dist/lib/secrets/bundles.js +222 -54
  91. package/dist/lib/secrets/index.d.ts +24 -5
  92. package/dist/lib/secrets/index.js +70 -41
  93. package/dist/lib/session/active.js +5 -5
  94. package/dist/lib/session/db.js +4 -4
  95. package/dist/lib/session/discover.js +2 -2
  96. package/dist/lib/session/render.js +21 -7
  97. package/dist/lib/shims.d.ts +28 -4
  98. package/dist/lib/shims.js +72 -14
  99. package/dist/lib/state.d.ts +22 -28
  100. package/dist/lib/state.js +83 -78
  101. package/dist/lib/sync-manifest.d.ts +2 -2
  102. package/dist/lib/sync-manifest.js +5 -5
  103. package/dist/lib/teams/agents.d.ts +4 -2
  104. package/dist/lib/teams/agents.js +11 -4
  105. package/dist/lib/teams/api.d.ts +1 -1
  106. package/dist/lib/teams/api.js +2 -2
  107. package/dist/lib/teams/index.d.ts +1 -0
  108. package/dist/lib/teams/index.js +1 -0
  109. package/dist/lib/teams/persistence.js +3 -3
  110. package/dist/lib/teams/registry.d.ts +12 -1
  111. package/dist/lib/teams/registry.js +12 -2
  112. package/dist/lib/teams/worktree.d.ts +30 -0
  113. package/dist/lib/teams/worktree.js +96 -0
  114. package/dist/lib/types.d.ts +12 -6
  115. package/dist/lib/types.js +3 -3
  116. package/dist/lib/versions.d.ts +32 -3
  117. package/dist/lib/versions.js +147 -119
  118. package/package.json +3 -2
  119. package/scripts/postinstall.js +29 -0
  120. package/dist/commands/refresh-memory.d.ts +0 -15
  121. package/dist/lib/memory-compile.d.ts +0 -66
@@ -0,0 +1,184 @@
1
+ /**
2
+ * PermissionsHandler - ResourceHandler implementation for permissions.
3
+ *
4
+ * Permissions are stored as YAML files in permissions/ directories at each layer.
5
+ * Resolution: project > user > system (higher layer wins on name conflict).
6
+ * Unlike other resources, permissions merge into agent-specific config files
7
+ * (Claude: settings.json, Codex: config.toml, OpenCode: opencode.jsonc).
8
+ */
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+ import { getSystemAgentsDir, getUserAgentsDir, getProjectAgentsDir, getEnabledExtraRepos, } from '../state.js';
12
+ import { parsePermissionSet, applyPermissionsToVersion, mergePermissionSets, PERMISSIONS_CAPABLE_AGENTS, } from '../permissions.js';
13
+ /**
14
+ * Get the permissions directory for a given layer root.
15
+ */
16
+ function getPermissionsDirForRoot(root) {
17
+ return path.join(root, 'permissions');
18
+ }
19
+ /**
20
+ * Get layer directories for permission resolution.
21
+ */
22
+ function getLayerDirs(cwd) {
23
+ return {
24
+ system: getSystemAgentsDir(),
25
+ user: getUserAgentsDir(),
26
+ project: cwd ? getProjectAgentsDir(cwd) : null,
27
+ extra: getEnabledExtraRepos().map((e) => e.dir),
28
+ };
29
+ }
30
+ /**
31
+ * List permission files in a directory.
32
+ * Returns only YAML files, stripping the extension for the name.
33
+ */
34
+ function listPermissionsInDir(dir) {
35
+ if (!fs.existsSync(dir))
36
+ return [];
37
+ try {
38
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
39
+ const permissions = [];
40
+ for (const entry of entries) {
41
+ if (!entry.isFile())
42
+ continue;
43
+ if (entry.name.startsWith('.'))
44
+ continue;
45
+ if (entry.name.endsWith('.yaml') || entry.name.endsWith('.yml')) {
46
+ permissions.push({
47
+ name: entry.name.replace(/\.(yaml|yml)$/, ''),
48
+ path: path.join(dir, entry.name),
49
+ });
50
+ }
51
+ }
52
+ return permissions;
53
+ }
54
+ catch {
55
+ return [];
56
+ }
57
+ }
58
+ /**
59
+ * Get the config file path for an agent's permissions.
60
+ */
61
+ function getAgentConfigPath(agent, versionHome) {
62
+ switch (agent) {
63
+ case 'claude':
64
+ return path.join(versionHome, '.claude', 'settings.json');
65
+ case 'codex':
66
+ return path.join(versionHome, '.codex', 'config.toml');
67
+ case 'opencode':
68
+ return path.join(versionHome, '.opencode', 'opencode.jsonc');
69
+ default:
70
+ return null;
71
+ }
72
+ }
73
+ export const PermissionsHandler = {
74
+ kind: 'permission',
75
+ /**
76
+ * List all permissions across layers, with higher layer winning on name conflict.
77
+ * Returns a union of all permissions, deduplicated by name.
78
+ */
79
+ listAll(agent, cwd) {
80
+ const layers = getLayerDirs(cwd);
81
+ const seen = new Set();
82
+ const results = [];
83
+ // Build layer roots in precedence order: project > user > system > extras
84
+ const roots = [];
85
+ if (layers.project) {
86
+ roots.push({ dir: getPermissionsDirForRoot(layers.project), layer: 'project' });
87
+ }
88
+ roots.push({ dir: getPermissionsDirForRoot(layers.user), layer: 'user' });
89
+ roots.push({ dir: getPermissionsDirForRoot(layers.system), layer: 'system' });
90
+ for (const extraDir of layers.extra) {
91
+ roots.push({ dir: getPermissionsDirForRoot(extraDir), layer: 'system' });
92
+ }
93
+ for (const { dir, layer } of roots) {
94
+ const permissions = listPermissionsInDir(dir);
95
+ for (const perm of permissions) {
96
+ if (seen.has(perm.name))
97
+ continue;
98
+ seen.add(perm.name);
99
+ const item = parsePermissionSet(perm.path);
100
+ if (item) {
101
+ results.push({
102
+ name: perm.name,
103
+ item,
104
+ layer,
105
+ path: perm.path,
106
+ });
107
+ }
108
+ }
109
+ }
110
+ return results.sort((a, b) => a.name.localeCompare(b.name));
111
+ },
112
+ /**
113
+ * Resolve a single permission by name.
114
+ * Returns the winning layer's version, or null if not found.
115
+ */
116
+ resolve(agent, name, cwd) {
117
+ const layers = getLayerDirs(cwd);
118
+ // Build candidate paths in precedence order: project > user > system > extras
119
+ const candidates = [];
120
+ if (layers.project) {
121
+ candidates.push({ dir: getPermissionsDirForRoot(layers.project), layer: 'project' });
122
+ }
123
+ candidates.push({ dir: getPermissionsDirForRoot(layers.user), layer: 'user' });
124
+ candidates.push({ dir: getPermissionsDirForRoot(layers.system), layer: 'system' });
125
+ for (const extraDir of layers.extra) {
126
+ candidates.push({ dir: getPermissionsDirForRoot(extraDir), layer: 'system' });
127
+ }
128
+ for (const { dir, layer } of candidates) {
129
+ // Try .yaml first, then .yml
130
+ for (const ext of ['.yaml', '.yml']) {
131
+ const filePath = path.join(dir, `${name}${ext}`);
132
+ const item = parsePermissionSet(filePath);
133
+ if (item) {
134
+ return { name, item, layer, path: filePath };
135
+ }
136
+ }
137
+ }
138
+ return null;
139
+ },
140
+ /**
141
+ * Sync resolved permissions to the agent's version home config file.
142
+ * Merges all resolved permissions into a single set and applies to the agent's config.
143
+ */
144
+ sync(agent, versionHome, cwd) {
145
+ // Only sync to agents that support permissions
146
+ if (!PERMISSIONS_CAPABLE_AGENTS.includes(agent)) {
147
+ return;
148
+ }
149
+ const resolved = this.listAll(agent, cwd);
150
+ if (resolved.length === 0)
151
+ return;
152
+ // Merge all permission sets into one
153
+ let merged = {
154
+ name: 'merged',
155
+ description: 'Merged from all layers',
156
+ allow: [],
157
+ deny: [],
158
+ };
159
+ for (const r of resolved) {
160
+ merged = mergePermissionSets(merged, r.item);
161
+ }
162
+ // Apply to the agent's config file
163
+ applyPermissionsToVersion(agent, merged, versionHome, true);
164
+ },
165
+ /**
166
+ * Permissions use YAML format.
167
+ */
168
+ format(_agent) {
169
+ return 'yaml';
170
+ },
171
+ /**
172
+ * Permissions directory name.
173
+ */
174
+ targetDir(_agent) {
175
+ return 'permissions';
176
+ },
177
+ /**
178
+ * Return the config file path where permissions are merged.
179
+ */
180
+ configPath(agent, versionHome) {
181
+ return getAgentConfigPath(agent, versionHome);
182
+ },
183
+ };
184
+ export default PermissionsHandler;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Rules resource handler.
3
+ *
4
+ * Rules are .md files in `<repo>/rules/subrules/` directories that get composed
5
+ * into the agent's instructions file (AGENTS.md -> CLAUDE.md/GEMINI.md/etc).
6
+ *
7
+ * Layer resolution: project > user > extras > system
8
+ * - Same-named subrule at higher layer wins (override)
9
+ * - All unique subrules across layers are unioned
10
+ */
11
+ import type { Layer, ResolvedItem, ResourceHandler } from './types.js';
12
+ /** A rule item is a subrule markdown fragment. */
13
+ export interface RuleItem {
14
+ /** The subrule name (without .md extension). */
15
+ name: string;
16
+ /** The full content of the rule file. */
17
+ content: string;
18
+ }
19
+ /** Layer directory entry for rules resolution. */
20
+ export interface RulesLayerDir {
21
+ layer: Layer;
22
+ dir: string;
23
+ }
24
+ /**
25
+ * List subrule markdown files in a directory.
26
+ * Returns names without the .md extension.
27
+ */
28
+ export declare function listSubrulesInDir(subrulesDir: string): string[];
29
+ /**
30
+ * Get layer directories for rules resolution using production paths.
31
+ */
32
+ export declare function getLayerDirs(cwd?: string): RulesLayerDir[];
33
+ /**
34
+ * List all rules from the given layer directories.
35
+ * Higher layers win on name collision.
36
+ */
37
+ export declare function listAllRules(layers: RulesLayerDir[]): ResolvedItem<RuleItem>[];
38
+ /**
39
+ * Resolve a single rule by name from the given layer directories.
40
+ * Higher layers win.
41
+ */
42
+ export declare function resolveRule(name: string, layers: RulesLayerDir[]): ResolvedItem<RuleItem> | null;
43
+ export declare const RulesHandler: ResourceHandler<RuleItem>;
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Rules resource handler.
3
+ *
4
+ * Rules are .md files in `<repo>/rules/subrules/` directories that get composed
5
+ * into the agent's instructions file (AGENTS.md -> CLAUDE.md/GEMINI.md/etc).
6
+ *
7
+ * Layer resolution: project > user > extras > system
8
+ * - Same-named subrule at higher layer wins (override)
9
+ * - All unique subrules across layers are unioned
10
+ */
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
13
+ import { getSystemRulesDir, getUserRulesDir, getProjectAgentsDir, getEnabledExtraRepos, } from '../state.js';
14
+ const SUBRULES_DIR = 'subrules';
15
+ const SUBRULES_README = 'README.md';
16
+ /**
17
+ * List subrule markdown files in a directory.
18
+ * Returns names without the .md extension.
19
+ */
20
+ export function listSubrulesInDir(subrulesDir) {
21
+ if (!fs.existsSync(subrulesDir))
22
+ return [];
23
+ try {
24
+ return fs
25
+ .readdirSync(subrulesDir)
26
+ .filter((f) => f.endsWith('.md') && f !== SUBRULES_README)
27
+ .map((f) => f.slice(0, -3))
28
+ .sort();
29
+ }
30
+ catch {
31
+ return [];
32
+ }
33
+ }
34
+ /**
35
+ * Get layer directories for rules resolution using production paths.
36
+ */
37
+ export function getLayerDirs(cwd) {
38
+ const dirs = [];
39
+ // Project layer (highest priority)
40
+ const projectDir = getProjectAgentsDir(cwd);
41
+ if (projectDir) {
42
+ const rulesDir = path.join(projectDir, 'rules');
43
+ if (fs.existsSync(rulesDir)) {
44
+ dirs.push({ layer: 'project', dir: rulesDir });
45
+ }
46
+ }
47
+ // User layer
48
+ const userDir = getUserRulesDir();
49
+ if (fs.existsSync(userDir)) {
50
+ dirs.push({ layer: 'user', dir: userDir });
51
+ }
52
+ // Extra repos (treated as user-level in layering)
53
+ for (const extra of getEnabledExtraRepos()) {
54
+ const rulesDir = path.join(extra.dir, 'rules');
55
+ if (fs.existsSync(rulesDir)) {
56
+ dirs.push({ layer: 'user', dir: rulesDir });
57
+ }
58
+ }
59
+ // System layer (lowest priority)
60
+ const systemDir = getSystemRulesDir();
61
+ if (fs.existsSync(systemDir)) {
62
+ dirs.push({ layer: 'system', dir: systemDir });
63
+ }
64
+ return dirs;
65
+ }
66
+ /**
67
+ * List all rules from the given layer directories.
68
+ * Higher layers win on name collision.
69
+ */
70
+ export function listAllRules(layers) {
71
+ const seen = new Set();
72
+ const results = [];
73
+ for (const { layer, dir } of layers) {
74
+ const subrulesDir = path.join(dir, SUBRULES_DIR);
75
+ for (const name of listSubrulesInDir(subrulesDir)) {
76
+ if (seen.has(name))
77
+ continue;
78
+ seen.add(name);
79
+ const filePath = path.join(subrulesDir, `${name}.md`);
80
+ let content = '';
81
+ try {
82
+ content = fs.readFileSync(filePath, 'utf-8');
83
+ }
84
+ catch {
85
+ continue;
86
+ }
87
+ results.push({
88
+ name,
89
+ item: { name, content },
90
+ layer,
91
+ path: filePath,
92
+ });
93
+ }
94
+ }
95
+ return results;
96
+ }
97
+ /**
98
+ * Resolve a single rule by name from the given layer directories.
99
+ * Higher layers win.
100
+ */
101
+ export function resolveRule(name, layers) {
102
+ for (const { layer, dir } of layers) {
103
+ const filePath = path.join(dir, SUBRULES_DIR, `${name}.md`);
104
+ if (fs.existsSync(filePath)) {
105
+ let content = '';
106
+ try {
107
+ content = fs.readFileSync(filePath, 'utf-8');
108
+ }
109
+ catch {
110
+ continue;
111
+ }
112
+ return {
113
+ name,
114
+ item: { name, content },
115
+ layer,
116
+ path: filePath,
117
+ };
118
+ }
119
+ }
120
+ return null;
121
+ }
122
+ export const RulesHandler = {
123
+ kind: 'rule',
124
+ listAll(_agent, cwd) {
125
+ return listAllRules(getLayerDirs(cwd));
126
+ },
127
+ resolve(_agent, name, cwd) {
128
+ return resolveRule(name, getLayerDirs(cwd));
129
+ },
130
+ sync(agent, versionHome, _cwd) {
131
+ // Rules sync is handled by the compose module and syncResourcesToVersion.
132
+ // This method ensures the agent config directory exists.
133
+ // The actual composition and write happens in versions.ts via composeRulesFromState().
134
+ const targetDir = path.join(versionHome, `.${agent}`);
135
+ if (!fs.existsSync(targetDir)) {
136
+ fs.mkdirSync(targetDir, { recursive: true });
137
+ }
138
+ },
139
+ format(_agent) {
140
+ return 'md';
141
+ },
142
+ targetDir(agent) {
143
+ // Rules don't have a target subdirectory - they're written to the instructions file
144
+ return `.${agent}`;
145
+ },
146
+ };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Skills resource handler.
3
+ *
4
+ * Skills are directory bundles with a SKILL.md containing YAML frontmatter.
5
+ * Format is the same for all agents. Resolution order: project > user > system.
6
+ */
7
+ import type { ResourceHandler } from './types.js';
8
+ import type { SkillMetadata } from '../types.js';
9
+ /**
10
+ * Layer directory provider for dependency injection in tests.
11
+ */
12
+ export interface LayerDirProvider {
13
+ getSystemSkillsDir(): string;
14
+ getUserSkillsDir(): string;
15
+ getProjectAgentsDir(cwd?: string): string | null;
16
+ getEnabledExtraRepos(): Array<{
17
+ alias: string;
18
+ dir: string;
19
+ url: string;
20
+ }>;
21
+ }
22
+ /** A resolved skill item with parsed metadata. */
23
+ export interface SkillItem {
24
+ /** Parsed SKILL.md frontmatter metadata. */
25
+ metadata: SkillMetadata;
26
+ /** Number of rule files in the skill's rules/ subdirectory. */
27
+ ruleCount: number;
28
+ /** Number of additional bundled files (beyond SKILL.md). */
29
+ fileCount: number;
30
+ }
31
+ /**
32
+ * Create a SkillsHandler with the given layer directory provider.
33
+ * Useful for testing with custom directory structures.
34
+ */
35
+ export declare function createSkillsHandler(provider?: LayerDirProvider): ResourceHandler<SkillItem>;
36
+ /** Default handler using real state module paths. */
37
+ export declare const SkillsHandler: ResourceHandler<SkillItem>;
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Skills resource handler.
3
+ *
4
+ * Skills are directory bundles with a SKILL.md containing YAML frontmatter.
5
+ * Format is the same for all agents. Resolution order: project > user > system.
6
+ */
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import * as yaml from 'yaml';
10
+ import { getSystemSkillsDir, getUserSkillsDir, getProjectAgentsDir, getEnabledExtraRepos, } from '../state.js';
11
+ /** Default provider uses the real state module. */
12
+ const defaultProvider = {
13
+ getSystemSkillsDir,
14
+ getUserSkillsDir,
15
+ getProjectAgentsDir,
16
+ getEnabledExtraRepos,
17
+ };
18
+ /**
19
+ * Parse SKILL.md frontmatter to extract skill metadata.
20
+ * Returns null if the file doesn't exist or has no valid frontmatter.
21
+ */
22
+ function parseSkillMetadata(skillDir) {
23
+ const skillMdPath = path.join(skillDir, 'SKILL.md');
24
+ if (!fs.existsSync(skillMdPath)) {
25
+ return null;
26
+ }
27
+ try {
28
+ const content = fs.readFileSync(skillMdPath, 'utf-8');
29
+ const lines = content.split('\n');
30
+ // Check for YAML frontmatter (required)
31
+ if (lines[0] === '---') {
32
+ const endIndex = lines.slice(1).findIndex((l) => l === '---');
33
+ if (endIndex > 0) {
34
+ const frontmatter = lines.slice(1, endIndex + 1).join('\n');
35
+ const parsed = yaml.parse(frontmatter);
36
+ return {
37
+ name: parsed.name || '',
38
+ description: parsed.description || '',
39
+ author: parsed.author,
40
+ version: parsed.version,
41
+ license: parsed.license,
42
+ keywords: parsed.keywords,
43
+ };
44
+ }
45
+ }
46
+ return null;
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ }
52
+ /**
53
+ * Count .md files in the skill's rules/ subdirectory.
54
+ */
55
+ function countSkillRules(skillDir) {
56
+ const rulesDir = path.join(skillDir, 'rules');
57
+ if (!fs.existsSync(rulesDir)) {
58
+ return 0;
59
+ }
60
+ try {
61
+ const files = fs.readdirSync(rulesDir);
62
+ return files.filter((f) => f.endsWith('.md')).length;
63
+ }
64
+ catch {
65
+ return 0;
66
+ }
67
+ }
68
+ /**
69
+ * Count bundled resource files in a skill directory (everything except SKILL.md).
70
+ */
71
+ function countSkillFiles(skillDir) {
72
+ if (!fs.existsSync(skillDir))
73
+ return 0;
74
+ let count = 0;
75
+ const walk = (dir) => {
76
+ let entries;
77
+ try {
78
+ entries = fs.readdirSync(dir, { withFileTypes: true });
79
+ }
80
+ catch {
81
+ return;
82
+ }
83
+ for (const entry of entries) {
84
+ if (entry.name.startsWith('.'))
85
+ continue;
86
+ const full = path.join(dir, entry.name);
87
+ if (entry.isDirectory()) {
88
+ walk(full);
89
+ }
90
+ else if (entry.isFile()) {
91
+ if (dir === skillDir && entry.name === 'SKILL.md')
92
+ continue;
93
+ count++;
94
+ }
95
+ }
96
+ };
97
+ walk(skillDir);
98
+ return count;
99
+ }
100
+ /**
101
+ * List skill directories in a given base path.
102
+ * Returns array of { name, fullPath } for directories containing SKILL.md.
103
+ */
104
+ function listSkillsInDir(baseDir) {
105
+ if (!fs.existsSync(baseDir))
106
+ return [];
107
+ const results = [];
108
+ try {
109
+ const entries = fs.readdirSync(baseDir, { withFileTypes: true });
110
+ for (const entry of entries) {
111
+ if (!entry.isDirectory() || entry.name.startsWith('.'))
112
+ continue;
113
+ const fullPath = path.join(baseDir, entry.name);
114
+ const skillMdPath = path.join(fullPath, 'SKILL.md');
115
+ if (fs.existsSync(skillMdPath)) {
116
+ results.push({ name: entry.name, fullPath });
117
+ }
118
+ }
119
+ }
120
+ catch {
121
+ // Ignore errors
122
+ }
123
+ return results;
124
+ }
125
+ /**
126
+ * Get layer directories for skill resolution.
127
+ */
128
+ function getLayerDirs(cwd, provider = defaultProvider) {
129
+ const layers = [];
130
+ // Project layer
131
+ const projectDir = cwd ? provider.getProjectAgentsDir(cwd) : null;
132
+ if (projectDir) {
133
+ layers.push({ layer: 'project', dir: path.join(projectDir, 'skills') });
134
+ }
135
+ // User layer
136
+ layers.push({ layer: 'user', dir: provider.getUserSkillsDir() });
137
+ // Extra repos (treated as user layer since they're user-configured)
138
+ for (const extra of provider.getEnabledExtraRepos()) {
139
+ layers.push({ layer: 'user', dir: path.join(extra.dir, 'skills') });
140
+ }
141
+ // System layer
142
+ layers.push({ layer: 'system', dir: provider.getSystemSkillsDir() });
143
+ return layers;
144
+ }
145
+ /**
146
+ * Create a SkillsHandler with the given layer directory provider.
147
+ * Useful for testing with custom directory structures.
148
+ */
149
+ export function createSkillsHandler(provider = defaultProvider) {
150
+ return {
151
+ kind: 'skill',
152
+ listAll(_agent, cwd) {
153
+ const seen = new Map();
154
+ const layers = getLayerDirs(cwd, provider);
155
+ // Process in order: project > user > system
156
+ // First occurrence wins (higher layer takes precedence)
157
+ for (const { layer, dir } of layers) {
158
+ const skills = listSkillsInDir(dir);
159
+ for (const { name, fullPath } of skills) {
160
+ if (seen.has(name))
161
+ continue; // Higher layer already has this skill
162
+ const metadata = parseSkillMetadata(fullPath);
163
+ if (!metadata)
164
+ continue; // Skip invalid skills
165
+ seen.set(name, {
166
+ name,
167
+ item: {
168
+ metadata,
169
+ ruleCount: countSkillRules(fullPath),
170
+ fileCount: countSkillFiles(fullPath),
171
+ },
172
+ layer,
173
+ path: fullPath,
174
+ });
175
+ }
176
+ }
177
+ return Array.from(seen.values()).sort((a, b) => a.name.localeCompare(b.name));
178
+ },
179
+ resolve(_agent, name, cwd) {
180
+ const layers = getLayerDirs(cwd, provider);
181
+ for (const { layer, dir } of layers) {
182
+ const skillPath = path.join(dir, name);
183
+ const skillMdPath = path.join(skillPath, 'SKILL.md');
184
+ if (fs.existsSync(skillMdPath)) {
185
+ const metadata = parseSkillMetadata(skillPath);
186
+ if (!metadata)
187
+ continue;
188
+ return {
189
+ name,
190
+ item: {
191
+ metadata,
192
+ ruleCount: countSkillRules(skillPath),
193
+ fileCount: countSkillFiles(skillPath),
194
+ },
195
+ layer,
196
+ path: skillPath,
197
+ };
198
+ }
199
+ }
200
+ return null;
201
+ },
202
+ sync(agent, versionHome, cwd) {
203
+ const targetDir = path.join(versionHome, `.${agent}`, 'skills');
204
+ // Ensure target directory exists
205
+ if (!fs.existsSync(targetDir)) {
206
+ fs.mkdirSync(targetDir, { recursive: true });
207
+ }
208
+ const resolved = this.listAll(agent, cwd);
209
+ for (const skill of resolved) {
210
+ const targetPath = path.join(targetDir, skill.name);
211
+ // Remove existing if present
212
+ if (fs.existsSync(targetPath) || fs.lstatSync(targetPath, { throwIfNoEntry: false })) {
213
+ try {
214
+ fs.rmSync(targetPath, { recursive: true, force: true });
215
+ }
216
+ catch {
217
+ // Ignore removal errors
218
+ }
219
+ }
220
+ // Copy skill directory
221
+ try {
222
+ fs.cpSync(skill.path, targetPath, { recursive: true });
223
+ }
224
+ catch {
225
+ // Ignore copy errors
226
+ }
227
+ }
228
+ },
229
+ format(_agent) {
230
+ return 'md';
231
+ },
232
+ targetDir(agent) {
233
+ return `.${agent}/skills`;
234
+ },
235
+ };
236
+ }
237
+ /** Default handler using real state module paths. */
238
+ export const SkillsHandler = createSkillsHandler();
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Subagents resource handler.
3
+ *
4
+ * Subagents are YAML files stored in subagents/ directories across layers.
5
+ * Format is the same for all agents. Resolution order: project > user > system.
6
+ */
7
+ import type { AgentId, ResolvedItem, ResourceHandler } from './types.js';
8
+ /** Parsed content of a subagent YAML file. */
9
+ export interface SubagentItem {
10
+ name: string;
11
+ description: string;
12
+ model?: string;
13
+ /** Hex color for UI display. */
14
+ color?: string;
15
+ /** Additional agent-specific config. */
16
+ config?: Record<string, unknown>;
17
+ }
18
+ export declare class SubagentsHandler implements ResourceHandler<SubagentItem> {
19
+ readonly kind: "subagent";
20
+ /**
21
+ * List all subagents across layers, with higher layer winning on name conflict.
22
+ * Returns a union of all subagents, deduplicated by name.
23
+ */
24
+ listAll(_agent: AgentId, cwd?: string): ResolvedItem<SubagentItem>[];
25
+ /**
26
+ * Resolve a single subagent by name.
27
+ * Returns the winning layer's version, or null if not found.
28
+ */
29
+ resolve(_agent: AgentId, name: string, cwd?: string): ResolvedItem<SubagentItem> | null;
30
+ /**
31
+ * Sync resolved subagents to the agent's version home directory.
32
+ * Copies YAML files to the target directory.
33
+ */
34
+ sync(agent: AgentId, versionHome: string, cwd?: string): void;
35
+ /**
36
+ * Get the file format this resource uses for a given agent.
37
+ * Subagents always use YAML format.
38
+ */
39
+ format(_agent: AgentId): 'md' | 'toml' | 'json' | 'yaml';
40
+ /**
41
+ * Get the target directory name in the agent's version home.
42
+ */
43
+ targetDir(_agent: AgentId): string;
44
+ }
45
+ /** Singleton instance of the SubagentsHandler. */
46
+ export declare const subagentsHandler: SubagentsHandler;