relayax-cli 0.3.51 → 0.3.53

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.
@@ -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,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerFeedback(program: Command): void;
@@ -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
+ }
@@ -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
  }
@@ -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');
@@ -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
  }
@@ -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 기반, 없으면 skip)
35
- if (agentId) {
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
  }
@@ -15,6 +15,8 @@ const config_js_1 = require("../lib/config.js");
15
15
  const preamble_js_1 = require("../lib/preamble.js");
16
16
  const version_check_js_1 = require("../lib/version-check.js");
17
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");
18
20
  // eslint-disable-next-line @typescript-eslint/no-var-requires
19
21
  const cliPkg = require('../../package.json');
20
22
  const VALID_DIRS = ['skills', 'agents', 'rules', 'commands', 'bin'];
@@ -173,7 +175,7 @@ function detectAgentDetails(agentDir, requires) {
173
175
  * 에이전트 진입점 커맨드(commands/{author}-{name}.md)를 생성한다.
174
176
  * root SKILL.md를 대체하여 에이전트의 얼굴 역할을 한다.
175
177
  */
176
- function generateEntryCommand(config, commands, skills, scopedSlug) {
178
+ function generateEntryCommand(config, commands, skills, scopedSlug, agentDir) {
177
179
  const lines = [];
178
180
  // Frontmatter
179
181
  lines.push('---');
@@ -181,7 +183,7 @@ function generateEntryCommand(config, commands, skills, scopedSlug) {
181
183
  lines.push('---');
182
184
  lines.push('');
183
185
  // Preamble
184
- lines.push((0, preamble_js_1.generatePreamble)(scopedSlug));
186
+ lines.push((0, preamble_js_1.generatePreamble)(scopedSlug, agentDir));
185
187
  lines.push('');
186
188
  // Agent header
187
189
  lines.push(`## ${config.name}`);
@@ -294,6 +296,7 @@ function registerPublish(program) {
294
296
  const relayDir = path_1.default.join(agentDir, '.relay');
295
297
  const relayYamlPath = path_1.default.join(relayDir, 'relay.yaml');
296
298
  const isTTY = Boolean(process.stdin.isTTY) && !json;
299
+ (0, step_tracker_js_1.trackCommand)('publish', { slug: undefined });
297
300
  // CLI update check before publish
298
301
  if (isTTY) {
299
302
  const cliUpdate = await (0, version_check_js_1.checkCliVersion)(true);
@@ -308,6 +311,7 @@ function registerPublish(program) {
308
311
  default: true,
309
312
  });
310
313
  if (!shouldContinue) {
314
+ (0, error_report_js_1.reportCliError)('publish', 'CANCELLED_CLI_UPDATE', `current:${cliUpdate.current} latest:${cliUpdate.latest}`);
311
315
  console.error('\n배포를 취소했습니다. CLI를 업데이트한 후 다시 시도하세요.');
312
316
  process.exit(0);
313
317
  }
@@ -321,6 +325,7 @@ function registerPublish(program) {
321
325
  // Check .relay/relay.yaml exists
322
326
  if (!fs_1.default.existsSync(relayYamlPath)) {
323
327
  if (!isTTY) {
328
+ (0, error_report_js_1.reportCliError)('publish', 'NOT_INITIALIZED', 'relay.yaml missing');
324
329
  console.error(JSON.stringify({
325
330
  error: 'NOT_INITIALIZED',
326
331
  message: '.relay/relay.yaml이 없습니다. 먼저 `relay create`를 실행하세요.',
@@ -385,6 +390,7 @@ function registerPublish(program) {
385
390
  const yamlContent = fs_1.default.readFileSync(relayYamlPath, 'utf-8');
386
391
  const config = parseRelayYaml(yamlContent);
387
392
  if (!config.slug || !config.name || !config.description) {
393
+ (0, error_report_js_1.reportCliError)('publish', 'INVALID_CONFIG', 'missing name/slug/description');
388
394
  console.error(JSON.stringify({
389
395
  error: 'INVALID_CONFIG',
390
396
  message: 'relay.yaml에 name, slug, description이 필요합니다.',
@@ -430,6 +436,7 @@ function registerPublish(program) {
430
436
  return fs_1.default.readdirSync(dirPath).filter((f) => !f.startsWith('.')).length > 0;
431
437
  });
432
438
  if (!hasDirs) {
439
+ (0, error_report_js_1.reportCliError)('publish', 'EMPTY_PACKAGE', 'no content dirs found');
433
440
  console.error(JSON.stringify({
434
441
  error: 'EMPTY_PACKAGE',
435
442
  message: '.relay/ 안에 skills/, agents/, rules/, commands/ 중 하나 이상에 파일이 있어야 합니다.',
@@ -440,6 +447,7 @@ function registerPublish(program) {
440
447
  // Get token (checked before tarball creation)
441
448
  const token = opts.token ?? process.env.RELAY_TOKEN ?? await (0, config_js_1.getValidToken)();
442
449
  if (!token) {
450
+ (0, error_report_js_1.reportCliError)('publish', 'NO_TOKEN', 'auth required');
443
451
  console.error(JSON.stringify({
444
452
  error: 'NO_TOKEN',
445
453
  message: '인증이 필요합니다. `relay login`을 먼저 실행하세요.',
@@ -472,6 +480,7 @@ function registerPublish(program) {
472
480
  else {
473
481
  console.error(`Organization '${opts.space}'를 찾을 수 없습니다.`);
474
482
  }
483
+ (0, error_report_js_1.reportCliError)('publish', 'INVALID_ORG', `org:${opts.space}`);
475
484
  process.exit(1);
476
485
  }
477
486
  }
@@ -507,6 +516,7 @@ function registerPublish(program) {
507
516
  }
508
517
  else if (orgs.length > 1 && json) {
509
518
  // --json 모드 + 여러 Org: 에이전트가 선택할 수 있도록 에러 반환
519
+ (0, error_report_js_1.reportCliError)('publish', 'MISSING_ORG', 'multiple orgs, none selected');
510
520
  console.error(JSON.stringify({
511
521
  error: 'MISSING_ORG',
512
522
  message: '배포할 Organization을 선택하세요.',
@@ -555,6 +565,7 @@ function registerPublish(program) {
555
565
  console.error(` → relay.yaml에 visibility: ${config.visibility} 저장됨\n`);
556
566
  }
557
567
  else {
568
+ (0, error_report_js_1.reportCliError)('publish', 'MISSING_VISIBILITY', 'visibility not set in relay.yaml');
558
569
  console.error(JSON.stringify({
559
570
  error: 'MISSING_VISIBILITY',
560
571
  message: 'relay.yaml에 visibility를 설정해주세요.',
@@ -648,7 +659,7 @@ function registerPublish(program) {
648
659
  // Generate bin/relay-preamble.sh (self-contained tracking + update check)
649
660
  (0, preamble_js_1.generatePreambleBin)(relayDir, config.slug, config_js_1.API_URL);
650
661
  // Generate entry command (commands/{author}-{name}.md)
651
- const entryContent = generateEntryCommand(config, detectedCommands, detectedSkills, config.slug);
662
+ const entryContent = generateEntryCommand(config, detectedCommands, detectedSkills, config.slug, relayDir);
652
663
  const commandsDir = path_1.default.join(relayDir, 'commands');
653
664
  if (!fs_1.default.existsSync(commandsDir)) {
654
665
  fs_1.default.mkdirSync(commandsDir, { recursive: true });
@@ -671,7 +682,7 @@ function registerPublish(program) {
671
682
  const entryFile = path_1.default.join(relayDir, 'commands', serverSlug.replace('/', '-') + '.md');
672
683
  if (fs_1.default.existsSync(entryFile)) {
673
684
  const { injectPreamble } = await import('../lib/preamble.js');
674
- injectPreamble(entryFile, result.slug);
685
+ injectPreamble(entryFile, result.slug, relayDir);
675
686
  }
676
687
  }
677
688
  }
@@ -716,6 +727,7 @@ function registerPublish(program) {
716
727
  }
717
728
  catch (err) {
718
729
  const message = err instanceof Error ? err.message : String(err);
730
+ (0, error_report_js_1.reportCliError)('publish', 'PUBLISH_FAILED', message);
719
731
  console.error(JSON.stringify({ error: 'PUBLISH_FAILED', message, fix: message }));
720
732
  process.exit(1);
721
733
  }
@@ -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
- const url = `${config_js_1.API_URL}/api/agents/${agentId}/ping`;
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
  });
@@ -37,6 +37,7 @@ const ENV_PREAMBLE = `## 환경 감지
37
37
  | \`relay uninstall <slug>\` | \`relay_uninstall\` tool 호출 |
38
38
  | \`relay package --init\` | \`relay_scan\` tool 호출 |
39
39
  | \`relay publish\` | \`relay_publish\` tool 호출 |
40
+ | \`relay login\` | \`relay_login\` tool 호출 |
40
41
 
41
42
  **중요**: 이후 지시에서 \`relay <명령어>\`로 표기된 것은 판별된 환경에 따라 변환하여 실행하세요.
42
43
 
@@ -0,0 +1,5 @@
1
+ /**
2
+ * CLI 에러를 서버에 fire-and-forget으로 리포트한다.
3
+ * 실패해도 사용자에게 표시하지 않는다.
4
+ */
5
+ export declare function reportCliError(command: string, errorCode: string, errorMessage: string): void;
@@ -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
+ }
@@ -5,9 +5,9 @@
5
5
  export declare function generatePreambleScript(slug: string, apiUrl: string): string;
6
6
  /**
7
7
  * SKILL.md / command에 삽입할 preamble 마크다운.
8
- * 단순히 relay ping을 호출한다.
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
  */
@@ -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
- DEVICE_HASH=$(echo "$HOSTNAME:$USER" | shasum -a 256 | cut -d' ' -f1)
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
- else
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
- * 단순히 relay ping을 호출한다.
61
+ * agentDir: 설치된 에이전트의 절대 경로 (install 시점에 결정)
52
62
  */
53
- function generatePreamble(slug) {
54
- const agentPath = slug.startsWith('@') ? slug.slice(1) : slug;
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 "$HOME/.relay/agents/${agentPath}/bin/relay-preamble.sh" 2>/dev/null || true
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,8 @@
1
+ /**
2
+ * CLI 명령 실행을 서버에 기록한다 (fire-and-forget).
3
+ * device_hash 기준으로 사용자 여정(login → create → publish)을 추적.
4
+ */
5
+ export declare function trackCommand(command: string, opts?: {
6
+ slug?: string;
7
+ success?: boolean;
8
+ }): void;
@@ -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
+ }
@@ -173,15 +173,33 @@ function createMcpServer() {
173
173
  project,
174
174
  })] };
175
175
  });
176
- server.tool('relay_check_update', 'CLI 및 에이전트 업데이트를 확인합니다', {}, async () => {
177
- const { checkCliVersion, checkAllAgents } = await import('../lib/version-check.js');
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
+ }
178
189
  const cliUpdate = await checkCliVersion(true);
179
- const agentUpdates = await checkAllAgents(true);
180
190
  const updates = [];
181
191
  if (cliUpdate)
182
192
  updates.push({ type: 'cli', current: cliUpdate.current, latest: cliUpdate.latest });
183
- for (const u of agentUpdates)
184
- updates.push({ type: 'agent', slug: u.slug, current: u.current, latest: u.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
+ }
185
203
  if (updates.length === 0) {
186
204
  return { content: [jsonText({ status: 'up_to_date', message: '모두 최신 버전입니다.', cli_version: pkg.version })] };
187
205
  }
@@ -230,6 +248,8 @@ function createMcpServer() {
230
248
  }
231
249
  const cfg = js_yaml_1.default.load(fs_1.default.readFileSync(relayYaml, 'utf-8'));
232
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);
233
253
  const tarPath = await createTarball(relayDir);
234
254
  try {
235
255
  const metadata = {
@@ -254,6 +274,61 @@ function createMcpServer() {
254
274
  return { content: [jsonText({ error: String(err) })], isError: true };
255
275
  }
256
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
+ });
257
332
  // ═══ relay_init — slash command 설치 ═══
258
333
  server.tool('relay_init', 'relay slash command를 설치합니다 (/relay-install, /relay-publish 등)', {}, async () => {
259
334
  try {
@@ -1,30 +1,37 @@
1
- ## Step 1. Relay CLI 준비
1
+ ## Step 1. Relay CLI 준비 및 업데이트
2
2
 
3
3
  ### 환경 A (터미널)
4
4
 
5
+ CLI가 설치되어 있지 않다면:
6
+
5
7
  ```bash
6
8
  npm install -g relayax-cli
7
9
  relay init
8
10
  ```
9
11
 
10
- 이미 설치되어 있다면 최신 버전으로 업데이트합니다:
12
+ 이미 설치되어 있다면 **반드시 최신 버전으로 업데이트**합니다:
11
13
 
12
14
  ```bash
13
15
  npm update -g relayax-cli
14
16
  relay init
15
17
  ```
16
18
 
19
+ `relay init`은 slash command를 설치/업데이트합니다. 업데이트 후 반드시 실행하세요.
20
+
17
21
  Node.js가 없는 경우:
18
22
  - macOS: `brew install node`
19
23
  - Windows: `winget install OpenJS.NodeJS.LTS`
20
24
  - Linux: `curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash && source ~/.bashrc && nvm install --lts`
21
25
 
22
- ### 환경 B (샌드박스)
26
+ ### 환경 B (MCP)
23
27
 
24
- 설치 없이 `npx`로 실행합니다. 이후 모든 `relay` 명령어를 `npx relayax-cli`로 대체하세요.
28
+ **Step 1-1. 버전 확인**: `relay_check_update` tool을 반드시 호출하세요.
29
+ - `status: "up_to_date"` → 최신 버전. Step 1-2로.
30
+ - `status: "updates_available"` → 사용자에게 업데이트 안내:
31
+ > CLI 업데이트가 있습니다. 터미널에서 `npm update -g relayax-cli`를 실행해주세요.
25
32
 
26
- ```bash
27
- npx relayax-cli --version
28
- ```
33
+ 사용자가 업데이트 완료 후 Claude를 재시작하면 최신 MCP 서버가 적용됩니다.
29
34
 
30
- 정상 출력되면 준비 완료입니다.
35
+ **Step 1-2. slash command 설치**: `relay_init` tool을 반드시 호출하세요.
36
+ - `/relay-install`, `/relay-publish` 등 slash command가 설치/업데이트됩니다.
37
+ - 이미 최신이면 자동으로 건너뜁니다.
@@ -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` | 사용자에게 터미널에서 `npx relayax-cli login --device` 안내 |
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
- **중요: 로그인은 반드시 터미널(bash)에서 CLI 명령어로 수행합니다. 브라우저에서 직접 relayax.com에 접속하거나 웹 로그인 페이지를 여는 것은 올바른 방법이 아닙니다.**
3
+ **중요: 브라우저에서 직접 relayax.com에 접속하거나 웹 로그인 페이지를 여는 것은 올바른 방법이 아닙니다.**
4
4
 
5
5
  먼저 로그인 상태를 확인합니다. 이미 로그인되어 있으면 이 단계를 건너뛰세요.
6
6
 
7
7
  - 환경 A: `relay status`
8
- - 환경 B: `npx relayax-cli status`
8
+ - 환경 B (MCP): `relay_status` tool 호출
9
9
 
10
- 로그인이 필요하면 **반드시 아래 bash 명령어를 실행**하세요.
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
- ```bash
26
- npx relayax-cli login --device
27
- ```
25
+ `relay_login` tool을 호출하세요. 이 tool이:
26
+ 1. Device code와 URL을 자동으로 발급합니다.
27
+ 2. 브라우저를 자동으로 엽니다.
28
+ 3. 사용자가 브라우저에서 코드를 입력하면 자동으로 감지합니다.
29
+ 4. 로그인 완료 후 결과를 반환합니다.
28
30
 
29
- - 화면에 URL과 8자리 코드가 표시됩니다.
30
- - 사용자에게 URL과 코드를 보여주세요. 사용자가 직접 브라우저에서 코드를 입력해야 합니다.
31
- - CLI가 자동으로 승인을 감지하고 "✓ 로그인 완료"를 출력합니다.
32
- - timeout이 짧으면 `--timeout 300`을 추가하세요.
31
+ 브라우저가 자동으로 열리지 않으면, 응답에 포함된 URL과 코드를 사용자에게 보여주세요.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relayax-cli",
3
- "version": "0.3.51",
3
+ "version": "0.3.53",
4
4
  "description": "RelayAX Agent Team Marketplace CLI - Install and manage agent teams",
5
5
  "main": "dist/index.js",
6
6
  "bin": {