relayax-cli 0.1.97 → 0.1.99

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.
@@ -58,10 +58,23 @@ async function selectToolsInteractively(detectedIds) {
58
58
  }
59
59
  /**
60
60
  * 글로벌 User 커맨드를 ~/.claude/commands/relay/에 설치한다.
61
+ * 기존 파일 중 현재 커맨드 목록에 없는 것은 제거한다.
61
62
  */
62
63
  function installGlobalUserCommands() {
63
64
  const globalDir = (0, command_adapter_js_1.getGlobalCommandDir)();
64
65
  fs_1.default.mkdirSync(globalDir, { recursive: true });
66
+ // 현재 커맨드 ID 세트
67
+ const currentIds = new Set(command_adapter_js_1.USER_COMMANDS.map((c) => c.id));
68
+ // 기존 파일 중 현재 목록에 없는 것 제거
69
+ if (fs_1.default.existsSync(globalDir)) {
70
+ for (const file of fs_1.default.readdirSync(globalDir)) {
71
+ const id = file.replace(/\.md$/, '');
72
+ if (!currentIds.has(id)) {
73
+ fs_1.default.unlinkSync(path_1.default.join(globalDir, file));
74
+ }
75
+ }
76
+ }
77
+ // 현재 커맨드 설치 (덮어쓰기)
65
78
  const commands = [];
66
79
  for (const cmd of command_adapter_js_1.USER_COMMANDS) {
67
80
  const filePath = (0, command_adapter_js_1.getGlobalCommandPath)(cmd.id);
@@ -160,12 +173,23 @@ function registerInit(program) {
160
173
  targetToolIds = detected.map((t) => t.value);
161
174
  }
162
175
  }
163
- // Builder 커맨드 설치
176
+ // Builder 커맨드 설치 (기존 파일 중 현재 목록에 없는 것 제거)
177
+ const builderIds = new Set(command_adapter_js_1.BUILDER_COMMANDS.map((c) => c.id));
164
178
  for (const toolId of targetToolIds) {
165
179
  const tool = ai_tools_js_1.AI_TOOLS.find((t) => t.value === toolId);
166
180
  if (!tool)
167
181
  continue;
168
182
  const adapter = (0, command_adapter_js_1.createAdapter)(tool);
183
+ const localDir = path_1.default.join(projectPath, tool.skillsDir, 'commands', 'relay');
184
+ // 기존 로컬 커맨드 중 Builder 목록에 없는 것 제거
185
+ if (fs_1.default.existsSync(localDir)) {
186
+ for (const file of fs_1.default.readdirSync(localDir)) {
187
+ const id = file.replace(/\.md$/, '');
188
+ if (!builderIds.has(id)) {
189
+ fs_1.default.unlinkSync(path_1.default.join(localDir, file));
190
+ }
191
+ }
192
+ }
169
193
  const installedCommands = [];
170
194
  for (const cmd of command_adapter_js_1.BUILDER_COMMANDS) {
171
195
  const filePath = path_1.default.join(projectPath, adapter.getFilePath(cmd.id));
@@ -1,52 +1,72 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.registerInstall = registerInstall;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
4
9
  const api_js_1 = require("../lib/api.js");
5
10
  const storage_js_1 = require("../lib/storage.js");
6
- const installer_js_1 = require("../lib/installer.js");
7
11
  const config_js_1 = require("../lib/config.js");
8
12
  function registerInstall(program) {
9
13
  program
10
14
  .command('install <slug>')
11
- .description('에이전트 팀 설치 (감지된 에이전트 CLI에 설치)')
12
- .option('--path <install_path>', '설치 경로 지정')
13
- .action(async (slug, opts) => {
15
+ .description('에이전트 팀 패키지를 .relay/teams/에 다운로드합니다')
16
+ .action(async (slug) => {
14
17
  const json = program.opts().json ?? false;
15
- const installPath = (0, config_js_1.getInstallPath)(opts.path);
18
+ const projectPath = process.cwd();
19
+ const teamDir = path_1.default.join(projectPath, '.relay', 'teams', slug);
16
20
  const tempDir = (0, storage_js_1.makeTempDir)();
17
21
  try {
18
22
  // 1. Fetch team metadata
19
23
  const team = await (0, api_js_1.fetchTeamInfo)(slug);
20
24
  // 2. Visibility check
21
25
  const visibility = team.visibility ?? 'public';
22
- if (visibility === 'login-only') {
26
+ if (visibility === 'login-only' || visibility === 'invite-only') {
23
27
  const token = (0, config_js_1.loadToken)();
24
28
  if (!token) {
25
- console.error(JSON.stringify({ error: 'LOGIN_REQUIRED', visibility: 'login-only', slug, message: '이 팀은 로그인이 필요합니다.' }));
29
+ console.error(JSON.stringify({
30
+ error: 'LOGIN_REQUIRED',
31
+ visibility,
32
+ slug,
33
+ message: visibility === 'invite-only'
34
+ ? '이 팀은 초대받은 사용자만 설치할 수 있습니다. 로그인이 필요합니다.'
35
+ : '이 팀은 로그인이 필요합니다.',
36
+ }));
26
37
  process.exit(1);
27
38
  }
28
39
  }
29
- else if (visibility === 'invite-only') {
30
- const token = (0, config_js_1.loadToken)();
31
- if (!token) {
32
- console.error(JSON.stringify({ error: 'LOGIN_REQUIRED', visibility: 'invite-only', slug, message: '이 팀은 초대받은 사용자만 설치할 수 있습니다. 로그인이 필요합니다.' }));
33
- process.exit(1);
34
- }
35
- // 실제 초대 여부는 서버(install API)에서 체크
36
- }
37
40
  // 3. Download package
38
41
  const tarPath = await (0, storage_js_1.downloadPackage)(team.package_url, tempDir);
39
- // 4. Extract
40
- const extractDir = `${tempDir}/extracted`;
41
- await (0, storage_js_1.extractPackage)(tarPath, extractDir);
42
- // 5. Copy files to install_path
43
- const files = (0, installer_js_1.installTeam)(extractDir, installPath);
42
+ // 4. Extract to .relay/teams/<slug>/
43
+ if (fs_1.default.existsSync(teamDir)) {
44
+ fs_1.default.rmSync(teamDir, { recursive: true, force: true });
45
+ }
46
+ fs_1.default.mkdirSync(teamDir, { recursive: true });
47
+ await (0, storage_js_1.extractPackage)(tarPath, teamDir);
48
+ // 5. Count extracted files
49
+ function countFiles(dir) {
50
+ let count = 0;
51
+ if (!fs_1.default.existsSync(dir))
52
+ return 0;
53
+ for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
54
+ if (entry.isDirectory()) {
55
+ count += countFiles(path_1.default.join(dir, entry.name));
56
+ }
57
+ else {
58
+ count++;
59
+ }
60
+ }
61
+ return count;
62
+ }
63
+ const fileCount = countFiles(teamDir);
44
64
  // 6. Record in installed.json
45
65
  const installed = (0, config_js_1.loadInstalled)();
46
66
  installed[slug] = {
47
67
  version: team.version,
48
68
  installed_at: new Date().toISOString(),
49
- files,
69
+ files: [teamDir],
50
70
  };
51
71
  (0, config_js_1.saveInstalled)(installed);
52
72
  // 7. Report install (non-blocking)
@@ -57,22 +77,65 @@ function registerInstall(program) {
57
77
  slug,
58
78
  version: team.version,
59
79
  commands: team.commands,
60
- files_installed: files.length,
61
- install_path: installPath,
80
+ files: fileCount,
81
+ install_path: teamDir,
62
82
  };
63
83
  if (json) {
64
84
  console.log(JSON.stringify(result));
65
85
  }
66
86
  else {
67
- console.log(`\n\x1b[32m✓ ${team.name} 설치 완료\x1b[0m v${team.version}`);
68
- console.log(` 설치 위치: \x1b[36m${installPath}\x1b[0m`);
69
- console.log(` 파일 수: ${files.length}개`);
87
+ const authorUsername = team.author?.username;
88
+ const authorDisplayName = team.author?.display_name ?? authorUsername ?? '';
89
+ const authorSuffix = authorUsername ? ` \x1b[90mby @${authorUsername}\x1b[0m` : '';
90
+ console.log(`\n\x1b[32m✓ ${team.name} 다운로드 완료\x1b[0m v${team.version}${authorSuffix}`);
91
+ console.log(` 위치: \x1b[36m${teamDir}\x1b[0m`);
92
+ console.log(` 파일: ${fileCount}개`);
70
93
  if (team.commands.length > 0) {
71
- console.log('\n 사용 가능한 커맨드:');
94
+ console.log('\n 포함된 커맨드:');
72
95
  for (const cmd of team.commands) {
73
96
  console.log(` \x1b[33m/${cmd.name}\x1b[0m - ${cmd.description}`);
74
97
  }
75
98
  }
99
+ // Builder info block
100
+ if (team.welcome) {
101
+ console.log(`\n ┌─ 💬 ${authorDisplayName}님의 메시지 ${'─'.repeat(Math.max(0, 38 - authorDisplayName.length))}┐`);
102
+ const lines = team.welcome.match(/.{1,50}/g) ?? [team.welcome];
103
+ for (const line of lines) {
104
+ console.log(` │ "${line}"`);
105
+ }
106
+ console.log(` └${'─'.repeat(44)}┘`);
107
+ }
108
+ const contactLinks = team.author?.contact_links ?? {};
109
+ const contactEntries = Object.entries(contactLinks);
110
+ if (contactEntries.length > 0) {
111
+ const parts = contactEntries.map(([key, val]) => `${key}: ${val}`);
112
+ console.log(`\n 연락처: ${parts.join(' | ')}`);
113
+ }
114
+ if (authorUsername) {
115
+ console.log(`\n \x1b[90m👤 프로필: relayax.com/@${authorUsername}\x1b[0m`);
116
+ }
117
+ if (team.latest_post && authorUsername) {
118
+ console.log(` \x1b[90m📝 활용 팁: relayax.com/@${authorUsername}/posts/${team.latest_post.slug}\x1b[0m`);
119
+ }
120
+ // Follow prompt (only when logged in)
121
+ const token = (0, config_js_1.loadToken)();
122
+ if (authorUsername && token) {
123
+ try {
124
+ const { confirm } = await import('@inquirer/prompts');
125
+ const shouldFollow = await confirm({
126
+ message: `@${authorUsername}을 팔로우하시겠습니까? (이메일로 새 소식을 받습니다)`,
127
+ default: true,
128
+ });
129
+ if (shouldFollow) {
130
+ await (0, api_js_1.followBuilder)(authorUsername);
131
+ console.log(`\x1b[32m ✓ @${authorUsername} 팔로우 완료\x1b[0m`);
132
+ }
133
+ }
134
+ catch {
135
+ // non-critical: skip on error or non-interactive terminal
136
+ }
137
+ }
138
+ console.log('\n 에이전트가 /relay-install로 환경을 구성합니다.');
76
139
  }
77
140
  }
78
141
  catch (err) {
package/dist/lib/api.d.ts CHANGED
@@ -8,3 +8,4 @@ export interface TeamVersionInfo {
8
8
  }
9
9
  export declare function fetchTeamVersions(slug: string): Promise<TeamVersionInfo[]>;
10
10
  export declare function reportInstall(slug: string): Promise<void>;
11
+ export declare function followBuilder(username: string): Promise<void>;
package/dist/lib/api.js CHANGED
@@ -4,6 +4,7 @@ exports.fetchTeamInfo = fetchTeamInfo;
4
4
  exports.searchTeams = searchTeams;
5
5
  exports.fetchTeamVersions = fetchTeamVersions;
6
6
  exports.reportInstall = reportInstall;
7
+ exports.followBuilder = followBuilder;
7
8
  const config_js_1 = require("./config.js");
8
9
  async function fetchTeamInfo(slug) {
9
10
  const url = `${config_js_1.API_URL}/api/registry/${slug}`;
@@ -42,3 +43,21 @@ async function reportInstall(slug) {
42
43
  // non-critical: ignore errors
43
44
  });
44
45
  }
46
+ async function followBuilder(username) {
47
+ const token = (0, config_js_1.loadToken)();
48
+ const headers = {
49
+ 'Content-Type': 'application/json',
50
+ };
51
+ if (token) {
52
+ headers['Authorization'] = `Bearer ${token}`;
53
+ }
54
+ const res = await fetch(`https://www.relayax.com/api/follows`, {
55
+ method: 'POST',
56
+ headers,
57
+ body: JSON.stringify({ following_username: username, email_opt_in: true }),
58
+ });
59
+ if (!res.ok) {
60
+ const body = await res.text();
61
+ throw new Error(`팔로우 실패 (${res.status}): ${body}`);
62
+ }
63
+ }
@@ -79,46 +79,52 @@ exports.USER_COMMANDS = [
79
79
  {
80
80
  id: 'relay-install',
81
81
  description: 'relay 마켓플레이스에서 에이전트 팀을 설치합니다',
82
- body: `요청된 에이전트 팀을 relay 마켓플레이스에서 다운로드하여 현재 프로젝트에 설치하고, 의존성을 확인·설치합니다.
82
+ body: `요청된 에이전트 팀을 relay 마켓플레이스에서 다운로드하고, 현재 에이전트 환경에 맞게 구성합니다.
83
83
 
84
84
  ## 실행 방법
85
85
 
86
- ### 1. 팀 설치
86
+ ### 1. 팀 패키지 다운로드
87
87
  - \`relay install <slug>\` 명령어를 실행합니다.
88
- - 설치 결과를 확인합니다 (설치된 파일 수, 사용 가능한 커맨드 목록).
89
-
90
- ### 2. 의존성 확인 및 설치
91
- 설치된 팀의 .relay/relay.yaml에 \`requires\` 섹션이 있으면 각 항목을 확인하고 처리합니다:
92
-
88
+ - 패키지가 \`.relay/teams/<slug>/\`에 다운로드됩니다.
89
+
90
+ ### 2. 패키지 내용 확인
91
+ 다운로드된 \`.relay/teams/<slug>/\` 디렉토리를 읽어 구조를 파악합니다:
92
+ - skills/ — 스킬 파일들
93
+ - commands/ — 슬래시 커맨드 파일들
94
+ - agents/ — 에이전트 설정 파일들
95
+ - rules/ — 룰 파일들
96
+ - relay.yaml — 팀 메타데이터 및 requirements
97
+
98
+ ### 3. 에이전트 환경에 맞게 배치
99
+ 현재 에이전트의 디렉토리 구조에 맞게 파일을 복사합니다:
100
+ - Claude Code: \`.relay/teams/<slug>/commands/\` → \`.claude/commands/\`에 복사
101
+ - Claude Code: \`.relay/teams/<slug>/skills/\` → \`.claude/skills/\`에 복사
102
+ - 다른 에이전트(Cursor, Cline 등): 해당 에이전트의 규칙에 맞는 디렉토리에 복사
103
+ - 에이전트 설정이나 룰은 적절한 위치에 배치
104
+
105
+ ### 4. Requirements 확인 및 설치
106
+ \`.relay/teams/<slug>/relay.yaml\`의 \`requires\` 섹션을 읽고 처리합니다:
93
107
  - **cli**: \`which <name>\`으로 확인 → 없으면 install 명령 실행 또는 안내
94
108
  - **npm**: \`npm list <package>\`로 확인 → 없으면 \`npm install\`
95
- - **env**: 환경변수 확인 → optional이면 경고, 필수면 설정 안내
96
- - **mcp**: MCP 서버 설정 안내
97
- - **teams**: \`relay install <team>\`으로 재귀 설치
98
-
99
- ### 3. 제작자 소개 (welcome 메시지)
100
- - API 응답의 \`welcome\` 필드가 있으면 제작자 메시지를 보여줍니다.
101
- - \`contact\` 필드가 있으면 연락처도 함께 표시합니다.
102
-
103
- ### 4. 팔로우 제안
104
- - "이 팀의 제작자 @username을 팔로우할까요?"라고 제안합니다.
105
- - 인증되어 있으면 POST /api/follows로 팔로우합니다.
106
- - 미인증이면 \`relay login\` 후 팔로우할 수 있다고 안내합니다.
109
+ - **env**: 환경변수 확인 → required이면 설정 안내, optional이면 알림
110
+ - **mcp**: MCP 서버 설정 — 에이전트의 MCP 설정에 추가 안내
111
+ - **runtime**: Node.js/Python 버전 확인
112
+ - **teams**: 의존하는 다른 팀 → \`relay install <team>\`으로 재귀 설치
113
+ ${LOGIN_JIT_GUIDE}
107
114
 
108
115
  ### 5. 완료 안내
109
- - 사용 가능한 커맨드 안내
116
+ - 배치된 파일과 활성화된 커맨드 목록을 보여줍니다.
110
117
  - "바로 사용해볼까요?" 제안
111
- ${LOGIN_JIT_GUIDE}
112
118
 
113
119
  ## 예시
114
120
 
115
121
  사용자: /relay-install contents-team
116
- → relay install contents-team 실행
117
- requires 확인: ✓ playwright, ✓ sharp 설치됨
118
- 설치 완료!
119
- 제작자 메시지 연락처 표시
120
- "@haemin을 팔로우할까요?" Yes 팔로우 완료
121
- → "바로 /cardnews를 사용해볼까요?"`,
122
+ → relay install contents-team 실행 (패키지 다운로드)
123
+ .relay/teams/contents-team/ 내용 확인
124
+ commands/cardnews.md .claude/commands/cardnews.md 복사
125
+ skills/pdf-gen.md .claude/skills/pdf-gen.md 복사
126
+ requires 확인: playwright 설치됨,sharp 설치됨
127
+ → " 설치 완료! /cardnews를 사용해볼까요?"`,
122
128
  },
123
129
  {
124
130
  id: 'relay-list',
@@ -294,13 +300,30 @@ requires:
294
300
  - .relay/portfolio/cover.png에 저장합니다.
295
301
  - 사용자에게 "이 cover를 사용할까요?" 확인. 직접 제공도 가능.
296
302
 
297
- #### 슬롯 2: demo (선택 — 동작 시연)
298
- - 유형에 따라 제안 여부를 판단합니다:
299
- - 브라우저 자동화 키워드 감지 (playwright, puppeteer, crawl, scrape, browser) → GIF 데모 제안
300
- - 그 외 → 건너뜀 (사용자가 원하면 수동 추가)
301
- - GIF: 최대 5MB, .relay/portfolio/demo.gif에 저장
302
- - 또는 외부 영상 URL (YouTube, Loom 등)을 relay.yaml에 기록
303
- - 사용자에게 "데모를 녹화할까요?" / "영상 URL이 있나요?" 확인
303
+ #### 슬롯 2: demo (자동 생성 — 동작 시연)
304
+ 에이전트가 **직접 팀의 커맨드를 실행하여** demo를 자동 생성합니다. 사용자에게 묻기 전에 먼저 만듭니다.
305
+
306
+ **생성 절차:**
307
+ 1. .relay/commands/ 의 커맨드 목록을 읽고, 가장 대표적인 커맨드를 선택합니다.
308
+ 2. 해당 커맨드를 예시 주제/데이터로 실행합니다.
309
+ - 콘텐츠 팀: "라즈베리파이5 신제품 소개" 같은 예시 주제로 커맨드 실행
310
+ - 크롤링 팀: 예시 URL로 실행하며 브라우저 동작을 녹화
311
+ - 분석 팀: 샘플 데이터로 실행하여 결과 캡처
312
+ 3. 실행 결과물로 demo를 생성합니다:
313
+ - 여러 장 이미지 (카드뉴스 등): 슬라이드쇼 GIF (각 장 2초 간격)
314
+ - 브라우저 동작: 동작 녹화 GIF
315
+ - 단일 결과물: 결과 이미지를 demo로 사용
316
+ 4. 생성된 demo를 사용자에게 보여줍니다: "이 demo를 사용할까요?"
317
+ - 수정 요청 가능: "다른 주제로 다시 만들어줘", "좀 더 짧게"
318
+ - 확정 시 .relay/portfolio/demo.gif에 저장
319
+
320
+ **예시 주제 선택 기준:**
321
+ - relay.yaml의 description, tags에서 도메인 힌트 추출
322
+ - 기존 output/에 결과물이 있으면 같은 주제 활용
323
+ - 없으면 팀 설명에서 가장 그럴듯한 예시 주제를 자체 생성
324
+
325
+ **GIF 규격:** 최대 5MB, .relay/portfolio/demo.gif
326
+ **대안:** 외부 영상 URL (YouTube, Loom)도 가능 — relay.yaml에 기록
304
327
 
305
328
  #### 슬롯 3: gallery (선택 — 결과물 쇼케이스, 최대 5장)
306
329
  - 규격: 800x600px 이하, WebP, 각 500KB 이하
@@ -33,7 +33,7 @@ async function extractPackage(tarPath, destDir) {
33
33
  if (!fs_1.default.existsSync(destDir)) {
34
34
  fs_1.default.mkdirSync(destDir, { recursive: true });
35
35
  }
36
- await (0, tar_1.extract)({ file: tarPath, cwd: destDir, strip: 1 });
36
+ await (0, tar_1.extract)({ file: tarPath, cwd: destDir });
37
37
  }
38
38
  function makeTempDir() {
39
39
  return fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'relay-'));
package/dist/types.d.ts CHANGED
@@ -9,6 +9,7 @@ export interface InstalledRegistry {
9
9
  export interface TeamRegistryInfo {
10
10
  slug: string;
11
11
  name: string;
12
+ description?: string;
12
13
  version: string;
13
14
  package_url: string;
14
15
  commands: {
@@ -20,7 +21,21 @@ export interface TeamRegistryInfo {
20
21
  rules: number;
21
22
  skills: number;
22
23
  };
24
+ tags?: string[];
25
+ install_count?: number;
26
+ requires?: Record<string, unknown>;
23
27
  visibility?: "public" | "login-only" | "invite-only";
28
+ welcome?: string | null;
29
+ contact?: Record<string, string> | null;
30
+ author?: {
31
+ username: string;
32
+ display_name: string | null;
33
+ contact_links: Record<string, string>;
34
+ } | null;
35
+ latest_post?: {
36
+ title: string;
37
+ slug: string;
38
+ } | null;
24
39
  }
25
40
  export interface SearchResult {
26
41
  slug: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relayax-cli",
3
- "version": "0.1.97",
3
+ "version": "0.1.99",
4
4
  "description": "RelayAX Agent Team Marketplace CLI - Install and manage agent teams",
5
5
  "main": "dist/index.js",
6
6
  "bin": {