relayax-cli 0.3.66 → 0.4.12

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/README.md CHANGED
@@ -1,42 +1,174 @@
1
- # relay-cli
1
+ # relay
2
2
 
3
- Agent Team Marketplace CLI - 에이전트 팀을 검색하고 설치하세요.
3
+ **The package manager for AI agents.**
4
+
5
+ Write once, run on any harness. One command install. Built-in usage analytics.
6
+
7
+ ```bash
8
+ npx relayax-cli install @gstack/code-review
9
+ ```
10
+
11
+ ```
12
+ ╭──────────────────────────────────────────────╮
13
+ │ │
14
+ │ relay — AI agent distribution for humans │
15
+ │ and machines. │
16
+ │ │
17
+ │ ✓ installed @gstack/code-review (v2.1.0) │
18
+ │ 3 skills, 1 agent ready │
19
+ │ │
20
+ ╰──────────────────────────────────────────────╯
21
+ ```
22
+
23
+ ---
24
+
25
+ ## Why
26
+
27
+ AI agents are stuck in silos. You build an agent for Claude Code — it doesn't work on OpenClaw. You share it on GitHub — no one knows it exists. You have no idea if anyone actually uses it.
28
+
29
+ **Relay fixes this.**
30
+
31
+ | Problem | Relay |
32
+ |---|---|
33
+ | Agents locked to one harness | Cross-harness compatibility (Claude, OpenClaw, nanoclaw) |
34
+ | No distribution channel | `relay install` — one command, done |
35
+ | Zero feedback from users | Built-in analytics — see which skills get used |
36
+
37
+ ---
4
38
 
5
39
  ## Quick Start
6
40
 
7
41
  ```bash
8
- # 글로벌 설치 없이 바로 사용
9
- npx relay-cli install @author/team-name
42
+ # Install an agent no setup required
43
+ npx relayax-cli install @author/agent-name
10
44
 
11
- # 또는 글로벌 설치
12
- npm install -g relayax-cli
13
- relay install @author/team-name
45
+ # Or install globally
46
+ npm i -g relayax-cli
47
+ relay install @author/agent-name
14
48
  ```
15
49
 
50
+ That's it. The agent is ready in your `.relay/agents/` directory, compatible with your harness.
51
+
52
+ ---
53
+
54
+ ## For Agent Builders
55
+
56
+ ```bash
57
+ # Publish your agent to the registry
58
+ relay publish
59
+
60
+ # See who's using it
61
+ relay status --analytics
62
+ ```
63
+
64
+ Relay tracks skill-level usage out of the box. No extra setup. You'll know exactly which skills land and which don't — so you can ship better agents, faster.
65
+
66
+ ### Package Format
67
+
68
+ ```yaml
69
+ # team.yaml
70
+ name: code-review
71
+ version: 2.1.0
72
+ harness:
73
+ - claude
74
+ - openclaw
75
+ - nanoclaw
76
+ agents:
77
+ - name: reviewer
78
+ type: passive
79
+ skills:
80
+ - name: review-pr
81
+ - name: security-check
82
+ - name: style-lint
83
+ ```
84
+
85
+ One spec. Every harness.
86
+
87
+ ---
88
+
16
89
  ## Commands
17
90
 
18
- | Command | Description |
19
- |---------|-------------|
20
- | `relay init` | 초기 설정 (설치 경로, API URL) |
21
- | `relay search <keyword>` | 검색 |
22
- | `relay install <name>` | 설치 |
23
- | `relay list` | 설치된 목록 |
24
- | `relay uninstall <name>` | 제거 |
91
+ | Command | What it does |
92
+ |---|---|
93
+ | `relay install <name>` | Install an agent |
94
+ | `relay search <keyword>` | Find agents in the registry |
95
+ | `relay publish` | Publish your agent |
96
+ | `relay list` | List installed agents |
97
+ | `relay status` | Check environment + analytics |
98
+ | `relay update` | Update agents to latest |
99
+ | `relay uninstall <name>` | Remove an agent |
100
+ | `relay diff <name>` | See what changed between versions |
25
101
 
26
- ## Options
102
+ All output is JSON by default (for AI agents). Add `--pretty` for human-readable format.
27
103
 
28
- - `--pretty` - 사람이 읽기 좋은 포맷으로 출력 (기본: JSON)
29
- - `--json` - JSON 출력 (기본값, 에이전트 친화적)
104
+ ---
30
105
 
31
- ## For AI Agents
106
+ ## How It Works
107
+
108
+ ```
109
+ relay install @team/agent
110
+
111
+ ╭──────────┴──────────╮
112
+ │ Relay Registry │
113
+ │ (relay.ax cloud) │
114
+ ╰──────────┬──────────╯
115
+
116
+ ╭──────────┴──────────╮
117
+ │ relay agent spec │
118
+ │ (universal format) │
119
+ ╰──┬───────┬───────┬──╯
120
+ │ │ │
121
+ ┌─────┴─┐ ┌──┴───┐ ┌─┴──────┐
122
+ │Claude │ │Open │ │nano │
123
+ │ Code │ │Claw │ │claw │
124
+ └───────┘ └──────┘ └────────┘
125
+ ```
32
126
 
33
- relay CLI는 에이전트가 1차 사용자입니다. 모든 출력은 JSON 기본입니다.
127
+ Relay resolves the right format for your harness automatically. Builders write one spec, users install with one command.
128
+
129
+ ---
130
+
131
+ ## AI-Native
132
+
133
+ Relay is built for AI agents as first-class users. The CLI outputs structured JSON so agents can search, install, and manage other agents autonomously.
34
134
 
35
135
  ```bash
36
- # 에이전트가 검색
37
- relay search "keyword" | jq '.results[].slug'
136
+ # An agent searching for tools
137
+ relay search "database migration" | jq '.results[].slug'
138
+
139
+ # An agent installing what it needs
140
+ relay install @tools/db-migrate
141
+ # → {"status":"ok","agent":"db-migrate","skills":["migrate","rollback","seed"]}
142
+ ```
38
143
 
39
- # 에이전트가 설치
40
- relay install @author/team-name
41
- # → {"status":"ok","team":"Team Name","commands":[...]}
144
+ Relay also ships as an **MCP server**, so any MCP-compatible agent can use it directly:
145
+
146
+ ```bash
147
+ relay mcp
42
148
  ```
149
+
150
+ ---
151
+
152
+ ## Open Core
153
+
154
+ The CLI and agent spec are open source (MIT). Build agents, publish them, self-host your own registry — no vendor lock-in.
155
+
156
+ [relay.ax](https://relayax.com) provides the hosted registry with:
157
+ - Private agent hosting
158
+ - Organization management & access control
159
+ - Usage analytics dashboard
160
+ - Enterprise SSO & audit logs
161
+
162
+ ---
163
+
164
+ ## Community
165
+
166
+ - [25+ production agents](https://relayax.com) ready to install
167
+ - [Builder docs](https://relayax.com/docs) for creating your own
168
+ - [Discord](#) for help and discussion
169
+
170
+ ---
171
+
172
+ <p align="center">
173
+ <sub>Built by <a href="https://relayax.com">RelayAX</a></sub>
174
+ </p>
@@ -143,7 +143,14 @@ function registerCreate(program) {
143
143
  });
144
144
  }
145
145
  }
146
- // 3. .relay/relay.yaml 생성
146
+ // 3. recommended_scope 자동 추천
147
+ // rules/ 존재 or 프레임워크 태그 → local, 그 외 → global
148
+ const frameworkTags = ['nextjs', 'react', 'vue', 'angular', 'svelte', 'nuxt', 'remix', 'astro', 'django', 'rails', 'laravel', 'spring', 'express', 'fastapi', 'flask'];
149
+ const hasRules = fs_1.default.existsSync(path_1.default.join(projectPath, '.relay', 'rules'))
150
+ || fs_1.default.existsSync(path_1.default.join(projectPath, 'rules'));
151
+ const hasFrameworkTag = tags.some((t) => frameworkTags.includes(t.toLowerCase()));
152
+ const recommendedScope = (hasRules || hasFrameworkTag) ? 'local' : 'global';
153
+ // 4. .relay/relay.yaml 생성
147
154
  fs_1.default.mkdirSync(relayDir, { recursive: true });
148
155
  const yamlData = {
149
156
  name,
@@ -151,6 +158,7 @@ function registerCreate(program) {
151
158
  description,
152
159
  version: '1.0.0',
153
160
  type: 'hybrid',
161
+ recommended_scope: recommendedScope,
154
162
  tags,
155
163
  visibility,
156
164
  contents: [],
@@ -187,6 +195,7 @@ function registerCreate(program) {
187
195
  status: 'ok',
188
196
  name,
189
197
  slug: slug,
198
+ recommended_scope: recommendedScope,
190
199
  relay_yaml: 'created',
191
200
  directories: createdDirs,
192
201
  local_commands: localResults,
@@ -194,8 +203,11 @@ function registerCreate(program) {
194
203
  }));
195
204
  }
196
205
  else {
206
+ const scopeLabel = recommendedScope === 'global' ? '\x1b[32m글로벌\x1b[0m' : '\x1b[33m로컬\x1b[0m';
207
+ const scopeReason = hasRules ? 'rules/ 감지' : hasFrameworkTag ? '프레임워크 태그 감지' : '범용 에이전트';
197
208
  console.log(`\n\x1b[32m✓ ${name} 에이전트 프로젝트 생성 완료\x1b[0m\n`);
198
209
  console.log(` .relay/relay.yaml 생성됨`);
210
+ console.log(` recommended_scope: ${scopeLabel} (${scopeReason})`);
199
211
  if (createdDirs.length > 0) {
200
212
  console.log(` 디렉토리 생성: ${createdDirs.join(', ')}`);
201
213
  }
@@ -1,9 +1,15 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.registerDiff = registerDiff;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const os_1 = __importDefault(require("os"));
4
10
  const api_js_1 = require("../lib/api.js");
5
11
  const slug_js_1 = require("../lib/slug.js");
6
- const storage_js_1 = require("../lib/storage.js");
12
+ const git_operations_js_1 = require("../lib/git-operations.js");
7
13
  function registerDiff(program) {
8
14
  program
9
15
  .command('diff <slug> <v1> <v2>')
@@ -13,6 +19,7 @@ function registerDiff(program) {
13
19
  try {
14
20
  const resolved = await (0, slug_js_1.resolveSlug)(slugInput);
15
21
  const versions = await (0, api_js_1.fetchAgentVersions)(resolved.full);
22
+ const info = await (0, api_js_1.fetchAgentInfo)(resolved.full);
16
23
  const ver1 = versions.find((v) => v.version === v1);
17
24
  const ver2 = versions.find((v) => v.version === v2);
18
25
  if (!ver1 || !ver2) {
@@ -21,19 +28,36 @@ function registerDiff(program) {
21
28
  if (!json) {
22
29
  console.log(`\n\x1b[1m${resolved.full}\x1b[0m v${v1} ↔ v${v2} 비교 중...\n`);
23
30
  }
24
- // Download both versions to temp dirs
25
- const tempDir1 = (0, storage_js_1.makeTempDir)();
26
- const tempDir2 = (0, storage_js_1.makeTempDir)();
27
- try {
28
- // Get download URLs for both versions via registry API
29
- // For now, we use the current version's package_url as fallback
30
- // The registry API returns the latest version; for specific versions,
31
- // we'd need a version-specific endpoint
32
- const info = await (0, api_js_1.fetchAgentInfo)(resolved.full);
33
- if (!info.package_url) {
34
- throw new Error('패키지 URL을 가져올 없습니다');
31
+ // Use git diff if git_url is available
32
+ if (info.git_url) {
33
+ (0, git_operations_js_1.checkGitInstalled)();
34
+ const tempDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'relay-diff-'));
35
+ try {
36
+ (0, git_operations_js_1.gitClone)(info.git_url, tempDir);
37
+ const diffOutput = (0, git_operations_js_1.gitDiff)(tempDir, `v${v1}`, `v${v2}`);
38
+ if (json) {
39
+ console.log(JSON.stringify({
40
+ slug: resolved.full,
41
+ v1: { version: v1, created_at: ver1.created_at, changelog: ver1.changelog },
42
+ v2: { version: v2, created_at: ver2.created_at, changelog: ver2.changelog },
43
+ diff: diffOutput,
44
+ }));
45
+ }
46
+ else {
47
+ if (diffOutput.trim()) {
48
+ console.log(diffOutput);
49
+ }
50
+ else {
51
+ console.log(' 변경 사항이 없습니다.');
52
+ }
53
+ }
35
54
  }
36
- // Since version-specific download isn't available yet, show versions info
55
+ finally {
56
+ fs_1.default.rmSync(tempDir, { recursive: true, force: true });
57
+ }
58
+ }
59
+ else {
60
+ // Fallback: show version info only (no git URL available)
37
61
  if (json) {
38
62
  console.log(JSON.stringify({
39
63
  slug: resolved.full,
@@ -50,13 +74,9 @@ function registerDiff(program) {
50
74
  if (ver2.changelog)
51
75
  console.log(` ${ver2.changelog}`);
52
76
  console.log();
53
- console.log(`\x1b[33m 버전별 패키지 다운로드 비교는 추후 지원 예정입니다.\x1b[0m`);
77
+ console.log(`\x1b[33m git 기반 diff는 새로 배포된 에이전트에서만 지원됩니다.\x1b[0m`);
54
78
  }
55
79
  }
56
- finally {
57
- (0, storage_js_1.removeTempDir)(tempDir1);
58
- (0, storage_js_1.removeTempDir)(tempDir2);
59
- }
60
80
  }
61
81
  catch (err) {
62
82
  const message = err instanceof Error ? err.message : String(err);
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerExport(program: Command): void;
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.registerExport = registerExport;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const js_yaml_1 = __importDefault(require("js-yaml"));
10
+ const manifest_generator_js_1 = require("../lib/manifest-generator.js");
11
+ const paths_js_1 = require("../lib/paths.js");
12
+ function registerExport(program) {
13
+ program
14
+ .command('export <platform>')
15
+ .description('로컬에서 플랫폼 네이티브 매니페스트를 생성합니다')
16
+ .option('--out <dir>', '출력 디렉토리 (기본: .relay/export/<platform>/)')
17
+ .option('--project <dir>', '프로젝트 루트 경로')
18
+ .action(async (platform, opts) => {
19
+ const json = program.opts().json ?? false;
20
+ const projectPath = (0, paths_js_1.resolveProjectPath)(opts.project);
21
+ const relayDir = path_1.default.join(projectPath, '.relay');
22
+ const relayYamlPath = path_1.default.join(relayDir, 'relay.yaml');
23
+ // Check relay.yaml exists
24
+ if (!fs_1.default.existsSync(relayYamlPath)) {
25
+ const msg = 'relay.yaml not found';
26
+ if (json) {
27
+ console.error(JSON.stringify({ error: 'NOT_FOUND', message: msg }));
28
+ }
29
+ else {
30
+ console.error(`\x1b[31m${msg}\x1b[0m`);
31
+ console.error(' relay.yaml이 있는 에이전트 디렉토리에서 실행하세요.');
32
+ }
33
+ process.exit(1);
34
+ }
35
+ // Validate platform
36
+ const validPlatforms = [...manifest_generator_js_1.SUPPORTED_PLATFORMS, 'all'];
37
+ if (!validPlatforms.includes(platform)) {
38
+ const msg = `지원하지 않는 플랫폼: ${platform}`;
39
+ if (json) {
40
+ console.error(JSON.stringify({ error: 'INVALID_PLATFORM', message: msg, supported: manifest_generator_js_1.SUPPORTED_PLATFORMS }));
41
+ }
42
+ else {
43
+ console.error(`\x1b[31m${msg}\x1b[0m`);
44
+ console.error(` 지원 플랫폼: ${manifest_generator_js_1.SUPPORTED_PLATFORMS.join(', ')}, all`);
45
+ }
46
+ process.exit(1);
47
+ }
48
+ // Parse relay.yaml
49
+ const yamlContent = fs_1.default.readFileSync(relayYamlPath, 'utf-8');
50
+ const raw = js_yaml_1.default.load(yamlContent) ?? {};
51
+ const manifestYaml = {
52
+ name: String(raw.name ?? ''),
53
+ slug: String(raw.slug ?? ''),
54
+ description: String(raw.description ?? ''),
55
+ version: String(raw.version ?? '1.0.0'),
56
+ source: raw.source ? String(raw.source) : undefined,
57
+ org_slug: raw.org_slug ? String(raw.org_slug) : undefined,
58
+ platforms: platform === 'all' ? undefined : [platform],
59
+ };
60
+ // Generate manifests
61
+ const files = (0, manifest_generator_js_1.generateManifests)(manifestYaml, relayDir);
62
+ if (files.length === 0) {
63
+ if (json) {
64
+ console.log(JSON.stringify({ status: 'empty', message: '생성할 매니페스트가 없습니다.' }));
65
+ }
66
+ else {
67
+ console.log('생성할 매니페스트가 없습니다.');
68
+ }
69
+ return;
70
+ }
71
+ // Determine output directory
72
+ const outDir = opts.out
73
+ ? path_1.default.resolve(opts.out)
74
+ : path_1.default.join(relayDir, 'export', platform);
75
+ fs_1.default.mkdirSync(outDir, { recursive: true });
76
+ // Write files
77
+ const written = [];
78
+ for (const file of files) {
79
+ const filePath = path_1.default.join(outDir, file.relativePath);
80
+ fs_1.default.mkdirSync(path_1.default.dirname(filePath), { recursive: true });
81
+ fs_1.default.writeFileSync(filePath, file.content);
82
+ written.push(file.relativePath);
83
+ }
84
+ if (json) {
85
+ console.log(JSON.stringify({ status: 'ok', platform, output_dir: outDir, files: written }));
86
+ }
87
+ else {
88
+ console.log(`\n\x1b[32m✓ 매니페스트 생성 완료\x1b[0m (${platform})`);
89
+ console.log(` 출력: \x1b[36m${outDir}\x1b[0m\n`);
90
+ for (const f of written) {
91
+ console.log(` \x1b[90m•\x1b[0m ${f}`);
92
+ }
93
+ // Platform-specific usage hints
94
+ console.log('');
95
+ if (platform === 'claude-code' || platform === 'all') {
96
+ console.log(' \x1b[90mClaude Code:\x1b[0m /plugin marketplace add <marketplace.json URL>');
97
+ }
98
+ if (platform === 'codex' || platform === 'all') {
99
+ console.log(' \x1b[90mCodex:\x1b[0m .codex-plugin/plugin.json을 Codex에 등록하세요');
100
+ }
101
+ if (platform === 'antigravity' || platform === 'all') {
102
+ console.log(' \x1b[90mAntigravity:\x1b[0m .agent/skills/를 프로젝트에 복사하세요');
103
+ }
104
+ }
105
+ });
106
+ }
@@ -1,13 +1,9 @@
1
1
  import { Command } from 'commander';
2
- /**
3
- * 글로벌 User 커맨드를 감지된 모든 에이전트 CLI에 설치한다.
4
- * ~/{skillsDir}/commands/relay/ 에 설치.
5
- * 기존 파일 중 현재 커맨드 목록에 없는 것은 제거한다.
6
- */
7
2
  export declare function installGlobalUserCommands(): {
8
3
  installed: boolean;
9
4
  commands: string[];
10
5
  tools: string[];
6
+ removed: string[];
11
7
  };
12
8
  /**
13
9
  * 글로벌 User 커맨드가 이미 설치되어 있는지 확인한다.
@@ -36,10 +36,15 @@ function showWelcome() {
36
36
  ' 에이전트 CLI에 relay 커맨드를 연결합니다.',
37
37
  '',
38
38
  ' \x1b[2mUser 커맨드 (글로벌)\x1b[0m',
39
- ' /relay-install 에이전트 탐색 & 설치',
40
- ' /relay-status 설치 현황 & Space',
39
+ ' /relay-explore 에이전트 탐색 & 추천',
40
+ ' /relay-create 에이전트 생성 & 배포',
41
+ ' /relay-status 설치 현황 & Organization',
41
42
  ' /relay-uninstall 에이전트 삭제',
42
43
  '',
44
+ ' \x1b[2mCLI 명령어\x1b[0m',
45
+ ' relay install 에이전트 설치 (CLI 한 줄 완결)',
46
+ ' relay publish 재배포 (--patch/--minor/--major)',
47
+ '',
43
48
  ];
44
49
  console.log(lines.join('\n'));
45
50
  }
@@ -65,20 +70,28 @@ async function selectToolsInteractively(detectedIds) {
65
70
  * ~/{skillsDir}/commands/relay/ 에 설치.
66
71
  * 기존 파일 중 현재 커맨드 목록에 없는 것은 제거한다.
67
72
  */
73
+ /** 제거된 레거시 커맨드 → 대체 안내 매핑 */
74
+ const LEGACY_COMMANDS = {
75
+ 'relay-install': 'relay install (CLI) 또는 /relay-explore',
76
+ 'relay-publish': 'relay publish --patch (CLI) 또는 /relay-create',
77
+ };
68
78
  function installGlobalUserCommands() {
69
79
  const globalCLIs = (0, ai_tools_js_1.detectGlobalCLIs)();
70
80
  const currentIds = new Set(command_adapter_js_1.USER_COMMANDS.map((c) => c.id));
71
81
  const commands = [];
72
82
  const tools = [];
73
- // 감지된 CLI가 없으면 설치하지 않음 (사용자가 --tools로 지정하거나 CLI를 먼저 설치해야 함)
83
+ const removed = [];
74
84
  const targetDirs = globalCLIs.map((t) => ({ name: t.name, dir: (0, command_adapter_js_1.getGlobalCommandDirForTool)(t.skillsDir), getPath: (id) => (0, command_adapter_js_1.getGlobalCommandPathForTool)(t.skillsDir, id) }));
75
85
  for (const target of targetDirs) {
76
86
  fs_1.default.mkdirSync(target.dir, { recursive: true });
77
- // 기존 파일 중 현재 목록에 없는 것 제거
87
+ // 기존 파일 중 현재 목록에 없는 것 제거 + 레거시 안내
78
88
  for (const file of fs_1.default.readdirSync(target.dir)) {
79
89
  const id = file.replace(/\.md$/, '');
80
90
  if (!currentIds.has(id)) {
81
91
  fs_1.default.unlinkSync(path_1.default.join(target.dir, file));
92
+ if (LEGACY_COMMANDS[id] && !removed.includes(id)) {
93
+ removed.push(id);
94
+ }
82
95
  }
83
96
  }
84
97
  // 현재 커맨드 설치 (덮어쓰기)
@@ -87,11 +100,10 @@ function installGlobalUserCommands() {
87
100
  }
88
101
  tools.push(target.name);
89
102
  }
90
- // commands 목록은 한 번만
91
103
  for (const cmd of command_adapter_js_1.USER_COMMANDS) {
92
104
  commands.push(cmd.id);
93
105
  }
94
- return { installed: true, commands, tools };
106
+ return { installed: true, commands, tools, removed };
95
107
  }
96
108
  /**
97
109
  * 글로벌 User 커맨드가 이미 설치되어 있는지 확인한다.
@@ -149,10 +161,12 @@ function registerInit(program) {
149
161
  // ── 1. 글로벌 User 커맨드 설치 ──
150
162
  let globalStatus = 'already';
151
163
  let globalTools = [];
164
+ let removedCommands = [];
152
165
  {
153
166
  const result = installGlobalUserCommands();
154
167
  globalStatus = hasGlobalUserCommands() ? 'updated' : 'installed';
155
168
  globalTools = result.tools;
169
+ removedCommands = result.removed;
156
170
  // Register relay-core in installed.json
157
171
  const installed = (0, config_js_1.loadInstalled)();
158
172
  installed['relay-core'] = {
@@ -245,6 +259,14 @@ function registerInit(program) {
245
259
  }
246
260
  else {
247
261
  console.log(`\n\x1b[32m✓ relay 초기화 완료\x1b[0m\n`);
262
+ // 레거시 커맨드 마이그레이션 안내
263
+ if (removedCommands.length > 0) {
264
+ console.log(` \x1b[33m⚠ 변경된 커맨드:\x1b[0m`);
265
+ for (const id of removedCommands) {
266
+ console.log(` \x1b[31m✗ /${id}\x1b[0m → ${LEGACY_COMMANDS[id]}`);
267
+ }
268
+ console.log();
269
+ }
248
270
  // 글로벌
249
271
  {
250
272
  const toolNames = globalTools.length > 0 ? globalTools.join(', ') : '(감지된 CLI 없음)';
@@ -9,6 +9,7 @@ const os_1 = __importDefault(require("os"));
9
9
  const path_1 = __importDefault(require("path"));
10
10
  const api_js_1 = require("../lib/api.js");
11
11
  const storage_js_1 = require("../lib/storage.js");
12
+ const git_operations_js_1 = require("../lib/git-operations.js");
12
13
  const config_js_1 = require("../lib/config.js");
13
14
  const slug_js_1 = require("../lib/slug.js");
14
15
  const preamble_js_1 = require("../lib/preamble.js");
@@ -212,11 +213,10 @@ function registerInstall(program) {
212
213
  throw new Error('에이전트 정보를 가져오지 못했습니다.');
213
214
  // Re-bind as non-optional so TypeScript tracks the narrowing through nested scopes
214
215
  let resolvedAgent = agent;
215
- // Scope 자동결정: --global/--local 플래그 > agent_type 기반
216
+ // Scope 자동결정: --global/--local 플래그 > recommended_scope > agent_type 기반
216
217
  const scope = _opts.global ? 'global'
217
218
  : _opts.local ? 'local'
218
- : resolvedAgent.type === 'passive' ? 'local'
219
- : 'global';
219
+ : resolvedAgent.recommended_scope ?? (resolvedAgent.type === 'passive' ? 'local' : 'global');
220
220
  const agentDir = scope === 'global'
221
221
  ? path_1.default.join(os_1.default.homedir(), '.relay', 'agents', parsed.owner, parsed.name)
222
222
  : path_1.default.join(projectPath, '.relay', 'agents', parsed.owner, parsed.name);
@@ -240,31 +240,39 @@ function registerInstall(program) {
240
240
  process.exit(1);
241
241
  }
242
242
  }
243
- // 3. Download package (retry once if signed URL expired)
244
- let tarPath;
245
- try {
246
- tarPath = await (0, storage_js_1.downloadPackage)(resolvedAgent.package_url, tempDir);
243
+ // 3. Download package: prefer git clone, fallback to tar.gz
244
+ const requestedVersion = versionMatch ? versionMatch[2] : undefined;
245
+ if (resolvedAgent.git_url) {
246
+ // Git clone path
247
+ (0, git_operations_js_1.checkGitInstalled)();
248
+ const gitUrl = (0, git_operations_js_1.buildGitUrl)(resolvedAgent.git_url, { code: _opts.code });
249
+ await (0, storage_js_1.clonePackage)(gitUrl, agentDir, requestedVersion);
247
250
  }
248
- catch (dlErr) {
249
- const dlMsg = dlErr instanceof Error ? dlErr.message : String(dlErr);
250
- if (dlMsg.includes('403') || dlMsg.includes('expired')) {
251
- // Signed URL expired — re-fetch agent info for new URL and retry
252
- if (!json) {
253
- console.error('\x1b[33m⚙ 다운로드 URL 만료, 재시도 중...\x1b[0m');
254
- }
255
- resolvedAgent = await (0, api_js_1.fetchAgentInfo)(slug);
251
+ else {
252
+ // Legacy tar.gz path (retry once if signed URL expired)
253
+ let tarPath;
254
+ try {
256
255
  tarPath = await (0, storage_js_1.downloadPackage)(resolvedAgent.package_url, tempDir);
257
256
  }
258
- else {
259
- throw dlErr;
257
+ catch (dlErr) {
258
+ const dlMsg = dlErr instanceof Error ? dlErr.message : String(dlErr);
259
+ if (dlMsg.includes('403') || dlMsg.includes('expired')) {
260
+ if (!json) {
261
+ console.error('\x1b[33m⚙ 다운로드 URL 만료, 재시도 중...\x1b[0m');
262
+ }
263
+ resolvedAgent = await (0, api_js_1.fetchAgentInfo)(slug);
264
+ tarPath = await (0, storage_js_1.downloadPackage)(resolvedAgent.package_url, tempDir);
265
+ }
266
+ else {
267
+ throw dlErr;
268
+ }
260
269
  }
270
+ if (fs_1.default.existsSync(agentDir)) {
271
+ fs_1.default.rmSync(agentDir, { recursive: true, force: true });
272
+ }
273
+ fs_1.default.mkdirSync(agentDir, { recursive: true });
274
+ await (0, storage_js_1.extractPackage)(tarPath, agentDir);
261
275
  }
262
- // 4. Extract to .relay/agents/<slug>/
263
- if (fs_1.default.existsSync(agentDir)) {
264
- fs_1.default.rmSync(agentDir, { recursive: true, force: true });
265
- }
266
- fs_1.default.mkdirSync(agentDir, { recursive: true });
267
- await (0, storage_js_1.extractPackage)(tarPath, agentDir);
268
276
  // 4.5. Inject preamble (update check) into SKILL.md and commands
269
277
  (0, preamble_js_1.injectPreambleToAgent)(agentDir, slug);
270
278
  // 5. Deploy symlinks to detected AI tool directories