relayax-cli 0.2.41 → 0.3.42
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/access.js +12 -12
- package/dist/commands/changelog.js +2 -2
- package/dist/commands/check-update.js +12 -12
- package/dist/commands/create.js +46 -19
- package/dist/commands/deploy-record.js +2 -2
- package/dist/commands/diff.d.ts +2 -0
- package/dist/commands/diff.js +72 -0
- package/dist/commands/grant.d.ts +33 -0
- package/dist/commands/grant.js +190 -0
- package/dist/commands/init.js +10 -10
- package/dist/commands/install.js +125 -256
- package/dist/commands/join.d.ts +3 -2
- package/dist/commands/join.js +18 -69
- package/dist/commands/list.js +23 -26
- package/dist/commands/login.js +10 -3
- package/dist/commands/orgs.d.ts +10 -0
- package/dist/commands/orgs.js +128 -0
- package/dist/commands/outdated.js +7 -7
- package/dist/commands/package.d.ts +18 -0
- package/dist/commands/package.js +355 -146
- package/dist/commands/ping.js +5 -5
- package/dist/commands/publish.d.ts +1 -1
- package/dist/commands/publish.js +105 -103
- package/dist/commands/search.js +2 -2
- package/dist/commands/status.js +11 -11
- package/dist/commands/uninstall.js +7 -7
- package/dist/commands/update.js +22 -22
- package/dist/commands/versions.d.ts +2 -0
- package/dist/commands/versions.js +44 -0
- package/dist/index.js +8 -2
- package/dist/lib/ai-tools.d.ts +15 -0
- package/dist/lib/ai-tools.js +48 -1
- package/dist/lib/api.d.ts +13 -12
- package/dist/lib/api.js +24 -39
- package/dist/lib/command-adapter.js +41 -693
- package/dist/lib/config.d.ts +10 -5
- package/dist/lib/config.js +106 -24
- package/dist/lib/guide.js +34 -79
- package/dist/lib/installer.d.ts +2 -2
- package/dist/lib/installer.js +4 -4
- package/dist/lib/preamble.d.ts +4 -4
- package/dist/lib/preamble.js +14 -15
- package/dist/lib/slug.d.ts +5 -1
- package/dist/lib/slug.js +52 -9
- package/dist/lib/update-cache.js +4 -4
- package/dist/lib/version-check.d.ts +3 -3
- package/dist/lib/version-check.js +13 -13
- package/dist/prompts/_business-card.md +41 -0
- package/dist/prompts/_error-handling.md +38 -0
- package/dist/prompts/_requirements-check.md +59 -0
- package/dist/prompts/_setup-cli.md +19 -0
- package/dist/prompts/_setup-login.md +7 -0
- package/dist/prompts/_setup-org.md +27 -0
- package/dist/prompts/business-card.md +41 -0
- package/dist/prompts/error-handling.md +38 -0
- package/dist/prompts/index.d.ts +7 -0
- package/dist/prompts/index.js +28 -0
- package/dist/prompts/install.md +187 -0
- package/dist/prompts/publish.md +444 -0
- package/dist/prompts/requirements-check.md +59 -0
- package/dist/types.d.ts +10 -10
- package/package.json +3 -3
package/dist/lib/config.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export declare const API_URL = "https://www.relayax.com";
|
|
|
9
9
|
export declare function getInstallPath(override?: string): string;
|
|
10
10
|
/** ~/.relay/ — 글로벌 (token, CLI cache) */
|
|
11
11
|
export declare function ensureGlobalRelayDir(): void;
|
|
12
|
-
/** cwd/.relay/ — 프로젝트 로컬 (installed.json,
|
|
12
|
+
/** cwd/.relay/ — 프로젝트 로컬 (installed.json, agents/) */
|
|
13
13
|
export declare function ensureProjectRelayDir(): void;
|
|
14
14
|
export interface TokenData {
|
|
15
15
|
access_token: string;
|
|
@@ -22,10 +22,15 @@ export declare function saveTokenData(data: TokenData): void;
|
|
|
22
22
|
export declare function saveToken(token: string): void;
|
|
23
23
|
/**
|
|
24
24
|
* 유효한 access_token을 반환한다.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
25
|
+
*
|
|
26
|
+
* Supabase는 refresh token rotation을 사용하므로:
|
|
27
|
+
* - refresh 시 이전 refresh_token이 무효화됨
|
|
28
|
+
* - 병렬 CLI 호출에서 동시 refresh 방지 필요 (lock)
|
|
29
|
+
* - refresh 성공 시 새 토큰을 즉시 파일에 저장
|
|
30
|
+
*
|
|
31
|
+
* 타이밍:
|
|
32
|
+
* - 만료 10분 전부터 proactive refresh
|
|
33
|
+
* - refresh 실패해도 access_token이 아직 유효하면 계속 사용
|
|
29
34
|
*/
|
|
30
35
|
export declare function getValidToken(): Promise<string | undefined>;
|
|
31
36
|
/** 프로젝트 로컬 installed.json 읽기 */
|
package/dist/lib/config.js
CHANGED
|
@@ -49,7 +49,7 @@ function ensureGlobalRelayDir() {
|
|
|
49
49
|
fs_1.default.mkdirSync(GLOBAL_RELAY_DIR, { recursive: true });
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
-
/** cwd/.relay/ — 프로젝트 로컬 (installed.json,
|
|
52
|
+
/** cwd/.relay/ — 프로젝트 로컬 (installed.json, agents/) */
|
|
53
53
|
function ensureProjectRelayDir() {
|
|
54
54
|
const dir = path_1.default.join(process.cwd(), '.relay');
|
|
55
55
|
if (!fs_1.default.existsSync(dir)) {
|
|
@@ -81,56 +81,138 @@ function saveTokenData(data) {
|
|
|
81
81
|
ensureGlobalRelayDir();
|
|
82
82
|
const tokenFile = path_1.default.join(GLOBAL_RELAY_DIR, 'token');
|
|
83
83
|
fs_1.default.writeFileSync(tokenFile, JSON.stringify(data), { mode: 0o600 });
|
|
84
|
+
// writeFileSync mode only applies on creation — fix existing files
|
|
85
|
+
fs_1.default.chmodSync(tokenFile, 0o600);
|
|
84
86
|
}
|
|
85
87
|
function saveToken(token) {
|
|
86
88
|
ensureGlobalRelayDir();
|
|
87
89
|
const tokenFile = path_1.default.join(GLOBAL_RELAY_DIR, 'token');
|
|
88
90
|
fs_1.default.writeFileSync(tokenFile, JSON.stringify({ access_token: token }), { mode: 0o600 });
|
|
91
|
+
fs_1.default.chmodSync(tokenFile, 0o600);
|
|
89
92
|
}
|
|
93
|
+
const LOCK_FILE = path_1.default.join(os_1.default.homedir(), '.relay', '.token.lock');
|
|
94
|
+
const LOCK_TIMEOUT = 15000; // 15s
|
|
90
95
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* 2. expires_at이 아직 유효하면 access_token 반환
|
|
94
|
-
* 3. 만료되었으면 refresh_token으로 갱신 시도
|
|
95
|
-
* 4. 갱신 실패 시 undefined (재로그인 필요)
|
|
96
|
+
* 파일 기반 lock — 여러 CLI 프로세스가 동시에 refresh하는 것을 방지.
|
|
97
|
+
* Supabase refresh token rotation으로 인해 동시 refresh가 치명적.
|
|
96
98
|
*/
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return
|
|
99
|
+
function acquireLock() {
|
|
100
|
+
try {
|
|
101
|
+
// O_EXCL: 파일이 이미 존재하면 실패 (atomic)
|
|
102
|
+
const fd = fs_1.default.openSync(LOCK_FILE, fs_1.default.constants.O_CREAT | fs_1.default.constants.O_EXCL | fs_1.default.constants.O_WRONLY);
|
|
103
|
+
fs_1.default.writeSync(fd, String(Date.now()));
|
|
104
|
+
fs_1.default.closeSync(fd);
|
|
105
|
+
return true;
|
|
104
106
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
catch {
|
|
108
|
+
// lock 파일이 이미 있음 — stale check
|
|
109
|
+
try {
|
|
110
|
+
const content = fs_1.default.readFileSync(LOCK_FILE, 'utf-8');
|
|
111
|
+
const lockTime = Number(content);
|
|
112
|
+
if (Date.now() - lockTime > LOCK_TIMEOUT) {
|
|
113
|
+
// stale lock — 제거 후 재시도
|
|
114
|
+
fs_1.default.unlinkSync(LOCK_FILE);
|
|
115
|
+
return acquireLock();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch { /* ignore */ }
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function releaseLock() {
|
|
123
|
+
try {
|
|
124
|
+
fs_1.default.unlinkSync(LOCK_FILE);
|
|
125
|
+
}
|
|
126
|
+
catch { /* ignore */ }
|
|
127
|
+
}
|
|
128
|
+
async function doRefresh(refreshToken) {
|
|
108
129
|
try {
|
|
109
130
|
const res = await fetch(`${exports.API_URL}/api/auth/refresh`, {
|
|
110
131
|
method: 'POST',
|
|
111
132
|
headers: { 'Content-Type': 'application/json' },
|
|
112
|
-
body: JSON.stringify({ refresh_token:
|
|
133
|
+
body: JSON.stringify({ refresh_token: refreshToken }),
|
|
134
|
+
signal: AbortSignal.timeout(10000),
|
|
113
135
|
});
|
|
114
136
|
if (!res.ok)
|
|
115
|
-
return
|
|
116
|
-
|
|
117
|
-
saveTokenData(refreshed);
|
|
118
|
-
return refreshed.access_token;
|
|
137
|
+
return null;
|
|
138
|
+
return (await res.json());
|
|
119
139
|
}
|
|
120
140
|
catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 유효한 access_token을 반환한다.
|
|
146
|
+
*
|
|
147
|
+
* Supabase는 refresh token rotation을 사용하므로:
|
|
148
|
+
* - refresh 시 이전 refresh_token이 무효화됨
|
|
149
|
+
* - 병렬 CLI 호출에서 동시 refresh 방지 필요 (lock)
|
|
150
|
+
* - refresh 성공 시 새 토큰을 즉시 파일에 저장
|
|
151
|
+
*
|
|
152
|
+
* 타이밍:
|
|
153
|
+
* - 만료 10분 전부터 proactive refresh
|
|
154
|
+
* - refresh 실패해도 access_token이 아직 유효하면 계속 사용
|
|
155
|
+
*/
|
|
156
|
+
async function getValidToken() {
|
|
157
|
+
// 매번 파일에서 새로 읽음 (다른 프로세스가 갱신했을 수 있으므로)
|
|
158
|
+
const data = loadTokenData();
|
|
159
|
+
if (!data)
|
|
121
160
|
return undefined;
|
|
161
|
+
const now = Date.now() / 1000;
|
|
162
|
+
// expires_at이 없으면(레거시) → 유효하다고 간주
|
|
163
|
+
if (!data.expires_at)
|
|
164
|
+
return data.access_token;
|
|
165
|
+
// 10분 이상 남았으면 → 그대로 사용 (refresh 불필요)
|
|
166
|
+
if (data.expires_at > now + 600) {
|
|
167
|
+
return data.access_token;
|
|
168
|
+
}
|
|
169
|
+
// refresh_token 없으면 만료 전까지만 사용
|
|
170
|
+
if (!data.refresh_token) {
|
|
171
|
+
return data.expires_at > now ? data.access_token : undefined;
|
|
172
|
+
}
|
|
173
|
+
// Refresh 시도 — lock으로 프로세스 간 동시 refresh 방지
|
|
174
|
+
if (acquireLock()) {
|
|
175
|
+
try {
|
|
176
|
+
const refreshed = await doRefresh(data.refresh_token);
|
|
177
|
+
if (refreshed) {
|
|
178
|
+
saveTokenData(refreshed);
|
|
179
|
+
return refreshed.access_token;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
releaseLock();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
// 다른 프로세스가 refresh 중 — 잠시 후 파일에서 다시 읽기
|
|
188
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
189
|
+
const retryData = loadTokenData();
|
|
190
|
+
if (retryData?.expires_at && retryData.expires_at > now + 30) {
|
|
191
|
+
return retryData.access_token;
|
|
192
|
+
}
|
|
122
193
|
}
|
|
194
|
+
// access_token이 아직 유효하면 사용
|
|
195
|
+
return data.expires_at > now ? data.access_token : undefined;
|
|
123
196
|
}
|
|
124
197
|
/**
|
|
125
|
-
*
|
|
126
|
-
*
|
|
198
|
+
* 레거시 키 정규화:
|
|
199
|
+
* - `@spaces/{slug}/{agent}` → `@{slug}/{agent}` (Space 레거시)
|
|
200
|
+
* - `space_slug` → `org_slug` (필드명 마이그레이션)
|
|
127
201
|
*/
|
|
128
202
|
function normalizeInstalledRegistry(raw) {
|
|
129
203
|
const normalized = {};
|
|
130
204
|
for (const [key, value] of Object.entries(raw)) {
|
|
205
|
+
// @spaces/ 레거시 키 정규화
|
|
131
206
|
const m = key.match(/^@spaces\/([a-z0-9][a-z0-9-]*)\/([a-z0-9][a-z0-9-]*)$/);
|
|
132
207
|
const normalizedKey = m ? `@${m[1]}/${m[2]}` : key;
|
|
133
|
-
|
|
208
|
+
// space_slug → org_slug 필드 마이그레이션
|
|
209
|
+
const entry = { ...value };
|
|
210
|
+
if ('space_slug' in entry) {
|
|
211
|
+
const spaceSlugs = entry;
|
|
212
|
+
entry.org_slug = spaceSlugs.space_slug;
|
|
213
|
+
delete spaceSlugs.space_slug;
|
|
214
|
+
}
|
|
215
|
+
normalized[normalizedKey] = entry;
|
|
134
216
|
}
|
|
135
217
|
return normalized;
|
|
136
218
|
}
|
package/dist/lib/guide.js
CHANGED
|
@@ -1,108 +1,63 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.generateGuide = generateGuide;
|
|
4
|
+
const index_js_1 = require("../prompts/index.js");
|
|
4
5
|
function buildSetupSection(needsLogin) {
|
|
5
|
-
const installStep = `## Step 1. Relay CLI 설치
|
|
6
|
-
|
|
7
|
-
relay CLI가 설치되어 있지 않다면:
|
|
8
|
-
|
|
9
|
-
\`\`\`bash
|
|
10
|
-
npm install -g relayax-cli
|
|
11
|
-
\`\`\`
|
|
12
|
-
|
|
13
|
-
Node.js가 없는 경우:
|
|
14
|
-
- macOS: \`brew install node\`
|
|
15
|
-
- Windows: \`winget install OpenJS.NodeJS.LTS\`
|
|
16
|
-
- Linux: \`curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash && source ~/.bashrc && nvm install --lts\`
|
|
17
|
-
|
|
18
|
-
npm 권한 오류 시:
|
|
19
|
-
\`\`\`bash
|
|
20
|
-
mkdir -p ~/.npm-global && npm config set prefix '~/.npm-global'
|
|
21
|
-
echo 'export PATH=~/.npm-global/bin:$PATH' >> ~/.zshrc && source ~/.zshrc
|
|
22
|
-
npm install -g relayax-cli
|
|
23
|
-
\`\`\``;
|
|
24
6
|
if (!needsLogin)
|
|
25
|
-
return
|
|
26
|
-
return `${
|
|
27
|
-
|
|
28
|
-
## Step 2. 로그인
|
|
29
|
-
|
|
30
|
-
\`\`\`bash
|
|
31
|
-
relay login
|
|
32
|
-
\`\`\`
|
|
33
|
-
|
|
34
|
-
브라우저가 열리면 GitHub 또는 카카오 계정으로 로그인하세요.`;
|
|
7
|
+
return index_js_1.SETUP_CLI;
|
|
8
|
+
return `${index_js_1.SETUP_CLI}\n\n${index_js_1.SETUP_LOGIN}`;
|
|
35
9
|
}
|
|
36
|
-
function
|
|
37
|
-
const
|
|
10
|
+
function buildRequiresSummary(requires) {
|
|
11
|
+
const lines = [];
|
|
38
12
|
if (requires.cli && requires.cli.length > 0) {
|
|
39
|
-
sections.push('### CLI 도구 설치\n');
|
|
40
13
|
for (const cli of requires.cli) {
|
|
41
|
-
const label = cli.required === false ? '
|
|
42
|
-
|
|
43
|
-
sections.push(`- **${cli.name}** ${label}: \`${cli.install}\``);
|
|
44
|
-
}
|
|
45
|
-
else {
|
|
46
|
-
sections.push(`- **${cli.name}** ${label}: 설치 후 \`which ${cli.name}\`으로 확인`);
|
|
47
|
-
}
|
|
14
|
+
const label = cli.required === false ? '선택' : '필수';
|
|
15
|
+
lines.push(`- cli: **${cli.name}** (${label})${cli.install ? ` — \`${cli.install}\`` : ''}`);
|
|
48
16
|
}
|
|
49
|
-
sections.push('');
|
|
50
17
|
}
|
|
51
18
|
if (requires.npm && requires.npm.length > 0) {
|
|
52
|
-
sections.push('### npm 패키지 설치\n');
|
|
53
|
-
sections.push('```bash');
|
|
54
19
|
const pkgNames = requires.npm.map((p) => typeof p === 'string' ? p : p.name);
|
|
55
|
-
|
|
56
|
-
sections.push('```\n');
|
|
20
|
+
lines.push(`- npm: ${pkgNames.map((n) => `**${n}**`).join(', ')}`);
|
|
57
21
|
}
|
|
58
22
|
if (requires.env && requires.env.length > 0) {
|
|
59
|
-
sections.push('### 환경변수 설정\n');
|
|
60
|
-
sections.push('```bash');
|
|
61
23
|
for (const env of requires.env) {
|
|
62
|
-
const label = env.required === false ? '
|
|
24
|
+
const label = env.required === false ? '선택' : '필수';
|
|
63
25
|
const desc = env.description ? ` — ${env.description}` : '';
|
|
64
|
-
|
|
26
|
+
lines.push(`- env: **${env.name}** (${label})${desc}`);
|
|
65
27
|
}
|
|
66
|
-
sections.push('```\n');
|
|
67
28
|
}
|
|
68
29
|
if (requires.mcp && requires.mcp.length > 0) {
|
|
69
|
-
sections.push('### MCP 서버 설정\n');
|
|
70
30
|
for (const mcp of requires.mcp) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
sections.push(`- 패키지: \`${mcp.package}\``);
|
|
74
|
-
if (mcp.config) {
|
|
75
|
-
sections.push(`- 실행: \`${mcp.config.command}${mcp.config.args ? ' ' + mcp.config.args.join(' ') : ''}\``);
|
|
76
|
-
}
|
|
77
|
-
if (mcp.env && mcp.env.length > 0) {
|
|
78
|
-
sections.push(`- 필요한 환경변수: ${mcp.env.map((e) => `\`${e}\``).join(', ')}`);
|
|
79
|
-
}
|
|
80
|
-
sections.push('');
|
|
31
|
+
const pkg = mcp.package ? ` — \`${mcp.package}\`` : '';
|
|
32
|
+
lines.push(`- mcp: **${mcp.name}**${pkg}`);
|
|
81
33
|
}
|
|
82
34
|
}
|
|
83
|
-
if (requires.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
for (const team of requires.teams) {
|
|
87
|
-
sections.push(`relay install ${team}`);
|
|
35
|
+
if (requires.agents && requires.agents.length > 0) {
|
|
36
|
+
for (const agent of requires.agents) {
|
|
37
|
+
lines.push(`- agents: **${agent}**`);
|
|
88
38
|
}
|
|
89
|
-
sections.push('```\n');
|
|
90
39
|
}
|
|
91
40
|
if (requires.permissions && requires.permissions.length > 0) {
|
|
92
|
-
|
|
93
|
-
sections.push('아래 도구 사용을 허용해야 합니다:\n');
|
|
94
|
-
for (const perm of requires.permissions) {
|
|
95
|
-
sections.push(`- \`${perm}\``);
|
|
96
|
-
}
|
|
97
|
-
sections.push('');
|
|
41
|
+
lines.push(`- permissions: ${requires.permissions.map((p) => `\`${p}\``).join(', ')}`);
|
|
98
42
|
}
|
|
99
|
-
|
|
43
|
+
return lines.join('\n');
|
|
44
|
+
}
|
|
45
|
+
function buildRequiresSection(requires) {
|
|
46
|
+
const summary = buildRequiresSummary(requires);
|
|
47
|
+
if (!summary)
|
|
100
48
|
return '';
|
|
101
|
-
return
|
|
49
|
+
return `## Step 4. 의존성 확인 및 설치
|
|
50
|
+
|
|
51
|
+
이 에이전트는 아래 의존성이 필요합니다:
|
|
52
|
+
|
|
53
|
+
${summary}
|
|
54
|
+
|
|
55
|
+
${index_js_1.REQUIREMENTS_CHECK}
|
|
56
|
+
`;
|
|
102
57
|
}
|
|
103
58
|
function generateGuide(config, commands, requires) {
|
|
104
59
|
const scopedSlug = config.slug.startsWith('@') ? config.slug : `@${config.slug}`;
|
|
105
|
-
const needsLogin = config.visibility === '
|
|
60
|
+
const needsLogin = config.visibility === 'internal' || config.visibility === 'private';
|
|
106
61
|
const setupSection = buildSetupSection(needsLogin);
|
|
107
62
|
const installStepNum = needsLogin ? 3 : 2;
|
|
108
63
|
const requiresSection = requires ? buildRequiresSection(requires) : '';
|
|
@@ -125,20 +80,20 @@ ${commands.map((cmd) => `- \`/${cmd.name}\`: ${cmd.description}`).join('\n')}
|
|
|
125
80
|
> ${config.description}
|
|
126
81
|
|
|
127
82
|
이 가이드는 [RelayAX](https://relayax.com)에서 자동 생성되었습니다.
|
|
128
|
-
아래 단계를 순서대로 실행하면
|
|
83
|
+
아래 단계를 순서대로 실행하면 에이전트가 설치됩니다.
|
|
129
84
|
|
|
130
85
|
${setupSection}
|
|
131
86
|
|
|
132
|
-
## Step ${installStepNum}.
|
|
87
|
+
## Step ${installStepNum}. 에이전트 설치
|
|
133
88
|
|
|
134
89
|
\`\`\`bash
|
|
135
90
|
relay install ${scopedSlug}
|
|
136
91
|
\`\`\`
|
|
137
92
|
|
|
138
|
-
설치 후
|
|
93
|
+
설치 후 아래 의존성을 확인하고 환경을 구성합니다.
|
|
139
94
|
|
|
140
95
|
${requiresSectionFinal}${usageHint}---
|
|
141
|
-
*출처: https://relayax.com — Agent
|
|
96
|
+
*출처: https://relayax.com — Agent Marketplace*
|
|
142
97
|
*설치: \`relay install ${scopedSlug}\`*
|
|
143
98
|
`;
|
|
144
99
|
}
|
package/dist/lib/installer.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export declare function
|
|
2
|
-
export declare function
|
|
1
|
+
export declare function installAgent(extractedDir: string, installPath: string): string[];
|
|
2
|
+
export declare function uninstallAgent(files: string[]): string[];
|
|
3
3
|
/**
|
|
4
4
|
* 빈 상위 디렉토리를 boundary까지 정리한다.
|
|
5
5
|
* 예: /home/.claude/skills/cardnews/ 가 비었으면 삭제, /home/.claude/skills/는 유지
|
package/dist/lib/installer.js
CHANGED
|
@@ -3,8 +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.
|
|
7
|
-
exports.
|
|
6
|
+
exports.installAgent = installAgent;
|
|
7
|
+
exports.uninstallAgent = uninstallAgent;
|
|
8
8
|
exports.cleanEmptyParents = cleanEmptyParents;
|
|
9
9
|
const fs_1 = __importDefault(require("fs"));
|
|
10
10
|
const path_1 = __importDefault(require("path"));
|
|
@@ -27,7 +27,7 @@ function copyDirRecursive(src, dest) {
|
|
|
27
27
|
}
|
|
28
28
|
return copiedFiles;
|
|
29
29
|
}
|
|
30
|
-
function
|
|
30
|
+
function installAgent(extractedDir, installPath) {
|
|
31
31
|
const installedFiles = [];
|
|
32
32
|
for (const dir of COPY_DIRS) {
|
|
33
33
|
const srcDir = path_1.default.join(extractedDir, dir);
|
|
@@ -36,7 +36,7 @@ function installTeam(extractedDir, installPath) {
|
|
|
36
36
|
}
|
|
37
37
|
return installedFiles;
|
|
38
38
|
}
|
|
39
|
-
function
|
|
39
|
+
function uninstallAgent(files) {
|
|
40
40
|
const removed = [];
|
|
41
41
|
for (const file of files) {
|
|
42
42
|
try {
|
package/dist/lib/preamble.d.ts
CHANGED
|
@@ -9,14 +9,14 @@ export declare function generatePreambleScript(slug: string, apiUrl: string): st
|
|
|
9
9
|
*/
|
|
10
10
|
export declare function generatePreamble(slug: string): string;
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
12
|
+
* agentDir에 bin/relay-preamble.sh를 생성한다.
|
|
13
13
|
*/
|
|
14
|
-
export declare function generatePreambleBin(
|
|
14
|
+
export declare function generatePreambleBin(agentDir: string, slug: string, apiUrl: string): void;
|
|
15
15
|
/**
|
|
16
16
|
* frontmatter(---...---) 뒤에 preamble을 삽입한다.
|
|
17
17
|
*/
|
|
18
18
|
export declare function injectPreamble(filePath: string, slug: string): void;
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
20
|
+
* 에이전트의 사용자 진입점 파일에 preamble을 주입한다.
|
|
21
21
|
*/
|
|
22
|
-
export declare function
|
|
22
|
+
export declare function injectPreambleToAgent(agentDir: string, slug: string): number;
|
package/dist/lib/preamble.js
CHANGED
|
@@ -7,7 +7,7 @@ exports.generatePreambleScript = generatePreambleScript;
|
|
|
7
7
|
exports.generatePreamble = generatePreamble;
|
|
8
8
|
exports.generatePreambleBin = generatePreambleBin;
|
|
9
9
|
exports.injectPreamble = injectPreamble;
|
|
10
|
-
exports.
|
|
10
|
+
exports.injectPreambleToAgent = injectPreambleToAgent;
|
|
11
11
|
const fs_1 = __importDefault(require("fs"));
|
|
12
12
|
const path_1 = __importDefault(require("path"));
|
|
13
13
|
const PREAMBLE_START = '<!-- RELAY_PREAMBLE_START - DO NOT EDIT -->';
|
|
@@ -17,11 +17,10 @@ const PREAMBLE_END = '<!-- RELAY_PREAMBLE_END -->';
|
|
|
17
17
|
* relay CLI가 있으면 사용, 없으면 curl fallback.
|
|
18
18
|
*/
|
|
19
19
|
function generatePreambleScript(slug, apiUrl) {
|
|
20
|
-
// slug format: @
|
|
20
|
+
// slug format: @owner/agent → extract agent slug for ping
|
|
21
21
|
const stripped = slug.startsWith('@') ? slug.slice(1) : slug;
|
|
22
22
|
const slashIdx = stripped.indexOf('/');
|
|
23
|
-
const
|
|
24
|
-
const teamPart = slashIdx !== -1 ? stripped.slice(slashIdx + 1) : stripped;
|
|
23
|
+
const agentSlug = slashIdx !== -1 ? stripped.slice(slashIdx + 1) : stripped;
|
|
25
24
|
return `#!/usr/bin/env bash
|
|
26
25
|
# relay-preamble.sh — auto-generated by relay publish
|
|
27
26
|
set +e
|
|
@@ -32,7 +31,7 @@ DEVICE_HASH=$(echo "$HOSTNAME:$USER" | shasum -a 256 | cut -d' ' -f1)
|
|
|
32
31
|
if command -v relay &>/dev/null; then
|
|
33
32
|
relay ping "${slug}" --quiet 2>/dev/null &
|
|
34
33
|
else
|
|
35
|
-
curl -sf -X POST "${apiUrl}/api/
|
|
34
|
+
curl -sf -X POST "${apiUrl}/api/agents/${agentSlug}/ping" \\
|
|
36
35
|
-H "Content-Type: application/json" \\
|
|
37
36
|
-d "{\\"device_hash\\":\\"$DEVICE_HASH\\"}" \\
|
|
38
37
|
2>/dev/null &
|
|
@@ -52,23 +51,23 @@ echo "RELAY_READY: ${slug}"
|
|
|
52
51
|
* 단순히 relay ping을 호출한다.
|
|
53
52
|
*/
|
|
54
53
|
function generatePreamble(slug) {
|
|
55
|
-
const
|
|
54
|
+
const agentPath = slug.startsWith('@') ? slug.slice(1) : slug;
|
|
56
55
|
return `${PREAMBLE_START}
|
|
57
56
|
## Preamble (run first)
|
|
58
57
|
|
|
59
58
|
\`\`\`bash
|
|
60
|
-
bash "$HOME/.relay/
|
|
59
|
+
bash "$HOME/.relay/agents/${agentPath}/bin/relay-preamble.sh" 2>/dev/null || true
|
|
61
60
|
\`\`\`
|
|
62
61
|
|
|
63
62
|
If output shows \`CLI_UPGRADE_AVAILABLE <old> <new>\`: tell user "relay v{new} available. Run: npm update -g relayax-cli"
|
|
64
|
-
If output shows \`
|
|
63
|
+
If output shows \`AGENT_UPGRADE_AVAILABLE\`: tell user "${slug} v{new} available (by @{author}). Run: relay update ${slug}"
|
|
65
64
|
${PREAMBLE_END}`;
|
|
66
65
|
}
|
|
67
66
|
/**
|
|
68
|
-
*
|
|
67
|
+
* agentDir에 bin/relay-preamble.sh를 생성한다.
|
|
69
68
|
*/
|
|
70
|
-
function generatePreambleBin(
|
|
71
|
-
const binDir = path_1.default.join(
|
|
69
|
+
function generatePreambleBin(agentDir, slug, apiUrl) {
|
|
70
|
+
const binDir = path_1.default.join(agentDir, 'bin');
|
|
72
71
|
if (!fs_1.default.existsSync(binDir)) {
|
|
73
72
|
fs_1.default.mkdirSync(binDir, { recursive: true });
|
|
74
73
|
}
|
|
@@ -107,12 +106,12 @@ function injectPreamble(filePath, slug) {
|
|
|
107
106
|
fs_1.default.writeFileSync(filePath, cleaned);
|
|
108
107
|
}
|
|
109
108
|
/**
|
|
110
|
-
*
|
|
109
|
+
* 에이전트의 사용자 진입점 파일에 preamble을 주입한다.
|
|
111
110
|
*/
|
|
112
|
-
function
|
|
111
|
+
function injectPreambleToAgent(agentDir, slug) {
|
|
113
112
|
let count = 0;
|
|
114
113
|
// 1. user-invocable 서브 스킬 SKILL.md
|
|
115
|
-
const skillsDir = path_1.default.join(
|
|
114
|
+
const skillsDir = path_1.default.join(agentDir, 'skills');
|
|
116
115
|
if (fs_1.default.existsSync(skillsDir)) {
|
|
117
116
|
function walkSkills(dir) {
|
|
118
117
|
for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
@@ -132,7 +131,7 @@ function injectPreambleToTeam(teamDir, slug) {
|
|
|
132
131
|
walkSkills(skillsDir);
|
|
133
132
|
}
|
|
134
133
|
// 2. commands/*.md
|
|
135
|
-
const commandsDir = path_1.default.join(
|
|
134
|
+
const commandsDir = path_1.default.join(agentDir, 'commands');
|
|
136
135
|
if (fs_1.default.existsSync(commandsDir)) {
|
|
137
136
|
for (const entry of fs_1.default.readdirSync(commandsDir, { withFileTypes: true })) {
|
|
138
137
|
if (!entry.isFile() || !entry.name.endsWith('.md'))
|
package/dist/lib/slug.d.ts
CHANGED
|
@@ -3,6 +3,11 @@ export interface ParsedSlug {
|
|
|
3
3
|
name: string;
|
|
4
4
|
full: string;
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* 임의의 문자열을 slug로 변환한다.
|
|
8
|
+
* 한글은 로마자로 변환된다 (예: "콘텐츠 에이전트" → "kontencheu-eijenteu").
|
|
9
|
+
*/
|
|
10
|
+
export declare function slugify(input: string): string;
|
|
6
11
|
/**
|
|
7
12
|
* Scoped slug(`@owner/name`)를 동기적으로 파싱한다.
|
|
8
13
|
* 단순 slug는 파싱할 수 없으므로 null을 반환한다.
|
|
@@ -15,6 +20,5 @@ export declare function isSimpleSlug(input: string): boolean;
|
|
|
15
20
|
/**
|
|
16
21
|
* Scoped 또는 단순 slug를 받아 ParsedSlug를 반환한다.
|
|
17
22
|
* 단순 slug는 서버에 resolve를 요청한다.
|
|
18
|
-
* `@spaces/{spaceSlug}/{teamSlug}` 형식은 `@{spaceSlug}/{teamSlug}`로 정규화된다.
|
|
19
23
|
*/
|
|
20
24
|
export declare function resolveSlug(input: string): Promise<ParsedSlug>;
|
package/dist/lib/slug.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.slugify = slugify;
|
|
3
4
|
exports.parseSlug = parseSlug;
|
|
4
5
|
exports.isScopedSlug = isScopedSlug;
|
|
5
6
|
exports.isSimpleSlug = isSimpleSlug;
|
|
@@ -7,6 +8,52 @@ exports.resolveSlug = resolveSlug;
|
|
|
7
8
|
const api_js_1 = require("./api.js");
|
|
8
9
|
const SCOPED_SLUG_RE = /^@([a-z0-9](?:[a-z0-9-]*[a-z0-9])?)\/([a-z0-9](?:[a-z0-9-]*[a-z0-9])?)$/;
|
|
9
10
|
const SIMPLE_SLUG_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
11
|
+
// ── 한글 로마자 변환 (Revised Romanization) ──
|
|
12
|
+
const INITIALS = [
|
|
13
|
+
'g', 'kk', 'n', 'd', 'tt', 'r', 'm', 'b', 'pp',
|
|
14
|
+
's', 'ss', '', 'j', 'jj', 'ch', 'k', 't', 'p', 'h',
|
|
15
|
+
];
|
|
16
|
+
const MEDIALS = [
|
|
17
|
+
'a', 'ae', 'ya', 'yae', 'eo', 'e', 'yeo', 'ye', 'o',
|
|
18
|
+
'wa', 'wae', 'oe', 'yo', 'u', 'wo', 'we', 'wi', 'yu',
|
|
19
|
+
'eu', 'ui', 'i',
|
|
20
|
+
];
|
|
21
|
+
const FINALS = [
|
|
22
|
+
'', 'k', 'k', 'k', 'n', 'n', 'n', 't', 'l',
|
|
23
|
+
'l', 'l', 'l', 'l', 'l', 'l', 'l', 'm', 'p',
|
|
24
|
+
'p', 't', 't', 'ng', 't', 't', 'k', 't', 'p', 't',
|
|
25
|
+
];
|
|
26
|
+
const HANGUL_BASE = 0xAC00;
|
|
27
|
+
function romanize(input) {
|
|
28
|
+
let result = '';
|
|
29
|
+
for (const ch of input) {
|
|
30
|
+
const code = ch.codePointAt(0);
|
|
31
|
+
if (code >= HANGUL_BASE && code < HANGUL_BASE + 11172) {
|
|
32
|
+
const offset = code - HANGUL_BASE;
|
|
33
|
+
const initial = Math.floor(offset / (21 * 28));
|
|
34
|
+
const medial = Math.floor((offset % (21 * 28)) / 28);
|
|
35
|
+
const final = offset % 28;
|
|
36
|
+
result += INITIALS[initial] + MEDIALS[medial] + FINALS[final];
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
result += ch;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 임의의 문자열을 slug로 변환한다.
|
|
46
|
+
* 한글은 로마자로 변환된다 (예: "콘텐츠 에이전트" → "kontencheu-eijenteu").
|
|
47
|
+
*/
|
|
48
|
+
function slugify(input) {
|
|
49
|
+
return romanize(input)
|
|
50
|
+
.toLowerCase()
|
|
51
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
52
|
+
.trim()
|
|
53
|
+
.replace(/\s+/g, '-')
|
|
54
|
+
.replace(/-+/g, '-')
|
|
55
|
+
.slice(0, 50);
|
|
56
|
+
}
|
|
10
57
|
/**
|
|
11
58
|
* Scoped slug(`@owner/name`)를 동기적으로 파싱한다.
|
|
12
59
|
* 단순 slug는 파싱할 수 없으므로 null을 반환한다.
|
|
@@ -28,24 +75,20 @@ function isSimpleSlug(input) {
|
|
|
28
75
|
/**
|
|
29
76
|
* Scoped 또는 단순 slug를 받아 ParsedSlug를 반환한다.
|
|
30
77
|
* 단순 slug는 서버에 resolve를 요청한다.
|
|
31
|
-
* `@spaces/{spaceSlug}/{teamSlug}` 형식은 `@{spaceSlug}/{teamSlug}`로 정규화된다.
|
|
32
78
|
*/
|
|
33
79
|
async function resolveSlug(input) {
|
|
34
|
-
// @spaces/{spaceSlug}/{teamSlug} → @{spaceSlug}/{teamSlug} 정규화
|
|
35
|
-
const spacesPrefix = input.match(/^@spaces\/([a-z0-9][a-z0-9-]*)\/([a-z0-9][a-z0-9-]*)$/);
|
|
36
|
-
const normalized = spacesPrefix ? `@${spacesPrefix[1]}/${spacesPrefix[2]}` : input;
|
|
37
80
|
// scoped slug면 바로 파싱
|
|
38
|
-
const parsed = parseSlug(
|
|
81
|
+
const parsed = parseSlug(input);
|
|
39
82
|
if (parsed)
|
|
40
83
|
return parsed;
|
|
41
84
|
// 단순 slug인지 검증
|
|
42
|
-
if (!isSimpleSlug(
|
|
85
|
+
if (!isSimpleSlug(input)) {
|
|
43
86
|
throw new Error(`잘못된 slug 형식입니다: '${input}'. @owner/name 또는 name 형태로 입력하세요.`);
|
|
44
87
|
}
|
|
45
88
|
// 서버에 resolve 요청
|
|
46
|
-
const results = await (0, api_js_1.resolveSlugFromServer)(
|
|
89
|
+
const results = await (0, api_js_1.resolveSlugFromServer)(input);
|
|
47
90
|
if (results.length === 0) {
|
|
48
|
-
throw new Error(`'${input}'
|
|
91
|
+
throw new Error(`'${input}' 에이전트를 찾을 수 없습니다.`);
|
|
49
92
|
}
|
|
50
93
|
if (results.length === 1) {
|
|
51
94
|
const r = results[0];
|
|
@@ -53,5 +96,5 @@ async function resolveSlug(input) {
|
|
|
53
96
|
}
|
|
54
97
|
// 여러 개 매칭
|
|
55
98
|
const list = results.map((r) => ` ${r.full}`).join('\n');
|
|
56
|
-
throw new Error(`'${input}'에 해당하는
|
|
99
|
+
throw new Error(`'${input}'에 해당하는 에이전트가 여러 개입니다. 전체 slug를 지정해주세요:\n${list}`);
|
|
57
100
|
}
|
package/dist/lib/update-cache.js
CHANGED
|
@@ -31,7 +31,7 @@ function isCacheValid(key, force) {
|
|
|
31
31
|
if (force)
|
|
32
32
|
return false;
|
|
33
33
|
const cache = loadCache();
|
|
34
|
-
const timestamp = key === 'cli' ? cache.cli : cache.
|
|
34
|
+
const timestamp = key === 'cli' ? cache.cli : cache.agents?.[key];
|
|
35
35
|
if (!timestamp)
|
|
36
36
|
return false;
|
|
37
37
|
return Date.now() - new Date(timestamp).getTime() < CACHE_TTL_MS;
|
|
@@ -43,9 +43,9 @@ function updateCacheTimestamp(key) {
|
|
|
43
43
|
cache.cli = now;
|
|
44
44
|
}
|
|
45
45
|
else {
|
|
46
|
-
if (!cache.
|
|
47
|
-
cache.
|
|
48
|
-
cache.
|
|
46
|
+
if (!cache.agents)
|
|
47
|
+
cache.agents = {};
|
|
48
|
+
cache.agents[key] = now;
|
|
49
49
|
}
|
|
50
50
|
saveCache(cache);
|
|
51
51
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export interface UpdateResult {
|
|
2
|
-
type: 'cli' | '
|
|
2
|
+
type: 'cli' | 'agent';
|
|
3
3
|
slug?: string;
|
|
4
4
|
current: string;
|
|
5
5
|
latest: string;
|
|
6
6
|
author?: string;
|
|
7
7
|
}
|
|
8
8
|
export declare function checkCliVersion(force?: boolean): Promise<UpdateResult | null>;
|
|
9
|
-
export declare function
|
|
10
|
-
export declare function
|
|
9
|
+
export declare function checkAgentVersion(slug: string, force?: boolean): Promise<UpdateResult | null>;
|
|
10
|
+
export declare function checkAllAgents(force?: boolean): Promise<UpdateResult[]>;
|