relayax-cli 0.3.50 → 0.3.52
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/create.js +2 -0
- package/dist/commands/feedback.d.ts +2 -0
- package/dist/commands/feedback.js +71 -0
- package/dist/commands/install.js +4 -0
- package/dist/commands/join.js +2 -0
- package/dist/commands/login.js +4 -0
- package/dist/commands/ping.js +2 -4
- package/dist/commands/publish.d.ts +54 -0
- package/dist/commands/publish.js +18 -4
- package/dist/commands/search.js +2 -0
- package/dist/index.js +2 -0
- package/dist/lib/api.d.ts +1 -1
- package/dist/lib/api.js +4 -1
- package/dist/lib/command-adapter.js +35 -4
- package/dist/lib/error-report.d.ts +5 -0
- package/dist/lib/error-report.js +28 -0
- package/dist/lib/preamble.d.ts +3 -3
- package/dist/lib/preamble.js +25 -13
- package/dist/lib/step-tracker.d.ts +8 -0
- package/dist/lib/step-tracker.js +28 -0
- package/dist/mcp/server.js +151 -64
- package/dist/prompts/_setup-environment.md +2 -1
- package/dist/prompts/_setup-login.md +10 -11
- package/package.json +1 -1
package/dist/commands/create.js
CHANGED
|
@@ -8,6 +8,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
10
10
|
const ai_tools_js_1 = require("../lib/ai-tools.js");
|
|
11
|
+
const step_tracker_js_1 = require("../lib/step-tracker.js");
|
|
11
12
|
const command_adapter_js_1 = require("../lib/command-adapter.js");
|
|
12
13
|
const init_js_1 = require("./init.js");
|
|
13
14
|
const slug_js_1 = require("../lib/slug.js");
|
|
@@ -33,6 +34,7 @@ function registerCreate(program) {
|
|
|
33
34
|
.option('--project <dir>', '프로젝트 루트 경로 (기본: cwd, 환경변수: RELAY_PROJECT_PATH)')
|
|
34
35
|
.action(async (name, opts) => {
|
|
35
36
|
const json = program.opts().json ?? false;
|
|
37
|
+
(0, step_tracker_js_1.trackCommand)('create');
|
|
36
38
|
const projectPath = (0, paths_js_1.resolveProjectPath)(opts.project);
|
|
37
39
|
const relayDir = path_1.default.join(projectPath, '.relay');
|
|
38
40
|
const relayYamlPath = path_1.default.join(relayDir, 'relay.yaml');
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerFeedback = registerFeedback;
|
|
4
|
+
const config_js_1 = require("../lib/config.js");
|
|
5
|
+
const device_hash_js_1 = require("../lib/device-hash.js");
|
|
6
|
+
function registerFeedback(program) {
|
|
7
|
+
program
|
|
8
|
+
.command('feedback <message>')
|
|
9
|
+
.description('피드백을 전송합니다')
|
|
10
|
+
.action(async (message) => {
|
|
11
|
+
const json = program.opts().json ?? false;
|
|
12
|
+
const deviceHash = (0, device_hash_js_1.getDeviceHash)();
|
|
13
|
+
// 설치된 에이전트 slug 목록
|
|
14
|
+
const installed = (0, config_js_1.loadInstalled)();
|
|
15
|
+
const installedAgents = Object.keys(installed);
|
|
16
|
+
// 로그인 사용자 정보 (optional)
|
|
17
|
+
let userId;
|
|
18
|
+
let username;
|
|
19
|
+
const token = await (0, config_js_1.getValidToken)();
|
|
20
|
+
if (token) {
|
|
21
|
+
try {
|
|
22
|
+
const res = await fetch(`${config_js_1.API_URL}/api/auth/me`, {
|
|
23
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
24
|
+
signal: AbortSignal.timeout(5000),
|
|
25
|
+
});
|
|
26
|
+
if (res.ok) {
|
|
27
|
+
const me = (await res.json());
|
|
28
|
+
userId = me.id;
|
|
29
|
+
username = me.username;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// ignore
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const res = await fetch(`${config_js_1.API_URL}/api/feedback`, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: { 'Content-Type': 'application/json' },
|
|
40
|
+
body: JSON.stringify({
|
|
41
|
+
message,
|
|
42
|
+
user_id: userId ?? null,
|
|
43
|
+
username: username ?? null,
|
|
44
|
+
device_hash: deviceHash,
|
|
45
|
+
installed_agents: installedAgents.length > 0 ? installedAgents : null,
|
|
46
|
+
}),
|
|
47
|
+
signal: AbortSignal.timeout(10000),
|
|
48
|
+
});
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
const body = await res.text().catch(() => '');
|
|
51
|
+
throw new Error(`서버 응답 오류 (${res.status}): ${body}`);
|
|
52
|
+
}
|
|
53
|
+
if (json) {
|
|
54
|
+
console.log(JSON.stringify({ status: 'ok', message: '피드백이 전송되었습니다' }));
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
console.log('\x1b[32m✓ 피드백이 전송되었습니다. 감사합니다!\x1b[0m');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
62
|
+
if (json) {
|
|
63
|
+
console.error(JSON.stringify({ error: 'FEEDBACK_FAILED', message: errMsg }));
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
console.error(`\x1b[31m피드백 전송 실패: ${errMsg}\x1b[0m`);
|
|
67
|
+
}
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
package/dist/commands/install.js
CHANGED
|
@@ -13,6 +13,8 @@ const slug_js_1 = require("../lib/slug.js");
|
|
|
13
13
|
const preamble_js_1 = require("../lib/preamble.js");
|
|
14
14
|
const init_js_1 = require("./init.js");
|
|
15
15
|
const paths_js_1 = require("../lib/paths.js");
|
|
16
|
+
const error_report_js_1 = require("../lib/error-report.js");
|
|
17
|
+
const step_tracker_js_1 = require("../lib/step-tracker.js");
|
|
16
18
|
function registerInstall(program) {
|
|
17
19
|
program
|
|
18
20
|
.command('install <slug>')
|
|
@@ -30,6 +32,7 @@ function registerInstall(program) {
|
|
|
30
32
|
}
|
|
31
33
|
(0, init_js_1.installGlobalUserCommands)();
|
|
32
34
|
}
|
|
35
|
+
(0, step_tracker_js_1.trackCommand)('install', { slug: slugInput });
|
|
33
36
|
try {
|
|
34
37
|
// Resolve scoped slug and fetch agent metadata
|
|
35
38
|
let agent;
|
|
@@ -258,6 +261,7 @@ function registerInstall(program) {
|
|
|
258
261
|
}
|
|
259
262
|
catch (err) {
|
|
260
263
|
const message = err instanceof Error ? err.message : String(err);
|
|
264
|
+
(0, error_report_js_1.reportCliError)('install', 'INSTALL_FAILED', message);
|
|
261
265
|
console.error(JSON.stringify({ error: 'INSTALL_FAILED', message, fix: message }));
|
|
262
266
|
process.exit(1);
|
|
263
267
|
}
|
package/dist/commands/join.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.joinOrg = joinOrg;
|
|
4
4
|
exports.registerJoin = registerJoin;
|
|
5
5
|
const config_js_1 = require("../lib/config.js");
|
|
6
|
+
const step_tracker_js_1 = require("../lib/step-tracker.js");
|
|
6
7
|
const init_js_1 = require("./init.js");
|
|
7
8
|
async function joinOrg(orgSlug, code) {
|
|
8
9
|
const token = await (0, config_js_1.getValidToken)();
|
|
@@ -36,6 +37,7 @@ function registerJoin(program) {
|
|
|
36
37
|
.requiredOption('--code <code>', '초대 코드 (UUID)')
|
|
37
38
|
.action(async (slug, opts) => {
|
|
38
39
|
const json = program.opts().json ?? false;
|
|
40
|
+
(0, step_tracker_js_1.trackCommand)('join', { slug });
|
|
39
41
|
if (!(0, init_js_1.hasGlobalUserCommands)()) {
|
|
40
42
|
if (!json) {
|
|
41
43
|
console.error('\x1b[33m⚠ relay init이 실행되지 않았습니다. 먼저 relay init을 실행하세요.\x1b[0m');
|
package/dist/commands/login.js
CHANGED
|
@@ -8,6 +8,8 @@ exports.registerLogin = registerLogin;
|
|
|
8
8
|
const http_1 = __importDefault(require("http"));
|
|
9
9
|
const child_process_1 = require("child_process");
|
|
10
10
|
const config_js_1 = require("../lib/config.js");
|
|
11
|
+
const error_report_js_1 = require("../lib/error-report.js");
|
|
12
|
+
const step_tracker_js_1 = require("../lib/step-tracker.js");
|
|
11
13
|
function openBrowser(url) {
|
|
12
14
|
const platform = process.platform;
|
|
13
15
|
try {
|
|
@@ -183,6 +185,7 @@ function registerLogin(program) {
|
|
|
183
185
|
.action(async (opts) => {
|
|
184
186
|
const json = program.opts().json ?? false;
|
|
185
187
|
(0, config_js_1.ensureGlobalRelayDir)();
|
|
188
|
+
(0, step_tracker_js_1.trackCommand)('login');
|
|
186
189
|
let accessToken = opts.token;
|
|
187
190
|
let refreshToken;
|
|
188
191
|
let expiresAt;
|
|
@@ -196,6 +199,7 @@ function registerLogin(program) {
|
|
|
196
199
|
}
|
|
197
200
|
catch (err) {
|
|
198
201
|
const msg = err instanceof Error ? err.message : '로그인 실패';
|
|
202
|
+
(0, error_report_js_1.reportCliError)('login', 'LOGIN_FAILED', msg);
|
|
199
203
|
if (json) {
|
|
200
204
|
console.error(JSON.stringify({ error: 'LOGIN_FAILED', message: msg, fix: opts.device ? '다시 시도하세요.' : 'relay login --device를 시도하세요.' }));
|
|
201
205
|
}
|
package/dist/commands/ping.js
CHANGED
|
@@ -31,10 +31,8 @@ function registerPing(program) {
|
|
|
31
31
|
const entry = local[slug] ?? global[slug];
|
|
32
32
|
const version = entry?.version;
|
|
33
33
|
const agentId = entry?.agent_id;
|
|
34
|
-
// Fire-and-forget ping (agent_id
|
|
35
|
-
|
|
36
|
-
await (0, api_js_1.sendUsagePing)(agentId, slug, version);
|
|
37
|
-
}
|
|
34
|
+
// Fire-and-forget ping (agent_id 없어도 slug fallback으로 전송)
|
|
35
|
+
await (0, api_js_1.sendUsagePing)(agentId ?? null, slug, version);
|
|
38
36
|
if (!opts.quiet) {
|
|
39
37
|
console.log(`RELAY_READY: ${slug}`);
|
|
40
38
|
}
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
interface CommandEntry {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
}
|
|
6
|
+
interface Components {
|
|
7
|
+
agents: number;
|
|
8
|
+
rules: number;
|
|
9
|
+
skills: number;
|
|
10
|
+
}
|
|
2
11
|
export interface RequiresCli {
|
|
3
12
|
name: string;
|
|
4
13
|
install?: string;
|
|
@@ -35,4 +44,49 @@ export interface Requires {
|
|
|
35
44
|
};
|
|
36
45
|
permissions?: string[];
|
|
37
46
|
}
|
|
47
|
+
interface AgentDetail {
|
|
48
|
+
name: string;
|
|
49
|
+
description: string;
|
|
50
|
+
uses: string[];
|
|
51
|
+
}
|
|
52
|
+
interface SkillDetail {
|
|
53
|
+
name: string;
|
|
54
|
+
description: string;
|
|
55
|
+
uses: string[];
|
|
56
|
+
}
|
|
57
|
+
export interface PublishMetadata {
|
|
58
|
+
slug: string;
|
|
59
|
+
name: string;
|
|
60
|
+
description: string;
|
|
61
|
+
long_description?: string;
|
|
62
|
+
tags: string[];
|
|
63
|
+
commands: CommandEntry[];
|
|
64
|
+
components: Components;
|
|
65
|
+
version: string;
|
|
66
|
+
changelog?: string;
|
|
67
|
+
requires?: Requires;
|
|
68
|
+
visibility?: 'public' | 'private' | 'internal';
|
|
69
|
+
type?: 'command' | 'passive' | 'hybrid';
|
|
70
|
+
cli_version?: string;
|
|
71
|
+
agent_names?: string[];
|
|
72
|
+
skill_names?: string[];
|
|
73
|
+
agent_details?: AgentDetail[];
|
|
74
|
+
skill_details?: SkillDetail[];
|
|
75
|
+
org_slug?: string;
|
|
76
|
+
}
|
|
77
|
+
export declare function createTarball(agentDir: string): Promise<string>;
|
|
78
|
+
interface PublishResult {
|
|
79
|
+
status: string;
|
|
80
|
+
slug: string;
|
|
81
|
+
version: string;
|
|
82
|
+
url: string;
|
|
83
|
+
profile?: {
|
|
84
|
+
username?: string;
|
|
85
|
+
display_name?: string;
|
|
86
|
+
contact_links?: Record<string, string>;
|
|
87
|
+
default_welcome?: string;
|
|
88
|
+
} | null;
|
|
89
|
+
}
|
|
90
|
+
export declare function publishToApi(token: string, tarPath: string, metadata: PublishMetadata): Promise<PublishResult>;
|
|
38
91
|
export declare function registerPublish(program: Command): void;
|
|
92
|
+
export {};
|
package/dist/commands/publish.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.createTarball = createTarball;
|
|
7
|
+
exports.publishToApi = publishToApi;
|
|
6
8
|
exports.registerPublish = registerPublish;
|
|
7
9
|
const fs_1 = __importDefault(require("fs"));
|
|
8
10
|
const path_1 = __importDefault(require("path"));
|
|
@@ -13,6 +15,8 @@ const config_js_1 = require("../lib/config.js");
|
|
|
13
15
|
const preamble_js_1 = require("../lib/preamble.js");
|
|
14
16
|
const version_check_js_1 = require("../lib/version-check.js");
|
|
15
17
|
const paths_js_1 = require("../lib/paths.js");
|
|
18
|
+
const error_report_js_1 = require("../lib/error-report.js");
|
|
19
|
+
const step_tracker_js_1 = require("../lib/step-tracker.js");
|
|
16
20
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
17
21
|
const cliPkg = require('../../package.json');
|
|
18
22
|
const VALID_DIRS = ['skills', 'agents', 'rules', 'commands', 'bin'];
|
|
@@ -171,7 +175,7 @@ function detectAgentDetails(agentDir, requires) {
|
|
|
171
175
|
* 에이전트 진입점 커맨드(commands/{author}-{name}.md)를 생성한다.
|
|
172
176
|
* root SKILL.md를 대체하여 에이전트의 얼굴 역할을 한다.
|
|
173
177
|
*/
|
|
174
|
-
function generateEntryCommand(config, commands, skills, scopedSlug) {
|
|
178
|
+
function generateEntryCommand(config, commands, skills, scopedSlug, agentDir) {
|
|
175
179
|
const lines = [];
|
|
176
180
|
// Frontmatter
|
|
177
181
|
lines.push('---');
|
|
@@ -179,7 +183,7 @@ function generateEntryCommand(config, commands, skills, scopedSlug) {
|
|
|
179
183
|
lines.push('---');
|
|
180
184
|
lines.push('');
|
|
181
185
|
// Preamble
|
|
182
|
-
lines.push((0, preamble_js_1.generatePreamble)(scopedSlug));
|
|
186
|
+
lines.push((0, preamble_js_1.generatePreamble)(scopedSlug, agentDir));
|
|
183
187
|
lines.push('');
|
|
184
188
|
// Agent header
|
|
185
189
|
lines.push(`## ${config.name}`);
|
|
@@ -292,6 +296,7 @@ function registerPublish(program) {
|
|
|
292
296
|
const relayDir = path_1.default.join(agentDir, '.relay');
|
|
293
297
|
const relayYamlPath = path_1.default.join(relayDir, 'relay.yaml');
|
|
294
298
|
const isTTY = Boolean(process.stdin.isTTY) && !json;
|
|
299
|
+
(0, step_tracker_js_1.trackCommand)('publish', { slug: undefined });
|
|
295
300
|
// CLI update check before publish
|
|
296
301
|
if (isTTY) {
|
|
297
302
|
const cliUpdate = await (0, version_check_js_1.checkCliVersion)(true);
|
|
@@ -306,6 +311,7 @@ function registerPublish(program) {
|
|
|
306
311
|
default: true,
|
|
307
312
|
});
|
|
308
313
|
if (!shouldContinue) {
|
|
314
|
+
(0, error_report_js_1.reportCliError)('publish', 'CANCELLED_CLI_UPDATE', `current:${cliUpdate.current} latest:${cliUpdate.latest}`);
|
|
309
315
|
console.error('\n배포를 취소했습니다. CLI를 업데이트한 후 다시 시도하세요.');
|
|
310
316
|
process.exit(0);
|
|
311
317
|
}
|
|
@@ -319,6 +325,7 @@ function registerPublish(program) {
|
|
|
319
325
|
// Check .relay/relay.yaml exists
|
|
320
326
|
if (!fs_1.default.existsSync(relayYamlPath)) {
|
|
321
327
|
if (!isTTY) {
|
|
328
|
+
(0, error_report_js_1.reportCliError)('publish', 'NOT_INITIALIZED', 'relay.yaml missing');
|
|
322
329
|
console.error(JSON.stringify({
|
|
323
330
|
error: 'NOT_INITIALIZED',
|
|
324
331
|
message: '.relay/relay.yaml이 없습니다. 먼저 `relay create`를 실행하세요.',
|
|
@@ -383,6 +390,7 @@ function registerPublish(program) {
|
|
|
383
390
|
const yamlContent = fs_1.default.readFileSync(relayYamlPath, 'utf-8');
|
|
384
391
|
const config = parseRelayYaml(yamlContent);
|
|
385
392
|
if (!config.slug || !config.name || !config.description) {
|
|
393
|
+
(0, error_report_js_1.reportCliError)('publish', 'INVALID_CONFIG', 'missing name/slug/description');
|
|
386
394
|
console.error(JSON.stringify({
|
|
387
395
|
error: 'INVALID_CONFIG',
|
|
388
396
|
message: 'relay.yaml에 name, slug, description이 필요합니다.',
|
|
@@ -428,6 +436,7 @@ function registerPublish(program) {
|
|
|
428
436
|
return fs_1.default.readdirSync(dirPath).filter((f) => !f.startsWith('.')).length > 0;
|
|
429
437
|
});
|
|
430
438
|
if (!hasDirs) {
|
|
439
|
+
(0, error_report_js_1.reportCliError)('publish', 'EMPTY_PACKAGE', 'no content dirs found');
|
|
431
440
|
console.error(JSON.stringify({
|
|
432
441
|
error: 'EMPTY_PACKAGE',
|
|
433
442
|
message: '.relay/ 안에 skills/, agents/, rules/, commands/ 중 하나 이상에 파일이 있어야 합니다.',
|
|
@@ -438,6 +447,7 @@ function registerPublish(program) {
|
|
|
438
447
|
// Get token (checked before tarball creation)
|
|
439
448
|
const token = opts.token ?? process.env.RELAY_TOKEN ?? await (0, config_js_1.getValidToken)();
|
|
440
449
|
if (!token) {
|
|
450
|
+
(0, error_report_js_1.reportCliError)('publish', 'NO_TOKEN', 'auth required');
|
|
441
451
|
console.error(JSON.stringify({
|
|
442
452
|
error: 'NO_TOKEN',
|
|
443
453
|
message: '인증이 필요합니다. `relay login`을 먼저 실행하세요.',
|
|
@@ -470,6 +480,7 @@ function registerPublish(program) {
|
|
|
470
480
|
else {
|
|
471
481
|
console.error(`Organization '${opts.space}'를 찾을 수 없습니다.`);
|
|
472
482
|
}
|
|
483
|
+
(0, error_report_js_1.reportCliError)('publish', 'INVALID_ORG', `org:${opts.space}`);
|
|
473
484
|
process.exit(1);
|
|
474
485
|
}
|
|
475
486
|
}
|
|
@@ -505,6 +516,7 @@ function registerPublish(program) {
|
|
|
505
516
|
}
|
|
506
517
|
else if (orgs.length > 1 && json) {
|
|
507
518
|
// --json 모드 + 여러 Org: 에이전트가 선택할 수 있도록 에러 반환
|
|
519
|
+
(0, error_report_js_1.reportCliError)('publish', 'MISSING_ORG', 'multiple orgs, none selected');
|
|
508
520
|
console.error(JSON.stringify({
|
|
509
521
|
error: 'MISSING_ORG',
|
|
510
522
|
message: '배포할 Organization을 선택하세요.',
|
|
@@ -553,6 +565,7 @@ function registerPublish(program) {
|
|
|
553
565
|
console.error(` → relay.yaml에 visibility: ${config.visibility} 저장됨\n`);
|
|
554
566
|
}
|
|
555
567
|
else {
|
|
568
|
+
(0, error_report_js_1.reportCliError)('publish', 'MISSING_VISIBILITY', 'visibility not set in relay.yaml');
|
|
556
569
|
console.error(JSON.stringify({
|
|
557
570
|
error: 'MISSING_VISIBILITY',
|
|
558
571
|
message: 'relay.yaml에 visibility를 설정해주세요.',
|
|
@@ -646,7 +659,7 @@ function registerPublish(program) {
|
|
|
646
659
|
// Generate bin/relay-preamble.sh (self-contained tracking + update check)
|
|
647
660
|
(0, preamble_js_1.generatePreambleBin)(relayDir, config.slug, config_js_1.API_URL);
|
|
648
661
|
// Generate entry command (commands/{author}-{name}.md)
|
|
649
|
-
const entryContent = generateEntryCommand(config, detectedCommands, detectedSkills, config.slug);
|
|
662
|
+
const entryContent = generateEntryCommand(config, detectedCommands, detectedSkills, config.slug, relayDir);
|
|
650
663
|
const commandsDir = path_1.default.join(relayDir, 'commands');
|
|
651
664
|
if (!fs_1.default.existsSync(commandsDir)) {
|
|
652
665
|
fs_1.default.mkdirSync(commandsDir, { recursive: true });
|
|
@@ -669,7 +682,7 @@ function registerPublish(program) {
|
|
|
669
682
|
const entryFile = path_1.default.join(relayDir, 'commands', serverSlug.replace('/', '-') + '.md');
|
|
670
683
|
if (fs_1.default.existsSync(entryFile)) {
|
|
671
684
|
const { injectPreamble } = await import('../lib/preamble.js');
|
|
672
|
-
injectPreamble(entryFile, result.slug);
|
|
685
|
+
injectPreamble(entryFile, result.slug, relayDir);
|
|
673
686
|
}
|
|
674
687
|
}
|
|
675
688
|
}
|
|
@@ -714,6 +727,7 @@ function registerPublish(program) {
|
|
|
714
727
|
}
|
|
715
728
|
catch (err) {
|
|
716
729
|
const message = err instanceof Error ? err.message : String(err);
|
|
730
|
+
(0, error_report_js_1.reportCliError)('publish', 'PUBLISH_FAILED', message);
|
|
717
731
|
console.error(JSON.stringify({ error: 'PUBLISH_FAILED', message, fix: message }));
|
|
718
732
|
process.exit(1);
|
|
719
733
|
}
|
package/dist/commands/search.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.registerSearch = registerSearch;
|
|
4
4
|
const api_js_1 = require("../lib/api.js");
|
|
5
|
+
const step_tracker_js_1 = require("../lib/step-tracker.js");
|
|
5
6
|
function formatTable(results) {
|
|
6
7
|
if (results.length === 0)
|
|
7
8
|
return '검색 결과가 없습니다.';
|
|
@@ -31,6 +32,7 @@ function registerSearch(program) {
|
|
|
31
32
|
.option('--space <space>', '특정 Space 내에서 검색')
|
|
32
33
|
.action(async (keyword, opts) => {
|
|
33
34
|
const json = program.opts().json ?? false;
|
|
35
|
+
(0, step_tracker_js_1.trackCommand)('search', { slug: keyword });
|
|
34
36
|
try {
|
|
35
37
|
const results = await (0, api_js_1.searchAgents)(keyword, opts.tag);
|
|
36
38
|
if (json) {
|
package/dist/index.js
CHANGED
|
@@ -25,6 +25,7 @@ const access_js_1 = require("./commands/access.js");
|
|
|
25
25
|
const grant_js_1 = require("./commands/grant.js");
|
|
26
26
|
const versions_js_1 = require("./commands/versions.js");
|
|
27
27
|
const diff_js_1 = require("./commands/diff.js");
|
|
28
|
+
const feedback_js_1 = require("./commands/feedback.js");
|
|
28
29
|
const server_js_1 = require("./mcp/server.js");
|
|
29
30
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
30
31
|
const pkg = require('../package.json');
|
|
@@ -57,6 +58,7 @@ program
|
|
|
57
58
|
(0, grant_js_1.registerGrant)(program);
|
|
58
59
|
(0, versions_js_1.registerVersions)(program);
|
|
59
60
|
(0, diff_js_1.registerDiff)(program);
|
|
61
|
+
(0, feedback_js_1.registerFeedback)(program);
|
|
60
62
|
program
|
|
61
63
|
.command('mcp')
|
|
62
64
|
.description('MCP 서버 모드로 실행합니다 (stdio transport)')
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -20,5 +20,5 @@ export interface ResolvedSlug {
|
|
|
20
20
|
full: string;
|
|
21
21
|
}
|
|
22
22
|
export declare function resolveSlugFromServer(name: string): Promise<ResolvedSlug[]>;
|
|
23
|
-
export declare function sendUsagePing(agentId: string, slug: string, version?: string): Promise<void>;
|
|
23
|
+
export declare function sendUsagePing(agentId: string | null, slug: string, version?: string): Promise<void>;
|
|
24
24
|
export declare function followBuilder(username: string): Promise<void>;
|
package/dist/lib/api.js
CHANGED
|
@@ -94,7 +94,9 @@ async function sendUsagePing(agentId, slug, version) {
|
|
|
94
94
|
const deviceHash = createHash('sha256')
|
|
95
95
|
.update(`${hostname()}:${userInfo().username}`)
|
|
96
96
|
.digest('hex');
|
|
97
|
-
|
|
97
|
+
// agentId(UUID)가 있으면 UUID 경로, 없으면 slug name으로 fallback
|
|
98
|
+
const pathParam = agentId || slug.replace(/^@/, '').split('/').pop() || slug;
|
|
99
|
+
const url = `${config_js_1.API_URL}/api/agents/${pathParam}/ping`;
|
|
98
100
|
const payload = { device_hash: deviceHash, slug };
|
|
99
101
|
if (version)
|
|
100
102
|
payload.installed_version = version;
|
|
@@ -106,6 +108,7 @@ async function sendUsagePing(agentId, slug, version) {
|
|
|
106
108
|
method: 'POST',
|
|
107
109
|
headers,
|
|
108
110
|
body: JSON.stringify(payload),
|
|
111
|
+
signal: AbortSignal.timeout(5000),
|
|
109
112
|
}).catch(() => {
|
|
110
113
|
// fire-and-forget: ignore errors
|
|
111
114
|
});
|
|
@@ -13,6 +13,37 @@ exports.formatCommandFile = formatCommandFile;
|
|
|
13
13
|
const os_1 = __importDefault(require("os"));
|
|
14
14
|
const path_1 = __importDefault(require("path"));
|
|
15
15
|
const index_js_1 = require("../prompts/index.js");
|
|
16
|
+
/**
|
|
17
|
+
* 환경 감지 프리앰블.
|
|
18
|
+
* slash command 실행 시 MCP tool 사용 가능 여부를 먼저 판별하여,
|
|
19
|
+
* 이후 모든 CLI 명령어를 적절한 방식으로 실행하도록 안내.
|
|
20
|
+
*/
|
|
21
|
+
const ENV_PREAMBLE = `## 환경 감지
|
|
22
|
+
|
|
23
|
+
이 커맨드를 실행하기 전에 환경을 판별하세요:
|
|
24
|
+
|
|
25
|
+
1. **relay MCP tool 사용 가능 여부 확인**: relay_status 같은 relay MCP tool을 호출할 수 있는지 확인하세요.
|
|
26
|
+
- 사용 가능 → **MCP 모드**로 실행 (아래 표 참고)
|
|
27
|
+
- 사용 불가 → **CLI 모드**로 실행 (bash 명령어 그대로 사용)
|
|
28
|
+
|
|
29
|
+
2. MCP 모드에서의 명령어 변환:
|
|
30
|
+
|
|
31
|
+
| CLI 명령어 | MCP tool |
|
|
32
|
+
|---|---|
|
|
33
|
+
| \`relay status\` | \`relay_status\` tool 호출 |
|
|
34
|
+
| \`relay list --json\` | \`relay_list\` tool 호출 |
|
|
35
|
+
| \`relay search <keyword>\` | \`relay_search\` tool 호출 |
|
|
36
|
+
| \`relay install <slug>\` | \`relay_install\` tool 호출 |
|
|
37
|
+
| \`relay uninstall <slug>\` | \`relay_uninstall\` tool 호출 |
|
|
38
|
+
| \`relay package --init\` | \`relay_scan\` tool 호출 |
|
|
39
|
+
| \`relay publish\` | \`relay_publish\` tool 호출 |
|
|
40
|
+
| \`relay login\` | \`relay_login\` tool 호출 |
|
|
41
|
+
|
|
42
|
+
**중요**: 이후 지시에서 \`relay <명령어>\`로 표기된 것은 판별된 환경에 따라 변환하여 실행하세요.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
`;
|
|
16
47
|
/**
|
|
17
48
|
* 로컬 어댑터 — 프로젝트 디렉토리 기준.
|
|
18
49
|
* {projectPath}/{skillsDir}/commands/relay/{id}.md
|
|
@@ -66,12 +97,12 @@ exports.USER_COMMANDS = [
|
|
|
66
97
|
{
|
|
67
98
|
id: 'relay-install',
|
|
68
99
|
description: 'relay에서 에이전트를 설치합니다',
|
|
69
|
-
body: index_js_1.INSTALL_PROMPT,
|
|
100
|
+
body: ENV_PREAMBLE + index_js_1.INSTALL_PROMPT,
|
|
70
101
|
},
|
|
71
102
|
{
|
|
72
103
|
id: 'relay-status',
|
|
73
104
|
description: '설치된 에이전트와 Organization 현황을 확인합니다',
|
|
74
|
-
body: `현재 설치된 에이전트와 소속 Organization 현황을 한눈에 보여줍니다.
|
|
105
|
+
body: ENV_PREAMBLE + `현재 설치된 에이전트와 소속 Organization 현황을 한눈에 보여줍니다.
|
|
75
106
|
|
|
76
107
|
## 실행 방법
|
|
77
108
|
|
|
@@ -160,7 +191,7 @@ ${index_js_1.ERROR_HANDLING_GUIDE}
|
|
|
160
191
|
{
|
|
161
192
|
id: 'relay-uninstall',
|
|
162
193
|
description: '설치된 에이전트를 삭제합니다',
|
|
163
|
-
body: `설치된 에이전트를 제거합니다. CLI가 패키지와 배치된 파일을 모두 정리합니다.
|
|
194
|
+
body: ENV_PREAMBLE + `설치된 에이전트를 제거합니다. CLI가 패키지와 배치된 파일을 모두 정리합니다.
|
|
164
195
|
|
|
165
196
|
## 실행 방법
|
|
166
197
|
|
|
@@ -181,7 +212,7 @@ ${index_js_1.ERROR_HANDLING_GUIDE}
|
|
|
181
212
|
{
|
|
182
213
|
id: 'relay-publish',
|
|
183
214
|
description: '현재 에이전트 패키지를 relay에 배포합니다',
|
|
184
|
-
body: index_js_1.PUBLISH_PROMPT,
|
|
215
|
+
body: ENV_PREAMBLE + index_js_1.PUBLISH_PROMPT,
|
|
185
216
|
},
|
|
186
217
|
];
|
|
187
218
|
// ─── Builder Commands (로컬 설치) ───
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.reportCliError = reportCliError;
|
|
4
|
+
const config_js_1 = require("./config.js");
|
|
5
|
+
const device_hash_js_1 = require("./device-hash.js");
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
7
|
+
const pkg = require('../../package.json');
|
|
8
|
+
/**
|
|
9
|
+
* CLI 에러를 서버에 fire-and-forget으로 리포트한다.
|
|
10
|
+
* 실패해도 사용자에게 표시하지 않는다.
|
|
11
|
+
*/
|
|
12
|
+
function reportCliError(command, errorCode, errorMessage) {
|
|
13
|
+
const deviceHash = (0, device_hash_js_1.getDeviceHash)();
|
|
14
|
+
fetch(`${config_js_1.API_URL}/api/analytics/cli-errors`, {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
headers: { 'Content-Type': 'application/json' },
|
|
17
|
+
body: JSON.stringify({
|
|
18
|
+
device_hash: deviceHash,
|
|
19
|
+
command,
|
|
20
|
+
error_code: errorCode,
|
|
21
|
+
error_message: errorMessage.slice(0, 200),
|
|
22
|
+
cli_version: pkg.version,
|
|
23
|
+
}),
|
|
24
|
+
signal: AbortSignal.timeout(5000),
|
|
25
|
+
}).catch(() => {
|
|
26
|
+
// fire-and-forget
|
|
27
|
+
});
|
|
28
|
+
}
|
package/dist/lib/preamble.d.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
export declare function generatePreambleScript(slug: string, apiUrl: string): string;
|
|
6
6
|
/**
|
|
7
7
|
* SKILL.md / command에 삽입할 preamble 마크다운.
|
|
8
|
-
*
|
|
8
|
+
* agentDir: 설치된 에이전트의 절대 경로 (install 시점에 결정)
|
|
9
9
|
*/
|
|
10
|
-
export declare function generatePreamble(slug: string): string;
|
|
10
|
+
export declare function generatePreamble(slug: string, agentDir: string): string;
|
|
11
11
|
/**
|
|
12
12
|
* agentDir에 bin/relay-preamble.sh를 생성한다.
|
|
13
13
|
*/
|
|
@@ -15,7 +15,7 @@ export declare function generatePreambleBin(agentDir: string, slug: string, apiU
|
|
|
15
15
|
/**
|
|
16
16
|
* frontmatter(---...---) 뒤에 preamble을 삽입한다.
|
|
17
17
|
*/
|
|
18
|
-
export declare function injectPreamble(filePath: string, slug: string): void;
|
|
18
|
+
export declare function injectPreamble(filePath: string, slug: string, agentDir: string): void;
|
|
19
19
|
/**
|
|
20
20
|
* 에이전트의 사용자 진입점 파일에 preamble을 주입한다.
|
|
21
21
|
*/
|
package/dist/lib/preamble.js
CHANGED
|
@@ -25,19 +25,29 @@ function generatePreambleScript(slug, apiUrl) {
|
|
|
25
25
|
# relay-preamble.sh — auto-generated by relay publish
|
|
26
26
|
set +e
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
# Device hash (shasum → sha256sum → openssl fallback)
|
|
29
|
+
_RAW="$(hostname):$(whoami)"
|
|
30
|
+
if command -v shasum &>/dev/null; then
|
|
31
|
+
DEVICE_HASH=$(printf '%s' "$_RAW" | shasum -a 256 | cut -d' ' -f1)
|
|
32
|
+
elif command -v sha256sum &>/dev/null; then
|
|
33
|
+
DEVICE_HASH=$(printf '%s' "$_RAW" | sha256sum | cut -d' ' -f1)
|
|
34
|
+
elif command -v openssl &>/dev/null; then
|
|
35
|
+
DEVICE_HASH=$(printf '%s' "$_RAW" | openssl dgst -sha256 | awk '{print $NF}')
|
|
36
|
+
else
|
|
37
|
+
DEVICE_HASH="unknown"
|
|
38
|
+
fi
|
|
29
39
|
|
|
30
40
|
# Usage ping
|
|
31
41
|
if command -v relay &>/dev/null; then
|
|
32
42
|
relay ping "${slug}" --quiet 2>/dev/null &
|
|
33
|
-
|
|
34
|
-
curl -sf -X POST "${apiUrl}/api/agents/${agentSlug}/ping" \\
|
|
43
|
+
elif command -v curl &>/dev/null; then
|
|
44
|
+
curl -sf --max-time 5 -X POST "${apiUrl}/api/agents/${agentSlug}/ping" \\
|
|
35
45
|
-H "Content-Type: application/json" \\
|
|
36
|
-
-d "{\\"device_hash\\":\\"$DEVICE_HASH\\"}" \\
|
|
46
|
+
-d "{\\"device_hash\\":\\"$DEVICE_HASH\\",\\"slug\\":\\"${slug}\\"}" \\
|
|
37
47
|
2>/dev/null &
|
|
38
48
|
fi
|
|
39
49
|
|
|
40
|
-
# Update check
|
|
50
|
+
# Update check (already 24h cached by relay CLI)
|
|
41
51
|
if command -v relay &>/dev/null; then
|
|
42
52
|
_UPD=$(relay check-update "${slug}" --quiet 2>/dev/null || true)
|
|
43
53
|
[ -n "$_UPD" ] && echo "$_UPD" || true
|
|
@@ -48,17 +58,19 @@ echo "RELAY_READY: ${slug}"
|
|
|
48
58
|
}
|
|
49
59
|
/**
|
|
50
60
|
* SKILL.md / command에 삽입할 preamble 마크다운.
|
|
51
|
-
*
|
|
61
|
+
* agentDir: 설치된 에이전트의 절대 경로 (install 시점에 결정)
|
|
52
62
|
*/
|
|
53
|
-
function generatePreamble(slug) {
|
|
54
|
-
const
|
|
63
|
+
function generatePreamble(slug, agentDir) {
|
|
64
|
+
const scriptPath = path_1.default.join(agentDir, 'bin', 'relay-preamble.sh');
|
|
55
65
|
return `${PREAMBLE_START}
|
|
56
66
|
## Preamble (run first)
|
|
57
67
|
|
|
58
68
|
\`\`\`bash
|
|
59
|
-
bash "$
|
|
69
|
+
[ -f "${scriptPath}" ] && bash "${scriptPath}" 2>/dev/null || true
|
|
60
70
|
\`\`\`
|
|
61
71
|
|
|
72
|
+
If the bash command above fails or the file is not found (sandbox/MCP environment), use the \`relay_check_update\` MCP tool instead: \`relay_check_update(slug: "${slug}")\`
|
|
73
|
+
|
|
62
74
|
If output shows \`CLI_UPGRADE_AVAILABLE <old> <new>\`: tell user "relay v{new} available. Run: npm update -g relayax-cli"
|
|
63
75
|
If output shows \`AGENT_UPGRADE_AVAILABLE\`: tell user "${slug} v{new} available (by @{author}). Run: relay update ${slug}"
|
|
64
76
|
${PREAMBLE_END}`;
|
|
@@ -78,9 +90,9 @@ function generatePreambleBin(agentDir, slug, apiUrl) {
|
|
|
78
90
|
/**
|
|
79
91
|
* frontmatter(---...---) 뒤에 preamble을 삽입한다.
|
|
80
92
|
*/
|
|
81
|
-
function injectPreamble(filePath, slug) {
|
|
93
|
+
function injectPreamble(filePath, slug, agentDir) {
|
|
82
94
|
const content = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
83
|
-
const preamble = generatePreamble(slug);
|
|
95
|
+
const preamble = generatePreamble(slug, agentDir);
|
|
84
96
|
// 기존 preamble 제거
|
|
85
97
|
let cleaned = content;
|
|
86
98
|
const startIdx = cleaned.indexOf(PREAMBLE_START);
|
|
@@ -122,7 +134,7 @@ function injectPreambleToAgent(agentDir, slug) {
|
|
|
122
134
|
else if (entry.name === 'SKILL.md') {
|
|
123
135
|
const content = fs_1.default.readFileSync(fullPath, 'utf-8');
|
|
124
136
|
if (/user-invocable:\s*true/i.test(content)) {
|
|
125
|
-
injectPreamble(fullPath, slug);
|
|
137
|
+
injectPreamble(fullPath, slug, agentDir);
|
|
126
138
|
count++;
|
|
127
139
|
}
|
|
128
140
|
}
|
|
@@ -136,7 +148,7 @@ function injectPreambleToAgent(agentDir, slug) {
|
|
|
136
148
|
for (const entry of fs_1.default.readdirSync(commandsDir, { withFileTypes: true })) {
|
|
137
149
|
if (!entry.isFile() || !entry.name.endsWith('.md'))
|
|
138
150
|
continue;
|
|
139
|
-
injectPreamble(path_1.default.join(commandsDir, entry.name), slug);
|
|
151
|
+
injectPreamble(path_1.default.join(commandsDir, entry.name), slug, agentDir);
|
|
140
152
|
count++;
|
|
141
153
|
}
|
|
142
154
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.trackCommand = trackCommand;
|
|
4
|
+
const config_js_1 = require("./config.js");
|
|
5
|
+
const device_hash_js_1 = require("./device-hash.js");
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
7
|
+
const pkg = require('../../package.json');
|
|
8
|
+
/**
|
|
9
|
+
* CLI 명령 실행을 서버에 기록한다 (fire-and-forget).
|
|
10
|
+
* device_hash 기준으로 사용자 여정(login → create → publish)을 추적.
|
|
11
|
+
*/
|
|
12
|
+
function trackCommand(command, opts) {
|
|
13
|
+
const deviceHash = (0, device_hash_js_1.getDeviceHash)();
|
|
14
|
+
fetch(`${config_js_1.API_URL}/api/analytics/cli-commands`, {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
headers: { 'Content-Type': 'application/json' },
|
|
17
|
+
body: JSON.stringify({
|
|
18
|
+
device_hash: deviceHash,
|
|
19
|
+
command,
|
|
20
|
+
slug: opts?.slug ?? null,
|
|
21
|
+
success: opts?.success ?? true,
|
|
22
|
+
cli_version: pkg.version,
|
|
23
|
+
}),
|
|
24
|
+
signal: AbortSignal.timeout(5000),
|
|
25
|
+
}).catch(() => {
|
|
26
|
+
// fire-and-forget
|
|
27
|
+
});
|
|
28
|
+
}
|
package/dist/mcp/server.js
CHANGED
|
@@ -16,6 +16,7 @@ const ai_tools_js_1 = require("../lib/ai-tools.js");
|
|
|
16
16
|
const preamble_js_1 = require("../lib/preamble.js");
|
|
17
17
|
const installer_js_1 = require("../lib/installer.js");
|
|
18
18
|
const paths_js_1 = require("../lib/paths.js");
|
|
19
|
+
// prompts are used in MCP Prompt definitions below
|
|
19
20
|
const fs_1 = __importDefault(require("fs"));
|
|
20
21
|
const path_1 = __importDefault(require("path"));
|
|
21
22
|
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
@@ -161,7 +162,48 @@ function createMcpServer() {
|
|
|
161
162
|
}
|
|
162
163
|
catch { /* skip */ }
|
|
163
164
|
}
|
|
164
|
-
|
|
165
|
+
// 버전 확인
|
|
166
|
+
const { checkCliVersion } = await import('../lib/version-check.js');
|
|
167
|
+
const cliUpdate = await checkCliVersion(true);
|
|
168
|
+
return { content: [jsonText({
|
|
169
|
+
cli: { version: pkg.version, update_available: cliUpdate ? cliUpdate.latest : null },
|
|
170
|
+
login: { authenticated: !!token, username },
|
|
171
|
+
agent_clis: detected.map((t) => t.name),
|
|
172
|
+
mounted_paths: mounted.map((m) => m.basePath),
|
|
173
|
+
project,
|
|
174
|
+
})] };
|
|
175
|
+
});
|
|
176
|
+
server.tool('relay_check_update', 'CLI 및 에이전트 업데이트를 확인합니다. slug 지정 시 해당 에이전트만 체크하며 사용 현황도 기록합니다 (preamble 대체).', {
|
|
177
|
+
slug: zod_1.z.string().optional().describe('특정 에이전트 slug (예: @owner/name). 생략하면 전체 체크'),
|
|
178
|
+
}, async ({ slug: slugInput }) => {
|
|
179
|
+
const { checkCliVersion, checkAgentVersion, checkAllAgents } = await import('../lib/version-check.js');
|
|
180
|
+
// slug가 지정되면 해당 에이전트의 usage ping도 함께 전송
|
|
181
|
+
if (slugInput) {
|
|
182
|
+
const local = (0, config_js_1.loadInstalled)();
|
|
183
|
+
const global = (0, config_js_1.loadGlobalInstalled)();
|
|
184
|
+
const entry = local[slugInput] ?? global[slugInput];
|
|
185
|
+
const agentId = entry?.agent_id ?? null;
|
|
186
|
+
const version = entry?.version;
|
|
187
|
+
(0, api_js_1.sendUsagePing)(agentId, slugInput, version);
|
|
188
|
+
}
|
|
189
|
+
const cliUpdate = await checkCliVersion(true);
|
|
190
|
+
const updates = [];
|
|
191
|
+
if (cliUpdate)
|
|
192
|
+
updates.push({ type: 'cli', current: cliUpdate.current, latest: cliUpdate.latest });
|
|
193
|
+
if (slugInput) {
|
|
194
|
+
const agentUpdate = await checkAgentVersion(slugInput, true);
|
|
195
|
+
if (agentUpdate)
|
|
196
|
+
updates.push({ type: 'agent', slug: agentUpdate.slug, current: agentUpdate.current, latest: agentUpdate.latest });
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
const agentUpdates = await checkAllAgents(true);
|
|
200
|
+
for (const u of agentUpdates)
|
|
201
|
+
updates.push({ type: 'agent', slug: u.slug, current: u.current, latest: u.latest });
|
|
202
|
+
}
|
|
203
|
+
if (updates.length === 0) {
|
|
204
|
+
return { content: [jsonText({ status: 'up_to_date', message: '모두 최신 버전입니다.', cli_version: pkg.version })] };
|
|
205
|
+
}
|
|
206
|
+
return { content: [jsonText({ status: 'updates_available', updates, message: 'CLI를 업데이트하려면: npm update -g relayax-cli' })] };
|
|
165
207
|
});
|
|
166
208
|
server.tool('relay_scan', '배포 가능한 스킬/에이전트/커맨드를 스캔합니다', {
|
|
167
209
|
project_path: zod_1.z.string().optional().describe('프로젝트 경로'),
|
|
@@ -190,73 +232,118 @@ function createMcpServer() {
|
|
|
190
232
|
}
|
|
191
233
|
return { content: [jsonText({ sources })] };
|
|
192
234
|
});
|
|
193
|
-
server.tool('relay_publish', '에이전트를 마켓플레이스에 배포합니다', {
|
|
194
|
-
project_path: zod_1.z.string().optional().describe('프로젝트 경로'),
|
|
235
|
+
server.tool('relay_publish', '에이전트를 마켓플레이스에 배포합니다 (.relay/ 디렉토리를 tar로 패키징하여 업로드)', {
|
|
236
|
+
project_path: zod_1.z.string().optional().describe('프로젝트 경로 (.relay/relay.yaml이 있는 디렉토리)'),
|
|
195
237
|
}, async ({ project_path }) => {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
238
|
+
try {
|
|
239
|
+
const projectPath = project_path ?? (0, paths_js_1.resolveProjectPath)();
|
|
240
|
+
const relayDir = path_1.default.join(projectPath, '.relay');
|
|
241
|
+
const relayYaml = path_1.default.join(relayDir, 'relay.yaml');
|
|
242
|
+
if (!fs_1.default.existsSync(relayYaml)) {
|
|
243
|
+
return { content: [jsonText({ error: 'NOT_INITIALIZED', message: '.relay/relay.yaml이 없습니다.' })], isError: true };
|
|
244
|
+
}
|
|
245
|
+
const token = await (0, config_js_1.getValidToken)();
|
|
246
|
+
if (!token) {
|
|
247
|
+
return { content: [jsonText({ error: 'LOGIN_REQUIRED', message: '배포하려면 로그인이 필요합니다.' })], isError: true };
|
|
248
|
+
}
|
|
249
|
+
const cfg = js_yaml_1.default.load(fs_1.default.readFileSync(relayYaml, 'utf-8'));
|
|
250
|
+
const { createTarball, publishToApi } = await import('../commands/publish.js');
|
|
251
|
+
// Generate bin/relay-preamble.sh (CLI publish와 동일하게)
|
|
252
|
+
(0, preamble_js_1.generatePreambleBin)(relayDir, cfg.slug, config_js_1.API_URL);
|
|
253
|
+
const tarPath = await createTarball(relayDir);
|
|
254
|
+
try {
|
|
255
|
+
const metadata = {
|
|
256
|
+
slug: cfg.slug,
|
|
257
|
+
name: cfg.name,
|
|
258
|
+
description: cfg.description ?? '',
|
|
259
|
+
tags: cfg.tags ?? [],
|
|
260
|
+
commands: [],
|
|
261
|
+
components: { skills: 0, agents: 0, rules: 0, commands: 0 },
|
|
262
|
+
version: cfg.version,
|
|
263
|
+
visibility: cfg.visibility ?? 'public',
|
|
264
|
+
cli_version: pkg.version,
|
|
265
|
+
};
|
|
266
|
+
const result = await publishToApi(token, tarPath, metadata);
|
|
267
|
+
return { content: [jsonText(result)] };
|
|
268
|
+
}
|
|
269
|
+
finally {
|
|
270
|
+
fs_1.default.unlinkSync(tarPath);
|
|
271
|
+
}
|
|
200
272
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
273
|
+
catch (err) {
|
|
274
|
+
return { content: [jsonText({ error: String(err) })], isError: true };
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
// ═══ relay_login — device code 로그인 ═══
|
|
278
|
+
server.tool('relay_login', 'Device Code 방식으로 로그인합니다. URL과 코드를 사용자에게 보여주고, 승인을 기다립니다.', {}, async () => {
|
|
279
|
+
try {
|
|
280
|
+
// 이미 로그인되어 있는지 확인
|
|
281
|
+
const existingToken = await (0, config_js_1.getValidToken)();
|
|
282
|
+
if (existingToken) {
|
|
283
|
+
const username = await resolveUsername(existingToken);
|
|
284
|
+
return { content: [jsonText({ status: 'already_authenticated', username })] };
|
|
285
|
+
}
|
|
286
|
+
// Device code 발급
|
|
287
|
+
const res = await fetch(`${config_js_1.API_URL}/api/auth/device/request`, { method: 'POST' });
|
|
288
|
+
if (!res.ok)
|
|
289
|
+
throw new Error('Device code 발급에 실패했습니다');
|
|
290
|
+
const { device_code, user_code, verification_url, expires_in } = await res.json();
|
|
291
|
+
// 브라우저 열기 시도
|
|
292
|
+
try {
|
|
293
|
+
const { execSync } = await import('child_process');
|
|
294
|
+
if (process.platform === 'darwin')
|
|
295
|
+
execSync(`open "${verification_url}?user_code=${user_code}"`, { stdio: 'ignore' });
|
|
296
|
+
else if (process.platform === 'win32')
|
|
297
|
+
execSync(`start "" "${verification_url}?user_code=${user_code}"`, { stdio: 'ignore' });
|
|
298
|
+
else
|
|
299
|
+
execSync(`xdg-open "${verification_url}?user_code=${user_code}"`, { stdio: 'ignore' });
|
|
300
|
+
}
|
|
301
|
+
catch { /* 브라우저 열기 실패 — 사용자가 직접 열어야 함 */ }
|
|
302
|
+
// Polling (최대 expires_in 초, 5초 간격)
|
|
303
|
+
const deadline = Date.now() + expires_in * 1000;
|
|
304
|
+
while (Date.now() < deadline) {
|
|
305
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
306
|
+
const pollRes = await fetch(`${config_js_1.API_URL}/api/auth/device/poll`, {
|
|
307
|
+
method: 'POST',
|
|
308
|
+
headers: { 'Content-Type': 'application/json' },
|
|
309
|
+
body: JSON.stringify({ device_code }),
|
|
310
|
+
});
|
|
311
|
+
if (!pollRes.ok)
|
|
312
|
+
continue;
|
|
313
|
+
const data = await pollRes.json();
|
|
314
|
+
if (data.status === 'approved' && data.token) {
|
|
315
|
+
const { saveTokenData, ensureGlobalRelayDir } = await import('../lib/config.js');
|
|
316
|
+
ensureGlobalRelayDir();
|
|
317
|
+
saveTokenData({
|
|
318
|
+
access_token: data.token,
|
|
319
|
+
refresh_token: data.refresh_token,
|
|
320
|
+
expires_at: data.expires_at ? Number(data.expires_at) : undefined,
|
|
321
|
+
});
|
|
322
|
+
const username = await resolveUsername(data.token);
|
|
323
|
+
return { content: [jsonText({ status: 'ok', message: '로그인 완료', username })] };
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return { content: [jsonText({ status: 'timeout', verification_url, user_code, message: `브라우저에서 ${verification_url} 을 열고 코드 ${user_code} 를 입력해주세요.` })], isError: true };
|
|
327
|
+
}
|
|
328
|
+
catch (err) {
|
|
329
|
+
return { content: [jsonText({ error: String(err) })], isError: true };
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
// ═══ relay_init — slash command 설치 ═══
|
|
333
|
+
server.tool('relay_init', 'relay slash command를 설치합니다 (/relay-install, /relay-publish 등)', {}, async () => {
|
|
334
|
+
try {
|
|
335
|
+
const { installGlobalUserCommands, hasGlobalUserCommands } = await import('../commands/init.js');
|
|
336
|
+
if (hasGlobalUserCommands()) {
|
|
337
|
+
installGlobalUserCommands(); // 업데이트
|
|
338
|
+
return { content: [jsonText({ status: 'updated', message: 'relay slash command가 업데이트되었습니다.' })] };
|
|
339
|
+
}
|
|
340
|
+
installGlobalUserCommands();
|
|
341
|
+
return { content: [jsonText({ status: 'installed', message: 'relay slash command가 설치되었습니다. /relay-install, /relay-publish 등을 사용할 수 있습니다.' })] };
|
|
342
|
+
}
|
|
343
|
+
catch (err) {
|
|
344
|
+
return { content: [jsonText({ error: String(err) })], isError: true };
|
|
204
345
|
}
|
|
205
|
-
const cfg = js_yaml_1.default.load(fs_1.default.readFileSync(relayYaml, 'utf-8'));
|
|
206
|
-
return { content: [jsonText({ status: 'ready', project_path: projectPath, name: cfg.name, slug: cfg.slug, version: cfg.version, message: '배포 준비 완료. relay-publish 프롬프트로 전체 워크플로우를 실행하세요.' })] };
|
|
207
346
|
});
|
|
208
|
-
// ═══ Prompts ═══
|
|
209
|
-
server.prompt('relay-install', '에이전트 검색 → 설치 가이드 워크플로우', {}, () => ({
|
|
210
|
-
messages: [{
|
|
211
|
-
role: 'user',
|
|
212
|
-
content: { type: 'text', text: `다음 가이드를 따라 에이전트를 설치하세요. relay MCP tool을 사용하여 각 단계를 실행합니다.
|
|
213
|
-
|
|
214
|
-
사용 가능한 tool: relay_status, relay_search, relay_install, relay_list, relay_status
|
|
215
|
-
|
|
216
|
-
## 워크플로우
|
|
217
|
-
|
|
218
|
-
1. relay_status로 로그인 상태 확인. 미인증이면 사용자에게 터미널에서 \`npx relayax-cli login --device\` 실행을 안내.
|
|
219
|
-
2. 사용자에게 어떤 에이전트를 찾는지 물어보기.
|
|
220
|
-
3. relay_search로 검색.
|
|
221
|
-
4. 검색 결과를 보여주고 선택하게 하기.
|
|
222
|
-
5. relay_install로 설치.
|
|
223
|
-
6. 설치 결과와 사용법 안내.` },
|
|
224
|
-
}],
|
|
225
|
-
}));
|
|
226
|
-
server.prompt('relay-publish', '에이전트 배포 가이드 워크플로우', {}, () => ({
|
|
227
|
-
messages: [{
|
|
228
|
-
role: 'user',
|
|
229
|
-
content: { type: 'text', text: `다음 가이드를 따라 에이전트를 배포하세요. relay MCP tool을 사용합니다.
|
|
230
|
-
|
|
231
|
-
사용 가능한 tool: relay_status, relay_scan, relay_status, relay_publish
|
|
232
|
-
|
|
233
|
-
## 워크플로우
|
|
234
|
-
|
|
235
|
-
1. relay_status로 로그인 상태 확인. 미인증이면 터미널에서 \`npx relayax-cli login --device\` 안내.
|
|
236
|
-
2. relay_scan으로 배포 가능한 스킬/에이전트/커맨드 목록 스캔.
|
|
237
|
-
3. 스캔 결과를 보여주고 사용자에게 어떤 항목을 배포할지 선택하게 하기.
|
|
238
|
-
4. relay_status로 프로젝트 상태 확인. .relay/relay.yaml이 없으면 프로젝트 생성 안내.
|
|
239
|
-
5. relay_publish로 배포.
|
|
240
|
-
6. 배포 결과를 사용자에게 보여주기.` },
|
|
241
|
-
}],
|
|
242
|
-
}));
|
|
243
|
-
server.prompt('relay-status', '환경 상태 확인', {}, () => ({
|
|
244
|
-
messages: [{
|
|
245
|
-
role: 'user',
|
|
246
|
-
content: { type: 'text', text: 'relay_status, relay_status, relay_list, relay_scan tool을 사용하여 현재 relay 환경 상태를 확인하고 사용자에게 보여주세요. 로그인 상태, 감지된 AI 도구, 프로젝트 정보, 설치된 에이전트, 배포 가능한 스킬 목록을 포함합니다.' },
|
|
247
|
-
}],
|
|
248
|
-
}));
|
|
249
|
-
server.prompt('relay-uninstall', '에이전트 제거 가이드', {}, () => ({
|
|
250
|
-
messages: [{
|
|
251
|
-
role: 'user',
|
|
252
|
-
content: { type: 'text', text: `다음 워크플로우를 따라 에이전트를 제거하세요:
|
|
253
|
-
|
|
254
|
-
1. relay_list로 설치된 에이전트 목록 표시.
|
|
255
|
-
2. 사용자에게 제거할 에이전트 선택하게 하기.
|
|
256
|
-
3. relay_uninstall로 제거.
|
|
257
|
-
4. 결과 보여주기.` },
|
|
258
|
-
}],
|
|
259
|
-
}));
|
|
260
347
|
return server;
|
|
261
348
|
}
|
|
262
349
|
// ─── Start ───
|
|
@@ -58,7 +58,8 @@ cat ~/Library/Application\ Support/Claude/claude_desktop_config.json 2>/dev/null
|
|
|
58
58
|
| "relay status" | `relay status` | `relay_status` tool 호출 |
|
|
59
59
|
| "relay install X" | `relay install X` | `relay_install` tool 호출 (`slug: "X"`) |
|
|
60
60
|
| "relay publish" | `relay publish` | `relay_publish` tool 호출 |
|
|
61
|
-
| "relay login" | `relay login` |
|
|
61
|
+
| "relay login" | `relay login` | `relay_login` tool 호출 |
|
|
62
62
|
| "relay scan" | `relay package --init` | `relay_scan` tool 호출 |
|
|
63
|
+
| "relay check-update X" | `relay check-update X` | `relay_check_update` tool 호출 (`slug: "X"`) |
|
|
63
64
|
|
|
64
65
|
처음 판별한 환경을 이후 계속 사용합니다.
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
## Step 2. 로그인
|
|
2
2
|
|
|
3
|
-
**중요:
|
|
3
|
+
**중요: 브라우저에서 직접 relayax.com에 접속하거나 웹 로그인 페이지를 여는 것은 올바른 방법이 아닙니다.**
|
|
4
4
|
|
|
5
5
|
먼저 로그인 상태를 확인합니다. 이미 로그인되어 있으면 이 단계를 건너뛰세요.
|
|
6
6
|
|
|
7
7
|
- 환경 A: `relay status`
|
|
8
|
-
- 환경 B: `
|
|
8
|
+
- 환경 B (MCP): `relay_status` tool 호출
|
|
9
9
|
|
|
10
|
-
로그인이 필요하면
|
|
10
|
+
로그인이 필요하면 아래 환경에 맞는 방법을 사용하세요.
|
|
11
11
|
|
|
12
12
|
### 환경 A (터미널)
|
|
13
13
|
|
|
@@ -20,13 +20,12 @@ relay login
|
|
|
20
20
|
|
|
21
21
|
"✓ 로그인 완료"가 출력되면 다음 단계로 진행합니다.
|
|
22
22
|
|
|
23
|
-
### 환경 B (
|
|
23
|
+
### 환경 B (MCP)
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
`relay_login` tool을 호출하세요. 이 tool이:
|
|
26
|
+
1. Device code와 URL을 자동으로 발급합니다.
|
|
27
|
+
2. 브라우저를 자동으로 엽니다.
|
|
28
|
+
3. 사용자가 브라우저에서 코드를 입력하면 자동으로 감지합니다.
|
|
29
|
+
4. 로그인 완료 후 결과를 반환합니다.
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
- 사용자에게 URL과 코드를 보여주세요. 사용자가 직접 브라우저에서 코드를 입력해야 합니다.
|
|
31
|
-
- CLI가 자동으로 승인을 감지하고 "✓ 로그인 완료"를 출력합니다.
|
|
32
|
-
- timeout이 짧으면 `--timeout 300`을 추가하세요.
|
|
31
|
+
브라우저가 자동으로 열리지 않으면, 응답에 포함된 URL과 코드를 사용자에게 보여주세요.
|