relayax-cli 0.4.25 → 0.4.27

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.
Files changed (39) hide show
  1. package/dist/commands/package.js +0 -12
  2. package/dist/commands/publish.js +1 -38
  3. package/dist/index.js +0 -2
  4. package/dist/lib/ai-tools.d.ts +0 -16
  5. package/dist/lib/ai-tools.js +0 -34
  6. package/dist/lib/command-adapter.js +2 -34
  7. package/dist/lib/preamble.d.ts +2 -2
  8. package/dist/lib/preamble.js +3 -43
  9. package/dist/mcp/server.js +7 -677
  10. package/dist/prompts/create.md +3 -6
  11. package/dist/prompts/explore.md +2 -0
  12. package/package.json +1 -1
  13. package/dist/commands/export.d.ts +0 -2
  14. package/dist/commands/export.js +0 -106
  15. package/dist/commands/follow.d.ts +0 -2
  16. package/dist/commands/follow.js +0 -45
  17. package/dist/commands/invite.d.ts +0 -2
  18. package/dist/commands/invite.js +0 -135
  19. package/dist/commands/spaces.d.ts +0 -11
  20. package/dist/commands/spaces.js +0 -69
  21. package/dist/lib/config.js.bak +0 -75
  22. package/dist/lib/guide.d.ts +0 -12
  23. package/dist/lib/guide.js +0 -60
  24. package/dist/lib/manifest-generator.d.ts +0 -20
  25. package/dist/lib/manifest-generator.js +0 -144
  26. package/dist/lib/security-scan.d.ts +0 -19
  27. package/dist/lib/security-scan.js +0 -114
  28. package/dist/prompts/_business-card.md +0 -41
  29. package/dist/prompts/_guide-instruction.md +0 -2
  30. package/dist/prompts/_requirements-check.md +0 -59
  31. package/dist/prompts/_setup-cli.md +0 -36
  32. package/dist/prompts/_setup-environment.md +0 -75
  33. package/dist/prompts/_setup-login.md +0 -31
  34. package/dist/prompts/_setup-org.md +0 -25
  35. package/dist/prompts/business-card.md +0 -41
  36. package/dist/prompts/error-handling.md +0 -38
  37. package/dist/prompts/install.md +0 -178
  38. package/dist/prompts/publish.md +0 -505
  39. package/dist/prompts/requirements-check.md +0 -59
@@ -9,695 +9,26 @@ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
9
9
  const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
10
10
  const zod_1 = require("zod");
11
11
  const config_js_1 = require("../lib/config.js");
12
- const api_js_1 = require("../lib/api.js");
13
- const slug_js_1 = require("../lib/slug.js");
14
- const storage_js_1 = require("../lib/storage.js");
15
- const git_operations_js_1 = require("../lib/git-operations.js");
16
- const ai_tools_js_1 = require("../lib/ai-tools.js");
17
- const preamble_js_1 = require("../lib/preamble.js");
18
- const installer_js_1 = require("../lib/installer.js");
19
- const paths_js_1 = require("../lib/paths.js");
20
- // prompts are used in MCP Prompt definitions below
21
12
  const fs_1 = __importDefault(require("fs"));
22
13
  const path_1 = __importDefault(require("path"));
23
- const js_yaml_1 = __importDefault(require("js-yaml"));
24
14
  // eslint-disable-next-line @typescript-eslint/no-var-requires
25
15
  const pkg = require('../../package.json');
26
16
  // ─── Helpers ───
27
- async function resolveUserInfo(token) {
28
- try {
29
- const res = await fetch(`${config_js_1.API_URL}/api/auth/me`, {
30
- headers: { Authorization: `Bearer ${token}` },
31
- });
32
- if (!res.ok)
33
- return {};
34
- const body = await res.json();
35
- return { username: body.username, email: body.email };
36
- }
37
- catch {
38
- return {};
39
- }
40
- }
41
- function countFiles(dir) {
42
- let count = 0;
43
- if (!fs_1.default.existsSync(dir))
44
- return 0;
45
- for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
46
- if (entry.isDirectory())
47
- count += countFiles(path_1.default.join(dir, entry.name));
48
- else
49
- count++;
50
- }
51
- return count;
52
- }
53
17
  function jsonText(obj) {
54
18
  return { type: 'text', text: JSON.stringify(obj) };
55
19
  }
56
- // 주요 도구 응답에 CLI 업데이트 경고를 병합하는 헬퍼
57
- // MCP 서버 프로세스는 Claude 재시작 전까지 유지되므로, 응답에 버전 정보를 포함시켜
58
- // 에이전트가 사용자에게 재시작을 안내할 수 있도록 한다.
59
- let _cachedCliUpdate;
60
- async function getCliUpdateWarning() {
61
- if (_cachedCliUpdate === undefined) {
62
- try {
63
- const { checkCliVersion } = await import('../lib/version-check.js');
64
- _cachedCliUpdate = await checkCliVersion(true);
65
- }
66
- catch {
67
- _cachedCliUpdate = null;
68
- }
69
- }
70
- if (!_cachedCliUpdate)
71
- return null;
72
- return {
73
- cli_update: {
74
- current: pkg.version,
75
- latest: _cachedCliUpdate.latest,
76
- message: `relay v${_cachedCliUpdate.latest}이 있습니다. npm update -g relayax-cli 후 Claude를 재시작해주세요.`,
77
- },
78
- };
79
- }
80
- function jsonTextWithUpdate(obj, update) {
81
- return jsonText(update ? { ...obj, ...update } : obj);
82
- }
83
- // MCP 서버는 Claude Desktop이 spawn하므로 cwd가 / 등 예측 불가한 경로일 수 있다.
84
- // project_path가 없을 때 cwd 대신 홈 디렉토리를 fallback으로 사용한다.
85
- const os_1 = __importDefault(require("os"));
86
- function resolveMcpProjectPath(projectPath) {
87
- if (projectPath)
88
- return projectPath;
89
- const resolved = (0, paths_js_1.resolveProjectPath)();
90
- // cwd가 / 또는 비정상적이면 홈 디렉토리 사용
91
- if (resolved === '/' || resolved === '')
92
- return os_1.default.homedir();
93
- return resolved;
94
- }
95
20
  // ─── Server ───
96
21
  function createMcpServer() {
97
- const server = new mcp_js_1.McpServer({ name: 'relay', version: pkg.version }, { capabilities: { tools: {}, prompts: {} } });
98
- // ═══ Tools ═══
99
- server.tool('relay_search', '에이전트를 검색합니다', {
100
- query: zod_1.z.string().describe('검색 키워드'),
101
- tag: zod_1.z.string().optional().describe('태그 필터'),
102
- }, async ({ query, tag }) => {
103
- try {
104
- const results = await (0, api_js_1.searchAgents)(query, tag);
105
- return { content: [jsonText({ results: results.map((r) => ({ slug: r.slug, name: r.name, description: r.description, installs: r.install_count })) })] };
106
- }
107
- catch (err) {
108
- return { content: [jsonText({ error: String(err) })], isError: true };
109
- }
110
- });
111
- server.tool('relay_install', '에이전트를 설치합니다', {
112
- slug: zod_1.z.string().describe('에이전트 slug (예: @owner/name)'),
113
- project_path: zod_1.z.string().optional().describe('프로젝트 경로 (기본: 홈 디렉토리)'),
114
- }, async ({ slug: slugInput, project_path }) => {
115
- try {
116
- const projectPath = resolveMcpProjectPath(project_path);
117
- const token = await (0, config_js_1.getValidToken)();
118
- const parsed = await (0, slug_js_1.resolveSlug)(slugInput);
119
- const fullSlug = parsed.full;
120
- const agent = await (0, api_js_1.fetchAgentInfo)(fullSlug);
121
- if (!agent)
122
- throw new Error('에이전트 정보를 가져오지 못했습니다.');
123
- if ((agent.visibility ?? 'public') !== 'public' && !token) {
124
- return { content: [jsonText({ error: 'LOGIN_REQUIRED', message: '이 에이전트는 로그인이 필요합니다.' })], isError: true };
125
- }
126
- const tempDir = (0, storage_js_1.makeTempDir)();
127
- try {
128
- const agentDir = path_1.default.join(projectPath, '.relay', 'agents', parsed.owner, parsed.name);
129
- if (!agent.git_url) {
130
- return { content: [jsonText({ error: 'NO_GIT_URL', message: '이 에이전트는 재publish가 필요합니다. 빌더에게 문의하세요.' })], isError: true };
131
- }
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 };
139
- }
140
- (0, preamble_js_1.injectPreambleToAgent)(agentDir, fullSlug);
141
- const installed = (0, config_js_1.loadInstalled)();
142
- installed[fullSlug] = { agent_id: agent.id, version: agent.version, installed_at: new Date().toISOString(), files: [agentDir] };
143
- (0, config_js_1.saveInstalled)(installed);
144
- await (0, api_js_1.reportInstall)(agent.id, fullSlug, agent.version);
145
- (0, api_js_1.sendUsagePing)(agent.id, fullSlug, agent.version);
146
- // relay.yaml에서 tags, requires, recommended_scope 읽기
147
- let agentTags = [];
148
- let agentRequires = null;
149
- let hasRules = false;
150
- let recommendedScope;
151
- try {
152
- const relayYamlPath = path_1.default.join(agentDir, 'relay.yaml');
153
- if (fs_1.default.existsSync(relayYamlPath)) {
154
- const cfg = js_yaml_1.default.load(fs_1.default.readFileSync(relayYamlPath, 'utf-8'));
155
- agentTags = cfg.tags ?? [];
156
- agentRequires = cfg.requires ?? null;
157
- if (cfg.recommended_scope === 'global' || cfg.recommended_scope === 'local') {
158
- recommendedScope = cfg.recommended_scope;
159
- }
160
- }
161
- hasRules = fs_1.default.existsSync(path_1.default.join(agentDir, 'rules')) && fs_1.default.readdirSync(path_1.default.join(agentDir, 'rules')).length > 0;
162
- }
163
- catch { /* non-critical */ }
164
- // recommended_scope가 relay.yaml에 없으면 휴리스틱으로 추론
165
- if (!recommendedScope) {
166
- const frameworkTags = ['nextjs', 'react', 'vue', 'angular', 'svelte', 'nuxt', 'remix', 'astro', 'django', 'rails', 'laravel', 'spring', 'express', 'fastapi', 'flask'];
167
- recommendedScope = (hasRules || agentTags.some((t) => frameworkTags.includes(t.toLowerCase()))) ? 'local' : 'global';
168
- }
169
- const cliUpdate = await getCliUpdateWarning();
170
- return { content: [jsonTextWithUpdate({
171
- status: 'ok', agent: agent.name, slug: fullSlug, version: agent.version,
172
- description: agent.description ?? '', tags: agentTags, requires: agentRequires, has_rules: hasRules,
173
- recommended_scope: recommendedScope,
174
- files: countFiles(agentDir), install_path: agentDir,
175
- scope_hint: `이 에이전트의 권장 배치 범위는 "${recommendedScope}"입니다. 사용자에게 확인 후 relay deploy --scope ${recommendedScope}로 배치하세요.`,
176
- }, cliUpdate)] };
177
- }
178
- finally {
179
- (0, storage_js_1.removeTempDir)(tempDir);
180
- }
181
- }
182
- catch (err) {
183
- return { content: [jsonText({ error: String(err) })], isError: true };
184
- }
185
- });
186
- server.tool('relay_uninstall', '에이전트를 제거합니다', {
187
- slug: zod_1.z.string().describe('에이전트 slug'),
188
- }, async ({ slug: slugInput }) => {
189
- try {
190
- const local = (0, config_js_1.loadInstalled)();
191
- const global = (0, config_js_1.loadGlobalInstalled)();
192
- const entry = local[slugInput] ?? global[slugInput];
193
- if (!entry) {
194
- return { content: [jsonText({ error: 'NOT_INSTALLED', message: `'${slugInput}'는 설치되어 있지 않습니다.` })], isError: true };
195
- }
196
- let removed = 0;
197
- if (local[slugInput]) {
198
- removed += (0, installer_js_1.uninstallAgent)(local[slugInput].files).length;
199
- delete local[slugInput];
200
- (0, config_js_1.saveInstalled)(local);
201
- }
202
- if (global[slugInput]) {
203
- removed += (0, installer_js_1.uninstallAgent)(global[slugInput].files).length;
204
- delete global[slugInput];
205
- (0, config_js_1.saveGlobalInstalled)(global);
206
- }
207
- return { content: [jsonText({ status: 'ok', removed_files: removed })] };
208
- }
209
- catch (err) {
210
- return { content: [jsonText({ error: String(err) })], isError: true };
211
- }
212
- });
213
- server.tool('relay_list', '설치된 에이전트 목록을 조회합니다', {}, async () => {
214
- const local = (0, config_js_1.loadInstalled)();
215
- const global = (0, config_js_1.loadGlobalInstalled)();
216
- const agents = [
217
- ...Object.entries(local).map(([slug, e]) => ({ slug, version: e.version, installed_at: e.installed_at, scope: 'local' })),
218
- ...Object.entries(global).map(([slug, e]) => ({ slug, version: e.version, installed_at: e.installed_at, scope: 'global' })),
219
- ];
220
- return { content: [jsonText({ agents })] };
221
- });
222
- server.tool('relay_status', '현재 relay 환경 상태를 표시합니다', {
223
- project_path: zod_1.z.string().optional().describe('프로젝트 경로'),
224
- }, async ({ project_path }) => {
225
- const projectPath = resolveMcpProjectPath(project_path);
226
- const token = await (0, config_js_1.getValidToken)();
227
- let username;
228
- let email;
229
- if (token) {
230
- const info = await resolveUserInfo(token);
231
- username = info.username;
232
- email = info.email;
233
- }
234
- const detected = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
235
- const mounted = (0, ai_tools_js_1.detectMountedCLIs)();
236
- const relayYaml = path_1.default.join(projectPath, '.relay', 'relay.yaml');
237
- let project = null;
238
- if (fs_1.default.existsSync(relayYaml)) {
239
- try {
240
- const cfg = js_yaml_1.default.load(fs_1.default.readFileSync(relayYaml, 'utf-8'));
241
- project = { is_agent: true, name: cfg.name, slug: cfg.slug, version: cfg.version };
242
- }
243
- catch { /* skip */ }
244
- }
245
- // 버전 확인
246
- const { checkCliVersion } = await import('../lib/version-check.js');
247
- const cliUpdate = await checkCliVersion(true);
248
- return { content: [jsonText({
249
- cli: { version: pkg.version, update_available: cliUpdate ? cliUpdate.latest : null },
250
- login: { authenticated: !!token, username, email },
251
- agent_clis: detected.map((t) => t.name),
252
- mounted_paths: mounted.map((m) => m.basePath),
253
- project,
254
- })] };
255
- });
256
- server.tool('relay_check_update', 'CLI 및 에이전트 업데이트를 확인합니다. slug 지정 시 해당 에이전트만 체크하며 사용 현황도 기록합니다 (preamble 대체).', {
257
- slug: zod_1.z.string().optional().describe('특정 에이전트 slug (예: @owner/name). 생략하면 전체 체크'),
258
- }, async ({ slug: slugInput }) => {
259
- const { checkCliVersion, checkAgentVersion, checkAllAgents } = await import('../lib/version-check.js');
260
- // slug가 지정되면 해당 에이전트의 usage ping도 함께 전송
261
- if (slugInput) {
262
- const local = (0, config_js_1.loadInstalled)();
263
- const global = (0, config_js_1.loadGlobalInstalled)();
264
- const entry = local[slugInput] ?? global[slugInput];
265
- const agentId = entry?.agent_id ?? null;
266
- const version = entry?.version;
267
- (0, api_js_1.sendUsagePing)(agentId, slugInput, version);
268
- }
269
- const cliUpdate = await checkCliVersion(true);
270
- const updates = [];
271
- if (cliUpdate)
272
- updates.push({ type: 'cli', current: cliUpdate.current, latest: cliUpdate.latest });
273
- if (slugInput) {
274
- const agentUpdate = await checkAgentVersion(slugInput, true);
275
- if (agentUpdate)
276
- updates.push({ type: 'agent', slug: agentUpdate.slug, current: agentUpdate.current, latest: agentUpdate.latest });
277
- }
278
- else {
279
- const agentUpdates = await checkAllAgents(true);
280
- for (const u of agentUpdates)
281
- updates.push({ type: 'agent', slug: u.slug, current: u.current, latest: u.latest });
282
- }
283
- if (updates.length === 0) {
284
- return { content: [jsonText({ status: 'up_to_date', message: '모두 최신 버전입니다.', cli_version: pkg.version })] };
285
- }
286
- return { content: [jsonText({ status: 'updates_available', updates, message: 'CLI를 업데이트하려면: npm update -g relayax-cli' })] };
287
- });
288
- server.tool('relay_scan', '배포 가능한 스킬/에이전트/커맨드를 스캔합니다', {
289
- project_path: zod_1.z.string().optional().describe('프로젝트 경로'),
290
- }, async ({ project_path }) => {
291
- const projectPath = resolveMcpProjectPath(project_path);
292
- const homeDir = (0, paths_js_1.resolveHome)();
293
- const sources = [];
294
- // 로컬
295
- for (const tool of (0, ai_tools_js_1.detectAgentCLIs)(projectPath)) {
296
- const items = (0, ai_tools_js_1.scanLocalItems)(projectPath, tool);
297
- if (items.length > 0)
298
- sources.push({ path: tool.skillsDir, location: 'local', name: tool.name, items: items.map((i) => ({ name: i.name, type: i.type })) });
299
- }
300
- // 글로벌
301
- const { detectGlobalCLIs } = await import('../lib/ai-tools.js');
302
- for (const tool of detectGlobalCLIs(homeDir)) {
303
- const items = (0, ai_tools_js_1.scanGlobalItems)(tool, homeDir);
304
- if (items.length > 0)
305
- sources.push({ path: `~/${tool.skillsDir}`, location: 'global', name: `${tool.name} (global)`, items: items.map((i) => ({ name: i.name, type: i.type })) });
306
- }
307
- // 마운트 (Cowork)
308
- for (const { tool, basePath } of (0, ai_tools_js_1.detectMountedCLIs)()) {
309
- const items = (0, ai_tools_js_1.scanMountedItems)(basePath, tool);
310
- if (items.length > 0)
311
- sources.push({ path: `${basePath}/${tool.skillsDir}`, location: 'mounted', name: `${tool.name} (mounted)`, items: items.map((i) => ({ name: i.name, type: i.type })) });
312
- }
313
- return { content: [jsonText({ sources })] };
314
- });
315
- server.tool('relay_package', '소스 디렉토리에서 .relay/로 콘텐츠를 패키징합니다. mode: init(최초 소스 탐색), sync(변경 반영), migrate(source→contents 마이그레이션)', {
316
- mode: zod_1.z.enum(['init', 'sync', 'migrate']).describe('패키징 모드'),
317
- project_path: zod_1.z.string().optional().describe('프로젝트 경로'),
318
- }, async ({ mode, project_path }) => {
319
- try {
320
- const projectPath = resolveMcpProjectPath(project_path);
321
- const homeDir = (0, paths_js_1.resolveHome)();
322
- const relayDir = path_1.default.join(projectPath, '.relay');
323
- const relayYamlPath = path_1.default.join(relayDir, 'relay.yaml');
324
- if (mode === 'init') {
325
- // 최초 패키징: 소스 탐색
326
- const { detectGlobalCLIs } = await import('../lib/ai-tools.js');
327
- const localTools = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
328
- const globalTools = detectGlobalCLIs(homeDir);
329
- const sources = [];
330
- for (const tool of localTools) {
331
- const items = (0, ai_tools_js_1.scanLocalItems)(projectPath, tool);
332
- if (items.length > 0)
333
- sources.push({ path: tool.skillsDir, location: 'local', name: tool.name, items: items.map((i) => ({ name: i.name, type: i.type, relativePath: i.relativePath })) });
334
- }
335
- for (const tool of globalTools) {
336
- const items = (0, ai_tools_js_1.scanGlobalItems)(tool, homeDir);
337
- if (items.length > 0)
338
- sources.push({ path: `~/${tool.skillsDir}`, location: 'global', name: `${tool.name} (global)`, items: items.map((i) => ({ name: i.name, type: i.type, relativePath: i.relativePath })) });
339
- }
340
- for (const { tool, basePath } of (0, ai_tools_js_1.detectMountedCLIs)()) {
341
- const items = (0, ai_tools_js_1.scanMountedItems)(basePath, tool);
342
- if (items.length > 0)
343
- sources.push({ path: `${basePath}/${tool.skillsDir}`, location: 'mounted', name: `${tool.name} (mounted)`, items: items.map((i) => ({ name: i.name, type: i.type, relativePath: i.relativePath })) });
344
- }
345
- // 기존 글로벌 에이전트 패키지 스캔
346
- const globalAgentsDir = path_1.default.join(homeDir, '.relay', 'agents');
347
- const existingAgents = [];
348
- if (fs_1.default.existsSync(globalAgentsDir)) {
349
- for (const entry of fs_1.default.readdirSync(globalAgentsDir, { withFileTypes: true })) {
350
- if (!entry.isDirectory() || entry.name.startsWith('.'))
351
- continue;
352
- const agentYaml = path_1.default.join(globalAgentsDir, entry.name, 'relay.yaml');
353
- if (fs_1.default.existsSync(agentYaml)) {
354
- try {
355
- const cfg = js_yaml_1.default.load(fs_1.default.readFileSync(agentYaml, 'utf-8'));
356
- existingAgents.push({ slug: cfg.slug ?? entry.name, name: cfg.name ?? entry.name, version: cfg.version ?? '0.0.0', path: `~/.relay/agents/${entry.name}` });
357
- }
358
- catch { /* skip */ }
359
- }
360
- }
361
- }
362
- return { content: [jsonText({ status: 'init_required', sources, existing_agents: existingAgents })] };
363
- }
364
- // sync / migrate는 relay.yaml이 필요
365
- if (!fs_1.default.existsSync(relayYamlPath)) {
366
- return { content: [jsonText({ error: 'NOT_INITIALIZED', message: '.relay/relay.yaml이 없습니다. mode: init으로 먼저 실행하세요.' })], isError: true };
367
- }
368
- if (mode === 'migrate') {
369
- const yamlMigrate = fs_1.default.readFileSync(relayYamlPath, 'utf-8');
370
- const cfgMigrate = js_yaml_1.default.load(yamlMigrate);
371
- if (cfgMigrate.contents) {
372
- return { content: [jsonText({ status: 'already_migrated', message: '이미 contents 형식입니다.' })] };
373
- }
374
- const legacySource = cfgMigrate.source;
375
- if (!legacySource) {
376
- return { content: [jsonText({ status: 'no_source', message: 'source 필드가 없습니다.' })], isError: true };
377
- }
378
- const localTools = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
379
- const tool = localTools.find((t) => t.skillsDir === legacySource);
380
- const migratedContents = [];
381
- if (tool) {
382
- const items = (0, ai_tools_js_1.scanLocalItems)(projectPath, tool);
383
- for (const item of items) {
384
- migratedContents.push({ name: item.name, type: item.type, from: `${legacySource}/${item.relativePath}` });
385
- }
386
- }
387
- delete cfgMigrate.source;
388
- cfgMigrate.contents = migratedContents;
389
- fs_1.default.writeFileSync(relayYamlPath, js_yaml_1.default.dump(cfgMigrate, { lineWidth: 120 }), 'utf-8');
390
- return { content: [jsonText({ status: 'migrated', contents: migratedContents })] };
391
- }
392
- // mode === 'sync'
393
- const { computeContentsDiff, syncContentsToRelay } = await import('../commands/package.js');
394
- const yamlContent = fs_1.default.readFileSync(relayYamlPath, 'utf-8');
395
- const config = js_yaml_1.default.load(yamlContent);
396
- const contents = config.contents ?? [];
397
- if (contents.length === 0) {
398
- return { content: [jsonText({ status: 'no_contents', message: 'relay.yaml에 contents가 없습니다.' })], isError: true };
399
- }
400
- const { diff: contentsDiff, newItems } = computeContentsDiff(contents, relayDir, projectPath);
401
- const hasChanges = contentsDiff.some((d) => d.status === 'modified');
402
- if (hasChanges) {
403
- syncContentsToRelay(contents, contentsDiff, relayDir, projectPath);
404
- }
405
- const summary = {
406
- modified: contentsDiff.filter((d) => d.status === 'modified').length,
407
- unchanged: contentsDiff.filter((d) => d.status === 'unchanged').length,
408
- source_missing: contentsDiff.filter((d) => d.status === 'source_missing').length,
409
- new_available: newItems.length,
410
- };
411
- return { content: [jsonText({ diff: contentsDiff, new_items: newItems, synced: hasChanges, summary })] };
412
- }
413
- catch (err) {
414
- return { content: [jsonText({ error: String(err) })], isError: true };
415
- }
416
- });
417
- server.tool('relay_org_list', '소속 Organization 목록을 조회합니다', {}, async () => {
418
- try {
419
- const token = await (0, config_js_1.getValidToken)();
420
- if (!token) {
421
- return { content: [jsonText({ error: 'LOGIN_REQUIRED', message: '로그인이 필요합니다.' })], isError: true };
422
- }
423
- const { fetchMyOrgs } = await import('../commands/orgs.js');
424
- const orgs = await fetchMyOrgs(token);
425
- return { content: [jsonText({ orgs: orgs.map((o) => ({ id: o.id, slug: o.slug, name: o.name, role: o.role })) })] };
426
- }
427
- catch (err) {
428
- return { content: [jsonText({ error: String(err) })], isError: true };
429
- }
430
- });
431
- server.tool('relay_org_create', '새 Organization을 생성합니다', {
432
- name: zod_1.z.string().describe('Organization 이름'),
433
- slug: zod_1.z.string().optional().describe('URL slug (미지정 시 이름에서 자동 생성)'),
434
- }, async ({ name, slug: slugInput }) => {
435
- try {
436
- const token = await (0, config_js_1.getValidToken)();
437
- if (!token) {
438
- return { content: [jsonText({ error: 'LOGIN_REQUIRED', message: '로그인이 필요합니다.' })], isError: true };
439
- }
440
- const slug = slugInput ?? name
441
- .toLowerCase()
442
- .replace(/[^a-z0-9\s-]/g, '')
443
- .replace(/[\s]+/g, '-')
444
- .replace(/-+/g, '-')
445
- .replace(/^-|-$/g, '')
446
- .slice(0, 50);
447
- const res = await fetch(`${config_js_1.API_URL}/api/orgs`, {
448
- method: 'POST',
449
- headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
450
- body: JSON.stringify({ name, slug }),
451
- });
452
- if (!res.ok) {
453
- const body = await res.json().catch(() => ({ message: `${res.status}` }));
454
- throw new Error(body.message ?? `Organization 생성 실패 (${res.status})`);
455
- }
456
- const org = await res.json();
457
- return { content: [jsonText({ status: 'created', org })] };
458
- }
459
- catch (err) {
460
- return { content: [jsonText({ error: String(err) })], isError: true };
461
- }
462
- });
463
- // ═══ grant / access / join ═══
464
- server.tool('relay_grant_create', '에이전트 또는 Organization의 접근 코드를 생성합니다', {
465
- agent_slug: zod_1.z.string().optional().describe('에이전트 slug (agent 접근 코드 생성 시)'),
466
- org_slug: zod_1.z.string().optional().describe('Organization slug (org 접근 코드 생성 시)'),
467
- max_uses: zod_1.z.number().optional().describe('최대 사용 횟수'),
468
- expires_at: zod_1.z.string().optional().describe('만료일 (ISO 8601)'),
469
- }, async ({ agent_slug, org_slug, max_uses, expires_at }) => {
470
- try {
471
- if (!agent_slug && !org_slug) {
472
- return { content: [jsonText({ error: 'MISSING_OPTION', message: 'agent_slug 또는 org_slug가 필요합니다.' })], isError: true };
473
- }
474
- const token = await (0, config_js_1.getValidToken)();
475
- if (!token)
476
- return { content: [jsonText({ error: 'LOGIN_REQUIRED', message: '로그인이 필요합니다.' })], isError: true };
477
- let agentId;
478
- let orgId;
479
- if (agent_slug) {
480
- const res = await fetch(`${config_js_1.API_URL}/api/agents/${agent_slug}`, { headers: { Authorization: `Bearer ${token}` } });
481
- if (!res.ok)
482
- throw new Error('에이전트를 찾을 수 없습니다.');
483
- agentId = (await res.json()).id;
484
- }
485
- if (org_slug) {
486
- const res = await fetch(`${config_js_1.API_URL}/api/orgs/${org_slug}`, { headers: { Authorization: `Bearer ${token}` } });
487
- if (!res.ok)
488
- throw new Error('Organization을 찾을 수 없습니다.');
489
- orgId = (await res.json()).id;
490
- }
491
- const { createAccessCode } = await import('../commands/grant.js');
492
- const result = await createAccessCode({
493
- type: agentId ? 'agent' : 'org',
494
- agent_id: agentId,
495
- org_id: orgId,
496
- max_uses,
497
- expires_at,
498
- });
499
- return { content: [jsonText({ status: 'created', ...result })] };
500
- }
501
- catch (err) {
502
- return { content: [jsonText({ error: String(err) })], isError: true };
503
- }
504
- });
505
- server.tool('relay_grant_use', '접근 코드를 사용하여 org 가입 또는 에이전트 접근 권한을 획득합니다', {
506
- code: zod_1.z.string().describe('접근 코드'),
507
- }, async ({ code }) => {
508
- try {
509
- const { useAccessCode } = await import('../commands/grant.js');
510
- const result = await useAccessCode(code);
511
- return { content: [jsonText({ ...result, status: 'ok' })] };
512
- }
513
- catch (err) {
514
- return { content: [jsonText({ error: String(err) })], isError: true };
515
- }
516
- });
517
- server.tool('relay_access', '접근 코드로 비공개 에이전트 접근 권한을 획득합니다 (설치는 별도로 relay_install 호출 필요)', {
518
- slug: zod_1.z.string().describe('에이전트 slug'),
519
- code: zod_1.z.string().describe('접근 코드'),
520
- }, async ({ slug: slugInput, code }) => {
521
- try {
522
- const token = await (0, config_js_1.getValidToken)();
523
- if (!token)
524
- return { content: [jsonText({ error: 'LOGIN_REQUIRED', message: '로그인이 필요합니다.' })], isError: true };
525
- const res = await fetch(`${config_js_1.API_URL}/api/agents/${slugInput}/claim-access`, {
526
- method: 'POST',
527
- headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
528
- body: JSON.stringify({ code }),
529
- });
530
- if (!res.ok) {
531
- const body = await res.json().catch(() => ({}));
532
- throw new Error(body.message ?? `접근 권한 획득 실패 (${res.status})`);
533
- }
534
- const result = await res.json();
535
- return { content: [jsonText({ status: 'ok', ...result })] };
536
- }
537
- catch (err) {
538
- return { content: [jsonText({ error: String(err) })], isError: true };
539
- }
540
- });
541
- server.tool('relay_join', '초대 코드로 Organization에 가입합니다', {
542
- code: zod_1.z.string().describe('초대 코드 (UUID)'),
543
- }, async ({ code }) => {
544
- try {
545
- const { useAccessCode } = await import('../commands/grant.js');
546
- const result = await useAccessCode(code);
547
- return { content: [jsonText({ ...result, status: 'ok' })] };
548
- }
549
- catch (err) {
550
- return { content: [jsonText({ error: String(err) })], isError: true };
551
- }
552
- });
553
- // ═══ relay_deploy_record — 배치 파일 기록 ═══
554
- server.tool('relay_deploy_record', '에이전트 파일 배치 정보를 installed.json에 기록합니다', {
555
- slug: zod_1.z.string().describe('에이전트 slug'),
556
- scope: zod_1.z.enum(['global', 'local']).describe('배치 범위 (global 또는 local)'),
557
- files: zod_1.z.array(zod_1.z.string()).optional().describe('배치된 파일 경로 목록'),
558
- }, async ({ slug: slugInput, scope, files }) => {
559
- try {
560
- const { isScopedSlug, parseSlug } = await import('../lib/slug.js');
561
- const localRegistry = (0, config_js_1.loadInstalled)();
562
- const globalRegistry = (0, config_js_1.loadGlobalInstalled)();
563
- let slug;
564
- if (isScopedSlug(slugInput)) {
565
- slug = slugInput;
566
- }
567
- else {
568
- const allKeys = [...Object.keys(localRegistry), ...Object.keys(globalRegistry)];
569
- const match = allKeys.find((key) => {
570
- const parsed = parseSlug(key);
571
- return parsed && parsed.name === slugInput;
572
- });
573
- slug = match ?? slugInput;
574
- }
575
- const entry = localRegistry[slug] ?? globalRegistry[slug];
576
- if (!entry) {
577
- return { content: [jsonText({ error: 'NOT_INSTALLED', message: `'${slugInput}'는 설치되어 있지 않습니다.` })], isError: true };
578
- }
579
- entry.deploy_scope = scope;
580
- entry.deployed_files = files ?? [];
581
- if (scope === 'global') {
582
- globalRegistry[slug] = entry;
583
- (0, config_js_1.saveGlobalInstalled)(globalRegistry);
584
- if (localRegistry[slug]) {
585
- localRegistry[slug] = entry;
586
- (0, config_js_1.saveInstalled)(localRegistry);
587
- }
588
- }
589
- else {
590
- localRegistry[slug] = entry;
591
- (0, config_js_1.saveInstalled)(localRegistry);
592
- }
593
- return { content: [jsonText({ status: 'ok', slug, deploy_scope: scope, deployed_files: (files ?? []).length })] };
594
- }
595
- catch (err) {
596
- return { content: [jsonText({ error: String(err) })], isError: true };
597
- }
598
- });
599
- // ═══ relay_login — device code 로그인 ═══
600
- server.tool('relay_login', 'Device Code 방식으로 로그인합니다. URL과 코드를 사용자에게 보여주고, 승인을 기다립니다.', {}, async () => {
601
- try {
602
- // 이미 로그인되어 있는지 확인
603
- const existingToken = await (0, config_js_1.getValidToken)();
604
- if (existingToken) {
605
- const { username, email } = await resolveUserInfo(existingToken);
606
- return { content: [jsonText({ status: 'already_authenticated', username, email })] };
607
- }
608
- // Device code 발급
609
- const res = await fetch(`${config_js_1.API_URL}/api/auth/device/request`, { method: 'POST' });
610
- if (!res.ok)
611
- throw new Error('Device code 발급에 실패했습니다');
612
- const { device_code, user_code, verification_url, expires_in } = await res.json();
613
- // 브라우저 열기 시도
614
- try {
615
- const { execSync } = await import('child_process');
616
- if (process.platform === 'darwin')
617
- execSync(`open "${verification_url}?user_code=${user_code}"`, { stdio: 'ignore' });
618
- else if (process.platform === 'win32')
619
- execSync(`start "" "${verification_url}?user_code=${user_code}"`, { stdio: 'ignore' });
620
- else
621
- execSync(`xdg-open "${verification_url}?user_code=${user_code}"`, { stdio: 'ignore' });
622
- }
623
- catch { /* 브라우저 열기 실패 — 사용자가 직접 열어야 함 */ }
624
- // Polling (최대 expires_in 초, 5초 간격)
625
- const deadline = Date.now() + expires_in * 1000;
626
- while (Date.now() < deadline) {
627
- await new Promise((r) => setTimeout(r, 5000));
628
- const pollRes = await fetch(`${config_js_1.API_URL}/api/auth/device/poll`, {
629
- method: 'POST',
630
- headers: { 'Content-Type': 'application/json' },
631
- body: JSON.stringify({ device_code }),
632
- });
633
- if (!pollRes.ok)
634
- continue;
635
- const data = await pollRes.json();
636
- if (data.status === 'approved' && data.token) {
637
- const { saveTokenData, ensureGlobalRelayDir } = await import('../lib/config.js');
638
- ensureGlobalRelayDir();
639
- saveTokenData({
640
- access_token: data.token,
641
- refresh_token: data.refresh_token,
642
- expires_at: data.expires_at ? Number(data.expires_at) : undefined,
643
- });
644
- const { username, email } = await resolveUserInfo(data.token);
645
- return { content: [jsonText({ status: 'ok', message: '로그인 완료', username, email })] };
646
- }
647
- }
648
- return { content: [jsonText({ status: 'timeout', verification_url, user_code, message: `브라우저에서 ${verification_url} 을 열고 코드 ${user_code} 를 입력해주세요.` })], isError: true };
649
- }
650
- catch (err) {
651
- return { content: [jsonText({ error: String(err) })], isError: true };
652
- }
653
- });
654
- // ═══ relay_guide — 에이전트 설치 가이드 조회 ═══
655
- server.tool('relay_guide', '에이전트 설치 가이드를 조회합니다. URL을 fetch할 수 없는 샌드박스 환경에서 사용하세요.', {
656
- slug: zod_1.z.string().describe('에이전트 slug (예: @owner/name)'),
657
- code: zod_1.z.string().optional().describe('접근 코드 (비공개 에이전트용)'),
658
- }, async ({ slug: slugInput, code }) => {
659
- try {
660
- const parsed = await (0, slug_js_1.resolveSlug)(slugInput);
661
- let url = `${config_js_1.API_URL}/api/registry/${parsed.owner}/${parsed.name}/guide.md`;
662
- if (code)
663
- url += `?code=${encodeURIComponent(code)}`;
664
- const res = await fetch(url);
665
- if (!res.ok) {
666
- if (res.status === 404)
667
- throw new Error('에이전트를 찾을 수 없습니다.');
668
- throw new Error(`가이드를 가져올 수 없습니다 (${res.status})`);
669
- }
670
- const guide = await res.text();
671
- return { content: [{ type: 'text', text: guide }] };
672
- }
673
- catch (err) {
674
- return { content: [jsonText({ error: String(err) })], isError: true };
675
- }
676
- });
677
- // ═══ relay_init — slash command 설치 ═══
678
- server.tool('relay_init', 'relay slash command를 설치합니다 (/relay-install, /relay-publish 등)', {}, async () => {
679
- try {
680
- const { installGlobalUserCommands, hasGlobalUserCommands } = await import('../commands/init.js');
681
- if (hasGlobalUserCommands()) {
682
- installGlobalUserCommands(); // 업데이트
683
- return { content: [jsonText({ status: 'updated', message: 'relay slash command가 업데이트되었습니다.' })] };
684
- }
685
- installGlobalUserCommands();
686
- return { content: [jsonText({ status: 'installed', message: 'relay slash command가 설치되었습니다. /relay-install, /relay-publish 등을 사용할 수 있습니다.' })] };
687
- }
688
- catch (err) {
689
- return { content: [jsonText({ error: String(err) })], isError: true };
690
- }
691
- });
692
- // ═══ Detail Images — 상세페이지 이미지 관리 ═══
22
+ const server = new mcp_js_1.McpServer({ name: 'relay', version: pkg.version }, { capabilities: { tools: {} } });
23
+ // ═══ Detail Images — 에이전트 상세페이지 이미지 관리 ═══
693
24
  server.tool('relay_detail_upload', '에이전트 상세페이지 이미지를 업로드합니다. 폴더 내 이미지를 파일명 순으로 정렬하여 업로드합니다 (기존 이미지 전체 교체).', {
694
25
  slug: zod_1.z.string().describe('에이전트 slug'),
695
26
  path: zod_1.z.string().describe('이미지가 있는 폴더 경로 (PNG/GIF/JPEG/WebP)'),
696
27
  }, async ({ slug, path: dirPath }) => {
697
28
  try {
698
- const token = (0, config_js_1.getValidToken)();
29
+ const token = await (0, config_js_1.getValidToken)();
699
30
  if (!token)
700
- return { content: [jsonText({ error: '로그인이 필요합니다. relay login을 먼저 실행하세요.' })], isError: true };
31
+ return { content: [jsonText({ error: 'LOGIN_REQUIRED', message: '로그인이 필요합니다.' })], isError: true };
701
32
  const absPath = path_1.default.resolve(dirPath);
702
33
  if (!fs_1.default.existsSync(absPath) || !fs_1.default.statSync(absPath).isDirectory()) {
703
34
  return { content: [jsonText({ error: `폴더를 찾을 수 없습니다: ${absPath}` })], isError: true };
@@ -728,8 +59,7 @@ function createMcpServer() {
728
59
  return { content: [jsonText({ error: body.message || `업로드 실패 (${res.status})` })], isError: true };
729
60
  }
730
61
  const result = await res.json();
731
- const update = await getCliUpdateWarning();
732
- return { content: [jsonTextWithUpdate({ status: 'uploaded', count: result.count, images: result.detail_images }, update)] };
62
+ return { content: [jsonText({ status: 'uploaded', count: result.count, images: result.detail_images })] };
733
63
  }
734
64
  catch (err) {
735
65
  return { content: [jsonText({ error: String(err) })], isError: true };
@@ -754,9 +84,9 @@ function createMcpServer() {
754
84
  slug: zod_1.z.string().describe('에이전트 slug'),
755
85
  }, async ({ slug }) => {
756
86
  try {
757
- const token = (0, config_js_1.getValidToken)();
87
+ const token = await (0, config_js_1.getValidToken)();
758
88
  if (!token)
759
- return { content: [jsonText({ error: '로그인이 필요합니다.' })], isError: true };
89
+ return { content: [jsonText({ error: 'LOGIN_REQUIRED', message: '로그인이 필요합니다.' })], isError: true };
760
90
  const res = await fetch(`${config_js_1.API_URL}/api/agents/${slug}/detail-images`, {
761
91
  method: 'DELETE',
762
92
  headers: { Authorization: `Bearer ${token}` },