relayax-cli 0.4.16 → 0.4.18

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.
@@ -132,13 +132,13 @@ function registerInstall(program) {
132
132
  }
133
133
  process.exit(1);
134
134
  }
135
- const claimRes = await fetch(`${config_js_1.API_URL}/api/agents/${slug}/claim-access`, {
135
+ const claimRes = await fetch(`${config_js_1.API_URL}/api/agents/${parsed.name}/claim-access`, {
136
136
  method: 'POST',
137
137
  headers: {
138
138
  'Content-Type': 'application/json',
139
139
  Authorization: `Bearer ${token}`,
140
140
  },
141
- body: JSON.stringify({ code: _opts.code }),
141
+ body: JSON.stringify({ code: _opts.code, owner: parsed.owner }),
142
142
  signal: AbortSignal.timeout(10000),
143
143
  });
144
144
  if (!claimRes.ok) {
@@ -240,38 +240,33 @@ function registerInstall(program) {
240
240
  process.exit(1);
241
241
  }
242
242
  }
243
- // 3. Download package: prefer git clone, fallback to tar.gz
243
+ // 3. Download package via git clone
244
244
  const requestedVersion = versionMatch ? versionMatch[2] : undefined;
245
- if (resolvedAgent.git_url) {
246
- // Git clone path
247
- (0, git_operations_js_1.checkGitInstalled)();
248
- const gitUrl = (0, git_operations_js_1.buildGitUrl)(resolvedAgent.git_url, { code: _opts.code });
249
- await (0, storage_js_1.clonePackage)(gitUrl, agentDir, requestedVersion);
250
- }
251
- else {
252
- // Legacy tar.gz path (retry once if signed URL expired)
253
- let tarPath;
254
- try {
255
- tarPath = await (0, storage_js_1.downloadPackage)(resolvedAgent.package_url, tempDir);
245
+ if (!resolvedAgent.git_url) {
246
+ const errMsg = '이 에이전트는 재publish가 필요합니다. 빌더에게 문의하세요.';
247
+ if (json) {
248
+ console.log(JSON.stringify({ error: 'NO_GIT_URL', message: errMsg }));
256
249
  }
257
- catch (dlErr) {
258
- const dlMsg = dlErr instanceof Error ? dlErr.message : String(dlErr);
259
- if (dlMsg.includes('403') || dlMsg.includes('expired')) {
260
- if (!json) {
261
- console.error('\x1b[33m⚙ 다운로드 URL 만료, 재시도 중...\x1b[0m');
262
- }
263
- resolvedAgent = await (0, api_js_1.fetchAgentInfo)(slug);
264
- tarPath = await (0, storage_js_1.downloadPackage)(resolvedAgent.package_url, tempDir);
265
- }
266
- else {
267
- throw dlErr;
268
- }
250
+ else {
251
+ console.error(`\x1b[31m✖ ${errMsg}\x1b[0m`);
269
252
  }
270
- if (fs_1.default.existsSync(agentDir)) {
271
- fs_1.default.rmSync(agentDir, { recursive: true, force: true });
253
+ process.exit(1);
254
+ }
255
+ (0, git_operations_js_1.checkGitInstalled)();
256
+ const gitUrl = (0, git_operations_js_1.buildGitUrl)(resolvedAgent.git_url, { code: _opts.code });
257
+ await (0, storage_js_1.clonePackage)(gitUrl, agentDir, requestedVersion);
258
+ // Verify clone has actual files (not just .git)
259
+ const clonedEntries = fs_1.default.readdirSync(agentDir).filter((f) => f !== '.git');
260
+ if (clonedEntries.length === 0) {
261
+ fs_1.default.rmSync(agentDir, { recursive: true, force: true });
262
+ const errMsg = '에이전트 패키지가 비어있습니다. 빌더에게 재publish를 요청하세요.';
263
+ if (json) {
264
+ console.log(JSON.stringify({ error: 'EMPTY_PACKAGE', message: errMsg }));
265
+ }
266
+ else {
267
+ console.error(`\x1b[31m✖ ${errMsg}\x1b[0m`);
272
268
  }
273
- fs_1.default.mkdirSync(agentDir, { recursive: true });
274
- await (0, storage_js_1.extractPackage)(tarPath, agentDir);
269
+ process.exit(1);
275
270
  }
276
271
  // 4.5. Inject preamble (update check) into SKILL.md and commands
277
272
  (0, preamble_js_1.injectPreambleToAgent)(agentDir, slug);
@@ -80,6 +80,7 @@ interface PublishResult {
80
80
  slug: string;
81
81
  version: string;
82
82
  url: string;
83
+ access_code?: string | null;
83
84
  profile?: {
84
85
  username?: string;
85
86
  display_name?: string;
@@ -746,13 +746,13 @@ function registerPublish(program) {
746
746
  console.error(`업로드 중...`);
747
747
  }
748
748
  const result = await publishToApi(token, tarPath, metadata);
749
- // Git push: commit and push to git server (non-fatal if git server unavailable)
750
- try {
751
- const gitUrl = result.git_url;
752
- if (gitUrl) {
753
- if (!json) {
754
- console.error('git 저장소에 푸시 중...');
755
- }
749
+ // Git push: commit and push to git server (required)
750
+ const gitUrl = result.git_url;
751
+ if (gitUrl) {
752
+ if (!json) {
753
+ console.error('git 저장소에 푸시 중...');
754
+ }
755
+ try {
756
756
  const isFirstPublish = !result.is_update;
757
757
  if (isFirstPublish) {
758
758
  await (0, git_operations_js_1.gitPublishInit)(relayDir, gitUrl, config.version);
@@ -761,12 +761,16 @@ function registerPublish(program) {
761
761
  await (0, git_operations_js_1.gitPublishUpdate)(relayDir, gitUrl, config.version);
762
762
  }
763
763
  }
764
- }
765
- catch (gitPushErr) {
766
- // Git push failure is non-fatal — tar.gz upload already succeeded
767
- if (!json) {
764
+ catch (gitPushErr) {
768
765
  const gpMsg = gitPushErr instanceof Error ? gitPushErr.message : String(gitPushErr);
769
- console.error(`\x1b[33m⚠ git push 실패 (배포는 완료됨): ${gpMsg}\x1b[0m`);
766
+ if (json) {
767
+ console.log(JSON.stringify({ error: 'GIT_PUSH_FAILED', message: `git push 실패: ${gpMsg}` }));
768
+ }
769
+ else {
770
+ console.error(`\x1b[31m✖ git push 실패: ${gpMsg}\x1b[0m`);
771
+ console.error('\x1b[33m 재시도하려면 relay publish를 다시 실행하세요.\x1b[0m');
772
+ }
773
+ process.exit(1);
770
774
  }
771
775
  }
772
776
  // Update entry command preamble with scoped slug from server (non-fatal)
@@ -784,15 +788,7 @@ function registerPublish(program) {
784
788
  // preamble update is best-effort — publish already succeeded
785
789
  }
786
790
  if (json) {
787
- // Enrich JSON output with plugin_url if git_url available
788
791
  const jsonResult = { ...result };
789
- const resultGitUrl = jsonResult.git_url;
790
- if (resultGitUrl) {
791
- const pSlug = jsonResult.slug.startsWith('@') ? jsonResult.slug.slice(1) : jsonResult.slug;
792
- const pName = pSlug.includes('/') ? pSlug.split('/')[1] : pSlug;
793
- jsonResult.plugin_url = `${config_js_1.API_URL}/api/registry/${pSlug}/plugin`;
794
- jsonResult.plugin_install_cmd = `/plugin install ${pName}`;
795
- }
796
792
  jsonResult.platforms = generatedPlatforms;
797
793
  console.log(JSON.stringify(jsonResult));
798
794
  }
@@ -803,50 +799,35 @@ function registerPublish(program) {
803
799
  // Build share block
804
800
  {
805
801
  const detailSlug = result.slug.startsWith('@') ? result.slug.slice(1) : result.slug;
806
- const accessCode = result.access_code;
807
- const gitUrl = result.git_url;
808
- // CLI install command
802
+ const accessCode = result.access_code ?? null;
803
+ // const gitUrl = (result as unknown as Record<string, unknown>).git_url as string | undefined // plugin disabled
804
+ // npx turnkey install command (works everywhere, no pre-install needed)
809
805
  const visibility = config.visibility ?? 'public';
810
- let installCmd;
806
+ let npxInstallCmd;
811
807
  if (visibility === 'internal' && accessCode) {
812
- installCmd = `npx relayax-cli install ${result.slug} --join-code ${accessCode}`;
808
+ npxInstallCmd = `npx relayax-cli install ${result.slug} --join-code ${accessCode}`;
813
809
  }
814
810
  else if (visibility === 'private' && accessCode) {
815
- installCmd = `npx relayax-cli install ${result.slug} --code ${accessCode}`;
811
+ npxInstallCmd = `npx relayax-cli install ${result.slug} --code ${accessCode}`;
816
812
  }
817
813
  else {
818
- installCmd = `npx relayax-cli install ${result.slug}`;
814
+ npxInstallCmd = `npx relayax-cli install ${result.slug}`;
819
815
  }
820
- // Plugin install commands (marketplace add + plugin install)
821
- const pluginSlug = detailSlug.includes('/') ? detailSlug.split('/')[1] : detailSlug;
822
- const pluginUrl = gitUrl ? `${config_js_1.API_URL}/api/registry/${detailSlug}/plugin` : null;
823
- // ── CLI 설치 (복사용) ──
824
- console.log(`\n \x1b[1m▸ CLI 설치\x1b[0m`);
816
+ // ── 설치 방법 (터미널 출력) ──
817
+ console.log(`\n \x1b[1m설치 방법\x1b[0m \x1b[90m(Claude Code, Cursor, Codex 등 모든 에이전트)\x1b[0m`);
825
818
  console.log(` ┌─`);
826
- console.log(` │ ${installCmd}`);
819
+ console.log(` │ ${npxInstallCmd}`);
827
820
  console.log(` └─`);
828
- // ── Claude Code Plugin 설치 (복사용) ──
829
- if (pluginUrl) {
830
- console.log(`\n \x1b[1m▸ Claude Code Plugin 설치\x1b[0m`);
831
- console.log(` ┌─`);
832
- console.log(` │ /plugin marketplace add ${pluginUrl}`);
833
- console.log(` │ /plugin install ${pluginSlug}`);
834
- console.log(` └─`);
835
- }
836
- // ── 소개 페이지 ──
837
- console.log(`\n \x1b[90m소개 페이지:\x1b[0m https://relayax.com/@${detailSlug}`);
821
+ console.log(`\n \x1b[90m소개:\x1b[0m https://relayax.com/@${detailSlug}`);
838
822
  // ── 공유 텍스트 (코드블록, 그대로 복붙) ──
839
823
  if (isTTY) {
840
824
  const shareBlock = [
841
825
  `[${config.name}] 설치하기`,
842
826
  ``,
843
- `# CLI`,
844
- installCmd,
827
+ npxInstallCmd,
828
+ ``,
829
+ `소개: https://relayax.com/@${detailSlug}`,
845
830
  ];
846
- if (pluginUrl) {
847
- shareBlock.push(``, `# Claude Code Plugin`, `/plugin marketplace add ${pluginUrl}`, `/plugin install ${pluginSlug}`);
848
- }
849
- shareBlock.push(``, `소개: https://relayax.com/@${detailSlug}`);
850
831
  const maxLen = Math.max(...shareBlock.map((l) => l.length));
851
832
  const border = '─'.repeat(maxLen + 2);
852
833
  console.log(`\n \x1b[90m┌${border}┐\x1b[0m`);
@@ -45,7 +45,11 @@ function gitClone(url, destDir, opts) {
45
45
  args.push('--depth', String(opts.depth));
46
46
  }
47
47
  args.push(url, destDir);
48
- (0, child_process_1.execFileSync)('git', args, { stdio: 'pipe' });
48
+ (0, child_process_1.execFileSync)('git', args, {
49
+ stdio: 'pipe',
50
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
51
+ timeout: 30000,
52
+ });
49
53
  }
50
54
  function gitAdd(dir, files = '.') {
51
55
  (0, child_process_1.execFileSync)('git', ['add', files], { cwd: dir, stdio: 'pipe' });
@@ -37,12 +37,21 @@ else
37
37
  DEVICE_HASH="unknown"
38
38
  fi
39
39
 
40
- # Usage ping
40
+ # Read relay token (for user identification in usage ping)
41
+ _RELAY_TOKEN=""
42
+ if [ -f "$HOME/.relay/token.json" ]; then
43
+ _RELAY_TOKEN=$(grep -o '"access_token":"[^"]*"' "$HOME/.relay/token.json" 2>/dev/null | head -1 | cut -d'"' -f4)
44
+ fi
45
+
46
+ # Usage ping (with user identity if logged in)
41
47
  if command -v relay &>/dev/null; then
42
48
  relay ping "${slug}" --quiet 2>/dev/null &
43
49
  elif command -v curl &>/dev/null; then
44
- curl -sf --max-time 5 -X POST "${apiUrl}/api/agents/${agentSlug}/ping" \\
50
+ _AUTH_HEADER=""
51
+ [ -n "$_RELAY_TOKEN" ] && _AUTH_HEADER="-H \\"Authorization: Bearer $_RELAY_TOKEN\\""
52
+ eval curl -sf --max-time 5 -X POST "${apiUrl}/api/agents/${agentSlug}/ping" \\
45
53
  -H "Content-Type: application/json" \\
54
+ $_AUTH_HEADER \\
46
55
  -d "{\\"device_hash\\":\\"$DEVICE_HASH\\",\\"slug\\":\\"${slug}\\"}" \\
47
56
  2>/dev/null &
48
57
  fi
@@ -126,18 +126,16 @@ function createMcpServer() {
126
126
  const tempDir = (0, storage_js_1.makeTempDir)();
127
127
  try {
128
128
  const agentDir = path_1.default.join(projectPath, '.relay', 'agents', parsed.owner, parsed.name);
129
- if (agent.git_url) {
130
- // Git clone path
131
- (0, git_operations_js_1.checkGitInstalled)();
132
- await (0, storage_js_1.clonePackage)(agent.git_url, agentDir);
129
+ if (!agent.git_url) {
130
+ return { content: [jsonText({ error: 'NO_GIT_URL', message: '이 에이전트는 재publish가 필요합니다. 빌더에게 문의하세요.' })], isError: true };
133
131
  }
134
- else {
135
- // Legacy tar.gz path
136
- const tarPath = await (0, storage_js_1.downloadPackage)(agent.package_url, tempDir);
137
- if (fs_1.default.existsSync(agentDir))
138
- fs_1.default.rmSync(agentDir, { recursive: true, force: true });
139
- fs_1.default.mkdirSync(agentDir, { recursive: true });
140
- await (0, storage_js_1.extractPackage)(tarPath, agentDir);
132
+ (0, git_operations_js_1.checkGitInstalled)();
133
+ await (0, storage_js_1.clonePackage)(agent.git_url, agentDir);
134
+ // Verify clone has actual files
135
+ const clonedEntries = fs_1.default.readdirSync(agentDir).filter((f) => f !== '.git');
136
+ if (clonedEntries.length === 0) {
137
+ fs_1.default.rmSync(agentDir, { recursive: true, force: true });
138
+ return { content: [jsonText({ error: 'EMPTY_PACKAGE', message: '에이전트 패키지가 비어있습니다. 빌더에게 재publish를 요청하세요.' })], isError: true };
141
139
  }
142
140
  (0, preamble_js_1.injectPreambleToAgent)(agentDir, fullSlug);
143
141
  const installed = (0, config_js_1.loadInstalled)();
@@ -734,6 +732,89 @@ function createMcpServer() {
734
732
  return { content: [jsonText({ error: String(err) })], isError: true };
735
733
  }
736
734
  });
735
+ // ═══ Detail Images — 상세페이지 이미지 관리 ═══
736
+ server.tool('relay_detail_upload', '에이전트 상세페이지 이미지를 업로드합니다. 폴더 내 이미지를 파일명 순으로 정렬하여 업로드합니다 (기존 이미지 전체 교체).', {
737
+ slug: zod_1.z.string().describe('에이전트 slug'),
738
+ path: zod_1.z.string().describe('이미지가 있는 폴더 경로 (PNG/GIF/JPEG/WebP)'),
739
+ }, async ({ slug, path: dirPath }) => {
740
+ try {
741
+ const token = (0, config_js_1.getValidToken)();
742
+ if (!token)
743
+ return { content: [jsonText({ error: '로그인이 필요합니다. relay login을 먼저 실행하세요.' })], isError: true };
744
+ const absPath = path_1.default.resolve(dirPath);
745
+ if (!fs_1.default.existsSync(absPath) || !fs_1.default.statSync(absPath).isDirectory()) {
746
+ return { content: [jsonText({ error: `폴더를 찾을 수 없습니다: ${absPath}` })], isError: true };
747
+ }
748
+ const imageExts = ['.png', '.jpg', '.jpeg', '.gif', '.webp'];
749
+ const files = fs_1.default.readdirSync(absPath)
750
+ .filter((f) => imageExts.includes(path_1.default.extname(f).toLowerCase()))
751
+ .sort();
752
+ if (files.length === 0) {
753
+ return { content: [jsonText({ error: '폴더에 이미지 파일이 없습니다 (PNG/GIF/JPEG/WebP)' })], isError: true };
754
+ }
755
+ const formData = new FormData();
756
+ for (const file of files) {
757
+ const filePath = path_1.default.join(absPath, file);
758
+ const buffer = fs_1.default.readFileSync(filePath);
759
+ const ext = path_1.default.extname(file).toLowerCase();
760
+ const mimeMap = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp' };
761
+ const blob = new Blob([buffer], { type: mimeMap[ext] || 'image/png' });
762
+ formData.append('files', blob, file);
763
+ }
764
+ const res = await fetch(`${config_js_1.API_URL}/api/agents/${slug}/detail-images`, {
765
+ method: 'POST',
766
+ headers: { Authorization: `Bearer ${token}` },
767
+ body: formData,
768
+ });
769
+ if (!res.ok) {
770
+ const body = await res.json().catch(() => ({}));
771
+ return { content: [jsonText({ error: body.message || `업로드 실패 (${res.status})` })], isError: true };
772
+ }
773
+ const result = await res.json();
774
+ const update = await getCliUpdateWarning();
775
+ return { content: [jsonTextWithUpdate({ status: 'uploaded', count: result.count, images: result.detail_images }, update)] };
776
+ }
777
+ catch (err) {
778
+ return { content: [jsonText({ error: String(err) })], isError: true };
779
+ }
780
+ });
781
+ server.tool('relay_detail_list', '에이전트 상세페이지 이미지 목록을 조회합니다', {
782
+ slug: zod_1.z.string().describe('에이전트 slug'),
783
+ }, async ({ slug }) => {
784
+ try {
785
+ const res = await fetch(`${config_js_1.API_URL}/api/agents/${slug}/detail-images`);
786
+ if (!res.ok) {
787
+ return { content: [jsonText({ error: `조회 실패 (${res.status})` })], isError: true };
788
+ }
789
+ const data = await res.json();
790
+ return { content: [jsonText({ detail_images: data.detail_images, count: data.detail_images.length })] };
791
+ }
792
+ catch (err) {
793
+ return { content: [jsonText({ error: String(err) })], isError: true };
794
+ }
795
+ });
796
+ server.tool('relay_detail_clear', '에이전트 상세페이지 이미지를 모두 삭제합니다', {
797
+ slug: zod_1.z.string().describe('에이전트 slug'),
798
+ }, async ({ slug }) => {
799
+ try {
800
+ const token = (0, config_js_1.getValidToken)();
801
+ if (!token)
802
+ return { content: [jsonText({ error: '로그인이 필요합니다.' })], isError: true };
803
+ const res = await fetch(`${config_js_1.API_URL}/api/agents/${slug}/detail-images`, {
804
+ method: 'DELETE',
805
+ headers: { Authorization: `Bearer ${token}` },
806
+ });
807
+ if (!res.ok) {
808
+ const body = await res.json().catch(() => ({}));
809
+ return { content: [jsonText({ error: body.message || `삭제 실패 (${res.status})` })], isError: true };
810
+ }
811
+ const result = await res.json();
812
+ return { content: [jsonText({ status: 'cleared', deleted: result.deleted })] };
813
+ }
814
+ catch (err) {
815
+ return { content: [jsonText({ error: String(err) })], isError: true };
816
+ }
817
+ });
737
818
  return server;
738
819
  }
739
820
  // ─── Start ───
@@ -143,10 +143,10 @@ relay.yaml이 없으면 새로 만들고, 있으면 변경사항을 반영합니
143
143
  1. **배포 결과 요약** — slug, 버전, 공개 범위, URL
144
144
  2. **설치 방법** — CLI 출력에 코드블록 형태로 이미 포함되어 있으므로, 그 내용을 사용자에게 안내합니다:
145
145
  - CLI: `npx relayax-cli install {slug}`
146
- - 출력에 `plugin_url`이 있으면 Claude Code Plugin:
146
+ - 출력에 `plugin_url`이 있으면 Claude Code Plugin (**두 명령을 순서대로 각각 실행**):
147
147
  ```
148
- /plugin marketplace add {pluginUrl}
149
- /plugin install {slug}
148
+ /plugin marketplace add {pluginUrl}
149
+ /plugin install {slug}
150
150
  ```
151
151
  - 에이전트 소개 페이지 URL
152
152
  3. **공유 텍스트** — CLI 출력의 공유 블록(┌─ ... ─┘)을 그대로 안내합니다. 팀에 바로 복붙할 수 있는 코드블록 형태입니다.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relayax-cli",
3
- "version": "0.4.16",
3
+ "version": "0.4.18",
4
4
  "description": "RelayAX Agent Team Marketplace CLI - Install and manage agent teams",
5
5
  "main": "dist/index.js",
6
6
  "bin": {