relayax-cli 0.3.58 → 0.3.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,10 +1,39 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
type DiffStatus = 'added' | 'modified' | 'deleted' | 'unchanged';
|
|
3
|
+
interface DiffEntry {
|
|
4
|
+
relPath: string;
|
|
5
|
+
status: DiffStatus;
|
|
6
|
+
}
|
|
2
7
|
import type { ContentType } from '../lib/ai-tools.js';
|
|
3
8
|
export interface ContentEntry {
|
|
4
9
|
name: string;
|
|
5
10
|
type: ContentType;
|
|
6
11
|
from: string;
|
|
7
12
|
}
|
|
13
|
+
type ContentDiffStatus = 'modified' | 'unchanged' | 'source_missing';
|
|
14
|
+
interface ContentDiffEntry {
|
|
15
|
+
name: string;
|
|
16
|
+
type: ContentType;
|
|
17
|
+
status: ContentDiffStatus;
|
|
18
|
+
files?: DiffEntry[];
|
|
19
|
+
}
|
|
20
|
+
interface NewItemEntry {
|
|
21
|
+
name: string;
|
|
22
|
+
type: ContentType;
|
|
23
|
+
source: string;
|
|
24
|
+
relativePath: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* contents 매니페스트 기반으로 각 항목의 원본과 .relay/ 복사본을 비교한다.
|
|
28
|
+
*/
|
|
29
|
+
export declare function computeContentsDiff(contents: ContentEntry[], relayDir: string, projectPath: string): {
|
|
30
|
+
diff: ContentDiffEntry[];
|
|
31
|
+
newItems: NewItemEntry[];
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* contents 항목 단위로 from → .relay/ 동기화한다.
|
|
35
|
+
*/
|
|
36
|
+
export declare function syncContentsToRelay(contents: ContentEntry[], contentsDiff: ContentDiffEntry[], relayDir: string, projectPath: string): void;
|
|
8
37
|
/**
|
|
9
38
|
* 패키지 홈 디렉토리를 결정한다.
|
|
10
39
|
* 1. 프로젝트에 .relay/가 있으면 → projectPath/.relay/
|
|
@@ -18,3 +47,4 @@ export declare function resolveRelayDir(projectPath: string, slug?: string): str
|
|
|
18
47
|
*/
|
|
19
48
|
export declare function initGlobalAgentHome(slug: string, yamlData: Record<string, unknown>): string;
|
|
20
49
|
export declare function registerPackage(program: Command): void;
|
|
50
|
+
export {};
|
package/dist/commands/package.js
CHANGED
|
@@ -3,6 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.computeContentsDiff = computeContentsDiff;
|
|
7
|
+
exports.syncContentsToRelay = syncContentsToRelay;
|
|
6
8
|
exports.resolveRelayDir = resolveRelayDir;
|
|
7
9
|
exports.initGlobalAgentHome = initGlobalAgentHome;
|
|
8
10
|
exports.registerPackage = registerPackage;
|
package/dist/mcp/server.js
CHANGED
|
@@ -291,6 +291,108 @@ function createMcpServer() {
|
|
|
291
291
|
}
|
|
292
292
|
return { content: [jsonText({ sources })] };
|
|
293
293
|
});
|
|
294
|
+
server.tool('relay_package', '소스 디렉토리에서 .relay/로 콘텐츠를 패키징합니다. mode: init(최초 소스 탐색), sync(변경 반영), migrate(source→contents 마이그레이션)', {
|
|
295
|
+
mode: zod_1.z.enum(['init', 'sync', 'migrate']).describe('패키징 모드'),
|
|
296
|
+
project_path: zod_1.z.string().optional().describe('프로젝트 경로'),
|
|
297
|
+
}, async ({ mode, project_path }) => {
|
|
298
|
+
try {
|
|
299
|
+
const projectPath = resolveMcpProjectPath(project_path);
|
|
300
|
+
const homeDir = (0, paths_js_1.resolveHome)();
|
|
301
|
+
const relayDir = path_1.default.join(projectPath, '.relay');
|
|
302
|
+
const relayYamlPath = path_1.default.join(relayDir, 'relay.yaml');
|
|
303
|
+
if (mode === 'init') {
|
|
304
|
+
// 최초 패키징: 소스 탐색
|
|
305
|
+
const { detectGlobalCLIs } = await import('../lib/ai-tools.js');
|
|
306
|
+
const localTools = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
|
|
307
|
+
const globalTools = detectGlobalCLIs(homeDir);
|
|
308
|
+
const sources = [];
|
|
309
|
+
for (const tool of localTools) {
|
|
310
|
+
const items = (0, ai_tools_js_1.scanLocalItems)(projectPath, tool);
|
|
311
|
+
if (items.length > 0)
|
|
312
|
+
sources.push({ path: tool.skillsDir, location: 'local', name: tool.name, items: items.map((i) => ({ name: i.name, type: i.type, relativePath: i.relativePath })) });
|
|
313
|
+
}
|
|
314
|
+
for (const tool of globalTools) {
|
|
315
|
+
const items = (0, ai_tools_js_1.scanGlobalItems)(tool, homeDir);
|
|
316
|
+
if (items.length > 0)
|
|
317
|
+
sources.push({ path: `~/${tool.skillsDir}`, location: 'global', name: `${tool.name} (global)`, items: items.map((i) => ({ name: i.name, type: i.type, relativePath: i.relativePath })) });
|
|
318
|
+
}
|
|
319
|
+
for (const { tool, basePath } of (0, ai_tools_js_1.detectMountedCLIs)()) {
|
|
320
|
+
const items = (0, ai_tools_js_1.scanMountedItems)(basePath, tool);
|
|
321
|
+
if (items.length > 0)
|
|
322
|
+
sources.push({ path: `${basePath}/${tool.skillsDir}`, location: 'mounted', name: `${tool.name} (mounted)`, items: items.map((i) => ({ name: i.name, type: i.type, relativePath: i.relativePath })) });
|
|
323
|
+
}
|
|
324
|
+
// 기존 글로벌 에이전트 패키지 스캔
|
|
325
|
+
const globalAgentsDir = path_1.default.join(homeDir, '.relay', 'agents');
|
|
326
|
+
const existingAgents = [];
|
|
327
|
+
if (fs_1.default.existsSync(globalAgentsDir)) {
|
|
328
|
+
for (const entry of fs_1.default.readdirSync(globalAgentsDir, { withFileTypes: true })) {
|
|
329
|
+
if (!entry.isDirectory() || entry.name.startsWith('.'))
|
|
330
|
+
continue;
|
|
331
|
+
const agentYaml = path_1.default.join(globalAgentsDir, entry.name, 'relay.yaml');
|
|
332
|
+
if (fs_1.default.existsSync(agentYaml)) {
|
|
333
|
+
try {
|
|
334
|
+
const cfg = js_yaml_1.default.load(fs_1.default.readFileSync(agentYaml, 'utf-8'));
|
|
335
|
+
existingAgents.push({ slug: cfg.slug ?? entry.name, name: cfg.name ?? entry.name, version: cfg.version ?? '0.0.0', path: `~/.relay/agents/${entry.name}` });
|
|
336
|
+
}
|
|
337
|
+
catch { /* skip */ }
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return { content: [jsonText({ status: 'init_required', sources, existing_agents: existingAgents })] };
|
|
342
|
+
}
|
|
343
|
+
// sync / migrate는 relay.yaml이 필요
|
|
344
|
+
if (!fs_1.default.existsSync(relayYamlPath)) {
|
|
345
|
+
return { content: [jsonText({ error: 'NOT_INITIALIZED', message: '.relay/relay.yaml이 없습니다. mode: init으로 먼저 실행하세요.' })], isError: true };
|
|
346
|
+
}
|
|
347
|
+
if (mode === 'migrate') {
|
|
348
|
+
const yamlMigrate = fs_1.default.readFileSync(relayYamlPath, 'utf-8');
|
|
349
|
+
const cfgMigrate = js_yaml_1.default.load(yamlMigrate);
|
|
350
|
+
if (cfgMigrate.contents) {
|
|
351
|
+
return { content: [jsonText({ status: 'already_migrated', message: '이미 contents 형식입니다.' })] };
|
|
352
|
+
}
|
|
353
|
+
const legacySource = cfgMigrate.source;
|
|
354
|
+
if (!legacySource) {
|
|
355
|
+
return { content: [jsonText({ status: 'no_source', message: 'source 필드가 없습니다.' })], isError: true };
|
|
356
|
+
}
|
|
357
|
+
const localTools = (0, ai_tools_js_1.detectAgentCLIs)(projectPath);
|
|
358
|
+
const tool = localTools.find((t) => t.skillsDir === legacySource);
|
|
359
|
+
const migratedContents = [];
|
|
360
|
+
if (tool) {
|
|
361
|
+
const items = (0, ai_tools_js_1.scanLocalItems)(projectPath, tool);
|
|
362
|
+
for (const item of items) {
|
|
363
|
+
migratedContents.push({ name: item.name, type: item.type, from: `${legacySource}/${item.relativePath}` });
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
delete cfgMigrate.source;
|
|
367
|
+
cfgMigrate.contents = migratedContents;
|
|
368
|
+
fs_1.default.writeFileSync(relayYamlPath, js_yaml_1.default.dump(cfgMigrate, { lineWidth: 120 }), 'utf-8');
|
|
369
|
+
return { content: [jsonText({ status: 'migrated', contents: migratedContents })] };
|
|
370
|
+
}
|
|
371
|
+
// mode === 'sync'
|
|
372
|
+
const { computeContentsDiff, syncContentsToRelay } = await import('../commands/package.js');
|
|
373
|
+
const yamlContent = fs_1.default.readFileSync(relayYamlPath, 'utf-8');
|
|
374
|
+
const config = js_yaml_1.default.load(yamlContent);
|
|
375
|
+
const contents = config.contents ?? [];
|
|
376
|
+
if (contents.length === 0) {
|
|
377
|
+
return { content: [jsonText({ status: 'no_contents', message: 'relay.yaml에 contents가 없습니다.' })], isError: true };
|
|
378
|
+
}
|
|
379
|
+
const { diff: contentsDiff, newItems } = computeContentsDiff(contents, relayDir, projectPath);
|
|
380
|
+
const hasChanges = contentsDiff.some((d) => d.status === 'modified');
|
|
381
|
+
if (hasChanges) {
|
|
382
|
+
syncContentsToRelay(contents, contentsDiff, relayDir, projectPath);
|
|
383
|
+
}
|
|
384
|
+
const summary = {
|
|
385
|
+
modified: contentsDiff.filter((d) => d.status === 'modified').length,
|
|
386
|
+
unchanged: contentsDiff.filter((d) => d.status === 'unchanged').length,
|
|
387
|
+
source_missing: contentsDiff.filter((d) => d.status === 'source_missing').length,
|
|
388
|
+
new_available: newItems.length,
|
|
389
|
+
};
|
|
390
|
+
return { content: [jsonText({ diff: contentsDiff, new_items: newItems, synced: hasChanges, summary })] };
|
|
391
|
+
}
|
|
392
|
+
catch (err) {
|
|
393
|
+
return { content: [jsonText({ error: String(err) })], isError: true };
|
|
394
|
+
}
|
|
395
|
+
});
|
|
294
396
|
server.tool('relay_org_list', '소속 Organization 목록을 조회합니다', {}, async () => {
|
|
295
397
|
try {
|
|
296
398
|
const token = await (0, config_js_1.getValidToken)();
|
|
@@ -59,6 +59,9 @@ cat ~/Library/Application\ Support/Claude/claude_desktop_config.json 2>/dev/null
|
|
|
59
59
|
| "relay install X" | `relay install X` | `relay_install` tool 호출 (`slug: "X"`) |
|
|
60
60
|
| "relay publish" | `relay publish` | `relay_publish` tool 호출 |
|
|
61
61
|
| "relay login" | `relay login` | `relay_login` tool 호출 |
|
|
62
|
+
| "relay package --init" | `relay package --init --json` | `relay_package` tool 호출 (`mode: "init"`) |
|
|
63
|
+
| "relay package --sync" | `relay package --sync --json` | `relay_package` tool 호출 (`mode: "sync"`) |
|
|
64
|
+
| "relay package --migrate" | `relay package --migrate --json` | `relay_package` tool 호출 (`mode: "migrate"`) |
|
|
62
65
|
| "relay scan" | `relay package --init` | `relay_scan` tool 호출 |
|
|
63
66
|
| "relay check-update X" | `relay check-update X` | `relay_check_update` tool 호출 (`slug: "X"`) |
|
|
64
67
|
| "relay orgs list" | `relay orgs list --json` | `relay_org_list` tool 호출 |
|
package/dist/prompts/publish.md
CHANGED
|
@@ -28,6 +28,13 @@
|
|
|
28
28
|
|
|
29
29
|
#### A. 최초 배포 (.relay/relay.yaml이 없음)
|
|
30
30
|
|
|
31
|
+
**환경 B (MCP)의 경우:**
|
|
32
|
+
`relay_package` MCP tool (`mode: "init"`)을 사용합니다. CLI의 `relay package --init --json`과 동일한 결과를 반환합니다.
|
|
33
|
+
- 결과의 `sources[]`는 배포할 콘텐츠 **후보** 목록입니다. 전부 배포 대상이 아닙니다.
|
|
34
|
+
- 사용자에게 어떤 콘텐츠를 패키지에 포함할지 반드시 물어보세요.
|
|
35
|
+
|
|
36
|
+
**환경 A (터미널)의 경우:**
|
|
37
|
+
|
|
31
38
|
##### 1단계: 소스 탐색
|
|
32
39
|
|
|
33
40
|
`relay package --init --json` 실행
|
|
@@ -172,6 +179,14 @@ contents:
|
|
|
172
179
|
|
|
173
180
|
#### B. 재배포 (.relay/relay.yaml이 있음)
|
|
174
181
|
|
|
182
|
+
**환경 B (MCP)의 경우:**
|
|
183
|
+
`relay_package` MCP tool을 사용합니다:
|
|
184
|
+
- `mode: "migrate"` — B-0 마이그레이션 (source → contents)
|
|
185
|
+
- `mode: "sync"` — B-1~B-2 동기화 + 변경 반영
|
|
186
|
+
- sync 결과의 `new_items`가 있으면 B-3 새 콘텐츠 추가도 진행합니다.
|
|
187
|
+
|
|
188
|
+
**환경 A (터미널)의 경우:**
|
|
189
|
+
|
|
175
190
|
##### B-0. 기존 source 필드 마이그레이션
|
|
176
191
|
|
|
177
192
|
relay.yaml에 기존 `source` 필드만 있고 `contents`가 없으면:
|
|
@@ -300,18 +315,19 @@ relay.yaml의 `visibility` 설정을 확인합니다.
|
|
|
300
315
|
|
|
301
316
|
Org가 선택된 경우:
|
|
302
317
|
- **사용자 질문 도구 호출:**
|
|
303
|
-
- question: "공개 범위를 선택하세요"
|
|
304
|
-
- options: `["
|
|
318
|
+
- question: "{org_name} Organization에 배포합니다. 공개 범위를 선택하세요"
|
|
319
|
+
- options: `["public — 외부인 포함 누구나 설치", "internal — 조직 구성원 누구나 설치", "private — 조직 내에서도 허가받은 사람만 설치"]`
|
|
305
320
|
|
|
306
|
-
Org가 없는
|
|
321
|
+
Org가 없는 경우 (개인 배포):
|
|
307
322
|
- **사용자 질문 도구 호출:**
|
|
308
323
|
- question: "공개 범위를 선택하세요"
|
|
309
|
-
- options: `["
|
|
324
|
+
- options: `["public — 누구나 검색하여 설치 가능", "private — 접근 링크를 받은 사람만 설치 가능"]`
|
|
310
325
|
|
|
311
326
|
**응답 처리:**
|
|
312
|
-
|
|
313
|
-
- "
|
|
314
|
-
- "
|
|
327
|
+
|
|
328
|
+
- "public" → relay.yaml에 `visibility: public` 저장
|
|
329
|
+
- "internal" → relay.yaml에 `visibility: internal` 저장
|
|
330
|
+
- "private" → relay.yaml에 `visibility: private` 저장. 배포 후 웹 대시보드에서 접근 코드를 생성하여 공유할 수 있다고 안내.
|
|
315
331
|
|
|
316
332
|
**재배포 (visibility 이미 설정됨):**
|
|
317
333
|
|