relayax-cli 0.4.15 → 0.4.17

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) {
@@ -242,13 +242,22 @@ function registerInstall(program) {
242
242
  }
243
243
  // 3. Download package: prefer git clone, fallback to tar.gz
244
244
  const requestedVersion = versionMatch ? versionMatch[2] : undefined;
245
+ let usedGit = false;
245
246
  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);
247
+ try {
248
+ (0, git_operations_js_1.checkGitInstalled)();
249
+ const gitUrl = (0, git_operations_js_1.buildGitUrl)(resolvedAgent.git_url, { code: _opts.code });
250
+ await (0, storage_js_1.clonePackage)(gitUrl, agentDir, requestedVersion);
251
+ usedGit = true;
252
+ }
253
+ catch (gitErr) {
254
+ const gitMsg = gitErr instanceof Error ? gitErr.message : String(gitErr);
255
+ if (!json) {
256
+ console.error(`\x1b[33m⚠ git clone 실패, tar.gz로 설치합니다: ${gitMsg}\x1b[0m`);
257
+ }
258
+ }
250
259
  }
251
- else {
260
+ if (!usedGit) {
252
261
  // Legacy tar.gz path (retry once if signed URL expired)
253
262
  let tarPath;
254
263
  try {
@@ -8,7 +8,8 @@ import type { ContentType } from '../lib/ai-tools.js';
8
8
  export interface ContentEntry {
9
9
  name: string;
10
10
  type: ContentType;
11
- from: string;
11
+ from?: string;
12
+ path?: string;
12
13
  }
13
14
  type ContentDiffStatus = 'modified' | 'unchanged' | 'source_missing';
14
15
  interface ContentDiffEntry {
@@ -16,6 +16,14 @@ const js_yaml_1 = __importDefault(require("js-yaml"));
16
16
  const ai_tools_js_1 = require("../lib/ai-tools.js");
17
17
  const paths_js_1 = require("../lib/paths.js");
18
18
  const SYNC_DIRS = ['skills', 'commands', 'agents', 'rules'];
19
+ /** from 또는 path 중 존재하는 값을 반환 */
20
+ function getFromPath(entry) {
21
+ const val = entry.from ?? entry.path;
22
+ if (!val) {
23
+ throw new Error(`contents 항목 "${entry.name}"에 from 또는 path가 필요합니다.`);
24
+ }
25
+ return val;
26
+ }
19
27
  // ─── Helpers ───
20
28
  function fileHash(filePath) {
21
29
  const content = fs_1.default.readFileSync(filePath);
@@ -121,7 +129,7 @@ function scanPath(absPath) {
121
129
  function computeContentsDiff(contents, relayDir, projectPath) {
122
130
  const diff = [];
123
131
  for (const entry of contents) {
124
- const absFrom = resolveFromPath(entry.from, projectPath);
132
+ const absFrom = resolveFromPath(getFromPath(entry), projectPath);
125
133
  if (!fs_1.default.existsSync(absFrom)) {
126
134
  diff.push({ name: entry.name, type: entry.type, status: 'source_missing' });
127
135
  continue;
@@ -152,7 +160,8 @@ function computeContentsDiff(contents, relayDir, projectPath) {
152
160
  * ~/.claude/agents/dev-lead.md → agents/dev-lead.md
153
161
  */
154
162
  function deriveRelaySubPath(entry) {
155
- const from = entry.from.startsWith('~/') ? entry.from.slice(2) : entry.from;
163
+ const fromPath = getFromPath(entry);
164
+ const from = fromPath.startsWith('~/') ? fromPath.slice(2) : fromPath;
156
165
  // skills/xxx, agents/xxx 등의 패턴을 추출
157
166
  for (const dir of SYNC_DIRS) {
158
167
  const idx = from.indexOf(`/${dir}/`);
@@ -211,7 +220,7 @@ function syncContentsToRelay(contents, contentsDiff, relayDir, projectPath) {
211
220
  const content = contents.find((c) => c.name === diffEntry.name && c.type === diffEntry.type);
212
221
  if (!content)
213
222
  continue;
214
- const absFrom = resolveFromPath(content.from, projectPath);
223
+ const absFrom = resolveFromPath(getFromPath(content), projectPath);
215
224
  const relaySubPath = deriveRelaySubPath(content);
216
225
  const relayTarget = path_1.default.join(relayDir, relaySubPath);
217
226
  // 단일 파일인 경우 직접 복사 (디렉토리 기반 diff/sync 불필요)
@@ -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;
@@ -784,15 +784,7 @@ function registerPublish(program) {
784
784
  // preamble update is best-effort — publish already succeeded
785
785
  }
786
786
  if (json) {
787
- // Enrich JSON output with plugin_url if git_url available
788
787
  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
788
  jsonResult.platforms = generatedPlatforms;
797
789
  console.log(JSON.stringify(jsonResult));
798
790
  }
@@ -803,50 +795,35 @@ function registerPublish(program) {
803
795
  // Build share block
804
796
  {
805
797
  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
798
+ const accessCode = result.access_code ?? null;
799
+ // const gitUrl = (result as unknown as Record<string, unknown>).git_url as string | undefined // plugin disabled
800
+ // npx turnkey install command (works everywhere, no pre-install needed)
809
801
  const visibility = config.visibility ?? 'public';
810
- let installCmd;
802
+ let npxInstallCmd;
811
803
  if (visibility === 'internal' && accessCode) {
812
- installCmd = `npx relayax-cli install ${result.slug} --join-code ${accessCode}`;
804
+ npxInstallCmd = `npx relayax-cli install ${result.slug} --join-code ${accessCode}`;
813
805
  }
814
806
  else if (visibility === 'private' && accessCode) {
815
- installCmd = `npx relayax-cli install ${result.slug} --code ${accessCode}`;
807
+ npxInstallCmd = `npx relayax-cli install ${result.slug} --code ${accessCode}`;
816
808
  }
817
809
  else {
818
- installCmd = `npx relayax-cli install ${result.slug}`;
810
+ npxInstallCmd = `npx relayax-cli install ${result.slug}`;
819
811
  }
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`);
812
+ // ── 설치 방법 (터미널 출력) ──
813
+ console.log(`\n \x1b[1m설치 방법\x1b[0m \x1b[90m(Claude Code, Cursor, Codex 등 모든 에이전트)\x1b[0m`);
825
814
  console.log(` ┌─`);
826
- console.log(` │ ${installCmd}`);
815
+ console.log(` │ ${npxInstallCmd}`);
827
816
  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}`);
817
+ console.log(`\n \x1b[90m소개:\x1b[0m https://relayax.com/@${detailSlug}`);
838
818
  // ── 공유 텍스트 (코드블록, 그대로 복붙) ──
839
819
  if (isTTY) {
840
820
  const shareBlock = [
841
821
  `[${config.name}] 설치하기`,
842
822
  ``,
843
- `# CLI`,
844
- installCmd,
823
+ npxInstallCmd,
824
+ ``,
825
+ `소개: https://relayax.com/@${detailSlug}`,
845
826
  ];
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
827
  const maxLen = Math.max(...shareBlock.map((l) => l.length));
851
828
  const border = '─'.repeat(maxLen + 2);
852
829
  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
@@ -734,6 +734,89 @@ function createMcpServer() {
734
734
  return { content: [jsonText({ error: String(err) })], isError: true };
735
735
  }
736
736
  });
737
+ // ═══ Detail Images — 상세페이지 이미지 관리 ═══
738
+ server.tool('relay_detail_upload', '에이전트 상세페이지 이미지를 업로드합니다. 폴더 내 이미지를 파일명 순으로 정렬하여 업로드합니다 (기존 이미지 전체 교체).', {
739
+ slug: zod_1.z.string().describe('에이전트 slug'),
740
+ path: zod_1.z.string().describe('이미지가 있는 폴더 경로 (PNG/GIF/JPEG/WebP)'),
741
+ }, async ({ slug, path: dirPath }) => {
742
+ try {
743
+ const token = (0, config_js_1.getValidToken)();
744
+ if (!token)
745
+ return { content: [jsonText({ error: '로그인이 필요합니다. relay login을 먼저 실행하세요.' })], isError: true };
746
+ const absPath = path_1.default.resolve(dirPath);
747
+ if (!fs_1.default.existsSync(absPath) || !fs_1.default.statSync(absPath).isDirectory()) {
748
+ return { content: [jsonText({ error: `폴더를 찾을 수 없습니다: ${absPath}` })], isError: true };
749
+ }
750
+ const imageExts = ['.png', '.jpg', '.jpeg', '.gif', '.webp'];
751
+ const files = fs_1.default.readdirSync(absPath)
752
+ .filter((f) => imageExts.includes(path_1.default.extname(f).toLowerCase()))
753
+ .sort();
754
+ if (files.length === 0) {
755
+ return { content: [jsonText({ error: '폴더에 이미지 파일이 없습니다 (PNG/GIF/JPEG/WebP)' })], isError: true };
756
+ }
757
+ const formData = new FormData();
758
+ for (const file of files) {
759
+ const filePath = path_1.default.join(absPath, file);
760
+ const buffer = fs_1.default.readFileSync(filePath);
761
+ const ext = path_1.default.extname(file).toLowerCase();
762
+ const mimeMap = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp' };
763
+ const blob = new Blob([buffer], { type: mimeMap[ext] || 'image/png' });
764
+ formData.append('files', blob, file);
765
+ }
766
+ const res = await fetch(`${config_js_1.API_URL}/api/agents/${slug}/detail-images`, {
767
+ method: 'POST',
768
+ headers: { Authorization: `Bearer ${token}` },
769
+ body: formData,
770
+ });
771
+ if (!res.ok) {
772
+ const body = await res.json().catch(() => ({}));
773
+ return { content: [jsonText({ error: body.message || `업로드 실패 (${res.status})` })], isError: true };
774
+ }
775
+ const result = await res.json();
776
+ const update = await getCliUpdateWarning();
777
+ return { content: [jsonTextWithUpdate({ status: 'uploaded', count: result.count, images: result.detail_images }, update)] };
778
+ }
779
+ catch (err) {
780
+ return { content: [jsonText({ error: String(err) })], isError: true };
781
+ }
782
+ });
783
+ server.tool('relay_detail_list', '에이전트 상세페이지 이미지 목록을 조회합니다', {
784
+ slug: zod_1.z.string().describe('에이전트 slug'),
785
+ }, async ({ slug }) => {
786
+ try {
787
+ const res = await fetch(`${config_js_1.API_URL}/api/agents/${slug}/detail-images`);
788
+ if (!res.ok) {
789
+ return { content: [jsonText({ error: `조회 실패 (${res.status})` })], isError: true };
790
+ }
791
+ const data = await res.json();
792
+ return { content: [jsonText({ detail_images: data.detail_images, count: data.detail_images.length })] };
793
+ }
794
+ catch (err) {
795
+ return { content: [jsonText({ error: String(err) })], isError: true };
796
+ }
797
+ });
798
+ server.tool('relay_detail_clear', '에이전트 상세페이지 이미지를 모두 삭제합니다', {
799
+ slug: zod_1.z.string().describe('에이전트 slug'),
800
+ }, async ({ slug }) => {
801
+ try {
802
+ const token = (0, config_js_1.getValidToken)();
803
+ if (!token)
804
+ return { content: [jsonText({ error: '로그인이 필요합니다.' })], isError: true };
805
+ const res = await fetch(`${config_js_1.API_URL}/api/agents/${slug}/detail-images`, {
806
+ method: 'DELETE',
807
+ headers: { Authorization: `Bearer ${token}` },
808
+ });
809
+ if (!res.ok) {
810
+ const body = await res.json().catch(() => ({}));
811
+ return { content: [jsonText({ error: body.message || `삭제 실패 (${res.status})` })], isError: true };
812
+ }
813
+ const result = await res.json();
814
+ return { content: [jsonText({ status: 'cleared', deleted: result.deleted })] };
815
+ }
816
+ catch (err) {
817
+ return { content: [jsonText({ error: String(err) })], isError: true };
818
+ }
819
+ });
737
820
  return server;
738
821
  }
739
822
  // ─── 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.15",
3
+ "version": "0.4.17",
4
4
  "description": "RelayAX Agent Team Marketplace CLI - Install and manage agent teams",
5
5
  "main": "dist/index.js",
6
6
  "bin": {