universal-dev-standards 4.1.0 → 4.2.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 (46) hide show
  1. package/bin/uds.js +75 -0
  2. package/bundled/core/ai-friendly-architecture.md +542 -0
  3. package/bundled/locales/zh-CN/README.md +210 -509
  4. package/bundled/locales/zh-CN/core/ai-friendly-architecture.md +306 -0
  5. package/bundled/locales/zh-CN/docs/AI-AGENT-ROADMAP.md +82 -22
  6. package/bundled/locales/zh-CN/integrations/gemini-cli/GEMINI.md +35 -3
  7. package/bundled/locales/zh-CN/integrations/github-copilot/COPILOT-CHAT-REFERENCE.md +89 -3
  8. package/bundled/locales/zh-CN/integrations/github-copilot/skills-mapping.md +8 -4
  9. package/bundled/locales/zh-TW/README.md +211 -490
  10. package/bundled/locales/zh-TW/core/ai-friendly-architecture.md +306 -0
  11. package/bundled/locales/zh-TW/docs/AI-AGENT-ROADMAP.md +82 -22
  12. package/bundled/locales/zh-TW/integrations/gemini-cli/GEMINI.md +35 -3
  13. package/bundled/locales/zh-TW/integrations/github-copilot/COPILOT-CHAT-REFERENCE.md +89 -3
  14. package/bundled/locales/zh-TW/integrations/github-copilot/skills-mapping.md +8 -4
  15. package/bundled/skills/claude-code/README.md +8 -0
  16. package/bundled/skills/claude-code/agents/README.md +305 -0
  17. package/bundled/skills/claude-code/agents/code-architect.md +259 -0
  18. package/bundled/skills/claude-code/agents/doc-writer.md +406 -0
  19. package/bundled/skills/claude-code/agents/reviewer.md +353 -0
  20. package/bundled/skills/claude-code/agents/spec-analyst.md +374 -0
  21. package/bundled/skills/claude-code/agents/test-specialist.md +364 -0
  22. package/bundled/skills/claude-code/workflows/README.md +303 -0
  23. package/bundled/skills/claude-code/workflows/code-review.workflow.yaml +186 -0
  24. package/bundled/skills/claude-code/workflows/feature-dev.workflow.yaml +174 -0
  25. package/bundled/skills/claude-code/workflows/integrated-flow.workflow.yaml +238 -0
  26. package/bundled/skills/claude-code/workflows/large-codebase-analysis.workflow.yaml +226 -0
  27. package/package.json +11 -1
  28. package/src/commands/agent.js +417 -0
  29. package/src/commands/ai-context.js +552 -0
  30. package/src/commands/check.js +3 -3
  31. package/src/commands/init.js +6 -3
  32. package/src/commands/workflow.js +425 -0
  33. package/src/config/ai-agent-paths.js +217 -13
  34. package/src/core/constants.js +514 -0
  35. package/src/core/errors.js +398 -0
  36. package/src/core/manifest.js +473 -0
  37. package/src/core/paths.js +398 -0
  38. package/src/prompts/init.js +7 -5
  39. package/src/utils/agent-adapter.js +320 -0
  40. package/src/utils/agents-installer.js +393 -0
  41. package/src/utils/context-chunker.js +467 -0
  42. package/src/utils/copier.js +59 -99
  43. package/src/utils/hasher.js +2 -16
  44. package/src/utils/integration-generator.js +28 -52
  45. package/src/utils/workflows-installer.js +545 -0
  46. package/standards-registry.json +166 -20
@@ -0,0 +1,320 @@
1
+ /**
2
+ * Agent Adapter
3
+ *
4
+ * Provides cross-tool adaptation for AGENT.md definitions.
5
+ * Adapts agents for different execution modes based on AI tool capabilities.
6
+ *
7
+ * @version 1.0.0
8
+ */
9
+
10
+ import { supportsTask, supportsAgents, getAgentConfig } from '../config/ai-agent-paths.js';
11
+
12
+ /**
13
+ * Execution modes for agents
14
+ */
15
+ export const ExecutionMode = {
16
+ TASK: 'task', // Use Task tool to spawn independent subagent (Claude Code, OpenCode)
17
+ INLINE: 'inline', // Inject agent content as context prefix (Cursor, Windsurf, etc.)
18
+ MANUAL: 'manual' // Generate manual instructions (unsupported tools)
19
+ };
20
+
21
+ /**
22
+ * Determine the execution mode for an agent on a specific AI tool
23
+ * @param {string} aiTool - AI tool identifier
24
+ * @returns {string} Execution mode
25
+ */
26
+ export function getExecutionMode(aiTool) {
27
+ if (supportsTask(aiTool)) {
28
+ return ExecutionMode.TASK;
29
+ }
30
+ if (supportsAgents(aiTool)) {
31
+ return ExecutionMode.INLINE;
32
+ }
33
+ return ExecutionMode.MANUAL;
34
+ }
35
+
36
+ /**
37
+ * Adapt agent configuration for a specific AI tool
38
+ * @param {Object} agentConfig - Parsed agent configuration
39
+ * @param {string} aiTool - Target AI tool identifier
40
+ * @returns {Object} Adapted configuration
41
+ */
42
+ export function adaptAgentForTool(agentConfig, aiTool) {
43
+ const mode = getExecutionMode(aiTool);
44
+
45
+ switch (mode) {
46
+ case ExecutionMode.TASK:
47
+ return adaptForTaskMode(agentConfig, aiTool);
48
+ case ExecutionMode.INLINE:
49
+ return adaptForInlineMode(agentConfig, aiTool);
50
+ case ExecutionMode.MANUAL:
51
+ return adaptForManualMode(agentConfig, aiTool);
52
+ default:
53
+ return { mode: ExecutionMode.MANUAL, error: `Unknown mode for ${aiTool}` };
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Adapt agent for Task tool execution mode
59
+ * @param {Object} agentConfig - Agent configuration
60
+ * @param {string} aiTool - AI tool identifier
61
+ * @returns {Object} Task mode configuration
62
+ */
63
+ function adaptForTaskMode(agentConfig, aiTool) {
64
+ const taskConfig = {
65
+ subagent_type: agentConfig.name || 'general-purpose',
66
+ description: truncateDescription(agentConfig.description),
67
+ model: agentConfig.model || 'sonnet',
68
+ };
69
+
70
+ // Map allowed/disallowed tools to Task tool format
71
+ if (agentConfig['allowed-tools']) {
72
+ taskConfig.allowedTools = parseToolList(agentConfig['allowed-tools']);
73
+ }
74
+ if (agentConfig['disallowed-tools']) {
75
+ taskConfig.disallowedTools = parseToolList(agentConfig['disallowed-tools']);
76
+ }
77
+
78
+ // Add skill dependencies as context
79
+ if (agentConfig.skills && agentConfig.skills.length > 0) {
80
+ taskConfig.skillDependencies = agentConfig.skills;
81
+ }
82
+
83
+ return {
84
+ mode: ExecutionMode.TASK,
85
+ aiTool,
86
+ taskConfig,
87
+ triggers: agentConfig.triggers || {}
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Adapt agent for inline context injection mode
93
+ * @param {Object} agentConfig - Agent configuration
94
+ * @param {string} aiTool - AI tool identifier
95
+ * @returns {Object} Inline mode configuration
96
+ */
97
+ function adaptForInlineMode(agentConfig, aiTool) {
98
+ // Generate context prefix for tools that don't support Task tool
99
+ const contextPrefix = generateInlineContextPrefix(agentConfig);
100
+
101
+ return {
102
+ mode: ExecutionMode.INLINE,
103
+ aiTool,
104
+ contextPrefix,
105
+ triggers: agentConfig.triggers || {},
106
+ note: 'This tool does not support subagent execution. Agent will be injected as context.'
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Adapt agent for manual instruction mode
112
+ * @param {Object} agentConfig - Agent configuration
113
+ * @param {string} aiTool - AI tool identifier
114
+ * @returns {Object} Manual mode configuration
115
+ */
116
+ function adaptForManualMode(agentConfig, aiTool) {
117
+ const instructions = generateManualInstructions(agentConfig);
118
+
119
+ return {
120
+ mode: ExecutionMode.MANUAL,
121
+ aiTool,
122
+ instructions,
123
+ note: 'This tool does not support agents. Use these instructions manually.'
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Generate inline context prefix for injection mode
129
+ * @param {Object} agentConfig - Agent configuration
130
+ * @returns {string} Context prefix
131
+ */
132
+ function generateInlineContextPrefix(agentConfig) {
133
+ const lines = [
134
+ `<!-- UDS Agent: ${agentConfig.name} -->`,
135
+ `<!-- Role: ${agentConfig.role || 'specialist'} -->`,
136
+ ''
137
+ ];
138
+
139
+ if (agentConfig.expertise && agentConfig.expertise.length > 0) {
140
+ lines.push(`## Agent Expertise: ${agentConfig.expertise.join(', ')}`);
141
+ lines.push('');
142
+ }
143
+
144
+ if (agentConfig.description) {
145
+ lines.push('## Agent Purpose');
146
+ lines.push(cleanDescription(agentConfig.description));
147
+ lines.push('');
148
+ }
149
+
150
+ // Add tool permission hints
151
+ if (agentConfig['allowed-tools']) {
152
+ const tools = parseToolList(agentConfig['allowed-tools']);
153
+ lines.push(`## Allowed Operations: ${tools.join(', ')}`);
154
+ }
155
+ if (agentConfig['disallowed-tools']) {
156
+ const tools = parseToolList(agentConfig['disallowed-tools']);
157
+ lines.push(`## Restricted Operations: ${tools.join(', ')}`);
158
+ }
159
+
160
+ lines.push('');
161
+ lines.push('---');
162
+ lines.push('');
163
+
164
+ return lines.join('\n');
165
+ }
166
+
167
+ /**
168
+ * Generate manual instructions for unsupported tools
169
+ * @param {Object} agentConfig - Agent configuration
170
+ * @returns {string} Manual instructions
171
+ */
172
+ function generateManualInstructions(agentConfig) {
173
+ const lines = [
174
+ `# ${agentConfig.name} Agent Instructions`,
175
+ '',
176
+ '## How to Use This Agent Manually',
177
+ '',
178
+ '1. Copy the agent content below into your AI assistant\'s context',
179
+ '2. Provide your task after the agent instructions',
180
+ '3. The AI will act according to the agent\'s role and expertise',
181
+ '',
182
+ '## Agent Information',
183
+ '',
184
+ `- **Name**: ${agentConfig.name}`,
185
+ `- **Role**: ${agentConfig.role || 'specialist'}`,
186
+ ];
187
+
188
+ if (agentConfig.expertise) {
189
+ lines.push(`- **Expertise**: ${agentConfig.expertise.join(', ')}`);
190
+ }
191
+
192
+ if (agentConfig.description) {
193
+ lines.push('');
194
+ lines.push('## Purpose');
195
+ lines.push(cleanDescription(agentConfig.description));
196
+ }
197
+
198
+ if (agentConfig.triggers && agentConfig.triggers.keywords) {
199
+ lines.push('');
200
+ lines.push('## Trigger Keywords');
201
+ lines.push(`Use this agent when: ${agentConfig.triggers.keywords.join(', ')}`);
202
+ }
203
+
204
+ return lines.join('\n');
205
+ }
206
+
207
+ /**
208
+ * Parse tool list from various formats
209
+ * @param {string|string[]} tools - Tool list
210
+ * @returns {string[]} Normalized tool list
211
+ */
212
+ function parseToolList(tools) {
213
+ if (Array.isArray(tools)) {
214
+ return tools;
215
+ }
216
+ if (typeof tools === 'string') {
217
+ // Handle comma-separated or bracket-enclosed format
218
+ return tools
219
+ .replace(/^\[|\]$/g, '')
220
+ .split(',')
221
+ .map(t => t.trim())
222
+ .filter(t => t.length > 0);
223
+ }
224
+ return [];
225
+ }
226
+
227
+ /**
228
+ * Truncate description for Task tool (short description)
229
+ * @param {string} description - Full description
230
+ * @returns {string} Truncated description (max 50 chars)
231
+ */
232
+ function truncateDescription(description) {
233
+ if (!description) return 'UDS Agent';
234
+ const cleaned = cleanDescription(description);
235
+ const firstLine = cleaned.split('\n')[0].trim();
236
+ if (firstLine.length <= 50) return firstLine;
237
+ return firstLine.substring(0, 47) + '...';
238
+ }
239
+
240
+ /**
241
+ * Clean description text
242
+ * @param {string} description - Description text
243
+ * @returns {string} Cleaned description
244
+ */
245
+ function cleanDescription(description) {
246
+ if (!description) return '';
247
+ return description
248
+ .replace(/^\|?\s*/, '') // Remove leading pipe and spaces (YAML multiline)
249
+ .replace(/\s+/g, ' ') // Normalize whitespace
250
+ .trim();
251
+ }
252
+
253
+ /**
254
+ * Get all supported AI tools and their execution modes
255
+ * @returns {Object[]} Array of tool info objects
256
+ */
257
+ export function getAllToolsWithModes() {
258
+ const tools = [
259
+ 'claude-code', 'opencode', 'cursor', 'cline',
260
+ 'roo-code', 'codex', 'copilot', 'windsurf', 'gemini-cli'
261
+ ];
262
+
263
+ return tools.map(tool => {
264
+ const config = getAgentConfig(tool);
265
+ return {
266
+ id: tool,
267
+ name: config?.name || tool,
268
+ mode: getExecutionMode(tool),
269
+ supportsTask: supportsTask(tool),
270
+ supportsAgents: supportsAgents(tool)
271
+ };
272
+ });
273
+ }
274
+
275
+ /**
276
+ * Check if an agent is compatible with a specific AI tool
277
+ * @param {Object} agentConfig - Agent configuration
278
+ * @param {string} aiTool - AI tool identifier
279
+ * @returns {Object} Compatibility info
280
+ */
281
+ export function checkAgentToolCompatibility(agentConfig, aiTool) {
282
+ const mode = getExecutionMode(aiTool);
283
+
284
+ const result = {
285
+ compatible: true,
286
+ mode,
287
+ warnings: [],
288
+ features: {
289
+ taskExecution: mode === ExecutionMode.TASK,
290
+ toolPermissions: mode === ExecutionMode.TASK,
291
+ skillDependencies: mode === ExecutionMode.TASK,
292
+ triggers: true
293
+ }
294
+ };
295
+
296
+ // Check tool permissions compatibility
297
+ if (agentConfig['allowed-tools'] && mode !== ExecutionMode.TASK) {
298
+ result.warnings.push('Tool permissions will be ignored (inline/manual mode)');
299
+ }
300
+
301
+ // Check model preference compatibility
302
+ if (agentConfig.model && mode !== ExecutionMode.TASK) {
303
+ result.warnings.push('Model preference will be ignored (inline/manual mode)');
304
+ }
305
+
306
+ // Check skill dependencies
307
+ if (agentConfig.skills && agentConfig.skills.length > 0 && mode !== ExecutionMode.TASK) {
308
+ result.warnings.push('Skill dependencies require manual loading (inline/manual mode)');
309
+ }
310
+
311
+ return result;
312
+ }
313
+
314
+ export default {
315
+ ExecutionMode,
316
+ getExecutionMode,
317
+ adaptAgentForTool,
318
+ getAllToolsWithModes,
319
+ checkAgentToolCompatibility
320
+ };
@@ -0,0 +1,393 @@
1
+ /**
2
+ * Agents Installer
3
+ *
4
+ * Provides installation and management of AGENT.md definitions
5
+ * across all supported AI coding assistants.
6
+ *
7
+ * @version 1.0.0
8
+ */
9
+
10
+ import { mkdirSync, writeFileSync, existsSync, readFileSync, readdirSync, statSync } from 'fs';
11
+ import { dirname, join, basename } from 'path';
12
+ import { fileURLToPath } from 'url';
13
+ import {
14
+ getAgentConfig,
15
+ getAgentsDirForAgent,
16
+ supportsAgents
17
+ } from '../config/ai-agent-paths.js';
18
+ import { computeFileHash } from './hasher.js';
19
+
20
+ // Get the CLI package root directory
21
+ const __filename = fileURLToPath(import.meta.url);
22
+ const __dirname = dirname(__filename);
23
+ const CLI_ROOT = join(__dirname, '..', '..');
24
+ const BUNDLED_DIR = join(CLI_ROOT, 'bundled');
25
+
26
+ /**
27
+ * Get the Agents source directory.
28
+ * Prioritizes bundled directory (npm install), falls back to development path.
29
+ * @returns {string} Path to agents source directory
30
+ */
31
+ function getAgentsSourceDir() {
32
+ const bundledPath = join(BUNDLED_DIR, 'skills', 'claude-code', 'agents');
33
+ if (existsSync(bundledPath)) {
34
+ return bundledPath;
35
+ }
36
+ // Development environment fallback
37
+ return join(CLI_ROOT, '..', 'skills', 'claude-code', 'agents');
38
+ }
39
+
40
+ const AGENTS_LOCAL_DIR = getAgentsSourceDir();
41
+
42
+ /**
43
+ * Get list of available agent names from local directory
44
+ * @returns {string[]} Array of agent names (without .md extension)
45
+ */
46
+ export function getAvailableAgentNames() {
47
+ if (!existsSync(AGENTS_LOCAL_DIR)) {
48
+ return [];
49
+ }
50
+
51
+ const NON_AGENT_ITEMS = ['README.md', '.DS_Store', '.manifest.json'];
52
+
53
+ try {
54
+ return readdirSync(AGENTS_LOCAL_DIR)
55
+ .filter(file => {
56
+ if (NON_AGENT_ITEMS.includes(file)) return false;
57
+ if (!file.endsWith('.md')) return false;
58
+ const itemPath = join(AGENTS_LOCAL_DIR, file);
59
+ return statSync(itemPath).isFile();
60
+ })
61
+ .map(file => basename(file, '.md'));
62
+ } catch {
63
+ return [];
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Get agent definition content
69
+ * @param {string} agentName - Agent name (without .md)
70
+ * @returns {string|null} Agent content or null if not found
71
+ */
72
+ export function getAgentContent(agentName) {
73
+ const sourcePath = join(AGENTS_LOCAL_DIR, `${agentName}.md`);
74
+ if (!existsSync(sourcePath)) {
75
+ return null;
76
+ }
77
+ return readFileSync(sourcePath, 'utf-8');
78
+ }
79
+
80
+ /**
81
+ * Parse agent frontmatter to extract metadata
82
+ * @param {string} content - Agent file content
83
+ * @returns {Object} Parsed frontmatter fields
84
+ */
85
+ export function parseAgentFrontmatter(content) {
86
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
87
+ if (!frontmatterMatch) {
88
+ return {};
89
+ }
90
+
91
+ const frontmatter = {};
92
+ const lines = frontmatterMatch[1].split('\n');
93
+
94
+ for (const line of lines) {
95
+ const match = line.match(/^(\w[\w-]*):\s*(.+)$/);
96
+ if (match) {
97
+ const [, key, value] = match;
98
+ // Handle arrays (simple case)
99
+ if (value.startsWith('[') && value.endsWith(']')) {
100
+ frontmatter[key] = value.slice(1, -1).split(',').map(s => s.trim().replace(/"/g, ''));
101
+ } else {
102
+ frontmatter[key] = value.trim();
103
+ }
104
+ }
105
+ }
106
+
107
+ return frontmatter;
108
+ }
109
+
110
+ /**
111
+ * Install agents for a specific AI tool
112
+ * @param {string} agent - AI tool identifier (e.g., 'claude-code', 'opencode')
113
+ * @param {string} level - 'user' or 'project'
114
+ * @param {string[]} agentNames - Array of agent names to install (null = all)
115
+ * @param {string} projectPath - Project root path (required for project level)
116
+ * @returns {Object} Installation result
117
+ */
118
+ export async function installAgentsForTool(agent, level, agentNames = null, projectPath = null) {
119
+ const config = getAgentConfig(agent);
120
+ if (!config || !supportsAgents(agent)) {
121
+ return {
122
+ success: false,
123
+ agent,
124
+ level,
125
+ error: `AI tool '${agent}' does not support agents installation`,
126
+ installed: [],
127
+ errors: []
128
+ };
129
+ }
130
+
131
+ // Get target directory
132
+ const targetDir = getAgentsDirForAgent(agent, level, projectPath);
133
+ if (!targetDir) {
134
+ return {
135
+ success: false,
136
+ agent,
137
+ level,
138
+ error: `Could not determine target directory for ${agent} at ${level} level`,
139
+ installed: [],
140
+ errors: []
141
+ };
142
+ }
143
+
144
+ // Ensure target directory exists
145
+ if (!existsSync(targetDir)) {
146
+ mkdirSync(targetDir, { recursive: true });
147
+ }
148
+
149
+ // Get agents to install
150
+ const availableAgents = getAvailableAgentNames();
151
+ const toInstall = agentNames || availableAgents;
152
+
153
+ const results = {
154
+ success: true,
155
+ agent,
156
+ level,
157
+ targetDir,
158
+ installed: [],
159
+ errors: [],
160
+ fileHashes: {}
161
+ };
162
+
163
+ for (const agentName of toInstall) {
164
+ const result = installSingleAgent(agentName, targetDir, agent);
165
+ if (result.success) {
166
+ results.installed.push(agentName);
167
+ } else {
168
+ results.errors.push({ agent: agentName, error: result.error });
169
+ results.success = false;
170
+ }
171
+ }
172
+
173
+ // Write manifest
174
+ if (results.installed.length > 0) {
175
+ writeAgentsManifest(agent, level, targetDir, results.installed);
176
+
177
+ // Compute file hashes for tracking
178
+ const now = new Date().toISOString();
179
+ for (const agentName of results.installed) {
180
+ const filePath = join(targetDir, `${agentName}.md`);
181
+ const hashInfo = computeFileHash(filePath);
182
+ if (hashInfo) {
183
+ results.fileHashes[`${agent}/${agentName}.md`] = {
184
+ ...hashInfo,
185
+ installedAt: now
186
+ };
187
+ }
188
+ }
189
+ }
190
+
191
+ return results;
192
+ }
193
+
194
+ /**
195
+ * Install a single agent to target directory
196
+ * @param {string} agentName - Agent name (without .md)
197
+ * @param {string} targetDir - Target directory
198
+ * @param {string} aiTool - AI tool identifier (for potential transformation)
199
+ * @returns {Object} Result
200
+ */
201
+ function installSingleAgent(agentName, targetDir, aiTool) {
202
+ const sourcePath = join(AGENTS_LOCAL_DIR, `${agentName}.md`);
203
+ const targetPath = join(targetDir, `${agentName}.md`);
204
+
205
+ if (!existsSync(sourcePath)) {
206
+ return {
207
+ success: false,
208
+ agent: agentName,
209
+ error: `Agent not found: ${agentName}`
210
+ };
211
+ }
212
+
213
+ try {
214
+ let content = readFileSync(sourcePath, 'utf-8');
215
+
216
+ // Transform content if needed for specific AI tools
217
+ content = transformAgentForTool(content, agentName, aiTool);
218
+
219
+ writeFileSync(targetPath, content);
220
+ return { success: true, agent: agentName, path: targetPath };
221
+ } catch (error) {
222
+ return {
223
+ success: false,
224
+ agent: agentName,
225
+ error: error.message
226
+ };
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Transform agent content for a specific AI tool if needed
232
+ * @param {string} content - Original agent content
233
+ * @param {string} agentName - Agent name
234
+ * @param {string} aiTool - AI tool identifier
235
+ * @returns {string} Transformed content
236
+ */
237
+ function transformAgentForTool(content, agentName, aiTool) {
238
+ // Claude Code and OpenCode use the same format (supports Task tool)
239
+ if (aiTool === 'claude-code' || aiTool === 'opencode' || aiTool === 'roo-code') {
240
+ return content;
241
+ }
242
+
243
+ // For tools that don't support Task tool, add inline mode header
244
+ // This helps the tool understand to inject the agent as context
245
+ const inlineModeHeader = `<!-- UDS Agent: ${agentName} -->
246
+ <!-- Execution Mode: inline (inject as context) -->
247
+
248
+ `;
249
+
250
+ // Check if content already has the header
251
+ if (content.includes('<!-- UDS Agent:')) {
252
+ return content;
253
+ }
254
+
255
+ return inlineModeHeader + content;
256
+ }
257
+
258
+ /**
259
+ * Write agents manifest
260
+ * @param {string} aiTool - AI tool identifier
261
+ * @param {string} level - 'user' or 'project'
262
+ * @param {string} targetDir - Target directory
263
+ * @param {string[]} agents - List of installed agents
264
+ */
265
+ function writeAgentsManifest(aiTool, level, targetDir, agents) {
266
+ const manifestPath = join(targetDir, '.manifest.json');
267
+ const { version } = JSON.parse(
268
+ readFileSync(join(CLI_ROOT, 'package.json'), 'utf-8')
269
+ );
270
+
271
+ const manifest = {
272
+ version,
273
+ source: 'universal-dev-standards',
274
+ type: 'agents',
275
+ aiTool,
276
+ level,
277
+ agents,
278
+ installedDate: new Date().toISOString().split('T')[0]
279
+ };
280
+
281
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
282
+ }
283
+
284
+ /**
285
+ * Get installed agents info for an AI tool
286
+ * @param {string} aiTool - AI tool identifier
287
+ * @param {string} level - 'user' or 'project'
288
+ * @param {string} projectPath - Project root path (required for project level)
289
+ * @returns {Object|null} Installed agents info or null
290
+ */
291
+ export function getInstalledAgentsForTool(aiTool, level = 'project', projectPath = null) {
292
+ const targetDir = getAgentsDirForAgent(aiTool, level, projectPath);
293
+ if (!targetDir || !existsSync(targetDir)) {
294
+ return null;
295
+ }
296
+
297
+ const manifestPath = join(targetDir, '.manifest.json');
298
+
299
+ // Count agent files
300
+ let agentFiles = [];
301
+ try {
302
+ agentFiles = readdirSync(targetDir)
303
+ .filter(f => f.endsWith('.md') && f !== 'README.md');
304
+ } catch {
305
+ return null;
306
+ }
307
+
308
+ if (agentFiles.length === 0) {
309
+ return null;
310
+ }
311
+
312
+ const getAgentName = (filename) => basename(filename, '.md');
313
+
314
+ if (!existsSync(manifestPath)) {
315
+ return {
316
+ installed: true,
317
+ count: agentFiles.length,
318
+ agents: agentFiles.map(getAgentName),
319
+ version: null,
320
+ aiTool,
321
+ level,
322
+ path: targetDir
323
+ };
324
+ }
325
+
326
+ try {
327
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
328
+ return {
329
+ installed: true,
330
+ count: agentFiles.length,
331
+ agents: manifest.agents || agentFiles.map(getAgentName),
332
+ version: manifest.version || null,
333
+ aiTool,
334
+ level,
335
+ path: targetDir,
336
+ installedDate: manifest.installedDate || null
337
+ };
338
+ } catch {
339
+ return {
340
+ installed: true,
341
+ count: agentFiles.length,
342
+ agents: agentFiles.map(getAgentName),
343
+ version: null,
344
+ aiTool,
345
+ level,
346
+ path: targetDir
347
+ };
348
+ }
349
+ }
350
+
351
+ /**
352
+ * Install agents to multiple AI tools at once
353
+ * @param {Array<{agent: string, level: string}>} installations - Array of installation targets
354
+ * @param {string[]} agentNames - Agents to install (null = all)
355
+ * @param {string} projectPath - Project root path
356
+ * @returns {Object} Combined results
357
+ */
358
+ export async function installAgentsToMultipleTools(installations, agentNames = null, projectPath = null) {
359
+ const results = {
360
+ success: true,
361
+ installations: [],
362
+ totalInstalled: 0,
363
+ totalErrors: 0,
364
+ allFileHashes: {}
365
+ };
366
+
367
+ for (const { agent, level } of installations) {
368
+ const result = await installAgentsForTool(agent, level, agentNames, projectPath);
369
+ results.installations.push(result);
370
+
371
+ if (!result.success) {
372
+ results.success = false;
373
+ }
374
+ results.totalInstalled += result.installed.length;
375
+ results.totalErrors += result.errors.length;
376
+
377
+ // Merge file hashes from this installation
378
+ if (result.fileHashes) {
379
+ Object.assign(results.allFileHashes, result.fileHashes);
380
+ }
381
+ }
382
+
383
+ return results;
384
+ }
385
+
386
+ export default {
387
+ installAgentsForTool,
388
+ getInstalledAgentsForTool,
389
+ installAgentsToMultipleTools,
390
+ getAvailableAgentNames,
391
+ getAgentContent,
392
+ parseAgentFrontmatter
393
+ };