nworks 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,8 +25,8 @@ nworks login \
25
25
  --private-key <PATH_TO_KEY> \
26
26
  --bot-id <BOT_ID>
27
27
 
28
- # User OAuth 로그인 (캘린더 등 사용자 API용)
29
- nworks login --user --scope calendar.read
28
+ # User OAuth 로그인 (캘린더, 드라이브 등 사용자 API용)
29
+ nworks login --user --scope calendar.read,file
30
30
 
31
31
  # 인증 확인
32
32
  nworks whoami
@@ -39,6 +39,12 @@ nworks directory members
39
39
 
40
40
  # 오늘 일정 조회
41
41
  nworks calendar list
42
+
43
+ # 드라이브 파일 목록
44
+ nworks drive list
45
+
46
+ # 파일 업로드
47
+ nworks drive upload --file ./report.pdf
42
48
  ```
43
49
 
44
50
  ## CLI Commands
@@ -95,6 +101,36 @@ nworks calendar list --user <userId>
95
101
 
96
102
  > **Note**: 캘린더 API는 User OAuth가 필요합니다. 먼저 `nworks login --user --scope calendar.read`를 실행하세요.
97
103
 
104
+ ### 드라이브 (User OAuth 필요)
105
+
106
+ ```bash
107
+ # 루트 파일/폴더 목록
108
+ nworks drive list
109
+
110
+ # 특정 폴더 내 파일 목록
111
+ nworks drive list --folder <folderId>
112
+
113
+ # 페이지네이션
114
+ nworks drive list --count 50 --cursor <nextCursor>
115
+
116
+ # 파일 업로드 (루트)
117
+ nworks drive upload --file ./report.pdf
118
+
119
+ # 특정 폴더에 업로드
120
+ nworks drive upload --file ./report.pdf --folder <folderId>
121
+
122
+ # 동일 파일명 덮어쓰기
123
+ nworks drive upload --file ./report.pdf --overwrite
124
+
125
+ # 파일 다운로드
126
+ nworks drive download --file-id <fileId>
127
+
128
+ # 다운로드 경로/파일명 지정
129
+ nworks drive download --file-id <fileId> --out ./downloads --name report.pdf
130
+ ```
131
+
132
+ > **Note**: 드라이브 API는 User OAuth가 필요합니다. 먼저 `nworks login --user --scope file`을 실행하세요. 읽기만 필요하면 `file.read` scope로 충분합니다.
133
+
98
134
  ### MCP 서버
99
135
 
100
136
  ```bash
@@ -134,6 +170,8 @@ nworks mcp --list-tools # 등록된 tool 목록
134
170
  | `bot.read` | Bot 채널/구성원 조회 | Service Account | `message members` |
135
171
  | `user.read` | 조직 구성원 조회 | Service Account | `directory members` |
136
172
  | `calendar.read` | 캘린더 일정 조회 | User OAuth | `calendar list` |
173
+ | `file` | 드라이브 읽기/쓰기 | User OAuth | `drive list/upload/download` |
174
+ | `file.read` | 드라이브 읽기 전용 | User OAuth | `drive list/download` |
137
175
 
138
176
  > **Tip**: scope를 변경한 후에는 토큰을 재발급해야 합니다.
139
177
  > ```bash
@@ -186,7 +224,7 @@ NWORKS_VERBOSE=1 # optional, 디버그 로깅
186
224
 
187
225
  - ~~**v0.1** — 메시지, 조직 구성원, MCP 서버~~
188
226
  - ~~**v0.2** — 캘린더 일정 조회 + User OAuth~~
189
- - **v0.3** — 드라이브 파일 조회/업로드 (`nworks drive`)
227
+ - ~~**v0.3** — 드라이브 파일 조회/업로드/다운로드 (`nworks drive`)~~
190
228
  - **v0.4** — 게시판, 메일 (`nworks board`, `nworks mail`)
191
229
 
192
230
  ## License
package/dist/index.js CHANGED
@@ -919,6 +919,51 @@ Content-Type: application/octet-stream\r
919
919
  if (!uploadRes.ok) return handleError(uploadRes);
920
920
  return await uploadRes.json();
921
921
  }
922
+ async function uploadBuffer(fileBuffer, fileName, userId = "me", folderId, overwrite = false, profile = "default") {
923
+ const fileSize = fileBuffer.length;
924
+ const base = `${BASE_URL3}/users/${userId}/drive/files`;
925
+ const createUrl = folderId ? `${base}/${folderId}` : base;
926
+ if (process.env["NWORKS_VERBOSE"] === "1") {
927
+ console.error(`[nworks] POST ${createUrl} (create upload URL for buffer)`);
928
+ }
929
+ const createRes = await authedFetch(
930
+ createUrl,
931
+ {
932
+ method: "POST",
933
+ headers: { "Content-Type": "application/json" },
934
+ body: JSON.stringify({ fileName, fileSize, overwrite })
935
+ },
936
+ profile
937
+ );
938
+ if (!createRes.ok) return handleError(createRes);
939
+ const { uploadUrl } = await createRes.json();
940
+ const boundary = `----nworks${Date.now()}`;
941
+ const header = Buffer.from(
942
+ `--${boundary}\r
943
+ Content-Disposition: form-data; name="Filedata"; filename="${fileName}"\r
944
+ Content-Type: application/octet-stream\r
945
+ \r
946
+ `
947
+ );
948
+ const footer = Buffer.from(`\r
949
+ --${boundary}--\r
950
+ `);
951
+ const body = Buffer.concat([header, fileBuffer, footer]);
952
+ if (process.env["NWORKS_VERBOSE"] === "1") {
953
+ console.error(`[nworks] POST ${uploadUrl} (upload buffer, ${fileSize} bytes)`);
954
+ }
955
+ const uploadRes = await authedFetch(
956
+ uploadUrl,
957
+ {
958
+ method: "POST",
959
+ headers: { "Content-Type": `multipart/form-data; boundary=${boundary}` },
960
+ body
961
+ },
962
+ profile
963
+ );
964
+ if (!uploadRes.ok) return handleError(uploadRes);
965
+ return await uploadRes.json();
966
+ }
922
967
  async function downloadFile(fileId, userId = "me", profile = "default") {
923
968
  const url2 = `${BASE_URL3}/users/${userId}/drive/files/${fileId}/download`;
924
969
  if (process.env["NWORKS_VERBOSE"] === "1") {
@@ -14979,28 +15024,54 @@ function registerTools(server) {
14979
15024
  );
14980
15025
  server.tool(
14981
15026
  "nworks_drive_upload",
14982
- "\uB85C\uCEEC \uD30C\uC77C\uC744 \uB4DC\uB77C\uC774\uBE0C\uC5D0 \uC5C5\uB85C\uB4DC\uD569\uB2C8\uB2E4 (User OAuth file scope \uD544\uC694)",
15027
+ "\uD30C\uC77C\uC744 \uB4DC\uB77C\uC774\uBE0C\uC5D0 \uC5C5\uB85C\uB4DC\uD569\uB2C8\uB2E4 (User OAuth file scope \uD544\uC694). content(base64)\uC640 fileName\uC73C\uB85C \uC804\uB2EC\uD558\uAC70\uB098, filePath\uB85C \uB85C\uCEEC \uD30C\uC77C \uACBD\uB85C\uB97C \uC9C0\uC815\uD569\uB2C8\uB2E4. MCP \uD074\uB77C\uC774\uC5B8\uD2B8\uC5D0\uC11C\uB294 content+fileName \uBC29\uC2DD\uC744 \uAD8C\uC7A5\uD569\uB2C8\uB2E4.",
14983
15028
  {
14984
- filePath: external_exports.string().describe("\uC5C5\uB85C\uB4DC\uD560 \uB85C\uCEEC \uD30C\uC77C \uACBD\uB85C"),
15029
+ content: external_exports.string().optional().describe("\uC5C5\uB85C\uB4DC\uD560 \uD30C\uC77C \uB0B4\uC6A9 (base64 \uC778\uCF54\uB529). filePath \uB300\uC2E0 \uC0AC\uC6A9"),
15030
+ fileName: external_exports.string().optional().describe("\uD30C\uC77C\uBA85 (content \uC0AC\uC6A9 \uC2DC \uD544\uC218)"),
15031
+ filePath: external_exports.string().optional().describe("\uC5C5\uB85C\uB4DC\uD560 \uB85C\uCEEC \uD30C\uC77C \uACBD\uB85C (content \uB300\uC2E0 \uC0AC\uC6A9, \uB85C\uCEEC \uD658\uACBD\uC5D0\uC11C\uB9CC \uB3D9\uC791)"),
14985
15032
  userId: external_exports.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)"),
14986
15033
  folderId: external_exports.string().optional().describe("\uC5C5\uB85C\uB4DC\uD560 \uD3F4\uB354 ID (\uBBF8\uC9C0\uC815 \uC2DC \uB8E8\uD2B8)"),
14987
15034
  overwrite: external_exports.boolean().optional().describe("\uB3D9\uC77C \uD30C\uC77C\uBA85 \uB36E\uC5B4\uC4F0\uAE30 (\uAE30\uBCF8: false)")
14988
15035
  },
14989
- async ({ filePath, userId, folderId, overwrite }) => {
15036
+ async ({ content, fileName, filePath, userId, folderId, overwrite }) => {
14990
15037
  try {
14991
- const result = await uploadFile(
14992
- filePath,
14993
- userId ?? "me",
14994
- folderId,
14995
- overwrite ?? false
14996
- );
15038
+ let result;
15039
+ if (content && fileName) {
15040
+ const buffer = Buffer.from(content, "base64");
15041
+ if (process.env["NWORKS_VERBOSE"] === "1") {
15042
+ console.error(`[nworks] MCP upload: fileName=${fileName}, bufferSize=${buffer.length}`);
15043
+ }
15044
+ result = await uploadBuffer(
15045
+ buffer,
15046
+ fileName,
15047
+ userId ?? "me",
15048
+ folderId,
15049
+ overwrite ?? false
15050
+ );
15051
+ } else if (filePath) {
15052
+ if (process.env["NWORKS_VERBOSE"] === "1") {
15053
+ console.error(`[nworks] MCP upload: filePath=${filePath}`);
15054
+ }
15055
+ result = await uploadFile(
15056
+ filePath,
15057
+ userId ?? "me",
15058
+ folderId,
15059
+ overwrite ?? false
15060
+ );
15061
+ } else {
15062
+ return {
15063
+ content: [{ type: "text", text: "Error: content+fileName \uB610\uB294 filePath \uC911 \uD558\uB098\uB97C \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4. MCP \uD074\uB77C\uC774\uC5B8\uD2B8\uC5D0\uC11C\uB294 \uD30C\uC77C \uB0B4\uC6A9\uC744 base64\uB85C \uC778\uCF54\uB529\uD558\uC5EC content \uD30C\uB77C\uBBF8\uD130\uC5D0 \uC804\uB2EC\uD558\uACE0, fileName\uC5D0 \uD30C\uC77C\uBA85\uC744 \uC9C0\uC815\uD558\uC138\uC694." }],
15064
+ isError: true
15065
+ };
15066
+ }
14997
15067
  return {
14998
15068
  content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }]
14999
15069
  };
15000
15070
  } catch (err) {
15001
15071
  const error48 = err;
15072
+ const detail = process.env["NWORKS_VERBOSE"] === "1" ? ` | stack: ${error48.stack}` : "";
15002
15073
  return {
15003
- content: [{ type: "text", text: `Error: ${error48.message}` }],
15074
+ content: [{ type: "text", text: `Error: ${error48.message}${detail}` }],
15004
15075
  isError: true
15005
15076
  };
15006
15077
  }