tdecollab 0.1.0

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 ADDED
@@ -0,0 +1,158 @@
1
+ # tdecollab
2
+
3
+ TDE 포털의 Confluence, JIRA, GitLab과 상호작용하는 CLI 및 MCP(Model Context Protocol) 통합 도구.
4
+
5
+ ## 개요
6
+
7
+ tdecollab은 TDE 포털에서 제공하는 협업 도구들을 하나의 CLI와 MCP 서버로 통합하여, 터미널이나 AI 에이전트(Claude 등)를 통해 편리하게 사용할 수 있도록 하는 경량화 서비스입니다.
8
+
9
+ ### 지원 서비스
10
+
11
+ | 서비스 | 주요 기능 |
12
+ |--------|----------|
13
+ | **Confluence** | 페이지 CRUD, Markdown↔Storage 변환, 검색, 라벨 관리, 페이지 트리 조회 |
14
+ | **JIRA** | 이슈 CRUD, JQL 검색, 상태 변경(트랜지션), 코멘트 관리, 프로젝트/보드 조회 |
15
+ | **GitLab** | 프로젝트 조회, MR 관리, 파이프라인 조회, 브랜치 관리, 파일 조회 |
16
+
17
+ ## 요구사항
18
+
19
+ - Node.js 20 이상
20
+ - pnpm
21
+
22
+ ## 설치
23
+
24
+ ```bash
25
+ pnpm install
26
+ pnpm build
27
+ ```
28
+
29
+ ## 설정
30
+
31
+ `.env.example`을 `.env`로 복사한 후 각 서비스의 인증 정보를 입력합니다.
32
+
33
+ ```bash
34
+ cp .env.example .env
35
+ ```
36
+
37
+ ### 환경변수
38
+
39
+ ```env
40
+ # Confluence
41
+ CONFLUENCE_BASE_URL=https://confluence.tde.sktelecom.com
42
+ CONFLUENCE_EMAIL=사번@sktelecom.com
43
+ CONFLUENCE_API_TOKEN=your-api-token
44
+
45
+ # JIRA
46
+ JIRA_BASE_URL=https://jira.tde.sktelecom.com
47
+ JIRA_EMAIL=사번@sktelecom.com
48
+ JIRA_API_TOKEN=your-api-token
49
+
50
+ # GitLab
51
+ GITLAB_BASE_URL=https://gitlab.tde.sktelecom.com
52
+ GITLAB_PRIVATE_TOKEN=your-private-token
53
+ ```
54
+
55
+ ## 사용법
56
+
57
+ ### CLI
58
+
59
+ ```bash
60
+ # Confluence
61
+ tdecollab confluence page get <pageId>
62
+ tdecollab confluence page get <pageId> --raw --quiet > page.html # HTML 원본 파일 저장
63
+ tdecollab confluence page create --space <key> --title <title> --file <path> # 파일 내용으로 생성
64
+ tdecollab confluence page create --space <key> --title <title> --content "Markdown 내용"
65
+ tdecollab confluence search <query>
66
+
67
+ # JIRA
68
+ tdecollab jira issue get <issueKey>
69
+ tdecollab jira search <jql>
70
+ tdecollab jira issue transition <issueKey> --to <status>
71
+
72
+ # GitLab
73
+ tdecollab gitlab mr list <projectId> --state opened
74
+ tdecollab gitlab pipeline get <projectId> <pipelineId>
75
+ ```
76
+
77
+ ### MCP 서버
78
+
79
+ Claude Desktop 또는 Claude Code에서 MCP 서버로 연동하여 사용할 수 있습니다.
80
+
81
+ ### 로컬 실행 (개발 중)
82
+
83
+ ```json
84
+ {
85
+ "mcpServers": {
86
+ "tdecollab-local": {
87
+ "command": "node",
88
+ "args": ["/absolute/path/to/tdecollab/dist/index.js"],
89
+ "env": {
90
+ "CONFLUENCE_BASE_URL": "...",
91
+ "CONFLUENCE_API_TOKEN": "..."
92
+ }
93
+ }
94
+ }
95
+ }
96
+ ```
97
+
98
+ ### npx 실행 (배포 후)
99
+
100
+ npm에 배포된 패키지를 npx로 즉시 실행할 수 있습니다.
101
+
102
+ ```json
103
+ {
104
+ "mcpServers": {
105
+ "tdecollab": {
106
+ "command": "npx",
107
+ "args": ["-y", "tdecollab", "mcp"],
108
+ "env": {
109
+ "CONFLUENCE_BASE_URL": "...",
110
+ "CONFLUENCE_API_TOKEN": "..."
111
+ }
112
+ }
113
+ }
114
+ }
115
+ ```
116
+
117
+ ## 개발
118
+
119
+ ```bash
120
+ # 개발 모드 (MCP 서버)
121
+ pnpm dev
122
+
123
+ # CLI 실행
124
+ pnpm cli -- confluence page get 12345
125
+
126
+ # 테스트
127
+ pnpm test
128
+
129
+ # 빌드
130
+ pnpm build
131
+
132
+ # 포맷팅
133
+ pnpm format
134
+ ```
135
+
136
+ ## 프로젝트 구조
137
+
138
+ ```
139
+ src/
140
+ ├── index.ts # MCP 서버 엔트리포인트
141
+ ├── cli.ts # CLI 엔트리포인트
142
+ ├── common/ # 공통 모듈 (인증, HTTP, 설정, 에러)
143
+ ├── confluence/ # Confluence 모듈 (api/tools/commands/converters)
144
+ ├── jira/ # JIRA 모듈 (api/tools/commands)
145
+ ├── gitlab/ # GitLab 모듈 (api/tools/commands)
146
+ └── mcp/ # MCP 서버 코어
147
+ ```
148
+
149
+ ## 문서
150
+
151
+ 상세한 설계 문서는 `docs/` 디렉토리를 참조하세요.
152
+
153
+ - [아키텍처](docs/architecture.md)
154
+ - [인증 및 설정](docs/auth-and-config.md)
155
+ - [MCP 서버 설계](docs/mcp-server-design.md)
156
+ - Confluence: [API 스펙](docs/confluence/api-spec.md) | [기능 정의](docs/confluence/features.md) | [MCP 도구](docs/confluence/mcp-tools.md)
157
+ - JIRA: [API 스펙](docs/jira/api-spec.md) | [기능 정의](docs/jira/features.md) | [MCP 도구](docs/jira/mcp-tools.md)
158
+ - GitLab: [API 스펙](docs/gitlab/api-spec.md) | [기능 정의](docs/gitlab/features.md) | [MCP 도구](docs/gitlab/mcp-tools.md)
@@ -0,0 +1,286 @@
1
+ import {
2
+ ConfluenceContentApi,
3
+ ConfluenceSearchApi,
4
+ ConfluenceSpaceApi,
5
+ MarkdownToStorageConverter,
6
+ StorageToMarkdownConverter,
7
+ createConfluenceClient,
8
+ loadConfluenceConfig,
9
+ logger
10
+ } from "./chunk-N44NISLJ.js";
11
+
12
+ // src/mcp/server.ts
13
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
14
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
+
16
+ // src/confluence/tools/index.ts
17
+ import { z } from "zod";
18
+
19
+ // src/confluence/api/label.ts
20
+ var ConfluenceLabelApi = class {
21
+ constructor(client) {
22
+ this.client = client;
23
+ }
24
+ async getLabels(pageId) {
25
+ const response = await this.client.get(`/rest/api/content/${pageId}/label`);
26
+ return response.data.results;
27
+ }
28
+ async addLabels(pageId, labels) {
29
+ const data = labels.map((name) => ({ prefix: "global", name }));
30
+ await this.client.post(`/rest/api/content/${pageId}/label`, data);
31
+ }
32
+ async removeLabel(pageId, labelName) {
33
+ await this.client.delete(`/rest/api/content/${pageId}/label`, {
34
+ params: { name: labelName }
35
+ });
36
+ }
37
+ };
38
+
39
+ // src/confluence/tools/index.ts
40
+ function registerConfluenceTools(server) {
41
+ try {
42
+ const config = loadConfluenceConfig();
43
+ const client = createConfluenceClient(config);
44
+ const contentApi = new ConfluenceContentApi(client);
45
+ const spaceApi = new ConfluenceSpaceApi(client);
46
+ const searchApi = new ConfluenceSearchApi(client);
47
+ const labelApi = new ConfluenceLabelApi(client);
48
+ const mdToStorage = new MarkdownToStorageConverter();
49
+ const storageToMd = new StorageToMarkdownConverter();
50
+ server.tool(
51
+ "confluence_get_page",
52
+ "Confluence \uD398\uC774\uC9C0 \uC0C1\uC138 \uC870\uD68C (Markdown \uBCC0\uD658 \uD3EC\uD568)",
53
+ {
54
+ pageId: z.string().describe("\uD398\uC774\uC9C0 ID")
55
+ },
56
+ async ({ pageId }) => {
57
+ const page = await contentApi.getPage(pageId);
58
+ const md = page.body?.storage?.value ? storageToMd.convert(page.body.storage.value) : "";
59
+ return {
60
+ content: [
61
+ {
62
+ type: "text",
63
+ text: `Title: ${page.title}
64
+ ID: ${page.id}
65
+ Space: ${page.space?.name} (${page.space?.key})
66
+ URL: ${page._links?.base}${page._links?.webui}
67
+
68
+ ${md}`
69
+ }
70
+ ]
71
+ };
72
+ }
73
+ );
74
+ server.tool(
75
+ "confluence_create_page",
76
+ "Confluence \uD398\uC774\uC9C0 \uC0DD\uC131",
77
+ {
78
+ spaceKey: z.string().describe("\uC2A4\uD398\uC774\uC2A4 \uD0A4"),
79
+ title: z.string().describe("\uD398\uC774\uC9C0 \uC81C\uBAA9"),
80
+ content: z.string().describe("\uD398\uC774\uC9C0 \uB0B4\uC6A9 (Markdown)"),
81
+ parentId: z.string().optional().describe("\uBD80\uBAA8 \uD398\uC774\uC9C0 ID"),
82
+ labels: z.array(z.string()).optional().describe("\uB77C\uBCA8 \uBAA9\uB85D")
83
+ },
84
+ async ({ spaceKey, title, content, parentId, labels }) => {
85
+ const storageBody = mdToStorage.convert(content);
86
+ const page = await contentApi.createPage({
87
+ spaceKey,
88
+ title,
89
+ body: storageBody,
90
+ parentId,
91
+ labels
92
+ });
93
+ return {
94
+ content: [
95
+ {
96
+ type: "text",
97
+ text: `\uD398\uC774\uC9C0 \uC0DD\uC131 \uC131\uACF5: ${page.title} (ID: ${page.id})
98
+ URL: ${page._links?.base}${page._links?.webui}`
99
+ }
100
+ ]
101
+ };
102
+ }
103
+ );
104
+ server.tool(
105
+ "confluence_update_page",
106
+ "Confluence \uD398\uC774\uC9C0 \uC218\uC815",
107
+ {
108
+ pageId: z.string().describe("\uD398\uC774\uC9C0 ID"),
109
+ title: z.string().describe("\uD398\uC774\uC9C0 \uC81C\uBAA9"),
110
+ content: z.string().describe("\uD398\uC774\uC9C0 \uB0B4\uC6A9 (Markdown)"),
111
+ version: z.number().describe("\uD604\uC7AC \uD398\uC774\uC9C0 \uBC84\uC804 (\uCDA9\uB3CC \uBC29\uC9C0\uC6A9)")
112
+ },
113
+ async ({ pageId, title, content, version }) => {
114
+ const storageBody = mdToStorage.convert(content);
115
+ const page = await contentApi.updatePage({
116
+ id: pageId,
117
+ title,
118
+ body: storageBody,
119
+ version
120
+ });
121
+ return {
122
+ content: [
123
+ {
124
+ type: "text",
125
+ text: `\uD398\uC774\uC9C0 \uC218\uC815 \uC131\uACF5: ${page.title} (Version: ${page.version?.number})`
126
+ }
127
+ ]
128
+ };
129
+ }
130
+ );
131
+ server.tool(
132
+ "confluence_search_pages",
133
+ "Confluence \uD398\uC774\uC9C0 \uAC80\uC0C9 (CQL)",
134
+ {
135
+ cql: z.string().describe('Confluence Query Language (\uC608: title ~ "guide")'),
136
+ limit: z.number().default(10).describe("\uACB0\uACFC \uAC1C\uC218 \uC81C\uD55C")
137
+ },
138
+ async ({ cql, limit }) => {
139
+ const result = await searchApi.searchByCql(cql, 0, limit);
140
+ const summary = result.results.map((p) => `- [${p.id}] ${p.title} (Space: ${p.space?.key})`).join("\n");
141
+ return {
142
+ content: [
143
+ {
144
+ type: "text",
145
+ text: `\uAC80\uC0C9 \uACB0\uACFC (${result.size}/${result.totalSize}):
146
+ ${summary}`
147
+ }
148
+ ]
149
+ };
150
+ }
151
+ );
152
+ server.tool(
153
+ "confluence_get_spaces",
154
+ "Confluence \uC2A4\uD398\uC774\uC2A4 \uBAA9\uB85D \uC870\uD68C",
155
+ {
156
+ limit: z.number().default(20).describe("\uACB0\uACFC \uAC1C\uC218 \uC81C\uD55C")
157
+ },
158
+ async ({ limit }) => {
159
+ const spaces = await spaceApi.getSpaces("global", 0, limit);
160
+ const summary = spaces.map((s) => `- [${s.key}] ${s.name}`).join("\n");
161
+ return {
162
+ content: [{ type: "text", text: `\uC2A4\uD398\uC774\uC2A4 \uBAA9\uB85D:
163
+ ${summary}` }]
164
+ };
165
+ }
166
+ );
167
+ server.tool(
168
+ "confluence_delete_page",
169
+ "Confluence \uD398\uC774\uC9C0 \uC0AD\uC81C",
170
+ {
171
+ pageId: z.string().describe("\uD398\uC774\uC9C0 ID")
172
+ },
173
+ async ({ pageId }) => {
174
+ await contentApi.deletePage(pageId);
175
+ return {
176
+ content: [
177
+ {
178
+ type: "text",
179
+ text: `\uD398\uC774\uC9C0 \uC0AD\uC81C \uC131\uACF5 (ID: ${pageId})`
180
+ }
181
+ ]
182
+ };
183
+ }
184
+ );
185
+ server.tool(
186
+ "confluence_get_page_tree",
187
+ "Confluence \uD398\uC774\uC9C0 \uD2B8\uB9AC (\uC790\uC2DD \uD398\uC774\uC9C0) \uC870\uD68C",
188
+ {
189
+ pageId: z.string().describe("\uD398\uC774\uC9C0 ID"),
190
+ limit: z.number().default(20).describe("\uACB0\uACFC \uAC1C\uC218 \uC81C\uD55C")
191
+ },
192
+ async ({ pageId, limit }) => {
193
+ const children = await contentApi.getChildPages(pageId, 0, limit);
194
+ const summary = children.map((p) => `- [${p.id}] ${p.title}`).join("\n");
195
+ return {
196
+ content: [
197
+ {
198
+ type: "text",
199
+ text: `\uC790\uC2DD \uD398\uC774\uC9C0 \uBAA9\uB85D (${children.length}):
200
+ ${summary}`
201
+ }
202
+ ]
203
+ };
204
+ }
205
+ );
206
+ server.tool(
207
+ "confluence_manage_labels",
208
+ "Confluence \uB77C\uBCA8 \uAD00\uB9AC",
209
+ {
210
+ pageId: z.string().describe("\uD398\uC774\uC9C0 ID"),
211
+ action: z.enum(["list", "add", "remove"]).describe("\uC791\uC5C5 \uC720\uD615"),
212
+ labels: z.array(z.string()).optional().describe("\uCD94\uAC00\uD560 \uB77C\uBCA8 \uBAA9\uB85D (add \uC791\uC5C5 \uC2DC \uD544\uC218)"),
213
+ label: z.string().optional().describe("\uC0AD\uC81C\uD560 \uB77C\uBCA8 (remove \uC791\uC5C5 \uC2DC \uD544\uC218)")
214
+ },
215
+ async ({ pageId, action, labels, label }) => {
216
+ if (action === "list") {
217
+ const result = await labelApi.getLabels(pageId);
218
+ const summary = result.map((l) => l.name).join(", ");
219
+ return {
220
+ content: [{ type: "text", text: `\uB77C\uBCA8 \uBAA9\uB85D: ${summary}` }]
221
+ };
222
+ } else if (action === "add") {
223
+ if (!labels || labels.length === 0) {
224
+ throw new Error("\uB77C\uBCA8 \uBAA9\uB85D\uC744 \uC785\uB825\uD574\uC8FC\uC138\uC694.");
225
+ }
226
+ await labelApi.addLabels(pageId, labels);
227
+ return {
228
+ content: [{ type: "text", text: `\uB77C\uBCA8 \uCD94\uAC00 \uC131\uACF5: ${labels.join(", ")}` }]
229
+ };
230
+ } else if (action === "remove") {
231
+ if (!label) {
232
+ throw new Error("\uC0AD\uC81C\uD560 \uB77C\uBCA8\uC744 \uC785\uB825\uD574\uC8FC\uC138\uC694.");
233
+ }
234
+ await labelApi.removeLabel(pageId, label);
235
+ return {
236
+ content: [{ type: "text", text: `\uB77C\uBCA8 \uC0AD\uC81C \uC131\uACF5: ${label}` }]
237
+ };
238
+ }
239
+ return { content: [] };
240
+ }
241
+ );
242
+ server.tool(
243
+ "confluence_convert_content",
244
+ "Confluence \uCEE8\uD150\uCE20 \uD3EC\uB9F7 \uBCC0\uD658 (Markdown \u2194 Storage)",
245
+ {
246
+ content: z.string().describe("\uBCC0\uD658\uD560 \uCEE8\uD150\uCE20"),
247
+ format: z.enum(["storage_to_markdown", "markdown_to_storage"]).describe("\uBCC0\uD658 \uBC29\uD5A5")
248
+ },
249
+ async ({ content, format }) => {
250
+ let result = "";
251
+ if (format === "storage_to_markdown") {
252
+ result = storageToMd.convert(content);
253
+ } else {
254
+ result = mdToStorage.convert(content);
255
+ }
256
+ return {
257
+ content: [{ type: "text", text: result }]
258
+ };
259
+ }
260
+ );
261
+ } catch (error) {
262
+ logger.warn(`Confluence \uC124\uC815 \uB85C\uB4DC \uC2E4\uD328 \uB610\uB294 \uB3C4\uAD6C \uB4F1\uB85D \uC911 \uC624\uB958 \uBC1C\uC0DD: ${error.message}`);
263
+ }
264
+ }
265
+
266
+ // src/mcp/server.ts
267
+ async function runServer() {
268
+ try {
269
+ const server = new McpServer({
270
+ name: "TDE Collab",
271
+ version: "1.0.0"
272
+ });
273
+ registerConfluenceTools(server);
274
+ const transport = new StdioServerTransport();
275
+ await server.connect(transport);
276
+ logger.info("TDE Collab MCP Server running on stdio");
277
+ } catch (error) {
278
+ logger.error(`Fatal error in MCP Server: ${error}`);
279
+ process.exit(1);
280
+ }
281
+ }
282
+
283
+ export {
284
+ runServer
285
+ };
286
+ //# sourceMappingURL=chunk-7JOEVDKT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/mcp/server.ts","../src/confluence/tools/index.ts","../src/confluence/api/label.ts"],"sourcesContent":["\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { registerConfluenceTools } from '../confluence/tools/index.js';\nimport { logger } from '../common/logger.js';\n\nexport async function runServer() {\n try {\n const server = new McpServer({\n name: 'TDE Collab',\n version: '1.0.0',\n });\n\n // Confluence 도구 등록\n registerConfluenceTools(server);\n\n // Stdio 전송 계층 연결\n const transport = new StdioServerTransport();\n await server.connect(transport);\n\n logger.info('TDE Collab MCP Server running on stdio');\n } catch (error) {\n logger.error(`Fatal error in MCP Server: ${error}`);\n process.exit(1);\n }\n}\n","import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { z } from 'zod';\nimport { ConfluenceContentApi } from '../api/content.js';\nimport { ConfluenceSpaceApi } from '../api/space.js';\nimport { ConfluenceSearchApi } from '../api/search.js';\nimport { ConfluenceLabelApi } from '../api/label.js';\nimport { createConfluenceClient } from '../api/client.js';\nimport { MarkdownToStorageConverter } from '../converters/md-to-storage.js';\nimport { StorageToMarkdownConverter } from '../converters/storage-to-md.js';\nimport { loadConfluenceConfig } from '../../common/config.js';\nimport { logger } from '../../common/logger.js';\n\nexport function registerConfluenceTools(server: McpServer) {\n try {\n const config = loadConfluenceConfig();\n const client = createConfluenceClient(config);\n\n const contentApi = new ConfluenceContentApi(client);\n const spaceApi = new ConfluenceSpaceApi(client);\n const searchApi = new ConfluenceSearchApi(client);\n const labelApi = new ConfluenceLabelApi(client);\n\n const mdToStorage = new MarkdownToStorageConverter();\n const storageToMd = new StorageToMarkdownConverter();\n\n // Tools\n\n server.tool(\n 'confluence_get_page',\n 'Confluence 페이지 상세 조회 (Markdown 변환 포함)',\n {\n pageId: z.string().describe('페이지 ID'),\n },\n async ({ pageId }) => {\n const page = await contentApi.getPage(pageId);\n const md = page.body?.storage?.value ? storageToMd.convert(page.body.storage.value) : '';\n\n return {\n content: [\n {\n type: 'text',\n text: `Title: ${page.title}\\nID: ${page.id}\\nSpace: ${page.space?.name} (${page.space?.key})\\nURL: ${page._links?.base}${page._links?.webui}\\n\\n${md}`,\n },\n ],\n };\n }\n );\n\n server.tool(\n 'confluence_create_page',\n 'Confluence 페이지 생성',\n {\n spaceKey: z.string().describe('스페이스 키'),\n title: z.string().describe('페이지 제목'),\n content: z.string().describe('페이지 내용 (Markdown)'),\n parentId: z.string().optional().describe('부모 페이지 ID'),\n labels: z.array(z.string()).optional().describe('라벨 목록'),\n },\n async ({ spaceKey, title, content, parentId, labels }) => {\n const storageBody = mdToStorage.convert(content);\n const page = await contentApi.createPage({\n spaceKey,\n title,\n body: storageBody,\n parentId,\n labels\n });\n\n return {\n content: [\n {\n type: 'text',\n text: `페이지 생성 성공: ${page.title} (ID: ${page.id})\\nURL: ${page._links?.base}${page._links?.webui}`,\n },\n ],\n };\n }\n );\n\n server.tool(\n 'confluence_update_page',\n 'Confluence 페이지 수정',\n {\n pageId: z.string().describe('페이지 ID'),\n title: z.string().describe('페이지 제목'),\n content: z.string().describe('페이지 내용 (Markdown)'),\n version: z.number().describe('현재 페이지 버전 (충돌 방지용)'),\n },\n async ({ pageId, title, content, version }) => {\n const storageBody = mdToStorage.convert(content);\n const page = await contentApi.updatePage({\n id: pageId,\n title,\n body: storageBody,\n version\n });\n\n return {\n content: [\n {\n type: 'text',\n text: `페이지 수정 성공: ${page.title} (Version: ${page.version?.number})`,\n },\n ],\n };\n }\n );\n\n server.tool(\n 'confluence_search_pages',\n 'Confluence 페이지 검색 (CQL)',\n {\n cql: z.string().describe('Confluence Query Language (예: title ~ \"guide\")'),\n limit: z.number().default(10).describe('결과 개수 제한'),\n },\n async ({ cql, limit }) => {\n const result = await searchApi.searchByCql(cql, 0, limit);\n const summary = result.results.map(p => `- [${p.id}] ${p.title} (Space: ${p.space?.key})`).join('\\n');\n\n return {\n content: [\n {\n type: 'text',\n text: `검색 결과 (${result.size}/${result.totalSize}):\\n${summary}`,\n },\n ],\n };\n }\n );\n\n server.tool(\n 'confluence_get_spaces',\n 'Confluence 스페이스 목록 조회',\n {\n limit: z.number().default(20).describe('결과 개수 제한')\n },\n async ({ limit }) => {\n const spaces = await spaceApi.getSpaces('global', 0, limit);\n const summary = spaces.map(s => `- [${s.key}] ${s.name}`).join('\\n');\n return {\n content: [{ type: 'text', text: `스페이스 목록:\\n${summary}` }]\n };\n }\n );\n\n server.tool(\n 'confluence_delete_page',\n 'Confluence 페이지 삭제',\n {\n pageId: z.string().describe('페이지 ID'),\n },\n async ({ pageId }) => {\n await contentApi.deletePage(pageId);\n return {\n content: [\n {\n type: 'text',\n text: `페이지 삭제 성공 (ID: ${pageId})`,\n },\n ],\n };\n }\n );\n\n server.tool(\n 'confluence_get_page_tree',\n 'Confluence 페이지 트리 (자식 페이지) 조회',\n {\n pageId: z.string().describe('페이지 ID'),\n limit: z.number().default(20).describe('결과 개수 제한'),\n },\n async ({ pageId, limit }) => {\n const children = await contentApi.getChildPages(pageId, 0, limit);\n const summary = children.map(p => `- [${p.id}] ${p.title}`).join('\\n');\n return {\n content: [\n {\n type: 'text',\n text: `자식 페이지 목록 (${children.length}):\\n${summary}`,\n },\n ],\n };\n }\n );\n\n server.tool(\n 'confluence_manage_labels',\n 'Confluence 라벨 관리',\n {\n pageId: z.string().describe('페이지 ID'),\n action: z.enum(['list', 'add', 'remove']).describe('작업 유형'),\n labels: z.array(z.string()).optional().describe('추가할 라벨 목록 (add 작업 시 필수)'),\n label: z.string().optional().describe('삭제할 라벨 (remove 작업 시 필수)'),\n },\n async ({ pageId, action, labels, label }) => {\n if (action === 'list') {\n const result = await labelApi.getLabels(pageId);\n const summary = result.map(l => l.name).join(', ');\n return {\n content: [{ type: 'text', text: `라벨 목록: ${summary}` }],\n };\n } else if (action === 'add') {\n if (!labels || labels.length === 0) {\n throw new Error('라벨 목록을 입력해주세요.');\n }\n await labelApi.addLabels(pageId, labels);\n return {\n content: [{ type: 'text', text: `라벨 추가 성공: ${labels.join(', ')}` }],\n };\n } else if (action === 'remove') {\n if (!label) {\n throw new Error('삭제할 라벨을 입력해주세요.');\n }\n await labelApi.removeLabel(pageId, label);\n return {\n content: [{ type: 'text', text: `라벨 삭제 성공: ${label}` }],\n };\n }\n return { content: [] };\n }\n );\n\n server.tool(\n 'confluence_convert_content',\n 'Confluence 컨텐츠 포맷 변환 (Markdown ↔ Storage)',\n {\n content: z.string().describe('변환할 컨텐츠'),\n format: z.enum(['storage_to_markdown', 'markdown_to_storage']).describe('변환 방향'),\n },\n async ({ content, format }) => {\n let result = '';\n if (format === 'storage_to_markdown') {\n result = storageToMd.convert(content);\n } else {\n result = mdToStorage.convert(content);\n }\n return {\n content: [{ type: 'text', text: result }],\n };\n }\n );\n\n } catch (error) {\n logger.warn(`Confluence 설정 로드 실패 또는 도구 등록 중 오류 발생: ${(error as Error).message}`);\n // Do not throw, just skip registration if config is missing (optional module pattern)\n }\n}\n","import { AxiosInstance } from 'axios';\nimport { ConfluenceLabel } from '../types.js';\n\nexport class ConfluenceLabelApi {\n constructor(private client: AxiosInstance) { }\n\n async getLabels(pageId: string): Promise<ConfluenceLabel[]> {\n const response = await this.client.get(`/rest/api/content/${pageId}/label`);\n return response.data.results;\n }\n\n async addLabels(pageId: string, labels: string[]): Promise<void> {\n const data = labels.map(name => ({ prefix: 'global', name }));\n await this.client.post(`/rest/api/content/${pageId}/label`, data);\n }\n\n async removeLabel(pageId: string, labelName: string): Promise<void> {\n // Label deletion usually requires query param for name\n // /rest/api/content/{id}/label?name={name}\n await this.client.delete(`/rest/api/content/${pageId}/label`, {\n params: { name: labelName }\n });\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,361 @@
1
+ // src/common/logger.ts
2
+ var Logger = class {
3
+ level = "info";
4
+ constructor() {
5
+ this.level = process.env.LOG_LEVEL || "info";
6
+ }
7
+ // 로그 메시지 포맷팅 (타임스탬프, 레벨 포함)
8
+ formatMessage(level, message, ...args) {
9
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
10
+ let formattedMessage = `[${timestamp}] [${level.toUpperCase()}] ${message}`;
11
+ if (args.length > 0) {
12
+ formattedMessage += " " + args.map(
13
+ (arg) => typeof arg === "object" ? JSON.stringify(arg) : String(arg)
14
+ ).join(" ");
15
+ }
16
+ formattedMessage = this.maskSensitiveData(formattedMessage);
17
+ return formattedMessage;
18
+ }
19
+ // 민감한 데이터(키, 패스워드 등) 마스킹 처리
20
+ maskSensitiveData(text) {
21
+ return text.replace(/((?:token|password|secret|key)["']?\s*[:=]\s*["']?)([^"'\s]+)(["']?)/gi, "$1******$3");
22
+ }
23
+ // Debug 레벨 로그 출력
24
+ debug(message, ...args) {
25
+ if (this.shouldLog("debug")) {
26
+ console.error(this.formatMessage("debug", message, ...args));
27
+ }
28
+ }
29
+ // Info 레벨 로그 출력
30
+ info(message, ...args) {
31
+ if (this.shouldLog("info")) {
32
+ console.error(this.formatMessage("info", message, ...args));
33
+ }
34
+ }
35
+ // Warn 레벨 로그 출력
36
+ warn(message, ...args) {
37
+ if (this.shouldLog("warn")) {
38
+ console.error(this.formatMessage("warn", message, ...args));
39
+ }
40
+ }
41
+ // Error 레벨 로그 출력
42
+ error(message, ...args) {
43
+ if (this.shouldLog("error")) {
44
+ console.error(this.formatMessage("error", message, ...args));
45
+ }
46
+ }
47
+ // 현재 설정된 로그 레벨에 따라 출력 여부 결정
48
+ shouldLog(level) {
49
+ const levels = ["debug", "info", "warn", "error"];
50
+ return levels.indexOf(level) >= levels.indexOf(this.level);
51
+ }
52
+ };
53
+ var logger = new Logger();
54
+
55
+ // src/confluence/api/content.ts
56
+ var ConfluenceContentApi = class {
57
+ constructor(client) {
58
+ this.client = client;
59
+ }
60
+ async getPage(id, expand) {
61
+ const expandParam = expand ? expand.join(",") : "body.storage,version,space,metadata.labels";
62
+ const response = await this.client.get(`/rest/api/content/${id}`, {
63
+ params: { expand: expandParam }
64
+ });
65
+ return response.data;
66
+ }
67
+ async getPageByTitle(spaceKey, title, expand) {
68
+ const expandParam = expand ? expand.join(",") : "body.storage,version,space";
69
+ const response = await this.client.get("/rest/api/content", {
70
+ params: {
71
+ spaceKey,
72
+ title,
73
+ expand: expandParam,
74
+ limit: 1
75
+ }
76
+ });
77
+ if (response.data.results && response.data.results.length > 0) {
78
+ return response.data.results[0];
79
+ }
80
+ return null;
81
+ }
82
+ async createPage(params) {
83
+ const data = {
84
+ type: "page",
85
+ title: params.title,
86
+ space: { key: params.spaceKey },
87
+ body: {
88
+ storage: {
89
+ value: params.body,
90
+ representation: "storage"
91
+ }
92
+ }
93
+ };
94
+ if (params.parentId) {
95
+ data.ancestors = [{ id: params.parentId }];
96
+ }
97
+ const response = await this.client.post("/rest/api/content", data);
98
+ if (params.labels && params.labels.length > 0) {
99
+ await this.addLabels(response.data.id, params.labels);
100
+ }
101
+ return response.data;
102
+ }
103
+ async updatePage(params) {
104
+ const data = {
105
+ version: { number: params.version + 1 },
106
+ title: params.title,
107
+ type: "page",
108
+ body: {
109
+ storage: {
110
+ value: params.body,
111
+ representation: "storage"
112
+ }
113
+ }
114
+ };
115
+ const response = await this.client.put(`/rest/api/content/${params.id}`, data);
116
+ return response.data;
117
+ }
118
+ async deletePage(id) {
119
+ await this.client.delete(`/rest/api/content/${id}`);
120
+ }
121
+ async getChildPages(id, start = 0, limit = 25) {
122
+ const response = await this.client.get(`/rest/api/content/${id}/child/page`, {
123
+ params: { start, limit }
124
+ });
125
+ return response.data.results;
126
+ }
127
+ // Label helper inside content api or separate?
128
+ // Let's implement basic label addition here since it's used in create.
129
+ // Actually, label logic is in label.ts, but due to circular dependency or convenience...
130
+ // Let's implement it here privately or import it.
131
+ // Better to keep it separate as per plan, but `this.client` is available here.
132
+ // I'll implement a simple one here or use the separate class later.
133
+ // For now, simple implementation to support createPage.
134
+ async addLabels(id, labels) {
135
+ const data = labels.map((name) => ({ prefix: "global", name }));
136
+ await this.client.post(`/rest/api/content/${id}/label`, data);
137
+ }
138
+ };
139
+
140
+ // src/confluence/api/space.ts
141
+ var ConfluenceSpaceApi = class {
142
+ constructor(client) {
143
+ this.client = client;
144
+ }
145
+ async getSpaces(type = "global", start = 0, limit = 25) {
146
+ const response = await this.client.get("/rest/api/space", {
147
+ params: { type, start, limit }
148
+ });
149
+ return response.data.results;
150
+ }
151
+ async getSpace(spaceKey) {
152
+ const response = await this.client.get(`/rest/api/space/${spaceKey}`);
153
+ return response.data;
154
+ }
155
+ };
156
+
157
+ // src/confluence/api/search.ts
158
+ var ConfluenceSearchApi = class {
159
+ constructor(client) {
160
+ this.client = client;
161
+ }
162
+ async searchByCql(cql, start = 0, limit = 25, expand) {
163
+ const expandParam = expand ? expand.join(",") : "body.storage,version,space,metadata.labels";
164
+ const response = await this.client.get("/rest/api/content/search", {
165
+ params: {
166
+ cql,
167
+ start,
168
+ limit,
169
+ expand: expandParam
170
+ }
171
+ });
172
+ return response.data;
173
+ }
174
+ };
175
+
176
+ // src/common/http-client.ts
177
+ import axios from "axios";
178
+
179
+ // src/common/errors.ts
180
+ var TdeCollabError = class extends Error {
181
+ constructor(message) {
182
+ super(message);
183
+ this.name = this.constructor.name;
184
+ Error.captureStackTrace(this, this.constructor);
185
+ }
186
+ };
187
+ var ApiError = class extends TdeCollabError {
188
+ constructor(statusCode, message, data) {
189
+ super(`API \uC694\uCCAD \uC2E4\uD328 (${statusCode}): ${message}`);
190
+ this.statusCode = statusCode;
191
+ this.data = data;
192
+ }
193
+ };
194
+ var AuthError = class extends TdeCollabError {
195
+ constructor(message = "\uC778\uC99D\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4") {
196
+ super(message);
197
+ }
198
+ };
199
+ var NotFoundError = class extends TdeCollabError {
200
+ constructor(resource, id) {
201
+ super(`${resource} '${id}'\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4`);
202
+ }
203
+ };
204
+ var ConflictError = class extends TdeCollabError {
205
+ constructor(message) {
206
+ super(message);
207
+ }
208
+ };
209
+
210
+ // src/common/http-client.ts
211
+ function createHttpClient(config) {
212
+ const client = axios.create({
213
+ baseURL: config.baseUrl,
214
+ timeout: 3e4,
215
+ // 30초 타임아웃
216
+ headers: {
217
+ "Content-Type": "application/json"
218
+ }
219
+ });
220
+ client.interceptors.request.use((reqConfig) => {
221
+ if (config.auth.username && config.auth.token) {
222
+ const token = Buffer.from(`${config.auth.username}:${config.auth.token}`).toString("base64");
223
+ reqConfig.headers.Authorization = `Basic ${token}`;
224
+ } else if (config.auth.token) {
225
+ if (!reqConfig.headers.Authorization && !reqConfig.headers["PRIVATE-TOKEN"]) {
226
+ reqConfig.headers.Authorization = `Bearer ${config.auth.token}`;
227
+ }
228
+ }
229
+ return reqConfig;
230
+ });
231
+ client.interceptors.response.use(
232
+ (response) => {
233
+ logger.debug(`[HTTP] ${response.status} ${response.config.method?.toUpperCase()} ${response.config.url}`);
234
+ return response;
235
+ },
236
+ (error) => {
237
+ if (error.response) {
238
+ const status = error.response.status;
239
+ const method = error.config?.method?.toUpperCase();
240
+ const url = error.config?.url;
241
+ const message = error.response.data?.message || error.message;
242
+ logger.error(`[HTTP] ${status} ${method} ${url} - ${message}`);
243
+ if (status === 401 || status === 403) {
244
+ throw new AuthError(`\uC778\uC99D \uC2E4\uD328: ${message}`);
245
+ }
246
+ if (status === 404) {
247
+ throw new NotFoundError("\uB9AC\uC18C\uC2A4", url || "unknown");
248
+ }
249
+ if (status === 409) {
250
+ throw new ConflictError(`\uCDA9\uB3CC \uBC1C\uC0DD: ${message}`);
251
+ }
252
+ throw new ApiError(status, message, error.response.data);
253
+ } else if (error.request) {
254
+ logger.error(`[HTTP] No Response: ${error.message}`);
255
+ throw new ApiError(0, `\uC11C\uBC84\uB85C\uBD80\uD130 \uC751\uB2F5\uC774 \uC5C6\uC2B5\uB2C8\uB2E4: ${error.message}`);
256
+ } else {
257
+ logger.error(`[HTTP] Request Error: ${error.message}`);
258
+ throw new ApiError(0, `\uC694\uCCAD \uC124\uC815 \uC911 \uC624\uB958 \uBC1C\uC0DD: ${error.message}`);
259
+ }
260
+ }
261
+ );
262
+ return client;
263
+ }
264
+
265
+ // src/confluence/api/client.ts
266
+ function createConfluenceClient(config) {
267
+ return createHttpClient(config);
268
+ }
269
+
270
+ // src/confluence/converters/md-to-storage.ts
271
+ import MarkdownIt from "markdown-it";
272
+ var MarkdownToStorageConverter = class {
273
+ md;
274
+ constructor() {
275
+ this.md = new MarkdownIt({
276
+ html: true,
277
+ linkify: true,
278
+ breaks: true
279
+ });
280
+ this.md.renderer.rules.fence = (tokens, idx) => {
281
+ const token = tokens[idx];
282
+ const code = token.content.trim();
283
+ const lang = token.info.trim();
284
+ return `<ac:structured-macro ac:name="code" ac:schema-version="1">
285
+ <ac:parameter ac:name="language">${lang || "text"}</ac:parameter>
286
+ <ac:plain-text-body><![CDATA[${code}]]></ac:plain-text-body>
287
+ </ac:structured-macro>`;
288
+ };
289
+ }
290
+ convert(markdown) {
291
+ return this.md.render(markdown);
292
+ }
293
+ };
294
+
295
+ // src/confluence/converters/storage-to-md.ts
296
+ var StorageToMarkdownConverter = class {
297
+ convert(storageHtml) {
298
+ let md = storageHtml;
299
+ md = md.replace(/<ac:structured-macro[^>]*ac:name="code"[^>]*>[\s\S]*?<ac:parameter[^>]*ac:name="language">([^<]*)<\/ac:parameter>[\s\S]*?<ac:plain-text-body><!\[CDATA\[([\s\S]*?)\]\]><\/ac:plain-text-body>[\s\S]*?<\/ac:structured-macro>/g, (match, lang, code) => {
300
+ return `\`\`\`${lang}
301
+ ${code}
302
+ \`\`\``;
303
+ });
304
+ md = md.replace(/<h1>(.*?)<\/h1>/g, "# $1\n");
305
+ md = md.replace(/<h2>(.*?)<\/h2>/g, "## $1\n");
306
+ md = md.replace(/<h3>(.*?)<\/h3>/g, "### $1\n");
307
+ md = md.replace(/<p>(.*?)<\/p>/g, "$1\n\n");
308
+ md = md.replace(/<strong>(.*?)<\/strong>/g, "**$1**");
309
+ md = md.replace(/<b>(.*?)<\/b>/g, "**$1**");
310
+ md = md.replace(/<em>(.*?)<\/em>/g, "*$1*");
311
+ md = md.replace(/<i>(.*?)<\/i>/g, "*$1*");
312
+ md = md.replace(/<ul>([\s\S]*?)<\/ul>/g, (match, content) => {
313
+ return content.replace(/<li>(.*?)<\/li>/g, "- $1\n");
314
+ });
315
+ md = md.replace(/<ol>([\s\S]*?)<\/ol>/g, (match, content) => {
316
+ let i = 1;
317
+ return content.replace(/<li>(.*?)<\/li>/g, () => `${i++}. $1
318
+ `);
319
+ });
320
+ md = md.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"');
321
+ return md.trim();
322
+ }
323
+ };
324
+
325
+ // src/common/config.ts
326
+ import dotenv from "dotenv";
327
+ dotenv.config();
328
+ function getEnvOrThrow(key, description) {
329
+ const value = process.env[key];
330
+ if (!value) {
331
+ const errorMsg = `\uD658\uACBD\uBCC0\uC218 '${key}'\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. (${description})`;
332
+ logger.error(errorMsg);
333
+ throw new Error(errorMsg);
334
+ }
335
+ return value;
336
+ }
337
+ function loadConfluenceConfig() {
338
+ const baseUrl = getEnvOrThrow("CONFLUENCE_BASE_URL", "Confluence \uAE30\uBCF8 URL");
339
+ const username = process.env.CONFLUENCE_USERNAME;
340
+ const token = getEnvOrThrow("CONFLUENCE_API_TOKEN", "Confluence API \uD1A0\uD070");
341
+ return {
342
+ baseUrl,
343
+ auth: {
344
+ username,
345
+ token
346
+ // API 토큰은 일반적으로 패스워드 필드에 사용되지만, 여기서는 token으로 저장하여 범용성 확보
347
+ }
348
+ };
349
+ }
350
+
351
+ export {
352
+ ConfluenceContentApi,
353
+ ConfluenceSpaceApi,
354
+ ConfluenceSearchApi,
355
+ logger,
356
+ createConfluenceClient,
357
+ MarkdownToStorageConverter,
358
+ StorageToMarkdownConverter,
359
+ loadConfluenceConfig
360
+ };
361
+ //# sourceMappingURL=chunk-N44NISLJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/common/logger.ts","../src/confluence/api/content.ts","../src/confluence/api/space.ts","../src/confluence/api/search.ts","../src/common/http-client.ts","../src/common/errors.ts","../src/confluence/api/client.ts","../src/confluence/converters/md-to-storage.ts","../src/confluence/converters/storage-to-md.ts","../src/common/config.ts"],"sourcesContent":["export type LogLevel = 'debug' | 'info' | 'warn' | 'error';\n\nclass Logger {\n private level: LogLevel = 'info';\n\n constructor() {\n this.level = (process.env.LOG_LEVEL as LogLevel) || 'info';\n }\n\n // 로그 메시지 포맷팅 (타임스탬프, 레벨 포함)\n private formatMessage(level: LogLevel, message: string, ...args: any[]): string {\n const timestamp = new Date().toISOString();\n let formattedMessage = `[${timestamp}] [${level.toUpperCase()}] ${message}`;\n\n if (args.length > 0) {\n formattedMessage += ' ' + args.map(arg =>\n typeof arg === 'object' ? JSON.stringify(arg) : String(arg)\n ).join(' ');\n }\n\n // 민감 정보 마스킹 (토큰, 비밀번호 등)\n formattedMessage = this.maskSensitiveData(formattedMessage);\n\n return formattedMessage;\n }\n\n // 민감한 데이터(키, 패스워드 등) 마스킹 처리\n private maskSensitiveData(text: string): string {\n // 키워드 기반 마스킹 (token, password, secret 등)\n // 간단한 예시 구현 (실제로는 정규식 등으로 더 정교하게 처리 가능)\n return text.replace(/((?:token|password|secret|key)[\"']?\\s*[:=]\\s*[\"']?)([^\"'\\s]+)([\"']?)/gi, '$1******$3');\n }\n\n // Debug 레벨 로그 출력\n debug(message: string, ...args: any[]) {\n if (this.shouldLog('debug')) {\n console.error(this.formatMessage('debug', message, ...args));\n }\n }\n\n // Info 레벨 로그 출력\n info(message: string, ...args: any[]) {\n if (this.shouldLog('info')) {\n console.error(this.formatMessage('info', message, ...args));\n }\n }\n\n // Warn 레벨 로그 출력\n warn(message: string, ...args: any[]) {\n if (this.shouldLog('warn')) {\n console.error(this.formatMessage('warn', message, ...args));\n }\n }\n\n // Error 레벨 로그 출력\n error(message: string, ...args: any[]) {\n if (this.shouldLog('error')) {\n console.error(this.formatMessage('error', message, ...args));\n }\n }\n\n // 현재 설정된 로그 레벨에 따라 출력 여부 결정\n private shouldLog(level: LogLevel): boolean {\n const levels: LogLevel[] = ['debug', 'info', 'warn', 'error'];\n return levels.indexOf(level) >= levels.indexOf(this.level);\n }\n}\n\nexport const logger = new Logger();\n","import { AxiosInstance } from 'axios';\nimport { ConfluencePageResponse, CreatePageParams, UpdatePageParams, ConfluenceLabel } from '../types.js';\n\nexport class ConfluenceContentApi {\n constructor(private client: AxiosInstance) { }\n\n async getPage(id: string, expand?: string[]): Promise<ConfluencePageResponse> {\n const expandParam = expand ? expand.join(',') : 'body.storage,version,space,metadata.labels';\n const response = await this.client.get(`/rest/api/content/${id}`, {\n params: { expand: expandParam }\n });\n return response.data;\n }\n\n async getPageByTitle(spaceKey: string, title: string, expand?: string[]): Promise<ConfluencePageResponse | null> {\n const expandParam = expand ? expand.join(',') : 'body.storage,version,space';\n const response = await this.client.get('/rest/api/content', {\n params: {\n spaceKey,\n title,\n expand: expandParam,\n limit: 1\n }\n });\n\n if (response.data.results && response.data.results.length > 0) {\n return response.data.results[0];\n }\n return null;\n }\n\n async createPage(params: CreatePageParams): Promise<ConfluencePageResponse> {\n const data: any = {\n type: 'page',\n title: params.title,\n space: { key: params.spaceKey },\n body: {\n storage: {\n value: params.body,\n representation: 'storage'\n }\n }\n };\n\n if (params.parentId) {\n data.ancestors = [{ id: params.parentId }];\n }\n\n const response = await this.client.post('/rest/api/content', data);\n\n // add labels if provided\n if (params.labels && params.labels.length > 0) {\n await this.addLabels(response.data.id, params.labels);\n // refetch to include labels in response if needed, \n // but simpler to just return the create response.\n // Or we can construct the object.\n }\n\n return response.data;\n }\n\n async updatePage(params: UpdatePageParams): Promise<ConfluencePageResponse> {\n const data = {\n version: { number: params.version + 1 },\n title: params.title,\n type: 'page',\n body: {\n storage: {\n value: params.body,\n representation: 'storage'\n }\n }\n };\n\n const response = await this.client.put(`/rest/api/content/${params.id}`, data);\n return response.data;\n }\n\n async deletePage(id: string): Promise<void> {\n await this.client.delete(`/rest/api/content/${id}`);\n }\n\n async getChildPages(id: string, start = 0, limit = 25): Promise<ConfluencePageResponse[]> {\n const response = await this.client.get(`/rest/api/content/${id}/child/page`, {\n params: { start, limit }\n });\n return response.data.results;\n }\n\n // Label helper inside content api or separate? \n // Let's implement basic label addition here since it's used in create.\n // Actually, label logic is in label.ts, but due to circular dependency or convenience...\n // Let's implement it here privately or import it.\n // Better to keep it separate as per plan, but `this.client` is available here.\n // I'll implement a simple one here or use the separate class later. \n // For now, simple implementation to support createPage.\n private async addLabels(id: string, labels: string[]): Promise<void> {\n const data = labels.map(name => ({ prefix: 'global', name }));\n await this.client.post(`/rest/api/content/${id}/label`, data);\n }\n}\n","import { AxiosInstance } from 'axios';\nimport { ConfluenceSpaceResponse } from '../types.js';\n\nexport class ConfluenceSpaceApi {\n constructor(private client: AxiosInstance) { }\n\n async getSpaces(type: string = 'global', start = 0, limit = 25): Promise<ConfluenceSpaceResponse[]> {\n const response = await this.client.get('/rest/api/space', {\n params: { type, start, limit }\n });\n return response.data.results;\n }\n\n async getSpace(spaceKey: string): Promise<ConfluenceSpaceResponse> {\n const response = await this.client.get(`/rest/api/space/${spaceKey}`);\n return response.data;\n }\n}\n","import { AxiosInstance } from 'axios';\nimport { ConfluenceSearchResponse } from '../types.js';\n\nexport class ConfluenceSearchApi {\n constructor(private client: AxiosInstance) { }\n\n async searchByCql(cql: string, start = 0, limit = 25, expand?: string[]): Promise<ConfluenceSearchResponse> {\n const expandParam = expand ? expand.join(',') : 'body.storage,version,space,metadata.labels';\n const response = await this.client.get('/rest/api/content/search', {\n params: {\n cql,\n start,\n limit,\n expand: expandParam\n }\n });\n\n return response.data;\n }\n}\n","import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';\nimport { ApiError, AuthError, NotFoundError, ConflictError } from './errors.js';\nimport { logger } from './logger.js';\nimport { ServiceConfig } from './types.js';\n\nexport function createHttpClient(config: ServiceConfig): AxiosInstance {\n const client = axios.create({\n baseURL: config.baseUrl,\n timeout: 30000, // 30초 타임아웃\n headers: {\n 'Content-Type': 'application/json',\n },\n });\n\n // 요청 인터셉터: 인증 헤더 주입\n client.interceptors.request.use((reqConfig) => {\n // Basic 인증 (사용자명 + 토큰)\n if (config.auth.username && config.auth.token) {\n const token = Buffer.from(`${config.auth.username}:${config.auth.token}`).toString('base64');\n reqConfig.headers.Authorization = `Basic ${token}`;\n }\n // 토큰 기반 인증 (GitLab 등)\n else if (config.auth.token) {\n // GitLab 등은 PRIVATE-TOKEN 헤더를 사용하기도 하나,\n // 여기서는 표준 Authorization Bearer 헤더를 기본으로 사용.\n // 서비스별 특수 헤더는 필요 시 호출자나 별도 로직에서 처리.\n if (!reqConfig.headers.Authorization && !reqConfig.headers['PRIVATE-TOKEN']) {\n reqConfig.headers.Authorization = `Bearer ${config.auth.token}`;\n }\n }\n return reqConfig;\n });\n\n // 응답 인터셉터: 에러 핸들링 및 로깅\n client.interceptors.response.use(\n (response: AxiosResponse) => {\n logger.debug(`[HTTP] ${response.status} ${response.config.method?.toUpperCase()} ${response.config.url}`);\n return response;\n },\n (error: AxiosError) => {\n if (error.response) {\n const status = error.response.status;\n const method = error.config?.method?.toUpperCase();\n const url = error.config?.url;\n const message = (error.response.data as any)?.message || error.message;\n\n logger.error(`[HTTP] ${status} ${method} ${url} - ${message}`);\n\n // 상태 코드별 커스텀 에러 매핑\n if (status === 401 || status === 403) {\n throw new AuthError(`인증 실패: ${message}`);\n }\n if (status === 404) {\n throw new NotFoundError('리소스', url || 'unknown');\n }\n if (status === 409) {\n throw new ConflictError(`충돌 발생: ${message}`);\n }\n\n throw new ApiError(status, message, error.response.data);\n } else if (error.request) {\n logger.error(`[HTTP] No Response: ${error.message}`);\n throw new ApiError(0, `서버로부터 응답이 없습니다: ${error.message}`);\n } else {\n logger.error(`[HTTP] Request Error: ${error.message}`);\n throw new ApiError(0, `요청 설정 중 오류 발생: ${error.message}`);\n }\n }\n );\n\n return client;\n}\n","// TDE Collab 기본 에러 클래스\nexport class TdeCollabError extends Error {\n constructor(message: string) {\n super(message);\n this.name = this.constructor.name;\n Error.captureStackTrace(this, this.constructor);\n }\n}\n\n// API 요청 실패 에러\nexport class ApiError extends TdeCollabError {\n constructor(\n public statusCode: number,\n message: string,\n public data?: any\n ) {\n super(`API 요청 실패 (${statusCode}): ${message}`);\n }\n}\n\n// 인증 실패 에러\nexport class AuthError extends TdeCollabError {\n constructor(message: string = '인증에 실패했습니다') {\n super(message);\n }\n}\n\n// 리소스 미발견 에러\nexport class NotFoundError extends TdeCollabError {\n constructor(resource: string, id: string) {\n super(`${resource} '${id}'를 찾을 수 없습니다`);\n }\n}\n\n// 리소스 충돌 에러\nexport class ConflictError extends TdeCollabError {\n constructor(message: string) {\n super(message);\n }\n}\n","import { AxiosInstance } from 'axios';\nimport { ConfluenceConfig } from '../../common/types.js';\nimport { createHttpClient } from '../../common/http-client.js';\n\nexport function createConfluenceClient(config: ConfluenceConfig): AxiosInstance {\n // Confluence API base context path handling\n // If user provides 'https://example.atlassian.net/wiki', we should use that.\n // API path usually appends '/rest/api/content' etc.\n\n // Ensure config.baseUrl includes '/wiki' if it's a cloud instance, usually users put it in env.\n // But standard is commonly base domain.\n // We'll trust the config for now, but client consumers should append specific endpoints.\n // Typically, Confluence API is at /wiki/rest/api if cloud, or /rest/api if server.\n // Let's assume baseUrl in config points to the root of the instance, e.g. https://site.atlassian.net/wiki\n\n return createHttpClient(config);\n}\n","import MarkdownIt from 'markdown-it';\n\nexport class MarkdownToStorageConverter {\n private md: MarkdownIt;\n\n constructor() {\n this.md = new MarkdownIt({\n html: true,\n linkify: true,\n breaks: true\n });\n\n // Custom renderer for code blocks to Confluence Code Macro\n this.md.renderer.rules.fence = (tokens, idx) => {\n const token = tokens[idx];\n const code = token.content.trim();\n const lang = token.info.trim();\n\n return `<ac:structured-macro ac:name=\"code\" ac:schema-version=\"1\">\n <ac:parameter ac:name=\"language\">${lang || 'text'}</ac:parameter>\n <ac:plain-text-body><![CDATA[${code}]]></ac:plain-text-body>\n</ac:structured-macro>`;\n };\n }\n\n convert(markdown: string): string {\n return this.md.render(markdown);\n }\n}\n","export class StorageToMarkdownConverter {\n convert(storageHtml: string): string {\n // Simple implementation for now.\n // In a real scenario, we would use a proper HTML parser like cheerio or jsdom, \n // or TurndownService.\n // For this phase, let's do basic replacements to demonstrate structure.\n\n let md = storageHtml;\n\n // Remove storage specific wrapper if any (usually not needed if just fragment)\n\n // Code Macro -> Markdown Code Block\n // Structure: <ac:structured-macro ac:name=\"code\">...<ac:parameter ac:name=\"language\">lang</ac:parameter>...<ac:plain-text-body><![CDATA[code]]></ac:plain-text-body></ac:structured-macro>\n\n // Regex based parsing is fragile but sufficient for controlled inputs/MVP.\n // 1. Extract Code Blocks\n md = md.replace(/<ac:structured-macro[^>]*ac:name=\"code\"[^>]*>[\\s\\S]*?<ac:parameter[^>]*ac:name=\"language\">([^<]*)<\\/ac:parameter>[\\s\\S]*?<ac:plain-text-body><!\\[CDATA\\[([\\s\\S]*?)\\]\\]><\\/ac:plain-text-body>[\\s\\S]*?<\\/ac:structured-macro>/g, (match, lang, code) => {\n return `\\`\\`\\`${lang}\\n${code}\\n\\`\\`\\``;\n });\n\n // Fallback for code macro without language param (if possible) - simplifying regex for now.\n\n // Basic HTML to MD mapping\n md = md.replace(/<h1>(.*?)<\\/h1>/g, '# $1\\n');\n md = md.replace(/<h2>(.*?)<\\/h2>/g, '## $1\\n');\n md = md.replace(/<h3>(.*?)<\\/h3>/g, '### $1\\n');\n md = md.replace(/<p>(.*?)<\\/p>/g, '$1\\n\\n');\n md = md.replace(/<strong>(.*?)<\\/strong>/g, '**$1**');\n md = md.replace(/<b>(.*?)<\\/b>/g, '**$1**');\n md = md.replace(/<em>(.*?)<\\/em>/g, '*$1*');\n md = md.replace(/<i>(.*?)<\\/i>/g, '*$1*');\n\n // Lists\n md = md.replace(/<ul>([\\s\\S]*?)<\\/ul>/g, (match, content) => {\n return content.replace(/<li>(.*?)<\\/li>/g, '- $1\\n');\n });\n md = md.replace(/<ol>([\\s\\S]*?)<\\/ol>/g, (match, content) => {\n let i = 1;\n return content.replace(/<li>(.*?)<\\/li>/g, () => `${i++}. $1\\n`);\n });\n\n // Decode HTML entities (basic ones)\n md = md.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&').replace(/&quot;/g, '\"');\n\n return md.trim();\n }\n}\n","import dotenv from 'dotenv';\nimport { ConfluenceConfig, GitlabConfig, JiraConfig } from './types.js';\nimport { logger } from './logger.js';\n\ndotenv.config();\n\n// 환경변수 조회 및 미설정 시 에러 발생\nfunction getEnvOrThrow(key: string, description: string): string {\n const value = process.env[key];\n if (!value) {\n const errorMsg = `환경변수 '${key}'가 설정되지 않았습니다. (${description})`;\n logger.error(errorMsg);\n throw new Error(errorMsg);\n }\n return value;\n}\n\n// Confluence 설정 로드\nexport function loadConfluenceConfig(): ConfluenceConfig {\n const baseUrl = getEnvOrThrow('CONFLUENCE_BASE_URL', 'Confluence 기본 URL');\n // Username은 선택값 (Bearer Token 사용 시 불필요)\n const username = process.env.CONFLUENCE_USERNAME;\n const token = getEnvOrThrow('CONFLUENCE_API_TOKEN', 'Confluence API 토큰');\n\n return {\n baseUrl,\n auth: {\n username,\n token, // API 토큰은 일반적으로 패스워드 필드에 사용되지만, 여기서는 token으로 저장하여 범용성 확보\n },\n };\n}\n\n// JIRA 설정 로드\nexport function loadJiraConfig(): JiraConfig {\n const baseUrl = getEnvOrThrow('JIRA_BASE_URL', 'JIRA 기본 URL');\n const username = getEnvOrThrow('JIRA_USERNAME', 'JIRA 사용자 이메일');\n const token = getEnvOrThrow('JIRA_API_TOKEN', 'JIRA API 토큰');\n\n return {\n baseUrl,\n auth: {\n username,\n token,\n },\n };\n}\n\n// GitLab 설정 로드\nexport function loadGitlabConfig(): GitlabConfig {\n const baseUrl = process.env.GITLAB_BASE_URL || 'https://gitlab.com';\n const token = getEnvOrThrow('GITLAB_PRIVATE_TOKEN', 'GitLab Private Token');\n\n return {\n baseUrl,\n auth: {\n token,\n },\n };\n}\n"],"mappings":"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","names":[]}
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ ConfluenceContentApi,
4
+ ConfluenceSearchApi,
5
+ ConfluenceSpaceApi,
6
+ MarkdownToStorageConverter,
7
+ StorageToMarkdownConverter,
8
+ createConfluenceClient,
9
+ loadConfluenceConfig
10
+ } from "./chunk-N44NISLJ.js";
11
+
12
+ // src/cli.ts
13
+ import { Command } from "commander";
14
+
15
+ // src/confluence/commands/index.ts
16
+ import fs from "fs";
17
+ import path from "path";
18
+ import chalk from "chalk";
19
+ import Table from "cli-table3";
20
+ function registerConfluenceCommands(program2) {
21
+ const confluenceCmd = program2.command("confluence").description("Confluence \uAD00\uB9AC");
22
+ const initClient = () => {
23
+ try {
24
+ const config = loadConfluenceConfig();
25
+ return createConfluenceClient(config);
26
+ } catch (e) {
27
+ console.error(chalk.red(`\uC124\uC815 \uB85C\uB4DC \uC2E4\uD328: ${e.message}`));
28
+ process.exit(1);
29
+ }
30
+ };
31
+ const pageCmd = confluenceCmd.command("page").description("\uD398\uC774\uC9C0 \uAD00\uB9AC");
32
+ pageCmd.command("get <pageId>").description("\uD398\uC774\uC9C0 \uC870\uD68C").option("-r, --raw", "Raw Storage Format \uCD9C\uB825").option("-q, --quiet", "\uBA54\uD0C0\uB370\uC774\uD130 \uC0DD\uB7B5").action(async (pageId, options) => {
33
+ const client = initClient();
34
+ const api = new ConfluenceContentApi(client);
35
+ const storageToMd = new StorageToMarkdownConverter();
36
+ try {
37
+ const page = await api.getPage(pageId);
38
+ if (!options.quiet) {
39
+ console.log(chalk.bold(`Title: ${page.title}`));
40
+ console.log(chalk.gray(`ID: ${page.id}`));
41
+ console.log(`Space: ${page.space?.name} (${page.space?.key})`);
42
+ console.log(`URL: ${page._links?.base}${page._links?.webui}`);
43
+ }
44
+ if (options.raw) {
45
+ if (!options.quiet) console.log(chalk.dim("--- Content (Storage Format) ---"));
46
+ if (page.body?.storage?.value) {
47
+ console.log(page.body.storage.value);
48
+ } else {
49
+ if (!options.quiet) console.log(chalk.yellow("(No content)"));
50
+ }
51
+ } else {
52
+ if (!options.quiet) console.log(chalk.dim("--- Content (Markdown) ---"));
53
+ if (page.body?.storage?.value) {
54
+ console.log(storageToMd.convert(page.body.storage.value));
55
+ } else {
56
+ if (!options.quiet) console.log(chalk.yellow("(No content)"));
57
+ }
58
+ }
59
+ } catch (e) {
60
+ console.error(chalk.red(`Error: ${e.message}`));
61
+ }
62
+ });
63
+ pageCmd.command("create").requiredOption("-s, --space <key>", "\uC2A4\uD398\uC774\uC2A4 \uD0A4").requiredOption("-t, --title <title>", "\uC81C\uBAA9").option("-c, --content <content>", "\uB0B4\uC6A9 (Markdown \uD14D\uC2A4\uD2B8)").option("-f, --file <path>", "\uB0B4\uC6A9 \uD30C\uC77C \uACBD\uB85C (Markdown)").option("-p, --parent <id>", "\uBD80\uBAA8 \uD398\uC774\uC9C0 ID").description("\uD398\uC774\uC9C0 \uC0DD\uC131").action(async (options) => {
64
+ const client = initClient();
65
+ const api = new ConfluenceContentApi(client);
66
+ const mdToStorage = new MarkdownToStorageConverter();
67
+ try {
68
+ let markdownContent = "";
69
+ if (options.file) {
70
+ try {
71
+ const filePath = path.resolve(process.cwd(), options.file);
72
+ markdownContent = fs.readFileSync(filePath, "utf-8");
73
+ } catch (e) {
74
+ console.error(chalk.red(`\uD30C\uC77C \uC77D\uAE30 \uC2E4\uD328: ${e.message}`));
75
+ process.exit(1);
76
+ }
77
+ } else if (options.content) {
78
+ markdownContent = options.content;
79
+ } else {
80
+ console.error(chalk.red("\uC624\uB958: --content \uB610\uB294 --file \uC635\uC158 \uC911 \uD558\uB098\uB294 \uD544\uC218\uC785\uB2C8\uB2E4."));
81
+ process.exit(1);
82
+ }
83
+ const page = await api.createPage({
84
+ spaceKey: options.space,
85
+ title: options.title,
86
+ body: mdToStorage.convert(markdownContent),
87
+ parentId: options.parent
88
+ });
89
+ console.log(chalk.green(`\uD398\uC774\uC9C0 \uC0DD\uC131 \uC644\uB8CC: ${page.title} (ID: ${page.id})`));
90
+ console.log(`URL: ${page._links?.base}${page._links?.webui}`);
91
+ } catch (e) {
92
+ console.error(chalk.red(`\uC0DD\uC131 \uC2E4\uD328: ${e.message}`));
93
+ }
94
+ });
95
+ const spaceCmd = confluenceCmd.command("space").description("\uC2A4\uD398\uC774\uC2A4 \uAD00\uB9AC");
96
+ spaceCmd.command("list").description("\uC2A4\uD398\uC774\uC2A4 \uBAA9\uB85D \uC870\uD68C").action(async () => {
97
+ const client = initClient();
98
+ const api = new ConfluenceSpaceApi(client);
99
+ try {
100
+ const spaces = await api.getSpaces();
101
+ const table = new Table({
102
+ head: ["Key", "Name", "Type", "ID"],
103
+ style: { head: ["cyan"] }
104
+ });
105
+ spaces.forEach((s) => table.push([s.key, s.name, s.type, s.id.toString()]));
106
+ console.log(table.toString());
107
+ } catch (e) {
108
+ console.error(chalk.red(`\uBAA9\uB85D \uC870\uD68C \uC2E4\uD328: ${e.message}`));
109
+ }
110
+ });
111
+ confluenceCmd.command("search <cql>").description("CQL \uAC80\uC0C9").action(async (cql) => {
112
+ const client = initClient();
113
+ const api = new ConfluenceSearchApi(client);
114
+ try {
115
+ const result = await api.searchByCql(cql);
116
+ console.log(chalk.bold(`\uAC80\uC0C9 \uACB0\uACFC: ${result.size}\uAC74 (\uCD1D ${result.totalSize}\uAC74)`));
117
+ const table = new Table({
118
+ head: ["ID", "Title", "Space", "URL"],
119
+ style: { head: ["cyan"] }
120
+ });
121
+ result.results.forEach((p) => table.push([
122
+ p.id,
123
+ p.title,
124
+ p.space?.key || "",
125
+ `${p._links?.base}${p._links?.webui}`
126
+ ]));
127
+ console.log(table.toString());
128
+ } catch (e) {
129
+ console.error(chalk.red(`\uAC80\uC0C9 \uC2E4\uD328: ${e.message}`));
130
+ }
131
+ });
132
+ }
133
+
134
+ // src/cli.ts
135
+ import dotenv from "dotenv";
136
+ dotenv.config();
137
+ var program = new Command();
138
+ program.name("tdecollab").description("TDE Collaboration CLI").version("0.1.0");
139
+ registerConfluenceCommands(program);
140
+ program.command("mcp").description("Run MCP Server").action(async () => {
141
+ const { runServer } = await import("./server-PHNRVUXJ.js");
142
+ await runServer();
143
+ });
144
+ program.parse(process.argv);
145
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts","../src/confluence/commands/index.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { Command } from 'commander';\nimport { registerConfluenceCommands } from './confluence/commands/index.js';\nimport dotenv from 'dotenv';\n\n// Load environment variables\ndotenv.config();\n\nconst program = new Command();\n\nprogram\n .name('tdecollab')\n .description('TDE Collaboration CLI')\n .version('0.1.0');\n\n// Register module commands\nregisterConfluenceCommands(program);\n\nprogram\n .command('mcp')\n .description('Run MCP Server')\n .action(async () => {\n const { runServer } = await import('./mcp/server.js');\n await runServer();\n });\n\nprogram.parse(process.argv);\n","import { Command } from 'commander';\nimport fs from 'fs';\nimport path from 'path';\nimport { ConfluenceContentApi } from '../api/content.js';\nimport { ConfluenceSpaceApi } from '../api/space.js';\nimport { ConfluenceSearchApi } from '../api/search.js';\nimport { ConfluenceLabelApi } from '../api/label.js';\nimport { createConfluenceClient } from '../api/client.js';\nimport { MarkdownToStorageConverter } from '../converters/md-to-storage.js';\nimport { StorageToMarkdownConverter } from '../converters/storage-to-md.js';\nimport { loadConfluenceConfig } from '../../common/config.js';\nimport { logger } from '../../common/logger.js';\nimport chalk from 'chalk';\nimport Table from 'cli-table3';\n\nexport function registerConfluenceCommands(program: Command) {\n const confluenceCmd = program.command('confluence')\n .description('Confluence 관리');\n\n // 공통 초기화 함수\n const initClient = () => {\n try {\n const config = loadConfluenceConfig();\n return createConfluenceClient(config);\n } catch (e: any) {\n console.error(chalk.red(`설정 로드 실패: ${e.message}`));\n process.exit(1);\n }\n };\n\n // --- Page Commands ---\n const pageCmd = confluenceCmd.command('page').description('페이지 관리');\n\n pageCmd.command('get <pageId>')\n .description('페이지 조회')\n .option('-r, --raw', 'Raw Storage Format 출력')\n .option('-q, --quiet', '메타데이터 생략')\n .action(async (pageId, options) => {\n const client = initClient();\n const api = new ConfluenceContentApi(client);\n const storageToMd = new StorageToMarkdownConverter();\n try {\n const page = await api.getPage(pageId);\n\n if (!options.quiet) {\n console.log(chalk.bold(`Title: ${page.title}`));\n console.log(chalk.gray(`ID: ${page.id}`));\n console.log(`Space: ${page.space?.name} (${page.space?.key})`);\n console.log(`URL: ${page._links?.base}${page._links?.webui}`);\n }\n\n if (options.raw) {\n if (!options.quiet) console.log(chalk.dim('--- Content (Storage Format) ---'));\n if (page.body?.storage?.value) {\n console.log(page.body.storage.value);\n } else {\n if (!options.quiet) console.log(chalk.yellow('(No content)'));\n }\n } else {\n if (!options.quiet) console.log(chalk.dim('--- Content (Markdown) ---'));\n if (page.body?.storage?.value) {\n console.log(storageToMd.convert(page.body.storage.value));\n } else {\n if (!options.quiet) console.log(chalk.yellow('(No content)'));\n }\n }\n } catch (e: any) {\n console.error(chalk.red(`Error: ${e.message}`));\n }\n });\n\n pageCmd.command('create')\n .requiredOption('-s, --space <key>', '스페이스 키')\n .requiredOption('-t, --title <title>', '제목')\n .option('-c, --content <content>', '내용 (Markdown 텍스트)')\n .option('-f, --file <path>', '내용 파일 경로 (Markdown)')\n .option('-p, --parent <id>', '부모 페이지 ID')\n .description('페이지 생성')\n .action(async (options) => {\n const client = initClient();\n const api = new ConfluenceContentApi(client);\n const mdToStorage = new MarkdownToStorageConverter();\n\n try {\n let markdownContent = '';\n\n if (options.file) {\n try {\n const filePath = path.resolve(process.cwd(), options.file);\n markdownContent = fs.readFileSync(filePath, 'utf-8');\n } catch (e: any) {\n console.error(chalk.red(`파일 읽기 실패: ${e.message}`));\n process.exit(1);\n }\n } else if (options.content) {\n markdownContent = options.content;\n } else {\n console.error(chalk.red('오류: --content 또는 --file 옵션 중 하나는 필수입니다.'));\n process.exit(1);\n }\n\n const page = await api.createPage({\n spaceKey: options.space,\n title: options.title,\n body: mdToStorage.convert(markdownContent),\n parentId: options.parent\n });\n console.log(chalk.green(`페이지 생성 완료: ${page.title} (ID: ${page.id})`));\n console.log(`URL: ${page._links?.base}${page._links?.webui}`);\n } catch (e: any) {\n console.error(chalk.red(`생성 실패: ${e.message}`));\n }\n });\n\n // --- Space Commands ---\n const spaceCmd = confluenceCmd.command('space').description('스페이스 관리');\n\n spaceCmd.command('list')\n .description('스페이스 목록 조회')\n .action(async () => {\n const client = initClient();\n const api = new ConfluenceSpaceApi(client);\n try {\n const spaces = await api.getSpaces();\n const table = new Table({\n head: ['Key', 'Name', 'Type', 'ID'],\n style: { head: ['cyan'] }\n });\n spaces.forEach(s => table.push([s.key, s.name, s.type, s.id.toString()]));\n console.log(table.toString());\n } catch (e: any) {\n console.error(chalk.red(`목록 조회 실패: ${e.message}`));\n }\n });\n\n // --- Search Commands ---\n confluenceCmd.command('search <cql>')\n .description('CQL 검색')\n .action(async (cql) => {\n const client = initClient();\n const api = new ConfluenceSearchApi(client);\n try {\n const result = await api.searchByCql(cql);\n console.log(chalk.bold(`검색 결과: ${result.size}건 (총 ${result.totalSize}건)`));\n const table = new Table({\n head: ['ID', 'Title', 'Space', 'URL'],\n style: { head: ['cyan'] }\n });\n result.results.forEach(p => table.push([\n p.id,\n p.title,\n p.space?.key || '',\n `${p._links?.base}${p._links?.webui}`\n ]));\n console.log(table.toString());\n } catch (e: any) {\n console.error(chalk.red(`검색 실패: ${e.message}`));\n }\n });\n}\n"],"mappings":"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","names":["program"]}
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ runServer
4
+ } from "./chunk-7JOEVDKT.js";
5
+ import "./chunk-N44NISLJ.js";
6
+
7
+ // src/index.ts
8
+ runServer().catch((error) => {
9
+ console.error("Fatal error in MCP Server entry point:", error);
10
+ process.exit(1);
11
+ });
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { runServer } from './mcp/server.js';\n\n\nrunServer().catch((error) => {\n console.error('Fatal error in MCP Server entry point:', error);\n process.exit(1);\n});\n"],"mappings":";;;;;;;AAIA,UAAU,EAAE,MAAM,CAAC,UAAU;AACzB,UAAQ,MAAM,0CAA0C,KAAK;AAC7D,UAAQ,KAAK,CAAC;AAClB,CAAC;","names":[]}
@@ -0,0 +1,8 @@
1
+ import {
2
+ runServer
3
+ } from "./chunk-7JOEVDKT.js";
4
+ import "./chunk-N44NISLJ.js";
5
+ export {
6
+ runServer
7
+ };
8
+ //# sourceMappingURL=server-PHNRVUXJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "tdecollab",
3
+ "version": "0.1.0",
4
+ "description": "Confluence, Jira, GitLab CLI 및 MCP 통합 도구",
5
+ "keywords": [
6
+ "mcp",
7
+ "mcp-server",
8
+ "confluence",
9
+ "jira",
10
+ "gitlab",
11
+ "cli"
12
+ ],
13
+ "type": "module",
14
+ "bin": {
15
+ "tdecollab": "./dist/cli.js",
16
+ "tdecollab-server": "./dist/index.js"
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "main": "./dist/index.js",
22
+ "scripts": {
23
+ "build": "tsup",
24
+ "dev": "tsx watch src/index.ts",
25
+ "cli": "tsx src/cli.ts",
26
+ "lint": "eslint src/",
27
+ "format": "prettier --write 'src/**/*.ts'",
28
+ "test": "vitest",
29
+ "test:run": "vitest run"
30
+ },
31
+ "dependencies": {
32
+ "@modelcontextprotocol/sdk": "^1.12.1",
33
+ "axios": "^1.7.9",
34
+ "chalk": "^5.4.1",
35
+ "cli-table3": "^0.6.5",
36
+ "commander": "^13.1.0",
37
+ "dotenv": "^16.4.7",
38
+ "markdown-it": "^14.1.0",
39
+ "zod": "^3.24.2"
40
+ },
41
+ "devDependencies": {
42
+ "@types/markdown-it": "^14.1.2",
43
+ "@types/node": "^22.13.4",
44
+ "axios-mock-adapter": "^2.1.0",
45
+ "prettier": "^3.5.2",
46
+ "tsup": "^8.4.0",
47
+ "tsx": "^4.19.2",
48
+ "typescript": "^5.7.3",
49
+ "vitest": "^3.0.5"
50
+ },
51
+ "engines": {
52
+ "node": ">=20.0.0"
53
+ }
54
+ }