relayax-cli 0.3.57 → 0.3.59
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/package.d.ts +30 -0
- package/dist/commands/package.js +2 -0
- package/dist/index.js +0 -2
- package/dist/mcp/server.js +275 -6
- package/dist/prompts/_setup-environment.md +8 -0
- package/dist/prompts/install.md +4 -13
- package/dist/prompts/publish.md +59 -25
- package/package.json +1 -1
|
@@ -1,10 +1,39 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
type DiffStatus = 'added' | 'modified' | 'deleted' | 'unchanged';
|
|
3
|
+
interface DiffEntry {
|
|
4
|
+
relPath: string;
|
|
5
|
+
status: DiffStatus;
|
|
6
|
+
}
|
|
2
7
|
import type { ContentType } from '../lib/ai-tools.js';
|
|
3
8
|
export interface ContentEntry {
|
|
4
9
|
name: string;
|
|
5
10
|
type: ContentType;
|
|
6
11
|
from: string;
|
|
7
12
|
}
|
|
13
|
+
type ContentDiffStatus = 'modified' | 'unchanged' | 'source_missing';
|
|
14
|
+
interface ContentDiffEntry {
|
|
15
|
+
name: string;
|
|
16
|
+
type: ContentType;
|
|
17
|
+
status: ContentDiffStatus;
|
|
18
|
+
files?: DiffEntry[];
|
|
19
|
+
}
|
|
20
|
+
interface NewItemEntry {
|
|
21
|
+
name: string;
|
|
22
|
+
type: ContentType;
|
|
23
|
+
source: string;
|
|
24
|
+
relativePath: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* contents 매니페스트 기반으로 각 항목의 원본과 .relay/ 복사본을 비교한다.
|
|
28
|
+
*/
|
|
29
|
+
export declare function computeContentsDiff(contents: ContentEntry[], relayDir: string, projectPath: string): {
|
|
30
|
+
diff: ContentDiffEntry[];
|
|
31
|
+
newItems: NewItemEntry[];
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* contents 항목 단위로 from → .relay/ 동기화한다.
|
|
35
|
+
*/
|
|
36
|
+
export declare function syncContentsToRelay(contents: ContentEntry[], contentsDiff: ContentDiffEntry[], relayDir: string, projectPath: string): void;
|
|
8
37
|
/**
|
|
9
38
|
* 패키지 홈 디렉토리를 결정한다.
|
|
10
39
|
* 1. 프로젝트에 .relay/가 있으면 → projectPath/.relay/
|
|
@@ -18,3 +47,4 @@ export declare function resolveRelayDir(projectPath: string, slug?: string): str
|
|
|
18
47
|
*/
|
|
19
48
|
export declare function initGlobalAgentHome(slug: string, yamlData: Record<string, unknown>): string;
|
|
20
49
|
export declare function registerPackage(program: Command): void;
|
|
50
|
+
export {};
|
package/dist/commands/package.js
CHANGED
|
@@ -3,6 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.computeContentsDiff = computeContentsDiff;
|
|
7
|
+
exports.syncContentsToRelay = syncContentsToRelay;
|
|
6
8
|
exports.resolveRelayDir = resolveRelayDir;
|
|
7
9
|
exports.initGlobalAgentHome = initGlobalAgentHome;
|
|
8
10
|
exports.registerPackage = registerPackage;
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,6 @@ const login_js_1 = require("./commands/login.js");
|
|
|
15
15
|
const update_js_1 = require("./commands/update.js");
|
|
16
16
|
const outdated_js_1 = require("./commands/outdated.js");
|
|
17
17
|
const check_update_js_1 = require("./commands/check-update.js");
|
|
18
|
-
const follow_js_1 = require("./commands/follow.js");
|
|
19
18
|
const changelog_js_1 = require("./commands/changelog.js");
|
|
20
19
|
const join_js_1 = require("./commands/join.js");
|
|
21
20
|
const orgs_js_1 = require("./commands/orgs.js");
|
|
@@ -48,7 +47,6 @@ program
|
|
|
48
47
|
(0, update_js_1.registerUpdate)(program);
|
|
49
48
|
(0, outdated_js_1.registerOutdated)(program);
|
|
50
49
|
(0, check_update_js_1.registerCheckUpdate)(program);
|
|
51
|
-
(0, follow_js_1.registerFollow)(program);
|
|
52
50
|
(0, changelog_js_1.registerChangelog)(program);
|
|
53
51
|
(0, join_js_1.registerJoin)(program);
|
|
54
52
|
(0, orgs_js_1.registerOrgs)(program);
|
package/dist/mcp/server.js
CHANGED
|
@@ -79,6 +79,18 @@ async function getCliUpdateWarning() {
|
|
|
79
79
|
function jsonTextWithUpdate(obj, update) {
|
|
80
80
|
return jsonText(update ? { ...obj, ...update } : obj);
|
|
81
81
|
}
|
|
82
|
+
// MCP 서버는 Claude Desktop이 spawn하므로 cwd가 / 등 예측 불가한 경로일 수 있다.
|
|
83
|
+
// project_path가 없을 때 cwd 대신 홈 디렉토리를 fallback으로 사용한다.
|
|
84
|
+
const os_1 = __importDefault(require("os"));
|
|
85
|
+
function resolveMcpProjectPath(projectPath) {
|
|
86
|
+
if (projectPath)
|
|
87
|
+
return projectPath;
|
|
88
|
+
const resolved = (0, paths_js_1.resolveProjectPath)();
|
|
89
|
+
// cwd가 / 또는 비정상적이면 홈 디렉토리 사용
|
|
90
|
+
if (resolved === '/' || resolved === '')
|
|
91
|
+
return os_1.default.homedir();
|
|
92
|
+
return resolved;
|
|
93
|
+
}
|
|
82
94
|
// ─── Server ───
|
|
83
95
|
function createMcpServer() {
|
|
84
96
|
const server = new mcp_js_1.McpServer({ name: 'relay', version: pkg.version }, { capabilities: { tools: {}, prompts: {} } });
|
|
@@ -97,10 +109,10 @@ function createMcpServer() {
|
|
|
97
109
|
});
|
|
98
110
|
server.tool('relay_install', '에이전트를 설치합니다', {
|
|
99
111
|
slug: zod_1.z.string().describe('에이전트 slug (예: @owner/name)'),
|
|
100
|
-
project_path: zod_1.z.string().optional().describe('프로젝트 경로 (기본:
|
|
112
|
+
project_path: zod_1.z.string().optional().describe('프로젝트 경로 (기본: 홈 디렉토리)'),
|
|
101
113
|
}, async ({ slug: slugInput, project_path }) => {
|
|
102
114
|
try {
|
|
103
|
-
const projectPath = project_path
|
|
115
|
+
const projectPath = resolveMcpProjectPath(project_path);
|
|
104
116
|
const token = await (0, config_js_1.getValidToken)();
|
|
105
117
|
const parsed = await (0, slug_js_1.resolveSlug)(slugInput);
|
|
106
118
|
const fullSlug = parsed.full;
|
|
@@ -124,8 +136,27 @@ function createMcpServer() {
|
|
|
124
136
|
(0, config_js_1.saveInstalled)(installed);
|
|
125
137
|
await (0, api_js_1.reportInstall)(agent.id, fullSlug, agent.version);
|
|
126
138
|
(0, api_js_1.sendUsagePing)(agent.id, fullSlug, agent.version);
|
|
139
|
+
// relay.yaml에서 tags, requires 읽기 (scope 판단용)
|
|
140
|
+
let agentTags = [];
|
|
141
|
+
let agentRequires = null;
|
|
142
|
+
let hasRules = false;
|
|
143
|
+
try {
|
|
144
|
+
const relayYamlPath = path_1.default.join(agentDir, 'relay.yaml');
|
|
145
|
+
if (fs_1.default.existsSync(relayYamlPath)) {
|
|
146
|
+
const cfg = js_yaml_1.default.load(fs_1.default.readFileSync(relayYamlPath, 'utf-8'));
|
|
147
|
+
agentTags = cfg.tags ?? [];
|
|
148
|
+
agentRequires = cfg.requires ?? null;
|
|
149
|
+
}
|
|
150
|
+
hasRules = fs_1.default.existsSync(path_1.default.join(agentDir, 'rules')) && fs_1.default.readdirSync(path_1.default.join(agentDir, 'rules')).length > 0;
|
|
151
|
+
}
|
|
152
|
+
catch { /* non-critical */ }
|
|
127
153
|
const cliUpdate = await getCliUpdateWarning();
|
|
128
|
-
return { content: [jsonTextWithUpdate({
|
|
154
|
+
return { content: [jsonTextWithUpdate({
|
|
155
|
+
status: 'ok', agent: agent.name, slug: fullSlug, version: agent.version,
|
|
156
|
+
description: agent.description ?? '', tags: agentTags, requires: agentRequires, has_rules: hasRules,
|
|
157
|
+
files: countFiles(agentDir), install_path: agentDir,
|
|
158
|
+
scope_hint: '설치 후 에이전트 성격에 따라 글로벌/로컬 배치를 사용자에게 물어보세요. tags에 특정 프레임워크가 있거나 rules/가 있으면 로컬 추천, 범용이면 글로벌 추천.',
|
|
159
|
+
}, cliUpdate)] };
|
|
129
160
|
}
|
|
130
161
|
finally {
|
|
131
162
|
(0, storage_js_1.removeTempDir)(tempDir);
|
|
@@ -174,7 +205,7 @@ function createMcpServer() {
|
|
|
174
205
|
server.tool('relay_status', '현재 relay 환경 상태를 표시합니다', {
|
|
175
206
|
project_path: zod_1.z.string().optional().describe('프로젝트 경로'),
|
|
176
207
|
}, async ({ project_path }) => {
|
|
177
|
-
const projectPath = project_path
|
|
208
|
+
const projectPath = resolveMcpProjectPath(project_path);
|
|
178
209
|
const token = await (0, config_js_1.getValidToken)();
|
|
179
210
|
let username;
|
|
180
211
|
if (token)
|
|
@@ -236,7 +267,7 @@ function createMcpServer() {
|
|
|
236
267
|
server.tool('relay_scan', '배포 가능한 스킬/에이전트/커맨드를 스캔합니다', {
|
|
237
268
|
project_path: zod_1.z.string().optional().describe('프로젝트 경로'),
|
|
238
269
|
}, async ({ project_path }) => {
|
|
239
|
-
const projectPath = project_path
|
|
270
|
+
const projectPath = resolveMcpProjectPath(project_path);
|
|
240
271
|
const homeDir = (0, paths_js_1.resolveHome)();
|
|
241
272
|
const sources = [];
|
|
242
273
|
// 로컬
|
|
@@ -260,6 +291,108 @@ function createMcpServer() {
|
|
|
260
291
|
}
|
|
261
292
|
return { content: [jsonText({ sources })] };
|
|
262
293
|
});
|
|
294
|
+
server.tool('relay_package', '소스 디렉토리에서 .relay/로 콘텐츠를 패키징합니다. mode: init(최초 소스 탐색), sync(변경 반영), migrate(source→contents 마이그레이션)', {
|
|
295
|
+
mode: zod_1.z.enum(['init', 'sync', 'migrate']).describe('패키징 모드'),
|
|
296
|
+
project_path: zod_1.z.string().optional().describe('프로젝트 경로'),
|
|
297
|
+
}, async ({ mode, project_path }) => {
|
|
298
|
+
try {
|
|
299
|
+
const projectPath = resolveMcpProjectPath(project_path);
|
|
300
|
+
const homeDir = (0, paths_js_1.resolveHome)();
|
|
301
|
+
const relayDir = path_1.default.join(projectPath, '.relay');
|
|
302
|
+
const relayYamlPath = path_1.default.join(relayDir, 'relay.yaml');
|
|
303
|
+
if (mode === 'init') {
|
|
304
|
+
// 최초 패키징: 소스 탐색
|
|
305
|
+
const { detectGlobalCLIs } = await import('../lib/ai-tools.js');
|
|
306
|
+
const localTools = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
|
|
307
|
+
const globalTools = detectGlobalCLIs(homeDir);
|
|
308
|
+
const sources = [];
|
|
309
|
+
for (const tool of localTools) {
|
|
310
|
+
const items = (0, ai_tools_js_1.scanLocalItems)(projectPath, tool);
|
|
311
|
+
if (items.length > 0)
|
|
312
|
+
sources.push({ path: tool.skillsDir, location: 'local', name: tool.name, items: items.map((i) => ({ name: i.name, type: i.type, relativePath: i.relativePath })) });
|
|
313
|
+
}
|
|
314
|
+
for (const tool of globalTools) {
|
|
315
|
+
const items = (0, ai_tools_js_1.scanGlobalItems)(tool, homeDir);
|
|
316
|
+
if (items.length > 0)
|
|
317
|
+
sources.push({ path: `~/${tool.skillsDir}`, location: 'global', name: `${tool.name} (global)`, items: items.map((i) => ({ name: i.name, type: i.type, relativePath: i.relativePath })) });
|
|
318
|
+
}
|
|
319
|
+
for (const { tool, basePath } of (0, ai_tools_js_1.detectMountedCLIs)()) {
|
|
320
|
+
const items = (0, ai_tools_js_1.scanMountedItems)(basePath, tool);
|
|
321
|
+
if (items.length > 0)
|
|
322
|
+
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 })) });
|
|
323
|
+
}
|
|
324
|
+
// 기존 글로벌 에이전트 패키지 스캔
|
|
325
|
+
const globalAgentsDir = path_1.default.join(homeDir, '.relay', 'agents');
|
|
326
|
+
const existingAgents = [];
|
|
327
|
+
if (fs_1.default.existsSync(globalAgentsDir)) {
|
|
328
|
+
for (const entry of fs_1.default.readdirSync(globalAgentsDir, { withFileTypes: true })) {
|
|
329
|
+
if (!entry.isDirectory() || entry.name.startsWith('.'))
|
|
330
|
+
continue;
|
|
331
|
+
const agentYaml = path_1.default.join(globalAgentsDir, entry.name, 'relay.yaml');
|
|
332
|
+
if (fs_1.default.existsSync(agentYaml)) {
|
|
333
|
+
try {
|
|
334
|
+
const cfg = js_yaml_1.default.load(fs_1.default.readFileSync(agentYaml, 'utf-8'));
|
|
335
|
+
existingAgents.push({ slug: cfg.slug ?? entry.name, name: cfg.name ?? entry.name, version: cfg.version ?? '0.0.0', path: `~/.relay/agents/${entry.name}` });
|
|
336
|
+
}
|
|
337
|
+
catch { /* skip */ }
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return { content: [jsonText({ status: 'init_required', sources, existing_agents: existingAgents })] };
|
|
342
|
+
}
|
|
343
|
+
// sync / migrate는 relay.yaml이 필요
|
|
344
|
+
if (!fs_1.default.existsSync(relayYamlPath)) {
|
|
345
|
+
return { content: [jsonText({ error: 'NOT_INITIALIZED', message: '.relay/relay.yaml이 없습니다. mode: init으로 먼저 실행하세요.' })], isError: true };
|
|
346
|
+
}
|
|
347
|
+
if (mode === 'migrate') {
|
|
348
|
+
const yamlMigrate = fs_1.default.readFileSync(relayYamlPath, 'utf-8');
|
|
349
|
+
const cfgMigrate = js_yaml_1.default.load(yamlMigrate);
|
|
350
|
+
if (cfgMigrate.contents) {
|
|
351
|
+
return { content: [jsonText({ status: 'already_migrated', message: '이미 contents 형식입니다.' })] };
|
|
352
|
+
}
|
|
353
|
+
const legacySource = cfgMigrate.source;
|
|
354
|
+
if (!legacySource) {
|
|
355
|
+
return { content: [jsonText({ status: 'no_source', message: 'source 필드가 없습니다.' })], isError: true };
|
|
356
|
+
}
|
|
357
|
+
const localTools = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
|
|
358
|
+
const tool = localTools.find((t) => t.skillsDir === legacySource);
|
|
359
|
+
const migratedContents = [];
|
|
360
|
+
if (tool) {
|
|
361
|
+
const items = (0, ai_tools_js_1.scanLocalItems)(projectPath, tool);
|
|
362
|
+
for (const item of items) {
|
|
363
|
+
migratedContents.push({ name: item.name, type: item.type, from: `${legacySource}/${item.relativePath}` });
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
delete cfgMigrate.source;
|
|
367
|
+
cfgMigrate.contents = migratedContents;
|
|
368
|
+
fs_1.default.writeFileSync(relayYamlPath, js_yaml_1.default.dump(cfgMigrate, { lineWidth: 120 }), 'utf-8');
|
|
369
|
+
return { content: [jsonText({ status: 'migrated', contents: migratedContents })] };
|
|
370
|
+
}
|
|
371
|
+
// mode === 'sync'
|
|
372
|
+
const { computeContentsDiff, syncContentsToRelay } = await import('../commands/package.js');
|
|
373
|
+
const yamlContent = fs_1.default.readFileSync(relayYamlPath, 'utf-8');
|
|
374
|
+
const config = js_yaml_1.default.load(yamlContent);
|
|
375
|
+
const contents = config.contents ?? [];
|
|
376
|
+
if (contents.length === 0) {
|
|
377
|
+
return { content: [jsonText({ status: 'no_contents', message: 'relay.yaml에 contents가 없습니다.' })], isError: true };
|
|
378
|
+
}
|
|
379
|
+
const { diff: contentsDiff, newItems } = computeContentsDiff(contents, relayDir, projectPath);
|
|
380
|
+
const hasChanges = contentsDiff.some((d) => d.status === 'modified');
|
|
381
|
+
if (hasChanges) {
|
|
382
|
+
syncContentsToRelay(contents, contentsDiff, relayDir, projectPath);
|
|
383
|
+
}
|
|
384
|
+
const summary = {
|
|
385
|
+
modified: contentsDiff.filter((d) => d.status === 'modified').length,
|
|
386
|
+
unchanged: contentsDiff.filter((d) => d.status === 'unchanged').length,
|
|
387
|
+
source_missing: contentsDiff.filter((d) => d.status === 'source_missing').length,
|
|
388
|
+
new_available: newItems.length,
|
|
389
|
+
};
|
|
390
|
+
return { content: [jsonText({ diff: contentsDiff, new_items: newItems, synced: hasChanges, summary })] };
|
|
391
|
+
}
|
|
392
|
+
catch (err) {
|
|
393
|
+
return { content: [jsonText({ error: String(err) })], isError: true };
|
|
394
|
+
}
|
|
395
|
+
});
|
|
263
396
|
server.tool('relay_org_list', '소속 Organization 목록을 조회합니다', {}, async () => {
|
|
264
397
|
try {
|
|
265
398
|
const token = await (0, config_js_1.getValidToken)();
|
|
@@ -310,7 +443,7 @@ function createMcpServer() {
|
|
|
310
443
|
project_path: zod_1.z.string().optional().describe('프로젝트 경로 (.relay/relay.yaml이 있는 디렉토리)'),
|
|
311
444
|
}, async ({ project_path }) => {
|
|
312
445
|
try {
|
|
313
|
-
const projectPath = project_path
|
|
446
|
+
const projectPath = resolveMcpProjectPath(project_path);
|
|
314
447
|
const relayDir = path_1.default.join(projectPath, '.relay');
|
|
315
448
|
const relayYaml = path_1.default.join(relayDir, 'relay.yaml');
|
|
316
449
|
if (!fs_1.default.existsSync(relayYaml)) {
|
|
@@ -349,6 +482,142 @@ function createMcpServer() {
|
|
|
349
482
|
return { content: [jsonText({ error: String(err) })], isError: true };
|
|
350
483
|
}
|
|
351
484
|
});
|
|
485
|
+
// ═══ grant / access / join ═══
|
|
486
|
+
server.tool('relay_grant_create', '에이전트 또는 Organization의 접근 코드를 생성합니다', {
|
|
487
|
+
agent_slug: zod_1.z.string().optional().describe('에이전트 slug (agent 접근 코드 생성 시)'),
|
|
488
|
+
org_slug: zod_1.z.string().optional().describe('Organization slug (org 접근 코드 생성 시)'),
|
|
489
|
+
max_uses: zod_1.z.number().optional().describe('최대 사용 횟수'),
|
|
490
|
+
expires_at: zod_1.z.string().optional().describe('만료일 (ISO 8601)'),
|
|
491
|
+
}, async ({ agent_slug, org_slug, max_uses, expires_at }) => {
|
|
492
|
+
try {
|
|
493
|
+
if (!agent_slug && !org_slug) {
|
|
494
|
+
return { content: [jsonText({ error: 'MISSING_OPTION', message: 'agent_slug 또는 org_slug가 필요합니다.' })], isError: true };
|
|
495
|
+
}
|
|
496
|
+
const token = await (0, config_js_1.getValidToken)();
|
|
497
|
+
if (!token)
|
|
498
|
+
return { content: [jsonText({ error: 'LOGIN_REQUIRED', message: '로그인이 필요합니다.' })], isError: true };
|
|
499
|
+
let agentId;
|
|
500
|
+
let orgId;
|
|
501
|
+
if (agent_slug) {
|
|
502
|
+
const res = await fetch(`${config_js_1.API_URL}/api/agents/${agent_slug}`, { headers: { Authorization: `Bearer ${token}` } });
|
|
503
|
+
if (!res.ok)
|
|
504
|
+
throw new Error('에이전트를 찾을 수 없습니다.');
|
|
505
|
+
agentId = (await res.json()).id;
|
|
506
|
+
}
|
|
507
|
+
if (org_slug) {
|
|
508
|
+
const res = await fetch(`${config_js_1.API_URL}/api/orgs/${org_slug}`, { headers: { Authorization: `Bearer ${token}` } });
|
|
509
|
+
if (!res.ok)
|
|
510
|
+
throw new Error('Organization을 찾을 수 없습니다.');
|
|
511
|
+
orgId = (await res.json()).id;
|
|
512
|
+
}
|
|
513
|
+
const { createAccessCode } = await import('../commands/grant.js');
|
|
514
|
+
const result = await createAccessCode({
|
|
515
|
+
type: agentId ? 'agent' : 'org',
|
|
516
|
+
agent_id: agentId,
|
|
517
|
+
org_id: orgId,
|
|
518
|
+
max_uses,
|
|
519
|
+
expires_at,
|
|
520
|
+
});
|
|
521
|
+
return { content: [jsonText({ status: 'created', ...result })] };
|
|
522
|
+
}
|
|
523
|
+
catch (err) {
|
|
524
|
+
return { content: [jsonText({ error: String(err) })], isError: true };
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
server.tool('relay_grant_use', '접근 코드를 사용하여 org 가입 또는 에이전트 접근 권한을 획득합니다', {
|
|
528
|
+
code: zod_1.z.string().describe('접근 코드'),
|
|
529
|
+
}, async ({ code }) => {
|
|
530
|
+
try {
|
|
531
|
+
const { useAccessCode } = await import('../commands/grant.js');
|
|
532
|
+
const result = await useAccessCode(code);
|
|
533
|
+
return { content: [jsonText({ ...result, status: 'ok' })] };
|
|
534
|
+
}
|
|
535
|
+
catch (err) {
|
|
536
|
+
return { content: [jsonText({ error: String(err) })], isError: true };
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
server.tool('relay_access', '접근 코드로 비공개 에이전트 접근 권한을 획득합니다 (설치는 별도로 relay_install 호출 필요)', {
|
|
540
|
+
slug: zod_1.z.string().describe('에이전트 slug'),
|
|
541
|
+
code: zod_1.z.string().describe('접근 코드'),
|
|
542
|
+
}, async ({ slug: slugInput, code }) => {
|
|
543
|
+
try {
|
|
544
|
+
const token = await (0, config_js_1.getValidToken)();
|
|
545
|
+
if (!token)
|
|
546
|
+
return { content: [jsonText({ error: 'LOGIN_REQUIRED', message: '로그인이 필요합니다.' })], isError: true };
|
|
547
|
+
const res = await fetch(`${config_js_1.API_URL}/api/agents/${slugInput}/claim-access`, {
|
|
548
|
+
method: 'POST',
|
|
549
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
550
|
+
body: JSON.stringify({ code }),
|
|
551
|
+
});
|
|
552
|
+
if (!res.ok) {
|
|
553
|
+
const body = await res.json().catch(() => ({}));
|
|
554
|
+
throw new Error(body.message ?? `접근 권한 획득 실패 (${res.status})`);
|
|
555
|
+
}
|
|
556
|
+
const result = await res.json();
|
|
557
|
+
return { content: [jsonText({ status: 'ok', ...result })] };
|
|
558
|
+
}
|
|
559
|
+
catch (err) {
|
|
560
|
+
return { content: [jsonText({ error: String(err) })], isError: true };
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
server.tool('relay_join', '초대 코드로 Organization에 가입합니다', {
|
|
564
|
+
code: zod_1.z.string().describe('초대 코드 (UUID)'),
|
|
565
|
+
}, async ({ code }) => {
|
|
566
|
+
try {
|
|
567
|
+
const { useAccessCode } = await import('../commands/grant.js');
|
|
568
|
+
const result = await useAccessCode(code);
|
|
569
|
+
return { content: [jsonText({ ...result, status: 'ok' })] };
|
|
570
|
+
}
|
|
571
|
+
catch (err) {
|
|
572
|
+
return { content: [jsonText({ error: String(err) })], isError: true };
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
// ═══ relay_deploy_record — 배치 파일 기록 ═══
|
|
576
|
+
server.tool('relay_deploy_record', '에이전트 파일 배치 정보를 installed.json에 기록합니다', {
|
|
577
|
+
slug: zod_1.z.string().describe('에이전트 slug'),
|
|
578
|
+
scope: zod_1.z.enum(['global', 'local']).describe('배치 범위 (global 또는 local)'),
|
|
579
|
+
files: zod_1.z.array(zod_1.z.string()).optional().describe('배치된 파일 경로 목록'),
|
|
580
|
+
}, async ({ slug: slugInput, scope, files }) => {
|
|
581
|
+
try {
|
|
582
|
+
const { isScopedSlug, parseSlug } = await import('../lib/slug.js');
|
|
583
|
+
const localRegistry = (0, config_js_1.loadInstalled)();
|
|
584
|
+
const globalRegistry = (0, config_js_1.loadGlobalInstalled)();
|
|
585
|
+
let slug;
|
|
586
|
+
if (isScopedSlug(slugInput)) {
|
|
587
|
+
slug = slugInput;
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
const allKeys = [...Object.keys(localRegistry), ...Object.keys(globalRegistry)];
|
|
591
|
+
const match = allKeys.find((key) => {
|
|
592
|
+
const parsed = parseSlug(key);
|
|
593
|
+
return parsed && parsed.name === slugInput;
|
|
594
|
+
});
|
|
595
|
+
slug = match ?? slugInput;
|
|
596
|
+
}
|
|
597
|
+
const entry = localRegistry[slug] ?? globalRegistry[slug];
|
|
598
|
+
if (!entry) {
|
|
599
|
+
return { content: [jsonText({ error: 'NOT_INSTALLED', message: `'${slugInput}'는 설치되어 있지 않습니다.` })], isError: true };
|
|
600
|
+
}
|
|
601
|
+
entry.deploy_scope = scope;
|
|
602
|
+
entry.deployed_files = files ?? [];
|
|
603
|
+
if (scope === 'global') {
|
|
604
|
+
globalRegistry[slug] = entry;
|
|
605
|
+
(0, config_js_1.saveGlobalInstalled)(globalRegistry);
|
|
606
|
+
if (localRegistry[slug]) {
|
|
607
|
+
localRegistry[slug] = entry;
|
|
608
|
+
(0, config_js_1.saveInstalled)(localRegistry);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
localRegistry[slug] = entry;
|
|
613
|
+
(0, config_js_1.saveInstalled)(localRegistry);
|
|
614
|
+
}
|
|
615
|
+
return { content: [jsonText({ status: 'ok', slug, deploy_scope: scope, deployed_files: (files ?? []).length })] };
|
|
616
|
+
}
|
|
617
|
+
catch (err) {
|
|
618
|
+
return { content: [jsonText({ error: String(err) })], isError: true };
|
|
619
|
+
}
|
|
620
|
+
});
|
|
352
621
|
// ═══ relay_login — device code 로그인 ═══
|
|
353
622
|
server.tool('relay_login', 'Device Code 방식으로 로그인합니다. URL과 코드를 사용자에게 보여주고, 승인을 기다립니다.', {}, async () => {
|
|
354
623
|
try {
|
|
@@ -59,9 +59,17 @@ cat ~/Library/Application\ Support/Claude/claude_desktop_config.json 2>/dev/null
|
|
|
59
59
|
| "relay install X" | `relay install X` | `relay_install` tool 호출 (`slug: "X"`) |
|
|
60
60
|
| "relay publish" | `relay publish` | `relay_publish` tool 호출 |
|
|
61
61
|
| "relay login" | `relay login` | `relay_login` tool 호출 |
|
|
62
|
+
| "relay package --init" | `relay package --init --json` | `relay_package` tool 호출 (`mode: "init"`) |
|
|
63
|
+
| "relay package --sync" | `relay package --sync --json` | `relay_package` tool 호출 (`mode: "sync"`) |
|
|
64
|
+
| "relay package --migrate" | `relay package --migrate --json` | `relay_package` tool 호출 (`mode: "migrate"`) |
|
|
62
65
|
| "relay scan" | `relay package --init` | `relay_scan` tool 호출 |
|
|
63
66
|
| "relay check-update X" | `relay check-update X` | `relay_check_update` tool 호출 (`slug: "X"`) |
|
|
64
67
|
| "relay orgs list" | `relay orgs list --json` | `relay_org_list` tool 호출 |
|
|
65
68
|
| "relay orgs create" | `relay orgs create "이름" --json` | `relay_org_create` tool 호출 (`name: "이름"`) |
|
|
69
|
+
| "relay grant create" | `relay grant create --agent <slug>` | `relay_grant_create` tool 호출 (`agent_slug: "<slug>"`) |
|
|
70
|
+
| "relay grant use" | `relay grant use --code <code>` | `relay_grant_use` tool 호출 (`code: "<code>"`) |
|
|
71
|
+
| "relay access" | `relay access <slug> --code <code>` | `relay_access` tool 호출 (`slug`, `code`) |
|
|
72
|
+
| "relay join" | `relay join <slug> --code <code>` | `relay_join` tool 호출 (`code: "<code>"`) |
|
|
73
|
+
| "relay deploy-record" | `relay deploy-record <slug> --scope <scope> --files ...` | `relay_deploy_record` tool 호출 |
|
|
66
74
|
|
|
67
75
|
처음 판별한 환경을 이후 계속 사용합니다.
|
package/dist/prompts/install.md
CHANGED
|
@@ -85,7 +85,9 @@ slug가 직접 주어지면 (`/relay-install @alice/doc-writer`) 이 단계를
|
|
|
85
85
|
3. 코드가 있으면 `relay access <slug> --code <code>`를 실행합니다.
|
|
86
86
|
4. 코드가 없으면 purchase_info의 url로 구매 안내합니다.
|
|
87
87
|
|
|
88
|
-
#### 2-2. 배치 범위 선택 (
|
|
88
|
+
#### 2-2. 배치 범위 선택 (필수 — 이 단계를 절대 건너뛰지 마세요)
|
|
89
|
+
|
|
90
|
+
**반드시 사용자에게 글로벌/로컬 중 어디에 설치할지 물어보세요.** 에이전트 메타데이터(tags, requires 등)가 없어도 반드시 물어봐야 합니다. 메타데이터가 없으면 글로벌을 추천하되, 사용자 확인은 필수입니다.
|
|
89
91
|
|
|
90
92
|
에이전트의 성격을 분석하여 글로벌/로컬 중 적합한 쪽을 추천합니다.
|
|
91
93
|
|
|
@@ -132,18 +134,7 @@ relay deploy-record <slug> --scope <global|local> --files <배치된_파일1> <
|
|
|
132
134
|
|
|
133
135
|
#### 3-1. 완료 안내
|
|
134
136
|
- 배치된 파일과 활성화된 커맨드 목록을 보여줍니다.
|
|
135
|
-
#### 3-2.
|
|
136
|
-
빌더의 username이 JSON 결과에 있으면 **반드시** 사용자 질문 도구를 호출하세요.
|
|
137
|
-
|
|
138
|
-
**사용자 질문 도구 호출:**
|
|
139
|
-
- question: `@{username}을 팔로우할까요? 새 버전 알림을 받을 수 있습니다.`
|
|
140
|
-
- options: `["팔로우", "건너뛰기"]`
|
|
141
|
-
|
|
142
|
-
**응답 처리:**
|
|
143
|
-
- "팔로우" → `relay follow @{username}` 실행. 로그인이 안 되어 있으면 `relay login` 먼저 실행 후 재시도.
|
|
144
|
-
- "건너뛰기" → 다음 단계로 진행
|
|
145
|
-
|
|
146
|
-
#### 3-3. 공유 가이드 (필수 — 설치 완료 시 반드시 표시)
|
|
137
|
+
#### 3-2. 공유 가이드 (필수 — 설치 완료 시 반드시 표시)
|
|
147
138
|
설치 완료 후 아래 공유용 설치 가이드를 표시합니다. 복사 가능한 코드 블록으로 보여줍니다.
|
|
148
139
|
|
|
149
140
|
```
|
package/dist/prompts/publish.md
CHANGED
|
@@ -28,6 +28,13 @@
|
|
|
28
28
|
|
|
29
29
|
#### A. 최초 배포 (.relay/relay.yaml이 없음)
|
|
30
30
|
|
|
31
|
+
**환경 B (MCP)의 경우:**
|
|
32
|
+
`relay_package` MCP tool (`mode: "init"`)을 사용합니다. CLI의 `relay package --init --json`과 동일한 결과를 반환합니다.
|
|
33
|
+
- 결과의 `sources[]`는 배포할 콘텐츠 **후보** 목록입니다. 전부 배포 대상이 아닙니다.
|
|
34
|
+
- 사용자에게 어떤 콘텐츠를 패키지에 포함할지 반드시 물어보세요.
|
|
35
|
+
|
|
36
|
+
**환경 A (터미널)의 경우:**
|
|
37
|
+
|
|
31
38
|
##### 1단계: 소스 탐색
|
|
32
39
|
|
|
33
40
|
`relay package --init --json` 실행
|
|
@@ -172,6 +179,14 @@ contents:
|
|
|
172
179
|
|
|
173
180
|
#### B. 재배포 (.relay/relay.yaml이 있음)
|
|
174
181
|
|
|
182
|
+
**환경 B (MCP)의 경우:**
|
|
183
|
+
`relay_package` MCP tool을 사용합니다:
|
|
184
|
+
- `mode: "migrate"` — B-0 마이그레이션 (source → contents)
|
|
185
|
+
- `mode: "sync"` — B-1~B-2 동기화 + 변경 반영
|
|
186
|
+
- sync 결과의 `new_items`가 있으면 B-3 새 콘텐츠 추가도 진행합니다.
|
|
187
|
+
|
|
188
|
+
**환경 A (터미널)의 경우:**
|
|
189
|
+
|
|
175
190
|
##### B-0. 기존 source 필드 마이그레이션
|
|
176
191
|
|
|
177
192
|
relay.yaml에 기존 `source` 필드만 있고 `contents`가 없으면:
|
|
@@ -263,45 +278,64 @@ relay.yaml의 현재 `version`을 읽고 semver 범프를 제안합니다.
|
|
|
263
278
|
- 유지 외 선택 → relay.yaml의 version을 선택된 값으로 업데이트
|
|
264
279
|
- 유지 → 그대로 진행
|
|
265
280
|
|
|
266
|
-
### Step 1. 공개 범위
|
|
281
|
+
### Step 1. Organization 선택 & 공개 범위 설정
|
|
267
282
|
|
|
268
|
-
|
|
283
|
+
#### 1-1. Organization 확인 (먼저 실행)
|
|
269
284
|
|
|
270
|
-
|
|
285
|
+
Organization 목록을 조회합니다:
|
|
286
|
+
- 환경 A: `relay orgs list --json` 실행
|
|
287
|
+
- 환경 B: `relay_org_list` MCP tool 호출
|
|
271
288
|
|
|
272
|
-
|
|
273
|
-
-
|
|
274
|
-
-
|
|
289
|
+
**Org가 0개이면:**
|
|
290
|
+
- **사용자 질문 도구 호출:**
|
|
291
|
+
- question: "Organization이 없습니다. 비공개 배포를 하려면 Organization이 필요합니다. Organization을 만들까요?"
|
|
292
|
+
- options: `["Organization 생성", "Organization 없이 계속 (공개/링크공유만 가능)"]`
|
|
293
|
+
- "Organization 생성" 선택 시:
|
|
294
|
+
- **사용자 질문 도구 호출:** question: "Organization 이름을 입력하세요."
|
|
295
|
+
- 환경 A: `relay orgs create "이름" --json` 실행
|
|
296
|
+
- 환경 B: `relay_org_create` MCP tool 호출
|
|
297
|
+
- 생성 후 org 목록을 갱신합니다.
|
|
298
|
+
|
|
299
|
+
**Org가 1개 이상이면:**
|
|
300
|
+
|
|
301
|
+
**사용자 질문 도구 호출 (Org가 1개여도 반드시 호출):**
|
|
302
|
+
- question: "어떤 Organization에 배포할까요?"
|
|
303
|
+
- options: `["<org1_name> (<org1_slug>)", "<org2_name> (<org2_slug>)", ..., "Organization 없이 배포 (공개/링크공유)"]`
|
|
304
|
+
- **중요: Org가 1개라도 자동 선택하지 말고 반드시 사용자에게 확인받으세요.**
|
|
275
305
|
|
|
276
306
|
**응답 처리:**
|
|
277
|
-
-
|
|
278
|
-
- "
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
- 환경 A: `relay orgs create "이름" --json` 실행
|
|
284
|
-
- 환경 B: `relay_org_create` MCP tool 호출 (tool이 없으면 사용자에게 "www.relayax.com/orgs 에서 Organization을 생성하세요"라고 안내)
|
|
285
|
-
- **사용자 질문 도구 호출:**
|
|
286
|
-
- question: "비공개 배포를 위해 Organization을 만들어야 합니다. Organization 이름을 입력하세요."
|
|
287
|
-
- 생성 후 해당 org를 선택하여 계속 진행합니다.
|
|
307
|
+
- Org 선택 → relay.yaml에 `org: <selected_slug>` 저장, 1-2단계로
|
|
308
|
+
- "Organization 없이 배포" → org 없이 1-2단계로 (비공개 옵션 제외)
|
|
309
|
+
|
|
310
|
+
#### 1-2. 공개 범위 선택
|
|
311
|
+
|
|
312
|
+
relay.yaml의 `visibility` 설정을 확인합니다.
|
|
288
313
|
|
|
289
|
-
|
|
290
|
-
- question: "어떤 Organization에 배포할까요?"
|
|
291
|
-
- options: `["<org1_name>", "<org2_name>", ...]`
|
|
292
|
-
- **중요: Org가 1개라도 자동 선택하지 말고 반드시 사용자에게 확인받으세요.**
|
|
314
|
+
**신규 배포 (visibility 미설정):**
|
|
293
315
|
|
|
294
|
-
|
|
316
|
+
Org가 선택된 경우:
|
|
317
|
+
- **사용자 질문 도구 호출:**
|
|
318
|
+
- question: "{org_name} Organization에 배포합니다. 공개 범위를 선택하세요"
|
|
319
|
+
- options: `["public — 외부인 포함 누구나 설치", "internal — 조직 구성원 누구나 설치", "private — 조직 내에서도 허가받은 사람만 설치"]`
|
|
320
|
+
|
|
321
|
+
Org가 없는 경우 (개인 배포):
|
|
322
|
+
- **사용자 질문 도구 호출:**
|
|
323
|
+
- question: "공개 범위를 선택하세요"
|
|
324
|
+
- options: `["public — 누구나 검색하여 설치 가능", "private — 접근 링크를 받은 사람만 설치 가능"]`
|
|
325
|
+
|
|
326
|
+
**응답 처리:**
|
|
295
327
|
|
|
296
|
-
|
|
328
|
+
- "public" → relay.yaml에 `visibility: public` 저장
|
|
329
|
+
- "internal" → relay.yaml에 `visibility: internal` 저장
|
|
330
|
+
- "private" → relay.yaml에 `visibility: private` 저장. 배포 후 웹 대시보드에서 접근 코드를 생성하여 공유할 수 있다고 안내.
|
|
297
331
|
|
|
298
|
-
|
|
332
|
+
**재배포 (visibility 이미 설정됨):**
|
|
299
333
|
|
|
300
334
|
**사용자 질문 도구 호출:**
|
|
301
335
|
- question: 공개일 때 "현재 **공개** 설정입니다. 유지할까요?", 링크공유일 때 "현재 **링크 공유** 설정입니다. 접근 링크가 있는 사람만 설치 가능합니다. 유지할까요?", 비공개일 때 "현재 **비공개** 설정입니다 (Org: {name}). 유지할까요?"
|
|
302
336
|
- options: `["유지", "변경"]`
|
|
303
337
|
|
|
304
|
-
"변경" →
|
|
338
|
+
"변경" → 1-1부터 다시 진행
|
|
305
339
|
|
|
306
340
|
### Step 2. 보안 점검 & requires 확인
|
|
307
341
|
|