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.
- package/dist/commands/install.js +16 -7
- package/dist/commands/package.d.ts +2 -1
- package/dist/commands/package.js +12 -3
- package/dist/commands/publish.d.ts +1 -0
- package/dist/commands/publish.js +14 -37
- package/dist/lib/git-operations.js +5 -1
- package/dist/lib/preamble.js +11 -2
- package/dist/mcp/server.js +83 -0
- package/dist/prompts/create.md +3 -3
- package/package.json +1 -1
package/dist/commands/install.js
CHANGED
|
@@ -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/${
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
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
|
|
11
|
+
from?: string;
|
|
12
|
+
path?: string;
|
|
12
13
|
}
|
|
13
14
|
type ContentDiffStatus = 'modified' | 'unchanged' | 'source_missing';
|
|
14
15
|
interface ContentDiffEntry {
|
package/dist/commands/package.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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 불필요)
|
package/dist/commands/publish.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
802
|
+
let npxInstallCmd;
|
|
811
803
|
if (visibility === 'internal' && accessCode) {
|
|
812
|
-
|
|
804
|
+
npxInstallCmd = `npx relayax-cli install ${result.slug} --join-code ${accessCode}`;
|
|
813
805
|
}
|
|
814
806
|
else if (visibility === 'private' && accessCode) {
|
|
815
|
-
|
|
807
|
+
npxInstallCmd = `npx relayax-cli install ${result.slug} --code ${accessCode}`;
|
|
816
808
|
}
|
|
817
809
|
else {
|
|
818
|
-
|
|
810
|
+
npxInstallCmd = `npx relayax-cli install ${result.slug}`;
|
|
819
811
|
}
|
|
820
|
-
//
|
|
821
|
-
|
|
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(` │ ${
|
|
815
|
+
console.log(` │ ${npxInstallCmd}`);
|
|
827
816
|
console.log(` └─`);
|
|
828
|
-
|
|
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
|
-
|
|
844
|
-
|
|
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, {
|
|
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' });
|
package/dist/lib/preamble.js
CHANGED
|
@@ -37,12 +37,21 @@ else
|
|
|
37
37
|
DEVICE_HASH="unknown"
|
|
38
38
|
fi
|
|
39
39
|
|
|
40
|
-
#
|
|
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
|
-
|
|
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
|
package/dist/mcp/server.js
CHANGED
|
@@ -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 ───
|
package/dist/prompts/create.md
CHANGED
|
@@ -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 출력의 공유 블록(┌─ ... ─┘)을 그대로 안내합니다. 팀에 바로 복붙할 수 있는 코드블록 형태입니다.
|