nworks-plus 1.3.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/dist/mcp.js ADDED
@@ -0,0 +1,2895 @@
1
+ // src/mcp/server.ts
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+
5
+ // src/mcp/tools.ts
6
+ import { z } from "zod";
7
+
8
+ // src/utils/error.ts
9
+ var AuthError = class extends Error {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = "AuthError";
13
+ }
14
+ };
15
+ var ApiError = class extends Error {
16
+ code;
17
+ statusCode;
18
+ constructor(code, description, statusCode) {
19
+ super(description);
20
+ this.name = "ApiError";
21
+ this.code = code;
22
+ this.statusCode = statusCode;
23
+ }
24
+ };
25
+
26
+ // src/auth/config.ts
27
+ import { readFile, writeFile, mkdir } from "fs/promises";
28
+ import { existsSync } from "fs";
29
+ import { homedir } from "os";
30
+ import { join } from "path";
31
+ function hasServiceAccountCreds(creds) {
32
+ return !!(creds.serviceAccount && creds.privateKeyPath && creds.botId);
33
+ }
34
+ var IS_UNIX = process.platform !== "win32";
35
+ var CONFIG_DIR = join(homedir(), ".config", "nworks");
36
+ var CREDENTIALS_PATH = join(CONFIG_DIR, "credentials.json");
37
+ var TOKEN_PATH = join(CONFIG_DIR, "token.json");
38
+ var USER_TOKEN_PATH = join(CONFIG_DIR, "user-token.json");
39
+ async function ensureConfigDir() {
40
+ if (!existsSync(CONFIG_DIR)) {
41
+ await mkdir(CONFIG_DIR, { recursive: true, ...IS_UNIX && { mode: 448 } });
42
+ }
43
+ }
44
+ function getCredentialsFromEnv() {
45
+ const clientId = process.env["NWORKS_CLIENT_ID"];
46
+ const clientSecret = process.env["NWORKS_CLIENT_SECRET"];
47
+ if (!clientId || !clientSecret) return null;
48
+ return {
49
+ clientId,
50
+ clientSecret,
51
+ serviceAccount: process.env["NWORKS_SERVICE_ACCOUNT"],
52
+ privateKeyPath: process.env["NWORKS_PRIVATE_KEY_PATH"],
53
+ botId: process.env["NWORKS_BOT_ID"],
54
+ domainId: process.env["NWORKS_DOMAIN_ID"]
55
+ };
56
+ }
57
+ async function loadCredentials(profile = "default") {
58
+ const envCreds = getCredentialsFromEnv();
59
+ if (envCreds) return envCreds;
60
+ if (!existsSync(CREDENTIALS_PATH)) {
61
+ throw new AuthError(
62
+ "Not logged in. Run `nworks login` or set environment variables."
63
+ );
64
+ }
65
+ const raw = await readFile(CREDENTIALS_PATH, "utf-8");
66
+ const profiles = JSON.parse(raw);
67
+ const creds = profiles[profile];
68
+ if (!creds) {
69
+ throw new AuthError(`Profile "${profile}" not found in credentials.`);
70
+ }
71
+ return creds;
72
+ }
73
+ async function saveCredentials(creds, profile = "default") {
74
+ await ensureConfigDir();
75
+ let profiles = {};
76
+ if (existsSync(CREDENTIALS_PATH)) {
77
+ const raw = await readFile(CREDENTIALS_PATH, "utf-8");
78
+ profiles = JSON.parse(raw);
79
+ }
80
+ profiles[profile] = creds;
81
+ await writeFile(
82
+ CREDENTIALS_PATH,
83
+ JSON.stringify(profiles, null, 2),
84
+ { encoding: "utf-8", ...IS_UNIX && { mode: 384 } }
85
+ );
86
+ }
87
+ async function loadToken(profile = "default") {
88
+ if (!existsSync(TOKEN_PATH)) return null;
89
+ const raw = await readFile(TOKEN_PATH, "utf-8");
90
+ const tokens = JSON.parse(raw);
91
+ const entry = tokens[profile];
92
+ if (!entry) return null;
93
+ return {
94
+ accessToken: String(entry["accessToken"]),
95
+ expiresAt: Number(entry["expiresAt"])
96
+ };
97
+ }
98
+ async function saveToken(token, profile = "default") {
99
+ await ensureConfigDir();
100
+ let tokens = {};
101
+ if (existsSync(TOKEN_PATH)) {
102
+ const raw = await readFile(TOKEN_PATH, "utf-8");
103
+ tokens = JSON.parse(raw);
104
+ }
105
+ tokens[profile] = token;
106
+ await writeFile(
107
+ TOKEN_PATH,
108
+ JSON.stringify(tokens, null, 2),
109
+ { encoding: "utf-8", ...IS_UNIX && { mode: 384 } }
110
+ );
111
+ }
112
+ async function loadUserToken(profile = "default") {
113
+ if (!existsSync(USER_TOKEN_PATH)) return null;
114
+ const raw = await readFile(USER_TOKEN_PATH, "utf-8");
115
+ const tokens = JSON.parse(raw);
116
+ const entry = tokens[profile];
117
+ if (!entry) return null;
118
+ return {
119
+ accessToken: String(entry["accessToken"]),
120
+ refreshToken: String(entry["refreshToken"]),
121
+ expiresAt: Number(entry["expiresAt"]),
122
+ scope: String(entry["scope"] ?? "")
123
+ };
124
+ }
125
+ async function saveUserToken(token, profile = "default") {
126
+ await ensureConfigDir();
127
+ let tokens = {};
128
+ if (existsSync(USER_TOKEN_PATH)) {
129
+ const raw = await readFile(USER_TOKEN_PATH, "utf-8");
130
+ tokens = JSON.parse(raw);
131
+ }
132
+ tokens[profile] = token;
133
+ await writeFile(
134
+ USER_TOKEN_PATH,
135
+ JSON.stringify(tokens, null, 2),
136
+ { encoding: "utf-8", ...IS_UNIX && { mode: 384 } }
137
+ );
138
+ }
139
+ async function clearCredentials(profile = "default") {
140
+ if (existsSync(CREDENTIALS_PATH)) {
141
+ const raw = await readFile(CREDENTIALS_PATH, "utf-8");
142
+ const profiles = JSON.parse(raw);
143
+ delete profiles[profile];
144
+ await writeFile(
145
+ CREDENTIALS_PATH,
146
+ JSON.stringify(profiles, null, 2),
147
+ { encoding: "utf-8", ...IS_UNIX && { mode: 384 } }
148
+ );
149
+ }
150
+ if (existsSync(TOKEN_PATH)) {
151
+ const raw = await readFile(TOKEN_PATH, "utf-8");
152
+ const tokens = JSON.parse(raw);
153
+ delete tokens[profile];
154
+ await writeFile(
155
+ TOKEN_PATH,
156
+ JSON.stringify(tokens, null, 2),
157
+ { encoding: "utf-8", ...IS_UNIX && { mode: 384 } }
158
+ );
159
+ }
160
+ if (existsSync(USER_TOKEN_PATH)) {
161
+ const raw = await readFile(USER_TOKEN_PATH, "utf-8");
162
+ const tokens = JSON.parse(raw);
163
+ delete tokens[profile];
164
+ await writeFile(
165
+ USER_TOKEN_PATH,
166
+ JSON.stringify(tokens, null, 2),
167
+ { encoding: "utf-8", ...IS_UNIX && { mode: 384 } }
168
+ );
169
+ }
170
+ }
171
+
172
+ // src/auth/jwt.ts
173
+ import { readFile as readFile2, stat } from "fs/promises";
174
+ import jwt from "jsonwebtoken";
175
+ async function createJWT(creds) {
176
+ if (!creds.serviceAccount || !creds.privateKeyPath) {
177
+ throw new AuthError(
178
+ "Service Account credentials required for bot authentication.\nRun `nworks login` with --service-account and --private-key flags."
179
+ );
180
+ }
181
+ const privateKey = await readFile2(creds.privateKeyPath, "utf-8");
182
+ if (process.platform !== "win32") {
183
+ const fileStat = await stat(creds.privateKeyPath);
184
+ const mode = fileStat.mode & 511;
185
+ if (mode & 63) {
186
+ console.error(
187
+ `[nworks] Warning: Private key file has permissions ${mode.toString(8)}. Recommended: 600`
188
+ );
189
+ }
190
+ }
191
+ const now = Math.floor(Date.now() / 1e3);
192
+ const payload = {
193
+ iss: creds.clientId,
194
+ sub: creds.serviceAccount,
195
+ iat: now,
196
+ exp: now + 3600
197
+ };
198
+ return jwt.sign(payload, privateKey, { algorithm: "RS256" });
199
+ }
200
+
201
+ // src/auth/token.ts
202
+ var AUTH_URL = "https://auth.worksmobile.com/oauth2/v2.0/token";
203
+ async function getValidToken(profile = "default") {
204
+ const cached = await loadToken(profile);
205
+ if (cached && cached.expiresAt > Date.now() / 1e3 + 300) {
206
+ return cached.accessToken;
207
+ }
208
+ return refreshToken(profile);
209
+ }
210
+ async function refreshToken(profile = "default") {
211
+ const creds = await loadCredentials(profile);
212
+ const assertion = await createJWT(creds);
213
+ const body = new URLSearchParams({
214
+ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
215
+ assertion,
216
+ client_id: creds.clientId,
217
+ client_secret: creds.clientSecret,
218
+ scope: process.env["NWORKS_SCOPE"] ?? "bot bot.read user.read"
219
+ });
220
+ const res = await fetch(AUTH_URL, {
221
+ method: "POST",
222
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
223
+ body: body.toString()
224
+ });
225
+ if (!res.ok) {
226
+ const text = await res.text();
227
+ const truncated = text.length > 200 ? text.substring(0, 200) + "..." : text;
228
+ throw new AuthError(`Token exchange failed (${res.status}): ${truncated}`);
229
+ }
230
+ const data = await res.json();
231
+ const expiresIn = Number(data.expires_in);
232
+ const tokenData = {
233
+ accessToken: data.access_token,
234
+ expiresAt: Math.floor(Date.now() / 1e3) + expiresIn
235
+ };
236
+ await saveToken(tokenData, profile);
237
+ return tokenData.accessToken;
238
+ }
239
+
240
+ // src/api/client.ts
241
+ var BASE_URL = "https://www.worksapis.com/v1.0";
242
+ var MAX_RETRIES = 3;
243
+ function sleep(ms) {
244
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
245
+ }
246
+ async function request(opts, _retryCount = 0) {
247
+ const { method, path, body, profile = "default" } = opts;
248
+ const token = await getValidToken(profile);
249
+ const url = `${BASE_URL}${path}`;
250
+ if (process.env["NWORKS_VERBOSE"] === "1") {
251
+ console.error(`[nworks] ${method} ${url}`);
252
+ }
253
+ const res = await fetch(url, {
254
+ method,
255
+ headers: {
256
+ Authorization: `Bearer ${token}`,
257
+ "Content-Type": "application/json"
258
+ },
259
+ body: body ? JSON.stringify(body) : void 0
260
+ });
261
+ if (res.status === 401 && _retryCount === 0) {
262
+ await refreshToken(profile);
263
+ return request(opts, _retryCount + 1);
264
+ }
265
+ if (res.status === 429 && _retryCount < MAX_RETRIES) {
266
+ const MAX_RETRY_AFTER = 60;
267
+ const rawRetry = parseInt(res.headers.get("Retry-After") ?? "5", 10);
268
+ const retryAfter = Math.min(Number.isNaN(rawRetry) ? 5 : rawRetry, MAX_RETRY_AFTER);
269
+ await sleep(retryAfter * 1e3);
270
+ return request(opts, _retryCount + 1);
271
+ }
272
+ if (!res.ok) {
273
+ let code = "UNKNOWN";
274
+ let description = `HTTP ${res.status}`;
275
+ try {
276
+ const errorBody = await res.json();
277
+ code = errorBody.code ?? code;
278
+ description = errorBody.description ?? description;
279
+ } catch {
280
+ }
281
+ if (res.status === 401) {
282
+ throw new AuthError(description);
283
+ }
284
+ throw new ApiError(code, description, res.status);
285
+ }
286
+ const text = await res.text();
287
+ if (!text) {
288
+ return void 0;
289
+ }
290
+ return JSON.parse(text);
291
+ }
292
+
293
+ // src/utils/sanitize.ts
294
+ import { basename, resolve, sep } from "path";
295
+ import { randomBytes } from "crypto";
296
+ function sanitizePathSegment(value) {
297
+ if (!value || typeof value !== "string") {
298
+ throw new Error("Path segment must be a non-empty string");
299
+ }
300
+ if (value === "me") return value;
301
+ if (/[/\\]/.test(value) || value.includes("..")) {
302
+ throw new Error(`Invalid path segment: "${value}"`);
303
+ }
304
+ return encodeURIComponent(value);
305
+ }
306
+ function sanitizeFileName(name) {
307
+ if (!name || typeof name !== "string") {
308
+ throw new Error("File name must be a non-empty string");
309
+ }
310
+ const base = basename(name);
311
+ return base.replace(/[\r\n"\\]/g, "_");
312
+ }
313
+ function validateLocalPath(filePath, allowedBase) {
314
+ const resolved = resolve(filePath);
315
+ if (allowedBase) {
316
+ const resolvedBase = resolve(allowedBase);
317
+ if (resolved !== resolvedBase && !resolved.startsWith(resolvedBase + sep)) {
318
+ throw new Error(`Path "${filePath}" escapes the allowed directory "${allowedBase}"`);
319
+ }
320
+ }
321
+ return resolved;
322
+ }
323
+ function validateRedirectUrl(location, allowedHosts) {
324
+ let parsed;
325
+ try {
326
+ parsed = new URL(location);
327
+ } catch {
328
+ throw new Error(`Invalid redirect URL: "${location}"`);
329
+ }
330
+ if (parsed.protocol !== "https:") {
331
+ throw new Error(`Redirect URL must use HTTPS: "${location}"`);
332
+ }
333
+ const isAllowed = allowedHosts.some(
334
+ (host) => parsed.hostname === host || parsed.hostname.endsWith("." + host)
335
+ );
336
+ if (!isAllowed) {
337
+ throw new Error(`Redirect to untrusted host: "${parsed.hostname}"`);
338
+ }
339
+ return location;
340
+ }
341
+ function generateSecureState() {
342
+ return randomBytes(32).toString("hex");
343
+ }
344
+
345
+ // src/api/message.ts
346
+ function buildContent(opts) {
347
+ const type = opts.type ?? "text";
348
+ if (type === "text") {
349
+ return { type: "text", text: opts.text };
350
+ }
351
+ if (type === "button") {
352
+ const actions = opts.actions ? (() => {
353
+ const parsed = JSON.parse(opts.actions);
354
+ if (!Array.isArray(parsed)) throw new Error("actions must be a JSON array");
355
+ return parsed;
356
+ })() : [];
357
+ return {
358
+ type: "button_template",
359
+ contentText: opts.text,
360
+ actions
361
+ };
362
+ }
363
+ if (type === "list") {
364
+ const elements = opts.elements ? (() => {
365
+ const parsed = JSON.parse(opts.elements);
366
+ if (!Array.isArray(parsed)) throw new Error("elements must be a JSON array");
367
+ return parsed;
368
+ })() : [];
369
+ return {
370
+ type: "list_template",
371
+ coverData: { text: opts.text },
372
+ elements
373
+ };
374
+ }
375
+ return { type: "text", text: opts.text };
376
+ }
377
+ async function send(opts) {
378
+ const profile = opts.profile ?? "default";
379
+ const creds = await loadCredentials(profile);
380
+ if (!creds.botId) {
381
+ throw new Error(
382
+ "Bot ID is required for sending messages.\nRun `nworks login` with --bot-id flag to set up bot credentials."
383
+ );
384
+ }
385
+ const content = buildContent(opts);
386
+ const body = { content };
387
+ if (opts.to) {
388
+ const result = await request({
389
+ method: "POST",
390
+ path: `/bots/${sanitizePathSegment(creds.botId)}/users/${sanitizePathSegment(opts.to)}/messages`,
391
+ body,
392
+ profile
393
+ });
394
+ return { success: true, messageId: result?.messageId };
395
+ }
396
+ if (opts.channel) {
397
+ const result = await request({
398
+ method: "POST",
399
+ path: `/bots/${sanitizePathSegment(creds.botId)}/channels/${sanitizePathSegment(opts.channel)}/messages`,
400
+ body,
401
+ profile
402
+ });
403
+ return { success: true, messageId: result?.messageId };
404
+ }
405
+ throw new Error("Either --to (userId) or --channel (channelId) is required.");
406
+ }
407
+ async function listMembers(channelId, profile = "default") {
408
+ const creds = await loadCredentials(profile);
409
+ if (!creds.botId) {
410
+ throw new Error(
411
+ "Bot ID is required for listing channel members.\nRun `nworks login` with --bot-id flag to set up bot credentials."
412
+ );
413
+ }
414
+ const result = await request({
415
+ method: "GET",
416
+ path: `/bots/${sanitizePathSegment(creds.botId)}/channels/${sanitizePathSegment(channelId)}/members`,
417
+ profile
418
+ });
419
+ return { members: result.members ?? [], responseMetaData: result.responseMetaData };
420
+ }
421
+
422
+ // src/api/directory.ts
423
+ async function listUsers(profile = "default") {
424
+ const result = await request({
425
+ method: "GET",
426
+ path: "/users",
427
+ profile
428
+ });
429
+ return { users: result.users ?? [], responseMetaData: result.responseMetaData };
430
+ }
431
+
432
+ // src/api/calendar.ts
433
+ import { randomUUID } from "crypto";
434
+
435
+ // src/auth/oauth-user.ts
436
+ import { createServer } from "http";
437
+ import { randomBytes as randomBytes2 } from "crypto";
438
+ import { URL as URL2 } from "url";
439
+ var AUTH_URL2 = "https://auth.worksmobile.com/oauth2/v2.0/authorize";
440
+ var TOKEN_URL = "https://auth.worksmobile.com/oauth2/v2.0/token";
441
+ var REDIRECT_PORT = 9876;
442
+ var REDIRECT_URI = `http://localhost:${REDIRECT_PORT}/callback`;
443
+ function buildAuthorizeUrl(clientId, scope, state) {
444
+ const params = new URLSearchParams({
445
+ client_id: clientId,
446
+ redirect_uri: REDIRECT_URI,
447
+ scope,
448
+ response_type: "code",
449
+ state
450
+ });
451
+ return `${AUTH_URL2}?${params.toString()}`;
452
+ }
453
+ function waitForAuthCode(expectedState) {
454
+ return new Promise((resolve2, reject) => {
455
+ const timeout = setTimeout(() => {
456
+ server.close();
457
+ reject(new AuthError("OAuth login timed out (120s). Try again."));
458
+ }, 12e4);
459
+ const server = createServer((req, res) => {
460
+ const url = new URL2(req.url ?? "/", `http://localhost:${REDIRECT_PORT}`);
461
+ if (url.pathname !== "/callback") {
462
+ res.writeHead(404);
463
+ res.end("Not found");
464
+ return;
465
+ }
466
+ const code = url.searchParams.get("code");
467
+ const error = url.searchParams.get("error");
468
+ const state = url.searchParams.get("state");
469
+ if (state !== expectedState) {
470
+ res.writeHead(403, { "Content-Type": "text/html; charset=utf-8" });
471
+ res.end("<h2>\uBCF4\uC548 \uC624\uB958</h2><p>state \uBD88\uC77C\uCE58. \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.</p>");
472
+ clearTimeout(timeout);
473
+ server.close();
474
+ reject(new AuthError("OAuth state mismatch \u2014 possible CSRF attack."));
475
+ return;
476
+ }
477
+ if (error) {
478
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
479
+ res.end("<h2>\uB85C\uADF8\uC778 \uC2E4\uD328</h2><p>\uC774 \uCC3D\uC744 \uB2EB\uC544\uB3C4 \uB429\uB2C8\uB2E4.</p>");
480
+ clearTimeout(timeout);
481
+ server.close();
482
+ reject(new AuthError(`OAuth error: ${error}`));
483
+ return;
484
+ }
485
+ if (!code) {
486
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
487
+ res.end("<h2>\uC798\uBABB\uB41C \uC694\uCCAD</h2><p>Authorization code\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.</p>");
488
+ clearTimeout(timeout);
489
+ server.close();
490
+ reject(new AuthError("Missing authorization code in callback."));
491
+ return;
492
+ }
493
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
494
+ res.end("<h2>\uB85C\uADF8\uC778 \uC131\uACF5!</h2><p>\uC774 \uCC3D\uC744 \uB2EB\uACE0 \uD130\uBBF8\uB110\uB85C \uB3CC\uC544\uAC00\uC138\uC694.</p>");
495
+ clearTimeout(timeout);
496
+ server.close();
497
+ resolve2(code);
498
+ });
499
+ server.listen(REDIRECT_PORT, "127.0.0.1", () => {
500
+ });
501
+ server.on("error", (err) => {
502
+ clearTimeout(timeout);
503
+ reject(new AuthError(`Failed to start callback server on port ${REDIRECT_PORT}: ${err.message}`));
504
+ });
505
+ });
506
+ }
507
+ async function exchangeCodeForToken(code, clientId, clientSecret) {
508
+ const body = new URLSearchParams({
509
+ grant_type: "authorization_code",
510
+ code,
511
+ client_id: clientId,
512
+ client_secret: clientSecret,
513
+ redirect_uri: REDIRECT_URI
514
+ });
515
+ const res = await fetch(TOKEN_URL, {
516
+ method: "POST",
517
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
518
+ body: body.toString()
519
+ });
520
+ if (!res.ok) {
521
+ const text = await res.text();
522
+ const truncated = text.length > 200 ? text.substring(0, 200) + "..." : text;
523
+ throw new AuthError(`Token exchange failed (${res.status}): ${truncated}`);
524
+ }
525
+ const data = await res.json();
526
+ return {
527
+ accessToken: data.access_token,
528
+ refreshToken: data.refresh_token,
529
+ expiresAt: Math.floor(Date.now() / 1e3) + Number(data.expires_in),
530
+ scope: data.scope
531
+ };
532
+ }
533
+ async function refreshUserToken(refreshToken2, profile = "default") {
534
+ const creds = await loadCredentials(profile);
535
+ const body = new URLSearchParams({
536
+ grant_type: "refresh_token",
537
+ refresh_token: refreshToken2,
538
+ client_id: creds.clientId,
539
+ client_secret: creds.clientSecret
540
+ });
541
+ const res = await fetch(TOKEN_URL, {
542
+ method: "POST",
543
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
544
+ body: body.toString()
545
+ });
546
+ if (!res.ok) {
547
+ const text = await res.text();
548
+ const truncated = text.length > 200 ? text.substring(0, 200) + "..." : text;
549
+ throw new AuthError(`Token refresh failed (${res.status}): ${truncated}`);
550
+ }
551
+ const data = await res.json();
552
+ return {
553
+ accessToken: data.access_token,
554
+ refreshToken: data.refresh_token ?? refreshToken2,
555
+ expiresAt: Math.floor(Date.now() / 1e3) + Number(data.expires_in),
556
+ scope: data.scope
557
+ };
558
+ }
559
+ function startOAuthCallbackServer(clientId, clientSecret, expectedState) {
560
+ return waitForAuthCode(expectedState).then(
561
+ (code) => exchangeCodeForToken(code, clientId, clientSecret)
562
+ );
563
+ }
564
+ async function revokeToken(token, clientId, clientSecret) {
565
+ try {
566
+ const res = await fetch("https://auth.worksmobile.com/oauth2/v2.0/revoke", {
567
+ method: "POST",
568
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
569
+ body: new URLSearchParams({
570
+ token,
571
+ client_id: clientId,
572
+ client_secret: clientSecret
573
+ }).toString()
574
+ });
575
+ if (!res.ok && process.env["NWORKS_VERBOSE"] === "1") {
576
+ console.error(`[nworks] Token revoke returned ${res.status}`);
577
+ }
578
+ } catch {
579
+ }
580
+ }
581
+
582
+ // src/auth/token-user.ts
583
+ async function getValidUserToken(profile = "default") {
584
+ const cached = await loadUserToken(profile);
585
+ if (!cached) {
586
+ throw new AuthError(
587
+ "User OAuth token not found. Run `nworks login --user` first."
588
+ );
589
+ }
590
+ if (cached.expiresAt > Date.now() / 1e3 + 300) {
591
+ return cached.accessToken;
592
+ }
593
+ const refreshed = await refreshUserToken(cached.refreshToken, profile);
594
+ await saveUserToken(refreshed, profile);
595
+ return refreshed.accessToken;
596
+ }
597
+
598
+ // src/api/calendar.ts
599
+ var BASE_URL2 = "https://www.worksapis.com/v1.0";
600
+ async function authedFetch(url, init, profile) {
601
+ const token = await getValidUserToken(profile);
602
+ const headers = new Headers(init.headers);
603
+ headers.set("Authorization", `Bearer ${token}`);
604
+ return fetch(url, { ...init, headers });
605
+ }
606
+ async function handleError(res) {
607
+ if (res.status === 401) {
608
+ throw new AuthError("User token expired. Run `nworks login --user --scope calendar` again.");
609
+ }
610
+ let code = "UNKNOWN";
611
+ let description = `HTTP ${res.status}`;
612
+ try {
613
+ const body = await res.json();
614
+ code = body.code ?? code;
615
+ description = body.description ?? description;
616
+ } catch {
617
+ }
618
+ throw new ApiError(code, description, res.status);
619
+ }
620
+ function generateEventId() {
621
+ return `event-${randomUUID()}`;
622
+ }
623
+ function normalizeDateTime(dt) {
624
+ const match = dt.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2})([+-]\d{2}:\d{2}|Z)?$/);
625
+ if (match) {
626
+ return `${match[1]}:00${match[2] ?? ""}`;
627
+ }
628
+ return dt;
629
+ }
630
+ async function listEvents(fromDateTime, untilDateTime, userId = "me", profile = "default") {
631
+ const from = encodeURIComponent(fromDateTime);
632
+ const until = encodeURIComponent(untilDateTime);
633
+ const url = `${BASE_URL2}/users/${sanitizePathSegment(userId)}/calendar/events?fromDateTime=${from}&untilDateTime=${until}`;
634
+ if (process.env["NWORKS_VERBOSE"] === "1") {
635
+ console.error(`[nworks] GET ${url}`);
636
+ }
637
+ const res = await authedFetch(url, { method: "GET" }, profile);
638
+ if (!res.ok) return handleError(res);
639
+ const data = await res.json();
640
+ return { events: data.events ?? [] };
641
+ }
642
+ async function listCalendars(userId = "me", profile = "default") {
643
+ const calendars = [];
644
+ let cursor;
645
+ do {
646
+ const params = new URLSearchParams();
647
+ params.set("count", "50");
648
+ if (cursor) params.set("cursor", cursor);
649
+ const url = `${BASE_URL2}/users/${sanitizePathSegment(userId)}/calendar-personals?${params.toString()}`;
650
+ if (process.env["NWORKS_VERBOSE"] === "1") {
651
+ console.error(`[nworks] GET ${url}`);
652
+ }
653
+ const res = await authedFetch(url, { method: "GET" }, profile);
654
+ if (!res.ok) return handleError(res);
655
+ const data = await res.json();
656
+ calendars.push(...data.calendarPersonals ?? []);
657
+ cursor = data.responseMetaData?.nextCursor || void 0;
658
+ } while (cursor);
659
+ return calendars;
660
+ }
661
+ async function listEventsInCalendar(calendarId, fromDateTime, untilDateTime, userId = "me", profile = "default") {
662
+ const from = encodeURIComponent(fromDateTime);
663
+ const until = encodeURIComponent(untilDateTime);
664
+ const url = `${BASE_URL2}/users/${sanitizePathSegment(userId)}/calendars/${sanitizePathSegment(calendarId)}/events?fromDateTime=${from}&untilDateTime=${until}`;
665
+ if (process.env["NWORKS_VERBOSE"] === "1") {
666
+ console.error(`[nworks] GET ${url}`);
667
+ }
668
+ const res = await authedFetch(url, { method: "GET" }, profile);
669
+ if (!res.ok) return handleError(res);
670
+ const data = await res.json();
671
+ return { events: data.events ?? [] };
672
+ }
673
+ async function listAllEvents(fromDateTime, untilDateTime, userId = "me", profile = "default") {
674
+ let calendars;
675
+ try {
676
+ calendars = await listCalendars(userId, profile);
677
+ } catch (err) {
678
+ if (process.env["NWORKS_VERBOSE"] === "1") {
679
+ console.error(`[nworks] listCalendars failed, falling back to default calendar: ${String(err)}`);
680
+ }
681
+ calendars = [];
682
+ }
683
+ if (calendars.length === 0) {
684
+ const result = await listEvents(fromDateTime, untilDateTime, userId, profile);
685
+ const events = result.events.flatMap(
686
+ (e) => e.eventComponents.map((c) => ({ ...c, calendarId: "", calendarName: "" }))
687
+ );
688
+ return { events, calendarCount: 0, skippedCalendars: [] };
689
+ }
690
+ const skippedCalendars = [];
691
+ const perCalendar = await Promise.all(
692
+ calendars.map(async (cal) => {
693
+ try {
694
+ const result = await listEventsInCalendar(
695
+ cal.calendarId,
696
+ fromDateTime,
697
+ untilDateTime,
698
+ userId,
699
+ profile
700
+ );
701
+ return result.events.flatMap(
702
+ (e) => e.eventComponents.map((c) => ({
703
+ ...c,
704
+ calendarId: cal.calendarId,
705
+ calendarName: cal.calendarName
706
+ }))
707
+ );
708
+ } catch (err) {
709
+ skippedCalendars.push(cal.calendarName || cal.calendarId);
710
+ if (process.env["NWORKS_VERBOSE"] === "1") {
711
+ console.error(`[nworks] calendar ${cal.calendarId} events failed: ${String(err)}`);
712
+ }
713
+ return [];
714
+ }
715
+ })
716
+ );
717
+ return { events: perCalendar.flat(), calendarCount: calendars.length, skippedCalendars };
718
+ }
719
+ async function createEvent(opts) {
720
+ const userId = opts.userId ?? "me";
721
+ const profile = opts.profile ?? "default";
722
+ const timeZone = opts.timeZone ?? "Asia/Seoul";
723
+ const eventId = generateEventId();
724
+ const eventComponent = {
725
+ eventId,
726
+ summary: opts.summary,
727
+ start: { dateTime: normalizeDateTime(opts.start), timeZone },
728
+ end: { dateTime: normalizeDateTime(opts.end), timeZone }
729
+ };
730
+ if (opts.description) eventComponent.description = opts.description;
731
+ if (opts.location) eventComponent.location = opts.location;
732
+ if (opts.transparency) eventComponent.transparency = opts.transparency;
733
+ if (opts.visibility) eventComponent.visibility = opts.visibility;
734
+ if (opts.attendees) {
735
+ eventComponent.attendees = opts.attendees.map((a) => ({
736
+ email: a.email,
737
+ displayName: a.displayName ?? "",
738
+ partstat: "NEEDS-ACTION",
739
+ isOptional: false,
740
+ isResource: false
741
+ }));
742
+ }
743
+ const body = {
744
+ eventComponents: [eventComponent],
745
+ sendNotification: opts.sendNotification ?? false
746
+ };
747
+ const url = `${BASE_URL2}/users/${sanitizePathSegment(userId)}/calendar/events`;
748
+ if (process.env["NWORKS_VERBOSE"] === "1") {
749
+ console.error(`[nworks] POST ${url}`);
750
+ console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
751
+ }
752
+ const res = await authedFetch(
753
+ url,
754
+ {
755
+ method: "POST",
756
+ headers: { "Content-Type": "application/json" },
757
+ body: JSON.stringify(body)
758
+ },
759
+ profile
760
+ );
761
+ if (res.status === 201) {
762
+ return await res.json();
763
+ }
764
+ if (!res.ok) return handleError(res);
765
+ return await res.json();
766
+ }
767
+ async function getEvent(eventId, userId = "me", profile = "default") {
768
+ const url = `${BASE_URL2}/users/${sanitizePathSegment(userId)}/calendar/events/${sanitizePathSegment(eventId)}`;
769
+ if (process.env["NWORKS_VERBOSE"] === "1") {
770
+ console.error(`[nworks] GET ${url}`);
771
+ }
772
+ const res = await authedFetch(url, { method: "GET" }, profile);
773
+ if (!res.ok) return handleError(res);
774
+ const data = await res.json();
775
+ const event = data.eventComponents[0];
776
+ if (!event) throw new ApiError("NOT_FOUND", "Event not found", 404);
777
+ return event;
778
+ }
779
+ async function updateEvent(opts) {
780
+ const userId = opts.userId ?? "me";
781
+ const profile = opts.profile ?? "default";
782
+ const timeZone = opts.timeZone ?? "Asia/Seoul";
783
+ const existing = await getEvent(opts.eventId, userId, profile);
784
+ const eventComponent = {
785
+ eventId: opts.eventId,
786
+ summary: opts.summary ?? existing.summary,
787
+ start: opts.start ? { dateTime: normalizeDateTime(opts.start), timeZone } : existing.start,
788
+ end: opts.end ? { dateTime: normalizeDateTime(opts.end), timeZone } : existing.end
789
+ };
790
+ if (opts.description !== void 0) eventComponent.description = opts.description;
791
+ else if (existing.description) eventComponent.description = existing.description;
792
+ if (opts.location !== void 0) eventComponent.location = opts.location;
793
+ else if (existing.location) eventComponent.location = existing.location;
794
+ if (opts.transparency !== void 0) eventComponent.transparency = opts.transparency;
795
+ if (opts.visibility !== void 0) eventComponent.visibility = opts.visibility;
796
+ if (opts.attendees !== void 0) {
797
+ eventComponent.attendees = opts.attendees.map((a) => ({
798
+ email: a.email,
799
+ displayName: a.displayName ?? "",
800
+ partstat: "NEEDS-ACTION",
801
+ isOptional: false,
802
+ isResource: false
803
+ }));
804
+ } else if (existing.attendees) {
805
+ eventComponent.attendees = existing.attendees;
806
+ }
807
+ const body = {
808
+ eventComponents: [eventComponent],
809
+ sendNotification: opts.sendNotification ?? false
810
+ };
811
+ const url = `${BASE_URL2}/users/${sanitizePathSegment(userId)}/calendar/events/${sanitizePathSegment(opts.eventId)}`;
812
+ if (process.env["NWORKS_VERBOSE"] === "1") {
813
+ console.error(`[nworks] PUT ${url}`);
814
+ console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
815
+ }
816
+ const res = await authedFetch(
817
+ url,
818
+ {
819
+ method: "PUT",
820
+ headers: { "Content-Type": "application/json" },
821
+ body: JSON.stringify(body)
822
+ },
823
+ profile
824
+ );
825
+ if (!res.ok) return handleError(res);
826
+ }
827
+ async function deleteEvent(eventId, userId = "me", sendNotification = false, profile = "default") {
828
+ const params = new URLSearchParams();
829
+ params.set("sendNotification", String(sendNotification));
830
+ const url = `${BASE_URL2}/users/${sanitizePathSegment(userId)}/calendar/events/${sanitizePathSegment(eventId)}?${params.toString()}`;
831
+ if (process.env["NWORKS_VERBOSE"] === "1") {
832
+ console.error(`[nworks] DELETE ${url}`);
833
+ }
834
+ const res = await authedFetch(url, { method: "DELETE" }, profile);
835
+ if (res.status === 204) return;
836
+ if (!res.ok) return handleError(res);
837
+ }
838
+
839
+ // src/api/drive.ts
840
+ import { readFile as readFile3, stat as stat2 } from "fs/promises";
841
+ import { basename as basename2 } from "path";
842
+ var BASE_URL3 = "https://www.worksapis.com/v1.0";
843
+ var MAX_UPLOAD_SIZE = 100 * 1024 * 1024;
844
+ var ALLOWED_HOSTS = [
845
+ "storage.worksmobile.com",
846
+ "www.worksapis.com",
847
+ "worksapis.com"
848
+ ];
849
+ async function authedFetch2(url, init, profile) {
850
+ const token = await getValidUserToken(profile);
851
+ const headers = new Headers(init.headers);
852
+ headers.set("Authorization", `Bearer ${token}`);
853
+ return fetch(url, { ...init, headers });
854
+ }
855
+ async function handleError2(res) {
856
+ if (res.status === 401) {
857
+ throw new AuthError("User token expired. Run `nworks login --user --scope file` again.");
858
+ }
859
+ let code = "UNKNOWN";
860
+ let description = `HTTP ${res.status}`;
861
+ try {
862
+ const body = await res.json();
863
+ code = body.code ?? code;
864
+ description = body.description ?? description;
865
+ } catch {
866
+ }
867
+ throw new ApiError(code, description, res.status);
868
+ }
869
+ async function listFiles(userId = "me", folderId, count = 20, cursor, profile = "default") {
870
+ const base = `${BASE_URL3}/users/${sanitizePathSegment(userId)}/drive/files`;
871
+ const path = folderId ? `${base}/${sanitizePathSegment(folderId)}/children` : base;
872
+ const params = new URLSearchParams();
873
+ params.set("count", String(count));
874
+ if (cursor) params.set("cursor", cursor);
875
+ const url = `${path}?${params.toString()}`;
876
+ if (process.env["NWORKS_VERBOSE"] === "1") {
877
+ console.error(`[nworks] GET ${url}`);
878
+ }
879
+ const res = await authedFetch2(url, { method: "GET" }, profile);
880
+ if (!res.ok) return handleError2(res);
881
+ const data = await res.json();
882
+ return { files: data.files ?? [], responseMetaData: data.responseMetaData };
883
+ }
884
+ async function uploadFile(localPath, userId = "me", folderId, overwrite = false, profile = "default") {
885
+ const fileName = basename2(localPath);
886
+ const safeName = sanitizeFileName(fileName);
887
+ const fileStat = await stat2(localPath);
888
+ const fileSize = fileStat.size;
889
+ if (fileSize > MAX_UPLOAD_SIZE) {
890
+ throw new ApiError("FILE_TOO_LARGE", `File size (${fileSize} bytes) exceeds maximum allowed (${MAX_UPLOAD_SIZE} bytes)`, 413);
891
+ }
892
+ const base = `${BASE_URL3}/users/${sanitizePathSegment(userId)}/drive/files`;
893
+ const createUrl = folderId ? `${base}/${sanitizePathSegment(folderId)}` : base;
894
+ if (process.env["NWORKS_VERBOSE"] === "1") {
895
+ console.error(`[nworks] POST ${createUrl} (create upload URL)`);
896
+ }
897
+ const createRes = await authedFetch2(
898
+ createUrl,
899
+ {
900
+ method: "POST",
901
+ headers: { "Content-Type": "application/json" },
902
+ body: JSON.stringify({ fileName, fileSize, overwrite })
903
+ },
904
+ profile
905
+ );
906
+ if (!createRes.ok) return handleError2(createRes);
907
+ const { uploadUrl } = await createRes.json();
908
+ validateRedirectUrl(uploadUrl, ALLOWED_HOSTS);
909
+ const fileBuffer = await readFile3(localPath);
910
+ const boundary = `----nworks${Date.now()}`;
911
+ const header = Buffer.from(
912
+ `--${boundary}\r
913
+ Content-Disposition: form-data; name="Filedata"; filename="${safeName}"\r
914
+ Content-Type: application/octet-stream\r
915
+ \r
916
+ `
917
+ );
918
+ const footer = Buffer.from(`\r
919
+ --${boundary}--\r
920
+ `);
921
+ const body = Buffer.concat([header, fileBuffer, footer]);
922
+ if (process.env["NWORKS_VERBOSE"] === "1") {
923
+ console.error(`[nworks] POST ${uploadUrl} (upload content, ${fileSize} bytes)`);
924
+ }
925
+ const uploadRes = await authedFetch2(
926
+ uploadUrl,
927
+ {
928
+ method: "POST",
929
+ headers: { "Content-Type": `multipart/form-data; boundary=${boundary}` },
930
+ body
931
+ },
932
+ profile
933
+ );
934
+ if (!uploadRes.ok) return handleError2(uploadRes);
935
+ return await uploadRes.json();
936
+ }
937
+ async function uploadBuffer(fileBuffer, fileName, userId = "me", folderId, overwrite = false, profile = "default") {
938
+ const fileSize = fileBuffer.length;
939
+ if (fileSize > MAX_UPLOAD_SIZE) {
940
+ throw new ApiError("FILE_TOO_LARGE", `File size (${fileSize} bytes) exceeds maximum allowed (${MAX_UPLOAD_SIZE} bytes)`, 413);
941
+ }
942
+ const base = `${BASE_URL3}/users/${sanitizePathSegment(userId)}/drive/files`;
943
+ const createUrl = folderId ? `${base}/${sanitizePathSegment(folderId)}` : base;
944
+ if (process.env["NWORKS_VERBOSE"] === "1") {
945
+ console.error(`[nworks] POST ${createUrl} (create upload URL for buffer)`);
946
+ }
947
+ const createRes = await authedFetch2(
948
+ createUrl,
949
+ {
950
+ method: "POST",
951
+ headers: { "Content-Type": "application/json" },
952
+ body: JSON.stringify({ fileName, fileSize, overwrite })
953
+ },
954
+ profile
955
+ );
956
+ if (!createRes.ok) return handleError2(createRes);
957
+ const { uploadUrl } = await createRes.json();
958
+ validateRedirectUrl(uploadUrl, ALLOWED_HOSTS);
959
+ const boundary = `----nworks${Date.now()}`;
960
+ const header = Buffer.from(
961
+ `--${boundary}\r
962
+ Content-Disposition: form-data; name="Filedata"; filename="${sanitizeFileName(fileName)}"\r
963
+ Content-Type: application/octet-stream\r
964
+ \r
965
+ `
966
+ );
967
+ const footer = Buffer.from(`\r
968
+ --${boundary}--\r
969
+ `);
970
+ const body = Buffer.concat([header, fileBuffer, footer]);
971
+ if (process.env["NWORKS_VERBOSE"] === "1") {
972
+ console.error(`[nworks] POST ${uploadUrl} (upload buffer, ${fileSize} bytes)`);
973
+ }
974
+ const uploadRes = await authedFetch2(
975
+ uploadUrl,
976
+ {
977
+ method: "POST",
978
+ headers: { "Content-Type": `multipart/form-data; boundary=${boundary}` },
979
+ body
980
+ },
981
+ profile
982
+ );
983
+ if (!uploadRes.ok) return handleError2(uploadRes);
984
+ return await uploadRes.json();
985
+ }
986
+ async function downloadFile(fileId, userId = "me", profile = "default") {
987
+ const url = `${BASE_URL3}/users/${sanitizePathSegment(userId)}/drive/files/${sanitizePathSegment(fileId)}/download`;
988
+ if (process.env["NWORKS_VERBOSE"] === "1") {
989
+ console.error(`[nworks] GET ${url} (get download URL)`);
990
+ }
991
+ const redirectRes = await authedFetch2(
992
+ url,
993
+ { method: "GET", redirect: "manual" },
994
+ profile
995
+ );
996
+ if (redirectRes.status === 401) {
997
+ throw new AuthError("User token expired. Run `nworks login --user --scope file` again.");
998
+ }
999
+ const location = redirectRes.headers.get("location");
1000
+ if (!location) {
1001
+ if (!redirectRes.ok) return handleError2(redirectRes);
1002
+ throw new ApiError("NO_REDIRECT", "No download URL returned", redirectRes.status);
1003
+ }
1004
+ const safeLocation = validateRedirectUrl(location, ALLOWED_HOSTS);
1005
+ if (process.env["NWORKS_VERBOSE"] === "1") {
1006
+ console.error(`[nworks] GET ${safeLocation} (download content)`);
1007
+ }
1008
+ const downloadRes = await fetch(safeLocation, { method: "GET" });
1009
+ if (!downloadRes.ok) return handleError2(downloadRes);
1010
+ const arrayBuffer = await downloadRes.arrayBuffer();
1011
+ const buffer = Buffer.from(arrayBuffer);
1012
+ const disposition = downloadRes.headers.get("content-disposition");
1013
+ let fileName;
1014
+ if (disposition) {
1015
+ const match = disposition.match(/filename\*?=(?:UTF-8''|"?)([^";]+)/i);
1016
+ if (match?.[1]) {
1017
+ fileName = decodeURIComponent(match[1].replace(/"/g, ""));
1018
+ }
1019
+ }
1020
+ return { buffer, fileName };
1021
+ }
1022
+
1023
+ // src/api/mail.ts
1024
+ var BASE_URL4 = "https://www.worksapis.com/v1.0";
1025
+ async function authedFetch3(url, init, profile) {
1026
+ const token = await getValidUserToken(profile);
1027
+ const headers = new Headers(init.headers);
1028
+ headers.set("Authorization", `Bearer ${token}`);
1029
+ return fetch(url, { ...init, headers });
1030
+ }
1031
+ async function handleError3(res) {
1032
+ if (res.status === 401) {
1033
+ throw new AuthError("User token expired. Run `nworks login --user --scope mail` again.");
1034
+ }
1035
+ let code = "UNKNOWN";
1036
+ let description = `HTTP ${res.status}`;
1037
+ try {
1038
+ const body = await res.json();
1039
+ code = body.code ?? code;
1040
+ description = body.description ?? description;
1041
+ } catch {
1042
+ }
1043
+ throw new ApiError(code, description, res.status);
1044
+ }
1045
+ async function sendMail(opts) {
1046
+ const userId = opts.userId ?? "me";
1047
+ const profile = opts.profile ?? "default";
1048
+ const url = `${BASE_URL4}/users/${sanitizePathSegment(userId)}/mail`;
1049
+ if (process.env["NWORKS_VERBOSE"] === "1") {
1050
+ console.error(`[nworks] POST ${url}`);
1051
+ }
1052
+ const body = {
1053
+ to: opts.to,
1054
+ subject: opts.subject
1055
+ };
1056
+ if (opts.body !== void 0) body.body = opts.body;
1057
+ if (opts.cc) body.cc = opts.cc;
1058
+ if (opts.bcc) body.bcc = opts.bcc;
1059
+ if (opts.contentType) body.contentType = opts.contentType;
1060
+ const res = await authedFetch3(
1061
+ url,
1062
+ {
1063
+ method: "POST",
1064
+ headers: { "Content-Type": "application/json" },
1065
+ body: JSON.stringify(body)
1066
+ },
1067
+ profile
1068
+ );
1069
+ if (res.status === 202) return;
1070
+ if (!res.ok) return handleError3(res);
1071
+ }
1072
+ async function listMails(folderId = 0, userId = "me", count = 30, cursor, isUnread, profile = "default") {
1073
+ const params = new URLSearchParams();
1074
+ params.set("count", String(count));
1075
+ if (cursor) params.set("cursor", cursor);
1076
+ if (isUnread) params.set("isUnread", "true");
1077
+ const url = `${BASE_URL4}/users/${sanitizePathSegment(userId)}/mail/mailfolders/${sanitizePathSegment(String(folderId))}/children?${params.toString()}`;
1078
+ if (process.env["NWORKS_VERBOSE"] === "1") {
1079
+ console.error(`[nworks] GET ${url}`);
1080
+ }
1081
+ const res = await authedFetch3(url, { method: "GET" }, profile);
1082
+ if (!res.ok) return handleError3(res);
1083
+ const data = await res.json();
1084
+ return {
1085
+ mails: data.mails ?? [],
1086
+ unreadCount: data.unreadCount,
1087
+ folderName: data.folderName,
1088
+ totalCount: data.totalCount,
1089
+ responseMetaData: data.responseMetaData
1090
+ };
1091
+ }
1092
+ async function readMail(mailId, userId = "me", profile = "default") {
1093
+ const url = `${BASE_URL4}/users/${sanitizePathSegment(userId)}/mail/${sanitizePathSegment(String(mailId))}`;
1094
+ if (process.env["NWORKS_VERBOSE"] === "1") {
1095
+ console.error(`[nworks] GET ${url}`);
1096
+ }
1097
+ const res = await authedFetch3(url, { method: "GET" }, profile);
1098
+ if (!res.ok) return handleError3(res);
1099
+ return await res.json();
1100
+ }
1101
+ async function downloadAttachment(mailId, attachmentId, userId = "me", profile = "default") {
1102
+ const url = `${BASE_URL4}/users/${sanitizePathSegment(userId)}/mail/${sanitizePathSegment(String(mailId))}/attachments/${sanitizePathSegment(String(attachmentId))}`;
1103
+ if (process.env["NWORKS_VERBOSE"] === "1") {
1104
+ console.error(`[nworks] GET ${url}`);
1105
+ }
1106
+ const res = await authedFetch3(url, { method: "GET" }, profile);
1107
+ if (!res.ok) return handleError3(res);
1108
+ const data = await res.json();
1109
+ if (!data.data) {
1110
+ throw new ApiError("NO_ATTACHMENT_DATA", "Attachment response had no data", res.status);
1111
+ }
1112
+ return {
1113
+ filename: data.filename ?? `attachment-${attachmentId}`,
1114
+ buffer: Buffer.from(data.data, "base64")
1115
+ };
1116
+ }
1117
+
1118
+ // src/api/task.ts
1119
+ var BASE_URL5 = "https://www.worksapis.com/v1.0";
1120
+ async function authedFetch4(url, init, profile) {
1121
+ const token = await getValidUserToken(profile);
1122
+ const headers = new Headers(init.headers);
1123
+ headers.set("Authorization", `Bearer ${token}`);
1124
+ return fetch(url, { ...init, headers });
1125
+ }
1126
+ async function handleError4(res) {
1127
+ if (res.status === 401) {
1128
+ throw new AuthError("User token expired. Run `nworks login --user --scope task` again.");
1129
+ }
1130
+ let code = "UNKNOWN";
1131
+ let description = `HTTP ${res.status}`;
1132
+ try {
1133
+ const body = await res.json();
1134
+ code = body.code ?? code;
1135
+ description = body.description ?? description;
1136
+ } catch {
1137
+ }
1138
+ throw new ApiError(code, description, res.status);
1139
+ }
1140
+ async function resolveUserId(userId, profile) {
1141
+ if (userId !== "me") return userId;
1142
+ const url = `${BASE_URL5}/users/${sanitizePathSegment(userId)}`;
1143
+ const res = await authedFetch4(url, { method: "GET" }, profile);
1144
+ if (!res.ok) return handleError4(res);
1145
+ const data = await res.json();
1146
+ if (process.env["NWORKS_VERBOSE"] === "1") {
1147
+ console.error(`[nworks] Resolved "me" \u2192 ${data.userId}`);
1148
+ }
1149
+ return data.userId;
1150
+ }
1151
+ async function listTasks(categoryId = "default", userId = "me", count = 50, cursor, status = "ALL", profile = "default") {
1152
+ const params = new URLSearchParams();
1153
+ params.set("categoryId", categoryId);
1154
+ params.set("count", String(count));
1155
+ params.set("status", status);
1156
+ if (cursor) params.set("cursor", cursor);
1157
+ const url = `${BASE_URL5}/users/${sanitizePathSegment(userId)}/tasks?${params.toString()}`;
1158
+ if (process.env["NWORKS_VERBOSE"] === "1") {
1159
+ console.error(`[nworks] GET ${url}`);
1160
+ }
1161
+ const res = await authedFetch4(url, { method: "GET" }, profile);
1162
+ if (!res.ok) return handleError4(res);
1163
+ const data = await res.json();
1164
+ return { tasks: data.tasks ?? [], responseMetaData: data.responseMetaData };
1165
+ }
1166
+ async function createTask(opts) {
1167
+ const userId = opts.userId ?? "me";
1168
+ const profile = opts.profile ?? "default";
1169
+ const resolvedUserId = await resolveUserId(userId, profile);
1170
+ const assignorId = opts.assignorId ?? resolvedUserId;
1171
+ const assigneeIds = opts.assigneeIds ?? [resolvedUserId];
1172
+ const body = {
1173
+ assignorId,
1174
+ assignees: assigneeIds.map((id) => ({ assigneeId: id, status: "TODO" })),
1175
+ title: opts.title,
1176
+ content: opts.content ?? "",
1177
+ completionCondition: "ANY_ONE"
1178
+ };
1179
+ if (opts.dueDate) body.dueDate = opts.dueDate;
1180
+ if (opts.categoryId) body.categoryId = opts.categoryId;
1181
+ const url = `${BASE_URL5}/users/${sanitizePathSegment(userId)}/tasks`;
1182
+ if (process.env["NWORKS_VERBOSE"] === "1") {
1183
+ console.error(`[nworks] POST ${url}`);
1184
+ console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
1185
+ }
1186
+ const res = await authedFetch4(
1187
+ url,
1188
+ {
1189
+ method: "POST",
1190
+ headers: { "Content-Type": "application/json" },
1191
+ body: JSON.stringify(body)
1192
+ },
1193
+ profile
1194
+ );
1195
+ if (res.status === 201) {
1196
+ return await res.json();
1197
+ }
1198
+ if (!res.ok) return handleError4(res);
1199
+ return await res.json();
1200
+ }
1201
+ async function updateTask(opts) {
1202
+ const profile = opts.profile ?? "default";
1203
+ const body = {};
1204
+ if (opts.title !== void 0) body.title = opts.title;
1205
+ if (opts.content !== void 0) body.content = opts.content;
1206
+ if (opts.dueDate !== void 0) body.dueDate = opts.dueDate;
1207
+ const url = `${BASE_URL5}/tasks/${sanitizePathSegment(opts.taskId)}`;
1208
+ if (process.env["NWORKS_VERBOSE"] === "1") {
1209
+ console.error(`[nworks] PATCH ${url}`);
1210
+ }
1211
+ const res = await authedFetch4(
1212
+ url,
1213
+ {
1214
+ method: "PATCH",
1215
+ headers: { "Content-Type": "application/json" },
1216
+ body: JSON.stringify(body)
1217
+ },
1218
+ profile
1219
+ );
1220
+ if (!res.ok) return handleError4(res);
1221
+ return await res.json();
1222
+ }
1223
+ async function completeTask(taskId, profile = "default") {
1224
+ const url = `${BASE_URL5}/tasks/${sanitizePathSegment(taskId)}/complete`;
1225
+ if (process.env["NWORKS_VERBOSE"] === "1") {
1226
+ console.error(`[nworks] POST ${url}`);
1227
+ }
1228
+ const res = await authedFetch4(
1229
+ url,
1230
+ { method: "POST" },
1231
+ profile
1232
+ );
1233
+ if (res.status === 204) return;
1234
+ if (!res.ok) return handleError4(res);
1235
+ }
1236
+ async function incompleteTask(taskId, profile = "default") {
1237
+ const url = `${BASE_URL5}/tasks/${sanitizePathSegment(taskId)}/incomplete`;
1238
+ if (process.env["NWORKS_VERBOSE"] === "1") {
1239
+ console.error(`[nworks] POST ${url}`);
1240
+ }
1241
+ const res = await authedFetch4(
1242
+ url,
1243
+ { method: "POST" },
1244
+ profile
1245
+ );
1246
+ if (res.status === 204) return;
1247
+ if (!res.ok) return handleError4(res);
1248
+ }
1249
+ async function deleteTask(taskId, profile = "default") {
1250
+ const url = `${BASE_URL5}/tasks/${sanitizePathSegment(taskId)}`;
1251
+ if (process.env["NWORKS_VERBOSE"] === "1") {
1252
+ console.error(`[nworks] DELETE ${url}`);
1253
+ }
1254
+ const res = await authedFetch4(
1255
+ url,
1256
+ { method: "DELETE" },
1257
+ profile
1258
+ );
1259
+ if (res.status === 204) return;
1260
+ if (!res.ok) return handleError4(res);
1261
+ }
1262
+
1263
+ // src/api/board.ts
1264
+ var BASE_URL6 = "https://www.worksapis.com/v1.0";
1265
+ async function authedFetch5(url, init, profile) {
1266
+ const token = await getValidUserToken(profile);
1267
+ const headers = new Headers(init.headers);
1268
+ headers.set("Authorization", `Bearer ${token}`);
1269
+ return fetch(url, { ...init, headers });
1270
+ }
1271
+ async function handleError5(res) {
1272
+ if (res.status === 401) {
1273
+ throw new AuthError("User token expired. Run `nworks login --user --scope board` again.");
1274
+ }
1275
+ let code = "UNKNOWN";
1276
+ let description = `HTTP ${res.status}`;
1277
+ try {
1278
+ const body = await res.json();
1279
+ code = body.code ?? code;
1280
+ description = body.description ?? description;
1281
+ } catch {
1282
+ }
1283
+ throw new ApiError(code, description, res.status);
1284
+ }
1285
+ function safeParseJson(text) {
1286
+ const safe = text.replace(
1287
+ /"((?:board|post|domain|user)Id)"\s*:\s*(\d{16,})/g,
1288
+ '"$1":"$2"'
1289
+ );
1290
+ return JSON.parse(safe);
1291
+ }
1292
+ async function listBoards(count = 20, cursor, profile = "default") {
1293
+ const params = new URLSearchParams();
1294
+ params.set("count", String(count));
1295
+ if (cursor) params.set("cursor", cursor);
1296
+ const url = `${BASE_URL6}/boards?${params.toString()}`;
1297
+ if (process.env["NWORKS_VERBOSE"] === "1") {
1298
+ console.error(`[nworks] GET ${url}`);
1299
+ }
1300
+ const res = await authedFetch5(url, { method: "GET" }, profile);
1301
+ if (!res.ok) return handleError5(res);
1302
+ const text = await res.text();
1303
+ if (process.env["NWORKS_VERBOSE"] === "1") {
1304
+ console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
1305
+ }
1306
+ const data = safeParseJson(text);
1307
+ return { boards: data.boards ?? [], responseMetaData: data.responseMetaData };
1308
+ }
1309
+ async function listPosts(boardId, count = 20, cursor, profile = "default") {
1310
+ const params = new URLSearchParams();
1311
+ params.set("count", String(count));
1312
+ if (cursor) params.set("cursor", cursor);
1313
+ const url = `${BASE_URL6}/boards/${sanitizePathSegment(boardId)}/posts?${params.toString()}`;
1314
+ if (process.env["NWORKS_VERBOSE"] === "1") {
1315
+ console.error(`[nworks] GET ${url}`);
1316
+ }
1317
+ const res = await authedFetch5(url, { method: "GET" }, profile);
1318
+ if (!res.ok) return handleError5(res);
1319
+ const text = await res.text();
1320
+ if (process.env["NWORKS_VERBOSE"] === "1") {
1321
+ console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
1322
+ }
1323
+ const data = safeParseJson(text);
1324
+ return { posts: data.posts ?? [], responseMetaData: data.responseMetaData };
1325
+ }
1326
+ async function readPost(boardId, postId, profile = "default") {
1327
+ const url = `${BASE_URL6}/boards/${sanitizePathSegment(boardId)}/posts/${sanitizePathSegment(postId)}`;
1328
+ if (process.env["NWORKS_VERBOSE"] === "1") {
1329
+ console.error(`[nworks] GET ${url}`);
1330
+ }
1331
+ const res = await authedFetch5(url, { method: "GET" }, profile);
1332
+ if (!res.ok) return handleError5(res);
1333
+ const text = await res.text();
1334
+ if (process.env["NWORKS_VERBOSE"] === "1") {
1335
+ console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
1336
+ }
1337
+ return safeParseJson(text);
1338
+ }
1339
+ async function createPost(opts) {
1340
+ const profile = opts.profile ?? "default";
1341
+ const body = {
1342
+ title: opts.title,
1343
+ body: opts.body ?? ""
1344
+ };
1345
+ if (opts.enableComment !== void 0) body.enableComment = opts.enableComment;
1346
+ if (opts.sendNotifications !== void 0) body.sendNotifications = opts.sendNotifications;
1347
+ const url = `${BASE_URL6}/boards/${sanitizePathSegment(opts.boardId)}/posts`;
1348
+ if (process.env["NWORKS_VERBOSE"] === "1") {
1349
+ console.error(`[nworks] POST ${url}`);
1350
+ console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
1351
+ }
1352
+ const res = await authedFetch5(
1353
+ url,
1354
+ {
1355
+ method: "POST",
1356
+ headers: { "Content-Type": "application/json" },
1357
+ body: JSON.stringify(body)
1358
+ },
1359
+ profile
1360
+ );
1361
+ if (res.status === 201 || res.ok) {
1362
+ const text = await res.text();
1363
+ if (process.env["NWORKS_VERBOSE"] === "1") {
1364
+ console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
1365
+ }
1366
+ return safeParseJson(text);
1367
+ }
1368
+ return handleError5(res);
1369
+ }
1370
+
1371
+ // src/commands/doctor.ts
1372
+ import { Command } from "commander";
1373
+ import { existsSync as existsSync2 } from "fs";
1374
+ import { readFile as readFile4 } from "fs/promises";
1375
+
1376
+ // src/output/format.ts
1377
+ function output(data, opts = {}) {
1378
+ const useJson = opts.json || !process.stdout.isTTY;
1379
+ if (useJson) {
1380
+ console.log(JSON.stringify(data, null, 2));
1381
+ return;
1382
+ }
1383
+ if (Array.isArray(data)) {
1384
+ printTable(data);
1385
+ } else if (typeof data === "object" && data !== null) {
1386
+ const record = data;
1387
+ for (const value of Object.values(record)) {
1388
+ if (Array.isArray(value) && value.length > 0) {
1389
+ printTable(value);
1390
+ return;
1391
+ }
1392
+ }
1393
+ for (const [key, value] of Object.entries(record)) {
1394
+ console.log(` ${key}: ${String(value)}`);
1395
+ }
1396
+ } else {
1397
+ console.log(String(data));
1398
+ }
1399
+ }
1400
+ function printTable(rows) {
1401
+ if (rows.length === 0) {
1402
+ console.log(" (no data)");
1403
+ return;
1404
+ }
1405
+ const first = rows[0];
1406
+ const keys = Object.keys(first);
1407
+ const widths = {};
1408
+ for (const key of keys) {
1409
+ widths[key] = key.length;
1410
+ }
1411
+ for (const row of rows) {
1412
+ const record = row;
1413
+ for (const key of keys) {
1414
+ const len = String(record[key] ?? "").length;
1415
+ if (len > (widths[key] ?? 0)) {
1416
+ widths[key] = len;
1417
+ }
1418
+ }
1419
+ }
1420
+ const header = keys.map((k) => k.padEnd(widths[k] ?? 0)).join(" ");
1421
+ const separator = keys.map((k) => "\u2500".repeat(widths[k] ?? 0)).join("\u2500\u2500");
1422
+ console.log(` ${header}`);
1423
+ console.log(` ${separator}`);
1424
+ for (const row of rows) {
1425
+ const record = row;
1426
+ const line = keys.map((k) => String(record[k] ?? "").padEnd(widths[k] ?? 0)).join(" ");
1427
+ console.log(` ${line}`);
1428
+ }
1429
+ }
1430
+ function errorOutput(error, opts = {}) {
1431
+ const payload = {
1432
+ success: false,
1433
+ error: { code: error.code ?? "ERROR", message: error.message, hint: error.hint }
1434
+ };
1435
+ if (opts.json || !process.stderr.isTTY) {
1436
+ console.error(JSON.stringify(payload, null, 2));
1437
+ } else {
1438
+ console.error(` Error: ${error.message}`);
1439
+ if (error.code) {
1440
+ console.error(` Code: ${error.code}`);
1441
+ }
1442
+ if (error.hint) {
1443
+ console.error(` \u2192 ${error.hint}`);
1444
+ }
1445
+ }
1446
+ }
1447
+
1448
+ // src/utils/error-hints.ts
1449
+ var REQUIRED_SCOPES = {
1450
+ calendar: "calendar",
1451
+ "calendar.list": "calendar.read",
1452
+ "calendar.create": "calendar calendar.read",
1453
+ "calendar.update": "calendar calendar.read",
1454
+ "calendar.delete": "calendar calendar.read",
1455
+ mail: "mail",
1456
+ "mail.send": "mail",
1457
+ "mail.list": "mail.read",
1458
+ "mail.read": "mail.read",
1459
+ task: "task",
1460
+ "task.list": "task.read",
1461
+ "task.create": "task user.read",
1462
+ "task.update": "task user.read",
1463
+ "task.delete": "task user.read",
1464
+ drive: "file",
1465
+ "drive.list": "file.read",
1466
+ "drive.upload": "file",
1467
+ "drive.download": "file.read",
1468
+ board: "board",
1469
+ "board.list": "board.read",
1470
+ "board.posts": "board.read",
1471
+ "board.read": "board.read",
1472
+ "board.create": "board"
1473
+ };
1474
+ var ERROR_HINTS_CLI = {
1475
+ FORBIDDEN: "\uAD8C\uD55C\uC774 \uBD80\uC871\uD569\uB2C8\uB2E4. Developer Console\uC5D0\uC11C OAuth Scope\uB97C \uD655\uC778\uD558\uC138\uC694.",
1476
+ ACCESS_DENIED: "\uC811\uADFC\uC774 \uAC70\uBD80\uB410\uC2B5\uB2C8\uB2E4. Admin\uC5D0\uC11C Bot\uC744 \uCD94\uAC00\uD588\uB294\uC9C0 \uD655\uC778\uD558\uC138\uC694.",
1477
+ SERVICE_ACCOUNT_NOT_ALLOWED: "\uC11C\uBE44\uC2A4 \uACC4\uC815\uC73C\uB85C\uB294 \uC774 API\uB97C \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `nworks login --user`\uB85C User OAuth \uB85C\uADF8\uC778\uD558\uC138\uC694.",
1478
+ UNAUTHORIZED: "\uC778\uC99D\uC774 \uB9CC\uB8CC\uB410\uC2B5\uB2C8\uB2E4. `nworks login`\uC73C\uB85C \uB2E4\uC2DC \uB85C\uADF8\uC778\uD558\uC138\uC694."
1479
+ };
1480
+ var ERROR_HINTS_MCP = {
1481
+ FORBIDDEN: "\uAD8C\uD55C\uC774 \uBD80\uC871\uD569\uB2C8\uB2E4. Developer Console\uC5D0\uC11C OAuth Scope\uB97C \uD655\uC778\uD558\uC138\uC694.",
1482
+ ACCESS_DENIED: "\uC811\uADFC\uC774 \uAC70\uBD80\uB410\uC2B5\uB2C8\uB2E4. Admin\uC5D0\uC11C Bot\uC744 \uCD94\uAC00\uD588\uB294\uC9C0 \uD655\uC778\uD558\uC138\uC694.",
1483
+ SERVICE_ACCOUNT_NOT_ALLOWED: "\uC11C\uBE44\uC2A4 \uACC4\uC815\uC73C\uB85C\uB294 \uC774 API\uB97C \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. nworks_login_user tool\uB85C User OAuth \uB85C\uADF8\uC778\uC744 \uBA3C\uC800 \uD574\uC8FC\uC138\uC694.",
1484
+ UNAUTHORIZED: "\uC778\uC99D\uC774 \uB9CC\uB8CC\uB410\uC2B5\uB2C8\uB2E4. nworks_login_user tool\uB85C \uB2E4\uC2DC \uBE0C\uB77C\uC6B0\uC800 \uB85C\uADF8\uC778\uD558\uC138\uC694. \uADF8\uB798\uB3C4 \uC548 \uB418\uBA74 nworks_setup tool\uB85C \uC7AC\uC124\uC815 \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694."
1485
+ };
1486
+ function cliErrorHint(err, area) {
1487
+ if (err instanceof ApiError) {
1488
+ const hint = ERROR_HINTS_CLI[err.code];
1489
+ const scopeHint = area ? buildScopeHint(area, "cli") : "";
1490
+ if (hint) {
1491
+ return `[${err.code}] ${err.message}
1492
+ \u2192 ${hint}${scopeHint}`;
1493
+ }
1494
+ return `[${err.code}] ${err.message}${scopeHint}`;
1495
+ }
1496
+ if (err instanceof AuthError) {
1497
+ return `${err.message}
1498
+ \u2192 ${ERROR_HINTS_CLI["UNAUTHORIZED"]}`;
1499
+ }
1500
+ return err.message;
1501
+ }
1502
+ function mcpErrorHint(err, area) {
1503
+ if (err instanceof ApiError) {
1504
+ const hint = ERROR_HINTS_MCP[err.code];
1505
+ const scopeHint = area ? buildScopeHint(area, "mcp") : "";
1506
+ if (hint) {
1507
+ return `Error: [${err.code}] ${err.message}
1508
+
1509
+ [\uC548\uB0B4] ${hint}${scopeHint}`;
1510
+ }
1511
+ return `Error: [${err.code}] ${err.message}${scopeHint}`;
1512
+ }
1513
+ if (err instanceof AuthError) {
1514
+ return `Error: ${err.message}
1515
+
1516
+ [\uC548\uB0B4] \uC778\uC99D \uC815\uBCF4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. nworks_setup tool\uC744 \uBA3C\uC800 \uD638\uCD9C\uD558\uC138\uC694 (Client ID \uD544\uC694, Client Secret\uC740 \uD658\uACBD\uBCC0\uC218 NWORKS_CLIENT_SECRET\uC5D0\uC11C \uC77D\uC74C). \uD658\uACBD\uBCC0\uC218\uAC00 \uC5C6\uC73C\uBA74 \uC0AC\uC6A9\uC790\uC5D0\uAC8C MCP \uC124\uC815 \uD30C\uC77C\uC758 env \uD544\uB4DC\uC5D0 NWORKS_CLIENT_SECRET \uCD94\uAC00\uB97C \uC548\uB0B4\uD558\uC138\uC694.`;
1517
+ }
1518
+ return `Error: ${err.message}`;
1519
+ }
1520
+ function buildScopeHint(area, mode) {
1521
+ const scope = REQUIRED_SCOPES[area];
1522
+ if (!scope) return "";
1523
+ const scopes = scope.split(" ").join(", ");
1524
+ if (mode === "cli") {
1525
+ return `
1526
+ \u2192 \uC774 \uBA85\uB839\uC5B4\uB294 ${scopes} scope\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. \`nworks login --user --scope "${scope}"\`\uB97C \uC2E4\uD589\uD558\uC138\uC694.`;
1527
+ }
1528
+ return `
1529
+ \u2192 \uC774 API\uB294 ${scopes} scope\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. nworks_login_user tool\uB85C \uB85C\uADF8\uC778\uD558\uC138\uC694 (scope\uB97C \uC9C0\uC815\uD558\uC9C0 \uC54A\uC73C\uBA74 \uC804\uCCB4 \uAD8C\uD55C\uC774 \uC790\uB3D9 \uD3EC\uD568\uB429\uB2C8\uB2E4).`;
1530
+ }
1531
+
1532
+ // src/output/cli-error.ts
1533
+ function cliError(err, opts = {}, area) {
1534
+ const error = err;
1535
+ if (error instanceof ApiError) {
1536
+ errorOutput(
1537
+ {
1538
+ code: error.code,
1539
+ message: error.message,
1540
+ hint: cliErrorHint(err, area).split("\n").slice(1).map((l) => l.replace(/^\s+→\s*/, "")).join(" ")
1541
+ },
1542
+ opts
1543
+ );
1544
+ } else if (error instanceof AuthError) {
1545
+ errorOutput(
1546
+ {
1547
+ code: "AUTH_ERROR",
1548
+ message: error.message,
1549
+ hint: "\uC778\uC99D\uC774 \uB9CC\uB8CC\uB410\uC2B5\uB2C8\uB2E4. `nworks login`\uC73C\uB85C \uB2E4\uC2DC \uB85C\uADF8\uC778\uD558\uC138\uC694."
1550
+ },
1551
+ opts
1552
+ );
1553
+ } else {
1554
+ errorOutput({ message: error.message }, opts);
1555
+ }
1556
+ process.exitCode = 1;
1557
+ }
1558
+
1559
+ // src/commands/doctor.ts
1560
+ async function runChecks(profile) {
1561
+ const results = [];
1562
+ let creds;
1563
+ try {
1564
+ creds = await loadCredentials(profile);
1565
+ results.push({ check: "credentials", status: "OK", detail: `clientId: ${creds.clientId}` });
1566
+ } catch {
1567
+ results.push({ check: "credentials", status: "FAIL", detail: "\uC778\uC99D \uC815\uBCF4 \uC5C6\uC74C. CLI: `nworks login --user` / MCP: nworks_setup tool \uC0AC\uC6A9 (\uD658\uACBD\uBCC0\uC218 NWORKS_CLIENT_SECRET \uD544\uC694)" });
1568
+ return results;
1569
+ }
1570
+ if (hasServiceAccountCreds(creds)) {
1571
+ results.push({ check: "serviceAccount", status: "OK", detail: creds.serviceAccount });
1572
+ } else {
1573
+ results.push({ check: "serviceAccount", status: "SKIP", detail: "\uBBF8\uC124\uC815 (\uBD07 \uBA54\uC2DC\uC9C0 \uC0AC\uC6A9 \uC2DC \uD544\uC694)" });
1574
+ }
1575
+ if (creds.privateKeyPath) {
1576
+ if (existsSync2(creds.privateKeyPath)) {
1577
+ try {
1578
+ await readFile4(creds.privateKeyPath, "utf-8");
1579
+ results.push({ check: "privateKey", status: "OK", detail: creds.privateKeyPath });
1580
+ } catch {
1581
+ results.push({ check: "privateKey", status: "FAIL", detail: `\uC77D\uAE30 \uBD88\uAC00: ${creds.privateKeyPath}` });
1582
+ }
1583
+ } else {
1584
+ results.push({ check: "privateKey", status: "FAIL", detail: `\uD30C\uC77C \uC5C6\uC74C: ${creds.privateKeyPath}` });
1585
+ }
1586
+ } else {
1587
+ results.push({ check: "privateKey", status: "SKIP", detail: "\uBBF8\uC124\uC815" });
1588
+ }
1589
+ if (creds.botId) {
1590
+ results.push({ check: "botId", status: "OK", detail: creds.botId });
1591
+ } else {
1592
+ results.push({ check: "botId", status: "SKIP", detail: "\uBBF8\uC124\uC815 (\uBA54\uC2DC\uC9C0 \uC804\uC1A1 \uC2DC \uD544\uC694)" });
1593
+ }
1594
+ const token = await loadToken(profile);
1595
+ if (token) {
1596
+ const valid = token.expiresAt > Date.now() / 1e3;
1597
+ results.push({
1598
+ check: "serviceToken",
1599
+ status: valid ? "OK" : "EXPIRED",
1600
+ detail: valid ? `\uB9CC\uB8CC: ${new Date(token.expiresAt * 1e3).toISOString()}` : `\uB9CC\uB8CC\uB428: ${new Date(token.expiresAt * 1e3).toISOString()}`
1601
+ });
1602
+ } else {
1603
+ results.push({ check: "serviceToken", status: "SKIP", detail: "\uD1A0\uD070 \uC5C6\uC74C" });
1604
+ }
1605
+ const userToken = await loadUserToken(profile);
1606
+ if (userToken) {
1607
+ const valid = userToken.expiresAt > Date.now() / 1e3;
1608
+ results.push({
1609
+ check: "userOAuth",
1610
+ status: valid ? "OK" : "EXPIRED",
1611
+ detail: valid ? `scope: ${userToken.scope} | \uB9CC\uB8CC: ${new Date(userToken.expiresAt * 1e3).toISOString()}` : `\uB9CC\uB8CC\uB428 | scope: ${userToken.scope}`
1612
+ });
1613
+ } else {
1614
+ results.push({ check: "userOAuth", status: "SKIP", detail: "\uD1A0\uD070 \uC5C6\uC74C. `nworks login --user` \uD544\uC694" });
1615
+ }
1616
+ if (hasServiceAccountCreds(creds) && token && token.expiresAt > Date.now() / 1e3) {
1617
+ try {
1618
+ const res = await fetch("https://www.worksapis.com/v1.0/users/me", {
1619
+ headers: { Authorization: `Bearer ${token.accessToken}` }
1620
+ });
1621
+ if (res.ok) {
1622
+ results.push({ check: "apiConnection", status: "OK", detail: "NAVER WORKS API \uC5F0\uACB0 \uC131\uACF5" });
1623
+ } else {
1624
+ results.push({ check: "apiConnection", status: "FAIL", detail: `HTTP ${res.status}` });
1625
+ }
1626
+ } catch (e) {
1627
+ results.push({ check: "apiConnection", status: "FAIL", detail: `\uC5F0\uACB0 \uC2E4\uD328: ${e.message}` });
1628
+ }
1629
+ } else if (userToken && userToken.expiresAt > Date.now() / 1e3) {
1630
+ try {
1631
+ const res = await fetch("https://www.worksapis.com/v1.0/users/me", {
1632
+ headers: { Authorization: `Bearer ${userToken.accessToken}` }
1633
+ });
1634
+ if (res.ok) {
1635
+ results.push({ check: "apiConnection", status: "OK", detail: "NAVER WORKS API \uC5F0\uACB0 \uC131\uACF5" });
1636
+ } else {
1637
+ results.push({ check: "apiConnection", status: "FAIL", detail: `HTTP ${res.status}` });
1638
+ }
1639
+ } catch (e) {
1640
+ results.push({ check: "apiConnection", status: "FAIL", detail: `\uC5F0\uACB0 \uC2E4\uD328: ${e.message}` });
1641
+ }
1642
+ } else {
1643
+ results.push({ check: "apiConnection", status: "SKIP", detail: "\uC720\uD6A8\uD55C \uD1A0\uD070 \uC5C6\uC74C \u2014 API \uD14C\uC2A4\uD2B8 \uAC74\uB108\uB700" });
1644
+ }
1645
+ return results;
1646
+ }
1647
+ var doctorCommand = new Command("doctor").description("Check nworks configuration and connectivity").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
1648
+ try {
1649
+ const results = await runChecks(opts.profile);
1650
+ if (opts.json || !process.stdout.isTTY) {
1651
+ output(results, opts);
1652
+ } else {
1653
+ console.log("\n nworks doctor\n");
1654
+ for (const r of results) {
1655
+ const icon = r.status === "OK" ? "\u2705" : r.status === "SKIP" ? "\u2796" : "\u274C";
1656
+ console.log(` ${icon} ${r.check.padEnd(16)} ${r.detail}`);
1657
+ }
1658
+ console.log();
1659
+ }
1660
+ } catch (err) {
1661
+ cliError(err, opts);
1662
+ }
1663
+ });
1664
+
1665
+ // src/utils/datetime.ts
1666
+ function todaySeoulRange() {
1667
+ const ymd = new Intl.DateTimeFormat("en-CA", {
1668
+ timeZone: "Asia/Seoul",
1669
+ year: "numeric",
1670
+ month: "2-digit",
1671
+ day: "2-digit"
1672
+ }).format(/* @__PURE__ */ new Date());
1673
+ return {
1674
+ from: `${ymd}T00:00:00+09:00`,
1675
+ until: `${ymd}T23:59:59+09:00`
1676
+ };
1677
+ }
1678
+
1679
+ // src/utils/html-to-text.ts
1680
+ var HTML_TAG = /<\/?(?:html|body|head|div|p|br|span|table|tr|td|th|thead|tbody|tfoot|a|img|ul|ol|li|h[1-6]|b|i|u|strong|em|blockquote|font|style|script|noscript|title|meta|center|hr|pre|section|article|nav|header|footer|main|figure|figcaption|small|sub|sup|mark|code)\b/i;
1681
+ var NAMED_ENTITIES = {
1682
+ amp: "&",
1683
+ lt: "<",
1684
+ gt: ">",
1685
+ quot: '"',
1686
+ apos: "'",
1687
+ nbsp: " ",
1688
+ copy: "\xA9",
1689
+ reg: "\xAE",
1690
+ trade: "\u2122",
1691
+ hellip: "\u2026",
1692
+ mdash: "\u2014",
1693
+ ndash: "\u2013",
1694
+ lsquo: "\u2018",
1695
+ rsquo: "\u2019",
1696
+ ldquo: "\u201C",
1697
+ rdquo: "\u201D",
1698
+ bull: "\u2022",
1699
+ middot: "\xB7",
1700
+ euro: "\u20AC",
1701
+ pound: "\xA3",
1702
+ cent: "\xA2"
1703
+ };
1704
+ function decodeEntities(s) {
1705
+ return s.replace(/&(#x?[0-9a-f]+|[a-z][a-z0-9]*);/gi, (m, ent) => {
1706
+ if (ent[0] === "#") {
1707
+ const isHex = ent[1] === "x" || ent[1] === "X";
1708
+ const code = isHex ? parseInt(ent.slice(2), 16) : parseInt(ent.slice(1), 10);
1709
+ if (Number.isNaN(code)) return m;
1710
+ try {
1711
+ return String.fromCodePoint(code);
1712
+ } catch {
1713
+ return m;
1714
+ }
1715
+ }
1716
+ const named = NAMED_ENTITIES[ent.toLowerCase()];
1717
+ return named ?? m;
1718
+ });
1719
+ }
1720
+ function htmlToText(input) {
1721
+ if (!input) return "";
1722
+ if (!HTML_TAG.test(input)) return input.trim();
1723
+ let s = input;
1724
+ s = s.replace(/<!--[\s\S]*?-->/g, "");
1725
+ s = s.replace(/<(script|style|head|noscript|title)\b[^>]*>[\s\S]*?<\/\1>/gi, "");
1726
+ s = s.replace(/<br\s*\/?>/gi, "\n");
1727
+ s = s.replace(/<li\b[^>]*>/gi, "\n\u2022 ");
1728
+ s = s.replace(/<\/(td|th)>/gi, " ");
1729
+ s = s.replace(
1730
+ /<\/(p|div|tr|h[1-6]|table|ul|ol|blockquote|section|article|header|footer|pre)>/gi,
1731
+ "\n"
1732
+ );
1733
+ s = s.replace(/<[^>]+>/g, "");
1734
+ s = decodeEntities(s);
1735
+ s = s.replace(/\r\n?/g, "\n").replace(/[^\S\n]+/g, " ").replace(/ *\n */g, "\n").replace(/\n{3,}/g, "\n\n").trim();
1736
+ return s;
1737
+ }
1738
+
1739
+ // src/utils/attachment-text.ts
1740
+ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
1741
+ "txt",
1742
+ "text",
1743
+ "csv",
1744
+ "tsv",
1745
+ "log",
1746
+ "md",
1747
+ "markdown",
1748
+ "json",
1749
+ "xml",
1750
+ "yaml",
1751
+ "yml",
1752
+ "ics",
1753
+ "vcs",
1754
+ "vcf",
1755
+ "srt",
1756
+ "vtt",
1757
+ "ini",
1758
+ "conf",
1759
+ "cfg",
1760
+ "properties",
1761
+ "env"
1762
+ ]);
1763
+ var HTML_EXTENSIONS = /* @__PURE__ */ new Set(["html", "htm", "xhtml"]);
1764
+ function extensionOf(filename) {
1765
+ const dot = filename.lastIndexOf(".");
1766
+ return dot >= 0 ? filename.slice(dot + 1).toLowerCase() : "";
1767
+ }
1768
+ function attachmentTextOrNull(filename, buffer) {
1769
+ const ext = extensionOf(filename);
1770
+ if (HTML_EXTENSIONS.has(ext)) {
1771
+ return htmlToText(buffer.toString("utf8"));
1772
+ }
1773
+ if (TEXT_EXTENSIONS.has(ext)) {
1774
+ return buffer.toString("utf8");
1775
+ }
1776
+ return null;
1777
+ }
1778
+
1779
+ // src/mcp/tools.ts
1780
+ function registerTools(server) {
1781
+ server.tool(
1782
+ "nworks_setup",
1783
+ `NAVER WORKS API \uC778\uC99D \uC815\uBCF4\uB97C \uC124\uC815\uD569\uB2C8\uB2E4.
1784
+
1785
+ \u25A0 \uC0AC\uC804 \uC900\uBE44 (\uC0AC\uC6A9\uC790\uAC00 \uC9C1\uC811 \uD574\uC57C \uD568):
1786
+ 1. https://dev.worksmobile.com \uC5D0\uC11C \uC571 \uC0DD\uC131 \uD6C4 Client ID\uC640 Client Secret\uC744 \uBC1C\uAE09\uBC1B\uC2B5\uB2C8\uB2E4.
1787
+ 2. MCP \uC124\uC815 \uD30C\uC77C(\uC608: claude_desktop_config.json)\uC758 nworks \uC11C\uBC84\uC5D0 env \uD544\uB4DC\uB97C \uCD94\uAC00\uD569\uB2C8\uB2E4:
1788
+ { "env": { "NWORKS_CLIENT_SECRET": "<\uBC1C\uAE09\uBC1B\uC740 Client Secret>" } }
1789
+ 3. MCP \uD074\uB77C\uC774\uC5B8\uD2B8(\uC608: Claude Desktop)\uB97C \uC7AC\uC2DC\uC791\uD569\uB2C8\uB2E4.
1790
+
1791
+ \u25A0 \uC774 tool\uC758 \uC5ED\uD560:
1792
+ - clientId(\uD544\uC218)\uC640 serviceAccount, botId, domainId(\uC120\uD0DD)\uB97C \uD30C\uB77C\uBBF8\uD130\uB85C \uBC1B\uC544 \uC800\uC7A5\uD569\uB2C8\uB2E4.
1793
+ - Client Secret\uC740 \uBCF4\uC548\uC744 \uC704\uD574 \uD30C\uB77C\uBBF8\uD130\uB85C \uBC1B\uC9C0 \uC54A\uC73C\uBA70, \uD658\uACBD\uBCC0\uC218 NWORKS_CLIENT_SECRET\uC5D0\uC11C \uC790\uB3D9\uC73C\uB85C \uC77D\uC2B5\uB2C8\uB2E4.
1794
+ - Service Account \uC0AC\uC6A9 \uC2DC \uD658\uACBD\uBCC0\uC218 NWORKS_PRIVATE_KEY_PATH\uB3C4 \uD544\uC694\uD569\uB2C8\uB2E4.
1795
+
1796
+ \u25A0 \uC124\uC815 \uD6C4 \uB2E4\uC74C \uB2E8\uACC4:
1797
+ - \uCE98\uB9B0\uB354/\uBA54\uC77C/\uB4DC\uB77C\uC774\uBE0C/\uD560\uC77C/\uAC8C\uC2DC\uD310 \u2192 nworks_login_user tool\uB85C \uBE0C\uB77C\uC6B0\uC800 \uB85C\uADF8\uC778 \uD544\uC694
1798
+ - \uBA54\uC2DC\uC9C0/\uAD6C\uC131\uC6D0\uC870\uD68C \u2192 Service Account \uC778\uC99D (serviceAccount + botId + NWORKS_PRIVATE_KEY_PATH)
1799
+
1800
+ \u25A0 \uD658\uACBD\uBCC0\uC218 NWORKS_CLIENT_SECRET\uC774 \uC5C6\uC73C\uBA74 \uC774 tool\uC740 \uC2E4\uD328\uD569\uB2C8\uB2E4. \uC2E4\uD328 \uC2DC \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uC704 \uC0AC\uC804 \uC900\uBE44 \uB2E8\uACC4\uB97C \uC548\uB0B4\uD558\uC138\uC694.
1801
+
1802
+ OAuth Redirect URI: http://localhost:9876/callback`,
1803
+ {
1804
+ clientId: z.string().describe("Client ID (Developer Console\uC5D0\uC11C \uBC1C\uAE09)"),
1805
+ serviceAccount: z.string().optional().describe("Service Account ID (\uC608: xxxxx.serviceaccount@domain)"),
1806
+ botId: z.string().optional().describe("Bot ID (\uBA54\uC2DC\uC9C0 \uC804\uC1A1 \uC2DC \uD544\uC694)"),
1807
+ domainId: z.string().optional().describe("Domain ID")
1808
+ },
1809
+ async ({ clientId, serviceAccount, botId, domainId }) => {
1810
+ try {
1811
+ const resolvedSecret = process.env["NWORKS_CLIENT_SECRET"];
1812
+ if (!resolvedSecret) {
1813
+ return {
1814
+ content: [{ type: "text", text: JSON.stringify({
1815
+ error: true,
1816
+ message: "\uD658\uACBD\uBCC0\uC218 NWORKS_CLIENT_SECRET\uC774 \uC124\uC815\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
1817
+ userAction: [
1818
+ "1. https://dev.worksmobile.com \uC5D0\uC11C \uC571\uC758 Client Secret\uC744 \uD655\uC778\uD569\uB2C8\uB2E4.",
1819
+ "2. MCP \uC124\uC815 \uD30C\uC77C(\uC608: claude_desktop_config.json)\uC744 \uC5F4\uACE0, nworks \uC11C\uBC84 \uC124\uC815\uC5D0 \uB2E4\uC74C\uC744 \uCD94\uAC00\uD569\uB2C8\uB2E4:",
1820
+ ' "env": { "NWORKS_CLIENT_SECRET": "<Client Secret>" }',
1821
+ "3. MCP \uD074\uB77C\uC774\uC5B8\uD2B8(\uC608: Claude Desktop)\uB97C \uC7AC\uC2DC\uC791\uD569\uB2C8\uB2E4.",
1822
+ "4. \uC7AC\uC2DC\uC791 \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694."
1823
+ ],
1824
+ configExample: '{ "mcpServers": { "nworks": { "command": "npx", "args": ["-y", "nworks", "mcp"], "env": { "NWORKS_CLIENT_SECRET": "<Client Secret>" } } } }'
1825
+ }) }],
1826
+ isError: true
1827
+ };
1828
+ }
1829
+ const resolvedPrivateKeyPath = process.env["NWORKS_PRIVATE_KEY_PATH"];
1830
+ await saveCredentials({
1831
+ clientId,
1832
+ clientSecret: resolvedSecret,
1833
+ serviceAccount,
1834
+ privateKeyPath: resolvedPrivateKeyPath,
1835
+ botId,
1836
+ domainId
1837
+ });
1838
+ const nextSteps = [];
1839
+ if (serviceAccount && resolvedPrivateKeyPath && botId) {
1840
+ nextSteps.push("Service Account \uC778\uC99D \uC900\uBE44 \uC644\uB8CC \u2014 \uBD07 \uBA54\uC2DC\uC9C0 \uB4F1 \uBC14\uB85C \uC0AC\uC6A9 \uAC00\uB2A5");
1841
+ } else if (serviceAccount && botId && !resolvedPrivateKeyPath) {
1842
+ nextSteps.push('NWORKS_PRIVATE_KEY_PATH \uD658\uACBD\uBCC0\uC218\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. Service Account \uC778\uC99D\uC5D0\uB294 Private Key \uD30C\uC77C \uACBD\uB85C\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uC548\uB0B4\uD558\uC138\uC694: (1) Developer Console(https://dev.worksmobile.com)\uC5D0\uC11C Private Key\uB97C \uB2E4\uC6B4\uB85C\uB4DC (2) MCP \uC124\uC815 \uD30C\uC77C\uC758 env\uC5D0 NWORKS_PRIVATE_KEY_PATH\uB97C \uCD94\uAC00 (\uC608: "NWORKS_PRIVATE_KEY_PATH": "C:/keys/private.key") (3) MCP \uD074\uB77C\uC774\uC5B8\uD2B8 \uC7AC\uC2DC\uC791');
1843
+ }
1844
+ nextSteps.push("User OAuth\uAC00 \uD544\uC694\uD55C API\uB294 nworks_login_user tool\uB85C \uBE0C\uB77C\uC6B0\uC800 \uB85C\uADF8\uC778\uC744 \uC9C4\uD589\uD558\uC138\uC694");
1845
+ const mask = (s) => s.length <= 4 ? "****" : `****${s.slice(-Math.min(4, Math.floor(s.length / 3)))}`;
1846
+ return {
1847
+ content: [
1848
+ {
1849
+ type: "text",
1850
+ text: JSON.stringify({
1851
+ success: true,
1852
+ message: "\uC778\uC99D \uC815\uBCF4\uAC00 \uC800\uC7A5\uB418\uC5C8\uC2B5\uB2C8\uB2E4.",
1853
+ nextSteps,
1854
+ clientId: mask(clientId),
1855
+ clientSecret: `${mask(resolvedSecret)} (\uD658\uACBD\uBCC0\uC218)`,
1856
+ serviceAccount: serviceAccount ?? null,
1857
+ privateKeyPath: resolvedPrivateKeyPath ? `${mask(resolvedPrivateKeyPath)} (\uD658\uACBD\uBCC0\uC218)` : null,
1858
+ botId: botId ?? null
1859
+ })
1860
+ }
1861
+ ]
1862
+ };
1863
+ } catch (err) {
1864
+ return {
1865
+ content: [{ type: "text", text: mcpErrorHint(err) }],
1866
+ isError: true
1867
+ };
1868
+ }
1869
+ }
1870
+ );
1871
+ server.tool(
1872
+ "nworks_message_send",
1873
+ "NAVER WORKS \uBA54\uC2DC\uC9C0\uB97C \uC804\uC1A1\uD569\uB2C8\uB2E4 (\uBD07\uC774 \uC0AC\uC6A9\uC790 \uB610\uB294 \uCC44\uB110\uC5D0 \uBC1C\uC1A1). Service Account \uC778\uC99D \uC0AC\uC6A9 (nworks_setup\uC5D0\uC11C serviceAccount, botId \uC124\uC815 + \uD658\uACBD\uBCC0\uC218 NWORKS_PRIVATE_KEY_PATH \uD544\uC694. User OAuth \uBD88\uD544\uC694)",
1874
+ {
1875
+ to: z.string().optional().describe("\uC218\uC2E0\uC790 userId (channel\uACFC \uD0DD 1). nworks_directory_members\uB85C userId \uC870\uD68C \uAC00\uB2A5"),
1876
+ channel: z.string().optional().describe("\uCC44\uB110 channelId (to\uC640 \uD0DD 1). nworks_message_members\uB85C \uCC44\uB110 \uAD6C\uC131\uC6D0 \uD655\uC778 \uAC00\uB2A5"),
1877
+ text: z.string().describe("\uBA54\uC2DC\uC9C0 \uBCF8\uBB38"),
1878
+ type: z.enum(["text", "button", "list"]).optional().describe("\uBA54\uC2DC\uC9C0 \uD0C0\uC785 (\uAE30\uBCF8: text)"),
1879
+ actions: z.string().optional().describe("\uBC84\uD2BC \uC561\uC158 JSON (type=button\uC77C \uB54C)"),
1880
+ elements: z.string().optional().describe("\uB9AC\uC2A4\uD2B8 \uD56D\uBAA9 JSON (type=list\uC77C \uB54C)")
1881
+ },
1882
+ async ({ to, channel, text, type, actions, elements }) => {
1883
+ try {
1884
+ const result = await send({
1885
+ to,
1886
+ channel,
1887
+ text,
1888
+ type,
1889
+ actions,
1890
+ elements
1891
+ });
1892
+ return {
1893
+ content: [{ type: "text", text: JSON.stringify(result) }]
1894
+ };
1895
+ } catch (err) {
1896
+ return {
1897
+ content: [{ type: "text", text: mcpErrorHint(err) }],
1898
+ isError: true
1899
+ };
1900
+ }
1901
+ }
1902
+ );
1903
+ server.tool(
1904
+ "nworks_message_members",
1905
+ "\uD2B9\uC815 \uCC44\uB110\uC758 \uAD6C\uC131\uC6D0 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. '\uC774 \uCC44\uB110\uC5D0 \uB204\uAC00 \uC788\uC5B4?' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. Service Account \uC778\uC99D \uC0AC\uC6A9 (nworks_setup \uD544\uC694)",
1906
+ {
1907
+ channel: z.string().describe("\uCC44\uB110 channelId")
1908
+ },
1909
+ async ({ channel }) => {
1910
+ try {
1911
+ const result = await listMembers(channel);
1912
+ return {
1913
+ content: [{ type: "text", text: JSON.stringify(result) }]
1914
+ };
1915
+ } catch (err) {
1916
+ return {
1917
+ content: [{ type: "text", text: mcpErrorHint(err) }],
1918
+ isError: true
1919
+ };
1920
+ }
1921
+ }
1922
+ );
1923
+ server.tool(
1924
+ "nworks_directory_members",
1925
+ "NAVER WORKS \uC870\uC9C1 \uAD6C\uC131\uC6D0(\uC9C1\uC6D0) \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. '\uAD6C\uC131\uC6D0 \uBAA9\uB85D \uBCF4\uC5EC\uC918', '\uD300\uC6D0 \uCC3E\uC544\uC918', '\uB204\uAD6C\uD55C\uD14C \uBA54\uC2DC\uC9C0 \uBCF4\uB0BC\uC9C0 userId \uCC3E\uAE30' \uB4F1\uC5D0 \uC0AC\uC6A9. Service Account \uC778\uC99D \uC0AC\uC6A9 (nworks_setup \uD544\uC694). \uBA54\uC2DC\uC9C0 \uC804\uC1A1 \uC2DC \uC218\uC2E0\uC790 userId\uB97C \uC5EC\uAE30\uC11C \uC870\uD68C \uAC00\uB2A5",
1926
+ {},
1927
+ async () => {
1928
+ try {
1929
+ const result = await listUsers();
1930
+ return {
1931
+ content: [{ type: "text", text: JSON.stringify(result) }]
1932
+ };
1933
+ } catch (err) {
1934
+ return {
1935
+ content: [{ type: "text", text: mcpErrorHint(err) }],
1936
+ isError: true
1937
+ };
1938
+ }
1939
+ }
1940
+ );
1941
+ server.tool(
1942
+ "nworks_calendar_list",
1943
+ "\uC0AC\uC6A9\uC790\uC758 \uBAA8\uB4E0 \uCE98\uB9B0\uB354\uC5D0\uC11C \uC77C\uC815/\uC2A4\uCF00\uC904\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4 (\uAC1C\uC778\xB7\uD1A1\uBC29 \uCE98\uB9B0\uB354 \uC804\uCCB4 \uC9D1\uACC4). '\uC624\uB298 \uC77C\uC815 \uC54C\uB824\uC918', '\uC774\uBC88 \uC8FC \uC2A4\uCF00\uC904 \uD655\uC778' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. \uAC01 \uC77C\uC815\uC5D0 \uC18C\uC18D \uCE98\uB9B0\uB354 \uC774\uB984\uC774 \uBD99\uC2B5\uB2C8\uB2E4. fromDateTime/untilDateTime\uC744 \uBAA8\uB450 \uC0DD\uB7B5\uD558\uBA74 \uC790\uB3D9\uC73C\uB85C '\uC624\uB298'(Asia/Seoul)\uB85C \uC870\uD68C\uD558\uBBC0\uB85C, \uD604\uC7AC \uB0A0\uC9DC\uB97C \uBAB0\uB77C\uB3C4 \uC548\uC804\uD569\uB2C8\uB2E4. \uD2B9\uC815 \uCE98\uB9B0\uB354\uB9CC \uBCF4\uB824\uBA74 calendarId \uC9C0\uC815. \uC751\uB2F5\uC758 skippedCalendars\uB294 \uAD8C\uD55C \uB4F1\uC73C\uB85C \uC870\uD68C \uC2E4\uD328\uD574 \uACB0\uACFC\uC5D0\uC11C \uBE60\uC9C4 \uCE98\uB9B0\uB354 \uBAA9\uB85D\uC785\uB2C8\uB2E4. User OAuth \uC778\uC99D \uD544\uC694 (calendar.read scope). \uBBF8\uB85C\uADF8\uC778 \uC2DC nworks_login_user\uB85C \uB85C\uADF8\uC778 \uD544\uC694",
1944
+ {
1945
+ fromDateTime: z.string().optional().describe("\uC2DC\uC791 \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss+09:00). \uC0DD\uB7B5 \uC2DC \uC624\uB298 00:00 (Asia/Seoul)"),
1946
+ untilDateTime: z.string().optional().describe("\uC885\uB8CC \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss+09:00). \uC0DD\uB7B5 \uC2DC \uC624\uB298 23:59 (Asia/Seoul)"),
1947
+ userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)"),
1948
+ calendarId: z.string().optional().describe("\uD2B9\uC815 \uCE98\uB9B0\uB354\uB9CC \uC870\uD68C (\uBBF8\uC9C0\uC815 \uC2DC \uBAA8\uB4E0 \uCE98\uB9B0\uB354 \uC9D1\uACC4). nworks_calendar_calendars\uB85C ID \uC870\uD68C \uAC00\uB2A5")
1949
+ },
1950
+ async ({ fromDateTime, untilDateTime, userId, calendarId }) => {
1951
+ try {
1952
+ const today = todaySeoulRange();
1953
+ const from = fromDateTime ?? today.from;
1954
+ const until = untilDateTime ?? today.until;
1955
+ if (calendarId) {
1956
+ const result = await listEventsInCalendar(
1957
+ calendarId,
1958
+ from,
1959
+ until,
1960
+ userId ?? "me"
1961
+ );
1962
+ const events2 = result.events.flatMap(
1963
+ (e) => e.eventComponents.map((c) => ({
1964
+ eventId: c.eventId,
1965
+ summary: c.summary,
1966
+ start: c.start.dateTime ?? c.start.date ?? "",
1967
+ end: c.end.dateTime ?? c.end.date ?? "",
1968
+ location: c.location ?? "",
1969
+ calendar: calendarId
1970
+ }))
1971
+ );
1972
+ return {
1973
+ content: [{ type: "text", text: JSON.stringify({ events: events2, count: events2.length, range: { from, until } }) }]
1974
+ };
1975
+ }
1976
+ const { events: raw, calendarCount, skippedCalendars } = await listAllEvents(from, until, userId ?? "me");
1977
+ const events = raw.map((c) => ({
1978
+ eventId: c.eventId,
1979
+ summary: c.summary,
1980
+ start: c.start.dateTime ?? c.start.date ?? "",
1981
+ end: c.end.dateTime ?? c.end.date ?? "",
1982
+ location: c.location ?? "",
1983
+ calendar: c.calendarName
1984
+ }));
1985
+ return {
1986
+ content: [{ type: "text", text: JSON.stringify({ events, count: events.length, calendarCount, skippedCalendars, range: { from, until } }) }]
1987
+ };
1988
+ } catch (err) {
1989
+ return {
1990
+ content: [{ type: "text", text: mcpErrorHint(err, "calendar.list") }],
1991
+ isError: true
1992
+ };
1993
+ }
1994
+ }
1995
+ );
1996
+ server.tool(
1997
+ "nworks_calendar_calendars",
1998
+ "\uC0AC\uC6A9\uC790\uAC00 \uC811\uADFC \uAC00\uB2A5\uD55C \uCE98\uB9B0\uB354 \uBAA9\uB85D\uACFC \uAC01 \uCE98\uB9B0\uB354 ID\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4 (\uAC1C\uC778\xB7\uD1A1\uBC29 \uCE98\uB9B0\uB354). '\uB0B4 \uCE98\uB9B0\uB354 \uBAA9\uB85D \uBCF4\uC5EC\uC918', \uD2B9\uC815 \uCE98\uB9B0\uB354\uC758 \uC77C\uC815\uB9CC \uC870\uD68C\uD558\uAE30 \uC804\uC5D0 ID\uB97C \uCC3E\uC744 \uB54C \uC0AC\uC6A9. User OAuth \uC778\uC99D \uD544\uC694 (calendar.read scope)",
1999
+ {
2000
+ userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
2001
+ },
2002
+ async ({ userId }) => {
2003
+ try {
2004
+ const calendars = await listCalendars(userId ?? "me");
2005
+ const list = calendars.map((c) => ({
2006
+ calendarId: c.calendarId,
2007
+ name: c.calendarName
2008
+ }));
2009
+ return {
2010
+ content: [{ type: "text", text: JSON.stringify({ calendars: list, count: list.length }) }]
2011
+ };
2012
+ } catch (err) {
2013
+ return {
2014
+ content: [{ type: "text", text: mcpErrorHint(err, "calendar.list") }],
2015
+ isError: true
2016
+ };
2017
+ }
2018
+ }
2019
+ );
2020
+ server.tool(
2021
+ "nworks_calendar_create",
2022
+ "\uCE98\uB9B0\uB354 \uC77C\uC815\uC744 \uC0C8\uB85C \uB9CC\uB4ED\uB2C8\uB2E4. '\uD68C\uC758 \uC7A1\uC544\uC918', '\uC77C\uC815 \uB4F1\uB85D\uD574\uC918' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. User OAuth \uC778\uC99D \uD544\uC694 (calendar + calendar.read scope)",
2023
+ {
2024
+ summary: z.string().describe("\uC77C\uC815 \uC81C\uBAA9"),
2025
+ start: z.string().describe("\uC2DC\uC791 \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss)"),
2026
+ end: z.string().describe("\uC885\uB8CC \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss)"),
2027
+ timeZone: z.string().optional().describe("\uD0C0\uC784\uC874 (\uAE30\uBCF8: Asia/Seoul)"),
2028
+ description: z.string().optional().describe("\uC77C\uC815 \uC124\uBA85"),
2029
+ location: z.string().optional().describe("\uC7A5\uC18C"),
2030
+ attendees: z.array(z.object({ email: z.string(), displayName: z.string().optional() })).optional().describe("\uCC38\uC11D\uC790 \uBAA9\uB85D"),
2031
+ sendNotification: z.boolean().optional().describe("\uCC38\uC11D\uC790\uC5D0\uAC8C \uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)"),
2032
+ userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
2033
+ },
2034
+ async ({
2035
+ summary,
2036
+ start,
2037
+ end,
2038
+ timeZone,
2039
+ description,
2040
+ location,
2041
+ attendees,
2042
+ sendNotification,
2043
+ userId
2044
+ }) => {
2045
+ try {
2046
+ const result = await createEvent({
2047
+ summary,
2048
+ start,
2049
+ end,
2050
+ timeZone,
2051
+ description,
2052
+ location,
2053
+ attendees,
2054
+ sendNotification,
2055
+ userId: userId ?? "me"
2056
+ });
2057
+ const event = result.eventComponents?.[0];
2058
+ return {
2059
+ content: [{ type: "text", text: JSON.stringify({ success: true, eventId: event?.eventId, summary: event?.summary, start: event?.start, end: event?.end }) }]
2060
+ };
2061
+ } catch (err) {
2062
+ return {
2063
+ content: [{ type: "text", text: mcpErrorHint(err, "calendar.create") }],
2064
+ isError: true
2065
+ };
2066
+ }
2067
+ }
2068
+ );
2069
+ server.tool(
2070
+ "nworks_calendar_update",
2071
+ "\uAE30\uC874 \uCE98\uB9B0\uB354 \uC77C\uC815\uC744 \uC218\uC815\uD569\uB2C8\uB2E4. '\uC77C\uC815 \uC2DC\uAC04 \uBCC0\uACBD\uD574\uC918', '\uD68C\uC758 \uC81C\uBAA9 \uBC14\uAFD4\uC918' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. User OAuth \uC778\uC99D \uD544\uC694 (calendar + calendar.read scope). eventId\uB294 nworks_calendar_list\uB85C \uC870\uD68C \uAC00\uB2A5",
2072
+ {
2073
+ eventId: z.string().describe("\uC77C\uC815 ID (nworks_calendar_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
2074
+ summary: z.string().optional().describe("\uC0C8 \uC81C\uBAA9"),
2075
+ start: z.string().optional().describe("\uC0C8 \uC2DC\uC791 \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss)"),
2076
+ end: z.string().optional().describe("\uC0C8 \uC885\uB8CC \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss)"),
2077
+ timeZone: z.string().optional().describe("\uD0C0\uC784\uC874 (\uAE30\uBCF8: Asia/Seoul)"),
2078
+ description: z.string().optional().describe("\uC0C8 \uC124\uBA85"),
2079
+ location: z.string().optional().describe("\uC0C8 \uC7A5\uC18C"),
2080
+ sendNotification: z.boolean().optional().describe("\uCC38\uC11D\uC790\uC5D0\uAC8C \uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)"),
2081
+ userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
2082
+ },
2083
+ async ({
2084
+ eventId,
2085
+ summary,
2086
+ start,
2087
+ end,
2088
+ timeZone,
2089
+ description,
2090
+ location,
2091
+ sendNotification,
2092
+ userId
2093
+ }) => {
2094
+ try {
2095
+ await updateEvent({
2096
+ eventId,
2097
+ summary,
2098
+ start,
2099
+ end,
2100
+ timeZone,
2101
+ description,
2102
+ location,
2103
+ sendNotification,
2104
+ userId: userId ?? "me"
2105
+ });
2106
+ return {
2107
+ content: [{ type: "text", text: JSON.stringify({ success: true, eventId, message: "\uC77C\uC815\uC774 \uC218\uC815\uB418\uC5C8\uC2B5\uB2C8\uB2E4" }) }]
2108
+ };
2109
+ } catch (err) {
2110
+ return {
2111
+ content: [{ type: "text", text: mcpErrorHint(err, "calendar.update") }],
2112
+ isError: true
2113
+ };
2114
+ }
2115
+ }
2116
+ );
2117
+ server.tool(
2118
+ "nworks_calendar_delete",
2119
+ "\uCE98\uB9B0\uB354 \uC77C\uC815\uC744 \uC0AD\uC81C\uD569\uB2C8\uB2E4. '\uC77C\uC815 \uCDE8\uC18C\uD574\uC918' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. User OAuth \uC778\uC99D \uD544\uC694 (calendar + calendar.read scope). eventId\uB294 nworks_calendar_list\uB85C \uC870\uD68C \uAC00\uB2A5",
2120
+ {
2121
+ eventId: z.string().describe("\uC0AD\uC81C\uD560 \uC77C\uC815 ID (nworks_calendar_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
2122
+ sendNotification: z.boolean().optional().describe("\uCC38\uC11D\uC790\uC5D0\uAC8C \uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)"),
2123
+ userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
2124
+ },
2125
+ async ({ eventId, sendNotification, userId }) => {
2126
+ try {
2127
+ await deleteEvent(
2128
+ eventId,
2129
+ userId ?? "me",
2130
+ sendNotification ?? false
2131
+ );
2132
+ return {
2133
+ content: [{ type: "text", text: JSON.stringify({ success: true, eventId, message: "\uC77C\uC815\uC774 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4" }) }]
2134
+ };
2135
+ } catch (err) {
2136
+ return {
2137
+ content: [{ type: "text", text: mcpErrorHint(err, "calendar.delete") }],
2138
+ isError: true
2139
+ };
2140
+ }
2141
+ }
2142
+ );
2143
+ server.tool(
2144
+ "nworks_drive_list",
2145
+ "NAVER WORKS \uB4DC\uB77C\uC774\uBE0C\uC758 \uD30C\uC77C/\uD3F4\uB354 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. '\uB4DC\uB77C\uC774\uBE0C \uD30C\uC77C \uBCF4\uC5EC\uC918', '\uB0B4 \uD30C\uC77C \uBAA9\uB85D' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. User OAuth \uC778\uC99D \uD544\uC694 (file.read scope)",
2146
+ {
2147
+ userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)"),
2148
+ folderId: z.string().optional().describe("\uD3F4\uB354 ID (\uBBF8\uC9C0\uC815 \uC2DC \uB8E8\uD2B8)"),
2149
+ count: z.number().optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 200)"),
2150
+ cursor: z.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
2151
+ },
2152
+ async ({ userId, folderId, count, cursor }) => {
2153
+ try {
2154
+ const result = await listFiles(
2155
+ userId ?? "me",
2156
+ folderId,
2157
+ count ?? 20,
2158
+ cursor
2159
+ );
2160
+ const files = result.files.map((f) => ({
2161
+ fileId: f.fileId,
2162
+ name: f.fileName,
2163
+ type: f.fileType,
2164
+ size: f.fileSize,
2165
+ modified: f.modifiedTime,
2166
+ path: f.filePath
2167
+ }));
2168
+ return {
2169
+ content: [{ type: "text", text: JSON.stringify({ files, count: files.length, hasMore: !!result.responseMetaData?.nextCursor, nextCursor: result.responseMetaData?.nextCursor ?? null }) }]
2170
+ };
2171
+ } catch (err) {
2172
+ return {
2173
+ content: [{ type: "text", text: mcpErrorHint(err, "drive.list") }],
2174
+ isError: true
2175
+ };
2176
+ }
2177
+ }
2178
+ );
2179
+ server.tool(
2180
+ "nworks_drive_upload",
2181
+ "\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.",
2182
+ {
2183
+ content: z.string().optional().describe("\uC5C5\uB85C\uB4DC\uD560 \uD30C\uC77C \uB0B4\uC6A9 (base64 \uC778\uCF54\uB529). filePath \uB300\uC2E0 \uC0AC\uC6A9"),
2184
+ fileName: z.string().optional().describe("\uD30C\uC77C\uBA85 (content \uC0AC\uC6A9 \uC2DC \uD544\uC218)"),
2185
+ filePath: z.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)"),
2186
+ userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)"),
2187
+ folderId: z.string().optional().describe("\uC5C5\uB85C\uB4DC\uD560 \uD3F4\uB354 ID (\uBBF8\uC9C0\uC815 \uC2DC \uB8E8\uD2B8)"),
2188
+ overwrite: z.boolean().optional().describe("\uB3D9\uC77C \uD30C\uC77C\uBA85 \uB36E\uC5B4\uC4F0\uAE30 (\uAE30\uBCF8: false)")
2189
+ },
2190
+ async ({ content, fileName, filePath, userId, folderId, overwrite }) => {
2191
+ try {
2192
+ let result;
2193
+ if (content && fileName) {
2194
+ const buffer = Buffer.from(content, "base64");
2195
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2196
+ console.error(`[nworks] MCP upload: fileName=${fileName}, bufferSize=${buffer.length}`);
2197
+ }
2198
+ result = await uploadBuffer(
2199
+ buffer,
2200
+ fileName,
2201
+ userId ?? "me",
2202
+ folderId,
2203
+ overwrite ?? false
2204
+ );
2205
+ } else if (filePath) {
2206
+ const safePath = validateLocalPath(filePath);
2207
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2208
+ console.error(`[nworks] MCP upload: filePath=${safePath}`);
2209
+ }
2210
+ result = await uploadFile(
2211
+ safePath,
2212
+ userId ?? "me",
2213
+ folderId,
2214
+ overwrite ?? false
2215
+ );
2216
+ } else {
2217
+ return {
2218
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: "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." }) }],
2219
+ isError: true
2220
+ };
2221
+ }
2222
+ return {
2223
+ content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }]
2224
+ };
2225
+ } catch (err) {
2226
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2227
+ console.error(`[nworks] drive upload error: ${err.stack}`);
2228
+ }
2229
+ return {
2230
+ content: [{ type: "text", text: mcpErrorHint(err, "drive.upload") }],
2231
+ isError: true
2232
+ };
2233
+ }
2234
+ }
2235
+ );
2236
+ server.tool(
2237
+ "nworks_drive_download",
2238
+ "\uB4DC\uB77C\uC774\uBE0C \uD30C\uC77C\uC744 \uB2E4\uC6B4\uB85C\uB4DC\uD569\uB2C8\uB2E4. User OAuth \uC778\uC99D \uD544\uC694 (file.read scope). outputDir\uC744 \uC9C0\uC815\uD558\uBA74 \uB85C\uCEEC\uC5D0 \uD30C\uC77C\uB85C \uC800\uC7A5\uD558\uACE0, \uBBF8\uC9C0\uC815 \uC2DC \uD30C\uC77C \uB0B4\uC6A9\uC744 \uC9C1\uC811 \uBC18\uD658\uD569\uB2C8\uB2E4 (\uD14D\uC2A4\uD2B8\uB294 text, \uBC14\uC774\uB108\uB9AC\uB294 base64). 5MB \uCD08\uACFC \uD30C\uC77C\uC740 \uBC18\uB4DC\uC2DC outputDir\uB97C \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.",
2239
+ {
2240
+ fileId: z.string().describe("\uB2E4\uC6B4\uB85C\uB4DC\uD560 \uD30C\uC77C ID (nworks_drive_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
2241
+ outputDir: z.string().optional().describe("\uC800\uC7A5 \uB514\uB809\uD1A0\uB9AC (\uC9C0\uC815 \uC2DC \uD30C\uC77C\uB85C \uC800\uC7A5, \uBBF8\uC9C0\uC815 \uC2DC \uB0B4\uC6A9\uC744 \uC9C1\uC811 \uBC18\uD658)"),
2242
+ outputName: z.string().optional().describe("\uC800\uC7A5 \uD30C\uC77C\uBA85 (\uBBF8\uC9C0\uC815 \uC2DC \uC6D0\uBCF8 \uD30C\uC77C\uBA85)"),
2243
+ userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
2244
+ },
2245
+ async ({ fileId, outputDir, outputName, userId }) => {
2246
+ try {
2247
+ const result = await downloadFile(
2248
+ fileId,
2249
+ userId ?? "me"
2250
+ );
2251
+ const fileName = outputName ?? result.fileName ?? fileId;
2252
+ if (outputDir) {
2253
+ const { writeFile: writeFile2 } = await import("fs/promises");
2254
+ const { join: join2 } = await import("path");
2255
+ const safeDir = validateLocalPath(outputDir);
2256
+ const safeName = sanitizeFileName(fileName);
2257
+ const outPath = join2(safeDir, safeName);
2258
+ validateLocalPath(outPath, safeDir);
2259
+ await writeFile2(outPath, result.buffer);
2260
+ return {
2261
+ content: [{ type: "text", text: JSON.stringify({ success: true, fileName, path: outPath, size: result.buffer.length }) }]
2262
+ };
2263
+ }
2264
+ const MAX_INLINE_SIZE = 5 * 1024 * 1024;
2265
+ if (result.buffer.length > MAX_INLINE_SIZE) {
2266
+ const sizeMB = (result.buffer.length / (1024 * 1024)).toFixed(1);
2267
+ return {
2268
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: `\uD30C\uC77C\uC774 \uB108\uBB34 \uD07D\uB2C8\uB2E4 (${sizeMB}MB). outputDir\uB97C \uC9C0\uC815\uD574\uC11C \uB85C\uCEEC\uC5D0 \uC800\uC7A5\uD558\uC138\uC694.`, fileName, size: result.buffer.length }) }],
2269
+ isError: true
2270
+ };
2271
+ }
2272
+ const textExtensions = /\.(txt|md|csv|json|xml|html|htm|css|js|ts|jsx|tsx|yaml|yml|toml|ini|cfg|conf|log|sh|bash|zsh|py|rb|java|go|rs|c|cpp|h|hpp|sql|graphql|env|gitignore|dockerignore|editorconfig)$/i;
2273
+ const isText = textExtensions.test(fileName);
2274
+ if (isText) {
2275
+ const text = result.buffer.toString("utf-8");
2276
+ return {
2277
+ content: [{ type: "text", text: JSON.stringify({ success: true, fileName, size: result.buffer.length, encoding: "text", content: text }) }]
2278
+ };
2279
+ }
2280
+ const base64 = result.buffer.toString("base64");
2281
+ return {
2282
+ content: [{ type: "text", text: JSON.stringify({ success: true, fileName, size: result.buffer.length, encoding: "base64", content: base64 }) }]
2283
+ };
2284
+ } catch (err) {
2285
+ return {
2286
+ content: [{ type: "text", text: mcpErrorHint(err, "drive.download") }],
2287
+ isError: true
2288
+ };
2289
+ }
2290
+ }
2291
+ );
2292
+ server.tool(
2293
+ "nworks_mail_send",
2294
+ "NAVER WORKS \uBA54\uC77C\uC744 \uC804\uC1A1\uD569\uB2C8\uB2E4. '\uBA54\uC77C \uBCF4\uB0B4\uC918', '\uC774\uBA54\uC77C \uC791\uC131\uD574\uC918' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. \uBE44\uB3D9\uAE30 \uC804\uC1A1(\uC131\uACF5 \uC2DC 202). User OAuth \uC778\uC99D \uD544\uC694 (mail scope)",
2295
+ {
2296
+ to: z.string().describe("\uC218\uC2E0\uC790 \uC774\uBA54\uC77C (\uC5EC\uB7EC \uBA85\uC740 ; \uB85C \uAD6C\uBD84)"),
2297
+ subject: z.string().describe("\uBA54\uC77C \uC81C\uBAA9"),
2298
+ body: z.string().optional().describe("\uBA54\uC77C \uBCF8\uBB38"),
2299
+ cc: z.string().optional().describe("\uCC38\uC870 \uC774\uBA54\uC77C (\uC5EC\uB7EC \uBA85\uC740 ; \uB85C \uAD6C\uBD84)"),
2300
+ bcc: z.string().optional().describe("\uC228\uC740\uCC38\uC870 \uC774\uBA54\uC77C (\uC5EC\uB7EC \uBA85\uC740 ; \uB85C \uAD6C\uBD84)"),
2301
+ contentType: z.enum(["html", "text"]).optional().describe("\uBCF8\uBB38 \uD615\uC2DD (\uAE30\uBCF8: html)"),
2302
+ userId: z.string().optional().describe("\uBC1C\uC2E0\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
2303
+ },
2304
+ async ({ to, subject, body, cc, bcc, contentType, userId }) => {
2305
+ try {
2306
+ await sendMail({
2307
+ to,
2308
+ subject,
2309
+ body,
2310
+ cc,
2311
+ bcc,
2312
+ contentType,
2313
+ userId: userId ?? "me"
2314
+ });
2315
+ return {
2316
+ content: [{ type: "text", text: JSON.stringify({ success: true, message: "\uBA54\uC77C\uC774 \uC804\uC1A1\uB418\uC5C8\uC2B5\uB2C8\uB2E4 (\uBE44\uB3D9\uAE30 \uCC98\uB9AC)" }) }]
2317
+ };
2318
+ } catch (err) {
2319
+ return {
2320
+ content: [{ type: "text", text: mcpErrorHint(err, "mail.send") }],
2321
+ isError: true
2322
+ };
2323
+ }
2324
+ }
2325
+ );
2326
+ server.tool(
2327
+ "nworks_mail_list",
2328
+ "\uBC1B\uC740 \uBA54\uC77C \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. '\uBA54\uC77C \uD655\uC778\uD574\uC918', '\uBC1B\uC740\uD3B8\uC9C0\uD568 \uBCF4\uC5EC\uC918', '\uC548 \uC77D\uC740 \uBA54\uC77C \uC788\uC5B4?' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. User OAuth \uC778\uC99D \uD544\uC694 (mail.read scope)",
2329
+ {
2330
+ folderId: z.number().optional().describe("\uBA54\uC77C \uD3F4\uB354 ID (\uAE30\uBCF8: 0 = \uBC1B\uC740\uD3B8\uC9C0\uD568)"),
2331
+ count: z.number().optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 30, \uCD5C\uB300: 200)"),
2332
+ cursor: z.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C"),
2333
+ isUnread: z.boolean().optional().describe("\uC77D\uC9C0 \uC54A\uC740 \uBA54\uC77C\uB9CC \uC870\uD68C"),
2334
+ userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
2335
+ },
2336
+ async ({ folderId, count, cursor, isUnread, userId }) => {
2337
+ try {
2338
+ const result = await listMails(
2339
+ folderId ?? 0,
2340
+ userId ?? "me",
2341
+ count ?? 30,
2342
+ cursor,
2343
+ isUnread
2344
+ );
2345
+ const mails = result.mails.map((m) => ({
2346
+ mailId: m.mailId,
2347
+ from: m.from.email,
2348
+ subject: m.subject,
2349
+ date: m.receivedTime,
2350
+ status: m.status,
2351
+ attachments: m.attachCount ?? 0
2352
+ }));
2353
+ return {
2354
+ content: [{ type: "text", text: JSON.stringify({ mails, count: mails.length, totalCount: result.totalCount, unreadCount: result.unreadCount, hasMore: !!result.responseMetaData?.nextCursor, nextCursor: result.responseMetaData?.nextCursor ?? null }) }]
2355
+ };
2356
+ } catch (err) {
2357
+ return {
2358
+ content: [{ type: "text", text: mcpErrorHint(err, "mail.list") }],
2359
+ isError: true
2360
+ };
2361
+ }
2362
+ }
2363
+ );
2364
+ server.tool(
2365
+ "nworks_mail_read",
2366
+ "\uD2B9\uC815 \uBA54\uC77C\uC758 \uC0C1\uC138 \uB0B4\uC6A9(\uBCF8\uBB38, \uCCA8\uBD80\uD30C\uC77C \uB4F1)\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. '\uC774 \uBA54\uC77C \uB0B4\uC6A9 \uBCF4\uC5EC\uC918', '\uC694\uC57D\uD574\uC918' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. \uBCF8\uBB38(body)\uC740 \uAE30\uBCF8\uC801\uC73C\uB85C HTML \uD0DC\uADF8\uB97C \uAC77\uC5B4\uB0B8 \uC815\uC81C \uD14D\uC2A4\uD2B8\uB85C \uBC18\uD658\uD558\uBBC0\uB85C \uADF8\uB300\uB85C \uC77D\uACE0 \uC694\uC57D\uD558\uAE30 \uC88B\uC2B5\uB2C8\uB2E4. \uC6D0\uBCF8 HTML\uC774 \uD544\uC694\uD558\uBA74 format\uC744 'html' \uB610\uB294 'both'\uB85C \uC9C0\uC815\uD558\uC138\uC694. mailId\uB294 nworks_mail_list\uB85C \uC870\uD68C \uAC00\uB2A5. User OAuth \uC778\uC99D \uD544\uC694 (mail.read scope)",
2367
+ {
2368
+ mailId: z.number().describe("\uBA54\uC77C ID (nworks_mail_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
2369
+ userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)"),
2370
+ format: z.enum(["text", "html", "both"]).optional().describe("\uBCF8\uBB38 \uD615\uC2DD (\uAE30\uBCF8 text=\uC815\uC81C \uD14D\uC2A4\uD2B8, html=\uC6D0\uBCF8 HTML, both=\uC815\uC81C+\uC6D0\uBCF8). \uB300\uBD80\uBD84 text\uB85C \uCDA9\uBD84")
2371
+ },
2372
+ async ({ mailId, userId, format }) => {
2373
+ try {
2374
+ const result = await readMail(
2375
+ mailId,
2376
+ userId ?? "me"
2377
+ );
2378
+ const mail = result.mail;
2379
+ const fmt = format ?? "text";
2380
+ const payload = {
2381
+ mailId: mail.mailId,
2382
+ from: mail.from,
2383
+ to: mail.to,
2384
+ cc: mail.cc ?? [],
2385
+ subject: mail.subject,
2386
+ date: mail.receivedTime,
2387
+ attachments: result.attachments?.map((a) => ({
2388
+ id: a.attachmentId,
2389
+ filename: a.filename,
2390
+ contentType: a.contentType,
2391
+ size: a.size
2392
+ })) ?? []
2393
+ };
2394
+ if (fmt === "html") {
2395
+ payload.body = mail.body;
2396
+ payload.bodyFormat = "html";
2397
+ } else {
2398
+ payload.body = htmlToText(mail.body);
2399
+ payload.bodyFormat = "text";
2400
+ if (fmt === "both") payload.bodyHtml = mail.body;
2401
+ }
2402
+ return {
2403
+ content: [{ type: "text", text: JSON.stringify(payload) }]
2404
+ };
2405
+ } catch (err) {
2406
+ return {
2407
+ content: [{ type: "text", text: mcpErrorHint(err, "mail.read") }],
2408
+ isError: true
2409
+ };
2410
+ }
2411
+ }
2412
+ );
2413
+ server.tool(
2414
+ "nworks_mail_attachment",
2415
+ "\uBA54\uC77C \uCCA8\uBD80\uD30C\uC77C\uC758 \uD14D\uC2A4\uD2B8 \uB0B4\uC6A9\uC744 \uC77D\uC2B5\uB2C8\uB2E4. '\uCCA8\uBD80\uD30C\uC77C \uB0B4\uC6A9 \uD655\uC778\uD574\uC918', 'CSV \uCCA8\uBD80 \uC694\uC57D\uD574\uC918' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. \uD14D\uC2A4\uD2B8\uB958(txt\xB7csv\xB7json\xB7xml\xB7html\xB7ics \uB4F1)\uB294 \uB0B4\uC6A9\uC744 \uADF8\uB300\uB85C \uBC18\uD658\uD558\uBA70, HTML\uC740 \uC815\uC81C \uD14D\uC2A4\uD2B8\uB85C \uBC18\uD658\uD569\uB2C8\uB2E4. PDF\xB7\uC624\uD53C\uC2A4 \uBB38\uC11C\xB7\uC774\uBBF8\uC9C0 \uAC19\uC740 \uBC14\uC774\uB108\uB9AC\uB294 \uD14D\uC2A4\uD2B8 \uCD94\uCD9C\uC774 \uBD88\uAC00\uD558\uBA70(\uBCC4\uB3C4 \uD30C\uC11C \uD544\uC694), \uC774 \uACBD\uC6B0 CLI `nworks mail attachment --out`\uC73C\uB85C \uC800\uC7A5\uD558\uB3C4\uB85D \uC548\uB0B4\uD569\uB2C8\uB2E4. mailId\xB7attachmentId\uB294 nworks_mail_read\uB85C \uC870\uD68C \uAC00\uB2A5. User OAuth \uC778\uC99D \uD544\uC694 (mail.read scope)",
2416
+ {
2417
+ mailId: z.number().describe("\uBA54\uC77C ID (nworks_mail_read\uB85C \uC870\uD68C \uAC00\uB2A5)"),
2418
+ attachmentId: z.number().describe("\uCCA8\uBD80\uD30C\uC77C ID (nworks_mail_read\uC758 attachments[].id)"),
2419
+ userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
2420
+ },
2421
+ async ({ mailId, attachmentId, userId }) => {
2422
+ try {
2423
+ const { filename, buffer } = await downloadAttachment(
2424
+ mailId,
2425
+ attachmentId,
2426
+ userId ?? "me"
2427
+ );
2428
+ const text = attachmentTextOrNull(filename, buffer);
2429
+ if (text !== null) {
2430
+ return {
2431
+ content: [{ type: "text", text: JSON.stringify({ filename, size: buffer.length, text }) }]
2432
+ };
2433
+ }
2434
+ return {
2435
+ content: [{ type: "text", text: JSON.stringify({
2436
+ filename,
2437
+ size: buffer.length,
2438
+ binary: true,
2439
+ message: "\uBC14\uC774\uB108\uB9AC \uCCA8\uBD80\uD30C\uC77C\uC774\uB77C \uD14D\uC2A4\uD2B8 \uCD94\uCD9C\uC774 \uBD88\uAC00\uD569\uB2C8\uB2E4. CLI `nworks mail attachment --id <mailId> --attachment <attachmentId> --out <\uACBD\uB85C>`\uB85C \uC800\uC7A5\uD558\uC138\uC694."
2440
+ }) }]
2441
+ };
2442
+ } catch (err) {
2443
+ return {
2444
+ content: [{ type: "text", text: mcpErrorHint(err, "mail.read") }],
2445
+ isError: true
2446
+ };
2447
+ }
2448
+ }
2449
+ );
2450
+ server.tool(
2451
+ "nworks_task_list",
2452
+ "\uD560 \uC77C(TODO) \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. '\uD560 \uC77C \uD655\uC778\uD574\uC918', 'TODO \uBAA9\uB85D \uBCF4\uC5EC\uC918', '\uB0A8\uC740 \uC5C5\uBB34 \uBB50 \uC788\uC5B4?' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. User OAuth \uC778\uC99D \uD544\uC694 (task.read scope)",
2453
+ {
2454
+ categoryId: z.string().optional().describe("\uCE74\uD14C\uACE0\uB9AC ID (\uAE30\uBCF8: default)"),
2455
+ status: z.enum(["TODO", "ALL"]).optional().describe("\uD544\uD130: TODO \uB610\uB294 ALL (\uAE30\uBCF8: ALL)"),
2456
+ count: z.number().optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 50, \uCD5C\uB300: 100)"),
2457
+ cursor: z.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C"),
2458
+ userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
2459
+ },
2460
+ async ({ categoryId, status, count, cursor, userId }) => {
2461
+ try {
2462
+ const result = await listTasks(
2463
+ categoryId ?? "default",
2464
+ userId ?? "me",
2465
+ count ?? 50,
2466
+ cursor,
2467
+ status ?? "ALL"
2468
+ );
2469
+ const tasks = result.tasks.map((t) => ({
2470
+ taskId: t.taskId,
2471
+ title: t.title,
2472
+ status: t.status,
2473
+ dueDate: t.dueDate,
2474
+ assignor: t.assignorName ?? t.assignorId,
2475
+ created: t.createdTime
2476
+ }));
2477
+ return {
2478
+ content: [{ type: "text", text: JSON.stringify({ tasks, count: tasks.length, hasMore: !!result.responseMetaData?.nextCursor, nextCursor: result.responseMetaData?.nextCursor ?? null }) }]
2479
+ };
2480
+ } catch (err) {
2481
+ return {
2482
+ content: [{ type: "text", text: mcpErrorHint(err, "task.list") }],
2483
+ isError: true
2484
+ };
2485
+ }
2486
+ }
2487
+ );
2488
+ server.tool(
2489
+ "nworks_task_create",
2490
+ "\uD560 \uC77C(TODO)\uC744 \uC0C8\uB85C \uB9CC\uB4ED\uB2C8\uB2E4. '\uD560 \uC77C \uCD94\uAC00\uD574\uC918', 'TODO \uB4F1\uB85D\uD574\uC918' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. \uAE30\uBCF8\uC801\uC73C\uB85C \uC790\uAE30 \uC790\uC2E0\uC5D0\uAC8C \uD560\uB2F9. User OAuth \uC778\uC99D \uD544\uC694 (task + user.read scope)",
2491
+ {
2492
+ title: z.string().describe("\uD560 \uC77C \uC81C\uBAA9"),
2493
+ content: z.string().optional().describe("\uD560 \uC77C \uB0B4\uC6A9"),
2494
+ dueDate: z.string().optional().describe("\uB9C8\uAC10\uC77C (YYYY-MM-DD)"),
2495
+ categoryId: z.string().optional().describe("\uCE74\uD14C\uACE0\uB9AC ID"),
2496
+ assigneeIds: z.array(z.string()).optional().describe("\uB2F4\uB2F9\uC790 user ID \uBAA9\uB85D (\uBBF8\uC9C0\uC815 \uC2DC \uC790\uAE30 \uC790\uC2E0)"),
2497
+ userId: z.string().optional().describe("\uC0DD\uC131\uC790 user ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
2498
+ },
2499
+ async ({ title, content, dueDate, categoryId, assigneeIds, userId }) => {
2500
+ try {
2501
+ const result = await createTask({
2502
+ title,
2503
+ content,
2504
+ dueDate,
2505
+ categoryId,
2506
+ assigneeIds,
2507
+ userId: userId ?? "me"
2508
+ });
2509
+ return {
2510
+ content: [{ type: "text", text: JSON.stringify({ success: true, taskId: result.taskId, title: result.title, status: result.status, dueDate: result.dueDate }) }]
2511
+ };
2512
+ } catch (err) {
2513
+ return {
2514
+ content: [{ type: "text", text: mcpErrorHint(err, "task.create") }],
2515
+ isError: true
2516
+ };
2517
+ }
2518
+ }
2519
+ );
2520
+ server.tool(
2521
+ "nworks_task_update",
2522
+ "\uD560 \uC77C\uC744 \uC218\uC815\uD558\uAC70\uB098 \uC644\uB8CC \uCC98\uB9AC\uD569\uB2C8\uB2E4. '\uD560 \uC77C \uC644\uB8CC \uCC98\uB9AC\uD574\uC918', '\uB9C8\uAC10\uC77C \uBCC0\uACBD\uD574\uC918' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. taskId\uB294 nworks_task_list\uB85C \uC870\uD68C \uAC00\uB2A5. User OAuth \uC778\uC99D \uD544\uC694 (task + user.read scope)",
2523
+ {
2524
+ taskId: z.string().describe("\uD560 \uC77C ID (nworks_task_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
2525
+ status: z.enum(["done", "todo"]).optional().describe("\uC0C1\uD0DC \uBCC0\uACBD: done(\uC644\uB8CC) \uB610\uB294 todo(\uBBF8\uC644\uB8CC)"),
2526
+ title: z.string().optional().describe("\uC0C8 \uC81C\uBAA9"),
2527
+ content: z.string().optional().describe("\uC0C8 \uB0B4\uC6A9"),
2528
+ dueDate: z.string().optional().describe("\uC0C8 \uB9C8\uAC10\uC77C (YYYY-MM-DD)")
2529
+ },
2530
+ async ({ taskId, status, title, content, dueDate }) => {
2531
+ try {
2532
+ if (status) {
2533
+ if (status === "done") {
2534
+ await completeTask(taskId);
2535
+ } else {
2536
+ await incompleteTask(taskId);
2537
+ }
2538
+ }
2539
+ if (title !== void 0 || content !== void 0 || dueDate !== void 0) {
2540
+ const result = await updateTask({ taskId, title, content, dueDate });
2541
+ return {
2542
+ content: [{ type: "text", text: JSON.stringify({ success: true, taskId: result.taskId, title: result.title, status: result.status, dueDate: result.dueDate }) }]
2543
+ };
2544
+ }
2545
+ if (status) {
2546
+ return {
2547
+ content: [{ type: "text", text: JSON.stringify({ success: true, taskId, status: status === "done" ? "DONE" : "TODO" }) }]
2548
+ };
2549
+ }
2550
+ return {
2551
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: "status, title, content, dueDate \uC911 \uD558\uB098 \uC774\uC0C1\uC744 \uC9C0\uC815\uD558\uC138\uC694." }) }],
2552
+ isError: true
2553
+ };
2554
+ } catch (err) {
2555
+ return {
2556
+ content: [{ type: "text", text: mcpErrorHint(err, "task.update") }],
2557
+ isError: true
2558
+ };
2559
+ }
2560
+ }
2561
+ );
2562
+ server.tool(
2563
+ "nworks_task_delete",
2564
+ "\uD560 \uC77C\uC744 \uC0AD\uC81C\uD569\uB2C8\uB2E4. taskId\uB294 nworks_task_list\uB85C \uC870\uD68C \uAC00\uB2A5. User OAuth \uC778\uC99D \uD544\uC694 (task + user.read scope)",
2565
+ {
2566
+ taskId: z.string().describe("\uC0AD\uC81C\uD560 \uD560 \uC77C ID (nworks_task_list\uB85C \uC870\uD68C \uAC00\uB2A5)")
2567
+ },
2568
+ async ({ taskId }) => {
2569
+ try {
2570
+ await deleteTask(taskId);
2571
+ return {
2572
+ content: [{ type: "text", text: JSON.stringify({ success: true, taskId, message: "\uD560 \uC77C\uC774 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4" }) }]
2573
+ };
2574
+ } catch (err) {
2575
+ return {
2576
+ content: [{ type: "text", text: mcpErrorHint(err, "task.delete") }],
2577
+ isError: true
2578
+ };
2579
+ }
2580
+ }
2581
+ );
2582
+ server.tool(
2583
+ "nworks_board_list",
2584
+ "NAVER WORKS \uAC8C\uC2DC\uD310 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. '\uAC8C\uC2DC\uD310 \uBB50 \uC788\uC5B4?', '\uACF5\uC9C0\uC0AC\uD56D \uAC8C\uC2DC\uD310 \uCC3E\uC544\uC918' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. User OAuth \uC778\uC99D \uD544\uC694 (board.read scope)",
2585
+ {
2586
+ count: z.number().optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20)"),
2587
+ cursor: z.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
2588
+ },
2589
+ async ({ count, cursor }) => {
2590
+ try {
2591
+ const result = await listBoards(count ?? 20, cursor);
2592
+ const boards = result.boards.map((b) => ({
2593
+ boardId: b.boardId,
2594
+ boardName: b.boardName,
2595
+ description: b.description ?? ""
2596
+ }));
2597
+ return {
2598
+ content: [{ type: "text", text: JSON.stringify({ boards, count: boards.length, hasMore: !!result.responseMetaData?.nextCursor, nextCursor: result.responseMetaData?.nextCursor ?? null }) }]
2599
+ };
2600
+ } catch (err) {
2601
+ return {
2602
+ content: [{ type: "text", text: mcpErrorHint(err, "board.list") }],
2603
+ isError: true
2604
+ };
2605
+ }
2606
+ }
2607
+ );
2608
+ server.tool(
2609
+ "nworks_board_posts",
2610
+ "\uAC8C\uC2DC\uD310\uC758 \uAE00 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. '\uAC8C\uC2DC\uD310 \uAE00 \uBCF4\uC5EC\uC918', '\uACF5\uC9C0\uC0AC\uD56D \uD655\uC778' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. boardId\uB294 nworks_board_list\uB85C \uC870\uD68C \uAC00\uB2A5. User OAuth \uC778\uC99D \uD544\uC694 (board.read scope)",
2611
+ {
2612
+ boardId: z.string().describe("\uAC8C\uC2DC\uD310 ID (nworks_board_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
2613
+ count: z.number().optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 40)"),
2614
+ cursor: z.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
2615
+ },
2616
+ async ({ boardId, count, cursor }) => {
2617
+ try {
2618
+ const result = await listPosts(boardId, count ?? 20, cursor);
2619
+ const posts = result.posts.map((p) => ({
2620
+ postId: p.postId,
2621
+ title: p.title,
2622
+ userName: p.userName ?? "",
2623
+ readCount: p.readCount ?? 0,
2624
+ commentCount: p.commentCount ?? 0,
2625
+ createdTime: p.createdTime ?? ""
2626
+ }));
2627
+ return {
2628
+ content: [{ type: "text", text: JSON.stringify({ posts, count: posts.length, hasMore: !!result.responseMetaData?.nextCursor, nextCursor: result.responseMetaData?.nextCursor ?? null }) }]
2629
+ };
2630
+ } catch (err) {
2631
+ return {
2632
+ content: [{ type: "text", text: mcpErrorHint(err, "board.posts") }],
2633
+ isError: true
2634
+ };
2635
+ }
2636
+ }
2637
+ );
2638
+ server.tool(
2639
+ "nworks_board_read",
2640
+ "\uAC8C\uC2DC\uD310 \uAE00\uC758 \uC0C1\uC138 \uB0B4\uC6A9\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. postId\uB294 nworks_board_posts\uB85C \uC870\uD68C \uAC00\uB2A5. User OAuth \uC778\uC99D \uD544\uC694 (board.read scope)",
2641
+ {
2642
+ boardId: z.string().describe("\uAC8C\uC2DC\uD310 ID (nworks_board_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
2643
+ postId: z.string().describe("\uAE00 ID (nworks_board_posts\uB85C \uC870\uD68C \uAC00\uB2A5)")
2644
+ },
2645
+ async ({ boardId, postId }) => {
2646
+ try {
2647
+ const post = await readPost(boardId, postId);
2648
+ return {
2649
+ content: [{ type: "text", text: JSON.stringify({
2650
+ postId: post.postId,
2651
+ boardId: post.boardId,
2652
+ title: post.title,
2653
+ body: post.body ?? "",
2654
+ userName: post.userName ?? "",
2655
+ readCount: post.readCount ?? 0,
2656
+ commentCount: post.commentCount ?? 0,
2657
+ createdTime: post.createdTime ?? "",
2658
+ updatedTime: post.updatedTime ?? ""
2659
+ }) }]
2660
+ };
2661
+ } catch (err) {
2662
+ return {
2663
+ content: [{ type: "text", text: mcpErrorHint(err, "board.read") }],
2664
+ isError: true
2665
+ };
2666
+ }
2667
+ }
2668
+ );
2669
+ server.tool(
2670
+ "nworks_board_create",
2671
+ "\uAC8C\uC2DC\uD310\uC5D0 \uAE00\uC744 \uC791\uC131\uD569\uB2C8\uB2E4. '\uAC8C\uC2DC\uD310\uC5D0 \uAE00 \uC62C\uB824\uC918', '\uACF5\uC9C0 \uC791\uC131\uD574\uC918' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. boardId\uB294 nworks_board_list\uB85C \uC870\uD68C \uAC00\uB2A5. User OAuth \uC778\uC99D \uD544\uC694 (board scope)",
2672
+ {
2673
+ boardId: z.string().describe("\uAC8C\uC2DC\uD310 ID (nworks_board_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
2674
+ title: z.string().describe("\uAE00 \uC81C\uBAA9"),
2675
+ body: z.string().optional().describe("\uAE00 \uBCF8\uBB38"),
2676
+ enableComment: z.boolean().optional().describe("\uB313\uAE00 \uD5C8\uC6A9 (\uAE30\uBCF8: true)"),
2677
+ sendNotifications: z.boolean().optional().describe("\uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)")
2678
+ },
2679
+ async ({ boardId, title, body, enableComment, sendNotifications }) => {
2680
+ try {
2681
+ const post = await createPost({
2682
+ boardId,
2683
+ title,
2684
+ body,
2685
+ enableComment,
2686
+ sendNotifications
2687
+ });
2688
+ return {
2689
+ content: [{ type: "text", text: JSON.stringify({ success: true, postId: post.postId, boardId: post.boardId, title: post.title }) }]
2690
+ };
2691
+ } catch (err) {
2692
+ return {
2693
+ content: [{ type: "text", text: mcpErrorHint(err, "board.create") }],
2694
+ isError: true
2695
+ };
2696
+ }
2697
+ }
2698
+ );
2699
+ server.tool(
2700
+ "nworks_login_user",
2701
+ "User OAuth \uB85C\uADF8\uC778\uC744 \uC2DC\uC791\uD569\uB2C8\uB2E4. \uBC18\uD658\uB41C URL\uC744 \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC5F4\uC5B4 NAVER WORKS\uC5D0 \uB85C\uADF8\uC778\uD558\uC138\uC694. \uB85C\uADF8\uC778 \uC644\uB8CC \uD6C4 \uC790\uB3D9\uC73C\uB85C \uD1A0\uD070\uC774 \uC800\uC7A5\uB429\uB2C8\uB2E4. \uC911\uC694: scope\uB97C \uC9C0\uC815\uD558\uC9C0 \uB9C8\uC138\uC694. \uAE30\uBCF8\uAC12\uC774 \uBAA8\uB4E0 API(\uCE98\uB9B0\uB354, \uBA54\uC77C, \uD560\uC77C, \uB4DC\uB77C\uC774\uBE0C, \uAC8C\uC2DC\uD310)\uB97C \uD3EC\uD568\uD558\uBBC0\uB85C \uD55C \uBC88 \uB85C\uADF8\uC778\uC73C\uB85C \uC804\uCCB4 \uAE30\uB2A5\uC744 \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4. scope\uB97C \uC881\uAC8C \uC9C0\uC815\uD558\uBA74 \uB2E4\uB978 \uAE30\uB2A5 \uC0AC\uC6A9 \uC2DC \uC7AC\uB85C\uADF8\uC778\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.",
2702
+ {
2703
+ scope: z.string().optional().describe("\uC9C0\uC815\uD558\uC9C0 \uB9C8\uC138\uC694 (\uAE30\uBCF8\uAC12\uC774 \uC804\uCCB4 scope \uD3EC\uD568). \uD2B9\uC218\uD55C \uACBD\uC6B0\uC5D0\uB9CC \uC0AC\uC6A9")
2704
+ },
2705
+ async ({ scope }) => {
2706
+ const DEFAULT_SCOPE = "calendar calendar.read file file.read mail mail.read task task.read user.read board board.read";
2707
+ try {
2708
+ const creds = await loadCredentials();
2709
+ const SCOPE_DEPS = {
2710
+ calendar: ["calendar.read"],
2711
+ task: ["user.read"],
2712
+ "task.read": ["user.read"]
2713
+ };
2714
+ const expandScopes = (scopes) => {
2715
+ const expanded = new Set(scopes);
2716
+ for (const s of scopes) {
2717
+ const deps = SCOPE_DEPS[s];
2718
+ if (deps) deps.forEach((d) => expanded.add(d));
2719
+ }
2720
+ return [...expanded];
2721
+ };
2722
+ const existingToken = await loadUserToken();
2723
+ const existingScopes = existingToken?.scope?.split(" ").filter(Boolean) ?? [];
2724
+ const requestedScopes = expandScopes((scope ?? DEFAULT_SCOPE).split(" ").filter(Boolean));
2725
+ const mergedScopes = [.../* @__PURE__ */ new Set([...existingScopes, ...requestedScopes])].join(" ");
2726
+ const state = generateSecureState();
2727
+ const authorizeUrl = buildAuthorizeUrl(creds.clientId, mergedScopes, state);
2728
+ startOAuthCallbackServer(creds.clientId, creds.clientSecret, state).then(
2729
+ (token) => saveUserToken({
2730
+ accessToken: token.accessToken,
2731
+ refreshToken: token.refreshToken,
2732
+ expiresAt: token.expiresAt,
2733
+ scope: token.scope
2734
+ })
2735
+ ).then(() => {
2736
+ console.error("[nworks] User OAuth login successful. Token saved.");
2737
+ }).catch((err) => {
2738
+ console.error(`[nworks] User OAuth login failed: ${err.message}`);
2739
+ });
2740
+ return {
2741
+ content: [
2742
+ {
2743
+ type: "text",
2744
+ text: JSON.stringify({
2745
+ message: "\uC544\uB798 URL\uC744 \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC5F4\uC5B4 NAVER WORKS\uC5D0 \uB85C\uADF8\uC778\uD558\uC138\uC694. \uB85C\uADF8\uC778 \uC644\uB8CC \uD6C4 \uC790\uB3D9\uC73C\uB85C \uD1A0\uD070\uC774 \uC800\uC7A5\uB429\uB2C8\uB2E4. (\uC81C\uD55C\uC2DC\uAC04: 120\uCD08)",
2746
+ loginUrl: authorizeUrl,
2747
+ scope: mergedScopes,
2748
+ callbackPort: 9876,
2749
+ timeout: "120\uCD08"
2750
+ })
2751
+ }
2752
+ ]
2753
+ };
2754
+ } catch (err) {
2755
+ return {
2756
+ content: [{ type: "text", text: mcpErrorHint(err) }],
2757
+ isError: true
2758
+ };
2759
+ }
2760
+ }
2761
+ );
2762
+ server.tool(
2763
+ "nworks_whoami",
2764
+ "\uD604\uC7AC \uC778\uC99D\uB41C NAVER WORKS \uACC4\uC815 \uC815\uBCF4\uC640 \uD1A0\uD070 \uC720\uD6A8 \uC0C1\uD0DC\uB97C \uD655\uC778\uD569\uB2C8\uB2E4. \uC778\uC99D \uBB38\uC81C \uC9C4\uB2E8 \uC2DC \uBA3C\uC800 \uD638\uCD9C",
2765
+ {},
2766
+ async () => {
2767
+ try {
2768
+ const creds = await loadCredentials();
2769
+ const token = await loadToken();
2770
+ const userToken = await loadUserToken();
2771
+ const isValid = token ? token.expiresAt > Date.now() / 1e3 : false;
2772
+ const userTokenValid = userToken ? userToken.expiresAt > Date.now() / 1e3 : false;
2773
+ const mask = (s) => s.length <= 4 ? "****" : `****${s.slice(-4)}`;
2774
+ const info = {
2775
+ serviceAccount: creds.serviceAccount ?? null,
2776
+ clientId: mask(creds.clientId),
2777
+ botId: creds.botId ?? null,
2778
+ tokenValid: isValid,
2779
+ userOAuth: userToken ? { valid: userTokenValid, scope: userToken.scope } : null
2780
+ };
2781
+ return {
2782
+ content: [{ type: "text", text: JSON.stringify(info) }]
2783
+ };
2784
+ } catch (err) {
2785
+ return {
2786
+ content: [{ type: "text", text: mcpErrorHint(err) }],
2787
+ isError: true
2788
+ };
2789
+ }
2790
+ }
2791
+ );
2792
+ server.tool(
2793
+ "nworks_logout",
2794
+ "\uC800\uC7A5\uB41C NAVER WORKS \uC778\uC99D \uC815\uBCF4\uC640 \uD1A0\uD070\uC744 \uBAA8\uB450 \uC0AD\uC81C\uD569\uB2C8\uB2E4",
2795
+ {},
2796
+ async () => {
2797
+ try {
2798
+ try {
2799
+ const creds = await loadCredentials();
2800
+ const token = await loadToken();
2801
+ const userToken = await loadUserToken();
2802
+ if (token?.accessToken) {
2803
+ await revokeToken(token.accessToken, creds.clientId, creds.clientSecret);
2804
+ }
2805
+ if (userToken?.refreshToken) {
2806
+ await revokeToken(userToken.refreshToken, creds.clientId, creds.clientSecret);
2807
+ }
2808
+ } catch {
2809
+ }
2810
+ await clearCredentials();
2811
+ return {
2812
+ content: [
2813
+ {
2814
+ type: "text",
2815
+ text: JSON.stringify({
2816
+ success: true,
2817
+ message: "\uC778\uC99D \uC815\uBCF4\uC640 \uD1A0\uD070\uC774 \uBAA8\uB450 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uB2E4\uC2DC \uC0AC\uC6A9\uD558\uB824\uBA74 nworks_setup tool\uB85C \uC7AC\uC124\uC815 \uD6C4 nworks_login_user\uB85C \uBE0C\uB77C\uC6B0\uC800 \uB85C\uADF8\uC778\uD558\uC138\uC694."
2818
+ })
2819
+ }
2820
+ ]
2821
+ };
2822
+ } catch (err) {
2823
+ return {
2824
+ content: [{ type: "text", text: mcpErrorHint(err) }],
2825
+ isError: true
2826
+ };
2827
+ }
2828
+ }
2829
+ );
2830
+ server.tool(
2831
+ "nworks_doctor",
2832
+ "NAVER WORKS \uC5F0\uACB0 \uC0C1\uD0DC\uB97C \uC9C4\uB2E8\uD569\uB2C8\uB2E4. \uC778\uC99D \uC815\uBCF4, \uD1A0\uD070, Private Key, API \uC5F0\uACB0\uC744 \uC810\uAC80\uD569\uB2C8\uB2E4.",
2833
+ {},
2834
+ async () => {
2835
+ try {
2836
+ const results = await runChecks("default");
2837
+ const maskedResults = results.map((r) => {
2838
+ if (r.check === "credentials" && r.status === "OK") {
2839
+ return { ...r, detail: r.detail.replace(/clientId: .+/, "clientId: ****") };
2840
+ }
2841
+ if (r.check === "privateKey" && r.status === "OK") {
2842
+ return { ...r, detail: "OK (path hidden)" };
2843
+ }
2844
+ if (r.check === "serviceAccount" && r.status === "OK") {
2845
+ const masked = r.detail.length <= 4 ? "****" : `****${r.detail.slice(-4)}`;
2846
+ return { ...r, detail: masked };
2847
+ }
2848
+ return r;
2849
+ });
2850
+ return {
2851
+ content: [{ type: "text", text: JSON.stringify(maskedResults) }]
2852
+ };
2853
+ } catch (err) {
2854
+ return {
2855
+ content: [{ type: "text", text: mcpErrorHint(err) }],
2856
+ isError: true
2857
+ };
2858
+ }
2859
+ }
2860
+ );
2861
+ }
2862
+
2863
+ // src/mcp/server.ts
2864
+ async function startMcpServer() {
2865
+ try {
2866
+ await loadCredentials();
2867
+ } catch {
2868
+ console.error(
2869
+ "[nworks] Authentication required. Run: nworks login"
2870
+ );
2871
+ }
2872
+ try {
2873
+ const userToken = await loadUserToken();
2874
+ if (!userToken) {
2875
+ console.error(
2876
+ "[nworks] User OAuth not configured. Run: nworks login --user"
2877
+ );
2878
+ }
2879
+ } catch {
2880
+ }
2881
+ const server = new McpServer({
2882
+ name: "nworks",
2883
+ version: "1.0.0"
2884
+ });
2885
+ registerTools(server);
2886
+ const transport = new StdioServerTransport();
2887
+ await server.connect(transport);
2888
+ }
2889
+
2890
+ // src/mcp.ts
2891
+ startMcpServer().catch((err) => {
2892
+ console.error("MCP server failed:", err);
2893
+ process.exit(1);
2894
+ });
2895
+ //# sourceMappingURL=mcp.js.map