nworks 1.2.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -190
- package/README.ja.md +459 -393
- package/README.ko.md +459 -393
- package/README.md +459 -393
- package/dist/index.js +1442 -508
- 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,31 @@ 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]);
|
|
535
647
|
} else {
|
|
536
|
-
|
|
648
|
+
execFile("xdg-open", [authorizeUrl]);
|
|
537
649
|
}
|
|
538
|
-
const tokenData = await startUserOAuthFlow(
|
|
650
|
+
const tokenData = await startUserOAuthFlow(mergedScope, profile, state);
|
|
539
651
|
await saveUserToken(tokenData, profile);
|
|
540
652
|
output(
|
|
541
653
|
{
|
|
@@ -562,7 +674,7 @@ async function handleServiceAccountLogin(opts) {
|
|
|
562
674
|
if (!botId) botId = process.env["NWORKS_BOT_ID"];
|
|
563
675
|
if (process.stdin.isTTY) {
|
|
564
676
|
if (!clientId) clientId = await prompt("Client ID: ");
|
|
565
|
-
if (!clientSecret) clientSecret = await
|
|
677
|
+
if (!clientSecret) clientSecret = await promptSecret("Client Secret: ");
|
|
566
678
|
if (!serviceAccount)
|
|
567
679
|
serviceAccount = await prompt("Service Account: ");
|
|
568
680
|
if (!privateKeyPath)
|
|
@@ -624,10 +736,10 @@ var REQUIRED_SCOPES = {
|
|
|
624
736
|
"board.create": "board"
|
|
625
737
|
};
|
|
626
738
|
var ERROR_HINTS_CLI = {
|
|
627
|
-
FORBIDDEN: "
|
|
628
|
-
ACCESS_DENIED: "
|
|
629
|
-
SERVICE_ACCOUNT_NOT_ALLOWED: "
|
|
630
|
-
UNAUTHORIZED: "
|
|
739
|
+
FORBIDDEN: "Insufficient permission. Check the OAuth scopes in the Developer Console.",
|
|
740
|
+
ACCESS_DENIED: "Access denied. Make sure the Bot is added in the Admin console.",
|
|
741
|
+
SERVICE_ACCOUNT_NOT_ALLOWED: "This API is not available with a Service Account. Log in with User OAuth via `nworks login --user`.",
|
|
742
|
+
UNAUTHORIZED: "Authentication expired. Log in again with `nworks login`."
|
|
631
743
|
};
|
|
632
744
|
var ERROR_HINTS_MCP = {
|
|
633
745
|
FORBIDDEN: "\uAD8C\uD55C\uC774 \uBD80\uC871\uD569\uB2C8\uB2E4. Developer Console\uC5D0\uC11C OAuth Scope\uB97C \uD655\uC778\uD558\uC138\uC694.",
|
|
@@ -675,7 +787,7 @@ function buildScopeHint(area, mode) {
|
|
|
675
787
|
const scopes = scope.split(" ").join(", ");
|
|
676
788
|
if (mode === "cli") {
|
|
677
789
|
return `
|
|
678
|
-
\u2192
|
|
790
|
+
\u2192 This command requires the ${scopes} scope(s). Run \`nworks login --user --scope "${scope}"\`.`;
|
|
679
791
|
}
|
|
680
792
|
return `
|
|
681
793
|
\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 +880,7 @@ import { Command as Command4 } from "commander";
|
|
|
768
880
|
var BASE_URL = "https://www.worksapis.com/v1.0";
|
|
769
881
|
var MAX_RETRIES = 3;
|
|
770
882
|
function sleep(ms) {
|
|
771
|
-
return new Promise((
|
|
883
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
772
884
|
}
|
|
773
885
|
async function request(opts, _retryCount = 0) {
|
|
774
886
|
const { method, path, body, profile = "default" } = opts;
|
|
@@ -818,8 +930,8 @@ async function request(opts, _retryCount = 0) {
|
|
|
818
930
|
}
|
|
819
931
|
|
|
820
932
|
// src/utils/sanitize.ts
|
|
821
|
-
import {
|
|
822
|
-
import { randomBytes as
|
|
933
|
+
import { resolve, sep } from "path";
|
|
934
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
823
935
|
function sanitizePathSegment(value) {
|
|
824
936
|
if (!value || typeof value !== "string") {
|
|
825
937
|
throw new Error("Path segment must be a non-empty string");
|
|
@@ -834,8 +946,9 @@ function sanitizeFileName(name) {
|
|
|
834
946
|
if (!name || typeof name !== "string") {
|
|
835
947
|
throw new Error("File name must be a non-empty string");
|
|
836
948
|
}
|
|
837
|
-
const
|
|
838
|
-
|
|
949
|
+
const segments = name.split(/[/\\]/).filter((s) => s.length > 0);
|
|
950
|
+
const base = segments[segments.length - 1] ?? "";
|
|
951
|
+
return base.replace(/[\r\n"]/g, "_");
|
|
839
952
|
}
|
|
840
953
|
function validateLocalPath(filePath, allowedBase) {
|
|
841
954
|
const resolved = resolve(filePath);
|
|
@@ -866,7 +979,7 @@ function validateRedirectUrl(location, allowedHosts) {
|
|
|
866
979
|
return location;
|
|
867
980
|
}
|
|
868
981
|
function generateSecureState() {
|
|
869
|
-
return
|
|
982
|
+
return randomBytes2(32).toString("hex");
|
|
870
983
|
}
|
|
871
984
|
|
|
872
985
|
// src/api/message.ts
|
|
@@ -988,20 +1101,25 @@ var messageCommand = new Command4("message").description("Bot message operations
|
|
|
988
1101
|
import { Command as Command5 } from "commander";
|
|
989
1102
|
|
|
990
1103
|
// src/api/directory.ts
|
|
991
|
-
async function listUsers(profile = "default") {
|
|
1104
|
+
async function listUsers(count = 100, cursor, profile = "default") {
|
|
1105
|
+
const params = new URLSearchParams();
|
|
1106
|
+
params.set("count", String(count));
|
|
1107
|
+
if (cursor) params.set("cursor", cursor);
|
|
992
1108
|
const result = await request({
|
|
993
1109
|
method: "GET",
|
|
994
|
-
path:
|
|
1110
|
+
path: `/users?${params.toString()}`,
|
|
995
1111
|
profile
|
|
996
1112
|
});
|
|
997
1113
|
return { users: result.users ?? [], responseMetaData: result.responseMetaData };
|
|
998
1114
|
}
|
|
999
1115
|
|
|
1000
1116
|
// 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) => {
|
|
1117
|
+
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
1118
|
try {
|
|
1003
|
-
const
|
|
1119
|
+
const count = opts.count ? Number(opts.count) : 100;
|
|
1120
|
+
const result = await listUsers(count, opts.cursor, opts.profile);
|
|
1004
1121
|
const formatted = {
|
|
1122
|
+
nextCursor: result.responseMetaData?.nextCursor ?? null,
|
|
1005
1123
|
users: result.users.map((u) => ({
|
|
1006
1124
|
userId: u.userId,
|
|
1007
1125
|
userName: [u.userName?.lastName, u.userName?.firstName].filter(Boolean).join(" ") || "",
|
|
@@ -1024,6 +1142,7 @@ import { Command as Command6 } from "commander";
|
|
|
1024
1142
|
import { randomUUID } from "crypto";
|
|
1025
1143
|
|
|
1026
1144
|
// src/auth/token-user.ts
|
|
1145
|
+
var inFlightRefresh = /* @__PURE__ */ new Map();
|
|
1027
1146
|
async function getValidUserToken(profile = "default") {
|
|
1028
1147
|
const cached = await loadUserToken(profile);
|
|
1029
1148
|
if (!cached) {
|
|
@@ -1034,22 +1153,106 @@ async function getValidUserToken(profile = "default") {
|
|
|
1034
1153
|
if (cached.expiresAt > Date.now() / 1e3 + 300) {
|
|
1035
1154
|
return cached.accessToken;
|
|
1036
1155
|
}
|
|
1156
|
+
return refreshValidUserToken(profile);
|
|
1157
|
+
}
|
|
1158
|
+
function refreshValidUserToken(profile = "default") {
|
|
1159
|
+
const existing = inFlightRefresh.get(profile);
|
|
1160
|
+
if (existing) return existing;
|
|
1161
|
+
const run = doRefresh(profile);
|
|
1162
|
+
inFlightRefresh.set(profile, run);
|
|
1163
|
+
void run.finally(() => inFlightRefresh.delete(profile)).catch(() => {
|
|
1164
|
+
});
|
|
1165
|
+
return run;
|
|
1166
|
+
}
|
|
1167
|
+
async function doRefresh(profile) {
|
|
1168
|
+
const cached = await loadUserToken(profile);
|
|
1169
|
+
if (!cached) {
|
|
1170
|
+
throw new AuthError(
|
|
1171
|
+
"User OAuth token not found. Run `nworks login --user` first."
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
1037
1174
|
const refreshed = await refreshUserToken(cached.refreshToken, profile);
|
|
1038
1175
|
await saveUserToken(refreshed, profile);
|
|
1039
1176
|
return refreshed.accessToken;
|
|
1040
1177
|
}
|
|
1041
1178
|
|
|
1042
|
-
// src/api/
|
|
1043
|
-
var
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1179
|
+
// src/api/user-client.ts
|
|
1180
|
+
var MAX_RETRIES2 = 3;
|
|
1181
|
+
var MAX_RETRY_AFTER_SECONDS = 60;
|
|
1182
|
+
var MAX_CONCURRENT_PER_HOST = 5;
|
|
1183
|
+
function sleep2(ms) {
|
|
1184
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
1185
|
+
}
|
|
1186
|
+
var HostSemaphore = class {
|
|
1187
|
+
constructor(max) {
|
|
1188
|
+
this.max = max;
|
|
1189
|
+
}
|
|
1190
|
+
active = 0;
|
|
1191
|
+
waiters = [];
|
|
1192
|
+
async acquire() {
|
|
1193
|
+
if (this.active < this.max) {
|
|
1194
|
+
this.active++;
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
await new Promise((resolve3) => this.waiters.push(resolve3));
|
|
1198
|
+
}
|
|
1199
|
+
release() {
|
|
1200
|
+
const next = this.waiters.shift();
|
|
1201
|
+
if (next) {
|
|
1202
|
+
next();
|
|
1203
|
+
} else {
|
|
1204
|
+
this.active--;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
var semaphores = /* @__PURE__ */ new Map();
|
|
1209
|
+
function semaphoreFor(host) {
|
|
1210
|
+
let semaphore = semaphores.get(host);
|
|
1211
|
+
if (!semaphore) {
|
|
1212
|
+
semaphore = new HostSemaphore(MAX_CONCURRENT_PER_HOST);
|
|
1213
|
+
semaphores.set(host, semaphore);
|
|
1214
|
+
}
|
|
1215
|
+
return semaphore;
|
|
1216
|
+
}
|
|
1217
|
+
async function userFetch(url, init, profile) {
|
|
1218
|
+
let refreshedOn401 = false;
|
|
1219
|
+
for (let attempt = 0; ; attempt++) {
|
|
1220
|
+
const res = await fetchOnce(url, init, profile);
|
|
1221
|
+
if (res.status === 401 && !refreshedOn401) {
|
|
1222
|
+
refreshedOn401 = true;
|
|
1223
|
+
await refreshValidUserToken(profile);
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
if (res.status === 429 && attempt < MAX_RETRIES2) {
|
|
1227
|
+
await sleep2(retryAfterMs(res));
|
|
1228
|
+
continue;
|
|
1229
|
+
}
|
|
1230
|
+
return res;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
async function fetchOnce(url, init, profile) {
|
|
1234
|
+
const host = new URL(url).host;
|
|
1235
|
+
const semaphore = semaphoreFor(host);
|
|
1236
|
+
await semaphore.acquire();
|
|
1237
|
+
try {
|
|
1238
|
+
const token = await getValidUserToken(profile);
|
|
1239
|
+
const headers = new Headers(init.headers);
|
|
1240
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
1241
|
+
return await fetch(url, { ...init, headers });
|
|
1242
|
+
} finally {
|
|
1243
|
+
semaphore.release();
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
function retryAfterMs(res) {
|
|
1247
|
+
const raw = parseInt(res.headers.get("Retry-After") ?? "5", 10);
|
|
1248
|
+
const seconds = Math.min(Number.isNaN(raw) ? 5 : raw, MAX_RETRY_AFTER_SECONDS);
|
|
1249
|
+
return seconds * 1e3;
|
|
1049
1250
|
}
|
|
1050
|
-
async function
|
|
1251
|
+
async function handleUserApiError(res) {
|
|
1051
1252
|
if (res.status === 401) {
|
|
1052
|
-
throw new AuthError(
|
|
1253
|
+
throw new AuthError(
|
|
1254
|
+
"User token expired or revoked. Run `nworks login --user` again."
|
|
1255
|
+
);
|
|
1053
1256
|
}
|
|
1054
1257
|
let code = "UNKNOWN";
|
|
1055
1258
|
let description = `HTTP ${res.status}`;
|
|
@@ -1061,6 +1264,9 @@ async function handleError(res) {
|
|
|
1061
1264
|
}
|
|
1062
1265
|
throw new ApiError(code, description, res.status);
|
|
1063
1266
|
}
|
|
1267
|
+
|
|
1268
|
+
// src/api/calendar.ts
|
|
1269
|
+
var BASE_URL2 = "https://www.worksapis.com/v1.0";
|
|
1064
1270
|
function generateEventId() {
|
|
1065
1271
|
return `event-${randomUUID()}`;
|
|
1066
1272
|
}
|
|
@@ -1078,8 +1284,8 @@ async function listEvents(fromDateTime, untilDateTime, userId = "me", profile =
|
|
|
1078
1284
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1079
1285
|
console.error(`[nworks] GET ${url}`);
|
|
1080
1286
|
}
|
|
1081
|
-
const res = await
|
|
1082
|
-
if (!res.ok) return
|
|
1287
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
1288
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1083
1289
|
const data = await res.json();
|
|
1084
1290
|
return { events: data.events ?? [] };
|
|
1085
1291
|
}
|
|
@@ -1116,7 +1322,7 @@ async function createEvent(opts) {
|
|
|
1116
1322
|
console.error(`[nworks] POST ${url}`);
|
|
1117
1323
|
console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
|
|
1118
1324
|
}
|
|
1119
|
-
const res = await
|
|
1325
|
+
const res = await userFetch(
|
|
1120
1326
|
url,
|
|
1121
1327
|
{
|
|
1122
1328
|
method: "POST",
|
|
@@ -1128,7 +1334,7 @@ async function createEvent(opts) {
|
|
|
1128
1334
|
if (res.status === 201) {
|
|
1129
1335
|
return await res.json();
|
|
1130
1336
|
}
|
|
1131
|
-
if (!res.ok) return
|
|
1337
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1132
1338
|
return await res.json();
|
|
1133
1339
|
}
|
|
1134
1340
|
async function getEvent(eventId, userId = "me", profile = "default") {
|
|
@@ -1136,8 +1342,8 @@ async function getEvent(eventId, userId = "me", profile = "default") {
|
|
|
1136
1342
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1137
1343
|
console.error(`[nworks] GET ${url}`);
|
|
1138
1344
|
}
|
|
1139
|
-
const res = await
|
|
1140
|
-
if (!res.ok) return
|
|
1345
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
1346
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1141
1347
|
const data = await res.json();
|
|
1142
1348
|
const event = data.eventComponents[0];
|
|
1143
1349
|
if (!event) throw new ApiError("NOT_FOUND", "Event not found", 404);
|
|
@@ -1180,7 +1386,7 @@ async function updateEvent(opts) {
|
|
|
1180
1386
|
console.error(`[nworks] PUT ${url}`);
|
|
1181
1387
|
console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
|
|
1182
1388
|
}
|
|
1183
|
-
const res = await
|
|
1389
|
+
const res = await userFetch(
|
|
1184
1390
|
url,
|
|
1185
1391
|
{
|
|
1186
1392
|
method: "PUT",
|
|
@@ -1189,7 +1395,7 @@ async function updateEvent(opts) {
|
|
|
1189
1395
|
},
|
|
1190
1396
|
profile
|
|
1191
1397
|
);
|
|
1192
|
-
if (!res.ok) return
|
|
1398
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1193
1399
|
}
|
|
1194
1400
|
async function deleteEvent(eventId, userId = "me", sendNotification = false, profile = "default") {
|
|
1195
1401
|
const params = new URLSearchParams();
|
|
@@ -1198,9 +1404,9 @@ async function deleteEvent(eventId, userId = "me", sendNotification = false, pro
|
|
|
1198
1404
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1199
1405
|
console.error(`[nworks] DELETE ${url}`);
|
|
1200
1406
|
}
|
|
1201
|
-
const res = await
|
|
1407
|
+
const res = await userFetch(url, { method: "DELETE" }, profile);
|
|
1202
1408
|
if (res.status === 204) return;
|
|
1203
|
-
if (!res.ok) return
|
|
1409
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1204
1410
|
}
|
|
1205
1411
|
|
|
1206
1412
|
// src/commands/calendar.ts
|
|
@@ -1321,34 +1527,15 @@ import { Command as Command7 } from "commander";
|
|
|
1321
1527
|
|
|
1322
1528
|
// src/api/drive.ts
|
|
1323
1529
|
import { readFile as readFile4, stat as stat2 } from "fs/promises";
|
|
1324
|
-
import { basename
|
|
1530
|
+
import { basename } from "path";
|
|
1325
1531
|
var BASE_URL3 = "https://www.worksapis.com/v1.0";
|
|
1326
1532
|
var MAX_UPLOAD_SIZE = 100 * 1024 * 1024;
|
|
1327
1533
|
var ALLOWED_HOSTS = [
|
|
1328
1534
|
"storage.worksmobile.com",
|
|
1535
|
+
"apis-storage.worksmobile.com",
|
|
1329
1536
|
"www.worksapis.com",
|
|
1330
1537
|
"worksapis.com"
|
|
1331
1538
|
];
|
|
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
1539
|
async function listFiles(userId = "me", folderId, count = 20, cursor, profile = "default") {
|
|
1353
1540
|
const base = `${BASE_URL3}/users/${sanitizePathSegment(userId)}/drive/files`;
|
|
1354
1541
|
const path = folderId ? `${base}/${sanitizePathSegment(folderId)}/children` : base;
|
|
@@ -1359,13 +1546,13 @@ async function listFiles(userId = "me", folderId, count = 20, cursor, profile =
|
|
|
1359
1546
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1360
1547
|
console.error(`[nworks] GET ${url}`);
|
|
1361
1548
|
}
|
|
1362
|
-
const res = await
|
|
1363
|
-
if (!res.ok) return
|
|
1549
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
1550
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1364
1551
|
const data = await res.json();
|
|
1365
1552
|
return { files: data.files ?? [], responseMetaData: data.responseMetaData };
|
|
1366
1553
|
}
|
|
1367
1554
|
async function uploadFile(localPath, userId = "me", folderId, overwrite = false, profile = "default") {
|
|
1368
|
-
const fileName =
|
|
1555
|
+
const fileName = basename(localPath);
|
|
1369
1556
|
const safeName = sanitizeFileName(fileName);
|
|
1370
1557
|
const fileStat = await stat2(localPath);
|
|
1371
1558
|
const fileSize = fileStat.size;
|
|
@@ -1377,7 +1564,7 @@ async function uploadFile(localPath, userId = "me", folderId, overwrite = false,
|
|
|
1377
1564
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1378
1565
|
console.error(`[nworks] POST ${createUrl} (create upload URL)`);
|
|
1379
1566
|
}
|
|
1380
|
-
const createRes = await
|
|
1567
|
+
const createRes = await userFetch(
|
|
1381
1568
|
createUrl,
|
|
1382
1569
|
{
|
|
1383
1570
|
method: "POST",
|
|
@@ -1386,7 +1573,7 @@ async function uploadFile(localPath, userId = "me", folderId, overwrite = false,
|
|
|
1386
1573
|
},
|
|
1387
1574
|
profile
|
|
1388
1575
|
);
|
|
1389
|
-
if (!createRes.ok) return
|
|
1576
|
+
if (!createRes.ok) return handleUserApiError(createRes);
|
|
1390
1577
|
const { uploadUrl } = await createRes.json();
|
|
1391
1578
|
validateRedirectUrl(uploadUrl, ALLOWED_HOSTS);
|
|
1392
1579
|
const fileBuffer = await readFile4(localPath);
|
|
@@ -1405,7 +1592,7 @@ Content-Type: application/octet-stream\r
|
|
|
1405
1592
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1406
1593
|
console.error(`[nworks] POST ${uploadUrl} (upload content, ${fileSize} bytes)`);
|
|
1407
1594
|
}
|
|
1408
|
-
const uploadRes = await
|
|
1595
|
+
const uploadRes = await userFetch(
|
|
1409
1596
|
uploadUrl,
|
|
1410
1597
|
{
|
|
1411
1598
|
method: "POST",
|
|
@@ -1414,7 +1601,7 @@ Content-Type: application/octet-stream\r
|
|
|
1414
1601
|
},
|
|
1415
1602
|
profile
|
|
1416
1603
|
);
|
|
1417
|
-
if (!uploadRes.ok) return
|
|
1604
|
+
if (!uploadRes.ok) return handleUserApiError(uploadRes);
|
|
1418
1605
|
return await uploadRes.json();
|
|
1419
1606
|
}
|
|
1420
1607
|
async function uploadBuffer(fileBuffer, fileName, userId = "me", folderId, overwrite = false, profile = "default") {
|
|
@@ -1427,7 +1614,7 @@ async function uploadBuffer(fileBuffer, fileName, userId = "me", folderId, overw
|
|
|
1427
1614
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1428
1615
|
console.error(`[nworks] POST ${createUrl} (create upload URL for buffer)`);
|
|
1429
1616
|
}
|
|
1430
|
-
const createRes = await
|
|
1617
|
+
const createRes = await userFetch(
|
|
1431
1618
|
createUrl,
|
|
1432
1619
|
{
|
|
1433
1620
|
method: "POST",
|
|
@@ -1436,7 +1623,7 @@ async function uploadBuffer(fileBuffer, fileName, userId = "me", folderId, overw
|
|
|
1436
1623
|
},
|
|
1437
1624
|
profile
|
|
1438
1625
|
);
|
|
1439
|
-
if (!createRes.ok) return
|
|
1626
|
+
if (!createRes.ok) return handleUserApiError(createRes);
|
|
1440
1627
|
const { uploadUrl } = await createRes.json();
|
|
1441
1628
|
validateRedirectUrl(uploadUrl, ALLOWED_HOSTS);
|
|
1442
1629
|
const boundary = `----nworks${Date.now()}`;
|
|
@@ -1454,7 +1641,7 @@ Content-Type: application/octet-stream\r
|
|
|
1454
1641
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1455
1642
|
console.error(`[nworks] POST ${uploadUrl} (upload buffer, ${fileSize} bytes)`);
|
|
1456
1643
|
}
|
|
1457
|
-
const uploadRes = await
|
|
1644
|
+
const uploadRes = await userFetch(
|
|
1458
1645
|
uploadUrl,
|
|
1459
1646
|
{
|
|
1460
1647
|
method: "POST",
|
|
@@ -1463,33 +1650,29 @@ Content-Type: application/octet-stream\r
|
|
|
1463
1650
|
},
|
|
1464
1651
|
profile
|
|
1465
1652
|
);
|
|
1466
|
-
if (!uploadRes.ok) return
|
|
1653
|
+
if (!uploadRes.ok) return handleUserApiError(uploadRes);
|
|
1467
1654
|
return await uploadRes.json();
|
|
1468
1655
|
}
|
|
1469
|
-
async function
|
|
1470
|
-
const url = `${BASE_URL3}/users/${sanitizePathSegment(userId)}/drive/files/${sanitizePathSegment(fileId)}/download`;
|
|
1656
|
+
async function downloadViaRedirect(url, profile) {
|
|
1471
1657
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1472
1658
|
console.error(`[nworks] GET ${url} (get download URL)`);
|
|
1473
1659
|
}
|
|
1474
|
-
const redirectRes = await
|
|
1660
|
+
const redirectRes = await userFetch(
|
|
1475
1661
|
url,
|
|
1476
1662
|
{ method: "GET", redirect: "manual" },
|
|
1477
1663
|
profile
|
|
1478
1664
|
);
|
|
1479
|
-
if (redirectRes.status === 401) {
|
|
1480
|
-
throw new AuthError("User token expired. Run `nworks login --user --scope file` again.");
|
|
1481
|
-
}
|
|
1482
1665
|
const location = redirectRes.headers.get("location");
|
|
1483
1666
|
if (!location) {
|
|
1484
|
-
if (!redirectRes.ok) return
|
|
1667
|
+
if (!redirectRes.ok) return handleUserApiError(redirectRes);
|
|
1485
1668
|
throw new ApiError("NO_REDIRECT", "No download URL returned", redirectRes.status);
|
|
1486
1669
|
}
|
|
1487
1670
|
const safeLocation = validateRedirectUrl(location, ALLOWED_HOSTS);
|
|
1488
1671
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1489
1672
|
console.error(`[nworks] GET ${safeLocation} (download content)`);
|
|
1490
1673
|
}
|
|
1491
|
-
const downloadRes = await
|
|
1492
|
-
if (!downloadRes.ok) return
|
|
1674
|
+
const downloadRes = await userFetch(safeLocation, { method: "GET" }, profile);
|
|
1675
|
+
if (!downloadRes.ok) return handleUserApiError(downloadRes);
|
|
1493
1676
|
const arrayBuffer = await downloadRes.arrayBuffer();
|
|
1494
1677
|
const buffer = Buffer.from(arrayBuffer);
|
|
1495
1678
|
const disposition = downloadRes.headers.get("content-disposition");
|
|
@@ -1502,6 +1685,42 @@ async function downloadFile(fileId, userId = "me", profile = "default") {
|
|
|
1502
1685
|
}
|
|
1503
1686
|
return { buffer, fileName };
|
|
1504
1687
|
}
|
|
1688
|
+
async function downloadFile(fileId, userId = "me", profile = "default") {
|
|
1689
|
+
const url = `${BASE_URL3}/users/${sanitizePathSegment(userId)}/drive/files/${sanitizePathSegment(fileId)}/download`;
|
|
1690
|
+
return downloadViaRedirect(url, profile);
|
|
1691
|
+
}
|
|
1692
|
+
async function listSharedDrives(count = 20, cursor, profile = "default") {
|
|
1693
|
+
const params = new URLSearchParams();
|
|
1694
|
+
params.set("count", String(count));
|
|
1695
|
+
if (cursor) params.set("cursor", cursor);
|
|
1696
|
+
const url = `${BASE_URL3}/sharedrives?${params.toString()}`;
|
|
1697
|
+
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1698
|
+
console.error(`[nworks] GET ${url}`);
|
|
1699
|
+
}
|
|
1700
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
1701
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1702
|
+
const data = await res.json();
|
|
1703
|
+
return { sharedDrives: data.sharedrives ?? [], responseMetaData: data.responseMetaData };
|
|
1704
|
+
}
|
|
1705
|
+
async function listSharedDriveFiles(sharedriveId, folderId, count = 20, cursor, profile = "default") {
|
|
1706
|
+
const base = `${BASE_URL3}/sharedrives/${sanitizePathSegment(sharedriveId)}/files`;
|
|
1707
|
+
const path = folderId ? `${base}/${sanitizePathSegment(folderId)}/children` : base;
|
|
1708
|
+
const params = new URLSearchParams();
|
|
1709
|
+
params.set("count", String(count));
|
|
1710
|
+
if (cursor) params.set("cursor", cursor);
|
|
1711
|
+
const url = `${path}?${params.toString()}`;
|
|
1712
|
+
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1713
|
+
console.error(`[nworks] GET ${url}`);
|
|
1714
|
+
}
|
|
1715
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
1716
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1717
|
+
const data = await res.json();
|
|
1718
|
+
return { files: data.files ?? [], responseMetaData: data.responseMetaData };
|
|
1719
|
+
}
|
|
1720
|
+
async function downloadSharedDriveFile(sharedriveId, fileId, profile = "default") {
|
|
1721
|
+
const url = `${BASE_URL3}/sharedrives/${sanitizePathSegment(sharedriveId)}/files/${sanitizePathSegment(fileId)}/download`;
|
|
1722
|
+
return downloadViaRedirect(url, profile);
|
|
1723
|
+
}
|
|
1505
1724
|
|
|
1506
1725
|
// src/commands/drive.ts
|
|
1507
1726
|
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 +1814,86 @@ var downloadCommand = new Command7("download").description("Download a file from
|
|
|
1595
1814
|
cliError(err, opts, "drive");
|
|
1596
1815
|
}
|
|
1597
1816
|
});
|
|
1598
|
-
var
|
|
1817
|
+
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) => {
|
|
1818
|
+
try {
|
|
1819
|
+
const result = await listSharedDrives(
|
|
1820
|
+
parseInt(opts.count, 10),
|
|
1821
|
+
opts.cursor,
|
|
1822
|
+
opts.profile
|
|
1823
|
+
);
|
|
1824
|
+
output(
|
|
1825
|
+
{
|
|
1826
|
+
sharedDrives: result.sharedDrives,
|
|
1827
|
+
count: result.sharedDrives.length,
|
|
1828
|
+
nextCursor: result.responseMetaData?.nextCursor ?? null
|
|
1829
|
+
},
|
|
1830
|
+
opts
|
|
1831
|
+
);
|
|
1832
|
+
} catch (err) {
|
|
1833
|
+
cliError(err, opts, "drive");
|
|
1834
|
+
}
|
|
1835
|
+
});
|
|
1836
|
+
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) => {
|
|
1837
|
+
try {
|
|
1838
|
+
const result = await listSharedDriveFiles(
|
|
1839
|
+
opts.sharedrive,
|
|
1840
|
+
opts.folder,
|
|
1841
|
+
parseInt(opts.count, 10),
|
|
1842
|
+
opts.cursor,
|
|
1843
|
+
opts.profile
|
|
1844
|
+
);
|
|
1845
|
+
const files = result.files.map((f) => ({
|
|
1846
|
+
name: f.fileName,
|
|
1847
|
+
type: f.fileType,
|
|
1848
|
+
size: f.fileSize,
|
|
1849
|
+
modified: f.modifiedTime,
|
|
1850
|
+
fileId: f.fileId
|
|
1851
|
+
}));
|
|
1852
|
+
output(
|
|
1853
|
+
{
|
|
1854
|
+
files,
|
|
1855
|
+
count: files.length,
|
|
1856
|
+
nextCursor: result.responseMetaData?.nextCursor ?? null
|
|
1857
|
+
},
|
|
1858
|
+
opts
|
|
1859
|
+
);
|
|
1860
|
+
} catch (err) {
|
|
1861
|
+
cliError(err, opts, "drive");
|
|
1862
|
+
}
|
|
1863
|
+
});
|
|
1864
|
+
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) => {
|
|
1865
|
+
try {
|
|
1866
|
+
const result = await downloadSharedDriveFile(
|
|
1867
|
+
opts.sharedrive,
|
|
1868
|
+
opts.fileId,
|
|
1869
|
+
opts.profile
|
|
1870
|
+
);
|
|
1871
|
+
const fileName = opts.name ?? result.fileName ?? opts.fileId;
|
|
1872
|
+
const outDir = opts.out ?? process.cwd();
|
|
1873
|
+
const outPath = join2(outDir, fileName);
|
|
1874
|
+
await writeFile2(outPath, result.buffer);
|
|
1875
|
+
output(
|
|
1876
|
+
{
|
|
1877
|
+
success: true,
|
|
1878
|
+
fileName,
|
|
1879
|
+
path: outPath,
|
|
1880
|
+
size: result.buffer.length
|
|
1881
|
+
},
|
|
1882
|
+
opts
|
|
1883
|
+
);
|
|
1884
|
+
} catch (err) {
|
|
1885
|
+
cliError(err, opts, "drive");
|
|
1886
|
+
}
|
|
1887
|
+
});
|
|
1888
|
+
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
1889
|
|
|
1600
1890
|
// src/commands/mail.ts
|
|
1891
|
+
import { writeFile as writeFile3 } from "fs/promises";
|
|
1892
|
+
import { join as join3 } from "path";
|
|
1601
1893
|
import { Command as Command8 } from "commander";
|
|
1602
1894
|
|
|
1603
1895
|
// src/api/mail.ts
|
|
1604
1896
|
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
1897
|
async function sendMail(opts) {
|
|
1626
1898
|
const userId = opts.userId ?? "me";
|
|
1627
1899
|
const profile = opts.profile ?? "default";
|
|
@@ -1637,7 +1909,7 @@ async function sendMail(opts) {
|
|
|
1637
1909
|
if (opts.cc) body.cc = opts.cc;
|
|
1638
1910
|
if (opts.bcc) body.bcc = opts.bcc;
|
|
1639
1911
|
if (opts.contentType) body.contentType = opts.contentType;
|
|
1640
|
-
const res = await
|
|
1912
|
+
const res = await userFetch(
|
|
1641
1913
|
url,
|
|
1642
1914
|
{
|
|
1643
1915
|
method: "POST",
|
|
@@ -1647,7 +1919,7 @@ async function sendMail(opts) {
|
|
|
1647
1919
|
profile
|
|
1648
1920
|
);
|
|
1649
1921
|
if (res.status === 202) return;
|
|
1650
|
-
if (!res.ok) return
|
|
1922
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1651
1923
|
}
|
|
1652
1924
|
async function listMails(folderId = 0, userId = "me", count = 30, cursor, isUnread, profile = "default") {
|
|
1653
1925
|
const params = new URLSearchParams();
|
|
@@ -1658,8 +1930,8 @@ async function listMails(folderId = 0, userId = "me", count = 30, cursor, isUnre
|
|
|
1658
1930
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1659
1931
|
console.error(`[nworks] GET ${url}`);
|
|
1660
1932
|
}
|
|
1661
|
-
const res = await
|
|
1662
|
-
if (!res.ok) return
|
|
1933
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
1934
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1663
1935
|
const data = await res.json();
|
|
1664
1936
|
return {
|
|
1665
1937
|
mails: data.mails ?? [],
|
|
@@ -1674,10 +1946,22 @@ async function readMail(mailId, userId = "me", profile = "default") {
|
|
|
1674
1946
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1675
1947
|
console.error(`[nworks] GET ${url}`);
|
|
1676
1948
|
}
|
|
1677
|
-
const res = await
|
|
1678
|
-
if (!res.ok) return
|
|
1949
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
1950
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1679
1951
|
return await res.json();
|
|
1680
1952
|
}
|
|
1953
|
+
async function downloadAttachment(mailId, attachmentId, userId = "me", profile = "default") {
|
|
1954
|
+
const url = `${BASE_URL4}/users/${sanitizePathSegment(userId)}/mail/${sanitizePathSegment(
|
|
1955
|
+
String(mailId)
|
|
1956
|
+
)}/attachments/${sanitizePathSegment(String(attachmentId))}`;
|
|
1957
|
+
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1958
|
+
console.error(`[nworks] GET ${url}`);
|
|
1959
|
+
}
|
|
1960
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
1961
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1962
|
+
const data = await res.json();
|
|
1963
|
+
return { buffer: Buffer.from(data.data, "base64"), filename: data.filename };
|
|
1964
|
+
}
|
|
1681
1965
|
|
|
1682
1966
|
// src/commands/mail.ts
|
|
1683
1967
|
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 +2049,43 @@ var readCommand = new Command8("read").description("Read a specific mail (requir
|
|
|
1765
2049
|
cliError(err, opts, "mail");
|
|
1766
2050
|
}
|
|
1767
2051
|
});
|
|
1768
|
-
var
|
|
2052
|
+
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) => {
|
|
2053
|
+
try {
|
|
2054
|
+
const result = await downloadAttachment(
|
|
2055
|
+
parseInt(opts.id, 10),
|
|
2056
|
+
parseInt(opts.attachmentId, 10),
|
|
2057
|
+
opts.user ?? "me",
|
|
2058
|
+
opts.profile
|
|
2059
|
+
);
|
|
2060
|
+
const fileName = opts.name ?? result.filename;
|
|
2061
|
+
const outDir = opts.out ?? process.cwd();
|
|
2062
|
+
const outPath = join3(outDir, fileName);
|
|
2063
|
+
await writeFile3(outPath, result.buffer);
|
|
2064
|
+
output(
|
|
2065
|
+
{
|
|
2066
|
+
success: true,
|
|
2067
|
+
fileName,
|
|
2068
|
+
path: outPath,
|
|
2069
|
+
size: result.buffer.length
|
|
2070
|
+
},
|
|
2071
|
+
opts
|
|
2072
|
+
);
|
|
2073
|
+
} catch (err) {
|
|
2074
|
+
cliError(err, opts, "mail");
|
|
2075
|
+
}
|
|
2076
|
+
});
|
|
2077
|
+
var mailCommand = new Command8("mail").description("Mail operations (requires User OAuth with mail scope)").addCommand(sendCommand2).addCommand(listCommand3).addCommand(readCommand).addCommand(downloadAttachmentCommand);
|
|
1769
2078
|
|
|
1770
2079
|
// src/commands/task.ts
|
|
1771
2080
|
import { Command as Command9 } from "commander";
|
|
1772
2081
|
|
|
1773
2082
|
// src/api/task.ts
|
|
1774
2083
|
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
2084
|
async function resolveUserId(userId, profile) {
|
|
1796
2085
|
if (userId !== "me") return userId;
|
|
1797
2086
|
const url = `${BASE_URL5}/users/${sanitizePathSegment(userId)}`;
|
|
1798
|
-
const res = await
|
|
1799
|
-
if (!res.ok) return
|
|
2087
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
2088
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1800
2089
|
const data = await res.json();
|
|
1801
2090
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1802
2091
|
console.error(`[nworks] Resolved "me" \u2192 ${data.userId}`);
|
|
@@ -1813,8 +2102,8 @@ async function listTasks(categoryId = "default", userId = "me", count = 50, curs
|
|
|
1813
2102
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1814
2103
|
console.error(`[nworks] GET ${url}`);
|
|
1815
2104
|
}
|
|
1816
|
-
const res = await
|
|
1817
|
-
if (!res.ok) return
|
|
2105
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
2106
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1818
2107
|
const data = await res.json();
|
|
1819
2108
|
return { tasks: data.tasks ?? [], responseMetaData: data.responseMetaData };
|
|
1820
2109
|
}
|
|
@@ -1838,7 +2127,7 @@ async function createTask(opts) {
|
|
|
1838
2127
|
console.error(`[nworks] POST ${url}`);
|
|
1839
2128
|
console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
|
|
1840
2129
|
}
|
|
1841
|
-
const res = await
|
|
2130
|
+
const res = await userFetch(
|
|
1842
2131
|
url,
|
|
1843
2132
|
{
|
|
1844
2133
|
method: "POST",
|
|
@@ -1850,7 +2139,7 @@ async function createTask(opts) {
|
|
|
1850
2139
|
if (res.status === 201) {
|
|
1851
2140
|
return await res.json();
|
|
1852
2141
|
}
|
|
1853
|
-
if (!res.ok) return
|
|
2142
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1854
2143
|
return await res.json();
|
|
1855
2144
|
}
|
|
1856
2145
|
async function updateTask(opts) {
|
|
@@ -1863,7 +2152,7 @@ async function updateTask(opts) {
|
|
|
1863
2152
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1864
2153
|
console.error(`[nworks] PATCH ${url}`);
|
|
1865
2154
|
}
|
|
1866
|
-
const res = await
|
|
2155
|
+
const res = await userFetch(
|
|
1867
2156
|
url,
|
|
1868
2157
|
{
|
|
1869
2158
|
method: "PATCH",
|
|
@@ -1872,7 +2161,7 @@ async function updateTask(opts) {
|
|
|
1872
2161
|
},
|
|
1873
2162
|
profile
|
|
1874
2163
|
);
|
|
1875
|
-
if (!res.ok) return
|
|
2164
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1876
2165
|
return await res.json();
|
|
1877
2166
|
}
|
|
1878
2167
|
async function completeTask(taskId, profile = "default") {
|
|
@@ -1880,39 +2169,39 @@ async function completeTask(taskId, profile = "default") {
|
|
|
1880
2169
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1881
2170
|
console.error(`[nworks] POST ${url}`);
|
|
1882
2171
|
}
|
|
1883
|
-
const res = await
|
|
2172
|
+
const res = await userFetch(
|
|
1884
2173
|
url,
|
|
1885
2174
|
{ method: "POST" },
|
|
1886
2175
|
profile
|
|
1887
2176
|
);
|
|
1888
2177
|
if (res.status === 204) return;
|
|
1889
|
-
if (!res.ok) return
|
|
2178
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1890
2179
|
}
|
|
1891
2180
|
async function incompleteTask(taskId, profile = "default") {
|
|
1892
2181
|
const url = `${BASE_URL5}/tasks/${sanitizePathSegment(taskId)}/incomplete`;
|
|
1893
2182
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1894
2183
|
console.error(`[nworks] POST ${url}`);
|
|
1895
2184
|
}
|
|
1896
|
-
const res = await
|
|
2185
|
+
const res = await userFetch(
|
|
1897
2186
|
url,
|
|
1898
2187
|
{ method: "POST" },
|
|
1899
2188
|
profile
|
|
1900
2189
|
);
|
|
1901
2190
|
if (res.status === 204) return;
|
|
1902
|
-
if (!res.ok) return
|
|
2191
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1903
2192
|
}
|
|
1904
2193
|
async function deleteTask(taskId, profile = "default") {
|
|
1905
2194
|
const url = `${BASE_URL5}/tasks/${sanitizePathSegment(taskId)}`;
|
|
1906
2195
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1907
2196
|
console.error(`[nworks] DELETE ${url}`);
|
|
1908
2197
|
}
|
|
1909
|
-
const res = await
|
|
2198
|
+
const res = await userFetch(
|
|
1910
2199
|
url,
|
|
1911
2200
|
{ method: "DELETE" },
|
|
1912
2201
|
profile
|
|
1913
2202
|
);
|
|
1914
2203
|
if (res.status === 204) return;
|
|
1915
|
-
if (!res.ok) return
|
|
2204
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1916
2205
|
}
|
|
1917
2206
|
|
|
1918
2207
|
// src/commands/task.ts
|
|
@@ -2035,26 +2324,6 @@ import { Command as Command10 } from "commander";
|
|
|
2035
2324
|
|
|
2036
2325
|
// src/api/board.ts
|
|
2037
2326
|
var BASE_URL6 = "https://www.worksapis.com/v1.0";
|
|
2038
|
-
async function authedFetch5(url, init, profile) {
|
|
2039
|
-
const token = await getValidUserToken(profile);
|
|
2040
|
-
const headers = new Headers(init.headers);
|
|
2041
|
-
headers.set("Authorization", `Bearer ${token}`);
|
|
2042
|
-
return fetch(url, { ...init, headers });
|
|
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
2327
|
function safeParseJson(text) {
|
|
2059
2328
|
const safe = text.replace(
|
|
2060
2329
|
/"((?:board|post|domain|user)Id)"\s*:\s*(\d{16,})/g,
|
|
@@ -2070,8 +2339,8 @@ async function listBoards(count = 20, cursor, profile = "default") {
|
|
|
2070
2339
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
2071
2340
|
console.error(`[nworks] GET ${url}`);
|
|
2072
2341
|
}
|
|
2073
|
-
const res = await
|
|
2074
|
-
if (!res.ok) return
|
|
2342
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
2343
|
+
if (!res.ok) return handleUserApiError(res);
|
|
2075
2344
|
const text = await res.text();
|
|
2076
2345
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
2077
2346
|
console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
|
|
@@ -2087,8 +2356,8 @@ async function listPosts(boardId, count = 20, cursor, profile = "default") {
|
|
|
2087
2356
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
2088
2357
|
console.error(`[nworks] GET ${url}`);
|
|
2089
2358
|
}
|
|
2090
|
-
const res = await
|
|
2091
|
-
if (!res.ok) return
|
|
2359
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
2360
|
+
if (!res.ok) return handleUserApiError(res);
|
|
2092
2361
|
const text = await res.text();
|
|
2093
2362
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
2094
2363
|
console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
|
|
@@ -2101,8 +2370,8 @@ async function readPost(boardId, postId, profile = "default") {
|
|
|
2101
2370
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
2102
2371
|
console.error(`[nworks] GET ${url}`);
|
|
2103
2372
|
}
|
|
2104
|
-
const res = await
|
|
2105
|
-
if (!res.ok) return
|
|
2373
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
2374
|
+
if (!res.ok) return handleUserApiError(res);
|
|
2106
2375
|
const text = await res.text();
|
|
2107
2376
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
2108
2377
|
console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
|
|
@@ -2122,7 +2391,7 @@ async function createPost(opts) {
|
|
|
2122
2391
|
console.error(`[nworks] POST ${url}`);
|
|
2123
2392
|
console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
|
|
2124
2393
|
}
|
|
2125
|
-
const res = await
|
|
2394
|
+
const res = await userFetch(
|
|
2126
2395
|
url,
|
|
2127
2396
|
{
|
|
2128
2397
|
method: "POST",
|
|
@@ -2138,7 +2407,7 @@ async function createPost(opts) {
|
|
|
2138
2407
|
}
|
|
2139
2408
|
return safeParseJson(text);
|
|
2140
2409
|
}
|
|
2141
|
-
return
|
|
2410
|
+
return handleUserApiError(res);
|
|
2142
2411
|
}
|
|
2143
2412
|
|
|
2144
2413
|
// src/commands/board.ts
|
|
@@ -2236,128 +2505,256 @@ var createCommand3 = new Command10("create").description("Create a post in a boa
|
|
|
2236
2505
|
});
|
|
2237
2506
|
var boardCommand = new Command10("board").description("Board operations (requires User OAuth)").addCommand(listCommand5).addCommand(postsCommand).addCommand(readCommand2).addCommand(createCommand3);
|
|
2238
2507
|
|
|
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
|
|
2508
|
+
// src/commands/contact.ts
|
|
2250
2509
|
import { Command as Command11 } from "commander";
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2510
|
+
|
|
2511
|
+
// src/api/contact.ts
|
|
2512
|
+
var BASE_URL7 = "https://www.worksapis.com/v1.0";
|
|
2513
|
+
async function listContacts(opts = {}) {
|
|
2514
|
+
const userId = opts.userId ?? "me";
|
|
2515
|
+
const profile = opts.profile ?? "default";
|
|
2516
|
+
const params = new URLSearchParams();
|
|
2517
|
+
params.set("count", String(opts.count ?? 20));
|
|
2518
|
+
if (opts.cursor) params.set("cursor", opts.cursor);
|
|
2519
|
+
if (opts.accessibleRange) params.set("accessibleRange", opts.accessibleRange);
|
|
2520
|
+
if (opts.contactTagId) params.set("contactTagId", opts.contactTagId);
|
|
2521
|
+
if (opts.email) params.set("email", opts.email);
|
|
2522
|
+
if (opts.telephone) params.set("telephone", opts.telephone);
|
|
2523
|
+
if (opts.orderBy) params.set("orderBy", opts.orderBy);
|
|
2524
|
+
if (opts.searchDateType) params.set("searchDateType", opts.searchDateType);
|
|
2525
|
+
if (opts.startDateTime) params.set("startDateTime", opts.startDateTime);
|
|
2526
|
+
if (opts.endDateTime) params.set("endDateTime", opts.endDateTime);
|
|
2527
|
+
const url = `${BASE_URL7}/users/${sanitizePathSegment(userId)}/contacts?${params.toString()}`;
|
|
2528
|
+
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
2529
|
+
console.error(`[nworks] GET ${url}`);
|
|
2262
2530
|
}
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2531
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
2532
|
+
if (!res.ok) return handleUserApiError(res);
|
|
2533
|
+
const data = await res.json();
|
|
2534
|
+
return { contacts: data.contacts ?? [], responseMetaData: data.responseMetaData };
|
|
2535
|
+
}
|
|
2536
|
+
async function getContact(contactId, profile = "default") {
|
|
2537
|
+
const url = `${BASE_URL7}/contacts/${sanitizePathSegment(contactId)}`;
|
|
2538
|
+
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
2539
|
+
console.error(`[nworks] GET ${url}`);
|
|
2267
2540
|
}
|
|
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" });
|
|
2541
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
2542
|
+
if (!res.ok) return handleUserApiError(res);
|
|
2543
|
+
return await res.json();
|
|
2544
|
+
}
|
|
2545
|
+
async function createContact(payload, profile = "default") {
|
|
2546
|
+
const url = `${BASE_URL7}/contacts`;
|
|
2547
|
+
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
2548
|
+
console.error(`[nworks] POST ${url}`);
|
|
2281
2549
|
}
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2550
|
+
const res = await userFetch(
|
|
2551
|
+
url,
|
|
2552
|
+
{
|
|
2553
|
+
method: "POST",
|
|
2554
|
+
headers: { "Content-Type": "application/json" },
|
|
2555
|
+
body: JSON.stringify(payload)
|
|
2556
|
+
},
|
|
2557
|
+
profile
|
|
2558
|
+
);
|
|
2559
|
+
if (!res.ok) return handleUserApiError(res);
|
|
2560
|
+
return await res.json();
|
|
2561
|
+
}
|
|
2562
|
+
async function updateContact(contactId, payload, partial = true, profile = "default") {
|
|
2563
|
+
const url = `${BASE_URL7}/contacts/${sanitizePathSegment(contactId)}`;
|
|
2564
|
+
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
2565
|
+
console.error(`[nworks] ${partial ? "PATCH" : "PUT"} ${url}`);
|
|
2286
2566
|
}
|
|
2287
|
-
const
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2567
|
+
const res = await userFetch(
|
|
2568
|
+
url,
|
|
2569
|
+
{
|
|
2570
|
+
method: partial ? "PATCH" : "PUT",
|
|
2571
|
+
headers: { "Content-Type": "application/json" },
|
|
2572
|
+
body: JSON.stringify(payload)
|
|
2573
|
+
},
|
|
2574
|
+
profile
|
|
2575
|
+
);
|
|
2576
|
+
if (!res.ok) return handleUserApiError(res);
|
|
2577
|
+
return await res.json();
|
|
2578
|
+
}
|
|
2579
|
+
async function deleteContact(contactId, profile = "default") {
|
|
2580
|
+
const url = `${BASE_URL7}/contacts/${sanitizePathSegment(contactId)}`;
|
|
2581
|
+
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
2582
|
+
console.error(`[nworks] DELETE ${url}`);
|
|
2297
2583
|
}
|
|
2298
|
-
const
|
|
2299
|
-
if (
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2584
|
+
const res = await userFetch(url, { method: "DELETE" }, profile);
|
|
2585
|
+
if (res.status === 204) return;
|
|
2586
|
+
if (!res.ok) return handleUserApiError(res);
|
|
2587
|
+
}
|
|
2588
|
+
async function listContactTags(opts = {}) {
|
|
2589
|
+
const userId = opts.userId ?? "me";
|
|
2590
|
+
const profile = opts.profile ?? "default";
|
|
2591
|
+
const params = new URLSearchParams();
|
|
2592
|
+
params.set("count", String(opts.count ?? 20));
|
|
2593
|
+
if (opts.cursor) params.set("cursor", opts.cursor);
|
|
2594
|
+
const url = `${BASE_URL7}/users/${sanitizePathSegment(userId)}/contact-tags?${params.toString()}`;
|
|
2595
|
+
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
2596
|
+
console.error(`[nworks] GET ${url}`);
|
|
2597
|
+
}
|
|
2598
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
2599
|
+
if (!res.ok) return handleUserApiError(res);
|
|
2600
|
+
const data = await res.json();
|
|
2601
|
+
return { contactTags: data.contactTags ?? [], responseMetaData: data.responseMetaData };
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
// src/commands/contact.ts
|
|
2605
|
+
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) => {
|
|
2606
|
+
try {
|
|
2607
|
+
const result = await listContacts({
|
|
2608
|
+
userId: opts.user ?? "me",
|
|
2609
|
+
count: parseInt(opts.count, 10),
|
|
2610
|
+
cursor: opts.cursor,
|
|
2611
|
+
contactTagId: opts.tag,
|
|
2612
|
+
email: opts.email,
|
|
2613
|
+
telephone: opts.tel,
|
|
2614
|
+
profile: opts.profile
|
|
2305
2615
|
});
|
|
2306
|
-
|
|
2307
|
-
|
|
2616
|
+
output(
|
|
2617
|
+
{
|
|
2618
|
+
contacts: result.contacts,
|
|
2619
|
+
count: result.contacts.length,
|
|
2620
|
+
nextCursor: result.responseMetaData?.nextCursor ?? null
|
|
2621
|
+
},
|
|
2622
|
+
opts
|
|
2623
|
+
);
|
|
2624
|
+
} catch (err) {
|
|
2625
|
+
cliError(err, opts, "contact");
|
|
2308
2626
|
}
|
|
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" });
|
|
2627
|
+
});
|
|
2628
|
+
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) => {
|
|
2629
|
+
try {
|
|
2630
|
+
const result = await getContact(opts.id, opts.profile);
|
|
2631
|
+
output(result, opts);
|
|
2632
|
+
} catch (err) {
|
|
2633
|
+
cliError(err, opts, "contact");
|
|
2337
2634
|
}
|
|
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) => {
|
|
2635
|
+
});
|
|
2636
|
+
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
2637
|
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();
|
|
2638
|
+
const payload = JSON.parse(opts.payload);
|
|
2639
|
+
if (opts.dryRun) {
|
|
2640
|
+
output({ dryRun: true, request: payload }, opts);
|
|
2641
|
+
return;
|
|
2352
2642
|
}
|
|
2643
|
+
const result = await createContact(payload, opts.profile);
|
|
2644
|
+
output({ success: true, ...result }, opts);
|
|
2353
2645
|
} catch (err) {
|
|
2354
|
-
cliError(err, opts);
|
|
2646
|
+
cliError(err, opts, "contact");
|
|
2647
|
+
}
|
|
2648
|
+
});
|
|
2649
|
+
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) => {
|
|
2650
|
+
try {
|
|
2651
|
+
const payload = JSON.parse(opts.payload);
|
|
2652
|
+
const result = await updateContact(
|
|
2653
|
+
opts.id,
|
|
2654
|
+
payload,
|
|
2655
|
+
!opts.replace,
|
|
2656
|
+
opts.profile
|
|
2657
|
+
);
|
|
2658
|
+
output({ success: true, ...result }, opts);
|
|
2659
|
+
} catch (err) {
|
|
2660
|
+
cliError(err, opts, "contact");
|
|
2661
|
+
}
|
|
2662
|
+
});
|
|
2663
|
+
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) => {
|
|
2664
|
+
try {
|
|
2665
|
+
await deleteContact(opts.id, opts.profile);
|
|
2666
|
+
output({ success: true, contactId: opts.id, message: "Contact deleted" }, opts);
|
|
2667
|
+
} catch (err) {
|
|
2668
|
+
cliError(err, opts, "contact");
|
|
2669
|
+
}
|
|
2670
|
+
});
|
|
2671
|
+
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) => {
|
|
2672
|
+
try {
|
|
2673
|
+
const result = await listContactTags({
|
|
2674
|
+
userId: opts.user ?? "me",
|
|
2675
|
+
count: parseInt(opts.count, 10),
|
|
2676
|
+
cursor: opts.cursor,
|
|
2677
|
+
profile: opts.profile
|
|
2678
|
+
});
|
|
2679
|
+
output(
|
|
2680
|
+
{
|
|
2681
|
+
contactTags: result.contactTags,
|
|
2682
|
+
count: result.contactTags.length,
|
|
2683
|
+
nextCursor: result.responseMetaData?.nextCursor ?? null
|
|
2684
|
+
},
|
|
2685
|
+
opts
|
|
2686
|
+
);
|
|
2687
|
+
} catch (err) {
|
|
2688
|
+
cliError(err, opts, "contact");
|
|
2355
2689
|
}
|
|
2356
2690
|
});
|
|
2691
|
+
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
2692
|
|
|
2358
|
-
// src/mcp
|
|
2359
|
-
|
|
2360
|
-
|
|
2693
|
+
// src/commands/mcp-cmd.ts
|
|
2694
|
+
import { Command as Command13 } from "commander";
|
|
2695
|
+
import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2696
|
+
|
|
2697
|
+
// src/mcp/server.ts
|
|
2698
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2699
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2700
|
+
|
|
2701
|
+
// src/mcp/tools/setup.ts
|
|
2702
|
+
import { z as z2 } from "zod";
|
|
2703
|
+
|
|
2704
|
+
// src/mcp/tool-registry.ts
|
|
2705
|
+
var TOOL_ANNOTATIONS = {
|
|
2706
|
+
nworks_setup: { title: "Save credentials", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
2707
|
+
nworks_message_send: { title: "Send message", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
2708
|
+
nworks_message_members: { title: "List channel members", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2709
|
+
nworks_directory_members: { title: "List organization members", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2710
|
+
nworks_calendar_list: { title: "List calendar events", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2711
|
+
nworks_calendar_create: { title: "Create calendar event", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
2712
|
+
nworks_calendar_update: { title: "Update calendar event", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
2713
|
+
nworks_calendar_delete: { title: "Delete calendar event", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
2714
|
+
nworks_drive_list: { title: "List drive files", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2715
|
+
nworks_drive_upload: { title: "Upload file to drive", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
2716
|
+
// download는 outputDir 지정 시 로컬 파일을 쓰므로 순수 읽기가 아니다(readOnlyHint=false).
|
|
2717
|
+
nworks_drive_download: { title: "Download drive file", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
2718
|
+
nworks_sharedrive_list: { title: "List shared drives", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2719
|
+
nworks_sharedrive_files: { title: "List shared drive files", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2720
|
+
nworks_sharedrive_download: { title: "Download shared drive file", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
2721
|
+
nworks_mail_send: { title: "Send mail", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
2722
|
+
nworks_mail_list: { title: "List mail", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2723
|
+
nworks_mail_read: { title: "Read mail", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2724
|
+
// drive_download와 같은 이유로 outputDir 지정 시 로컬 파일을 쓴다(readOnlyHint=false).
|
|
2725
|
+
nworks_mail_download_attachment: { title: "Download mail attachment", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
2726
|
+
nworks_task_list: { title: "List tasks", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2727
|
+
nworks_task_create: { title: "Create task", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
2728
|
+
nworks_task_update: { title: "Update task", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
2729
|
+
nworks_task_delete: { title: "Delete task", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
2730
|
+
nworks_board_list: { title: "List boards", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2731
|
+
nworks_board_posts: { title: "List board posts", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2732
|
+
nworks_board_read: { title: "Read board post", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2733
|
+
nworks_board_create: { title: "Create board post", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
2734
|
+
nworks_contact_list: { title: "List contacts", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2735
|
+
nworks_contact_get: { title: "Get contact", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2736
|
+
nworks_contact_create: { title: "Create contact", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
2737
|
+
nworks_contact_update: { title: "Update contact", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
2738
|
+
nworks_contact_delete: { title: "Delete contact", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
2739
|
+
nworks_contact_list_tags: { title: "List contact tags", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2740
|
+
nworks_login_user: { title: "Log in with User OAuth", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
2741
|
+
nworks_whoami: { title: "Show auth status", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
2742
|
+
nworks_logout: { title: "Log out", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
2743
|
+
nworks_doctor: { title: "Diagnose connection", readOnlyHint: true, idempotentHint: true, openWorldHint: true }
|
|
2744
|
+
};
|
|
2745
|
+
function defineTool(server, name, description, inputSchema, handler) {
|
|
2746
|
+
const annotations = TOOL_ANNOTATIONS[name];
|
|
2747
|
+
server.registerTool(
|
|
2748
|
+
name,
|
|
2749
|
+
{ description, inputSchema, ...annotations ? { annotations } : {} },
|
|
2750
|
+
handler
|
|
2751
|
+
);
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
// src/mcp/tools/setup.ts
|
|
2755
|
+
function registerSetupTools(server) {
|
|
2756
|
+
defineTool(
|
|
2757
|
+
server,
|
|
2361
2758
|
"nworks_setup",
|
|
2362
2759
|
`NAVER WORKS API \uC778\uC99D \uC815\uBCF4\uB97C \uC124\uC815\uD569\uB2C8\uB2E4.
|
|
2363
2760
|
|
|
@@ -2380,10 +2777,10 @@ function registerTools(server) {
|
|
|
2380
2777
|
|
|
2381
2778
|
OAuth Redirect URI: http://localhost:9876/callback`,
|
|
2382
2779
|
{
|
|
2383
|
-
clientId:
|
|
2384
|
-
serviceAccount:
|
|
2385
|
-
botId:
|
|
2386
|
-
domainId:
|
|
2780
|
+
clientId: z2.string().describe("Client ID (Developer Console\uC5D0\uC11C \uBC1C\uAE09)"),
|
|
2781
|
+
serviceAccount: z2.string().optional().describe("Service Account ID (\uC608: xxxxx.serviceaccount@domain)"),
|
|
2782
|
+
botId: z2.string().optional().describe("Bot ID (\uBA54\uC2DC\uC9C0 \uC804\uC1A1 \uC2DC \uD544\uC694)"),
|
|
2783
|
+
domainId: z2.string().optional().describe("Domain ID")
|
|
2387
2784
|
},
|
|
2388
2785
|
async ({ clientId, serviceAccount, botId, domainId }) => {
|
|
2389
2786
|
try {
|
|
@@ -2421,7 +2818,7 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2421
2818
|
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
2819
|
}
|
|
2423
2820
|
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
|
|
2821
|
+
const mask2 = (s) => s.length <= 4 ? "****" : `****${s.slice(-Math.min(4, Math.floor(s.length / 3)))}`;
|
|
2425
2822
|
return {
|
|
2426
2823
|
content: [
|
|
2427
2824
|
{
|
|
@@ -2430,10 +2827,10 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2430
2827
|
success: true,
|
|
2431
2828
|
message: "\uC778\uC99D \uC815\uBCF4\uAC00 \uC800\uC7A5\uB418\uC5C8\uC2B5\uB2C8\uB2E4.",
|
|
2432
2829
|
nextSteps,
|
|
2433
|
-
clientId:
|
|
2434
|
-
clientSecret: `${
|
|
2830
|
+
clientId: mask2(clientId),
|
|
2831
|
+
clientSecret: `${mask2(resolvedSecret)} (\uD658\uACBD\uBCC0\uC218)`,
|
|
2435
2832
|
serviceAccount: serviceAccount ?? null,
|
|
2436
|
-
privateKeyPath: resolvedPrivateKeyPath ? `${
|
|
2833
|
+
privateKeyPath: resolvedPrivateKeyPath ? `${mask2(resolvedPrivateKeyPath)} (\uD658\uACBD\uBCC0\uC218)` : null,
|
|
2437
2834
|
botId: botId ?? null
|
|
2438
2835
|
})
|
|
2439
2836
|
}
|
|
@@ -2447,16 +2844,22 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2447
2844
|
}
|
|
2448
2845
|
}
|
|
2449
2846
|
);
|
|
2450
|
-
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
// src/mcp/tools/message.ts
|
|
2850
|
+
import { z as z3 } from "zod";
|
|
2851
|
+
function registerMessageTools(server) {
|
|
2852
|
+
defineTool(
|
|
2853
|
+
server,
|
|
2451
2854
|
"nworks_message_send",
|
|
2452
2855
|
"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
2856
|
{
|
|
2454
|
-
to:
|
|
2455
|
-
channel:
|
|
2456
|
-
text:
|
|
2457
|
-
type:
|
|
2458
|
-
actions:
|
|
2459
|
-
elements:
|
|
2857
|
+
to: z3.string().optional().describe("\uC218\uC2E0\uC790 userId (channel\uACFC \uD0DD 1). nworks_directory_members\uB85C userId \uC870\uD68C \uAC00\uB2A5"),
|
|
2858
|
+
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"),
|
|
2859
|
+
text: z3.string().describe("\uBA54\uC2DC\uC9C0 \uBCF8\uBB38"),
|
|
2860
|
+
type: z3.enum(["text", "button", "list"]).optional().describe("\uBA54\uC2DC\uC9C0 \uD0C0\uC785 (\uAE30\uBCF8: text)"),
|
|
2861
|
+
actions: z3.string().optional().describe("\uBC84\uD2BC \uC561\uC158 JSON (type=button\uC77C \uB54C)"),
|
|
2862
|
+
elements: z3.string().optional().describe("\uB9AC\uC2A4\uD2B8 \uD56D\uBAA9 JSON (type=list\uC77C \uB54C)")
|
|
2460
2863
|
},
|
|
2461
2864
|
async ({ to, channel, text, type, actions, elements }) => {
|
|
2462
2865
|
try {
|
|
@@ -2479,11 +2882,12 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2479
2882
|
}
|
|
2480
2883
|
}
|
|
2481
2884
|
);
|
|
2482
|
-
|
|
2885
|
+
defineTool(
|
|
2886
|
+
server,
|
|
2483
2887
|
"nworks_message_members",
|
|
2484
2888
|
"\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
2889
|
{
|
|
2486
|
-
channel:
|
|
2890
|
+
channel: z3.string().describe("\uCC44\uB110 channelId")
|
|
2487
2891
|
},
|
|
2488
2892
|
async ({ channel }) => {
|
|
2489
2893
|
try {
|
|
@@ -2499,15 +2903,29 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2499
2903
|
}
|
|
2500
2904
|
}
|
|
2501
2905
|
);
|
|
2502
|
-
|
|
2906
|
+
}
|
|
2907
|
+
|
|
2908
|
+
// src/mcp/tools/directory.ts
|
|
2909
|
+
import { z as z4 } from "zod";
|
|
2910
|
+
function registerDirectoryTools(server) {
|
|
2911
|
+
defineTool(
|
|
2912
|
+
server,
|
|
2503
2913
|
"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
|
-
|
|
2914
|
+
"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",
|
|
2915
|
+
{
|
|
2916
|
+
count: z4.number().int().min(1).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 100)"),
|
|
2917
|
+
cursor: z4.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C (\uC774\uC804 \uC751\uB2F5\uC758 nextCursor \uAC12)")
|
|
2918
|
+
},
|
|
2919
|
+
async ({ count, cursor }) => {
|
|
2507
2920
|
try {
|
|
2508
|
-
const result = await listUsers();
|
|
2921
|
+
const result = await listUsers(count ?? 100, cursor);
|
|
2509
2922
|
return {
|
|
2510
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
2923
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
2924
|
+
users: result.users,
|
|
2925
|
+
count: result.users.length,
|
|
2926
|
+
hasMore: !!result.responseMetaData?.nextCursor,
|
|
2927
|
+
nextCursor: result.responseMetaData?.nextCursor ?? null
|
|
2928
|
+
}) }]
|
|
2511
2929
|
};
|
|
2512
2930
|
} catch (err) {
|
|
2513
2931
|
return {
|
|
@@ -2517,13 +2935,19 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2517
2935
|
}
|
|
2518
2936
|
}
|
|
2519
2937
|
);
|
|
2520
|
-
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
// src/mcp/tools/calendar.ts
|
|
2941
|
+
import { z as z5 } from "zod";
|
|
2942
|
+
function registerCalendarTools(server) {
|
|
2943
|
+
defineTool(
|
|
2944
|
+
server,
|
|
2521
2945
|
"nworks_calendar_list",
|
|
2522
2946
|
"\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
2947
|
{
|
|
2524
|
-
fromDateTime:
|
|
2525
|
-
untilDateTime:
|
|
2526
|
-
userId:
|
|
2948
|
+
fromDateTime: z5.string().describe("\uC2DC\uC791 \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss+09:00)"),
|
|
2949
|
+
untilDateTime: z5.string().describe("\uC885\uB8CC \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss+09:00)"),
|
|
2950
|
+
userId: z5.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
|
|
2527
2951
|
},
|
|
2528
2952
|
async ({ fromDateTime, untilDateTime, userId }) => {
|
|
2529
2953
|
try {
|
|
@@ -2552,19 +2976,20 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2552
2976
|
}
|
|
2553
2977
|
}
|
|
2554
2978
|
);
|
|
2555
|
-
|
|
2979
|
+
defineTool(
|
|
2980
|
+
server,
|
|
2556
2981
|
"nworks_calendar_create",
|
|
2557
2982
|
"\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
2983
|
{
|
|
2559
|
-
summary:
|
|
2560
|
-
start:
|
|
2561
|
-
end:
|
|
2562
|
-
timeZone:
|
|
2563
|
-
description:
|
|
2564
|
-
location:
|
|
2565
|
-
attendees:
|
|
2566
|
-
sendNotification:
|
|
2567
|
-
userId:
|
|
2984
|
+
summary: z5.string().describe("\uC77C\uC815 \uC81C\uBAA9"),
|
|
2985
|
+
start: z5.string().describe("\uC2DC\uC791 \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss)"),
|
|
2986
|
+
end: z5.string().describe("\uC885\uB8CC \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss)"),
|
|
2987
|
+
timeZone: z5.string().optional().describe("\uD0C0\uC784\uC874 (\uAE30\uBCF8: Asia/Seoul)"),
|
|
2988
|
+
description: z5.string().optional().describe("\uC77C\uC815 \uC124\uBA85"),
|
|
2989
|
+
location: z5.string().optional().describe("\uC7A5\uC18C"),
|
|
2990
|
+
attendees: z5.array(z5.object({ email: z5.string(), displayName: z5.string().optional() })).optional().describe("\uCC38\uC11D\uC790 \uBAA9\uB85D"),
|
|
2991
|
+
sendNotification: z5.boolean().optional().describe("\uCC38\uC11D\uC790\uC5D0\uAC8C \uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)"),
|
|
2992
|
+
userId: z5.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
|
|
2568
2993
|
},
|
|
2569
2994
|
async ({
|
|
2570
2995
|
summary,
|
|
@@ -2601,19 +3026,20 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2601
3026
|
}
|
|
2602
3027
|
}
|
|
2603
3028
|
);
|
|
2604
|
-
|
|
3029
|
+
defineTool(
|
|
3030
|
+
server,
|
|
2605
3031
|
"nworks_calendar_update",
|
|
2606
3032
|
"\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
3033
|
{
|
|
2608
|
-
eventId:
|
|
2609
|
-
summary:
|
|
2610
|
-
start:
|
|
2611
|
-
end:
|
|
2612
|
-
timeZone:
|
|
2613
|
-
description:
|
|
2614
|
-
location:
|
|
2615
|
-
sendNotification:
|
|
2616
|
-
userId:
|
|
3034
|
+
eventId: z5.string().describe("\uC77C\uC815 ID (nworks_calendar_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
|
|
3035
|
+
summary: z5.string().optional().describe("\uC0C8 \uC81C\uBAA9"),
|
|
3036
|
+
start: z5.string().optional().describe("\uC0C8 \uC2DC\uC791 \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss)"),
|
|
3037
|
+
end: z5.string().optional().describe("\uC0C8 \uC885\uB8CC \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss)"),
|
|
3038
|
+
timeZone: z5.string().optional().describe("\uD0C0\uC784\uC874 (\uAE30\uBCF8: Asia/Seoul)"),
|
|
3039
|
+
description: z5.string().optional().describe("\uC0C8 \uC124\uBA85"),
|
|
3040
|
+
location: z5.string().optional().describe("\uC0C8 \uC7A5\uC18C"),
|
|
3041
|
+
sendNotification: z5.boolean().optional().describe("\uCC38\uC11D\uC790\uC5D0\uAC8C \uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)"),
|
|
3042
|
+
userId: z5.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
|
|
2617
3043
|
},
|
|
2618
3044
|
async ({
|
|
2619
3045
|
eventId,
|
|
@@ -2649,13 +3075,14 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2649
3075
|
}
|
|
2650
3076
|
}
|
|
2651
3077
|
);
|
|
2652
|
-
|
|
3078
|
+
defineTool(
|
|
3079
|
+
server,
|
|
2653
3080
|
"nworks_calendar_delete",
|
|
2654
3081
|
"\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
3082
|
{
|
|
2656
|
-
eventId:
|
|
2657
|
-
sendNotification:
|
|
2658
|
-
userId:
|
|
3083
|
+
eventId: z5.string().describe("\uC0AD\uC81C\uD560 \uC77C\uC815 ID (nworks_calendar_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
|
|
3084
|
+
sendNotification: z5.boolean().optional().describe("\uCC38\uC11D\uC790\uC5D0\uAC8C \uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)"),
|
|
3085
|
+
userId: z5.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
|
|
2659
3086
|
},
|
|
2660
3087
|
async ({ eventId, sendNotification, userId }) => {
|
|
2661
3088
|
try {
|
|
@@ -2675,14 +3102,20 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2675
3102
|
}
|
|
2676
3103
|
}
|
|
2677
3104
|
);
|
|
2678
|
-
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
// src/mcp/tools/drive.ts
|
|
3108
|
+
import { z as z6 } from "zod";
|
|
3109
|
+
function registerDriveTools(server) {
|
|
3110
|
+
defineTool(
|
|
3111
|
+
server,
|
|
2679
3112
|
"nworks_drive_list",
|
|
2680
3113
|
"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
3114
|
{
|
|
2682
|
-
userId:
|
|
2683
|
-
folderId:
|
|
2684
|
-
count:
|
|
2685
|
-
cursor:
|
|
3115
|
+
userId: z6.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)"),
|
|
3116
|
+
folderId: z6.string().optional().describe("\uD3F4\uB354 ID (\uBBF8\uC9C0\uC815 \uC2DC \uB8E8\uD2B8)"),
|
|
3117
|
+
count: z6.number().int().min(1).max(200).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 200)"),
|
|
3118
|
+
cursor: z6.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
|
|
2686
3119
|
},
|
|
2687
3120
|
async ({ userId, folderId, count, cursor }) => {
|
|
2688
3121
|
try {
|
|
@@ -2711,16 +3144,17 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2711
3144
|
}
|
|
2712
3145
|
}
|
|
2713
3146
|
);
|
|
2714
|
-
|
|
3147
|
+
defineTool(
|
|
3148
|
+
server,
|
|
2715
3149
|
"nworks_drive_upload",
|
|
2716
3150
|
"\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
3151
|
{
|
|
2718
|
-
content:
|
|
2719
|
-
fileName:
|
|
2720
|
-
filePath:
|
|
2721
|
-
userId:
|
|
2722
|
-
folderId:
|
|
2723
|
-
overwrite:
|
|
3152
|
+
content: z6.string().optional().describe("\uC5C5\uB85C\uB4DC\uD560 \uD30C\uC77C \uB0B4\uC6A9 (base64 \uC778\uCF54\uB529). filePath \uB300\uC2E0 \uC0AC\uC6A9"),
|
|
3153
|
+
fileName: z6.string().optional().describe("\uD30C\uC77C\uBA85 (content \uC0AC\uC6A9 \uC2DC \uD544\uC218)"),
|
|
3154
|
+
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)"),
|
|
3155
|
+
userId: z6.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)"),
|
|
3156
|
+
folderId: z6.string().optional().describe("\uC5C5\uB85C\uB4DC\uD560 \uD3F4\uB354 ID (\uBBF8\uC9C0\uC815 \uC2DC \uB8E8\uD2B8)"),
|
|
3157
|
+
overwrite: z6.boolean().optional().describe("\uB3D9\uC77C \uD30C\uC77C\uBA85 \uB36E\uC5B4\uC4F0\uAE30 (\uAE30\uBCF8: false)")
|
|
2724
3158
|
},
|
|
2725
3159
|
async ({ content, fileName, filePath, userId, folderId, overwrite }) => {
|
|
2726
3160
|
try {
|
|
@@ -2755,43 +3189,157 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2755
3189
|
};
|
|
2756
3190
|
}
|
|
2757
3191
|
return {
|
|
2758
|
-
content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }]
|
|
3192
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }]
|
|
3193
|
+
};
|
|
3194
|
+
} catch (err) {
|
|
3195
|
+
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
3196
|
+
console.error(`[nworks] drive upload error: ${err.stack}`);
|
|
3197
|
+
}
|
|
3198
|
+
return {
|
|
3199
|
+
content: [{ type: "text", text: mcpErrorHint(err, "drive.upload") }],
|
|
3200
|
+
isError: true
|
|
3201
|
+
};
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
);
|
|
3205
|
+
defineTool(
|
|
3206
|
+
server,
|
|
3207
|
+
"nworks_drive_download",
|
|
3208
|
+
"\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.",
|
|
3209
|
+
{
|
|
3210
|
+
fileId: z6.string().describe("\uB2E4\uC6B4\uB85C\uB4DC\uD560 \uD30C\uC77C ID (nworks_drive_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
|
|
3211
|
+
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)"),
|
|
3212
|
+
outputName: z6.string().optional().describe("\uC800\uC7A5 \uD30C\uC77C\uBA85 (\uBBF8\uC9C0\uC815 \uC2DC \uC6D0\uBCF8 \uD30C\uC77C\uBA85)"),
|
|
3213
|
+
userId: z6.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
|
|
3214
|
+
},
|
|
3215
|
+
async ({ fileId, outputDir, outputName, userId }) => {
|
|
3216
|
+
try {
|
|
3217
|
+
const result = await downloadFile(
|
|
3218
|
+
fileId,
|
|
3219
|
+
userId ?? "me"
|
|
3220
|
+
);
|
|
3221
|
+
const fileName = outputName ?? result.fileName ?? fileId;
|
|
3222
|
+
if (outputDir) {
|
|
3223
|
+
const { writeFile: writeFile4 } = await import("fs/promises");
|
|
3224
|
+
const { join: join5 } = await import("path");
|
|
3225
|
+
const safeDir = validateLocalPath(outputDir);
|
|
3226
|
+
const safeName = sanitizeFileName(fileName);
|
|
3227
|
+
const outPath = join5(safeDir, safeName);
|
|
3228
|
+
validateLocalPath(outPath, safeDir);
|
|
3229
|
+
await writeFile4(outPath, result.buffer);
|
|
3230
|
+
return {
|
|
3231
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, fileName, path: outPath, size: result.buffer.length }) }]
|
|
3232
|
+
};
|
|
3233
|
+
}
|
|
3234
|
+
const MAX_INLINE_SIZE = 5 * 1024 * 1024;
|
|
3235
|
+
if (result.buffer.length > MAX_INLINE_SIZE) {
|
|
3236
|
+
const sizeMB = (result.buffer.length / (1024 * 1024)).toFixed(1);
|
|
3237
|
+
return {
|
|
3238
|
+
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 }) }],
|
|
3239
|
+
isError: true
|
|
3240
|
+
};
|
|
3241
|
+
}
|
|
3242
|
+
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;
|
|
3243
|
+
const isText = textExtensions.test(fileName);
|
|
3244
|
+
if (isText) {
|
|
3245
|
+
const text = result.buffer.toString("utf-8");
|
|
3246
|
+
return {
|
|
3247
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, fileName, size: result.buffer.length, encoding: "text", content: text }) }]
|
|
3248
|
+
};
|
|
3249
|
+
}
|
|
3250
|
+
const base64 = result.buffer.toString("base64");
|
|
3251
|
+
return {
|
|
3252
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, fileName, size: result.buffer.length, encoding: "base64", content: base64 }) }]
|
|
3253
|
+
};
|
|
3254
|
+
} catch (err) {
|
|
3255
|
+
return {
|
|
3256
|
+
content: [{ type: "text", text: mcpErrorHint(err, "drive.download") }],
|
|
3257
|
+
isError: true
|
|
3258
|
+
};
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
);
|
|
3262
|
+
defineTool(
|
|
3263
|
+
server,
|
|
3264
|
+
"nworks_sharedrive_list",
|
|
3265
|
+
"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)",
|
|
3266
|
+
{
|
|
3267
|
+
count: z6.number().int().min(1).max(200).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 200)"),
|
|
3268
|
+
cursor: z6.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
|
|
3269
|
+
},
|
|
3270
|
+
async ({ count, cursor }) => {
|
|
3271
|
+
try {
|
|
3272
|
+
const result = await listSharedDrives(count ?? 20, cursor);
|
|
3273
|
+
return {
|
|
3274
|
+
content: [{ type: "text", text: JSON.stringify({ sharedDrives: result.sharedDrives, count: result.sharedDrives.length, hasMore: !!result.responseMetaData?.nextCursor, nextCursor: result.responseMetaData?.nextCursor ?? null }) }]
|
|
3275
|
+
};
|
|
3276
|
+
} catch (err) {
|
|
3277
|
+
return {
|
|
3278
|
+
content: [{ type: "text", text: mcpErrorHint(err, "drive.sharedrive_list") }],
|
|
3279
|
+
isError: true
|
|
3280
|
+
};
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
);
|
|
3284
|
+
defineTool(
|
|
3285
|
+
server,
|
|
3286
|
+
"nworks_sharedrive_files",
|
|
3287
|
+
"\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)",
|
|
3288
|
+
{
|
|
3289
|
+
sharedriveId: z6.string().describe("\uACF5\uC720 \uB4DC\uB77C\uC774\uBE0C ID (nworks_sharedrive_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
|
|
3290
|
+
folderId: z6.string().optional().describe("\uD3F4\uB354 ID (\uBBF8\uC9C0\uC815 \uC2DC \uB8E8\uD2B8)"),
|
|
3291
|
+
count: z6.number().int().min(1).max(200).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 200)"),
|
|
3292
|
+
cursor: z6.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
|
|
3293
|
+
},
|
|
3294
|
+
async ({ sharedriveId, folderId, count, cursor }) => {
|
|
3295
|
+
try {
|
|
3296
|
+
const result = await listSharedDriveFiles(
|
|
3297
|
+
sharedriveId,
|
|
3298
|
+
folderId,
|
|
3299
|
+
count ?? 20,
|
|
3300
|
+
cursor
|
|
3301
|
+
);
|
|
3302
|
+
const files = result.files.map((f) => ({
|
|
3303
|
+
fileId: f.fileId,
|
|
3304
|
+
name: f.fileName,
|
|
3305
|
+
type: f.fileType,
|
|
3306
|
+
size: f.fileSize,
|
|
3307
|
+
modified: f.modifiedTime,
|
|
3308
|
+
path: f.filePath
|
|
3309
|
+
}));
|
|
3310
|
+
return {
|
|
3311
|
+
content: [{ type: "text", text: JSON.stringify({ files, count: files.length, hasMore: !!result.responseMetaData?.nextCursor, nextCursor: result.responseMetaData?.nextCursor ?? null }) }]
|
|
2759
3312
|
};
|
|
2760
3313
|
} catch (err) {
|
|
2761
|
-
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
2762
|
-
console.error(`[nworks] drive upload error: ${err.stack}`);
|
|
2763
|
-
}
|
|
2764
3314
|
return {
|
|
2765
|
-
content: [{ type: "text", text: mcpErrorHint(err, "drive.
|
|
3315
|
+
content: [{ type: "text", text: mcpErrorHint(err, "drive.sharedrive_files") }],
|
|
2766
3316
|
isError: true
|
|
2767
3317
|
};
|
|
2768
3318
|
}
|
|
2769
3319
|
}
|
|
2770
3320
|
);
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
"
|
|
3321
|
+
defineTool(
|
|
3322
|
+
server,
|
|
3323
|
+
"nworks_sharedrive_download",
|
|
3324
|
+
"\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.",
|
|
2774
3325
|
{
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
3326
|
+
sharedriveId: z6.string().describe("\uACF5\uC720 \uB4DC\uB77C\uC774\uBE0C ID (nworks_sharedrive_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
|
|
3327
|
+
fileId: z6.string().describe("\uB2E4\uC6B4\uB85C\uB4DC\uD560 \uD30C\uC77C ID (nworks_sharedrive_files\uB85C \uC870\uD68C \uAC00\uB2A5)"),
|
|
3328
|
+
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)"),
|
|
3329
|
+
outputName: z6.string().optional().describe("\uC800\uC7A5 \uD30C\uC77C\uBA85 (\uBBF8\uC9C0\uC815 \uC2DC \uC6D0\uBCF8 \uD30C\uC77C\uBA85)")
|
|
2779
3330
|
},
|
|
2780
|
-
async ({ fileId, outputDir, outputName
|
|
3331
|
+
async ({ sharedriveId, fileId, outputDir, outputName }) => {
|
|
2781
3332
|
try {
|
|
2782
|
-
const result = await
|
|
2783
|
-
fileId,
|
|
2784
|
-
userId ?? "me"
|
|
2785
|
-
);
|
|
3333
|
+
const result = await downloadSharedDriveFile(sharedriveId, fileId);
|
|
2786
3334
|
const fileName = outputName ?? result.fileName ?? fileId;
|
|
2787
3335
|
if (outputDir) {
|
|
2788
|
-
const { writeFile:
|
|
2789
|
-
const { join:
|
|
3336
|
+
const { writeFile: writeFile4 } = await import("fs/promises");
|
|
3337
|
+
const { join: join5 } = await import("path");
|
|
2790
3338
|
const safeDir = validateLocalPath(outputDir);
|
|
2791
3339
|
const safeName = sanitizeFileName(fileName);
|
|
2792
|
-
const outPath =
|
|
3340
|
+
const outPath = join5(safeDir, safeName);
|
|
2793
3341
|
validateLocalPath(outPath, safeDir);
|
|
2794
|
-
await
|
|
3342
|
+
await writeFile4(outPath, result.buffer);
|
|
2795
3343
|
return {
|
|
2796
3344
|
content: [{ type: "text", text: JSON.stringify({ success: true, fileName, path: outPath, size: result.buffer.length }) }]
|
|
2797
3345
|
};
|
|
@@ -2818,23 +3366,29 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2818
3366
|
};
|
|
2819
3367
|
} catch (err) {
|
|
2820
3368
|
return {
|
|
2821
|
-
content: [{ type: "text", text: mcpErrorHint(err, "drive.
|
|
3369
|
+
content: [{ type: "text", text: mcpErrorHint(err, "drive.sharedrive_download") }],
|
|
2822
3370
|
isError: true
|
|
2823
3371
|
};
|
|
2824
3372
|
}
|
|
2825
3373
|
}
|
|
2826
3374
|
);
|
|
2827
|
-
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3377
|
+
// src/mcp/tools/mail.ts
|
|
3378
|
+
import { z as z7 } from "zod";
|
|
3379
|
+
function registerMailTools(server) {
|
|
3380
|
+
defineTool(
|
|
3381
|
+
server,
|
|
2828
3382
|
"nworks_mail_send",
|
|
2829
3383
|
"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)",
|
|
2830
3384
|
{
|
|
2831
|
-
to:
|
|
2832
|
-
subject:
|
|
2833
|
-
body:
|
|
2834
|
-
cc:
|
|
2835
|
-
bcc:
|
|
2836
|
-
contentType:
|
|
2837
|
-
userId:
|
|
3385
|
+
to: z7.string().describe("\uC218\uC2E0\uC790 \uC774\uBA54\uC77C (\uC5EC\uB7EC \uBA85\uC740 ; \uB85C \uAD6C\uBD84)"),
|
|
3386
|
+
subject: z7.string().describe("\uBA54\uC77C \uC81C\uBAA9"),
|
|
3387
|
+
body: z7.string().optional().describe("\uBA54\uC77C \uBCF8\uBB38"),
|
|
3388
|
+
cc: z7.string().optional().describe("\uCC38\uC870 \uC774\uBA54\uC77C (\uC5EC\uB7EC \uBA85\uC740 ; \uB85C \uAD6C\uBD84)"),
|
|
3389
|
+
bcc: z7.string().optional().describe("\uC228\uC740\uCC38\uC870 \uC774\uBA54\uC77C (\uC5EC\uB7EC \uBA85\uC740 ; \uB85C \uAD6C\uBD84)"),
|
|
3390
|
+
contentType: z7.enum(["html", "text"]).optional().describe("\uBCF8\uBB38 \uD615\uC2DD (\uAE30\uBCF8: html)"),
|
|
3391
|
+
userId: z7.string().optional().describe("\uBC1C\uC2E0\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
|
|
2838
3392
|
},
|
|
2839
3393
|
async ({ to, subject, body, cc, bcc, contentType, userId }) => {
|
|
2840
3394
|
try {
|
|
@@ -2858,15 +3412,16 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2858
3412
|
}
|
|
2859
3413
|
}
|
|
2860
3414
|
);
|
|
2861
|
-
|
|
3415
|
+
defineTool(
|
|
3416
|
+
server,
|
|
2862
3417
|
"nworks_mail_list",
|
|
2863
3418
|
"\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)",
|
|
2864
3419
|
{
|
|
2865
|
-
folderId:
|
|
2866
|
-
count:
|
|
2867
|
-
cursor:
|
|
2868
|
-
isUnread:
|
|
2869
|
-
userId:
|
|
3420
|
+
folderId: z7.number().int().min(0).optional().describe("\uBA54\uC77C \uD3F4\uB354 ID (\uAE30\uBCF8: 0 = \uBC1B\uC740\uD3B8\uC9C0\uD568)"),
|
|
3421
|
+
count: z7.number().int().min(1).max(200).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 30, \uCD5C\uB300: 200)"),
|
|
3422
|
+
cursor: z7.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C"),
|
|
3423
|
+
isUnread: z7.boolean().optional().describe("\uC77D\uC9C0 \uC54A\uC740 \uBA54\uC77C\uB9CC \uC870\uD68C"),
|
|
3424
|
+
userId: z7.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
|
|
2870
3425
|
},
|
|
2871
3426
|
async ({ folderId, count, cursor, isUnread, userId }) => {
|
|
2872
3427
|
try {
|
|
@@ -2896,12 +3451,13 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2896
3451
|
}
|
|
2897
3452
|
}
|
|
2898
3453
|
);
|
|
2899
|
-
|
|
3454
|
+
defineTool(
|
|
3455
|
+
server,
|
|
2900
3456
|
"nworks_mail_read",
|
|
2901
3457
|
"\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
3458
|
{
|
|
2903
|
-
mailId:
|
|
2904
|
-
userId:
|
|
3459
|
+
mailId: z7.number().int().min(0).describe("\uBA54\uC77C ID (nworks_mail_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
|
|
3460
|
+
userId: z7.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
|
|
2905
3461
|
},
|
|
2906
3462
|
async ({ mailId, userId }) => {
|
|
2907
3463
|
try {
|
|
@@ -2935,15 +3491,80 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2935
3491
|
}
|
|
2936
3492
|
}
|
|
2937
3493
|
);
|
|
2938
|
-
|
|
3494
|
+
defineTool(
|
|
3495
|
+
server,
|
|
3496
|
+
"nworks_mail_download_attachment",
|
|
3497
|
+
"\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.",
|
|
3498
|
+
{
|
|
3499
|
+
mailId: z7.number().int().min(0).describe("\uBA54\uC77C ID (nworks_mail_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
|
|
3500
|
+
attachmentId: z7.number().int().min(0).describe("\uCCA8\uBD80\uD30C\uC77C ID (nworks_mail_read\uB85C \uC870\uD68C \uAC00\uB2A5)"),
|
|
3501
|
+
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)"),
|
|
3502
|
+
outputName: z7.string().optional().describe("\uC800\uC7A5 \uD30C\uC77C\uBA85 (\uBBF8\uC9C0\uC815 \uC2DC \uC6D0\uBCF8 \uD30C\uC77C\uBA85)"),
|
|
3503
|
+
userId: z7.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
|
|
3504
|
+
},
|
|
3505
|
+
async ({ mailId, attachmentId, outputDir, outputName, userId }) => {
|
|
3506
|
+
try {
|
|
3507
|
+
const result = await downloadAttachment(
|
|
3508
|
+
mailId,
|
|
3509
|
+
attachmentId,
|
|
3510
|
+
userId ?? "me"
|
|
3511
|
+
);
|
|
3512
|
+
const fileName = outputName ?? result.filename;
|
|
3513
|
+
if (outputDir) {
|
|
3514
|
+
const { writeFile: writeFile4 } = await import("fs/promises");
|
|
3515
|
+
const { join: join5 } = await import("path");
|
|
3516
|
+
const safeDir = validateLocalPath(outputDir);
|
|
3517
|
+
const safeName = sanitizeFileName(fileName);
|
|
3518
|
+
const outPath = join5(safeDir, safeName);
|
|
3519
|
+
validateLocalPath(outPath, safeDir);
|
|
3520
|
+
await writeFile4(outPath, result.buffer);
|
|
3521
|
+
return {
|
|
3522
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, fileName, path: outPath, size: result.buffer.length }) }]
|
|
3523
|
+
};
|
|
3524
|
+
}
|
|
3525
|
+
const MAX_INLINE_SIZE = 5 * 1024 * 1024;
|
|
3526
|
+
if (result.buffer.length > MAX_INLINE_SIZE) {
|
|
3527
|
+
const sizeMB = (result.buffer.length / (1024 * 1024)).toFixed(1);
|
|
3528
|
+
return {
|
|
3529
|
+
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 }) }],
|
|
3530
|
+
isError: true
|
|
3531
|
+
};
|
|
3532
|
+
}
|
|
3533
|
+
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;
|
|
3534
|
+
const isText = textExtensions.test(fileName);
|
|
3535
|
+
if (isText) {
|
|
3536
|
+
const text = result.buffer.toString("utf-8");
|
|
3537
|
+
return {
|
|
3538
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, fileName, size: result.buffer.length, encoding: "text", content: text }) }]
|
|
3539
|
+
};
|
|
3540
|
+
}
|
|
3541
|
+
const base64 = result.buffer.toString("base64");
|
|
3542
|
+
return {
|
|
3543
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, fileName, size: result.buffer.length, encoding: "base64", content: base64 }) }]
|
|
3544
|
+
};
|
|
3545
|
+
} catch (err) {
|
|
3546
|
+
return {
|
|
3547
|
+
content: [{ type: "text", text: mcpErrorHint(err, "mail.download_attachment") }],
|
|
3548
|
+
isError: true
|
|
3549
|
+
};
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
);
|
|
3553
|
+
}
|
|
3554
|
+
|
|
3555
|
+
// src/mcp/tools/task.ts
|
|
3556
|
+
import { z as z8 } from "zod";
|
|
3557
|
+
function registerTaskTools(server) {
|
|
3558
|
+
defineTool(
|
|
3559
|
+
server,
|
|
2939
3560
|
"nworks_task_list",
|
|
2940
3561
|
"\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
3562
|
{
|
|
2942
|
-
categoryId:
|
|
2943
|
-
status:
|
|
2944
|
-
count:
|
|
2945
|
-
cursor:
|
|
2946
|
-
userId:
|
|
3563
|
+
categoryId: z8.string().optional().describe("\uCE74\uD14C\uACE0\uB9AC ID (\uAE30\uBCF8: default)"),
|
|
3564
|
+
status: z8.enum(["TODO", "ALL"]).optional().describe("\uD544\uD130: TODO \uB610\uB294 ALL (\uAE30\uBCF8: ALL)"),
|
|
3565
|
+
count: z8.number().int().min(1).max(100).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 50, \uCD5C\uB300: 100)"),
|
|
3566
|
+
cursor: z8.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C"),
|
|
3567
|
+
userId: z8.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
|
|
2947
3568
|
},
|
|
2948
3569
|
async ({ categoryId, status, count, cursor, userId }) => {
|
|
2949
3570
|
try {
|
|
@@ -2973,16 +3594,17 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2973
3594
|
}
|
|
2974
3595
|
}
|
|
2975
3596
|
);
|
|
2976
|
-
|
|
3597
|
+
defineTool(
|
|
3598
|
+
server,
|
|
2977
3599
|
"nworks_task_create",
|
|
2978
3600
|
"\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
3601
|
{
|
|
2980
|
-
title:
|
|
2981
|
-
content:
|
|
2982
|
-
dueDate:
|
|
2983
|
-
categoryId:
|
|
2984
|
-
assigneeIds:
|
|
2985
|
-
userId:
|
|
3602
|
+
title: z8.string().describe("\uD560 \uC77C \uC81C\uBAA9"),
|
|
3603
|
+
content: z8.string().optional().describe("\uD560 \uC77C \uB0B4\uC6A9"),
|
|
3604
|
+
dueDate: z8.string().optional().describe("\uB9C8\uAC10\uC77C (YYYY-MM-DD)"),
|
|
3605
|
+
categoryId: z8.string().optional().describe("\uCE74\uD14C\uACE0\uB9AC ID"),
|
|
3606
|
+
assigneeIds: z8.array(z8.string()).optional().describe("\uB2F4\uB2F9\uC790 user ID \uBAA9\uB85D (\uBBF8\uC9C0\uC815 \uC2DC \uC790\uAE30 \uC790\uC2E0)"),
|
|
3607
|
+
userId: z8.string().optional().describe("\uC0DD\uC131\uC790 user ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
|
|
2986
3608
|
},
|
|
2987
3609
|
async ({ title, content, dueDate, categoryId, assigneeIds, userId }) => {
|
|
2988
3610
|
try {
|
|
@@ -3005,15 +3627,16 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3005
3627
|
}
|
|
3006
3628
|
}
|
|
3007
3629
|
);
|
|
3008
|
-
|
|
3630
|
+
defineTool(
|
|
3631
|
+
server,
|
|
3009
3632
|
"nworks_task_update",
|
|
3010
3633
|
"\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
3634
|
{
|
|
3012
|
-
taskId:
|
|
3013
|
-
status:
|
|
3014
|
-
title:
|
|
3015
|
-
content:
|
|
3016
|
-
dueDate:
|
|
3635
|
+
taskId: z8.string().describe("\uD560 \uC77C ID (nworks_task_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
|
|
3636
|
+
status: z8.enum(["done", "todo"]).optional().describe("\uC0C1\uD0DC \uBCC0\uACBD: done(\uC644\uB8CC) \uB610\uB294 todo(\uBBF8\uC644\uB8CC)"),
|
|
3637
|
+
title: z8.string().optional().describe("\uC0C8 \uC81C\uBAA9"),
|
|
3638
|
+
content: z8.string().optional().describe("\uC0C8 \uB0B4\uC6A9"),
|
|
3639
|
+
dueDate: z8.string().optional().describe("\uC0C8 \uB9C8\uAC10\uC77C (YYYY-MM-DD)")
|
|
3017
3640
|
},
|
|
3018
3641
|
async ({ taskId, status, title, content, dueDate }) => {
|
|
3019
3642
|
try {
|
|
@@ -3047,11 +3670,12 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3047
3670
|
}
|
|
3048
3671
|
}
|
|
3049
3672
|
);
|
|
3050
|
-
|
|
3673
|
+
defineTool(
|
|
3674
|
+
server,
|
|
3051
3675
|
"nworks_task_delete",
|
|
3052
3676
|
"\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
3677
|
{
|
|
3054
|
-
taskId:
|
|
3678
|
+
taskId: z8.string().describe("\uC0AD\uC81C\uD560 \uD560 \uC77C ID (nworks_task_list\uB85C \uC870\uD68C \uAC00\uB2A5)")
|
|
3055
3679
|
},
|
|
3056
3680
|
async ({ taskId }) => {
|
|
3057
3681
|
try {
|
|
@@ -3067,12 +3691,18 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3067
3691
|
}
|
|
3068
3692
|
}
|
|
3069
3693
|
);
|
|
3070
|
-
|
|
3694
|
+
}
|
|
3695
|
+
|
|
3696
|
+
// src/mcp/tools/board.ts
|
|
3697
|
+
import { z as z9 } from "zod";
|
|
3698
|
+
function registerBoardTools(server) {
|
|
3699
|
+
defineTool(
|
|
3700
|
+
server,
|
|
3071
3701
|
"nworks_board_list",
|
|
3072
3702
|
"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
3703
|
{
|
|
3074
|
-
count:
|
|
3075
|
-
cursor:
|
|
3704
|
+
count: z9.number().int().min(1).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20)"),
|
|
3705
|
+
cursor: z9.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
|
|
3076
3706
|
},
|
|
3077
3707
|
async ({ count, cursor }) => {
|
|
3078
3708
|
try {
|
|
@@ -3093,13 +3723,14 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3093
3723
|
}
|
|
3094
3724
|
}
|
|
3095
3725
|
);
|
|
3096
|
-
|
|
3726
|
+
defineTool(
|
|
3727
|
+
server,
|
|
3097
3728
|
"nworks_board_posts",
|
|
3098
3729
|
"\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
3730
|
{
|
|
3100
|
-
boardId:
|
|
3101
|
-
count:
|
|
3102
|
-
cursor:
|
|
3731
|
+
boardId: z9.string().describe("\uAC8C\uC2DC\uD310 ID (nworks_board_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
|
|
3732
|
+
count: z9.number().int().min(1).max(40).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 40)"),
|
|
3733
|
+
cursor: z9.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
|
|
3103
3734
|
},
|
|
3104
3735
|
async ({ boardId, count, cursor }) => {
|
|
3105
3736
|
try {
|
|
@@ -3123,12 +3754,13 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3123
3754
|
}
|
|
3124
3755
|
}
|
|
3125
3756
|
);
|
|
3126
|
-
|
|
3757
|
+
defineTool(
|
|
3758
|
+
server,
|
|
3127
3759
|
"nworks_board_read",
|
|
3128
3760
|
"\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
3761
|
{
|
|
3130
|
-
boardId:
|
|
3131
|
-
postId:
|
|
3762
|
+
boardId: z9.string().describe("\uAC8C\uC2DC\uD310 ID (nworks_board_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
|
|
3763
|
+
postId: z9.string().describe("\uAE00 ID (nworks_board_posts\uB85C \uC870\uD68C \uAC00\uB2A5)")
|
|
3132
3764
|
},
|
|
3133
3765
|
async ({ boardId, postId }) => {
|
|
3134
3766
|
try {
|
|
@@ -3154,15 +3786,16 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3154
3786
|
}
|
|
3155
3787
|
}
|
|
3156
3788
|
);
|
|
3157
|
-
|
|
3789
|
+
defineTool(
|
|
3790
|
+
server,
|
|
3158
3791
|
"nworks_board_create",
|
|
3159
3792
|
"\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
3793
|
{
|
|
3161
|
-
boardId:
|
|
3162
|
-
title:
|
|
3163
|
-
body:
|
|
3164
|
-
enableComment:
|
|
3165
|
-
sendNotifications:
|
|
3794
|
+
boardId: z9.string().describe("\uAC8C\uC2DC\uD310 ID (nworks_board_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
|
|
3795
|
+
title: z9.string().describe("\uAE00 \uC81C\uBAA9"),
|
|
3796
|
+
body: z9.string().optional().describe("\uAE00 \uBCF8\uBB38"),
|
|
3797
|
+
enableComment: z9.boolean().optional().describe("\uB313\uAE00 \uD5C8\uC6A9 (\uAE30\uBCF8: true)"),
|
|
3798
|
+
sendNotifications: z9.boolean().optional().describe("\uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)")
|
|
3166
3799
|
},
|
|
3167
3800
|
async ({ boardId, title, body, enableComment, sendNotifications }) => {
|
|
3168
3801
|
try {
|
|
@@ -3184,33 +3817,173 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3184
3817
|
}
|
|
3185
3818
|
}
|
|
3186
3819
|
);
|
|
3187
|
-
|
|
3820
|
+
}
|
|
3821
|
+
|
|
3822
|
+
// src/mcp/tools/contact.ts
|
|
3823
|
+
import { z as z10 } from "zod";
|
|
3824
|
+
function registerContactTools(server) {
|
|
3825
|
+
defineTool(
|
|
3826
|
+
server,
|
|
3827
|
+
"nworks_contact_list",
|
|
3828
|
+
"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)",
|
|
3829
|
+
{
|
|
3830
|
+
userId: z10.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)"),
|
|
3831
|
+
count: z10.number().int().min(1).max(500).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 500)"),
|
|
3832
|
+
cursor: z10.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C"),
|
|
3833
|
+
contactTagId: z10.string().optional().describe("\uC5F0\uB77D\uCC98 \uD0DC\uADF8 ID\uB85C \uD544\uD130\uB9C1 (nworks_contact_list_tags\uB85C \uC870\uD68C \uAC00\uB2A5)"),
|
|
3834
|
+
email: z10.string().optional().describe("\uC774\uBA54\uC77C\uB85C \uD544\uD130\uB9C1"),
|
|
3835
|
+
telephone: z10.string().optional().describe("\uC804\uD654\uBC88\uD638\uB85C \uD544\uD130\uB9C1")
|
|
3836
|
+
},
|
|
3837
|
+
async ({ userId, count, cursor, contactTagId, email, telephone }) => {
|
|
3838
|
+
try {
|
|
3839
|
+
const result = await listContacts({
|
|
3840
|
+
userId: userId ?? "me",
|
|
3841
|
+
count: count ?? 20,
|
|
3842
|
+
cursor,
|
|
3843
|
+
contactTagId,
|
|
3844
|
+
email,
|
|
3845
|
+
telephone
|
|
3846
|
+
});
|
|
3847
|
+
return {
|
|
3848
|
+
content: [{ type: "text", text: JSON.stringify({ contacts: result.contacts, count: result.contacts.length, hasMore: !!result.responseMetaData?.nextCursor, nextCursor: result.responseMetaData?.nextCursor ?? null }) }]
|
|
3849
|
+
};
|
|
3850
|
+
} catch (err) {
|
|
3851
|
+
return {
|
|
3852
|
+
content: [{ type: "text", text: mcpErrorHint(err, "contact.list") }],
|
|
3853
|
+
isError: true
|
|
3854
|
+
};
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
);
|
|
3858
|
+
defineTool(
|
|
3859
|
+
server,
|
|
3860
|
+
"nworks_contact_get",
|
|
3861
|
+
"\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)",
|
|
3862
|
+
{
|
|
3863
|
+
contactId: z10.string().describe("\uC5F0\uB77D\uCC98 ID (nworks_contact_list\uB85C \uC870\uD68C \uAC00\uB2A5)")
|
|
3864
|
+
},
|
|
3865
|
+
async ({ contactId }) => {
|
|
3866
|
+
try {
|
|
3867
|
+
const result = await getContact(contactId);
|
|
3868
|
+
return {
|
|
3869
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
3870
|
+
};
|
|
3871
|
+
} catch (err) {
|
|
3872
|
+
return {
|
|
3873
|
+
content: [{ type: "text", text: mcpErrorHint(err, "contact.get") }],
|
|
3874
|
+
isError: true
|
|
3875
|
+
};
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
);
|
|
3879
|
+
defineTool(
|
|
3880
|
+
server,
|
|
3881
|
+
"nworks_contact_create",
|
|
3882
|
+
`\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)`,
|
|
3883
|
+
{
|
|
3884
|
+
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")
|
|
3885
|
+
},
|
|
3886
|
+
async ({ payload }) => {
|
|
3887
|
+
try {
|
|
3888
|
+
const result = await createContact(payload);
|
|
3889
|
+
return {
|
|
3890
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }]
|
|
3891
|
+
};
|
|
3892
|
+
} catch (err) {
|
|
3893
|
+
return {
|
|
3894
|
+
content: [{ type: "text", text: mcpErrorHint(err, "contact.create") }],
|
|
3895
|
+
isError: true
|
|
3896
|
+
};
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
3899
|
+
);
|
|
3900
|
+
defineTool(
|
|
3901
|
+
server,
|
|
3902
|
+
"nworks_contact_update",
|
|
3903
|
+
"\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)",
|
|
3904
|
+
{
|
|
3905
|
+
contactId: z10.string().describe("\uC5F0\uB77D\uCC98 ID (nworks_contact_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
|
|
3906
|
+
payload: z10.record(z10.string(), z10.unknown()).describe("\uC218\uC815\uD560 \uD544\uB4DC (name, email, tel, company \uB4F1)"),
|
|
3907
|
+
replace: z10.boolean().optional().describe("\uC804\uCCB4 \uAD50\uCCB4(PUT) \uC5EC\uBD80 (\uAE30\uBCF8: false, \uBD80\uBD84 \uC218\uC815 PATCH)")
|
|
3908
|
+
},
|
|
3909
|
+
async ({ contactId, payload, replace }) => {
|
|
3910
|
+
try {
|
|
3911
|
+
const result = await updateContact(contactId, payload, !(replace ?? false));
|
|
3912
|
+
return {
|
|
3913
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }]
|
|
3914
|
+
};
|
|
3915
|
+
} catch (err) {
|
|
3916
|
+
return {
|
|
3917
|
+
content: [{ type: "text", text: mcpErrorHint(err, "contact.update") }],
|
|
3918
|
+
isError: true
|
|
3919
|
+
};
|
|
3920
|
+
}
|
|
3921
|
+
}
|
|
3922
|
+
);
|
|
3923
|
+
defineTool(
|
|
3924
|
+
server,
|
|
3925
|
+
"nworks_contact_delete",
|
|
3926
|
+
"\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)",
|
|
3927
|
+
{
|
|
3928
|
+
contactId: z10.string().describe("\uC0AD\uC81C\uD560 \uC5F0\uB77D\uCC98 ID (nworks_contact_list\uB85C \uC870\uD68C \uAC00\uB2A5)")
|
|
3929
|
+
},
|
|
3930
|
+
async ({ contactId }) => {
|
|
3931
|
+
try {
|
|
3932
|
+
await deleteContact(contactId);
|
|
3933
|
+
return {
|
|
3934
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, contactId, message: "\uC5F0\uB77D\uCC98\uAC00 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4" }) }]
|
|
3935
|
+
};
|
|
3936
|
+
} catch (err) {
|
|
3937
|
+
return {
|
|
3938
|
+
content: [{ type: "text", text: mcpErrorHint(err, "contact.delete") }],
|
|
3939
|
+
isError: true
|
|
3940
|
+
};
|
|
3941
|
+
}
|
|
3942
|
+
}
|
|
3943
|
+
);
|
|
3944
|
+
defineTool(
|
|
3945
|
+
server,
|
|
3946
|
+
"nworks_contact_list_tags",
|
|
3947
|
+
"\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)",
|
|
3948
|
+
{
|
|
3949
|
+
userId: z10.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)"),
|
|
3950
|
+
count: z10.number().int().min(1).max(500).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 500)"),
|
|
3951
|
+
cursor: z10.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
|
|
3952
|
+
},
|
|
3953
|
+
async ({ userId, count, cursor }) => {
|
|
3954
|
+
try {
|
|
3955
|
+
const result = await listContactTags({ userId: userId ?? "me", count: count ?? 20, cursor });
|
|
3956
|
+
return {
|
|
3957
|
+
content: [{ type: "text", text: JSON.stringify({ contactTags: result.contactTags, count: result.contactTags.length, hasMore: !!result.responseMetaData?.nextCursor, nextCursor: result.responseMetaData?.nextCursor ?? null }) }]
|
|
3958
|
+
};
|
|
3959
|
+
} catch (err) {
|
|
3960
|
+
return {
|
|
3961
|
+
content: [{ type: "text", text: mcpErrorHint(err, "contact.list_tags") }],
|
|
3962
|
+
isError: true
|
|
3963
|
+
};
|
|
3964
|
+
}
|
|
3965
|
+
}
|
|
3966
|
+
);
|
|
3967
|
+
}
|
|
3968
|
+
|
|
3969
|
+
// src/mcp/tools/auth.ts
|
|
3970
|
+
import { z as z11 } from "zod";
|
|
3971
|
+
function registerAuthTools(server) {
|
|
3972
|
+
defineTool(
|
|
3973
|
+
server,
|
|
3188
3974
|
"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:
|
|
3975
|
+
"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
3976
|
{
|
|
3191
|
-
|
|
3977
|
+
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."),
|
|
3978
|
+
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
3979
|
},
|
|
3193
|
-
async ({ scope }) => {
|
|
3194
|
-
const DEFAULT_SCOPE = "calendar calendar.read file file.read mail mail.read task task.read user.read board board.read";
|
|
3980
|
+
async ({ preset, scope }) => {
|
|
3195
3981
|
try {
|
|
3196
3982
|
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
3983
|
const existingToken = await loadUserToken();
|
|
3211
3984
|
const existingScopes = existingToken?.scope?.split(" ").filter(Boolean) ?? [];
|
|
3212
|
-
const requestedScopes =
|
|
3213
|
-
const mergedScopes =
|
|
3985
|
+
const requestedScopes = resolveScopes(preset ?? scope);
|
|
3986
|
+
const mergedScopes = mergeScopes(existingScopes, requestedScopes);
|
|
3214
3987
|
const state = generateSecureState();
|
|
3215
3988
|
const authorizeUrl = buildAuthorizeUrl(creds.clientId, mergedScopes, state);
|
|
3216
3989
|
startOAuthCallbackServer(creds.clientId, creds.clientSecret, state).then(
|
|
@@ -3247,7 +4020,8 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3247
4020
|
}
|
|
3248
4021
|
}
|
|
3249
4022
|
);
|
|
3250
|
-
|
|
4023
|
+
defineTool(
|
|
4024
|
+
server,
|
|
3251
4025
|
"nworks_whoami",
|
|
3252
4026
|
"\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
4027
|
{},
|
|
@@ -3258,10 +4032,10 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3258
4032
|
const userToken = await loadUserToken();
|
|
3259
4033
|
const isValid = token ? token.expiresAt > Date.now() / 1e3 : false;
|
|
3260
4034
|
const userTokenValid = userToken ? userToken.expiresAt > Date.now() / 1e3 : false;
|
|
3261
|
-
const
|
|
4035
|
+
const mask2 = (s) => s.length <= 4 ? "****" : `****${s.slice(-4)}`;
|
|
3262
4036
|
const info = {
|
|
3263
4037
|
serviceAccount: creds.serviceAccount ?? null,
|
|
3264
|
-
clientId:
|
|
4038
|
+
clientId: mask2(creds.clientId),
|
|
3265
4039
|
botId: creds.botId ?? null,
|
|
3266
4040
|
tokenValid: isValid,
|
|
3267
4041
|
userOAuth: userToken ? { valid: userTokenValid, scope: userToken.scope } : null
|
|
@@ -3277,7 +4051,8 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3277
4051
|
}
|
|
3278
4052
|
}
|
|
3279
4053
|
);
|
|
3280
|
-
|
|
4054
|
+
defineTool(
|
|
4055
|
+
server,
|
|
3281
4056
|
"nworks_logout",
|
|
3282
4057
|
"\uC800\uC7A5\uB41C NAVER WORKS \uC778\uC99D \uC815\uBCF4\uC640 \uD1A0\uD070\uC744 \uBAA8\uB450 \uC0AD\uC81C\uD569\uB2C8\uB2E4",
|
|
3283
4058
|
{},
|
|
@@ -3315,7 +4090,141 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3315
4090
|
}
|
|
3316
4091
|
}
|
|
3317
4092
|
);
|
|
3318
|
-
|
|
4093
|
+
}
|
|
4094
|
+
|
|
4095
|
+
// src/commands/doctor.ts
|
|
4096
|
+
import { Command as Command12 } from "commander";
|
|
4097
|
+
import { existsSync as existsSync3 } from "fs";
|
|
4098
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
4099
|
+
import { resolve as resolve2, join as join4, sep as sep2 } from "path";
|
|
4100
|
+
function mask(value) {
|
|
4101
|
+
return value.length <= 4 ? "****" : `****${value.slice(-4)}`;
|
|
4102
|
+
}
|
|
4103
|
+
async function runChecks(profile) {
|
|
4104
|
+
const results = [];
|
|
4105
|
+
let creds;
|
|
4106
|
+
try {
|
|
4107
|
+
creds = await loadCredentials(profile);
|
|
4108
|
+
results.push({ check: "credentials", status: "OK", detail: `clientId: ${mask(creds.clientId)}` });
|
|
4109
|
+
} catch {
|
|
4110
|
+
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)." });
|
|
4111
|
+
return results;
|
|
4112
|
+
}
|
|
4113
|
+
if (hasServiceAccountCreds(creds)) {
|
|
4114
|
+
results.push({ check: "serviceAccount", status: "OK", detail: creds.serviceAccount });
|
|
4115
|
+
} else {
|
|
4116
|
+
results.push({ check: "serviceAccount", status: "SKIP", detail: "Not set (required for bot messaging)" });
|
|
4117
|
+
}
|
|
4118
|
+
if (creds.privateKeyPath) {
|
|
4119
|
+
if (existsSync3(creds.privateKeyPath)) {
|
|
4120
|
+
try {
|
|
4121
|
+
await readFile5(creds.privateKeyPath, "utf-8");
|
|
4122
|
+
results.push({ check: "privateKey", status: "OK", detail: creds.privateKeyPath });
|
|
4123
|
+
} catch {
|
|
4124
|
+
results.push({ check: "privateKey", status: "FAIL", detail: `Cannot read: ${creds.privateKeyPath}` });
|
|
4125
|
+
}
|
|
4126
|
+
} else {
|
|
4127
|
+
results.push({ check: "privateKey", status: "FAIL", detail: `File not found: ${creds.privateKeyPath}` });
|
|
4128
|
+
}
|
|
4129
|
+
} else {
|
|
4130
|
+
results.push({ check: "privateKey", status: "SKIP", detail: "Not set" });
|
|
4131
|
+
}
|
|
4132
|
+
if (creds.privateKeyPath && existsSync3(creds.privateKeyPath)) {
|
|
4133
|
+
const resolvedKey = resolve2(creds.privateKeyPath);
|
|
4134
|
+
const cwd = resolve2(process.cwd());
|
|
4135
|
+
const insideCwd = resolvedKey === cwd || resolvedKey.startsWith(cwd + sep2);
|
|
4136
|
+
const looksLikeProject = existsSync3(join4(cwd, ".git")) || existsSync3(join4(cwd, "package.json"));
|
|
4137
|
+
if (insideCwd && looksLikeProject) {
|
|
4138
|
+
results.push({
|
|
4139
|
+
check: "keyLocation",
|
|
4140
|
+
status: "WARN",
|
|
4141
|
+
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."
|
|
4142
|
+
});
|
|
4143
|
+
}
|
|
4144
|
+
}
|
|
4145
|
+
if (creds.botId) {
|
|
4146
|
+
results.push({ check: "botId", status: "OK", detail: creds.botId });
|
|
4147
|
+
} else {
|
|
4148
|
+
results.push({ check: "botId", status: "SKIP", detail: "Not set (required to send messages)" });
|
|
4149
|
+
}
|
|
4150
|
+
const token = await loadToken(profile);
|
|
4151
|
+
if (token) {
|
|
4152
|
+
const valid = token.expiresAt > Date.now() / 1e3;
|
|
4153
|
+
results.push({
|
|
4154
|
+
check: "serviceToken",
|
|
4155
|
+
status: valid ? "OK" : "EXPIRED",
|
|
4156
|
+
detail: valid ? `expires: ${new Date(token.expiresAt * 1e3).toISOString()}` : `expired: ${new Date(token.expiresAt * 1e3).toISOString()}`
|
|
4157
|
+
});
|
|
4158
|
+
} else {
|
|
4159
|
+
results.push({ check: "serviceToken", status: "SKIP", detail: "No token" });
|
|
4160
|
+
}
|
|
4161
|
+
const userToken = await loadUserToken(profile);
|
|
4162
|
+
if (userToken) {
|
|
4163
|
+
const valid = userToken.expiresAt > Date.now() / 1e3;
|
|
4164
|
+
results.push({
|
|
4165
|
+
check: "userOAuth",
|
|
4166
|
+
status: valid ? "OK" : "EXPIRED",
|
|
4167
|
+
detail: valid ? `scope: ${userToken.scope} | expires: ${new Date(userToken.expiresAt * 1e3).toISOString()}` : `expired | scope: ${userToken.scope}`
|
|
4168
|
+
});
|
|
4169
|
+
} else {
|
|
4170
|
+
results.push({ check: "userOAuth", status: "SKIP", detail: "No token. Run `nworks login --user`." });
|
|
4171
|
+
}
|
|
4172
|
+
if (hasServiceAccountCreds(creds) && token && token.expiresAt > Date.now() / 1e3) {
|
|
4173
|
+
try {
|
|
4174
|
+
const res = await fetch("https://www.worksapis.com/v1.0/users/me", {
|
|
4175
|
+
headers: { Authorization: `Bearer ${token.accessToken}` }
|
|
4176
|
+
});
|
|
4177
|
+
if (res.ok) {
|
|
4178
|
+
results.push({ check: "apiConnection", status: "OK", detail: "Connected to NAVER WORKS API" });
|
|
4179
|
+
} else {
|
|
4180
|
+
results.push({ check: "apiConnection", status: "FAIL", detail: `HTTP ${res.status}` });
|
|
4181
|
+
}
|
|
4182
|
+
} catch (e) {
|
|
4183
|
+
results.push({ check: "apiConnection", status: "FAIL", detail: `Connection failed: ${e.message}` });
|
|
4184
|
+
}
|
|
4185
|
+
} else if (userToken && userToken.expiresAt > Date.now() / 1e3) {
|
|
4186
|
+
try {
|
|
4187
|
+
const res = await fetch("https://www.worksapis.com/v1.0/users/me", {
|
|
4188
|
+
headers: { Authorization: `Bearer ${userToken.accessToken}` }
|
|
4189
|
+
});
|
|
4190
|
+
if (res.ok) {
|
|
4191
|
+
results.push({ check: "apiConnection", status: "OK", detail: "Connected to NAVER WORKS API" });
|
|
4192
|
+
} else {
|
|
4193
|
+
results.push({ check: "apiConnection", status: "FAIL", detail: `HTTP ${res.status}` });
|
|
4194
|
+
}
|
|
4195
|
+
} catch (e) {
|
|
4196
|
+
results.push({ check: "apiConnection", status: "FAIL", detail: `Connection failed: ${e.message}` });
|
|
4197
|
+
}
|
|
4198
|
+
} else {
|
|
4199
|
+
results.push({ check: "apiConnection", status: "SKIP", detail: "No valid token \u2014 skipping API test" });
|
|
4200
|
+
}
|
|
4201
|
+
return results;
|
|
4202
|
+
}
|
|
4203
|
+
var doctorCommand = new Command12("doctor").description("Check nworks configuration and connectivity").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
|
|
4204
|
+
try {
|
|
4205
|
+
const results = await runChecks(opts.profile);
|
|
4206
|
+
if (opts.json || !process.stdout.isTTY) {
|
|
4207
|
+
output(results, opts);
|
|
4208
|
+
} else {
|
|
4209
|
+
console.log("\n nworks doctor\n");
|
|
4210
|
+
for (const r of results) {
|
|
4211
|
+
const icon = r.status === "OK" ? "\u2705" : r.status === "SKIP" ? "\u2796" : r.status === "WARN" ? "\u26A0\uFE0F" : "\u274C";
|
|
4212
|
+
console.log(` ${icon} ${r.check.padEnd(16)} ${r.detail}`);
|
|
4213
|
+
}
|
|
4214
|
+
console.log();
|
|
4215
|
+
}
|
|
4216
|
+
if (results.some((r) => r.status === "FAIL")) {
|
|
4217
|
+
process.exitCode = 1;
|
|
4218
|
+
}
|
|
4219
|
+
} catch (err) {
|
|
4220
|
+
cliError(err, opts);
|
|
4221
|
+
}
|
|
4222
|
+
});
|
|
4223
|
+
|
|
4224
|
+
// src/mcp/tools/diagnostics.ts
|
|
4225
|
+
function registerDiagnosticsTools(server) {
|
|
4226
|
+
defineTool(
|
|
4227
|
+
server,
|
|
3319
4228
|
"nworks_doctor",
|
|
3320
4229
|
"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
4230
|
{},
|
|
@@ -3348,6 +4257,21 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3348
4257
|
);
|
|
3349
4258
|
}
|
|
3350
4259
|
|
|
4260
|
+
// src/mcp/tools.ts
|
|
4261
|
+
function registerTools(server) {
|
|
4262
|
+
registerSetupTools(server);
|
|
4263
|
+
registerMessageTools(server);
|
|
4264
|
+
registerDirectoryTools(server);
|
|
4265
|
+
registerCalendarTools(server);
|
|
4266
|
+
registerDriveTools(server);
|
|
4267
|
+
registerMailTools(server);
|
|
4268
|
+
registerTaskTools(server);
|
|
4269
|
+
registerBoardTools(server);
|
|
4270
|
+
registerContactTools(server);
|
|
4271
|
+
registerAuthTools(server);
|
|
4272
|
+
registerDiagnosticsTools(server);
|
|
4273
|
+
}
|
|
4274
|
+
|
|
3351
4275
|
// src/mcp/server.ts
|
|
3352
4276
|
async function startMcpServer() {
|
|
3353
4277
|
try {
|
|
@@ -3368,7 +4292,7 @@ async function startMcpServer() {
|
|
|
3368
4292
|
}
|
|
3369
4293
|
const server = new McpServer({
|
|
3370
4294
|
name: "nworks",
|
|
3371
|
-
version:
|
|
4295
|
+
version: VERSION
|
|
3372
4296
|
});
|
|
3373
4297
|
registerTools(server);
|
|
3374
4298
|
const transport = new StdioServerTransport();
|
|
@@ -3376,22 +4300,31 @@ async function startMcpServer() {
|
|
|
3376
4300
|
}
|
|
3377
4301
|
|
|
3378
4302
|
// src/commands/mcp-cmd.ts
|
|
3379
|
-
|
|
4303
|
+
function listRegisteredTools() {
|
|
4304
|
+
const server = new McpServer2({ name: "nworks", version: "0.0.0" });
|
|
4305
|
+
registerTools(server);
|
|
4306
|
+
const registered = server._registeredTools;
|
|
4307
|
+
return Object.entries(registered).map(([name, tool]) => ({
|
|
4308
|
+
name,
|
|
4309
|
+
summary: (tool.description ?? "").split("\n")[0]?.trim() ?? ""
|
|
4310
|
+
}));
|
|
4311
|
+
}
|
|
4312
|
+
var mcpCommand = new Command13("mcp").description("Start MCP server (stdio transport)").option("--list-tools", "List available MCP tools").action(async (opts) => {
|
|
3380
4313
|
if (opts.listTools) {
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
4314
|
+
const tools = listRegisteredTools();
|
|
4315
|
+
const width = tools.length ? Math.max(...tools.map((t) => t.name.length)) : 0;
|
|
4316
|
+
for (const { name, summary } of tools) {
|
|
4317
|
+
console.log(`${name.padEnd(width)} ${summary}`);
|
|
4318
|
+
}
|
|
4319
|
+
console.log(`
|
|
4320
|
+
${tools.length} tools`);
|
|
3386
4321
|
return;
|
|
3387
4322
|
}
|
|
3388
4323
|
await startMcpServer();
|
|
3389
4324
|
});
|
|
3390
4325
|
|
|
3391
4326
|
// 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");
|
|
4327
|
+
var program = new Command14().name("nworks").description("NAVER WORKS CLI \u2014 built for humans and AI agents").version(VERSION);
|
|
3395
4328
|
program.addCommand(loginCommand);
|
|
3396
4329
|
program.addCommand(logoutCommand);
|
|
3397
4330
|
program.addCommand(whoamiCommand);
|
|
@@ -3402,6 +4335,7 @@ program.addCommand(driveCommand);
|
|
|
3402
4335
|
program.addCommand(mailCommand);
|
|
3403
4336
|
program.addCommand(taskCommand);
|
|
3404
4337
|
program.addCommand(boardCommand);
|
|
4338
|
+
program.addCommand(contactCommand);
|
|
3405
4339
|
program.addCommand(mcpCommand);
|
|
3406
4340
|
program.addCommand(doctorCommand);
|
|
3407
4341
|
program.parse();
|