relayax-cli 0.1.2 → 0.1.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.
@@ -6,74 +6,117 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.registerInit = registerInit;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
- const os_1 = __importDefault(require("os"));
10
- const readline_1 = __importDefault(require("readline"));
11
- const config_js_1 = require("../lib/config.js");
12
- const DEFAULT_API_URL = 'https://relayax.com';
13
- const PRESET_PATHS = {
14
- '1': path_1.default.join(os_1.default.homedir(), '.claude'),
15
- '2': path_1.default.join(os_1.default.homedir(), '.gemini'),
16
- };
17
- function prompt(rl, question) {
18
- return new Promise((resolve) => rl.question(question, resolve));
9
+ const ai_tools_js_1 = require("../lib/ai-tools.js");
10
+ const command_adapter_js_1 = require("../lib/command-adapter.js");
11
+ const VALID_TEAM_DIRS = ['skills', 'agents', 'rules', 'commands'];
12
+ function resolveTools(toolsArg) {
13
+ const raw = toolsArg.trim().toLowerCase();
14
+ if (raw === 'all') {
15
+ return ai_tools_js_1.AI_TOOLS.map((t) => t.value);
16
+ }
17
+ const tokens = raw.split(',').map((t) => t.trim()).filter(Boolean);
18
+ const valid = new Set(ai_tools_js_1.AI_TOOLS.map((t) => t.value));
19
+ const invalid = tokens.filter((t) => !valid.has(t));
20
+ if (invalid.length > 0) {
21
+ throw new Error(`알 수 없는 도구: ${invalid.join(', ')}\n사용 가능: ${[...valid].join(', ')}`);
22
+ }
23
+ return tokens;
19
24
  }
20
25
  function registerInit(program) {
21
26
  program
22
27
  .command('init')
23
- .description('relayax 초기화 설치 경로 설정')
24
- .option('--path <install_path>', '설치 경로 직접 지정')
25
- .option('--api-url <url>', `API URL (기본값: ${DEFAULT_API_URL})`)
28
+ .description('에이전트 CLI를 감지하고 relay 슬래시 커맨드를 설치합니다')
29
+ .option('--tools <tools>', '설치할 에이전트 CLI 지정 (all 또는 쉼표 구분)')
26
30
  .action(async (opts) => {
27
31
  const pretty = program.opts().pretty ?? false;
28
- const api_url = opts.apiUrl ?? DEFAULT_API_URL;
29
- let install_path;
30
- if (opts.path) {
31
- install_path = opts.path;
32
- }
33
- else if (!pretty) {
34
- // non-pretty / agent mode: use default
35
- install_path = PRESET_PATHS['1'];
32
+ const projectPath = process.cwd();
33
+ // 1. 에이전트 CLI 감지 또는 --tools 지정
34
+ let targetToolIds;
35
+ if (opts.tools) {
36
+ targetToolIds = resolveTools(opts.tools);
36
37
  }
37
38
  else {
38
- const rl = readline_1.default.createInterface({
39
- input: process.stdin,
40
- output: process.stdout,
41
- });
42
- console.log('\n설치 경로를 선택하세요:');
43
- console.log(' 1) ~/.claude/ (Claude Code) [기본값]');
44
- console.log(' 2) ~/.gemini/ (Gemini CLI)');
45
- console.log(' 3) 직접 입력');
46
- const choice = (await prompt(rl, '\n선택 [1]: ')).trim() || '1';
47
- if (choice === '3') {
48
- install_path = (await prompt(rl, '경로 입력: ')).trim();
39
+ const detected = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
40
+ if (detected.length === 0) {
41
+ const msg = '에이전트 CLI 디렉토리를 찾을 수 없습니다.\n' +
42
+ '프로젝트에 .claude/, .gemini/, .cursor/ 등이 있는지 확인하세요.\n' +
43
+ '또는 --tools 옵션으로 직접 지정할 수 있습니다.';
44
+ if (pretty) {
45
+ console.error(`\n\x1b[31m✗ ${msg}\x1b[0m`);
46
+ }
47
+ else {
48
+ console.error(JSON.stringify({ error: 'NO_AGENT_CLI', message: msg }));
49
+ }
50
+ process.exit(1);
49
51
  }
50
- else {
51
- install_path = PRESET_PATHS[choice] ?? PRESET_PATHS['1'];
52
+ targetToolIds = detected.map((t) => t.value);
53
+ }
54
+ // 2. 각 에이전트 CLI에 슬래시 커맨드 설치
55
+ const results = [];
56
+ for (const toolId of targetToolIds) {
57
+ const tool = ai_tools_js_1.AI_TOOLS.find((t) => t.value === toolId);
58
+ if (!tool)
59
+ continue;
60
+ const adapter = (0, command_adapter_js_1.createAdapter)(tool);
61
+ const installedCommands = [];
62
+ for (const cmd of command_adapter_js_1.RELAY_COMMANDS) {
63
+ const filePath = path_1.default.join(projectPath, adapter.getFilePath(cmd.id));
64
+ const fileContent = adapter.formatFile(cmd);
65
+ fs_1.default.mkdirSync(path_1.default.dirname(filePath), { recursive: true });
66
+ fs_1.default.writeFileSync(filePath, fileContent);
67
+ installedCommands.push(cmd.id);
52
68
  }
53
- rl.close();
69
+ results.push({ tool: tool.name, commands: installedCommands });
54
70
  }
55
- // Resolve ~ in custom paths
56
- if (install_path.startsWith('~')) {
57
- install_path = path_1.default.join(os_1.default.homedir(), install_path.slice(1));
71
+ // 3. relay.yaml 생성 (팀 패키지 구조가 있는 경우)
72
+ const relayYamlPath = path_1.default.join(projectPath, 'relay.yaml');
73
+ let relayYamlStatus = 'skipped';
74
+ if (fs_1.default.existsSync(relayYamlPath)) {
75
+ relayYamlStatus = 'exists';
58
76
  }
59
- // Ensure install path exists
60
- if (!fs_1.default.existsSync(install_path)) {
61
- fs_1.default.mkdirSync(install_path, { recursive: true });
77
+ else {
78
+ const hasTeamDirs = VALID_TEAM_DIRS.some((d) => {
79
+ const dirPath = path_1.default.join(projectPath, d);
80
+ if (!fs_1.default.existsSync(dirPath))
81
+ return false;
82
+ return fs_1.default.readdirSync(dirPath).filter((f) => !f.startsWith('.')).length > 0;
83
+ });
84
+ if (hasTeamDirs) {
85
+ const dirName = path_1.default.basename(projectPath);
86
+ const yaml = [
87
+ `name: "${dirName}"`,
88
+ `slug: "${dirName}"`,
89
+ `description: ""`,
90
+ `version: "1.0.0"`,
91
+ `tags: []`,
92
+ ].join('\n') + '\n';
93
+ fs_1.default.writeFileSync(relayYamlPath, yaml);
94
+ relayYamlStatus = 'created';
95
+ }
62
96
  }
63
- (0, config_js_1.ensureRelayDir)();
64
- (0, config_js_1.saveConfig)({ install_path, api_url });
65
- const result = {
66
- status: 'ok',
67
- install_path,
68
- api_url,
69
- };
97
+ // 4. 출력
70
98
  if (pretty) {
71
- console.log('\n\x1b[32m✓ relay 초기화 완료\x1b[0m');
72
- console.log(` 설치 경로: \x1b[36m${install_path}\x1b[0m`);
73
- console.log(` API URL: \x1b[36m${api_url}\x1b[0m`);
99
+ console.log('\n\x1b[32m✓ relay 초기화 완료\x1b[0m\n');
100
+ for (const r of results) {
101
+ console.log(` \x1b[36m${r.tool}\x1b[0m`);
102
+ for (const cmd of r.commands) {
103
+ console.log(` /${cmd}`);
104
+ }
105
+ }
106
+ if (relayYamlStatus === 'created') {
107
+ console.log(`\n relay.yaml 생성됨 (팀 패키지 구조 감지)`);
108
+ }
109
+ else if (relayYamlStatus === 'exists') {
110
+ console.log(`\n relay.yaml 이미 존재`);
111
+ }
112
+ console.log('\n IDE를 재시작하면 슬래시 커맨드가 활성화됩니다.');
74
113
  }
75
114
  else {
76
- console.log(JSON.stringify(result));
115
+ console.log(JSON.stringify({
116
+ status: 'ok',
117
+ tools: results,
118
+ relay_yaml: relayYamlStatus,
119
+ }));
77
120
  }
78
121
  });
79
122
  }
@@ -8,7 +8,7 @@ const config_js_1 = require("../lib/config.js");
8
8
  function registerInstall(program) {
9
9
  program
10
10
  .command('install <slug>')
11
- .description('에이전트 팀 설치 (현재 디렉토리의 .claude/에 설치)')
11
+ .description('에이전트 팀 설치 (감지된 에이전트 CLI에 설치)')
12
12
  .option('--path <install_path>', '설치 경로 지정 (기본: ./.claude)')
13
13
  .action(async (slug, opts) => {
14
14
  const pretty = program.opts().pretty ?? false;
@@ -7,16 +7,44 @@ exports.registerPublish = registerPublish;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const os_1 = __importDefault(require("os"));
10
- const readline_1 = require("readline");
11
10
  const tar_1 = require("tar");
12
11
  const config_js_1 = require("../lib/config.js");
13
12
  const VALID_DIRS = ['skills', 'agents', 'rules', 'commands'];
14
- function slugify(str) {
15
- return str
16
- .toLowerCase()
17
- .replace(/[^a-z0-9-]/g, '-')
18
- .replace(/-+/g, '-')
19
- .replace(/^-|-$/g, '');
13
+ function parseRelayYaml(content) {
14
+ const result = {};
15
+ const tags = [];
16
+ let inTags = false;
17
+ for (const line of content.split('\n')) {
18
+ const trimmed = line.trim();
19
+ if (inTags) {
20
+ if (trimmed.startsWith('- ')) {
21
+ tags.push(trimmed.slice(2).replace(/^["']|["']$/g, ''));
22
+ continue;
23
+ }
24
+ else {
25
+ inTags = false;
26
+ }
27
+ }
28
+ if (trimmed === 'tags: []') {
29
+ result.tags = [];
30
+ continue;
31
+ }
32
+ if (trimmed === 'tags:') {
33
+ inTags = true;
34
+ continue;
35
+ }
36
+ const match = trimmed.match(/^(\w+):\s*["']?(.+?)["']?$/);
37
+ if (match) {
38
+ result[match[1]] = match[2];
39
+ }
40
+ }
41
+ return {
42
+ name: String(result.name ?? ''),
43
+ slug: String(result.slug ?? ''),
44
+ description: String(result.description ?? ''),
45
+ version: String(result.version ?? '1.0.0'),
46
+ tags,
47
+ };
20
48
  }
21
49
  function detectCommands(teamDir) {
22
50
  const cmdDir = path_1.default.join(teamDir, 'commands');
@@ -30,7 +58,6 @@ function detectCommands(teamDir) {
30
58
  try {
31
59
  const content = fs_1.default.readFileSync(path_1.default.join(cmdDir, file), 'utf-8');
32
60
  const lines = content.split('\n').map((l) => l.trim()).filter(Boolean);
33
- // Check frontmatter for description
34
61
  if (lines[0] === '---') {
35
62
  const endIdx = lines.indexOf('---', 1);
36
63
  if (endIdx > 0) {
@@ -41,7 +68,6 @@ function detectCommands(teamDir) {
41
68
  }
42
69
  }
43
70
  else if (lines[0]) {
44
- // Use first line, strip leading # if heading
45
71
  description = lines[0].replace(/^#+\s*/, '');
46
72
  }
47
73
  }
@@ -58,34 +84,24 @@ function countDir(teamDir, dirName) {
58
84
  return 0;
59
85
  return fs_1.default.readdirSync(dirPath).filter((f) => !f.startsWith('.')).length;
60
86
  }
61
- async function prompt(question, defaultVal) {
62
- const rl = (0, readline_1.createInterface)({ input: process.stdin, output: process.stderr });
63
- const hint = defaultVal ? ` (기본값: ${defaultVal})` : '';
64
- return new Promise((resolve) => {
65
- rl.question(`${question}${hint}: `, (answer) => {
66
- rl.close();
67
- resolve(answer.trim() || defaultVal || '');
68
- });
69
- });
70
- }
71
87
  async function createTarball(teamDir) {
72
88
  const tmpFile = path_1.default.join(os_1.default.tmpdir(), `relay-publish-${Date.now()}.tar.gz`);
73
- const parent = path_1.default.dirname(teamDir);
74
- const dirName = path_1.default.basename(teamDir);
89
+ // Only include valid dirs that exist
90
+ const dirsToInclude = VALID_DIRS.filter((d) => fs_1.default.existsSync(path_1.default.join(teamDir, d)));
75
91
  await (0, tar_1.create)({
76
92
  gzip: true,
77
93
  file: tmpFile,
78
- cwd: parent,
79
- }, [dirName]);
94
+ cwd: teamDir,
95
+ }, [...dirsToInclude]);
80
96
  return tmpFile;
81
97
  }
82
- async function publishToApi(apiUrl, token, tarPath, metadata) {
98
+ async function publishToApi(token, tarPath, metadata) {
83
99
  const fileBuffer = fs_1.default.readFileSync(tarPath);
84
100
  const blob = new Blob([fileBuffer], { type: 'application/gzip' });
85
101
  const form = new FormData();
86
102
  form.append('package', blob, `${metadata.slug}-${metadata.version}.tar.gz`);
87
103
  form.append('metadata', JSON.stringify(metadata));
88
- const res = await fetch(`${apiUrl}/api/publish`, {
104
+ const res = await fetch(`${config_js_1.API_URL}/api/publish`, {
89
105
  method: 'POST',
90
106
  headers: { Authorization: `Bearer ${token}` },
91
107
  body: form,
@@ -99,27 +115,42 @@ async function publishToApi(apiUrl, token, tarPath, metadata) {
99
115
  }
100
116
  function registerPublish(program) {
101
117
  program
102
- .command('publish [dir]')
103
- .description('에이전트 팀을 마켓플레이스에 배포합니다')
104
- .option('--name <name>', ' 표시명')
105
- .option('--description <desc>', '한 줄 설명')
106
- .option('--tag <tag>', '태그 (여러 번 사용 가능)', (val, prev) => [...prev, val], [])
107
- .option('--token <token>', 'Supabase 인증 토큰')
108
- .option('--slug <slug>', 'URL용 슬러그 (기본: 디렉토리명)')
109
- .option('--version <ver>', '버전 (기본: 1.0.0)', '1.0.0')
110
- .action(async (dir, opts) => {
118
+ .command('publish')
119
+ .description('현재 패키지를 마켓플레이스에 배포합니다 (relay.yaml 필요)')
120
+ .option('--token <token>', '인증 토큰')
121
+ .action(async (opts) => {
111
122
  const pretty = program.opts().pretty ?? false;
112
- const teamDir = path_1.default.resolve(dir ?? process.cwd());
113
- if (!fs_1.default.existsSync(teamDir) || !fs_1.default.statSync(teamDir).isDirectory()) {
114
- console.error(JSON.stringify({ error: 'INVALID_DIR', message: `디렉토리를 찾을 수 없습니다: ${teamDir}` }));
123
+ const teamDir = process.cwd();
124
+ const relayYamlPath = path_1.default.join(teamDir, 'relay.yaml');
125
+ // Check relay.yaml exists
126
+ if (!fs_1.default.existsSync(relayYamlPath)) {
127
+ console.error(JSON.stringify({
128
+ error: 'NOT_INITIALIZED',
129
+ message: 'relay.yaml이 없습니다. 먼저 `relay init`을 실행하세요.',
130
+ }));
131
+ process.exit(1);
132
+ }
133
+ // Parse relay.yaml
134
+ const yamlContent = fs_1.default.readFileSync(relayYamlPath, 'utf-8');
135
+ const config = parseRelayYaml(yamlContent);
136
+ if (!config.slug || !config.name || !config.description) {
137
+ console.error(JSON.stringify({
138
+ error: 'INVALID_CONFIG',
139
+ message: 'relay.yaml에 name, slug, description이 필요합니다.',
140
+ }));
115
141
  process.exit(1);
116
142
  }
117
143
  // Validate structure
118
- const hasDirs = VALID_DIRS.some((d) => fs_1.default.existsSync(path_1.default.join(teamDir, d)));
144
+ const hasDirs = VALID_DIRS.some((d) => {
145
+ const dirPath = path_1.default.join(teamDir, d);
146
+ if (!fs_1.default.existsSync(dirPath))
147
+ return false;
148
+ return fs_1.default.readdirSync(dirPath).filter((f) => !f.startsWith('.')).length > 0;
149
+ });
119
150
  if (!hasDirs) {
120
151
  console.error(JSON.stringify({
121
- error: 'INVALID_STRUCTURE',
122
- message: `팀 디렉토리에는 skills/, agents/, rules/, commands/ 중 하나 이상이 있어야 합니다`,
152
+ error: 'EMPTY_PACKAGE',
153
+ message: 'skills/, agents/, rules/, commands/ 중 하나 이상에 파일이 있어야 합니다.',
123
154
  }));
124
155
  process.exit(1);
125
156
  }
@@ -128,43 +159,27 @@ function registerPublish(program) {
128
159
  if (!token) {
129
160
  console.error(JSON.stringify({
130
161
  error: 'NO_TOKEN',
131
- message: '인증 토큰이 필요합니다. --token 플래그, RELAY_TOKEN 환경변수, 또는 `relay login`을 사용하세요.',
162
+ message: '인증이 필요합니다. `relay login`을 먼저 실행하세요.',
132
163
  }));
133
164
  process.exit(1);
134
165
  }
135
- // Auto-detect
136
- const autoSlug = slugify(path_1.default.basename(teamDir));
137
166
  const detectedCommands = detectCommands(teamDir);
138
167
  const components = {
139
168
  agents: countDir(teamDir, 'agents'),
140
169
  rules: countDir(teamDir, 'rules'),
141
170
  skills: countDir(teamDir, 'skills'),
142
171
  };
143
- // Gather metadata (prompt if not provided)
144
- const isInteractive = process.stdin.isTTY;
145
- const slug = opts.slug ?? (isInteractive ? await prompt('슬러그', autoSlug) : autoSlug);
146
- const name = opts.name ?? (isInteractive ? await prompt('팀 이름', path_1.default.basename(teamDir)) : path_1.default.basename(teamDir));
147
- const description = opts.description ?? (isInteractive ? await prompt('한 줄 설명') : '');
148
- let tags = opts.tag;
149
- if (tags.length === 0 && isInteractive) {
150
- const tagsInput = await prompt('태그 (쉼표 구분)', '');
151
- tags = tagsInput ? tagsInput.split(',').map((t) => t.trim()).filter(Boolean) : [];
152
- }
153
- if (!description) {
154
- console.error(JSON.stringify({ error: 'MISSING_DESCRIPTION', message: 'description은 필수입니다' }));
155
- process.exit(1);
156
- }
157
172
  const metadata = {
158
- slug,
159
- name,
160
- description,
161
- tags,
173
+ slug: config.slug,
174
+ name: config.name,
175
+ description: config.description,
176
+ tags: config.tags,
162
177
  commands: detectedCommands,
163
178
  components,
164
- version: opts.version,
179
+ version: config.version,
165
180
  };
166
181
  if (pretty) {
167
- console.error(`패키지 생성 중...`);
182
+ console.error(`패키지 생성 중... (${config.name} v${config.version})`);
168
183
  }
169
184
  let tarPath = null;
170
185
  try {
@@ -172,9 +187,9 @@ function registerPublish(program) {
172
187
  if (pretty) {
173
188
  console.error(`업로드 중...`);
174
189
  }
175
- const result = await publishToApi(config_js_1.API_URL, token, tarPath, metadata);
190
+ const result = await publishToApi(token, tarPath, metadata);
176
191
  if (pretty) {
177
- console.log(`\n\x1b[32m✓ ${name} 배포 완료\x1b[0m v${result.version}`);
192
+ console.log(`\n\x1b[32m✓ ${config.name} 배포 완료\x1b[0m v${result.version}`);
178
193
  console.log(` 슬러그: \x1b[36m${result.slug}\x1b[0m`);
179
194
  console.log(` URL: \x1b[36m${result.url}\x1b[0m`);
180
195
  }
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const commander_1 = require("commander");
5
+ const init_js_1 = require("./commands/init.js");
5
6
  const search_js_1 = require("./commands/search.js");
6
7
  const install_js_1 = require("./commands/install.js");
7
8
  const list_js_1 = require("./commands/list.js");
@@ -16,6 +17,7 @@ program
16
17
  .description('RelayAX Agent Team Marketplace CLI')
17
18
  .version(pkg.version)
18
19
  .option('--pretty', '인간 친화적 출력 (기본값: JSON)');
20
+ (0, init_js_1.registerInit)(program);
19
21
  (0, search_js_1.registerSearch)(program);
20
22
  (0, install_js_1.registerInstall)(program);
21
23
  (0, list_js_1.registerList)(program);
@@ -0,0 +1,14 @@
1
+ export interface AITool {
2
+ name: string;
3
+ value: string;
4
+ skillsDir: string;
5
+ }
6
+ /**
7
+ * Agent Skills 표준을 지원하는 에이전트 CLI 목록.
8
+ * @fission-ai/openspec의 AI_TOOLS에서 차용.
9
+ */
10
+ export declare const AI_TOOLS: AITool[];
11
+ /**
12
+ * 프로젝트 디렉토리에서 에이전트 CLI 디렉토리를 감지한다.
13
+ */
14
+ export declare function detectAgentCLIs(projectPath: string): AITool[];
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.AI_TOOLS = void 0;
7
+ exports.detectAgentCLIs = detectAgentCLIs;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ /**
11
+ * Agent Skills 표준을 지원하는 에이전트 CLI 목록.
12
+ * @fission-ai/openspec의 AI_TOOLS에서 차용.
13
+ */
14
+ exports.AI_TOOLS = [
15
+ { name: 'Amazon Q Developer', value: 'amazon-q', skillsDir: '.amazonq' },
16
+ { name: 'Antigravity', value: 'antigravity', skillsDir: '.agent' },
17
+ { name: 'Auggie', value: 'auggie', skillsDir: '.augment' },
18
+ { name: 'Claude Code', value: 'claude', skillsDir: '.claude' },
19
+ { name: 'Cline', value: 'cline', skillsDir: '.cline' },
20
+ { name: 'Codex', value: 'codex', skillsDir: '.codex' },
21
+ { name: 'CodeBuddy', value: 'codebuddy', skillsDir: '.codebuddy' },
22
+ { name: 'Continue', value: 'continue', skillsDir: '.continue' },
23
+ { name: 'CoStrict', value: 'costrict', skillsDir: '.cospec' },
24
+ { name: 'Crush', value: 'crush', skillsDir: '.crush' },
25
+ { name: 'Cursor', value: 'cursor', skillsDir: '.cursor' },
26
+ { name: 'Factory Droid', value: 'factory', skillsDir: '.factory' },
27
+ { name: 'Gemini CLI', value: 'gemini', skillsDir: '.gemini' },
28
+ { name: 'GitHub Copilot', value: 'github-copilot', skillsDir: '.github' },
29
+ { name: 'iFlow', value: 'iflow', skillsDir: '.iflow' },
30
+ { name: 'Kilo Code', value: 'kilocode', skillsDir: '.kilocode' },
31
+ { name: 'Kiro', value: 'kiro', skillsDir: '.kiro' },
32
+ { name: 'OpenCode', value: 'opencode', skillsDir: '.opencode' },
33
+ { name: 'Pi', value: 'pi', skillsDir: '.pi' },
34
+ { name: 'Qoder', value: 'qoder', skillsDir: '.qoder' },
35
+ { name: 'Qwen Code', value: 'qwen', skillsDir: '.qwen' },
36
+ { name: 'RooCode', value: 'roocode', skillsDir: '.roo' },
37
+ { name: 'Trae', value: 'trae', skillsDir: '.trae' },
38
+ { name: 'Windsurf', value: 'windsurf', skillsDir: '.windsurf' },
39
+ ];
40
+ /**
41
+ * 프로젝트 디렉토리에서 에이전트 CLI 디렉토리를 감지한다.
42
+ */
43
+ function detectAgentCLIs(projectPath) {
44
+ return exports.AI_TOOLS.filter((tool) => fs_1.default.existsSync(path_1.default.join(projectPath, tool.skillsDir)));
45
+ }
@@ -0,0 +1,21 @@
1
+ import type { AITool } from './ai-tools.js';
2
+ export interface CommandContent {
3
+ id: string;
4
+ description: string;
5
+ body: string;
6
+ }
7
+ export interface ToolCommandAdapter {
8
+ toolId: string;
9
+ getFilePath(commandId: string): string;
10
+ formatFile(content: CommandContent): string;
11
+ }
12
+ /**
13
+ * 기본 어댑터 — 대부분의 에이전트 CLI가 동일한 패턴 사용.
14
+ * {skillsDir}/commands/relay/{id}.md
15
+ */
16
+ export declare function createAdapter(tool: AITool): ToolCommandAdapter;
17
+ /**
18
+ * relay 슬래시 커맨드 템플릿.
19
+ * CLI 명령어(원자적 API)를 조합하는 에이전트 워크플로우.
20
+ */
21
+ export declare const RELAY_COMMANDS: CommandContent[];
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.RELAY_COMMANDS = void 0;
7
+ exports.createAdapter = createAdapter;
8
+ const path_1 = __importDefault(require("path"));
9
+ /**
10
+ * 기본 어댑터 — 대부분의 에이전트 CLI가 동일한 패턴 사용.
11
+ * {skillsDir}/commands/relay/{id}.md
12
+ */
13
+ function createAdapter(tool) {
14
+ return {
15
+ toolId: tool.value,
16
+ getFilePath(commandId) {
17
+ return path_1.default.join(tool.skillsDir, 'commands', 'relay', `${commandId}.md`);
18
+ },
19
+ formatFile(content) {
20
+ return `---\ndescription: ${content.description}\n---\n\n${content.body}\n`;
21
+ },
22
+ };
23
+ }
24
+ /**
25
+ * relay 슬래시 커맨드 템플릿.
26
+ * CLI 명령어(원자적 API)를 조합하는 에이전트 워크플로우.
27
+ */
28
+ exports.RELAY_COMMANDS = [
29
+ {
30
+ id: 'relay-explore',
31
+ description: 'relay 마켓플레이스를 탐색하고 프로젝트에 맞는 팀을 찾습니다',
32
+ body: `사용자의 요청이나 현재 프로젝트 맥락에 맞는 에이전트 팀을 relay 마켓플레이스에서 탐색합니다.
33
+
34
+ ## 실행 방법
35
+
36
+ 1. 사용자의 요청에서 키워드를 추출합니다. 명시적 키워드가 없으면 현재 프로젝트를 분석하여 적절한 검색어를 판단합니다.
37
+ 2. \`relay search <keyword>\` 명령어를 실행합니다 (필요하면 여러 키워드로 반복).
38
+ 3. 결과를 현재 프로젝트 맥락과 대조하여 가장 도움될 팀을 추천합니다:
39
+ - 팀 이름과 설명
40
+ - 제공하는 커맨드 목록
41
+ - 왜 이 팀이 지금 프로젝트에 맞는지 설명
42
+ 4. 관심 있는 팀이 있다면 \`/relay-install <slug>\`로 바로 설치할 수 있다고 안내합니다.
43
+
44
+ ## 예시
45
+
46
+ 사용자: /relay-explore 콘텐츠 만들 수 있는 팀 있어?
47
+ → relay search 콘텐츠 실행
48
+ → 결과 해석: "contents-team이 카드뉴스, PDF, PPT를 만들 수 있어요"
49
+ → 프로젝트 맥락 기반 추천
50
+ → "/relay-install contents-team으로 설치할 수 있어요"`,
51
+ },
52
+ {
53
+ id: 'relay-install',
54
+ description: 'relay 마켓플레이스에서 에이전트 팀을 설치합니다',
55
+ body: `요청된 에이전트 팀을 relay 마켓플레이스에서 다운로드하여 현재 프로젝트에 설치합니다.
56
+
57
+ ## 실행 방법
58
+
59
+ 1. \`relay install <slug> --pretty\` 명령어를 실행합니다.
60
+ 2. 설치 결과를 확인합니다:
61
+ - 설치된 파일 수
62
+ - 사용 가능해진 커맨드 목록
63
+ 3. 각 커맨드의 사용법을 간단히 안내합니다.
64
+ 4. "바로 사용해볼까요?"라고 제안합니다.
65
+ 5. 사용자가 원하면 첫 번째 커맨드를 실행해봅니다.
66
+
67
+ ## 예시
68
+
69
+ 사용자: /relay-install contents-team
70
+ → relay install contents-team --pretty 실행
71
+ → "설치 완료! 다음 커맨드를 사용할 수 있습니다:"
72
+ → /cardnews - 카드뉴스 제작
73
+ → /detailpage - 상세페이지 제작
74
+ → "바로 /cardnews를 사용해볼까요?"`,
75
+ },
76
+ {
77
+ id: 'relay-publish',
78
+ description: '현재 팀 패키지를 relay 마켓플레이스에 배포합니다',
79
+ body: `현재 디렉토리의 에이전트 팀을 relay 마켓플레이스에 배포합니다.
80
+
81
+ ## 실행 방법
82
+
83
+ 1. relay.yaml이 있는지 확인합니다. 없으면 사용자에게 팀 정보를 물어보고 생성합니다.
84
+ 2. skills/, agents/, rules/, commands/ 디렉토리 구조를 확인합니다.
85
+ 3. \`relay login\`으로 인증 상태를 확인합니다. 미인증이면 로그인을 안내합니다.
86
+ 4. \`relay publish --pretty\` 명령어를 실행합니다.
87
+ 5. 배포 결과와 마켓플레이스 URL을 보여줍니다.`,
88
+ },
89
+ ];
@@ -1,8 +1,10 @@
1
1
  import type { InstalledRegistry } from '../types.js';
2
2
  export declare const API_URL = "https://relayax.com";
3
3
  /**
4
- * 현재 디렉토리의 .claude/ 를 기본 설치 경로로 사용한다.
5
- * --path 옵션으로 오버라이드 가능.
4
+ * 설치 경로를 결정한다.
5
+ * 1. --path 옵션이 있으면 그대로 사용
6
+ * 2. 에이전트 CLI 자동 감지 → 감지된 경로 사용
7
+ * 3. 감지 안 되면 현재 디렉토리에 직접 설치
6
8
  */
7
9
  export declare function getInstallPath(override?: string): string;
8
10
  export declare function ensureRelayDir(): void;
@@ -13,12 +13,15 @@ exports.saveInstalled = saveInstalled;
13
13
  const fs_1 = __importDefault(require("fs"));
14
14
  const path_1 = __importDefault(require("path"));
15
15
  const os_1 = __importDefault(require("os"));
16
+ const ai_tools_js_1 = require("./ai-tools.js");
16
17
  exports.API_URL = 'https://relayax.com';
17
18
  const RELAY_DIR = path_1.default.join(os_1.default.homedir(), '.relay');
18
19
  const INSTALLED_FILE = path_1.default.join(RELAY_DIR, 'installed.json');
19
20
  /**
20
- * 현재 디렉토리의 .claude/ 를 기본 설치 경로로 사용한다.
21
- * --path 옵션으로 오버라이드 가능.
21
+ * 설치 경로를 결정한다.
22
+ * 1. --path 옵션이 있으면 그대로 사용
23
+ * 2. 에이전트 CLI 자동 감지 → 감지된 경로 사용
24
+ * 3. 감지 안 되면 현재 디렉토리에 직접 설치
22
25
  */
23
26
  function getInstallPath(override) {
24
27
  if (override) {
@@ -27,7 +30,12 @@ function getInstallPath(override) {
27
30
  : path_1.default.resolve(override);
28
31
  return resolved;
29
32
  }
30
- return path_1.default.join(process.cwd(), '.claude');
33
+ const cwd = process.cwd();
34
+ const detected = (0, ai_tools_js_1.detectAgentCLIs)(cwd);
35
+ if (detected.length >= 1) {
36
+ return path_1.default.join(cwd, detected[0].skillsDir);
37
+ }
38
+ return cwd;
31
39
  }
32
40
  function ensureRelayDir() {
33
41
  if (!fs_1.default.existsSync(RELAY_DIR)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relayax-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "RelayAX Agent Team Marketplace CLI - Install and manage agent teams",
5
5
  "main": "dist/index.js",
6
6
  "bin": {