nworks 1.2.2 → 1.3.1
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/LICENSE +190 -190
- package/README.ja.md +459 -393
- package/README.ko.md +459 -393
- package/README.md +459 -393
- package/dist/index.js +1467 -531
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +1948 -1218
- package/dist/mcp.js.map +1 -1
- package/package.json +61 -58
package/dist/index.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
+
import { Command as Command14 } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/version.ts
|
|
4
7
|
import { createRequire } from "module";
|
|
5
|
-
|
|
8
|
+
var require2 = createRequire(import.meta.url);
|
|
9
|
+
var pkg = require2("../package.json");
|
|
10
|
+
var VERSION = pkg.version;
|
|
6
11
|
|
|
7
12
|
// src/commands/login.ts
|
|
8
13
|
import { Command } from "commander";
|
|
@@ -11,10 +16,11 @@ import { existsSync as existsSync2 } from "fs";
|
|
|
11
16
|
import { createInterface } from "readline/promises";
|
|
12
17
|
|
|
13
18
|
// src/auth/config.ts
|
|
14
|
-
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
19
|
+
import { readFile, writeFile, mkdir, chmod, rename } from "fs/promises";
|
|
15
20
|
import { existsSync } from "fs";
|
|
16
21
|
import { homedir } from "os";
|
|
17
22
|
import { join } from "path";
|
|
23
|
+
import { z } from "zod";
|
|
18
24
|
|
|
19
25
|
// src/utils/error.ts
|
|
20
26
|
var AuthError = class extends Error {
|
|
@@ -43,11 +49,40 @@ var CONFIG_DIR = join(homedir(), ".config", "nworks");
|
|
|
43
49
|
var CREDENTIALS_PATH = join(CONFIG_DIR, "credentials.json");
|
|
44
50
|
var TOKEN_PATH = join(CONFIG_DIR, "token.json");
|
|
45
51
|
var USER_TOKEN_PATH = join(CONFIG_DIR, "user-token.json");
|
|
52
|
+
var PROFILE_NAME_PATTERN = /^[\w.-]+$/;
|
|
53
|
+
var FORBIDDEN_PROFILE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
54
|
+
var DOTS_ONLY = /^\.+$/;
|
|
55
|
+
function validateProfileName(profile) {
|
|
56
|
+
if (!PROFILE_NAME_PATTERN.test(profile) || FORBIDDEN_PROFILE_KEYS.has(profile) || DOTS_ONLY.test(profile)) {
|
|
57
|
+
throw new AuthError(
|
|
58
|
+
`Invalid profile name "${profile}". Use only letters, digits, ".", "_", or "-" (and not "." / ".." alone).`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
var credentialsSchema = z.object({
|
|
63
|
+
clientId: z.string(),
|
|
64
|
+
clientSecret: z.string(),
|
|
65
|
+
serviceAccount: z.string().optional(),
|
|
66
|
+
privateKeyPath: z.string().optional(),
|
|
67
|
+
botId: z.string().optional(),
|
|
68
|
+
domainId: z.string().optional()
|
|
69
|
+
}).passthrough();
|
|
46
70
|
async function ensureConfigDir() {
|
|
47
71
|
if (!existsSync(CONFIG_DIR)) {
|
|
48
72
|
await mkdir(CONFIG_DIR, { recursive: true, ...IS_UNIX && { mode: 448 } });
|
|
73
|
+
} else if (IS_UNIX) {
|
|
74
|
+
await chmod(CONFIG_DIR, 448);
|
|
49
75
|
}
|
|
50
76
|
}
|
|
77
|
+
var tempFileCounter = 0;
|
|
78
|
+
async function writeSecureFile(path, data) {
|
|
79
|
+
const tempPath = `${path}.${process.pid}.${tempFileCounter++}.tmp`;
|
|
80
|
+
await writeFile(tempPath, data, { encoding: "utf-8", ...IS_UNIX && { mode: 384 } });
|
|
81
|
+
if (IS_UNIX) {
|
|
82
|
+
await chmod(tempPath, 384);
|
|
83
|
+
}
|
|
84
|
+
await rename(tempPath, path);
|
|
85
|
+
}
|
|
51
86
|
function getCredentialsFromEnv() {
|
|
52
87
|
const clientId = process.env["NWORKS_CLIENT_ID"];
|
|
53
88
|
const clientSecret = process.env["NWORKS_CLIENT_SECRET"];
|
|
@@ -64,6 +99,7 @@ function getCredentialsFromEnv() {
|
|
|
64
99
|
async function loadCredentials(profile = "default") {
|
|
65
100
|
const envCreds = getCredentialsFromEnv();
|
|
66
101
|
if (envCreds) return envCreds;
|
|
102
|
+
validateProfileName(profile);
|
|
67
103
|
if (!existsSync(CREDENTIALS_PATH)) {
|
|
68
104
|
throw new AuthError(
|
|
69
105
|
"Not logged in. Run `nworks login` or set environment variables."
|
|
@@ -75,9 +111,16 @@ async function loadCredentials(profile = "default") {
|
|
|
75
111
|
if (!creds) {
|
|
76
112
|
throw new AuthError(`Profile "${profile}" not found in credentials.`);
|
|
77
113
|
}
|
|
78
|
-
|
|
114
|
+
const parsed = credentialsSchema.safeParse(creds);
|
|
115
|
+
if (!parsed.success) {
|
|
116
|
+
throw new AuthError(
|
|
117
|
+
`Credentials for profile "${profile}" are malformed. Run \`nworks login\` to reconfigure.`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
return parsed.data;
|
|
79
121
|
}
|
|
80
122
|
async function saveCredentials(creds, profile = "default") {
|
|
123
|
+
validateProfileName(profile);
|
|
81
124
|
await ensureConfigDir();
|
|
82
125
|
let profiles = {};
|
|
83
126
|
if (existsSync(CREDENTIALS_PATH)) {
|
|
@@ -85,13 +128,10 @@ async function saveCredentials(creds, profile = "default") {
|
|
|
85
128
|
profiles = JSON.parse(raw);
|
|
86
129
|
}
|
|
87
130
|
profiles[profile] = creds;
|
|
88
|
-
await
|
|
89
|
-
CREDENTIALS_PATH,
|
|
90
|
-
JSON.stringify(profiles, null, 2),
|
|
91
|
-
{ encoding: "utf-8", ...IS_UNIX && { mode: 384 } }
|
|
92
|
-
);
|
|
131
|
+
await writeSecureFile(CREDENTIALS_PATH, JSON.stringify(profiles, null, 2));
|
|
93
132
|
}
|
|
94
133
|
async function loadToken(profile = "default") {
|
|
134
|
+
validateProfileName(profile);
|
|
95
135
|
if (!existsSync(TOKEN_PATH)) return null;
|
|
96
136
|
const raw = await readFile(TOKEN_PATH, "utf-8");
|
|
97
137
|
const tokens = JSON.parse(raw);
|
|
@@ -103,6 +143,7 @@ async function loadToken(profile = "default") {
|
|
|
103
143
|
};
|
|
104
144
|
}
|
|
105
145
|
async function saveToken(token, profile = "default") {
|
|
146
|
+
validateProfileName(profile);
|
|
106
147
|
await ensureConfigDir();
|
|
107
148
|
let tokens = {};
|
|
108
149
|
if (existsSync(TOKEN_PATH)) {
|
|
@@ -110,13 +151,10 @@ async function saveToken(token, profile = "default") {
|
|
|
110
151
|
tokens = JSON.parse(raw);
|
|
111
152
|
}
|
|
112
153
|
tokens[profile] = token;
|
|
113
|
-
await
|
|
114
|
-
TOKEN_PATH,
|
|
115
|
-
JSON.stringify(tokens, null, 2),
|
|
116
|
-
{ encoding: "utf-8", ...IS_UNIX && { mode: 384 } }
|
|
117
|
-
);
|
|
154
|
+
await writeSecureFile(TOKEN_PATH, JSON.stringify(tokens, null, 2));
|
|
118
155
|
}
|
|
119
156
|
async function loadUserToken(profile = "default") {
|
|
157
|
+
validateProfileName(profile);
|
|
120
158
|
if (!existsSync(USER_TOKEN_PATH)) return null;
|
|
121
159
|
const raw = await readFile(USER_TOKEN_PATH, "utf-8");
|
|
122
160
|
const tokens = JSON.parse(raw);
|
|
@@ -130,6 +168,7 @@ async function loadUserToken(profile = "default") {
|
|
|
130
168
|
};
|
|
131
169
|
}
|
|
132
170
|
async function saveUserToken(token, profile = "default") {
|
|
171
|
+
validateProfileName(profile);
|
|
133
172
|
await ensureConfigDir();
|
|
134
173
|
let tokens = {};
|
|
135
174
|
if (existsSync(USER_TOKEN_PATH)) {
|
|
@@ -137,42 +176,27 @@ async function saveUserToken(token, profile = "default") {
|
|
|
137
176
|
tokens = JSON.parse(raw);
|
|
138
177
|
}
|
|
139
178
|
tokens[profile] = token;
|
|
140
|
-
await
|
|
141
|
-
USER_TOKEN_PATH,
|
|
142
|
-
JSON.stringify(tokens, null, 2),
|
|
143
|
-
{ encoding: "utf-8", ...IS_UNIX && { mode: 384 } }
|
|
144
|
-
);
|
|
179
|
+
await writeSecureFile(USER_TOKEN_PATH, JSON.stringify(tokens, null, 2));
|
|
145
180
|
}
|
|
146
181
|
async function clearCredentials(profile = "default") {
|
|
182
|
+
validateProfileName(profile);
|
|
147
183
|
if (existsSync(CREDENTIALS_PATH)) {
|
|
148
184
|
const raw = await readFile(CREDENTIALS_PATH, "utf-8");
|
|
149
185
|
const profiles = JSON.parse(raw);
|
|
150
186
|
delete profiles[profile];
|
|
151
|
-
await
|
|
152
|
-
CREDENTIALS_PATH,
|
|
153
|
-
JSON.stringify(profiles, null, 2),
|
|
154
|
-
{ encoding: "utf-8", ...IS_UNIX && { mode: 384 } }
|
|
155
|
-
);
|
|
187
|
+
await writeSecureFile(CREDENTIALS_PATH, JSON.stringify(profiles, null, 2));
|
|
156
188
|
}
|
|
157
189
|
if (existsSync(TOKEN_PATH)) {
|
|
158
190
|
const raw = await readFile(TOKEN_PATH, "utf-8");
|
|
159
191
|
const tokens = JSON.parse(raw);
|
|
160
192
|
delete tokens[profile];
|
|
161
|
-
await
|
|
162
|
-
TOKEN_PATH,
|
|
163
|
-
JSON.stringify(tokens, null, 2),
|
|
164
|
-
{ encoding: "utf-8", ...IS_UNIX && { mode: 384 } }
|
|
165
|
-
);
|
|
193
|
+
await writeSecureFile(TOKEN_PATH, JSON.stringify(tokens, null, 2));
|
|
166
194
|
}
|
|
167
195
|
if (existsSync(USER_TOKEN_PATH)) {
|
|
168
196
|
const raw = await readFile(USER_TOKEN_PATH, "utf-8");
|
|
169
197
|
const tokens = JSON.parse(raw);
|
|
170
198
|
delete tokens[profile];
|
|
171
|
-
await
|
|
172
|
-
USER_TOKEN_PATH,
|
|
173
|
-
JSON.stringify(tokens, null, 2),
|
|
174
|
-
{ encoding: "utf-8", ...IS_UNIX && { mode: 384 } }
|
|
175
|
-
);
|
|
199
|
+
await writeSecureFile(USER_TOKEN_PATH, JSON.stringify(tokens, null, 2));
|
|
176
200
|
}
|
|
177
201
|
}
|
|
178
202
|
|
|
@@ -246,7 +270,6 @@ async function refreshToken(profile = "default") {
|
|
|
246
270
|
|
|
247
271
|
// src/auth/oauth-user.ts
|
|
248
272
|
import { createServer } from "http";
|
|
249
|
-
import { randomBytes } from "crypto";
|
|
250
273
|
import { URL as URL2 } from "url";
|
|
251
274
|
var AUTH_URL2 = "https://auth.worksmobile.com/oauth2/v2.0/authorize";
|
|
252
275
|
var TOKEN_URL = "https://auth.worksmobile.com/oauth2/v2.0/token";
|
|
@@ -262,13 +285,13 @@ function buildAuthorizeUrl(clientId, scope, state) {
|
|
|
262
285
|
});
|
|
263
286
|
return `${AUTH_URL2}?${params.toString()}`;
|
|
264
287
|
}
|
|
265
|
-
async function startUserOAuthFlow(_scope, profile
|
|
288
|
+
async function startUserOAuthFlow(_scope, profile, expectedState) {
|
|
266
289
|
const creds = await loadCredentials(profile);
|
|
267
|
-
const code = await waitForAuthCode(expectedState
|
|
290
|
+
const code = await waitForAuthCode(expectedState);
|
|
268
291
|
return exchangeCodeForToken(code, creds.clientId, creds.clientSecret);
|
|
269
292
|
}
|
|
270
293
|
function waitForAuthCode(expectedState) {
|
|
271
|
-
return new Promise((
|
|
294
|
+
return new Promise((resolve3, reject) => {
|
|
272
295
|
const timeout = setTimeout(() => {
|
|
273
296
|
server.close();
|
|
274
297
|
reject(new AuthError("OAuth login timed out (120s). Try again."));
|
|
@@ -285,10 +308,7 @@ function waitForAuthCode(expectedState) {
|
|
|
285
308
|
const state = url.searchParams.get("state");
|
|
286
309
|
if (state !== expectedState) {
|
|
287
310
|
res.writeHead(403, { "Content-Type": "text/html; charset=utf-8" });
|
|
288
|
-
res.end("<h2>\uBCF4\uC548 \uC624\uB958</h2><p>state \uBD88\uC77C\uCE58. \uB2E4\uC2DC \
|
|
289
|
-
clearTimeout(timeout);
|
|
290
|
-
server.close();
|
|
291
|
-
reject(new AuthError("OAuth state mismatch \u2014 possible CSRF attack."));
|
|
311
|
+
res.end("<h2>\uBCF4\uC548 \uC624\uB958</h2><p>state \uBD88\uC77C\uCE58. \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uB85C\uADF8\uC778\uC744 \uB2E4\uC2DC \uC9C4\uD589\uD558\uC138\uC694.</p>");
|
|
292
312
|
return;
|
|
293
313
|
}
|
|
294
314
|
if (error) {
|
|
@@ -311,7 +331,7 @@ function waitForAuthCode(expectedState) {
|
|
|
311
331
|
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>");
|
|
312
332
|
clearTimeout(timeout);
|
|
313
333
|
server.close();
|
|
314
|
-
|
|
334
|
+
resolve3(code);
|
|
315
335
|
});
|
|
316
336
|
server.listen(REDIRECT_PORT, "127.0.0.1", () => {
|
|
317
337
|
});
|
|
@@ -321,6 +341,17 @@ function waitForAuthCode(expectedState) {
|
|
|
321
341
|
});
|
|
322
342
|
});
|
|
323
343
|
}
|
|
344
|
+
async function safeErrorSummary(res) {
|
|
345
|
+
const text = await res.text();
|
|
346
|
+
try {
|
|
347
|
+
const body = JSON.parse(text);
|
|
348
|
+
if (body.error || body.error_description) {
|
|
349
|
+
return [body.error, body.error_description].filter(Boolean).join(": ");
|
|
350
|
+
}
|
|
351
|
+
} catch {
|
|
352
|
+
}
|
|
353
|
+
return text.length > 200 ? text.slice(0, 200) + "..." : text;
|
|
354
|
+
}
|
|
324
355
|
async function exchangeCodeForToken(code, clientId, clientSecret) {
|
|
325
356
|
const body = new URLSearchParams({
|
|
326
357
|
grant_type: "authorization_code",
|
|
@@ -335,9 +366,7 @@ async function exchangeCodeForToken(code, clientId, clientSecret) {
|
|
|
335
366
|
body: body.toString()
|
|
336
367
|
});
|
|
337
368
|
if (!res.ok) {
|
|
338
|
-
|
|
339
|
-
const truncated = text.length > 200 ? text.substring(0, 200) + "..." : text;
|
|
340
|
-
throw new AuthError(`Token exchange failed (${res.status}): ${truncated}`);
|
|
369
|
+
throw new AuthError(`Token exchange failed (${res.status}): ${await safeErrorSummary(res)}`);
|
|
341
370
|
}
|
|
342
371
|
const data = await res.json();
|
|
343
372
|
return {
|
|
@@ -361,9 +390,7 @@ async function refreshUserToken(refreshToken2, profile = "default") {
|
|
|
361
390
|
body: body.toString()
|
|
362
391
|
});
|
|
363
392
|
if (!res.ok) {
|
|
364
|
-
|
|
365
|
-
const truncated = text.length > 200 ? text.substring(0, 200) + "..." : text;
|
|
366
|
-
throw new AuthError(`Token refresh failed (${res.status}): ${truncated}`);
|
|
393
|
+
throw new AuthError(`Token refresh failed (${res.status}): ${await safeErrorSummary(res)}`);
|
|
367
394
|
}
|
|
368
395
|
const data = await res.json();
|
|
369
396
|
return {
|
|
@@ -396,6 +423,69 @@ async function revokeToken(token, clientId, clientSecret) {
|
|
|
396
423
|
}
|
|
397
424
|
}
|
|
398
425
|
|
|
426
|
+
// src/auth/scopes.ts
|
|
427
|
+
var READ_SCOPES = [
|
|
428
|
+
"calendar.read",
|
|
429
|
+
"file.read",
|
|
430
|
+
"mail.read",
|
|
431
|
+
"task.read",
|
|
432
|
+
"user.read",
|
|
433
|
+
"board.read",
|
|
434
|
+
"contact.read"
|
|
435
|
+
];
|
|
436
|
+
var ALL_SCOPES = [
|
|
437
|
+
"calendar",
|
|
438
|
+
"calendar.read",
|
|
439
|
+
"file",
|
|
440
|
+
"file.read",
|
|
441
|
+
"mail",
|
|
442
|
+
"mail.read",
|
|
443
|
+
"task",
|
|
444
|
+
"task.read",
|
|
445
|
+
"user.read",
|
|
446
|
+
"board",
|
|
447
|
+
"board.read",
|
|
448
|
+
"contact",
|
|
449
|
+
"contact.read"
|
|
450
|
+
];
|
|
451
|
+
var SCOPE_DEPENDENCIES = {
|
|
452
|
+
calendar: ["calendar.read"],
|
|
453
|
+
file: ["file.read"],
|
|
454
|
+
mail: ["mail.read"],
|
|
455
|
+
task: ["task.read", "user.read"],
|
|
456
|
+
"task.read": ["user.read"],
|
|
457
|
+
board: ["board.read"],
|
|
458
|
+
contact: ["contact.read"]
|
|
459
|
+
};
|
|
460
|
+
var SCOPE_PRESETS = {
|
|
461
|
+
readonly: READ_SCOPES,
|
|
462
|
+
// default를 all로 두는 이유: 현재 제품의 "한 번 로그인으로 전체 기능" 약속을 보존한다.
|
|
463
|
+
// 읽기 전용을 기본값으로 하면 신규 사용자가 쓰기 기능(일정 생성 등)을 쓸 때 재로그인해야 한다.
|
|
464
|
+
// 안전을 최우선하려면 readonly로 바꾸면 되지만, 삭제류 도구는 이미 destructiveHint
|
|
465
|
+
// annotation으로 클라이언트 승인 게이트가 걸린다.
|
|
466
|
+
default: ALL_SCOPES,
|
|
467
|
+
all: ALL_SCOPES
|
|
468
|
+
};
|
|
469
|
+
function isPreset(value) {
|
|
470
|
+
return value === "readonly" || value === "default" || value === "all";
|
|
471
|
+
}
|
|
472
|
+
function expandScopeDependencies(scopes) {
|
|
473
|
+
const expanded = new Set(scopes);
|
|
474
|
+
for (const scope of scopes) {
|
|
475
|
+
const deps = SCOPE_DEPENDENCIES[scope];
|
|
476
|
+
if (deps) deps.forEach((d) => expanded.add(d));
|
|
477
|
+
}
|
|
478
|
+
return [...expanded];
|
|
479
|
+
}
|
|
480
|
+
function resolveScopes(input) {
|
|
481
|
+
if (!input) return SCOPE_PRESETS.default;
|
|
482
|
+
if (isPreset(input)) return SCOPE_PRESETS[input];
|
|
483
|
+
return expandScopeDependencies(input.split(" ").filter(Boolean));
|
|
484
|
+
}
|
|
485
|
+
function mergeScopes(existing, requested) {
|
|
486
|
+
return [.../* @__PURE__ */ new Set([...existing, ...requested])].join(" ");
|
|
487
|
+
}
|
|
488
|
+
|
|
399
489
|
// src/output/format.ts
|
|
400
490
|
function output(data, opts = {}) {
|
|
401
491
|
const useJson = opts.json || !process.stdout.isTTY;
|
|
@@ -469,7 +559,7 @@ function errorOutput(error, opts = {}) {
|
|
|
469
559
|
}
|
|
470
560
|
|
|
471
561
|
// src/commands/login.ts
|
|
472
|
-
import { randomBytes
|
|
562
|
+
import { randomBytes } from "crypto";
|
|
473
563
|
async function prompt(question) {
|
|
474
564
|
const rl = createInterface({
|
|
475
565
|
input: process.stdin,
|
|
@@ -482,11 +572,24 @@ async function prompt(question) {
|
|
|
482
572
|
rl.close();
|
|
483
573
|
}
|
|
484
574
|
}
|
|
485
|
-
|
|
575
|
+
async function promptSecret(question) {
|
|
576
|
+
process.stdout.write(question);
|
|
577
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
578
|
+
rl._writeToOutput = () => {
|
|
579
|
+
};
|
|
580
|
+
try {
|
|
581
|
+
const answer = await rl.question("");
|
|
582
|
+
process.stdout.write("\n");
|
|
583
|
+
return answer.trim();
|
|
584
|
+
} finally {
|
|
585
|
+
rl.close();
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
var loginCommand = new Command("login").description("Authenticate with NAVER WORKS").option("--user", "User OAuth login (opens browser)").option("--preset <preset>", "Scope preset: readonly | default | all (default: all)").option("--scope <scope>", "Advanced: raw space-separated scopes (overrides --preset)").option("--client-id <id>", "Client ID").option("--client-secret <secret>", "Client Secret").option("--service-account <account>", "Service Account ID").option("--private-key <path>", "Path to private key file (.key)").option("--bot-id <id>", "Bot ID").option("--domain-id <id>", "Domain ID (optional)").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
|
|
486
589
|
try {
|
|
487
590
|
const profile = opts.profile;
|
|
488
591
|
if (opts.user) {
|
|
489
|
-
await handleUserLogin(
|
|
592
|
+
await handleUserLogin(profile, opts);
|
|
490
593
|
} else {
|
|
491
594
|
await handleServiceAccountLogin(opts);
|
|
492
595
|
}
|
|
@@ -496,7 +599,7 @@ var loginCommand = new Command("login").description("Authenticate with NAVER WOR
|
|
|
496
599
|
process.exitCode = 1;
|
|
497
600
|
}
|
|
498
601
|
});
|
|
499
|
-
async function handleUserLogin(
|
|
602
|
+
async function handleUserLogin(profile, opts) {
|
|
500
603
|
let clientId = opts.clientId;
|
|
501
604
|
let clientSecret = opts.clientSecret;
|
|
502
605
|
try {
|
|
@@ -507,7 +610,7 @@ async function handleUserLogin(scope, profile, opts) {
|
|
|
507
610
|
}
|
|
508
611
|
if (process.stdin.isTTY) {
|
|
509
612
|
if (!clientId) clientId = await prompt("Client ID: ");
|
|
510
|
-
if (!clientSecret) clientSecret = await
|
|
613
|
+
if (!clientSecret) clientSecret = await promptSecret("Client Secret: ");
|
|
511
614
|
}
|
|
512
615
|
if (!clientId || !clientSecret) {
|
|
513
616
|
throw new Error(
|
|
@@ -520,22 +623,33 @@ async function handleUserLogin(scope, profile, opts) {
|
|
|
520
623
|
} catch {
|
|
521
624
|
await saveCredentials({ clientId, clientSecret }, profile);
|
|
522
625
|
}
|
|
523
|
-
const
|
|
524
|
-
|
|
626
|
+
const requestedScopes = resolveScopes(opts.preset ?? opts.scope);
|
|
627
|
+
let existingScopes = [];
|
|
628
|
+
try {
|
|
629
|
+
const existingToken = await loadUserToken(profile);
|
|
630
|
+
existingScopes = existingToken?.scope?.split(" ").filter(Boolean) ?? [];
|
|
631
|
+
} catch {
|
|
632
|
+
}
|
|
633
|
+
const mergedScope = mergeScopes(existingScopes, requestedScopes);
|
|
634
|
+
const state = randomBytes(16).toString("hex");
|
|
635
|
+
const authorizeUrl = buildAuthorizeUrl(clientId, mergedScope, state);
|
|
525
636
|
console.error(`
|
|
526
637
|
Opening browser for NAVER WORKS login...`);
|
|
527
638
|
console.error(`If the browser does not open, visit this URL:
|
|
528
639
|
`);
|
|
529
640
|
console.error(` ${authorizeUrl}
|
|
530
641
|
`);
|
|
531
|
-
const {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
642
|
+
const { execFile } = await import("child_process");
|
|
643
|
+
if (process.platform === "darwin") {
|
|
644
|
+
execFile("open", [authorizeUrl]);
|
|
645
|
+
} else if (process.platform === "win32") {
|
|
646
|
+
execFile("cmd", ["/c", "start", '""', `"${authorizeUrl}"`], {
|
|
647
|
+
windowsVerbatimArguments: true
|
|
648
|
+
});
|
|
535
649
|
} else {
|
|
536
|
-
|
|
650
|
+
execFile("xdg-open", [authorizeUrl]);
|
|
537
651
|
}
|
|
538
|
-
const tokenData = await startUserOAuthFlow(
|
|
652
|
+
const tokenData = await startUserOAuthFlow(mergedScope, profile, state);
|
|
539
653
|
await saveUserToken(tokenData, profile);
|
|
540
654
|
output(
|
|
541
655
|
{
|
|
@@ -562,7 +676,7 @@ async function handleServiceAccountLogin(opts) {
|
|
|
562
676
|
if (!botId) botId = process.env["NWORKS_BOT_ID"];
|
|
563
677
|
if (process.stdin.isTTY) {
|
|
564
678
|
if (!clientId) clientId = await prompt("Client ID: ");
|
|
565
|
-
if (!clientSecret) clientSecret = await
|
|
679
|
+
if (!clientSecret) clientSecret = await promptSecret("Client Secret: ");
|
|
566
680
|
if (!serviceAccount)
|
|
567
681
|
serviceAccount = await prompt("Service Account: ");
|
|
568
682
|
if (!privateKeyPath)
|
|
@@ -624,10 +738,10 @@ var REQUIRED_SCOPES = {
|
|
|
624
738
|
"board.create": "board"
|
|
625
739
|
};
|
|
626
740
|
var ERROR_HINTS_CLI = {
|
|
627
|
-
FORBIDDEN: "
|
|
628
|
-
ACCESS_DENIED: "
|
|
629
|
-
SERVICE_ACCOUNT_NOT_ALLOWED: "
|
|
630
|
-
UNAUTHORIZED: "
|
|
741
|
+
FORBIDDEN: "Insufficient permission. Check the OAuth scopes in the Developer Console.",
|
|
742
|
+
ACCESS_DENIED: "Access denied. Make sure the Bot is added in the Admin console.",
|
|
743
|
+
SERVICE_ACCOUNT_NOT_ALLOWED: "This API is not available with a Service Account. Log in with User OAuth via `nworks login --user`.",
|
|
744
|
+
UNAUTHORIZED: "Authentication expired. Log in again with `nworks login`."
|
|
631
745
|
};
|
|
632
746
|
var ERROR_HINTS_MCP = {
|
|
633
747
|
FORBIDDEN: "\uAD8C\uD55C\uC774 \uBD80\uC871\uD569\uB2C8\uB2E4. Developer Console\uC5D0\uC11C OAuth Scope\uB97C \uD655\uC778\uD558\uC138\uC694.",
|
|
@@ -675,7 +789,7 @@ function buildScopeHint(area, mode) {
|
|
|
675
789
|
const scopes = scope.split(" ").join(", ");
|
|
676
790
|
if (mode === "cli") {
|
|
677
791
|
return `
|
|
678
|
-
\u2192
|
|
792
|
+
\u2192 This command requires the ${scopes} scope(s). Run \`nworks login --user --scope "${scope}"\`.`;
|
|
679
793
|
}
|
|
680
794
|
return `
|
|
681
795
|
\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).`;
|
|
@@ -768,7 +882,7 @@ import { Command as Command4 } from "commander";
|
|
|
768
882
|
var BASE_URL = "https://www.worksapis.com/v1.0";
|
|
769
883
|
var MAX_RETRIES = 3;
|
|
770
884
|
function sleep(ms) {
|
|
771
|
-
return new Promise((
|
|
885
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
772
886
|
}
|
|
773
887
|
async function request(opts, _retryCount = 0) {
|
|
774
888
|
const { method, path, body, profile = "default" } = opts;
|
|
@@ -818,8 +932,8 @@ async function request(opts, _retryCount = 0) {
|
|
|
818
932
|
}
|
|
819
933
|
|
|
820
934
|
// src/utils/sanitize.ts
|
|
821
|
-
import {
|
|
822
|
-
import { randomBytes as
|
|
935
|
+
import { resolve, sep } from "path";
|
|
936
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
823
937
|
function sanitizePathSegment(value) {
|
|
824
938
|
if (!value || typeof value !== "string") {
|
|
825
939
|
throw new Error("Path segment must be a non-empty string");
|
|
@@ -834,8 +948,9 @@ function sanitizeFileName(name) {
|
|
|
834
948
|
if (!name || typeof name !== "string") {
|
|
835
949
|
throw new Error("File name must be a non-empty string");
|
|
836
950
|
}
|
|
837
|
-
const
|
|
838
|
-
|
|
951
|
+
const segments = name.split(/[/\\]/).filter((s) => s.length > 0);
|
|
952
|
+
const base = segments[segments.length - 1] ?? "";
|
|
953
|
+
return base.replace(/[\r\n"]/g, "_");
|
|
839
954
|
}
|
|
840
955
|
function validateLocalPath(filePath, allowedBase) {
|
|
841
956
|
const resolved = resolve(filePath);
|
|
@@ -866,7 +981,7 @@ function validateRedirectUrl(location, allowedHosts) {
|
|
|
866
981
|
return location;
|
|
867
982
|
}
|
|
868
983
|
function generateSecureState() {
|
|
869
|
-
return
|
|
984
|
+
return randomBytes2(32).toString("hex");
|
|
870
985
|
}
|
|
871
986
|
|
|
872
987
|
// src/api/message.ts
|
|
@@ -988,20 +1103,25 @@ var messageCommand = new Command4("message").description("Bot message operations
|
|
|
988
1103
|
import { Command as Command5 } from "commander";
|
|
989
1104
|
|
|
990
1105
|
// src/api/directory.ts
|
|
991
|
-
async function listUsers(profile = "default") {
|
|
1106
|
+
async function listUsers(count = 100, cursor, profile = "default") {
|
|
1107
|
+
const params = new URLSearchParams();
|
|
1108
|
+
params.set("count", String(count));
|
|
1109
|
+
if (cursor) params.set("cursor", cursor);
|
|
992
1110
|
const result = await request({
|
|
993
1111
|
method: "GET",
|
|
994
|
-
path:
|
|
1112
|
+
path: `/users?${params.toString()}`,
|
|
995
1113
|
profile
|
|
996
1114
|
});
|
|
997
1115
|
return { users: result.users ?? [], responseMetaData: result.responseMetaData };
|
|
998
1116
|
}
|
|
999
1117
|
|
|
1000
1118
|
// src/commands/directory.ts
|
|
1001
|
-
var membersCommand2 = new Command5("members").description("List organization members (requires directory.read scope)").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
|
|
1119
|
+
var membersCommand2 = new Command5("members").description("List organization members (requires directory.read scope)").option("--count <n>", "Items per page (default: 100)").option("--cursor <cursor>", "Pagination cursor from a previous response").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
|
|
1002
1120
|
try {
|
|
1003
|
-
const
|
|
1121
|
+
const count = opts.count ? Number(opts.count) : 100;
|
|
1122
|
+
const result = await listUsers(count, opts.cursor, opts.profile);
|
|
1004
1123
|
const formatted = {
|
|
1124
|
+
nextCursor: result.responseMetaData?.nextCursor ?? null,
|
|
1005
1125
|
users: result.users.map((u) => ({
|
|
1006
1126
|
userId: u.userId,
|
|
1007
1127
|
userName: [u.userName?.lastName, u.userName?.firstName].filter(Boolean).join(" ") || "",
|
|
@@ -1024,6 +1144,7 @@ import { Command as Command6 } from "commander";
|
|
|
1024
1144
|
import { randomUUID } from "crypto";
|
|
1025
1145
|
|
|
1026
1146
|
// src/auth/token-user.ts
|
|
1147
|
+
var inFlightRefresh = /* @__PURE__ */ new Map();
|
|
1027
1148
|
async function getValidUserToken(profile = "default") {
|
|
1028
1149
|
const cached = await loadUserToken(profile);
|
|
1029
1150
|
if (!cached) {
|
|
@@ -1034,22 +1155,106 @@ async function getValidUserToken(profile = "default") {
|
|
|
1034
1155
|
if (cached.expiresAt > Date.now() / 1e3 + 300) {
|
|
1035
1156
|
return cached.accessToken;
|
|
1036
1157
|
}
|
|
1158
|
+
return refreshValidUserToken(profile);
|
|
1159
|
+
}
|
|
1160
|
+
function refreshValidUserToken(profile = "default") {
|
|
1161
|
+
const existing = inFlightRefresh.get(profile);
|
|
1162
|
+
if (existing) return existing;
|
|
1163
|
+
const run = doRefresh(profile);
|
|
1164
|
+
inFlightRefresh.set(profile, run);
|
|
1165
|
+
void run.finally(() => inFlightRefresh.delete(profile)).catch(() => {
|
|
1166
|
+
});
|
|
1167
|
+
return run;
|
|
1168
|
+
}
|
|
1169
|
+
async function doRefresh(profile) {
|
|
1170
|
+
const cached = await loadUserToken(profile);
|
|
1171
|
+
if (!cached) {
|
|
1172
|
+
throw new AuthError(
|
|
1173
|
+
"User OAuth token not found. Run `nworks login --user` first."
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1037
1176
|
const refreshed = await refreshUserToken(cached.refreshToken, profile);
|
|
1038
1177
|
await saveUserToken(refreshed, profile);
|
|
1039
1178
|
return refreshed.accessToken;
|
|
1040
1179
|
}
|
|
1041
1180
|
|
|
1042
|
-
// src/api/
|
|
1043
|
-
var
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1181
|
+
// src/api/user-client.ts
|
|
1182
|
+
var MAX_RETRIES2 = 3;
|
|
1183
|
+
var MAX_RETRY_AFTER_SECONDS = 60;
|
|
1184
|
+
var MAX_CONCURRENT_PER_HOST = 5;
|
|
1185
|
+
function sleep2(ms) {
|
|
1186
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
1187
|
+
}
|
|
1188
|
+
var HostSemaphore = class {
|
|
1189
|
+
constructor(max) {
|
|
1190
|
+
this.max = max;
|
|
1191
|
+
}
|
|
1192
|
+
active = 0;
|
|
1193
|
+
waiters = [];
|
|
1194
|
+
async acquire() {
|
|
1195
|
+
if (this.active < this.max) {
|
|
1196
|
+
this.active++;
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
await new Promise((resolve3) => this.waiters.push(resolve3));
|
|
1200
|
+
}
|
|
1201
|
+
release() {
|
|
1202
|
+
const next = this.waiters.shift();
|
|
1203
|
+
if (next) {
|
|
1204
|
+
next();
|
|
1205
|
+
} else {
|
|
1206
|
+
this.active--;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
};
|
|
1210
|
+
var semaphores = /* @__PURE__ */ new Map();
|
|
1211
|
+
function semaphoreFor(host) {
|
|
1212
|
+
let semaphore = semaphores.get(host);
|
|
1213
|
+
if (!semaphore) {
|
|
1214
|
+
semaphore = new HostSemaphore(MAX_CONCURRENT_PER_HOST);
|
|
1215
|
+
semaphores.set(host, semaphore);
|
|
1216
|
+
}
|
|
1217
|
+
return semaphore;
|
|
1049
1218
|
}
|
|
1050
|
-
async function
|
|
1219
|
+
async function userFetch(url, init, profile) {
|
|
1220
|
+
let refreshedOn401 = false;
|
|
1221
|
+
for (let attempt = 0; ; attempt++) {
|
|
1222
|
+
const res = await fetchOnce(url, init, profile);
|
|
1223
|
+
if (res.status === 401 && !refreshedOn401) {
|
|
1224
|
+
refreshedOn401 = true;
|
|
1225
|
+
await refreshValidUserToken(profile);
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
if (res.status === 429 && attempt < MAX_RETRIES2) {
|
|
1229
|
+
await sleep2(retryAfterMs(res));
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
return res;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
async function fetchOnce(url, init, profile) {
|
|
1236
|
+
const host = new URL(url).host;
|
|
1237
|
+
const semaphore = semaphoreFor(host);
|
|
1238
|
+
await semaphore.acquire();
|
|
1239
|
+
try {
|
|
1240
|
+
const token = await getValidUserToken(profile);
|
|
1241
|
+
const headers = new Headers(init.headers);
|
|
1242
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
1243
|
+
return await fetch(url, { ...init, headers });
|
|
1244
|
+
} finally {
|
|
1245
|
+
semaphore.release();
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
function retryAfterMs(res) {
|
|
1249
|
+
const raw = parseInt(res.headers.get("Retry-After") ?? "5", 10);
|
|
1250
|
+
const seconds = Math.min(Number.isNaN(raw) ? 5 : raw, MAX_RETRY_AFTER_SECONDS);
|
|
1251
|
+
return seconds * 1e3;
|
|
1252
|
+
}
|
|
1253
|
+
async function handleUserApiError(res) {
|
|
1051
1254
|
if (res.status === 401) {
|
|
1052
|
-
throw new AuthError(
|
|
1255
|
+
throw new AuthError(
|
|
1256
|
+
"User token expired or revoked. Run `nworks login --user` again."
|
|
1257
|
+
);
|
|
1053
1258
|
}
|
|
1054
1259
|
let code = "UNKNOWN";
|
|
1055
1260
|
let description = `HTTP ${res.status}`;
|
|
@@ -1061,6 +1266,9 @@ async function handleError(res) {
|
|
|
1061
1266
|
}
|
|
1062
1267
|
throw new ApiError(code, description, res.status);
|
|
1063
1268
|
}
|
|
1269
|
+
|
|
1270
|
+
// src/api/calendar.ts
|
|
1271
|
+
var BASE_URL2 = "https://www.worksapis.com/v1.0";
|
|
1064
1272
|
function generateEventId() {
|
|
1065
1273
|
return `event-${randomUUID()}`;
|
|
1066
1274
|
}
|
|
@@ -1078,8 +1286,8 @@ async function listEvents(fromDateTime, untilDateTime, userId = "me", profile =
|
|
|
1078
1286
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1079
1287
|
console.error(`[nworks] GET ${url}`);
|
|
1080
1288
|
}
|
|
1081
|
-
const res = await
|
|
1082
|
-
if (!res.ok) return
|
|
1289
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
1290
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1083
1291
|
const data = await res.json();
|
|
1084
1292
|
return { events: data.events ?? [] };
|
|
1085
1293
|
}
|
|
@@ -1116,7 +1324,7 @@ async function createEvent(opts) {
|
|
|
1116
1324
|
console.error(`[nworks] POST ${url}`);
|
|
1117
1325
|
console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
|
|
1118
1326
|
}
|
|
1119
|
-
const res = await
|
|
1327
|
+
const res = await userFetch(
|
|
1120
1328
|
url,
|
|
1121
1329
|
{
|
|
1122
1330
|
method: "POST",
|
|
@@ -1128,7 +1336,7 @@ async function createEvent(opts) {
|
|
|
1128
1336
|
if (res.status === 201) {
|
|
1129
1337
|
return await res.json();
|
|
1130
1338
|
}
|
|
1131
|
-
if (!res.ok) return
|
|
1339
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1132
1340
|
return await res.json();
|
|
1133
1341
|
}
|
|
1134
1342
|
async function getEvent(eventId, userId = "me", profile = "default") {
|
|
@@ -1136,8 +1344,8 @@ async function getEvent(eventId, userId = "me", profile = "default") {
|
|
|
1136
1344
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1137
1345
|
console.error(`[nworks] GET ${url}`);
|
|
1138
1346
|
}
|
|
1139
|
-
const res = await
|
|
1140
|
-
if (!res.ok) return
|
|
1347
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
1348
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1141
1349
|
const data = await res.json();
|
|
1142
1350
|
const event = data.eventComponents[0];
|
|
1143
1351
|
if (!event) throw new ApiError("NOT_FOUND", "Event not found", 404);
|
|
@@ -1180,7 +1388,7 @@ async function updateEvent(opts) {
|
|
|
1180
1388
|
console.error(`[nworks] PUT ${url}`);
|
|
1181
1389
|
console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
|
|
1182
1390
|
}
|
|
1183
|
-
const res = await
|
|
1391
|
+
const res = await userFetch(
|
|
1184
1392
|
url,
|
|
1185
1393
|
{
|
|
1186
1394
|
method: "PUT",
|
|
@@ -1189,7 +1397,7 @@ async function updateEvent(opts) {
|
|
|
1189
1397
|
},
|
|
1190
1398
|
profile
|
|
1191
1399
|
);
|
|
1192
|
-
if (!res.ok) return
|
|
1400
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1193
1401
|
}
|
|
1194
1402
|
async function deleteEvent(eventId, userId = "me", sendNotification = false, profile = "default") {
|
|
1195
1403
|
const params = new URLSearchParams();
|
|
@@ -1198,9 +1406,9 @@ async function deleteEvent(eventId, userId = "me", sendNotification = false, pro
|
|
|
1198
1406
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1199
1407
|
console.error(`[nworks] DELETE ${url}`);
|
|
1200
1408
|
}
|
|
1201
|
-
const res = await
|
|
1409
|
+
const res = await userFetch(url, { method: "DELETE" }, profile);
|
|
1202
1410
|
if (res.status === 204) return;
|
|
1203
|
-
if (!res.ok) return
|
|
1411
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1204
1412
|
}
|
|
1205
1413
|
|
|
1206
1414
|
// src/commands/calendar.ts
|
|
@@ -1321,34 +1529,15 @@ import { Command as Command7 } from "commander";
|
|
|
1321
1529
|
|
|
1322
1530
|
// src/api/drive.ts
|
|
1323
1531
|
import { readFile as readFile4, stat as stat2 } from "fs/promises";
|
|
1324
|
-
import { basename
|
|
1532
|
+
import { basename } from "path";
|
|
1325
1533
|
var BASE_URL3 = "https://www.worksapis.com/v1.0";
|
|
1326
1534
|
var MAX_UPLOAD_SIZE = 100 * 1024 * 1024;
|
|
1327
1535
|
var ALLOWED_HOSTS = [
|
|
1328
1536
|
"storage.worksmobile.com",
|
|
1537
|
+
"apis-storage.worksmobile.com",
|
|
1329
1538
|
"www.worksapis.com",
|
|
1330
1539
|
"worksapis.com"
|
|
1331
1540
|
];
|
|
1332
|
-
async function authedFetch2(url, init, profile) {
|
|
1333
|
-
const token = await getValidUserToken(profile);
|
|
1334
|
-
const headers = new Headers(init.headers);
|
|
1335
|
-
headers.set("Authorization", `Bearer ${token}`);
|
|
1336
|
-
return fetch(url, { ...init, headers });
|
|
1337
|
-
}
|
|
1338
|
-
async function handleError2(res) {
|
|
1339
|
-
if (res.status === 401) {
|
|
1340
|
-
throw new AuthError("User token expired. Run `nworks login --user --scope file` again.");
|
|
1341
|
-
}
|
|
1342
|
-
let code = "UNKNOWN";
|
|
1343
|
-
let description = `HTTP ${res.status}`;
|
|
1344
|
-
try {
|
|
1345
|
-
const body = await res.json();
|
|
1346
|
-
code = body.code ?? code;
|
|
1347
|
-
description = body.description ?? description;
|
|
1348
|
-
} catch {
|
|
1349
|
-
}
|
|
1350
|
-
throw new ApiError(code, description, res.status);
|
|
1351
|
-
}
|
|
1352
1541
|
async function listFiles(userId = "me", folderId, count = 20, cursor, profile = "default") {
|
|
1353
1542
|
const base = `${BASE_URL3}/users/${sanitizePathSegment(userId)}/drive/files`;
|
|
1354
1543
|
const path = folderId ? `${base}/${sanitizePathSegment(folderId)}/children` : base;
|
|
@@ -1359,13 +1548,13 @@ async function listFiles(userId = "me", folderId, count = 20, cursor, profile =
|
|
|
1359
1548
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1360
1549
|
console.error(`[nworks] GET ${url}`);
|
|
1361
1550
|
}
|
|
1362
|
-
const res = await
|
|
1363
|
-
if (!res.ok) return
|
|
1551
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
1552
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1364
1553
|
const data = await res.json();
|
|
1365
1554
|
return { files: data.files ?? [], responseMetaData: data.responseMetaData };
|
|
1366
1555
|
}
|
|
1367
1556
|
async function uploadFile(localPath, userId = "me", folderId, overwrite = false, profile = "default") {
|
|
1368
|
-
const fileName =
|
|
1557
|
+
const fileName = basename(localPath);
|
|
1369
1558
|
const safeName = sanitizeFileName(fileName);
|
|
1370
1559
|
const fileStat = await stat2(localPath);
|
|
1371
1560
|
const fileSize = fileStat.size;
|
|
@@ -1377,7 +1566,7 @@ async function uploadFile(localPath, userId = "me", folderId, overwrite = false,
|
|
|
1377
1566
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1378
1567
|
console.error(`[nworks] POST ${createUrl} (create upload URL)`);
|
|
1379
1568
|
}
|
|
1380
|
-
const createRes = await
|
|
1569
|
+
const createRes = await userFetch(
|
|
1381
1570
|
createUrl,
|
|
1382
1571
|
{
|
|
1383
1572
|
method: "POST",
|
|
@@ -1386,7 +1575,7 @@ async function uploadFile(localPath, userId = "me", folderId, overwrite = false,
|
|
|
1386
1575
|
},
|
|
1387
1576
|
profile
|
|
1388
1577
|
);
|
|
1389
|
-
if (!createRes.ok) return
|
|
1578
|
+
if (!createRes.ok) return handleUserApiError(createRes);
|
|
1390
1579
|
const { uploadUrl } = await createRes.json();
|
|
1391
1580
|
validateRedirectUrl(uploadUrl, ALLOWED_HOSTS);
|
|
1392
1581
|
const fileBuffer = await readFile4(localPath);
|
|
@@ -1405,7 +1594,7 @@ Content-Type: application/octet-stream\r
|
|
|
1405
1594
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1406
1595
|
console.error(`[nworks] POST ${uploadUrl} (upload content, ${fileSize} bytes)`);
|
|
1407
1596
|
}
|
|
1408
|
-
const uploadRes = await
|
|
1597
|
+
const uploadRes = await userFetch(
|
|
1409
1598
|
uploadUrl,
|
|
1410
1599
|
{
|
|
1411
1600
|
method: "POST",
|
|
@@ -1414,7 +1603,7 @@ Content-Type: application/octet-stream\r
|
|
|
1414
1603
|
},
|
|
1415
1604
|
profile
|
|
1416
1605
|
);
|
|
1417
|
-
if (!uploadRes.ok) return
|
|
1606
|
+
if (!uploadRes.ok) return handleUserApiError(uploadRes);
|
|
1418
1607
|
return await uploadRes.json();
|
|
1419
1608
|
}
|
|
1420
1609
|
async function uploadBuffer(fileBuffer, fileName, userId = "me", folderId, overwrite = false, profile = "default") {
|
|
@@ -1427,7 +1616,7 @@ async function uploadBuffer(fileBuffer, fileName, userId = "me", folderId, overw
|
|
|
1427
1616
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1428
1617
|
console.error(`[nworks] POST ${createUrl} (create upload URL for buffer)`);
|
|
1429
1618
|
}
|
|
1430
|
-
const createRes = await
|
|
1619
|
+
const createRes = await userFetch(
|
|
1431
1620
|
createUrl,
|
|
1432
1621
|
{
|
|
1433
1622
|
method: "POST",
|
|
@@ -1436,7 +1625,7 @@ async function uploadBuffer(fileBuffer, fileName, userId = "me", folderId, overw
|
|
|
1436
1625
|
},
|
|
1437
1626
|
profile
|
|
1438
1627
|
);
|
|
1439
|
-
if (!createRes.ok) return
|
|
1628
|
+
if (!createRes.ok) return handleUserApiError(createRes);
|
|
1440
1629
|
const { uploadUrl } = await createRes.json();
|
|
1441
1630
|
validateRedirectUrl(uploadUrl, ALLOWED_HOSTS);
|
|
1442
1631
|
const boundary = `----nworks${Date.now()}`;
|
|
@@ -1454,7 +1643,7 @@ Content-Type: application/octet-stream\r
|
|
|
1454
1643
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1455
1644
|
console.error(`[nworks] POST ${uploadUrl} (upload buffer, ${fileSize} bytes)`);
|
|
1456
1645
|
}
|
|
1457
|
-
const uploadRes = await
|
|
1646
|
+
const uploadRes = await userFetch(
|
|
1458
1647
|
uploadUrl,
|
|
1459
1648
|
{
|
|
1460
1649
|
method: "POST",
|
|
@@ -1463,33 +1652,29 @@ Content-Type: application/octet-stream\r
|
|
|
1463
1652
|
},
|
|
1464
1653
|
profile
|
|
1465
1654
|
);
|
|
1466
|
-
if (!uploadRes.ok) return
|
|
1655
|
+
if (!uploadRes.ok) return handleUserApiError(uploadRes);
|
|
1467
1656
|
return await uploadRes.json();
|
|
1468
1657
|
}
|
|
1469
|
-
async function
|
|
1470
|
-
const url = `${BASE_URL3}/users/${sanitizePathSegment(userId)}/drive/files/${sanitizePathSegment(fileId)}/download`;
|
|
1658
|
+
async function downloadViaRedirect(url, profile) {
|
|
1471
1659
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1472
1660
|
console.error(`[nworks] GET ${url} (get download URL)`);
|
|
1473
1661
|
}
|
|
1474
|
-
const redirectRes = await
|
|
1662
|
+
const redirectRes = await userFetch(
|
|
1475
1663
|
url,
|
|
1476
1664
|
{ method: "GET", redirect: "manual" },
|
|
1477
1665
|
profile
|
|
1478
1666
|
);
|
|
1479
|
-
if (redirectRes.status === 401) {
|
|
1480
|
-
throw new AuthError("User token expired. Run `nworks login --user --scope file` again.");
|
|
1481
|
-
}
|
|
1482
1667
|
const location = redirectRes.headers.get("location");
|
|
1483
1668
|
if (!location) {
|
|
1484
|
-
if (!redirectRes.ok) return
|
|
1669
|
+
if (!redirectRes.ok) return handleUserApiError(redirectRes);
|
|
1485
1670
|
throw new ApiError("NO_REDIRECT", "No download URL returned", redirectRes.status);
|
|
1486
1671
|
}
|
|
1487
1672
|
const safeLocation = validateRedirectUrl(location, ALLOWED_HOSTS);
|
|
1488
1673
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1489
1674
|
console.error(`[nworks] GET ${safeLocation} (download content)`);
|
|
1490
1675
|
}
|
|
1491
|
-
const downloadRes = await
|
|
1492
|
-
if (!downloadRes.ok) return
|
|
1676
|
+
const downloadRes = await userFetch(safeLocation, { method: "GET" }, profile);
|
|
1677
|
+
if (!downloadRes.ok) return handleUserApiError(downloadRes);
|
|
1493
1678
|
const arrayBuffer = await downloadRes.arrayBuffer();
|
|
1494
1679
|
const buffer = Buffer.from(arrayBuffer);
|
|
1495
1680
|
const disposition = downloadRes.headers.get("content-disposition");
|
|
@@ -1502,6 +1687,42 @@ async function downloadFile(fileId, userId = "me", profile = "default") {
|
|
|
1502
1687
|
}
|
|
1503
1688
|
return { buffer, fileName };
|
|
1504
1689
|
}
|
|
1690
|
+
async function downloadFile(fileId, userId = "me", profile = "default") {
|
|
1691
|
+
const url = `${BASE_URL3}/users/${sanitizePathSegment(userId)}/drive/files/${sanitizePathSegment(fileId)}/download`;
|
|
1692
|
+
return downloadViaRedirect(url, profile);
|
|
1693
|
+
}
|
|
1694
|
+
async function listSharedDrives(count = 20, cursor, profile = "default") {
|
|
1695
|
+
const params = new URLSearchParams();
|
|
1696
|
+
params.set("count", String(count));
|
|
1697
|
+
if (cursor) params.set("cursor", cursor);
|
|
1698
|
+
const url = `${BASE_URL3}/sharedrives?${params.toString()}`;
|
|
1699
|
+
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1700
|
+
console.error(`[nworks] GET ${url}`);
|
|
1701
|
+
}
|
|
1702
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
1703
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1704
|
+
const data = await res.json();
|
|
1705
|
+
return { sharedDrives: data.sharedrives ?? [], responseMetaData: data.responseMetaData };
|
|
1706
|
+
}
|
|
1707
|
+
async function listSharedDriveFiles(sharedriveId, folderId, count = 20, cursor, profile = "default") {
|
|
1708
|
+
const base = `${BASE_URL3}/sharedrives/${sanitizePathSegment(sharedriveId)}/files`;
|
|
1709
|
+
const path = folderId ? `${base}/${sanitizePathSegment(folderId)}/children` : base;
|
|
1710
|
+
const params = new URLSearchParams();
|
|
1711
|
+
params.set("count", String(count));
|
|
1712
|
+
if (cursor) params.set("cursor", cursor);
|
|
1713
|
+
const url = `${path}?${params.toString()}`;
|
|
1714
|
+
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1715
|
+
console.error(`[nworks] GET ${url}`);
|
|
1716
|
+
}
|
|
1717
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
1718
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1719
|
+
const data = await res.json();
|
|
1720
|
+
return { files: data.files ?? [], responseMetaData: data.responseMetaData };
|
|
1721
|
+
}
|
|
1722
|
+
async function downloadSharedDriveFile(sharedriveId, fileId, profile = "default") {
|
|
1723
|
+
const url = `${BASE_URL3}/sharedrives/${sanitizePathSegment(sharedriveId)}/files/${sanitizePathSegment(fileId)}/download`;
|
|
1724
|
+
return downloadViaRedirect(url, profile);
|
|
1725
|
+
}
|
|
1505
1726
|
|
|
1506
1727
|
// src/commands/drive.ts
|
|
1507
1728
|
var listCommand2 = new Command7("list").description("List files in Drive (requires User OAuth with file or file.read scope)").option("--user <userId>", "Target user ID (default: me)").option("--folder <folderId>", "Folder ID to list (default: root)").option("--count <n>", "Items per page (default: 20)", "20").option("--cursor <cursor>", "Pagination cursor").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
|
|
@@ -1595,33 +1816,86 @@ var downloadCommand = new Command7("download").description("Download a file from
|
|
|
1595
1816
|
cliError(err, opts, "drive");
|
|
1596
1817
|
}
|
|
1597
1818
|
});
|
|
1598
|
-
var
|
|
1819
|
+
var sharedriveListCommand = new Command7("sharedrive-list").description("List shared drives (requires User OAuth with file or file.read scope)").option("--count <n>", "Items per page (default: 20)", "20").option("--cursor <cursor>", "Pagination cursor").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
|
|
1820
|
+
try {
|
|
1821
|
+
const result = await listSharedDrives(
|
|
1822
|
+
parseInt(opts.count, 10),
|
|
1823
|
+
opts.cursor,
|
|
1824
|
+
opts.profile
|
|
1825
|
+
);
|
|
1826
|
+
output(
|
|
1827
|
+
{
|
|
1828
|
+
sharedDrives: result.sharedDrives,
|
|
1829
|
+
count: result.sharedDrives.length,
|
|
1830
|
+
nextCursor: result.responseMetaData?.nextCursor ?? null
|
|
1831
|
+
},
|
|
1832
|
+
opts
|
|
1833
|
+
);
|
|
1834
|
+
} catch (err) {
|
|
1835
|
+
cliError(err, opts, "drive");
|
|
1836
|
+
}
|
|
1837
|
+
});
|
|
1838
|
+
var sharedriveFilesCommand = new Command7("sharedrive-files").description("List files in a shared drive (requires User OAuth with file or file.read scope)").requiredOption("--sharedrive <sharedriveId>", "Shared drive ID (from sharedrive-list)").option("--folder <fileId>", "Folder ID to list (default: root)").option("--count <n>", "Items per page (default: 20)", "20").option("--cursor <cursor>", "Pagination cursor").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
|
|
1839
|
+
try {
|
|
1840
|
+
const result = await listSharedDriveFiles(
|
|
1841
|
+
opts.sharedrive,
|
|
1842
|
+
opts.folder,
|
|
1843
|
+
parseInt(opts.count, 10),
|
|
1844
|
+
opts.cursor,
|
|
1845
|
+
opts.profile
|
|
1846
|
+
);
|
|
1847
|
+
const files = result.files.map((f) => ({
|
|
1848
|
+
name: f.fileName,
|
|
1849
|
+
type: f.fileType,
|
|
1850
|
+
size: f.fileSize,
|
|
1851
|
+
modified: f.modifiedTime,
|
|
1852
|
+
fileId: f.fileId
|
|
1853
|
+
}));
|
|
1854
|
+
output(
|
|
1855
|
+
{
|
|
1856
|
+
files,
|
|
1857
|
+
count: files.length,
|
|
1858
|
+
nextCursor: result.responseMetaData?.nextCursor ?? null
|
|
1859
|
+
},
|
|
1860
|
+
opts
|
|
1861
|
+
);
|
|
1862
|
+
} catch (err) {
|
|
1863
|
+
cliError(err, opts, "drive");
|
|
1864
|
+
}
|
|
1865
|
+
});
|
|
1866
|
+
var sharedriveDownloadCommand = new Command7("sharedrive-download").description("Download a file from a shared drive (requires User OAuth with file or file.read scope)").requiredOption("--sharedrive <sharedriveId>", "Shared drive ID (from sharedrive-list)").requiredOption("--file-id <fileId>", "File ID to download").option("--out <path>", "Output directory (default: current directory)").option("--name <filename>", "Output filename (default: original name)").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
|
|
1867
|
+
try {
|
|
1868
|
+
const result = await downloadSharedDriveFile(
|
|
1869
|
+
opts.sharedrive,
|
|
1870
|
+
opts.fileId,
|
|
1871
|
+
opts.profile
|
|
1872
|
+
);
|
|
1873
|
+
const fileName = opts.name ?? result.fileName ?? opts.fileId;
|
|
1874
|
+
const outDir = opts.out ?? process.cwd();
|
|
1875
|
+
const outPath = join2(outDir, fileName);
|
|
1876
|
+
await writeFile2(outPath, result.buffer);
|
|
1877
|
+
output(
|
|
1878
|
+
{
|
|
1879
|
+
success: true,
|
|
1880
|
+
fileName,
|
|
1881
|
+
path: outPath,
|
|
1882
|
+
size: result.buffer.length
|
|
1883
|
+
},
|
|
1884
|
+
opts
|
|
1885
|
+
);
|
|
1886
|
+
} catch (err) {
|
|
1887
|
+
cliError(err, opts, "drive");
|
|
1888
|
+
}
|
|
1889
|
+
});
|
|
1890
|
+
var driveCommand = new Command7("drive").description("Drive operations (requires User OAuth with file scope)").addCommand(listCommand2).addCommand(uploadCommand).addCommand(downloadCommand).addCommand(sharedriveListCommand).addCommand(sharedriveFilesCommand).addCommand(sharedriveDownloadCommand);
|
|
1599
1891
|
|
|
1600
1892
|
// src/commands/mail.ts
|
|
1893
|
+
import { writeFile as writeFile3 } from "fs/promises";
|
|
1894
|
+
import { join as join3 } from "path";
|
|
1601
1895
|
import { Command as Command8 } from "commander";
|
|
1602
1896
|
|
|
1603
1897
|
// src/api/mail.ts
|
|
1604
1898
|
var BASE_URL4 = "https://www.worksapis.com/v1.0";
|
|
1605
|
-
async function authedFetch3(url, init, profile) {
|
|
1606
|
-
const token = await getValidUserToken(profile);
|
|
1607
|
-
const headers = new Headers(init.headers);
|
|
1608
|
-
headers.set("Authorization", `Bearer ${token}`);
|
|
1609
|
-
return fetch(url, { ...init, headers });
|
|
1610
|
-
}
|
|
1611
|
-
async function handleError3(res) {
|
|
1612
|
-
if (res.status === 401) {
|
|
1613
|
-
throw new AuthError("User token expired. Run `nworks login --user --scope mail` again.");
|
|
1614
|
-
}
|
|
1615
|
-
let code = "UNKNOWN";
|
|
1616
|
-
let description = `HTTP ${res.status}`;
|
|
1617
|
-
try {
|
|
1618
|
-
const body = await res.json();
|
|
1619
|
-
code = body.code ?? code;
|
|
1620
|
-
description = body.description ?? description;
|
|
1621
|
-
} catch {
|
|
1622
|
-
}
|
|
1623
|
-
throw new ApiError(code, description, res.status);
|
|
1624
|
-
}
|
|
1625
1899
|
async function sendMail(opts) {
|
|
1626
1900
|
const userId = opts.userId ?? "me";
|
|
1627
1901
|
const profile = opts.profile ?? "default";
|
|
@@ -1637,7 +1911,7 @@ async function sendMail(opts) {
|
|
|
1637
1911
|
if (opts.cc) body.cc = opts.cc;
|
|
1638
1912
|
if (opts.bcc) body.bcc = opts.bcc;
|
|
1639
1913
|
if (opts.contentType) body.contentType = opts.contentType;
|
|
1640
|
-
const res = await
|
|
1914
|
+
const res = await userFetch(
|
|
1641
1915
|
url,
|
|
1642
1916
|
{
|
|
1643
1917
|
method: "POST",
|
|
@@ -1647,7 +1921,7 @@ async function sendMail(opts) {
|
|
|
1647
1921
|
profile
|
|
1648
1922
|
);
|
|
1649
1923
|
if (res.status === 202) return;
|
|
1650
|
-
if (!res.ok) return
|
|
1924
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1651
1925
|
}
|
|
1652
1926
|
async function listMails(folderId = 0, userId = "me", count = 30, cursor, isUnread, profile = "default") {
|
|
1653
1927
|
const params = new URLSearchParams();
|
|
@@ -1658,8 +1932,8 @@ async function listMails(folderId = 0, userId = "me", count = 30, cursor, isUnre
|
|
|
1658
1932
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1659
1933
|
console.error(`[nworks] GET ${url}`);
|
|
1660
1934
|
}
|
|
1661
|
-
const res = await
|
|
1662
|
-
if (!res.ok) return
|
|
1935
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
1936
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1663
1937
|
const data = await res.json();
|
|
1664
1938
|
return {
|
|
1665
1939
|
mails: data.mails ?? [],
|
|
@@ -1674,10 +1948,22 @@ async function readMail(mailId, userId = "me", profile = "default") {
|
|
|
1674
1948
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1675
1949
|
console.error(`[nworks] GET ${url}`);
|
|
1676
1950
|
}
|
|
1677
|
-
const res = await
|
|
1678
|
-
if (!res.ok) return
|
|
1951
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
1952
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1679
1953
|
return await res.json();
|
|
1680
1954
|
}
|
|
1955
|
+
async function downloadAttachment(mailId, attachmentId, userId = "me", profile = "default") {
|
|
1956
|
+
const url = `${BASE_URL4}/users/${sanitizePathSegment(userId)}/mail/${sanitizePathSegment(
|
|
1957
|
+
String(mailId)
|
|
1958
|
+
)}/attachments/${sanitizePathSegment(String(attachmentId))}`;
|
|
1959
|
+
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1960
|
+
console.error(`[nworks] GET ${url}`);
|
|
1961
|
+
}
|
|
1962
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
1963
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1964
|
+
const data = await res.json();
|
|
1965
|
+
return { buffer: Buffer.from(data.data, "base64"), filename: data.filename };
|
|
1966
|
+
}
|
|
1681
1967
|
|
|
1682
1968
|
// src/commands/mail.ts
|
|
1683
1969
|
var sendCommand2 = new Command8("send").description("Send a mail (requires User OAuth with mail scope)").requiredOption("--to <emails>", "Recipient email addresses (separate with ;)").requiredOption("--subject <subject>", "Mail subject").option("--body <body>", "Mail body content").option("--cc <emails>", "CC email addresses (separate with ;)").option("--bcc <emails>", "BCC email addresses (separate with ;)").option("--content-type <type>", "Body format: html or text", "html").option("--user <userId>", "Sender user ID (default: me)").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").option("--dry-run", "Print request without sending").action(async (opts) => {
|
|
@@ -1765,38 +2051,43 @@ var readCommand = new Command8("read").description("Read a specific mail (requir
|
|
|
1765
2051
|
cliError(err, opts, "mail");
|
|
1766
2052
|
}
|
|
1767
2053
|
});
|
|
1768
|
-
var
|
|
2054
|
+
var downloadAttachmentCommand = new Command8("download-attachment").description("Download a mail attachment (requires User OAuth with mail or mail.read scope)").requiredOption("--id <mailId>", "Mail ID (from mail list)").requiredOption("--attachment-id <attachmentId>", "Attachment ID (from mail read)").option("--out <path>", "Output directory (default: current directory)").option("--name <filename>", "Output filename (default: original name)").option("--user <userId>", "Target user ID (default: me)").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
|
|
2055
|
+
try {
|
|
2056
|
+
const result = await downloadAttachment(
|
|
2057
|
+
parseInt(opts.id, 10),
|
|
2058
|
+
parseInt(opts.attachmentId, 10),
|
|
2059
|
+
opts.user ?? "me",
|
|
2060
|
+
opts.profile
|
|
2061
|
+
);
|
|
2062
|
+
const fileName = opts.name ?? result.filename;
|
|
2063
|
+
const outDir = opts.out ?? process.cwd();
|
|
2064
|
+
const outPath = join3(outDir, fileName);
|
|
2065
|
+
await writeFile3(outPath, result.buffer);
|
|
2066
|
+
output(
|
|
2067
|
+
{
|
|
2068
|
+
success: true,
|
|
2069
|
+
fileName,
|
|
2070
|
+
path: outPath,
|
|
2071
|
+
size: result.buffer.length
|
|
2072
|
+
},
|
|
2073
|
+
opts
|
|
2074
|
+
);
|
|
2075
|
+
} catch (err) {
|
|
2076
|
+
cliError(err, opts, "mail");
|
|
2077
|
+
}
|
|
2078
|
+
});
|
|
2079
|
+
var mailCommand = new Command8("mail").description("Mail operations (requires User OAuth with mail scope)").addCommand(sendCommand2).addCommand(listCommand3).addCommand(readCommand).addCommand(downloadAttachmentCommand);
|
|
1769
2080
|
|
|
1770
2081
|
// src/commands/task.ts
|
|
1771
2082
|
import { Command as Command9 } from "commander";
|
|
1772
2083
|
|
|
1773
2084
|
// src/api/task.ts
|
|
1774
2085
|
var BASE_URL5 = "https://www.worksapis.com/v1.0";
|
|
1775
|
-
async function authedFetch4(url, init, profile) {
|
|
1776
|
-
const token = await getValidUserToken(profile);
|
|
1777
|
-
const headers = new Headers(init.headers);
|
|
1778
|
-
headers.set("Authorization", `Bearer ${token}`);
|
|
1779
|
-
return fetch(url, { ...init, headers });
|
|
1780
|
-
}
|
|
1781
|
-
async function handleError4(res) {
|
|
1782
|
-
if (res.status === 401) {
|
|
1783
|
-
throw new AuthError("User token expired. Run `nworks login --user --scope task` again.");
|
|
1784
|
-
}
|
|
1785
|
-
let code = "UNKNOWN";
|
|
1786
|
-
let description = `HTTP ${res.status}`;
|
|
1787
|
-
try {
|
|
1788
|
-
const body = await res.json();
|
|
1789
|
-
code = body.code ?? code;
|
|
1790
|
-
description = body.description ?? description;
|
|
1791
|
-
} catch {
|
|
1792
|
-
}
|
|
1793
|
-
throw new ApiError(code, description, res.status);
|
|
1794
|
-
}
|
|
1795
2086
|
async function resolveUserId(userId, profile) {
|
|
1796
2087
|
if (userId !== "me") return userId;
|
|
1797
2088
|
const url = `${BASE_URL5}/users/${sanitizePathSegment(userId)}`;
|
|
1798
|
-
const res = await
|
|
1799
|
-
if (!res.ok) return
|
|
2089
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
2090
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1800
2091
|
const data = await res.json();
|
|
1801
2092
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1802
2093
|
console.error(`[nworks] Resolved "me" \u2192 ${data.userId}`);
|
|
@@ -1813,8 +2104,8 @@ async function listTasks(categoryId = "default", userId = "me", count = 50, curs
|
|
|
1813
2104
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1814
2105
|
console.error(`[nworks] GET ${url}`);
|
|
1815
2106
|
}
|
|
1816
|
-
const res = await
|
|
1817
|
-
if (!res.ok) return
|
|
2107
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
2108
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1818
2109
|
const data = await res.json();
|
|
1819
2110
|
return { tasks: data.tasks ?? [], responseMetaData: data.responseMetaData };
|
|
1820
2111
|
}
|
|
@@ -1838,7 +2129,7 @@ async function createTask(opts) {
|
|
|
1838
2129
|
console.error(`[nworks] POST ${url}`);
|
|
1839
2130
|
console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
|
|
1840
2131
|
}
|
|
1841
|
-
const res = await
|
|
2132
|
+
const res = await userFetch(
|
|
1842
2133
|
url,
|
|
1843
2134
|
{
|
|
1844
2135
|
method: "POST",
|
|
@@ -1850,7 +2141,7 @@ async function createTask(opts) {
|
|
|
1850
2141
|
if (res.status === 201) {
|
|
1851
2142
|
return await res.json();
|
|
1852
2143
|
}
|
|
1853
|
-
if (!res.ok) return
|
|
2144
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1854
2145
|
return await res.json();
|
|
1855
2146
|
}
|
|
1856
2147
|
async function updateTask(opts) {
|
|
@@ -1863,7 +2154,7 @@ async function updateTask(opts) {
|
|
|
1863
2154
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1864
2155
|
console.error(`[nworks] PATCH ${url}`);
|
|
1865
2156
|
}
|
|
1866
|
-
const res = await
|
|
2157
|
+
const res = await userFetch(
|
|
1867
2158
|
url,
|
|
1868
2159
|
{
|
|
1869
2160
|
method: "PATCH",
|
|
@@ -1872,7 +2163,7 @@ async function updateTask(opts) {
|
|
|
1872
2163
|
},
|
|
1873
2164
|
profile
|
|
1874
2165
|
);
|
|
1875
|
-
if (!res.ok) return
|
|
2166
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1876
2167
|
return await res.json();
|
|
1877
2168
|
}
|
|
1878
2169
|
async function completeTask(taskId, profile = "default") {
|
|
@@ -1880,39 +2171,39 @@ async function completeTask(taskId, profile = "default") {
|
|
|
1880
2171
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1881
2172
|
console.error(`[nworks] POST ${url}`);
|
|
1882
2173
|
}
|
|
1883
|
-
const res = await
|
|
2174
|
+
const res = await userFetch(
|
|
1884
2175
|
url,
|
|
1885
2176
|
{ method: "POST" },
|
|
1886
2177
|
profile
|
|
1887
2178
|
);
|
|
1888
2179
|
if (res.status === 204) return;
|
|
1889
|
-
if (!res.ok) return
|
|
2180
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1890
2181
|
}
|
|
1891
2182
|
async function incompleteTask(taskId, profile = "default") {
|
|
1892
2183
|
const url = `${BASE_URL5}/tasks/${sanitizePathSegment(taskId)}/incomplete`;
|
|
1893
2184
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1894
2185
|
console.error(`[nworks] POST ${url}`);
|
|
1895
2186
|
}
|
|
1896
|
-
const res = await
|
|
2187
|
+
const res = await userFetch(
|
|
1897
2188
|
url,
|
|
1898
2189
|
{ method: "POST" },
|
|
1899
2190
|
profile
|
|
1900
2191
|
);
|
|
1901
2192
|
if (res.status === 204) return;
|
|
1902
|
-
if (!res.ok) return
|
|
2193
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1903
2194
|
}
|
|
1904
2195
|
async function deleteTask(taskId, profile = "default") {
|
|
1905
2196
|
const url = `${BASE_URL5}/tasks/${sanitizePathSegment(taskId)}`;
|
|
1906
2197
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1907
2198
|
console.error(`[nworks] DELETE ${url}`);
|
|
1908
2199
|
}
|
|
1909
|
-
const res = await
|
|
2200
|
+
const res = await userFetch(
|
|
1910
2201
|
url,
|
|
1911
2202
|
{ method: "DELETE" },
|
|
1912
2203
|
profile
|
|
1913
2204
|
);
|
|
1914
2205
|
if (res.status === 204) return;
|
|
1915
|
-
if (!res.ok) return
|
|
2206
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1916
2207
|
}
|
|
1917
2208
|
|
|
1918
2209
|
// src/commands/task.ts
|
|
@@ -2035,32 +2326,12 @@ import { Command as Command10 } from "commander";
|
|
|
2035
2326
|
|
|
2036
2327
|
// src/api/board.ts
|
|
2037
2328
|
var BASE_URL6 = "https://www.worksapis.com/v1.0";
|
|
2038
|
-
|
|
2039
|
-
const
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
async function handleError5(res) {
|
|
2045
|
-
if (res.status === 401) {
|
|
2046
|
-
throw new AuthError("User token expired. Run `nworks login --user --scope board` again.");
|
|
2047
|
-
}
|
|
2048
|
-
let code = "UNKNOWN";
|
|
2049
|
-
let description = `HTTP ${res.status}`;
|
|
2050
|
-
try {
|
|
2051
|
-
const body = await res.json();
|
|
2052
|
-
code = body.code ?? code;
|
|
2053
|
-
description = body.description ?? description;
|
|
2054
|
-
} catch {
|
|
2055
|
-
}
|
|
2056
|
-
throw new ApiError(code, description, res.status);
|
|
2057
|
-
}
|
|
2058
|
-
function safeParseJson(text) {
|
|
2059
|
-
const safe = text.replace(
|
|
2060
|
-
/"((?:board|post|domain|user)Id)"\s*:\s*(\d{16,})/g,
|
|
2061
|
-
'"$1":"$2"'
|
|
2062
|
-
);
|
|
2063
|
-
return JSON.parse(safe);
|
|
2329
|
+
function safeParseJson(text) {
|
|
2330
|
+
const safe = text.replace(
|
|
2331
|
+
/"((?:board|post|domain|user)Id)"\s*:\s*(\d{16,})/g,
|
|
2332
|
+
'"$1":"$2"'
|
|
2333
|
+
);
|
|
2334
|
+
return JSON.parse(safe);
|
|
2064
2335
|
}
|
|
2065
2336
|
async function listBoards(count = 20, cursor, profile = "default") {
|
|
2066
2337
|
const params = new URLSearchParams();
|
|
@@ -2070,8 +2341,8 @@ async function listBoards(count = 20, cursor, profile = "default") {
|
|
|
2070
2341
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
2071
2342
|
console.error(`[nworks] GET ${url}`);
|
|
2072
2343
|
}
|
|
2073
|
-
const res = await
|
|
2074
|
-
if (!res.ok) return
|
|
2344
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
2345
|
+
if (!res.ok) return handleUserApiError(res);
|
|
2075
2346
|
const text = await res.text();
|
|
2076
2347
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
2077
2348
|
console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
|
|
@@ -2087,8 +2358,8 @@ async function listPosts(boardId, count = 20, cursor, profile = "default") {
|
|
|
2087
2358
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
2088
2359
|
console.error(`[nworks] GET ${url}`);
|
|
2089
2360
|
}
|
|
2090
|
-
const res = await
|
|
2091
|
-
if (!res.ok) return
|
|
2361
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
2362
|
+
if (!res.ok) return handleUserApiError(res);
|
|
2092
2363
|
const text = await res.text();
|
|
2093
2364
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
2094
2365
|
console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
|
|
@@ -2101,8 +2372,8 @@ async function readPost(boardId, postId, profile = "default") {
|
|
|
2101
2372
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
2102
2373
|
console.error(`[nworks] GET ${url}`);
|
|
2103
2374
|
}
|
|
2104
|
-
const res = await
|
|
2105
|
-
if (!res.ok) return
|
|
2375
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
2376
|
+
if (!res.ok) return handleUserApiError(res);
|
|
2106
2377
|
const text = await res.text();
|
|
2107
2378
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
2108
2379
|
console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
|
|
@@ -2122,7 +2393,7 @@ async function createPost(opts) {
|
|
|
2122
2393
|
console.error(`[nworks] POST ${url}`);
|
|
2123
2394
|
console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
|
|
2124
2395
|
}
|
|
2125
|
-
const res = await
|
|
2396
|
+
const res = await userFetch(
|
|
2126
2397
|
url,
|
|
2127
2398
|
{
|
|
2128
2399
|
method: "POST",
|
|
@@ -2138,7 +2409,7 @@ async function createPost(opts) {
|
|
|
2138
2409
|
}
|
|
2139
2410
|
return safeParseJson(text);
|
|
2140
2411
|
}
|
|
2141
|
-
return
|
|
2412
|
+
return handleUserApiError(res);
|
|
2142
2413
|
}
|
|
2143
2414
|
|
|
2144
2415
|
// src/commands/board.ts
|
|
@@ -2236,128 +2507,256 @@ var createCommand3 = new Command10("create").description("Create a post in a boa
|
|
|
2236
2507
|
});
|
|
2237
2508
|
var boardCommand = new Command10("board").description("Board operations (requires User OAuth)").addCommand(listCommand5).addCommand(postsCommand).addCommand(readCommand2).addCommand(createCommand3);
|
|
2238
2509
|
|
|
2239
|
-
// src/commands/
|
|
2240
|
-
import { Command as Command12 } from "commander";
|
|
2241
|
-
|
|
2242
|
-
// src/mcp/server.ts
|
|
2243
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2244
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2245
|
-
|
|
2246
|
-
// src/mcp/tools.ts
|
|
2247
|
-
import { z } from "zod";
|
|
2248
|
-
|
|
2249
|
-
// src/commands/doctor.ts
|
|
2510
|
+
// src/commands/contact.ts
|
|
2250
2511
|
import { Command as Command11 } from "commander";
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2512
|
+
|
|
2513
|
+
// src/api/contact.ts
|
|
2514
|
+
var BASE_URL7 = "https://www.worksapis.com/v1.0";
|
|
2515
|
+
async function listContacts(opts = {}) {
|
|
2516
|
+
const userId = opts.userId ?? "me";
|
|
2517
|
+
const profile = opts.profile ?? "default";
|
|
2518
|
+
const params = new URLSearchParams();
|
|
2519
|
+
params.set("count", String(opts.count ?? 20));
|
|
2520
|
+
if (opts.cursor) params.set("cursor", opts.cursor);
|
|
2521
|
+
if (opts.accessibleRange) params.set("accessibleRange", opts.accessibleRange);
|
|
2522
|
+
if (opts.contactTagId) params.set("contactTagId", opts.contactTagId);
|
|
2523
|
+
if (opts.email) params.set("email", opts.email);
|
|
2524
|
+
if (opts.telephone) params.set("telephone", opts.telephone);
|
|
2525
|
+
if (opts.orderBy) params.set("orderBy", opts.orderBy);
|
|
2526
|
+
if (opts.searchDateType) params.set("searchDateType", opts.searchDateType);
|
|
2527
|
+
if (opts.startDateTime) params.set("startDateTime", opts.startDateTime);
|
|
2528
|
+
if (opts.endDateTime) params.set("endDateTime", opts.endDateTime);
|
|
2529
|
+
const url = `${BASE_URL7}/users/${sanitizePathSegment(userId)}/contacts?${params.toString()}`;
|
|
2530
|
+
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
2531
|
+
console.error(`[nworks] GET ${url}`);
|
|
2262
2532
|
}
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2533
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
2534
|
+
if (!res.ok) return handleUserApiError(res);
|
|
2535
|
+
const data = await res.json();
|
|
2536
|
+
return { contacts: data.contacts ?? [], responseMetaData: data.responseMetaData };
|
|
2537
|
+
}
|
|
2538
|
+
async function getContact(contactId, profile = "default") {
|
|
2539
|
+
const url = `${BASE_URL7}/contacts/${sanitizePathSegment(contactId)}`;
|
|
2540
|
+
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
2541
|
+
console.error(`[nworks] GET ${url}`);
|
|
2267
2542
|
}
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
} else {
|
|
2277
|
-
results.push({ check: "privateKey", status: "FAIL", detail: `\uD30C\uC77C \uC5C6\uC74C: ${creds.privateKeyPath}` });
|
|
2278
|
-
}
|
|
2279
|
-
} else {
|
|
2280
|
-
results.push({ check: "privateKey", status: "SKIP", detail: "\uBBF8\uC124\uC815" });
|
|
2543
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
2544
|
+
if (!res.ok) return handleUserApiError(res);
|
|
2545
|
+
return await res.json();
|
|
2546
|
+
}
|
|
2547
|
+
async function createContact(payload, profile = "default") {
|
|
2548
|
+
const url = `${BASE_URL7}/contacts`;
|
|
2549
|
+
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
2550
|
+
console.error(`[nworks] POST ${url}`);
|
|
2281
2551
|
}
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2552
|
+
const res = await userFetch(
|
|
2553
|
+
url,
|
|
2554
|
+
{
|
|
2555
|
+
method: "POST",
|
|
2556
|
+
headers: { "Content-Type": "application/json" },
|
|
2557
|
+
body: JSON.stringify(payload)
|
|
2558
|
+
},
|
|
2559
|
+
profile
|
|
2560
|
+
);
|
|
2561
|
+
if (!res.ok) return handleUserApiError(res);
|
|
2562
|
+
return await res.json();
|
|
2563
|
+
}
|
|
2564
|
+
async function updateContact(contactId, payload, partial = true, profile = "default") {
|
|
2565
|
+
const url = `${BASE_URL7}/contacts/${sanitizePathSegment(contactId)}`;
|
|
2566
|
+
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
2567
|
+
console.error(`[nworks] ${partial ? "PATCH" : "PUT"} ${url}`);
|
|
2286
2568
|
}
|
|
2287
|
-
const
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2569
|
+
const res = await userFetch(
|
|
2570
|
+
url,
|
|
2571
|
+
{
|
|
2572
|
+
method: partial ? "PATCH" : "PUT",
|
|
2573
|
+
headers: { "Content-Type": "application/json" },
|
|
2574
|
+
body: JSON.stringify(payload)
|
|
2575
|
+
},
|
|
2576
|
+
profile
|
|
2577
|
+
);
|
|
2578
|
+
if (!res.ok) return handleUserApiError(res);
|
|
2579
|
+
return await res.json();
|
|
2580
|
+
}
|
|
2581
|
+
async function deleteContact(contactId, profile = "default") {
|
|
2582
|
+
const url = `${BASE_URL7}/contacts/${sanitizePathSegment(contactId)}`;
|
|
2583
|
+
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
2584
|
+
console.error(`[nworks] DELETE ${url}`);
|
|
2297
2585
|
}
|
|
2298
|
-
const
|
|
2299
|
-
if (
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2586
|
+
const res = await userFetch(url, { method: "DELETE" }, profile);
|
|
2587
|
+
if (res.status === 204) return;
|
|
2588
|
+
if (!res.ok) return handleUserApiError(res);
|
|
2589
|
+
}
|
|
2590
|
+
async function listContactTags(opts = {}) {
|
|
2591
|
+
const userId = opts.userId ?? "me";
|
|
2592
|
+
const profile = opts.profile ?? "default";
|
|
2593
|
+
const params = new URLSearchParams();
|
|
2594
|
+
params.set("count", String(opts.count ?? 20));
|
|
2595
|
+
if (opts.cursor) params.set("cursor", opts.cursor);
|
|
2596
|
+
const url = `${BASE_URL7}/users/${sanitizePathSegment(userId)}/contact-tags?${params.toString()}`;
|
|
2597
|
+
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
2598
|
+
console.error(`[nworks] GET ${url}`);
|
|
2599
|
+
}
|
|
2600
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
2601
|
+
if (!res.ok) return handleUserApiError(res);
|
|
2602
|
+
const data = await res.json();
|
|
2603
|
+
return { contactTags: data.contactTags ?? [], responseMetaData: data.responseMetaData };
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
// src/commands/contact.ts
|
|
2607
|
+
var listCommand6 = new Command11("list").description("List contacts (requires User OAuth with contact or contact.read scope)").option("--user <userId>", "Target user ID (default: me)").option("--count <n>", "Items per page (default: 20)", "20").option("--cursor <cursor>", "Pagination cursor").option("--tag <contactTagId>", "Filter by contact tag ID").option("--email <email>", "Filter by email").option("--tel <telephone>", "Filter by phone number").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
|
|
2608
|
+
try {
|
|
2609
|
+
const result = await listContacts({
|
|
2610
|
+
userId: opts.user ?? "me",
|
|
2611
|
+
count: parseInt(opts.count, 10),
|
|
2612
|
+
cursor: opts.cursor,
|
|
2613
|
+
contactTagId: opts.tag,
|
|
2614
|
+
email: opts.email,
|
|
2615
|
+
telephone: opts.tel,
|
|
2616
|
+
profile: opts.profile
|
|
2305
2617
|
});
|
|
2306
|
-
|
|
2307
|
-
|
|
2618
|
+
output(
|
|
2619
|
+
{
|
|
2620
|
+
contacts: result.contacts,
|
|
2621
|
+
count: result.contacts.length,
|
|
2622
|
+
nextCursor: result.responseMetaData?.nextCursor ?? null
|
|
2623
|
+
},
|
|
2624
|
+
opts
|
|
2625
|
+
);
|
|
2626
|
+
} catch (err) {
|
|
2627
|
+
cliError(err, opts, "contact");
|
|
2308
2628
|
}
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
} else {
|
|
2317
|
-
results.push({ check: "apiConnection", status: "FAIL", detail: `HTTP ${res.status}` });
|
|
2318
|
-
}
|
|
2319
|
-
} catch (e) {
|
|
2320
|
-
results.push({ check: "apiConnection", status: "FAIL", detail: `\uC5F0\uACB0 \uC2E4\uD328: ${e.message}` });
|
|
2321
|
-
}
|
|
2322
|
-
} else if (userToken && userToken.expiresAt > Date.now() / 1e3) {
|
|
2323
|
-
try {
|
|
2324
|
-
const res = await fetch("https://www.worksapis.com/v1.0/users/me", {
|
|
2325
|
-
headers: { Authorization: `Bearer ${userToken.accessToken}` }
|
|
2326
|
-
});
|
|
2327
|
-
if (res.ok) {
|
|
2328
|
-
results.push({ check: "apiConnection", status: "OK", detail: "NAVER WORKS API \uC5F0\uACB0 \uC131\uACF5" });
|
|
2329
|
-
} else {
|
|
2330
|
-
results.push({ check: "apiConnection", status: "FAIL", detail: `HTTP ${res.status}` });
|
|
2331
|
-
}
|
|
2332
|
-
} catch (e) {
|
|
2333
|
-
results.push({ check: "apiConnection", status: "FAIL", detail: `\uC5F0\uACB0 \uC2E4\uD328: ${e.message}` });
|
|
2334
|
-
}
|
|
2335
|
-
} else {
|
|
2336
|
-
results.push({ check: "apiConnection", status: "SKIP", detail: "\uC720\uD6A8\uD55C \uD1A0\uD070 \uC5C6\uC74C \u2014 API \uD14C\uC2A4\uD2B8 \uAC74\uB108\uB700" });
|
|
2629
|
+
});
|
|
2630
|
+
var getCommand = new Command11("get").description("Get a contact by ID (requires User OAuth with contact or contact.read scope)").requiredOption("--id <contactId>", "Contact ID (from contact list)").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
|
|
2631
|
+
try {
|
|
2632
|
+
const result = await getContact(opts.id, opts.profile);
|
|
2633
|
+
output(result, opts);
|
|
2634
|
+
} catch (err) {
|
|
2635
|
+
cliError(err, opts, "contact");
|
|
2337
2636
|
}
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
var doctorCommand = new Command11("doctor").description("Check nworks configuration and connectivity").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
|
|
2637
|
+
});
|
|
2638
|
+
var createCommand4 = new Command11("create").description("Create a contact (requires User OAuth with contact scope)").requiredOption("--payload <json>", "Contact fields as JSON (passed through to the NAVER WORKS API)").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").option("--dry-run", "Print request without creating").action(async (opts) => {
|
|
2341
2639
|
try {
|
|
2342
|
-
const
|
|
2343
|
-
if (opts.
|
|
2344
|
-
output(
|
|
2345
|
-
|
|
2346
|
-
console.log("\n nworks doctor\n");
|
|
2347
|
-
for (const r of results) {
|
|
2348
|
-
const icon = r.status === "OK" ? "\u2705" : r.status === "SKIP" ? "\u2796" : "\u274C";
|
|
2349
|
-
console.log(` ${icon} ${r.check.padEnd(16)} ${r.detail}`);
|
|
2350
|
-
}
|
|
2351
|
-
console.log();
|
|
2640
|
+
const payload = JSON.parse(opts.payload);
|
|
2641
|
+
if (opts.dryRun) {
|
|
2642
|
+
output({ dryRun: true, request: payload }, opts);
|
|
2643
|
+
return;
|
|
2352
2644
|
}
|
|
2645
|
+
const result = await createContact(payload, opts.profile);
|
|
2646
|
+
output({ success: true, ...result }, opts);
|
|
2353
2647
|
} catch (err) {
|
|
2354
|
-
cliError(err, opts);
|
|
2648
|
+
cliError(err, opts, "contact");
|
|
2649
|
+
}
|
|
2650
|
+
});
|
|
2651
|
+
var updateCommand3 = new Command11("update").description("Update a contact (requires User OAuth with contact scope)").requiredOption("--id <contactId>", "Contact ID (from contact list)").requiredOption("--payload <json>", "Fields to update as JSON (passed through to the NAVER WORKS API)").option("--replace", "Use PUT instead of PATCH (replace entire contact)", false).option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
|
|
2652
|
+
try {
|
|
2653
|
+
const payload = JSON.parse(opts.payload);
|
|
2654
|
+
const result = await updateContact(
|
|
2655
|
+
opts.id,
|
|
2656
|
+
payload,
|
|
2657
|
+
!opts.replace,
|
|
2658
|
+
opts.profile
|
|
2659
|
+
);
|
|
2660
|
+
output({ success: true, ...result }, opts);
|
|
2661
|
+
} catch (err) {
|
|
2662
|
+
cliError(err, opts, "contact");
|
|
2663
|
+
}
|
|
2664
|
+
});
|
|
2665
|
+
var deleteCommand3 = new Command11("delete").description("Delete a contact (requires User OAuth with contact scope)").requiredOption("--id <contactId>", "Contact ID (from contact list)").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
|
|
2666
|
+
try {
|
|
2667
|
+
await deleteContact(opts.id, opts.profile);
|
|
2668
|
+
output({ success: true, contactId: opts.id, message: "Contact deleted" }, opts);
|
|
2669
|
+
} catch (err) {
|
|
2670
|
+
cliError(err, opts, "contact");
|
|
2671
|
+
}
|
|
2672
|
+
});
|
|
2673
|
+
var listTagsCommand = new Command11("list-tags").description("List contact tags (requires User OAuth with contact or contact.read scope)").option("--user <userId>", "Target user ID (default: me)").option("--count <n>", "Items per page (default: 20)", "20").option("--cursor <cursor>", "Pagination cursor").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
|
|
2674
|
+
try {
|
|
2675
|
+
const result = await listContactTags({
|
|
2676
|
+
userId: opts.user ?? "me",
|
|
2677
|
+
count: parseInt(opts.count, 10),
|
|
2678
|
+
cursor: opts.cursor,
|
|
2679
|
+
profile: opts.profile
|
|
2680
|
+
});
|
|
2681
|
+
output(
|
|
2682
|
+
{
|
|
2683
|
+
contactTags: result.contactTags,
|
|
2684
|
+
count: result.contactTags.length,
|
|
2685
|
+
nextCursor: result.responseMetaData?.nextCursor ?? null
|
|
2686
|
+
},
|
|
2687
|
+
opts
|
|
2688
|
+
);
|
|
2689
|
+
} catch (err) {
|
|
2690
|
+
cliError(err, opts, "contact");
|
|
2355
2691
|
}
|
|
2356
2692
|
});
|
|
2693
|
+
var contactCommand = new Command11("contact").description("Contact operations (requires User OAuth with contact scope)").addCommand(listCommand6).addCommand(getCommand).addCommand(createCommand4).addCommand(updateCommand3).addCommand(deleteCommand3).addCommand(listTagsCommand);
|
|
2357
2694
|
|
|
2358
|
-
// src/mcp
|
|
2359
|
-
|
|
2360
|
-
|
|
2695
|
+
// src/commands/mcp-cmd.ts
|
|
2696
|
+
import { Command as Command13 } from "commander";
|
|
2697
|
+
import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2698
|
+
|
|
2699
|
+
// src/mcp/server.ts
|
|
2700
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2701
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2702
|
+
|
|
2703
|
+
// src/mcp/tools/setup.ts
|
|
2704
|
+
import { z as z2 } from "zod";
|
|
2705
|
+
|
|
2706
|
+
// src/mcp/tool-registry.ts
|
|
2707
|
+
var TOOL_ANNOTATIONS = {
|
|
2708
|
+
nworks_setup: { title: "Save credentials", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
2709
|
+
nworks_message_send: { title: "Send message", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
2710
|
+
nworks_message_members: { title: "List channel members", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2711
|
+
nworks_directory_members: { title: "List organization members", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2712
|
+
nworks_calendar_list: { title: "List calendar events", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2713
|
+
nworks_calendar_create: { title: "Create calendar event", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
2714
|
+
nworks_calendar_update: { title: "Update calendar event", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
2715
|
+
nworks_calendar_delete: { title: "Delete calendar event", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
2716
|
+
nworks_drive_list: { title: "List drive files", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2717
|
+
nworks_drive_upload: { title: "Upload file to drive", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
2718
|
+
// download는 outputDir 지정 시 로컬 파일을 쓰므로 순수 읽기가 아니다(readOnlyHint=false).
|
|
2719
|
+
nworks_drive_download: { title: "Download drive file", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
2720
|
+
nworks_sharedrive_list: { title: "List shared drives", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2721
|
+
nworks_sharedrive_files: { title: "List shared drive files", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2722
|
+
nworks_sharedrive_download: { title: "Download shared drive file", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
2723
|
+
nworks_mail_send: { title: "Send mail", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
2724
|
+
nworks_mail_list: { title: "List mail", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2725
|
+
nworks_mail_read: { title: "Read mail", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2726
|
+
// drive_download와 같은 이유로 outputDir 지정 시 로컬 파일을 쓴다(readOnlyHint=false).
|
|
2727
|
+
nworks_mail_download_attachment: { title: "Download mail attachment", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
2728
|
+
nworks_task_list: { title: "List tasks", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2729
|
+
nworks_task_create: { title: "Create task", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
2730
|
+
nworks_task_update: { title: "Update task", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
2731
|
+
nworks_task_delete: { title: "Delete task", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
2732
|
+
nworks_board_list: { title: "List boards", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2733
|
+
nworks_board_posts: { title: "List board posts", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2734
|
+
nworks_board_read: { title: "Read board post", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2735
|
+
nworks_board_create: { title: "Create board post", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
2736
|
+
nworks_contact_list: { title: "List contacts", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2737
|
+
nworks_contact_get: { title: "Get contact", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2738
|
+
nworks_contact_create: { title: "Create contact", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
2739
|
+
nworks_contact_update: { title: "Update contact", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
2740
|
+
nworks_contact_delete: { title: "Delete contact", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
2741
|
+
nworks_contact_list_tags: { title: "List contact tags", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2742
|
+
nworks_login_user: { title: "Log in with User OAuth", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
2743
|
+
nworks_whoami: { title: "Show auth status", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2744
|
+
nworks_logout: { title: "Log out", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
2745
|
+
nworks_doctor: { title: "Diagnose connection", readOnlyHint: true, idempotentHint: true, openWorldHint: true }
|
|
2746
|
+
};
|
|
2747
|
+
function defineTool(server, name, description, inputSchema, handler) {
|
|
2748
|
+
const annotations = TOOL_ANNOTATIONS[name];
|
|
2749
|
+
server.registerTool(
|
|
2750
|
+
name,
|
|
2751
|
+
{ description, inputSchema, ...annotations ? { annotations } : {} },
|
|
2752
|
+
handler
|
|
2753
|
+
);
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
// src/mcp/tools/setup.ts
|
|
2757
|
+
function registerSetupTools(server) {
|
|
2758
|
+
defineTool(
|
|
2759
|
+
server,
|
|
2361
2760
|
"nworks_setup",
|
|
2362
2761
|
`NAVER WORKS API \uC778\uC99D \uC815\uBCF4\uB97C \uC124\uC815\uD569\uB2C8\uB2E4.
|
|
2363
2762
|
|
|
@@ -2380,10 +2779,10 @@ function registerTools(server) {
|
|
|
2380
2779
|
|
|
2381
2780
|
OAuth Redirect URI: http://localhost:9876/callback`,
|
|
2382
2781
|
{
|
|
2383
|
-
clientId:
|
|
2384
|
-
serviceAccount:
|
|
2385
|
-
botId:
|
|
2386
|
-
domainId:
|
|
2782
|
+
clientId: z2.string().describe("Client ID (Developer Console\uC5D0\uC11C \uBC1C\uAE09)"),
|
|
2783
|
+
serviceAccount: z2.string().optional().describe("Service Account ID (\uC608: xxxxx.serviceaccount@domain)"),
|
|
2784
|
+
botId: z2.string().optional().describe("Bot ID (\uBA54\uC2DC\uC9C0 \uC804\uC1A1 \uC2DC \uD544\uC694)"),
|
|
2785
|
+
domainId: z2.string().optional().describe("Domain ID")
|
|
2387
2786
|
},
|
|
2388
2787
|
async ({ clientId, serviceAccount, botId, domainId }) => {
|
|
2389
2788
|
try {
|
|
@@ -2421,7 +2820,7 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2421
2820
|
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');
|
|
2422
2821
|
}
|
|
2423
2822
|
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");
|
|
2424
|
-
const
|
|
2823
|
+
const mask2 = (s) => s.length <= 4 ? "****" : `****${s.slice(-Math.min(4, Math.floor(s.length / 3)))}`;
|
|
2425
2824
|
return {
|
|
2426
2825
|
content: [
|
|
2427
2826
|
{
|
|
@@ -2430,10 +2829,10 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2430
2829
|
success: true,
|
|
2431
2830
|
message: "\uC778\uC99D \uC815\uBCF4\uAC00 \uC800\uC7A5\uB418\uC5C8\uC2B5\uB2C8\uB2E4.",
|
|
2432
2831
|
nextSteps,
|
|
2433
|
-
clientId:
|
|
2434
|
-
clientSecret: `${
|
|
2832
|
+
clientId: mask2(clientId),
|
|
2833
|
+
clientSecret: `${mask2(resolvedSecret)} (\uD658\uACBD\uBCC0\uC218)`,
|
|
2435
2834
|
serviceAccount: serviceAccount ?? null,
|
|
2436
|
-
privateKeyPath: resolvedPrivateKeyPath ? `${
|
|
2835
|
+
privateKeyPath: resolvedPrivateKeyPath ? `${mask2(resolvedPrivateKeyPath)} (\uD658\uACBD\uBCC0\uC218)` : null,
|
|
2437
2836
|
botId: botId ?? null
|
|
2438
2837
|
})
|
|
2439
2838
|
}
|
|
@@ -2447,16 +2846,22 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2447
2846
|
}
|
|
2448
2847
|
}
|
|
2449
2848
|
);
|
|
2450
|
-
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2851
|
+
// src/mcp/tools/message.ts
|
|
2852
|
+
import { z as z3 } from "zod";
|
|
2853
|
+
function registerMessageTools(server) {
|
|
2854
|
+
defineTool(
|
|
2855
|
+
server,
|
|
2451
2856
|
"nworks_message_send",
|
|
2452
2857
|
"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)",
|
|
2453
2858
|
{
|
|
2454
|
-
to:
|
|
2455
|
-
channel:
|
|
2456
|
-
text:
|
|
2457
|
-
type:
|
|
2458
|
-
actions:
|
|
2459
|
-
elements:
|
|
2859
|
+
to: z3.string().optional().describe("\uC218\uC2E0\uC790 userId (channel\uACFC \uD0DD 1). nworks_directory_members\uB85C userId \uC870\uD68C \uAC00\uB2A5"),
|
|
2860
|
+
channel: z3.string().optional().describe("\uCC44\uB110 channelId (to\uC640 \uD0DD 1). nworks_message_members\uB85C \uCC44\uB110 \uAD6C\uC131\uC6D0 \uD655\uC778 \uAC00\uB2A5"),
|
|
2861
|
+
text: z3.string().describe("\uBA54\uC2DC\uC9C0 \uBCF8\uBB38"),
|
|
2862
|
+
type: z3.enum(["text", "button", "list"]).optional().describe("\uBA54\uC2DC\uC9C0 \uD0C0\uC785 (\uAE30\uBCF8: text)"),
|
|
2863
|
+
actions: z3.string().optional().describe("\uBC84\uD2BC \uC561\uC158 JSON (type=button\uC77C \uB54C)"),
|
|
2864
|
+
elements: z3.string().optional().describe("\uB9AC\uC2A4\uD2B8 \uD56D\uBAA9 JSON (type=list\uC77C \uB54C)")
|
|
2460
2865
|
},
|
|
2461
2866
|
async ({ to, channel, text, type, actions, elements }) => {
|
|
2462
2867
|
try {
|
|
@@ -2479,11 +2884,12 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2479
2884
|
}
|
|
2480
2885
|
}
|
|
2481
2886
|
);
|
|
2482
|
-
|
|
2887
|
+
defineTool(
|
|
2888
|
+
server,
|
|
2483
2889
|
"nworks_message_members",
|
|
2484
2890
|
"\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)",
|
|
2485
2891
|
{
|
|
2486
|
-
channel:
|
|
2892
|
+
channel: z3.string().describe("\uCC44\uB110 channelId")
|
|
2487
2893
|
},
|
|
2488
2894
|
async ({ channel }) => {
|
|
2489
2895
|
try {
|
|
@@ -2499,15 +2905,29 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2499
2905
|
}
|
|
2500
2906
|
}
|
|
2501
2907
|
);
|
|
2502
|
-
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
// src/mcp/tools/directory.ts
|
|
2911
|
+
import { z as z4 } from "zod";
|
|
2912
|
+
function registerDirectoryTools(server) {
|
|
2913
|
+
defineTool(
|
|
2914
|
+
server,
|
|
2503
2915
|
"nworks_directory_members",
|
|
2504
|
-
"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",
|
|
2505
|
-
{
|
|
2506
|
-
|
|
2916
|
+
"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. \uAD6C\uC131\uC6D0\uC774 \uB9CE\uC73C\uBA74 nextCursor\uB85C \uB2E4\uC74C \uD398\uC774\uC9C0\uB97C \uC774\uC5B4\uC11C \uC870\uD68C",
|
|
2917
|
+
{
|
|
2918
|
+
count: z4.number().int().min(1).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 100)"),
|
|
2919
|
+
cursor: z4.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C (\uC774\uC804 \uC751\uB2F5\uC758 nextCursor \uAC12)")
|
|
2920
|
+
},
|
|
2921
|
+
async ({ count, cursor }) => {
|
|
2507
2922
|
try {
|
|
2508
|
-
const result = await listUsers();
|
|
2923
|
+
const result = await listUsers(count ?? 100, cursor);
|
|
2509
2924
|
return {
|
|
2510
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
2925
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
2926
|
+
users: result.users,
|
|
2927
|
+
count: result.users.length,
|
|
2928
|
+
hasMore: !!result.responseMetaData?.nextCursor,
|
|
2929
|
+
nextCursor: result.responseMetaData?.nextCursor ?? null
|
|
2930
|
+
}) }]
|
|
2511
2931
|
};
|
|
2512
2932
|
} catch (err) {
|
|
2513
2933
|
return {
|
|
@@ -2517,13 +2937,19 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2517
2937
|
}
|
|
2518
2938
|
}
|
|
2519
2939
|
);
|
|
2520
|
-
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
// src/mcp/tools/calendar.ts
|
|
2943
|
+
import { z as z5 } from "zod";
|
|
2944
|
+
function registerCalendarTools(server) {
|
|
2945
|
+
defineTool(
|
|
2946
|
+
server,
|
|
2521
2947
|
"nworks_calendar_list",
|
|
2522
2948
|
"\uC0AC\uC6A9\uC790\uC758 \uCE98\uB9B0\uB354 \uC77C\uC815/\uC2A4\uCF00\uC904\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. '\uC624\uB298 \uC77C\uC815 \uC54C\uB824\uC918', '\uC774\uBC88 \uC8FC \uC2A4\uCF00\uC904 \uD655\uC778' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. User OAuth \uC778\uC99D \uD544\uC694 (calendar.read scope). \uBBF8\uB85C\uADF8\uC778 \uC2DC nworks_login_user\uB85C \uB85C\uADF8\uC778 \uD544\uC694",
|
|
2523
2949
|
{
|
|
2524
|
-
fromDateTime:
|
|
2525
|
-
untilDateTime:
|
|
2526
|
-
userId:
|
|
2950
|
+
fromDateTime: z5.string().describe("\uC2DC\uC791 \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss+09:00)"),
|
|
2951
|
+
untilDateTime: z5.string().describe("\uC885\uB8CC \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss+09:00)"),
|
|
2952
|
+
userId: z5.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
|
|
2527
2953
|
},
|
|
2528
2954
|
async ({ fromDateTime, untilDateTime, userId }) => {
|
|
2529
2955
|
try {
|
|
@@ -2552,19 +2978,20 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2552
2978
|
}
|
|
2553
2979
|
}
|
|
2554
2980
|
);
|
|
2555
|
-
|
|
2981
|
+
defineTool(
|
|
2982
|
+
server,
|
|
2556
2983
|
"nworks_calendar_create",
|
|
2557
2984
|
"\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)",
|
|
2558
2985
|
{
|
|
2559
|
-
summary:
|
|
2560
|
-
start:
|
|
2561
|
-
end:
|
|
2562
|
-
timeZone:
|
|
2563
|
-
description:
|
|
2564
|
-
location:
|
|
2565
|
-
attendees:
|
|
2566
|
-
sendNotification:
|
|
2567
|
-
userId:
|
|
2986
|
+
summary: z5.string().describe("\uC77C\uC815 \uC81C\uBAA9"),
|
|
2987
|
+
start: z5.string().describe("\uC2DC\uC791 \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss)"),
|
|
2988
|
+
end: z5.string().describe("\uC885\uB8CC \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss)"),
|
|
2989
|
+
timeZone: z5.string().optional().describe("\uD0C0\uC784\uC874 (\uAE30\uBCF8: Asia/Seoul)"),
|
|
2990
|
+
description: z5.string().optional().describe("\uC77C\uC815 \uC124\uBA85"),
|
|
2991
|
+
location: z5.string().optional().describe("\uC7A5\uC18C"),
|
|
2992
|
+
attendees: z5.array(z5.object({ email: z5.string(), displayName: z5.string().optional() })).optional().describe("\uCC38\uC11D\uC790 \uBAA9\uB85D"),
|
|
2993
|
+
sendNotification: z5.boolean().optional().describe("\uCC38\uC11D\uC790\uC5D0\uAC8C \uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)"),
|
|
2994
|
+
userId: z5.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
|
|
2568
2995
|
},
|
|
2569
2996
|
async ({
|
|
2570
2997
|
summary,
|
|
@@ -2601,19 +3028,20 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2601
3028
|
}
|
|
2602
3029
|
}
|
|
2603
3030
|
);
|
|
2604
|
-
|
|
3031
|
+
defineTool(
|
|
3032
|
+
server,
|
|
2605
3033
|
"nworks_calendar_update",
|
|
2606
3034
|
"\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",
|
|
2607
3035
|
{
|
|
2608
|
-
eventId:
|
|
2609
|
-
summary:
|
|
2610
|
-
start:
|
|
2611
|
-
end:
|
|
2612
|
-
timeZone:
|
|
2613
|
-
description:
|
|
2614
|
-
location:
|
|
2615
|
-
sendNotification:
|
|
2616
|
-
userId:
|
|
3036
|
+
eventId: z5.string().describe("\uC77C\uC815 ID (nworks_calendar_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
|
|
3037
|
+
summary: z5.string().optional().describe("\uC0C8 \uC81C\uBAA9"),
|
|
3038
|
+
start: z5.string().optional().describe("\uC0C8 \uC2DC\uC791 \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss)"),
|
|
3039
|
+
end: z5.string().optional().describe("\uC0C8 \uC885\uB8CC \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss)"),
|
|
3040
|
+
timeZone: z5.string().optional().describe("\uD0C0\uC784\uC874 (\uAE30\uBCF8: Asia/Seoul)"),
|
|
3041
|
+
description: z5.string().optional().describe("\uC0C8 \uC124\uBA85"),
|
|
3042
|
+
location: z5.string().optional().describe("\uC0C8 \uC7A5\uC18C"),
|
|
3043
|
+
sendNotification: z5.boolean().optional().describe("\uCC38\uC11D\uC790\uC5D0\uAC8C \uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)"),
|
|
3044
|
+
userId: z5.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
|
|
2617
3045
|
},
|
|
2618
3046
|
async ({
|
|
2619
3047
|
eventId,
|
|
@@ -2649,13 +3077,14 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2649
3077
|
}
|
|
2650
3078
|
}
|
|
2651
3079
|
);
|
|
2652
|
-
|
|
3080
|
+
defineTool(
|
|
3081
|
+
server,
|
|
2653
3082
|
"nworks_calendar_delete",
|
|
2654
3083
|
"\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",
|
|
2655
3084
|
{
|
|
2656
|
-
eventId:
|
|
2657
|
-
sendNotification:
|
|
2658
|
-
userId:
|
|
3085
|
+
eventId: z5.string().describe("\uC0AD\uC81C\uD560 \uC77C\uC815 ID (nworks_calendar_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
|
|
3086
|
+
sendNotification: z5.boolean().optional().describe("\uCC38\uC11D\uC790\uC5D0\uAC8C \uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)"),
|
|
3087
|
+
userId: z5.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
|
|
2659
3088
|
},
|
|
2660
3089
|
async ({ eventId, sendNotification, userId }) => {
|
|
2661
3090
|
try {
|
|
@@ -2675,14 +3104,20 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2675
3104
|
}
|
|
2676
3105
|
}
|
|
2677
3106
|
);
|
|
2678
|
-
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
// src/mcp/tools/drive.ts
|
|
3110
|
+
import { z as z6 } from "zod";
|
|
3111
|
+
function registerDriveTools(server) {
|
|
3112
|
+
defineTool(
|
|
3113
|
+
server,
|
|
2679
3114
|
"nworks_drive_list",
|
|
2680
3115
|
"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)",
|
|
2681
3116
|
{
|
|
2682
|
-
userId:
|
|
2683
|
-
folderId:
|
|
2684
|
-
count:
|
|
2685
|
-
cursor:
|
|
3117
|
+
userId: z6.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)"),
|
|
3118
|
+
folderId: z6.string().optional().describe("\uD3F4\uB354 ID (\uBBF8\uC9C0\uC815 \uC2DC \uB8E8\uD2B8)"),
|
|
3119
|
+
count: z6.number().int().min(1).max(200).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 200)"),
|
|
3120
|
+
cursor: z6.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
|
|
2686
3121
|
},
|
|
2687
3122
|
async ({ userId, folderId, count, cursor }) => {
|
|
2688
3123
|
try {
|
|
@@ -2711,16 +3146,17 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2711
3146
|
}
|
|
2712
3147
|
}
|
|
2713
3148
|
);
|
|
2714
|
-
|
|
3149
|
+
defineTool(
|
|
3150
|
+
server,
|
|
2715
3151
|
"nworks_drive_upload",
|
|
2716
3152
|
"\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.",
|
|
2717
3153
|
{
|
|
2718
|
-
content:
|
|
2719
|
-
fileName:
|
|
2720
|
-
filePath:
|
|
2721
|
-
userId:
|
|
2722
|
-
folderId:
|
|
2723
|
-
overwrite:
|
|
3154
|
+
content: z6.string().optional().describe("\uC5C5\uB85C\uB4DC\uD560 \uD30C\uC77C \uB0B4\uC6A9 (base64 \uC778\uCF54\uB529). filePath \uB300\uC2E0 \uC0AC\uC6A9"),
|
|
3155
|
+
fileName: z6.string().optional().describe("\uD30C\uC77C\uBA85 (content \uC0AC\uC6A9 \uC2DC \uD544\uC218)"),
|
|
3156
|
+
filePath: z6.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)"),
|
|
3157
|
+
userId: z6.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)"),
|
|
3158
|
+
folderId: z6.string().optional().describe("\uC5C5\uB85C\uB4DC\uD560 \uD3F4\uB354 ID (\uBBF8\uC9C0\uC815 \uC2DC \uB8E8\uD2B8)"),
|
|
3159
|
+
overwrite: z6.boolean().optional().describe("\uB3D9\uC77C \uD30C\uC77C\uBA85 \uB36E\uC5B4\uC4F0\uAE30 (\uAE30\uBCF8: false)")
|
|
2724
3160
|
},
|
|
2725
3161
|
async ({ content, fileName, filePath, userId, folderId, overwrite }) => {
|
|
2726
3162
|
try {
|
|
@@ -2768,14 +3204,15 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2768
3204
|
}
|
|
2769
3205
|
}
|
|
2770
3206
|
);
|
|
2771
|
-
|
|
3207
|
+
defineTool(
|
|
3208
|
+
server,
|
|
2772
3209
|
"nworks_drive_download",
|
|
2773
3210
|
"\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.",
|
|
2774
3211
|
{
|
|
2775
|
-
fileId:
|
|
2776
|
-
outputDir:
|
|
2777
|
-
outputName:
|
|
2778
|
-
userId:
|
|
3212
|
+
fileId: z6.string().describe("\uB2E4\uC6B4\uB85C\uB4DC\uD560 \uD30C\uC77C ID (nworks_drive_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
|
|
3213
|
+
outputDir: z6.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)"),
|
|
3214
|
+
outputName: z6.string().optional().describe("\uC800\uC7A5 \uD30C\uC77C\uBA85 (\uBBF8\uC9C0\uC815 \uC2DC \uC6D0\uBCF8 \uD30C\uC77C\uBA85)"),
|
|
3215
|
+
userId: z6.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
|
|
2779
3216
|
},
|
|
2780
3217
|
async ({ fileId, outputDir, outputName, userId }) => {
|
|
2781
3218
|
try {
|
|
@@ -2785,13 +3222,13 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2785
3222
|
);
|
|
2786
3223
|
const fileName = outputName ?? result.fileName ?? fileId;
|
|
2787
3224
|
if (outputDir) {
|
|
2788
|
-
const { writeFile:
|
|
2789
|
-
const { join:
|
|
3225
|
+
const { writeFile: writeFile4 } = await import("fs/promises");
|
|
3226
|
+
const { join: join5 } = await import("path");
|
|
2790
3227
|
const safeDir = validateLocalPath(outputDir);
|
|
2791
3228
|
const safeName = sanitizeFileName(fileName);
|
|
2792
|
-
const outPath =
|
|
3229
|
+
const outPath = join5(safeDir, safeName);
|
|
2793
3230
|
validateLocalPath(outPath, safeDir);
|
|
2794
|
-
await
|
|
3231
|
+
await writeFile4(outPath, result.buffer);
|
|
2795
3232
|
return {
|
|
2796
3233
|
content: [{ type: "text", text: JSON.stringify({ success: true, fileName, path: outPath, size: result.buffer.length }) }]
|
|
2797
3234
|
};
|
|
@@ -2824,66 +3261,186 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2824
3261
|
}
|
|
2825
3262
|
}
|
|
2826
3263
|
);
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
"
|
|
3264
|
+
defineTool(
|
|
3265
|
+
server,
|
|
3266
|
+
"nworks_sharedrive_list",
|
|
3267
|
+
"NAVER WORKS \uACF5\uC720 \uB4DC\uB77C\uC774\uBE0C \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. '\uACF5\uC720 \uB4DC\uB77C\uC774\uBE0C \uBB50 \uC788\uC5B4?', '\uD300 \uACF5\uC720 \uB4DC\uB77C\uC774\uBE0C \uCC3E\uC544\uC918' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. User OAuth \uC778\uC99D \uD544\uC694 (file.read scope)",
|
|
2830
3268
|
{
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
body: z.string().optional().describe("\uBA54\uC77C \uBCF8\uBB38"),
|
|
2834
|
-
cc: z.string().optional().describe("\uCC38\uC870 \uC774\uBA54\uC77C (\uC5EC\uB7EC \uBA85\uC740 ; \uB85C \uAD6C\uBD84)"),
|
|
2835
|
-
bcc: z.string().optional().describe("\uC228\uC740\uCC38\uC870 \uC774\uBA54\uC77C (\uC5EC\uB7EC \uBA85\uC740 ; \uB85C \uAD6C\uBD84)"),
|
|
2836
|
-
contentType: z.enum(["html", "text"]).optional().describe("\uBCF8\uBB38 \uD615\uC2DD (\uAE30\uBCF8: html)"),
|
|
2837
|
-
userId: z.string().optional().describe("\uBC1C\uC2E0\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
|
|
3269
|
+
count: z6.number().int().min(1).max(200).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 200)"),
|
|
3270
|
+
cursor: z6.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
|
|
2838
3271
|
},
|
|
2839
|
-
async ({
|
|
3272
|
+
async ({ count, cursor }) => {
|
|
2840
3273
|
try {
|
|
2841
|
-
await
|
|
2842
|
-
to,
|
|
2843
|
-
subject,
|
|
2844
|
-
body,
|
|
2845
|
-
cc,
|
|
2846
|
-
bcc,
|
|
2847
|
-
contentType,
|
|
2848
|
-
userId: userId ?? "me"
|
|
2849
|
-
});
|
|
3274
|
+
const result = await listSharedDrives(count ?? 20, cursor);
|
|
2850
3275
|
return {
|
|
2851
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
3276
|
+
content: [{ type: "text", text: JSON.stringify({ sharedDrives: result.sharedDrives, count: result.sharedDrives.length, hasMore: !!result.responseMetaData?.nextCursor, nextCursor: result.responseMetaData?.nextCursor ?? null }) }]
|
|
2852
3277
|
};
|
|
2853
3278
|
} catch (err) {
|
|
2854
3279
|
return {
|
|
2855
|
-
content: [{ type: "text", text: mcpErrorHint(err, "
|
|
3280
|
+
content: [{ type: "text", text: mcpErrorHint(err, "drive.sharedrive_list") }],
|
|
2856
3281
|
isError: true
|
|
2857
3282
|
};
|
|
2858
3283
|
}
|
|
2859
3284
|
}
|
|
2860
3285
|
);
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
"
|
|
3286
|
+
defineTool(
|
|
3287
|
+
server,
|
|
3288
|
+
"nworks_sharedrive_files",
|
|
3289
|
+
"\uACF5\uC720 \uB4DC\uB77C\uC774\uBE0C\uC758 \uD30C\uC77C/\uD3F4\uB354 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. sharedriveId\uB294 nworks_sharedrive_list\uB85C \uC870\uD68C \uAC00\uB2A5. User OAuth \uC778\uC99D \uD544\uC694 (file.read scope)",
|
|
2864
3290
|
{
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
|
|
3291
|
+
sharedriveId: z6.string().describe("\uACF5\uC720 \uB4DC\uB77C\uC774\uBE0C ID (nworks_sharedrive_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
|
|
3292
|
+
folderId: z6.string().optional().describe("\uD3F4\uB354 ID (\uBBF8\uC9C0\uC815 \uC2DC \uB8E8\uD2B8)"),
|
|
3293
|
+
count: z6.number().int().min(1).max(200).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 200)"),
|
|
3294
|
+
cursor: z6.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
|
|
2870
3295
|
},
|
|
2871
|
-
async ({ folderId, count, cursor
|
|
3296
|
+
async ({ sharedriveId, folderId, count, cursor }) => {
|
|
2872
3297
|
try {
|
|
2873
|
-
const result = await
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
count ??
|
|
2877
|
-
cursor
|
|
2878
|
-
isUnread
|
|
3298
|
+
const result = await listSharedDriveFiles(
|
|
3299
|
+
sharedriveId,
|
|
3300
|
+
folderId,
|
|
3301
|
+
count ?? 20,
|
|
3302
|
+
cursor
|
|
2879
3303
|
);
|
|
2880
|
-
const
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
3304
|
+
const files = result.files.map((f) => ({
|
|
3305
|
+
fileId: f.fileId,
|
|
3306
|
+
name: f.fileName,
|
|
3307
|
+
type: f.fileType,
|
|
3308
|
+
size: f.fileSize,
|
|
3309
|
+
modified: f.modifiedTime,
|
|
3310
|
+
path: f.filePath
|
|
3311
|
+
}));
|
|
3312
|
+
return {
|
|
3313
|
+
content: [{ type: "text", text: JSON.stringify({ files, count: files.length, hasMore: !!result.responseMetaData?.nextCursor, nextCursor: result.responseMetaData?.nextCursor ?? null }) }]
|
|
3314
|
+
};
|
|
3315
|
+
} catch (err) {
|
|
3316
|
+
return {
|
|
3317
|
+
content: [{ type: "text", text: mcpErrorHint(err, "drive.sharedrive_files") }],
|
|
3318
|
+
isError: true
|
|
3319
|
+
};
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
);
|
|
3323
|
+
defineTool(
|
|
3324
|
+
server,
|
|
3325
|
+
"nworks_sharedrive_download",
|
|
3326
|
+
"\uACF5\uC720 \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.",
|
|
3327
|
+
{
|
|
3328
|
+
sharedriveId: z6.string().describe("\uACF5\uC720 \uB4DC\uB77C\uC774\uBE0C ID (nworks_sharedrive_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
|
|
3329
|
+
fileId: z6.string().describe("\uB2E4\uC6B4\uB85C\uB4DC\uD560 \uD30C\uC77C ID (nworks_sharedrive_files\uB85C \uC870\uD68C \uAC00\uB2A5)"),
|
|
3330
|
+
outputDir: z6.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)"),
|
|
3331
|
+
outputName: z6.string().optional().describe("\uC800\uC7A5 \uD30C\uC77C\uBA85 (\uBBF8\uC9C0\uC815 \uC2DC \uC6D0\uBCF8 \uD30C\uC77C\uBA85)")
|
|
3332
|
+
},
|
|
3333
|
+
async ({ sharedriveId, fileId, outputDir, outputName }) => {
|
|
3334
|
+
try {
|
|
3335
|
+
const result = await downloadSharedDriveFile(sharedriveId, fileId);
|
|
3336
|
+
const fileName = outputName ?? result.fileName ?? fileId;
|
|
3337
|
+
if (outputDir) {
|
|
3338
|
+
const { writeFile: writeFile4 } = await import("fs/promises");
|
|
3339
|
+
const { join: join5 } = await import("path");
|
|
3340
|
+
const safeDir = validateLocalPath(outputDir);
|
|
3341
|
+
const safeName = sanitizeFileName(fileName);
|
|
3342
|
+
const outPath = join5(safeDir, safeName);
|
|
3343
|
+
validateLocalPath(outPath, safeDir);
|
|
3344
|
+
await writeFile4(outPath, result.buffer);
|
|
3345
|
+
return {
|
|
3346
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, fileName, path: outPath, size: result.buffer.length }) }]
|
|
3347
|
+
};
|
|
3348
|
+
}
|
|
3349
|
+
const MAX_INLINE_SIZE = 5 * 1024 * 1024;
|
|
3350
|
+
if (result.buffer.length > MAX_INLINE_SIZE) {
|
|
3351
|
+
const sizeMB = (result.buffer.length / (1024 * 1024)).toFixed(1);
|
|
3352
|
+
return {
|
|
3353
|
+
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 }) }],
|
|
3354
|
+
isError: true
|
|
3355
|
+
};
|
|
3356
|
+
}
|
|
3357
|
+
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;
|
|
3358
|
+
const isText = textExtensions.test(fileName);
|
|
3359
|
+
if (isText) {
|
|
3360
|
+
const text = result.buffer.toString("utf-8");
|
|
3361
|
+
return {
|
|
3362
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, fileName, size: result.buffer.length, encoding: "text", content: text }) }]
|
|
3363
|
+
};
|
|
3364
|
+
}
|
|
3365
|
+
const base64 = result.buffer.toString("base64");
|
|
3366
|
+
return {
|
|
3367
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, fileName, size: result.buffer.length, encoding: "base64", content: base64 }) }]
|
|
3368
|
+
};
|
|
3369
|
+
} catch (err) {
|
|
3370
|
+
return {
|
|
3371
|
+
content: [{ type: "text", text: mcpErrorHint(err, "drive.sharedrive_download") }],
|
|
3372
|
+
isError: true
|
|
3373
|
+
};
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
);
|
|
3377
|
+
}
|
|
3378
|
+
|
|
3379
|
+
// src/mcp/tools/mail.ts
|
|
3380
|
+
import { z as z7 } from "zod";
|
|
3381
|
+
function registerMailTools(server) {
|
|
3382
|
+
defineTool(
|
|
3383
|
+
server,
|
|
3384
|
+
"nworks_mail_send",
|
|
3385
|
+
"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)",
|
|
3386
|
+
{
|
|
3387
|
+
to: z7.string().describe("\uC218\uC2E0\uC790 \uC774\uBA54\uC77C (\uC5EC\uB7EC \uBA85\uC740 ; \uB85C \uAD6C\uBD84)"),
|
|
3388
|
+
subject: z7.string().describe("\uBA54\uC77C \uC81C\uBAA9"),
|
|
3389
|
+
body: z7.string().optional().describe("\uBA54\uC77C \uBCF8\uBB38"),
|
|
3390
|
+
cc: z7.string().optional().describe("\uCC38\uC870 \uC774\uBA54\uC77C (\uC5EC\uB7EC \uBA85\uC740 ; \uB85C \uAD6C\uBD84)"),
|
|
3391
|
+
bcc: z7.string().optional().describe("\uC228\uC740\uCC38\uC870 \uC774\uBA54\uC77C (\uC5EC\uB7EC \uBA85\uC740 ; \uB85C \uAD6C\uBD84)"),
|
|
3392
|
+
contentType: z7.enum(["html", "text"]).optional().describe("\uBCF8\uBB38 \uD615\uC2DD (\uAE30\uBCF8: html)"),
|
|
3393
|
+
userId: z7.string().optional().describe("\uBC1C\uC2E0\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
|
|
3394
|
+
},
|
|
3395
|
+
async ({ to, subject, body, cc, bcc, contentType, userId }) => {
|
|
3396
|
+
try {
|
|
3397
|
+
await sendMail({
|
|
3398
|
+
to,
|
|
3399
|
+
subject,
|
|
3400
|
+
body,
|
|
3401
|
+
cc,
|
|
3402
|
+
bcc,
|
|
3403
|
+
contentType,
|
|
3404
|
+
userId: userId ?? "me"
|
|
3405
|
+
});
|
|
3406
|
+
return {
|
|
3407
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, message: "\uBA54\uC77C\uC774 \uC804\uC1A1\uB418\uC5C8\uC2B5\uB2C8\uB2E4 (\uBE44\uB3D9\uAE30 \uCC98\uB9AC)" }) }]
|
|
3408
|
+
};
|
|
3409
|
+
} catch (err) {
|
|
3410
|
+
return {
|
|
3411
|
+
content: [{ type: "text", text: mcpErrorHint(err, "mail.send") }],
|
|
3412
|
+
isError: true
|
|
3413
|
+
};
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
);
|
|
3417
|
+
defineTool(
|
|
3418
|
+
server,
|
|
3419
|
+
"nworks_mail_list",
|
|
3420
|
+
"\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)",
|
|
3421
|
+
{
|
|
3422
|
+
folderId: z7.number().int().min(0).optional().describe("\uBA54\uC77C \uD3F4\uB354 ID (\uAE30\uBCF8: 0 = \uBC1B\uC740\uD3B8\uC9C0\uD568)"),
|
|
3423
|
+
count: z7.number().int().min(1).max(200).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 30, \uCD5C\uB300: 200)"),
|
|
3424
|
+
cursor: z7.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C"),
|
|
3425
|
+
isUnread: z7.boolean().optional().describe("\uC77D\uC9C0 \uC54A\uC740 \uBA54\uC77C\uB9CC \uC870\uD68C"),
|
|
3426
|
+
userId: z7.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
|
|
3427
|
+
},
|
|
3428
|
+
async ({ folderId, count, cursor, isUnread, userId }) => {
|
|
3429
|
+
try {
|
|
3430
|
+
const result = await listMails(
|
|
3431
|
+
folderId ?? 0,
|
|
3432
|
+
userId ?? "me",
|
|
3433
|
+
count ?? 30,
|
|
3434
|
+
cursor,
|
|
3435
|
+
isUnread
|
|
3436
|
+
);
|
|
3437
|
+
const mails = result.mails.map((m) => ({
|
|
3438
|
+
mailId: m.mailId,
|
|
3439
|
+
from: m.from.email,
|
|
3440
|
+
subject: m.subject,
|
|
3441
|
+
date: m.receivedTime,
|
|
3442
|
+
status: m.status,
|
|
3443
|
+
attachments: m.attachCount ?? 0
|
|
2887
3444
|
}));
|
|
2888
3445
|
return {
|
|
2889
3446
|
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 }) }]
|
|
@@ -2896,12 +3453,13 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2896
3453
|
}
|
|
2897
3454
|
}
|
|
2898
3455
|
);
|
|
2899
|
-
|
|
3456
|
+
defineTool(
|
|
3457
|
+
server,
|
|
2900
3458
|
"nworks_mail_read",
|
|
2901
3459
|
"\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' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. mailId\uB294 nworks_mail_list\uB85C \uC870\uD68C \uAC00\uB2A5. User OAuth \uC778\uC99D \uD544\uC694 (mail.read scope)",
|
|
2902
3460
|
{
|
|
2903
|
-
mailId:
|
|
2904
|
-
userId:
|
|
3461
|
+
mailId: z7.number().int().min(0).describe("\uBA54\uC77C ID (nworks_mail_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
|
|
3462
|
+
userId: z7.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
|
|
2905
3463
|
},
|
|
2906
3464
|
async ({ mailId, userId }) => {
|
|
2907
3465
|
try {
|
|
@@ -2935,15 +3493,80 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2935
3493
|
}
|
|
2936
3494
|
}
|
|
2937
3495
|
);
|
|
2938
|
-
|
|
3496
|
+
defineTool(
|
|
3497
|
+
server,
|
|
3498
|
+
"nworks_mail_download_attachment",
|
|
3499
|
+
"\uBA54\uC77C \uCCA8\uBD80\uD30C\uC77C\uC744 \uB2E4\uC6B4\uB85C\uB4DC\uD569\uB2C8\uB2E4. mailId\uC640 attachmentId\uB294 nworks_mail_read\uB85C \uC870\uD68C \uAC00\uB2A5. User OAuth \uC778\uC99D \uD544\uC694 (mail.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.",
|
|
3500
|
+
{
|
|
3501
|
+
mailId: z7.number().int().min(0).describe("\uBA54\uC77C ID (nworks_mail_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
|
|
3502
|
+
attachmentId: z7.number().int().min(0).describe("\uCCA8\uBD80\uD30C\uC77C ID (nworks_mail_read\uB85C \uC870\uD68C \uAC00\uB2A5)"),
|
|
3503
|
+
outputDir: z7.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)"),
|
|
3504
|
+
outputName: z7.string().optional().describe("\uC800\uC7A5 \uD30C\uC77C\uBA85 (\uBBF8\uC9C0\uC815 \uC2DC \uC6D0\uBCF8 \uD30C\uC77C\uBA85)"),
|
|
3505
|
+
userId: z7.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
|
|
3506
|
+
},
|
|
3507
|
+
async ({ mailId, attachmentId, outputDir, outputName, userId }) => {
|
|
3508
|
+
try {
|
|
3509
|
+
const result = await downloadAttachment(
|
|
3510
|
+
mailId,
|
|
3511
|
+
attachmentId,
|
|
3512
|
+
userId ?? "me"
|
|
3513
|
+
);
|
|
3514
|
+
const fileName = outputName ?? result.filename;
|
|
3515
|
+
if (outputDir) {
|
|
3516
|
+
const { writeFile: writeFile4 } = await import("fs/promises");
|
|
3517
|
+
const { join: join5 } = await import("path");
|
|
3518
|
+
const safeDir = validateLocalPath(outputDir);
|
|
3519
|
+
const safeName = sanitizeFileName(fileName);
|
|
3520
|
+
const outPath = join5(safeDir, safeName);
|
|
3521
|
+
validateLocalPath(outPath, safeDir);
|
|
3522
|
+
await writeFile4(outPath, result.buffer);
|
|
3523
|
+
return {
|
|
3524
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, fileName, path: outPath, size: result.buffer.length }) }]
|
|
3525
|
+
};
|
|
3526
|
+
}
|
|
3527
|
+
const MAX_INLINE_SIZE = 5 * 1024 * 1024;
|
|
3528
|
+
if (result.buffer.length > MAX_INLINE_SIZE) {
|
|
3529
|
+
const sizeMB = (result.buffer.length / (1024 * 1024)).toFixed(1);
|
|
3530
|
+
return {
|
|
3531
|
+
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 }) }],
|
|
3532
|
+
isError: true
|
|
3533
|
+
};
|
|
3534
|
+
}
|
|
3535
|
+
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;
|
|
3536
|
+
const isText = textExtensions.test(fileName);
|
|
3537
|
+
if (isText) {
|
|
3538
|
+
const text = result.buffer.toString("utf-8");
|
|
3539
|
+
return {
|
|
3540
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, fileName, size: result.buffer.length, encoding: "text", content: text }) }]
|
|
3541
|
+
};
|
|
3542
|
+
}
|
|
3543
|
+
const base64 = result.buffer.toString("base64");
|
|
3544
|
+
return {
|
|
3545
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, fileName, size: result.buffer.length, encoding: "base64", content: base64 }) }]
|
|
3546
|
+
};
|
|
3547
|
+
} catch (err) {
|
|
3548
|
+
return {
|
|
3549
|
+
content: [{ type: "text", text: mcpErrorHint(err, "mail.download_attachment") }],
|
|
3550
|
+
isError: true
|
|
3551
|
+
};
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
);
|
|
3555
|
+
}
|
|
3556
|
+
|
|
3557
|
+
// src/mcp/tools/task.ts
|
|
3558
|
+
import { z as z8 } from "zod";
|
|
3559
|
+
function registerTaskTools(server) {
|
|
3560
|
+
defineTool(
|
|
3561
|
+
server,
|
|
2939
3562
|
"nworks_task_list",
|
|
2940
3563
|
"\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)",
|
|
2941
3564
|
{
|
|
2942
|
-
categoryId:
|
|
2943
|
-
status:
|
|
2944
|
-
count:
|
|
2945
|
-
cursor:
|
|
2946
|
-
userId:
|
|
3565
|
+
categoryId: z8.string().optional().describe("\uCE74\uD14C\uACE0\uB9AC ID (\uAE30\uBCF8: default)"),
|
|
3566
|
+
status: z8.enum(["TODO", "ALL"]).optional().describe("\uD544\uD130: TODO \uB610\uB294 ALL (\uAE30\uBCF8: ALL)"),
|
|
3567
|
+
count: z8.number().int().min(1).max(100).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 50, \uCD5C\uB300: 100)"),
|
|
3568
|
+
cursor: z8.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C"),
|
|
3569
|
+
userId: z8.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
|
|
2947
3570
|
},
|
|
2948
3571
|
async ({ categoryId, status, count, cursor, userId }) => {
|
|
2949
3572
|
try {
|
|
@@ -2973,16 +3596,17 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2973
3596
|
}
|
|
2974
3597
|
}
|
|
2975
3598
|
);
|
|
2976
|
-
|
|
3599
|
+
defineTool(
|
|
3600
|
+
server,
|
|
2977
3601
|
"nworks_task_create",
|
|
2978
3602
|
"\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)",
|
|
2979
3603
|
{
|
|
2980
|
-
title:
|
|
2981
|
-
content:
|
|
2982
|
-
dueDate:
|
|
2983
|
-
categoryId:
|
|
2984
|
-
assigneeIds:
|
|
2985
|
-
userId:
|
|
3604
|
+
title: z8.string().describe("\uD560 \uC77C \uC81C\uBAA9"),
|
|
3605
|
+
content: z8.string().optional().describe("\uD560 \uC77C \uB0B4\uC6A9"),
|
|
3606
|
+
dueDate: z8.string().optional().describe("\uB9C8\uAC10\uC77C (YYYY-MM-DD)"),
|
|
3607
|
+
categoryId: z8.string().optional().describe("\uCE74\uD14C\uACE0\uB9AC ID"),
|
|
3608
|
+
assigneeIds: z8.array(z8.string()).optional().describe("\uB2F4\uB2F9\uC790 user ID \uBAA9\uB85D (\uBBF8\uC9C0\uC815 \uC2DC \uC790\uAE30 \uC790\uC2E0)"),
|
|
3609
|
+
userId: z8.string().optional().describe("\uC0DD\uC131\uC790 user ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
|
|
2986
3610
|
},
|
|
2987
3611
|
async ({ title, content, dueDate, categoryId, assigneeIds, userId }) => {
|
|
2988
3612
|
try {
|
|
@@ -3005,15 +3629,16 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3005
3629
|
}
|
|
3006
3630
|
}
|
|
3007
3631
|
);
|
|
3008
|
-
|
|
3632
|
+
defineTool(
|
|
3633
|
+
server,
|
|
3009
3634
|
"nworks_task_update",
|
|
3010
3635
|
"\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)",
|
|
3011
3636
|
{
|
|
3012
|
-
taskId:
|
|
3013
|
-
status:
|
|
3014
|
-
title:
|
|
3015
|
-
content:
|
|
3016
|
-
dueDate:
|
|
3637
|
+
taskId: z8.string().describe("\uD560 \uC77C ID (nworks_task_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
|
|
3638
|
+
status: z8.enum(["done", "todo"]).optional().describe("\uC0C1\uD0DC \uBCC0\uACBD: done(\uC644\uB8CC) \uB610\uB294 todo(\uBBF8\uC644\uB8CC)"),
|
|
3639
|
+
title: z8.string().optional().describe("\uC0C8 \uC81C\uBAA9"),
|
|
3640
|
+
content: z8.string().optional().describe("\uC0C8 \uB0B4\uC6A9"),
|
|
3641
|
+
dueDate: z8.string().optional().describe("\uC0C8 \uB9C8\uAC10\uC77C (YYYY-MM-DD)")
|
|
3017
3642
|
},
|
|
3018
3643
|
async ({ taskId, status, title, content, dueDate }) => {
|
|
3019
3644
|
try {
|
|
@@ -3047,11 +3672,12 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3047
3672
|
}
|
|
3048
3673
|
}
|
|
3049
3674
|
);
|
|
3050
|
-
|
|
3675
|
+
defineTool(
|
|
3676
|
+
server,
|
|
3051
3677
|
"nworks_task_delete",
|
|
3052
3678
|
"\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)",
|
|
3053
3679
|
{
|
|
3054
|
-
taskId:
|
|
3680
|
+
taskId: z8.string().describe("\uC0AD\uC81C\uD560 \uD560 \uC77C ID (nworks_task_list\uB85C \uC870\uD68C \uAC00\uB2A5)")
|
|
3055
3681
|
},
|
|
3056
3682
|
async ({ taskId }) => {
|
|
3057
3683
|
try {
|
|
@@ -3067,12 +3693,18 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3067
3693
|
}
|
|
3068
3694
|
}
|
|
3069
3695
|
);
|
|
3070
|
-
|
|
3696
|
+
}
|
|
3697
|
+
|
|
3698
|
+
// src/mcp/tools/board.ts
|
|
3699
|
+
import { z as z9 } from "zod";
|
|
3700
|
+
function registerBoardTools(server) {
|
|
3701
|
+
defineTool(
|
|
3702
|
+
server,
|
|
3071
3703
|
"nworks_board_list",
|
|
3072
3704
|
"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)",
|
|
3073
3705
|
{
|
|
3074
|
-
count:
|
|
3075
|
-
cursor:
|
|
3706
|
+
count: z9.number().int().min(1).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20)"),
|
|
3707
|
+
cursor: z9.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
|
|
3076
3708
|
},
|
|
3077
3709
|
async ({ count, cursor }) => {
|
|
3078
3710
|
try {
|
|
@@ -3093,13 +3725,14 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3093
3725
|
}
|
|
3094
3726
|
}
|
|
3095
3727
|
);
|
|
3096
|
-
|
|
3728
|
+
defineTool(
|
|
3729
|
+
server,
|
|
3097
3730
|
"nworks_board_posts",
|
|
3098
3731
|
"\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)",
|
|
3099
3732
|
{
|
|
3100
|
-
boardId:
|
|
3101
|
-
count:
|
|
3102
|
-
cursor:
|
|
3733
|
+
boardId: z9.string().describe("\uAC8C\uC2DC\uD310 ID (nworks_board_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
|
|
3734
|
+
count: z9.number().int().min(1).max(40).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 40)"),
|
|
3735
|
+
cursor: z9.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
|
|
3103
3736
|
},
|
|
3104
3737
|
async ({ boardId, count, cursor }) => {
|
|
3105
3738
|
try {
|
|
@@ -3123,12 +3756,13 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3123
3756
|
}
|
|
3124
3757
|
}
|
|
3125
3758
|
);
|
|
3126
|
-
|
|
3759
|
+
defineTool(
|
|
3760
|
+
server,
|
|
3127
3761
|
"nworks_board_read",
|
|
3128
3762
|
"\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)",
|
|
3129
3763
|
{
|
|
3130
|
-
boardId:
|
|
3131
|
-
postId:
|
|
3764
|
+
boardId: z9.string().describe("\uAC8C\uC2DC\uD310 ID (nworks_board_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
|
|
3765
|
+
postId: z9.string().describe("\uAE00 ID (nworks_board_posts\uB85C \uC870\uD68C \uAC00\uB2A5)")
|
|
3132
3766
|
},
|
|
3133
3767
|
async ({ boardId, postId }) => {
|
|
3134
3768
|
try {
|
|
@@ -3154,15 +3788,16 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3154
3788
|
}
|
|
3155
3789
|
}
|
|
3156
3790
|
);
|
|
3157
|
-
|
|
3791
|
+
defineTool(
|
|
3792
|
+
server,
|
|
3158
3793
|
"nworks_board_create",
|
|
3159
3794
|
"\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)",
|
|
3160
3795
|
{
|
|
3161
|
-
boardId:
|
|
3162
|
-
title:
|
|
3163
|
-
body:
|
|
3164
|
-
enableComment:
|
|
3165
|
-
sendNotifications:
|
|
3796
|
+
boardId: z9.string().describe("\uAC8C\uC2DC\uD310 ID (nworks_board_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
|
|
3797
|
+
title: z9.string().describe("\uAE00 \uC81C\uBAA9"),
|
|
3798
|
+
body: z9.string().optional().describe("\uAE00 \uBCF8\uBB38"),
|
|
3799
|
+
enableComment: z9.boolean().optional().describe("\uB313\uAE00 \uD5C8\uC6A9 (\uAE30\uBCF8: true)"),
|
|
3800
|
+
sendNotifications: z9.boolean().optional().describe("\uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)")
|
|
3166
3801
|
},
|
|
3167
3802
|
async ({ boardId, title, body, enableComment, sendNotifications }) => {
|
|
3168
3803
|
try {
|
|
@@ -3184,33 +3819,173 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3184
3819
|
}
|
|
3185
3820
|
}
|
|
3186
3821
|
);
|
|
3187
|
-
|
|
3822
|
+
}
|
|
3823
|
+
|
|
3824
|
+
// src/mcp/tools/contact.ts
|
|
3825
|
+
import { z as z10 } from "zod";
|
|
3826
|
+
function registerContactTools(server) {
|
|
3827
|
+
defineTool(
|
|
3828
|
+
server,
|
|
3829
|
+
"nworks_contact_list",
|
|
3830
|
+
"NAVER WORKS \uAC1C\uC778 \uC5F0\uB77D\uCC98 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. '\uC5F0\uB77D\uCC98 \uBAA9\uB85D \uBCF4\uC5EC\uC918', '\uB0B4 \uBA85\uD568\uCCA9 \uD655\uC778' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. User OAuth \uC778\uC99D \uD544\uC694 (contact.read scope)",
|
|
3831
|
+
{
|
|
3832
|
+
userId: z10.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)"),
|
|
3833
|
+
count: z10.number().int().min(1).max(500).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 500)"),
|
|
3834
|
+
cursor: z10.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C"),
|
|
3835
|
+
contactTagId: z10.string().optional().describe("\uC5F0\uB77D\uCC98 \uD0DC\uADF8 ID\uB85C \uD544\uD130\uB9C1 (nworks_contact_list_tags\uB85C \uC870\uD68C \uAC00\uB2A5)"),
|
|
3836
|
+
email: z10.string().optional().describe("\uC774\uBA54\uC77C\uB85C \uD544\uD130\uB9C1"),
|
|
3837
|
+
telephone: z10.string().optional().describe("\uC804\uD654\uBC88\uD638\uB85C \uD544\uD130\uB9C1")
|
|
3838
|
+
},
|
|
3839
|
+
async ({ userId, count, cursor, contactTagId, email, telephone }) => {
|
|
3840
|
+
try {
|
|
3841
|
+
const result = await listContacts({
|
|
3842
|
+
userId: userId ?? "me",
|
|
3843
|
+
count: count ?? 20,
|
|
3844
|
+
cursor,
|
|
3845
|
+
contactTagId,
|
|
3846
|
+
email,
|
|
3847
|
+
telephone
|
|
3848
|
+
});
|
|
3849
|
+
return {
|
|
3850
|
+
content: [{ type: "text", text: JSON.stringify({ contacts: result.contacts, count: result.contacts.length, hasMore: !!result.responseMetaData?.nextCursor, nextCursor: result.responseMetaData?.nextCursor ?? null }) }]
|
|
3851
|
+
};
|
|
3852
|
+
} catch (err) {
|
|
3853
|
+
return {
|
|
3854
|
+
content: [{ type: "text", text: mcpErrorHint(err, "contact.list") }],
|
|
3855
|
+
isError: true
|
|
3856
|
+
};
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
);
|
|
3860
|
+
defineTool(
|
|
3861
|
+
server,
|
|
3862
|
+
"nworks_contact_get",
|
|
3863
|
+
"\uD2B9\uC815 \uC5F0\uB77D\uCC98\uC758 \uC0C1\uC138 \uC815\uBCF4\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4. contactId\uB294 nworks_contact_list\uB85C \uC870\uD68C \uAC00\uB2A5. User OAuth \uC778\uC99D \uD544\uC694 (contact.read scope)",
|
|
3864
|
+
{
|
|
3865
|
+
contactId: z10.string().describe("\uC5F0\uB77D\uCC98 ID (nworks_contact_list\uB85C \uC870\uD68C \uAC00\uB2A5)")
|
|
3866
|
+
},
|
|
3867
|
+
async ({ contactId }) => {
|
|
3868
|
+
try {
|
|
3869
|
+
const result = await getContact(contactId);
|
|
3870
|
+
return {
|
|
3871
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
3872
|
+
};
|
|
3873
|
+
} catch (err) {
|
|
3874
|
+
return {
|
|
3875
|
+
content: [{ type: "text", text: mcpErrorHint(err, "contact.get") }],
|
|
3876
|
+
isError: true
|
|
3877
|
+
};
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
3880
|
+
);
|
|
3881
|
+
defineTool(
|
|
3882
|
+
server,
|
|
3883
|
+
"nworks_contact_create",
|
|
3884
|
+
`\uC0C8 \uC5F0\uB77D\uCC98\uB97C \uB9CC\uB4ED\uB2C8\uB2E4. '\uC5F0\uB77D\uCC98 \uCD94\uAC00\uD574\uC918' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. payload\uB294 NAVER WORKS \uC5F0\uB77D\uCC98 API \uC2A4\uD399\uC5D0 \uB9DE\uB294 JSON \uAC1D\uCCB4\uB97C \uADF8\uB300\uB85C \uC804\uB2EC\uD569\uB2C8\uB2E4. contactName\uACFC permission\uC740 \uD544\uC218\uC774\uBA70, permission.accessibleMembers\uC5D0\uB294 \uCD5C\uC18C 1\uBA85\uC774 \uC788\uC5B4\uC57C \uD569\uB2C8\uB2E4(\uBCF4\uD1B5 \uBCF8\uC778). \uC608: {"contactName":{"lastName":"\uAE40","firstName":"\uCCA0\uC218"},"emails":[{"email":"a@b.com","primary":true}],"permission":{"accessibleRange":"MEMBER","isCoEditing":false,"accessibleMembers":[{"id":"<userId>","type":"USER"}]}}. User OAuth \uC778\uC99D \uD544\uC694 (contact scope)`,
|
|
3885
|
+
{
|
|
3886
|
+
payload: z10.record(z10.string(), z10.unknown()).describe("\uC5F0\uB77D\uCC98 \uD544\uB4DC. \uD544\uC218: contactName, permission(accessibleRange\xB7isCoEditing\xB7accessibleMembers \uCD5C\uC18C 1\uBA85). \uC120\uD0DD: emails[], telephones[], organizations[], memo")
|
|
3887
|
+
},
|
|
3888
|
+
async ({ payload }) => {
|
|
3889
|
+
try {
|
|
3890
|
+
const result = await createContact(payload);
|
|
3891
|
+
return {
|
|
3892
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }]
|
|
3893
|
+
};
|
|
3894
|
+
} catch (err) {
|
|
3895
|
+
return {
|
|
3896
|
+
content: [{ type: "text", text: mcpErrorHint(err, "contact.create") }],
|
|
3897
|
+
isError: true
|
|
3898
|
+
};
|
|
3899
|
+
}
|
|
3900
|
+
}
|
|
3901
|
+
);
|
|
3902
|
+
defineTool(
|
|
3903
|
+
server,
|
|
3904
|
+
"nworks_contact_update",
|
|
3905
|
+
"\uAE30\uC874 \uC5F0\uB77D\uCC98\uB97C \uC218\uC815\uD569\uB2C8\uB2E4. contactId\uB294 nworks_contact_list\uB85C \uC870\uD68C \uAC00\uB2A5. payload\uB294 \uC218\uC815\uD560 \uD544\uB4DC\uB9CC \uB2F4\uC740 JSON \uAC1D\uCCB4\uC785\uB2C8\uB2E4. User OAuth \uC778\uC99D \uD544\uC694 (contact scope)",
|
|
3906
|
+
{
|
|
3907
|
+
contactId: z10.string().describe("\uC5F0\uB77D\uCC98 ID (nworks_contact_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
|
|
3908
|
+
payload: z10.record(z10.string(), z10.unknown()).describe("\uC218\uC815\uD560 \uD544\uB4DC (name, email, tel, company \uB4F1)"),
|
|
3909
|
+
replace: z10.boolean().optional().describe("\uC804\uCCB4 \uAD50\uCCB4(PUT) \uC5EC\uBD80 (\uAE30\uBCF8: false, \uBD80\uBD84 \uC218\uC815 PATCH)")
|
|
3910
|
+
},
|
|
3911
|
+
async ({ contactId, payload, replace }) => {
|
|
3912
|
+
try {
|
|
3913
|
+
const result = await updateContact(contactId, payload, !(replace ?? false));
|
|
3914
|
+
return {
|
|
3915
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }]
|
|
3916
|
+
};
|
|
3917
|
+
} catch (err) {
|
|
3918
|
+
return {
|
|
3919
|
+
content: [{ type: "text", text: mcpErrorHint(err, "contact.update") }],
|
|
3920
|
+
isError: true
|
|
3921
|
+
};
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
);
|
|
3925
|
+
defineTool(
|
|
3926
|
+
server,
|
|
3927
|
+
"nworks_contact_delete",
|
|
3928
|
+
"\uC5F0\uB77D\uCC98\uB97C \uC0AD\uC81C\uD569\uB2C8\uB2E4. contactId\uB294 nworks_contact_list\uB85C \uC870\uD68C \uAC00\uB2A5. User OAuth \uC778\uC99D \uD544\uC694 (contact scope)",
|
|
3929
|
+
{
|
|
3930
|
+
contactId: z10.string().describe("\uC0AD\uC81C\uD560 \uC5F0\uB77D\uCC98 ID (nworks_contact_list\uB85C \uC870\uD68C \uAC00\uB2A5)")
|
|
3931
|
+
},
|
|
3932
|
+
async ({ contactId }) => {
|
|
3933
|
+
try {
|
|
3934
|
+
await deleteContact(contactId);
|
|
3935
|
+
return {
|
|
3936
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, contactId, message: "\uC5F0\uB77D\uCC98\uAC00 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4" }) }]
|
|
3937
|
+
};
|
|
3938
|
+
} catch (err) {
|
|
3939
|
+
return {
|
|
3940
|
+
content: [{ type: "text", text: mcpErrorHint(err, "contact.delete") }],
|
|
3941
|
+
isError: true
|
|
3942
|
+
};
|
|
3943
|
+
}
|
|
3944
|
+
}
|
|
3945
|
+
);
|
|
3946
|
+
defineTool(
|
|
3947
|
+
server,
|
|
3948
|
+
"nworks_contact_list_tags",
|
|
3949
|
+
"\uC5F0\uB77D\uCC98 \uD0DC\uADF8(\uADF8\uB8F9) \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. '\uC5F0\uB77D\uCC98 \uD0DC\uADF8 \uBAA9\uB85D \uBCF4\uC5EC\uC918' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. User OAuth \uC778\uC99D \uD544\uC694 (contact.read scope)",
|
|
3950
|
+
{
|
|
3951
|
+
userId: z10.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)"),
|
|
3952
|
+
count: z10.number().int().min(1).max(500).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 500)"),
|
|
3953
|
+
cursor: z10.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
|
|
3954
|
+
},
|
|
3955
|
+
async ({ userId, count, cursor }) => {
|
|
3956
|
+
try {
|
|
3957
|
+
const result = await listContactTags({ userId: userId ?? "me", count: count ?? 20, cursor });
|
|
3958
|
+
return {
|
|
3959
|
+
content: [{ type: "text", text: JSON.stringify({ contactTags: result.contactTags, count: result.contactTags.length, hasMore: !!result.responseMetaData?.nextCursor, nextCursor: result.responseMetaData?.nextCursor ?? null }) }]
|
|
3960
|
+
};
|
|
3961
|
+
} catch (err) {
|
|
3962
|
+
return {
|
|
3963
|
+
content: [{ type: "text", text: mcpErrorHint(err, "contact.list_tags") }],
|
|
3964
|
+
isError: true
|
|
3965
|
+
};
|
|
3966
|
+
}
|
|
3967
|
+
}
|
|
3968
|
+
);
|
|
3969
|
+
}
|
|
3970
|
+
|
|
3971
|
+
// src/mcp/tools/auth.ts
|
|
3972
|
+
import { z as z11 } from "zod";
|
|
3973
|
+
function registerAuthTools(server) {
|
|
3974
|
+
defineTool(
|
|
3975
|
+
server,
|
|
3188
3976
|
"nworks_login_user",
|
|
3189
|
-
"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:
|
|
3977
|
+
"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: preset\uC744 \uC9C0\uC815\uD558\uC9C0 \uB9C8\uC138\uC694. \uAE30\uBCF8\uAC12(default=all)\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. \uC77D\uAE30 \uC804\uC6A9\uC73C\uB85C \uC81C\uD55C\uD558\uB824\uBA74 preset='readonly'\uB97C \uC9C0\uC815\uD558\uC138\uC694(\uC774 \uACBD\uC6B0 \uC0DD\uC131/\uC218\uC815/\uC0AD\uC81C \uB3C4\uAD6C\uB294 \uC7AC\uB85C\uADF8\uC778 \uD544\uC694).",
|
|
3190
3978
|
{
|
|
3191
|
-
|
|
3979
|
+
preset: z11.enum(["readonly", "default", "all"]).optional().describe("scope \uD504\uB9AC\uC14B. \uAE30\uBCF8 default(=all, \uC804\uCCB4 \uAE30\uB2A5). readonly\uB294 \uC77D\uAE30 \uC804\uC6A9."),
|
|
3980
|
+
scope: z11.string().optional().describe("\uACE0\uAE09: \uACF5\uBC31 \uAD6C\uBD84 raw scope \uC9C1\uC811 \uC9C0\uC815(preset \uB300\uC2E0). \uD2B9\uC218\uD55C \uACBD\uC6B0\uC5D0\uB9CC \uC0AC\uC6A9")
|
|
3192
3981
|
},
|
|
3193
|
-
async ({ scope }) => {
|
|
3194
|
-
const DEFAULT_SCOPE = "calendar calendar.read file file.read mail mail.read task task.read user.read board board.read";
|
|
3982
|
+
async ({ preset, scope }) => {
|
|
3195
3983
|
try {
|
|
3196
3984
|
const creds = await loadCredentials();
|
|
3197
|
-
const SCOPE_DEPS = {
|
|
3198
|
-
calendar: ["calendar.read"],
|
|
3199
|
-
task: ["user.read"],
|
|
3200
|
-
"task.read": ["user.read"]
|
|
3201
|
-
};
|
|
3202
|
-
const expandScopes = (scopes) => {
|
|
3203
|
-
const expanded = new Set(scopes);
|
|
3204
|
-
for (const s of scopes) {
|
|
3205
|
-
const deps = SCOPE_DEPS[s];
|
|
3206
|
-
if (deps) deps.forEach((d) => expanded.add(d));
|
|
3207
|
-
}
|
|
3208
|
-
return [...expanded];
|
|
3209
|
-
};
|
|
3210
3985
|
const existingToken = await loadUserToken();
|
|
3211
3986
|
const existingScopes = existingToken?.scope?.split(" ").filter(Boolean) ?? [];
|
|
3212
|
-
const requestedScopes =
|
|
3213
|
-
const mergedScopes =
|
|
3987
|
+
const requestedScopes = resolveScopes(preset ?? scope);
|
|
3988
|
+
const mergedScopes = mergeScopes(existingScopes, requestedScopes);
|
|
3214
3989
|
const state = generateSecureState();
|
|
3215
3990
|
const authorizeUrl = buildAuthorizeUrl(creds.clientId, mergedScopes, state);
|
|
3216
3991
|
startOAuthCallbackServer(creds.clientId, creds.clientSecret, state).then(
|
|
@@ -3247,7 +4022,8 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3247
4022
|
}
|
|
3248
4023
|
}
|
|
3249
4024
|
);
|
|
3250
|
-
|
|
4025
|
+
defineTool(
|
|
4026
|
+
server,
|
|
3251
4027
|
"nworks_whoami",
|
|
3252
4028
|
"\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",
|
|
3253
4029
|
{},
|
|
@@ -3258,10 +4034,10 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3258
4034
|
const userToken = await loadUserToken();
|
|
3259
4035
|
const isValid = token ? token.expiresAt > Date.now() / 1e3 : false;
|
|
3260
4036
|
const userTokenValid = userToken ? userToken.expiresAt > Date.now() / 1e3 : false;
|
|
3261
|
-
const
|
|
4037
|
+
const mask2 = (s) => s.length <= 4 ? "****" : `****${s.slice(-4)}`;
|
|
3262
4038
|
const info = {
|
|
3263
4039
|
serviceAccount: creds.serviceAccount ?? null,
|
|
3264
|
-
clientId:
|
|
4040
|
+
clientId: mask2(creds.clientId),
|
|
3265
4041
|
botId: creds.botId ?? null,
|
|
3266
4042
|
tokenValid: isValid,
|
|
3267
4043
|
userOAuth: userToken ? { valid: userTokenValid, scope: userToken.scope } : null
|
|
@@ -3277,7 +4053,8 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3277
4053
|
}
|
|
3278
4054
|
}
|
|
3279
4055
|
);
|
|
3280
|
-
|
|
4056
|
+
defineTool(
|
|
4057
|
+
server,
|
|
3281
4058
|
"nworks_logout",
|
|
3282
4059
|
"\uC800\uC7A5\uB41C NAVER WORKS \uC778\uC99D \uC815\uBCF4\uC640 \uD1A0\uD070\uC744 \uBAA8\uB450 \uC0AD\uC81C\uD569\uB2C8\uB2E4",
|
|
3283
4060
|
{},
|
|
@@ -3315,7 +4092,141 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3315
4092
|
}
|
|
3316
4093
|
}
|
|
3317
4094
|
);
|
|
3318
|
-
|
|
4095
|
+
}
|
|
4096
|
+
|
|
4097
|
+
// src/commands/doctor.ts
|
|
4098
|
+
import { Command as Command12 } from "commander";
|
|
4099
|
+
import { existsSync as existsSync3 } from "fs";
|
|
4100
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
4101
|
+
import { resolve as resolve2, join as join4, sep as sep2 } from "path";
|
|
4102
|
+
function mask(value) {
|
|
4103
|
+
return value.length <= 4 ? "****" : `****${value.slice(-4)}`;
|
|
4104
|
+
}
|
|
4105
|
+
async function runChecks(profile) {
|
|
4106
|
+
const results = [];
|
|
4107
|
+
let creds;
|
|
4108
|
+
try {
|
|
4109
|
+
creds = await loadCredentials(profile);
|
|
4110
|
+
results.push({ check: "credentials", status: "OK", detail: `clientId: ${mask(creds.clientId)}` });
|
|
4111
|
+
} catch {
|
|
4112
|
+
results.push({ check: "credentials", status: "FAIL", detail: "No credentials found. CLI: run `nworks login --user`. MCP: use the nworks_setup tool (env NWORKS_CLIENT_SECRET required)." });
|
|
4113
|
+
return results;
|
|
4114
|
+
}
|
|
4115
|
+
if (hasServiceAccountCreds(creds)) {
|
|
4116
|
+
results.push({ check: "serviceAccount", status: "OK", detail: creds.serviceAccount });
|
|
4117
|
+
} else {
|
|
4118
|
+
results.push({ check: "serviceAccount", status: "SKIP", detail: "Not set (required for bot messaging)" });
|
|
4119
|
+
}
|
|
4120
|
+
if (creds.privateKeyPath) {
|
|
4121
|
+
if (existsSync3(creds.privateKeyPath)) {
|
|
4122
|
+
try {
|
|
4123
|
+
await readFile5(creds.privateKeyPath, "utf-8");
|
|
4124
|
+
results.push({ check: "privateKey", status: "OK", detail: creds.privateKeyPath });
|
|
4125
|
+
} catch {
|
|
4126
|
+
results.push({ check: "privateKey", status: "FAIL", detail: `Cannot read: ${creds.privateKeyPath}` });
|
|
4127
|
+
}
|
|
4128
|
+
} else {
|
|
4129
|
+
results.push({ check: "privateKey", status: "FAIL", detail: `File not found: ${creds.privateKeyPath}` });
|
|
4130
|
+
}
|
|
4131
|
+
} else {
|
|
4132
|
+
results.push({ check: "privateKey", status: "SKIP", detail: "Not set" });
|
|
4133
|
+
}
|
|
4134
|
+
if (creds.privateKeyPath && existsSync3(creds.privateKeyPath)) {
|
|
4135
|
+
const resolvedKey = resolve2(creds.privateKeyPath);
|
|
4136
|
+
const cwd = resolve2(process.cwd());
|
|
4137
|
+
const insideCwd = resolvedKey === cwd || resolvedKey.startsWith(cwd + sep2);
|
|
4138
|
+
const looksLikeProject = existsSync3(join4(cwd, ".git")) || existsSync3(join4(cwd, "package.json"));
|
|
4139
|
+
if (insideCwd && looksLikeProject) {
|
|
4140
|
+
results.push({
|
|
4141
|
+
check: "keyLocation",
|
|
4142
|
+
status: "WARN",
|
|
4143
|
+
detail: "Private key is inside a project directory. Risk of exposure via editor plugins, AI tools, or `npm pack` \u2014 move it outside (e.g. your home directory) and update the path."
|
|
4144
|
+
});
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4147
|
+
if (creds.botId) {
|
|
4148
|
+
results.push({ check: "botId", status: "OK", detail: creds.botId });
|
|
4149
|
+
} else {
|
|
4150
|
+
results.push({ check: "botId", status: "SKIP", detail: "Not set (required to send messages)" });
|
|
4151
|
+
}
|
|
4152
|
+
const token = await loadToken(profile);
|
|
4153
|
+
if (token) {
|
|
4154
|
+
const valid = token.expiresAt > Date.now() / 1e3;
|
|
4155
|
+
results.push({
|
|
4156
|
+
check: "serviceToken",
|
|
4157
|
+
status: valid ? "OK" : "EXPIRED",
|
|
4158
|
+
detail: valid ? `expires: ${new Date(token.expiresAt * 1e3).toISOString()}` : `expired: ${new Date(token.expiresAt * 1e3).toISOString()}`
|
|
4159
|
+
});
|
|
4160
|
+
} else {
|
|
4161
|
+
results.push({ check: "serviceToken", status: "SKIP", detail: "No token" });
|
|
4162
|
+
}
|
|
4163
|
+
const userToken = await loadUserToken(profile);
|
|
4164
|
+
if (userToken) {
|
|
4165
|
+
const valid = userToken.expiresAt > Date.now() / 1e3;
|
|
4166
|
+
results.push({
|
|
4167
|
+
check: "userOAuth",
|
|
4168
|
+
status: valid ? "OK" : "EXPIRED",
|
|
4169
|
+
detail: valid ? `scope: ${userToken.scope} | expires: ${new Date(userToken.expiresAt * 1e3).toISOString()}` : `expired | scope: ${userToken.scope}`
|
|
4170
|
+
});
|
|
4171
|
+
} else {
|
|
4172
|
+
results.push({ check: "userOAuth", status: "SKIP", detail: "No token. Run `nworks login --user`." });
|
|
4173
|
+
}
|
|
4174
|
+
if (hasServiceAccountCreds(creds) && token && token.expiresAt > Date.now() / 1e3) {
|
|
4175
|
+
try {
|
|
4176
|
+
const res = await fetch("https://www.worksapis.com/v1.0/users/me", {
|
|
4177
|
+
headers: { Authorization: `Bearer ${token.accessToken}` }
|
|
4178
|
+
});
|
|
4179
|
+
if (res.ok) {
|
|
4180
|
+
results.push({ check: "apiConnection", status: "OK", detail: "Connected to NAVER WORKS API" });
|
|
4181
|
+
} else {
|
|
4182
|
+
results.push({ check: "apiConnection", status: "FAIL", detail: `HTTP ${res.status}` });
|
|
4183
|
+
}
|
|
4184
|
+
} catch (e) {
|
|
4185
|
+
results.push({ check: "apiConnection", status: "FAIL", detail: `Connection failed: ${e.message}` });
|
|
4186
|
+
}
|
|
4187
|
+
} else if (userToken && userToken.expiresAt > Date.now() / 1e3) {
|
|
4188
|
+
try {
|
|
4189
|
+
const res = await fetch("https://www.worksapis.com/v1.0/users/me", {
|
|
4190
|
+
headers: { Authorization: `Bearer ${userToken.accessToken}` }
|
|
4191
|
+
});
|
|
4192
|
+
if (res.ok) {
|
|
4193
|
+
results.push({ check: "apiConnection", status: "OK", detail: "Connected to NAVER WORKS API" });
|
|
4194
|
+
} else {
|
|
4195
|
+
results.push({ check: "apiConnection", status: "FAIL", detail: `HTTP ${res.status}` });
|
|
4196
|
+
}
|
|
4197
|
+
} catch (e) {
|
|
4198
|
+
results.push({ check: "apiConnection", status: "FAIL", detail: `Connection failed: ${e.message}` });
|
|
4199
|
+
}
|
|
4200
|
+
} else {
|
|
4201
|
+
results.push({ check: "apiConnection", status: "SKIP", detail: "No valid token \u2014 skipping API test" });
|
|
4202
|
+
}
|
|
4203
|
+
return results;
|
|
4204
|
+
}
|
|
4205
|
+
var doctorCommand = new Command12("doctor").description("Check nworks configuration and connectivity").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
|
|
4206
|
+
try {
|
|
4207
|
+
const results = await runChecks(opts.profile);
|
|
4208
|
+
if (opts.json || !process.stdout.isTTY) {
|
|
4209
|
+
output(results, opts);
|
|
4210
|
+
} else {
|
|
4211
|
+
console.log("\n nworks doctor\n");
|
|
4212
|
+
for (const r of results) {
|
|
4213
|
+
const icon = r.status === "OK" ? "\u2705" : r.status === "SKIP" ? "\u2796" : r.status === "WARN" ? "\u26A0\uFE0F" : "\u274C";
|
|
4214
|
+
console.log(` ${icon} ${r.check.padEnd(16)} ${r.detail}`);
|
|
4215
|
+
}
|
|
4216
|
+
console.log();
|
|
4217
|
+
}
|
|
4218
|
+
if (results.some((r) => r.status === "FAIL")) {
|
|
4219
|
+
process.exitCode = 1;
|
|
4220
|
+
}
|
|
4221
|
+
} catch (err) {
|
|
4222
|
+
cliError(err, opts);
|
|
4223
|
+
}
|
|
4224
|
+
});
|
|
4225
|
+
|
|
4226
|
+
// src/mcp/tools/diagnostics.ts
|
|
4227
|
+
function registerDiagnosticsTools(server) {
|
|
4228
|
+
defineTool(
|
|
4229
|
+
server,
|
|
3319
4230
|
"nworks_doctor",
|
|
3320
4231
|
"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.",
|
|
3321
4232
|
{},
|
|
@@ -3348,6 +4259,21 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3348
4259
|
);
|
|
3349
4260
|
}
|
|
3350
4261
|
|
|
4262
|
+
// src/mcp/tools.ts
|
|
4263
|
+
function registerTools(server) {
|
|
4264
|
+
registerSetupTools(server);
|
|
4265
|
+
registerMessageTools(server);
|
|
4266
|
+
registerDirectoryTools(server);
|
|
4267
|
+
registerCalendarTools(server);
|
|
4268
|
+
registerDriveTools(server);
|
|
4269
|
+
registerMailTools(server);
|
|
4270
|
+
registerTaskTools(server);
|
|
4271
|
+
registerBoardTools(server);
|
|
4272
|
+
registerContactTools(server);
|
|
4273
|
+
registerAuthTools(server);
|
|
4274
|
+
registerDiagnosticsTools(server);
|
|
4275
|
+
}
|
|
4276
|
+
|
|
3351
4277
|
// src/mcp/server.ts
|
|
3352
4278
|
async function startMcpServer() {
|
|
3353
4279
|
try {
|
|
@@ -3368,7 +4294,7 @@ async function startMcpServer() {
|
|
|
3368
4294
|
}
|
|
3369
4295
|
const server = new McpServer({
|
|
3370
4296
|
name: "nworks",
|
|
3371
|
-
version:
|
|
4297
|
+
version: VERSION
|
|
3372
4298
|
});
|
|
3373
4299
|
registerTools(server);
|
|
3374
4300
|
const transport = new StdioServerTransport();
|
|
@@ -3376,22 +4302,31 @@ async function startMcpServer() {
|
|
|
3376
4302
|
}
|
|
3377
4303
|
|
|
3378
4304
|
// src/commands/mcp-cmd.ts
|
|
3379
|
-
|
|
4305
|
+
function listRegisteredTools() {
|
|
4306
|
+
const server = new McpServer2({ name: "nworks", version: "0.0.0" });
|
|
4307
|
+
registerTools(server);
|
|
4308
|
+
const registered = server._registeredTools;
|
|
4309
|
+
return Object.entries(registered).map(([name, tool]) => ({
|
|
4310
|
+
name,
|
|
4311
|
+
summary: (tool.description ?? "").split("\n")[0]?.trim() ?? ""
|
|
4312
|
+
}));
|
|
4313
|
+
}
|
|
4314
|
+
var mcpCommand = new Command13("mcp").description("Start MCP server (stdio transport)").option("--list-tools", "List available MCP tools").action(async (opts) => {
|
|
3380
4315
|
if (opts.listTools) {
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
4316
|
+
const tools = listRegisteredTools();
|
|
4317
|
+
const width = tools.length ? Math.max(...tools.map((t) => t.name.length)) : 0;
|
|
4318
|
+
for (const { name, summary } of tools) {
|
|
4319
|
+
console.log(`${name.padEnd(width)} ${summary}`);
|
|
4320
|
+
}
|
|
4321
|
+
console.log(`
|
|
4322
|
+
${tools.length} tools`);
|
|
3386
4323
|
return;
|
|
3387
4324
|
}
|
|
3388
4325
|
await startMcpServer();
|
|
3389
4326
|
});
|
|
3390
4327
|
|
|
3391
4328
|
// src/index.ts
|
|
3392
|
-
var
|
|
3393
|
-
var { version } = require2("../package.json");
|
|
3394
|
-
var program = new Command13().name("nworks").description("NAVER WORKS CLI \u2014 built for humans and AI agents").version(version).option("--json", "Always output JSON").option("-v, --verbose", "Debug logging").option("--dry-run", "Print request without calling API").option("-p, --profile <name>", "Profile name", "default");
|
|
4329
|
+
var program = new Command14().name("nworks").description("NAVER WORKS CLI \u2014 built for humans and AI agents").version(VERSION);
|
|
3395
4330
|
program.addCommand(loginCommand);
|
|
3396
4331
|
program.addCommand(logoutCommand);
|
|
3397
4332
|
program.addCommand(whoamiCommand);
|
|
@@ -3402,6 +4337,7 @@ program.addCommand(driveCommand);
|
|
|
3402
4337
|
program.addCommand(mailCommand);
|
|
3403
4338
|
program.addCommand(taskCommand);
|
|
3404
4339
|
program.addCommand(boardCommand);
|
|
4340
|
+
program.addCommand(contactCommand);
|
|
3405
4341
|
program.addCommand(mcpCommand);
|
|
3406
4342
|
program.addCommand(doctorCommand);
|
|
3407
4343
|
program.parse();
|