nworks 1.2.1 → 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/README.ja.md +71 -5
- package/README.ko.md +71 -5
- package/README.md +71 -5
- package/dist/index.js +1685 -550
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +2096 -1177
- package/dist/mcp.js.map +1 -1
- package/package.json +5 -2
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 {
|
|
@@ -38,15 +44,45 @@ var ApiError = class extends Error {
|
|
|
38
44
|
function hasServiceAccountCreds(creds) {
|
|
39
45
|
return !!(creds.serviceAccount && creds.privateKeyPath && creds.botId);
|
|
40
46
|
}
|
|
47
|
+
var IS_UNIX = process.platform !== "win32";
|
|
41
48
|
var CONFIG_DIR = join(homedir(), ".config", "nworks");
|
|
42
49
|
var CREDENTIALS_PATH = join(CONFIG_DIR, "credentials.json");
|
|
43
50
|
var TOKEN_PATH = join(CONFIG_DIR, "token.json");
|
|
44
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();
|
|
45
70
|
async function ensureConfigDir() {
|
|
46
71
|
if (!existsSync(CONFIG_DIR)) {
|
|
47
|
-
await mkdir(CONFIG_DIR, { recursive: true });
|
|
72
|
+
await mkdir(CONFIG_DIR, { recursive: true, ...IS_UNIX && { mode: 448 } });
|
|
73
|
+
} else if (IS_UNIX) {
|
|
74
|
+
await chmod(CONFIG_DIR, 448);
|
|
48
75
|
}
|
|
49
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
|
+
}
|
|
50
86
|
function getCredentialsFromEnv() {
|
|
51
87
|
const clientId = process.env["NWORKS_CLIENT_ID"];
|
|
52
88
|
const clientSecret = process.env["NWORKS_CLIENT_SECRET"];
|
|
@@ -63,6 +99,7 @@ function getCredentialsFromEnv() {
|
|
|
63
99
|
async function loadCredentials(profile = "default") {
|
|
64
100
|
const envCreds = getCredentialsFromEnv();
|
|
65
101
|
if (envCreds) return envCreds;
|
|
102
|
+
validateProfileName(profile);
|
|
66
103
|
if (!existsSync(CREDENTIALS_PATH)) {
|
|
67
104
|
throw new AuthError(
|
|
68
105
|
"Not logged in. Run `nworks login` or set environment variables."
|
|
@@ -74,9 +111,16 @@ async function loadCredentials(profile = "default") {
|
|
|
74
111
|
if (!creds) {
|
|
75
112
|
throw new AuthError(`Profile "${profile}" not found in credentials.`);
|
|
76
113
|
}
|
|
77
|
-
|
|
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;
|
|
78
121
|
}
|
|
79
122
|
async function saveCredentials(creds, profile = "default") {
|
|
123
|
+
validateProfileName(profile);
|
|
80
124
|
await ensureConfigDir();
|
|
81
125
|
let profiles = {};
|
|
82
126
|
if (existsSync(CREDENTIALS_PATH)) {
|
|
@@ -84,9 +128,10 @@ async function saveCredentials(creds, profile = "default") {
|
|
|
84
128
|
profiles = JSON.parse(raw);
|
|
85
129
|
}
|
|
86
130
|
profiles[profile] = creds;
|
|
87
|
-
await
|
|
131
|
+
await writeSecureFile(CREDENTIALS_PATH, JSON.stringify(profiles, null, 2));
|
|
88
132
|
}
|
|
89
133
|
async function loadToken(profile = "default") {
|
|
134
|
+
validateProfileName(profile);
|
|
90
135
|
if (!existsSync(TOKEN_PATH)) return null;
|
|
91
136
|
const raw = await readFile(TOKEN_PATH, "utf-8");
|
|
92
137
|
const tokens = JSON.parse(raw);
|
|
@@ -98,6 +143,7 @@ async function loadToken(profile = "default") {
|
|
|
98
143
|
};
|
|
99
144
|
}
|
|
100
145
|
async function saveToken(token, profile = "default") {
|
|
146
|
+
validateProfileName(profile);
|
|
101
147
|
await ensureConfigDir();
|
|
102
148
|
let tokens = {};
|
|
103
149
|
if (existsSync(TOKEN_PATH)) {
|
|
@@ -105,9 +151,10 @@ async function saveToken(token, profile = "default") {
|
|
|
105
151
|
tokens = JSON.parse(raw);
|
|
106
152
|
}
|
|
107
153
|
tokens[profile] = token;
|
|
108
|
-
await
|
|
154
|
+
await writeSecureFile(TOKEN_PATH, JSON.stringify(tokens, null, 2));
|
|
109
155
|
}
|
|
110
156
|
async function loadUserToken(profile = "default") {
|
|
157
|
+
validateProfileName(profile);
|
|
111
158
|
if (!existsSync(USER_TOKEN_PATH)) return null;
|
|
112
159
|
const raw = await readFile(USER_TOKEN_PATH, "utf-8");
|
|
113
160
|
const tokens = JSON.parse(raw);
|
|
@@ -121,6 +168,7 @@ async function loadUserToken(profile = "default") {
|
|
|
121
168
|
};
|
|
122
169
|
}
|
|
123
170
|
async function saveUserToken(token, profile = "default") {
|
|
171
|
+
validateProfileName(profile);
|
|
124
172
|
await ensureConfigDir();
|
|
125
173
|
let tokens = {};
|
|
126
174
|
if (existsSync(USER_TOKEN_PATH)) {
|
|
@@ -128,35 +176,32 @@ async function saveUserToken(token, profile = "default") {
|
|
|
128
176
|
tokens = JSON.parse(raw);
|
|
129
177
|
}
|
|
130
178
|
tokens[profile] = token;
|
|
131
|
-
await
|
|
179
|
+
await writeSecureFile(USER_TOKEN_PATH, JSON.stringify(tokens, null, 2));
|
|
132
180
|
}
|
|
133
181
|
async function clearCredentials(profile = "default") {
|
|
182
|
+
validateProfileName(profile);
|
|
134
183
|
if (existsSync(CREDENTIALS_PATH)) {
|
|
135
184
|
const raw = await readFile(CREDENTIALS_PATH, "utf-8");
|
|
136
185
|
const profiles = JSON.parse(raw);
|
|
137
186
|
delete profiles[profile];
|
|
138
|
-
await
|
|
139
|
-
CREDENTIALS_PATH,
|
|
140
|
-
JSON.stringify(profiles, null, 2),
|
|
141
|
-
"utf-8"
|
|
142
|
-
);
|
|
187
|
+
await writeSecureFile(CREDENTIALS_PATH, JSON.stringify(profiles, null, 2));
|
|
143
188
|
}
|
|
144
189
|
if (existsSync(TOKEN_PATH)) {
|
|
145
190
|
const raw = await readFile(TOKEN_PATH, "utf-8");
|
|
146
191
|
const tokens = JSON.parse(raw);
|
|
147
192
|
delete tokens[profile];
|
|
148
|
-
await
|
|
193
|
+
await writeSecureFile(TOKEN_PATH, JSON.stringify(tokens, null, 2));
|
|
149
194
|
}
|
|
150
195
|
if (existsSync(USER_TOKEN_PATH)) {
|
|
151
196
|
const raw = await readFile(USER_TOKEN_PATH, "utf-8");
|
|
152
197
|
const tokens = JSON.parse(raw);
|
|
153
198
|
delete tokens[profile];
|
|
154
|
-
await
|
|
199
|
+
await writeSecureFile(USER_TOKEN_PATH, JSON.stringify(tokens, null, 2));
|
|
155
200
|
}
|
|
156
201
|
}
|
|
157
202
|
|
|
158
203
|
// src/auth/jwt.ts
|
|
159
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
204
|
+
import { readFile as readFile2, stat } from "fs/promises";
|
|
160
205
|
import jwt from "jsonwebtoken";
|
|
161
206
|
async function createJWT(creds) {
|
|
162
207
|
if (!creds.serviceAccount || !creds.privateKeyPath) {
|
|
@@ -165,6 +210,15 @@ async function createJWT(creds) {
|
|
|
165
210
|
);
|
|
166
211
|
}
|
|
167
212
|
const privateKey = await readFile2(creds.privateKeyPath, "utf-8");
|
|
213
|
+
if (process.platform !== "win32") {
|
|
214
|
+
const fileStat = await stat(creds.privateKeyPath);
|
|
215
|
+
const mode = fileStat.mode & 511;
|
|
216
|
+
if (mode & 63) {
|
|
217
|
+
console.error(
|
|
218
|
+
`[nworks] Warning: Private key file has permissions ${mode.toString(8)}. Recommended: 600`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
168
222
|
const now = Math.floor(Date.now() / 1e3);
|
|
169
223
|
const payload = {
|
|
170
224
|
iss: creds.clientId,
|
|
@@ -201,7 +255,8 @@ async function refreshToken(profile = "default") {
|
|
|
201
255
|
});
|
|
202
256
|
if (!res.ok) {
|
|
203
257
|
const text = await res.text();
|
|
204
|
-
|
|
258
|
+
const truncated = text.length > 200 ? text.substring(0, 200) + "..." : text;
|
|
259
|
+
throw new AuthError(`Token exchange failed (${res.status}): ${truncated}`);
|
|
205
260
|
}
|
|
206
261
|
const data = await res.json();
|
|
207
262
|
const expiresIn = Number(data.expires_in);
|
|
@@ -215,7 +270,7 @@ async function refreshToken(profile = "default") {
|
|
|
215
270
|
|
|
216
271
|
// src/auth/oauth-user.ts
|
|
217
272
|
import { createServer } from "http";
|
|
218
|
-
import { URL } from "url";
|
|
273
|
+
import { URL as URL2 } from "url";
|
|
219
274
|
var AUTH_URL2 = "https://auth.worksmobile.com/oauth2/v2.0/authorize";
|
|
220
275
|
var TOKEN_URL = "https://auth.worksmobile.com/oauth2/v2.0/token";
|
|
221
276
|
var REDIRECT_PORT = 9876;
|
|
@@ -230,19 +285,19 @@ function buildAuthorizeUrl(clientId, scope, state) {
|
|
|
230
285
|
});
|
|
231
286
|
return `${AUTH_URL2}?${params.toString()}`;
|
|
232
287
|
}
|
|
233
|
-
async function startUserOAuthFlow(_scope, profile
|
|
288
|
+
async function startUserOAuthFlow(_scope, profile, expectedState) {
|
|
234
289
|
const creds = await loadCredentials(profile);
|
|
235
|
-
const code = await waitForAuthCode();
|
|
290
|
+
const code = await waitForAuthCode(expectedState);
|
|
236
291
|
return exchangeCodeForToken(code, creds.clientId, creds.clientSecret);
|
|
237
292
|
}
|
|
238
|
-
function waitForAuthCode() {
|
|
239
|
-
return new Promise((
|
|
293
|
+
function waitForAuthCode(expectedState) {
|
|
294
|
+
return new Promise((resolve3, reject) => {
|
|
240
295
|
const timeout = setTimeout(() => {
|
|
241
296
|
server.close();
|
|
242
297
|
reject(new AuthError("OAuth login timed out (120s). Try again."));
|
|
243
298
|
}, 12e4);
|
|
244
299
|
const server = createServer((req, res) => {
|
|
245
|
-
const url = new
|
|
300
|
+
const url = new URL2(req.url ?? "/", `http://localhost:${REDIRECT_PORT}`);
|
|
246
301
|
if (url.pathname !== "/callback") {
|
|
247
302
|
res.writeHead(404);
|
|
248
303
|
res.end("Not found");
|
|
@@ -250,6 +305,12 @@ function waitForAuthCode() {
|
|
|
250
305
|
}
|
|
251
306
|
const code = url.searchParams.get("code");
|
|
252
307
|
const error = url.searchParams.get("error");
|
|
308
|
+
const state = url.searchParams.get("state");
|
|
309
|
+
if (state !== expectedState) {
|
|
310
|
+
res.writeHead(403, { "Content-Type": "text/html; charset=utf-8" });
|
|
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>");
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
253
314
|
if (error) {
|
|
254
315
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
255
316
|
res.end("<h2>\uB85C\uADF8\uC778 \uC2E4\uD328</h2><p>\uC774 \uCC3D\uC744 \uB2EB\uC544\uB3C4 \uB429\uB2C8\uB2E4.</p>");
|
|
@@ -270,9 +331,9 @@ function waitForAuthCode() {
|
|
|
270
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>");
|
|
271
332
|
clearTimeout(timeout);
|
|
272
333
|
server.close();
|
|
273
|
-
|
|
334
|
+
resolve3(code);
|
|
274
335
|
});
|
|
275
|
-
server.listen(REDIRECT_PORT, () => {
|
|
336
|
+
server.listen(REDIRECT_PORT, "127.0.0.1", () => {
|
|
276
337
|
});
|
|
277
338
|
server.on("error", (err) => {
|
|
278
339
|
clearTimeout(timeout);
|
|
@@ -280,6 +341,17 @@ function waitForAuthCode() {
|
|
|
280
341
|
});
|
|
281
342
|
});
|
|
282
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
|
+
}
|
|
283
355
|
async function exchangeCodeForToken(code, clientId, clientSecret) {
|
|
284
356
|
const body = new URLSearchParams({
|
|
285
357
|
grant_type: "authorization_code",
|
|
@@ -294,8 +366,7 @@ async function exchangeCodeForToken(code, clientId, clientSecret) {
|
|
|
294
366
|
body: body.toString()
|
|
295
367
|
});
|
|
296
368
|
if (!res.ok) {
|
|
297
|
-
|
|
298
|
-
throw new AuthError(`Token exchange failed (${res.status}): ${text}`);
|
|
369
|
+
throw new AuthError(`Token exchange failed (${res.status}): ${await safeErrorSummary(res)}`);
|
|
299
370
|
}
|
|
300
371
|
const data = await res.json();
|
|
301
372
|
return {
|
|
@@ -319,8 +390,7 @@ async function refreshUserToken(refreshToken2, profile = "default") {
|
|
|
319
390
|
body: body.toString()
|
|
320
391
|
});
|
|
321
392
|
if (!res.ok) {
|
|
322
|
-
|
|
323
|
-
throw new AuthError(`Token refresh failed (${res.status}): ${text}`);
|
|
393
|
+
throw new AuthError(`Token refresh failed (${res.status}): ${await safeErrorSummary(res)}`);
|
|
324
394
|
}
|
|
325
395
|
const data = await res.json();
|
|
326
396
|
return {
|
|
@@ -330,11 +400,91 @@ async function refreshUserToken(refreshToken2, profile = "default") {
|
|
|
330
400
|
scope: data.scope
|
|
331
401
|
};
|
|
332
402
|
}
|
|
333
|
-
function startOAuthCallbackServer(clientId, clientSecret) {
|
|
334
|
-
return waitForAuthCode().then(
|
|
403
|
+
function startOAuthCallbackServer(clientId, clientSecret, expectedState) {
|
|
404
|
+
return waitForAuthCode(expectedState).then(
|
|
335
405
|
(code) => exchangeCodeForToken(code, clientId, clientSecret)
|
|
336
406
|
);
|
|
337
407
|
}
|
|
408
|
+
async function revokeToken(token, clientId, clientSecret) {
|
|
409
|
+
try {
|
|
410
|
+
const res = await fetch("https://auth.worksmobile.com/oauth2/v2.0/revoke", {
|
|
411
|
+
method: "POST",
|
|
412
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
413
|
+
body: new URLSearchParams({
|
|
414
|
+
token,
|
|
415
|
+
client_id: clientId,
|
|
416
|
+
client_secret: clientSecret
|
|
417
|
+
}).toString()
|
|
418
|
+
});
|
|
419
|
+
if (!res.ok && process.env["NWORKS_VERBOSE"] === "1") {
|
|
420
|
+
console.error(`[nworks] Token revoke returned ${res.status}`);
|
|
421
|
+
}
|
|
422
|
+
} catch {
|
|
423
|
+
}
|
|
424
|
+
}
|
|
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
|
+
}
|
|
338
488
|
|
|
339
489
|
// src/output/format.ts
|
|
340
490
|
function output(data, opts = {}) {
|
|
@@ -422,11 +572,24 @@ async function prompt(question) {
|
|
|
422
572
|
rl.close();
|
|
423
573
|
}
|
|
424
574
|
}
|
|
425
|
-
|
|
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) => {
|
|
426
589
|
try {
|
|
427
590
|
const profile = opts.profile;
|
|
428
591
|
if (opts.user) {
|
|
429
|
-
await handleUserLogin(
|
|
592
|
+
await handleUserLogin(profile, opts);
|
|
430
593
|
} else {
|
|
431
594
|
await handleServiceAccountLogin(opts);
|
|
432
595
|
}
|
|
@@ -436,7 +599,7 @@ var loginCommand = new Command("login").description("Authenticate with NAVER WOR
|
|
|
436
599
|
process.exitCode = 1;
|
|
437
600
|
}
|
|
438
601
|
});
|
|
439
|
-
async function handleUserLogin(
|
|
602
|
+
async function handleUserLogin(profile, opts) {
|
|
440
603
|
let clientId = opts.clientId;
|
|
441
604
|
let clientSecret = opts.clientSecret;
|
|
442
605
|
try {
|
|
@@ -447,7 +610,7 @@ async function handleUserLogin(scope, profile, opts) {
|
|
|
447
610
|
}
|
|
448
611
|
if (process.stdin.isTTY) {
|
|
449
612
|
if (!clientId) clientId = await prompt("Client ID: ");
|
|
450
|
-
if (!clientSecret) clientSecret = await
|
|
613
|
+
if (!clientSecret) clientSecret = await promptSecret("Client Secret: ");
|
|
451
614
|
}
|
|
452
615
|
if (!clientId || !clientSecret) {
|
|
453
616
|
throw new Error(
|
|
@@ -460,22 +623,31 @@ async function handleUserLogin(scope, profile, opts) {
|
|
|
460
623
|
} catch {
|
|
461
624
|
await saveCredentials({ clientId, clientSecret }, profile);
|
|
462
625
|
}
|
|
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);
|
|
463
634
|
const state = randomBytes(16).toString("hex");
|
|
464
|
-
const authorizeUrl = buildAuthorizeUrl(clientId,
|
|
635
|
+
const authorizeUrl = buildAuthorizeUrl(clientId, mergedScope, state);
|
|
465
636
|
console.error(`
|
|
466
637
|
Opening browser for NAVER WORKS login...`);
|
|
467
638
|
console.error(`If the browser does not open, visit this URL:
|
|
468
639
|
`);
|
|
469
640
|
console.error(` ${authorizeUrl}
|
|
470
641
|
`);
|
|
471
|
-
const {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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]);
|
|
475
647
|
} else {
|
|
476
|
-
|
|
648
|
+
execFile("xdg-open", [authorizeUrl]);
|
|
477
649
|
}
|
|
478
|
-
const tokenData = await startUserOAuthFlow(
|
|
650
|
+
const tokenData = await startUserOAuthFlow(mergedScope, profile, state);
|
|
479
651
|
await saveUserToken(tokenData, profile);
|
|
480
652
|
output(
|
|
481
653
|
{
|
|
@@ -502,7 +674,7 @@ async function handleServiceAccountLogin(opts) {
|
|
|
502
674
|
if (!botId) botId = process.env["NWORKS_BOT_ID"];
|
|
503
675
|
if (process.stdin.isTTY) {
|
|
504
676
|
if (!clientId) clientId = await prompt("Client ID: ");
|
|
505
|
-
if (!clientSecret) clientSecret = await
|
|
677
|
+
if (!clientSecret) clientSecret = await promptSecret("Client Secret: ");
|
|
506
678
|
if (!serviceAccount)
|
|
507
679
|
serviceAccount = await prompt("Service Account: ");
|
|
508
680
|
if (!privateKeyPath)
|
|
@@ -564,10 +736,10 @@ var REQUIRED_SCOPES = {
|
|
|
564
736
|
"board.create": "board"
|
|
565
737
|
};
|
|
566
738
|
var ERROR_HINTS_CLI = {
|
|
567
|
-
FORBIDDEN: "
|
|
568
|
-
ACCESS_DENIED: "
|
|
569
|
-
SERVICE_ACCOUNT_NOT_ALLOWED: "
|
|
570
|
-
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`."
|
|
571
743
|
};
|
|
572
744
|
var ERROR_HINTS_MCP = {
|
|
573
745
|
FORBIDDEN: "\uAD8C\uD55C\uC774 \uBD80\uC871\uD569\uB2C8\uB2E4. Developer Console\uC5D0\uC11C OAuth Scope\uB97C \uD655\uC778\uD558\uC138\uC694.",
|
|
@@ -615,7 +787,7 @@ function buildScopeHint(area, mode) {
|
|
|
615
787
|
const scopes = scope.split(" ").join(", ");
|
|
616
788
|
if (mode === "cli") {
|
|
617
789
|
return `
|
|
618
|
-
\u2192
|
|
790
|
+
\u2192 This command requires the ${scopes} scope(s). Run \`nworks login --user --scope "${scope}"\`.`;
|
|
619
791
|
}
|
|
620
792
|
return `
|
|
621
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).`;
|
|
@@ -652,6 +824,18 @@ function cliError(err, opts = {}, area) {
|
|
|
652
824
|
var logoutCommand = new Command2("logout").description("Remove stored credentials and tokens").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
|
|
653
825
|
try {
|
|
654
826
|
const profile = opts.profile;
|
|
827
|
+
try {
|
|
828
|
+
const creds = await loadCredentials(profile);
|
|
829
|
+
const token = await loadToken(profile);
|
|
830
|
+
const userToken = await loadUserToken(profile);
|
|
831
|
+
if (token?.accessToken) {
|
|
832
|
+
await revokeToken(token.accessToken, creds.clientId, creds.clientSecret);
|
|
833
|
+
}
|
|
834
|
+
if (userToken?.refreshToken) {
|
|
835
|
+
await revokeToken(userToken.refreshToken, creds.clientId, creds.clientSecret);
|
|
836
|
+
}
|
|
837
|
+
} catch {
|
|
838
|
+
}
|
|
655
839
|
await clearCredentials(profile);
|
|
656
840
|
output({ success: true, message: `Logged out (profile: ${profile})` }, opts);
|
|
657
841
|
} catch (err) {
|
|
@@ -696,7 +880,7 @@ import { Command as Command4 } from "commander";
|
|
|
696
880
|
var BASE_URL = "https://www.worksapis.com/v1.0";
|
|
697
881
|
var MAX_RETRIES = 3;
|
|
698
882
|
function sleep(ms) {
|
|
699
|
-
return new Promise((
|
|
883
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
700
884
|
}
|
|
701
885
|
async function request(opts, _retryCount = 0) {
|
|
702
886
|
const { method, path, body, profile = "default" } = opts;
|
|
@@ -718,7 +902,9 @@ async function request(opts, _retryCount = 0) {
|
|
|
718
902
|
return request(opts, _retryCount + 1);
|
|
719
903
|
}
|
|
720
904
|
if (res.status === 429 && _retryCount < MAX_RETRIES) {
|
|
721
|
-
const
|
|
905
|
+
const MAX_RETRY_AFTER = 60;
|
|
906
|
+
const rawRetry = parseInt(res.headers.get("Retry-After") ?? "5", 10);
|
|
907
|
+
const retryAfter = Math.min(Number.isNaN(rawRetry) ? 5 : rawRetry, MAX_RETRY_AFTER);
|
|
722
908
|
await sleep(retryAfter * 1e3);
|
|
723
909
|
return request(opts, _retryCount + 1);
|
|
724
910
|
}
|
|
@@ -743,6 +929,59 @@ async function request(opts, _retryCount = 0) {
|
|
|
743
929
|
return JSON.parse(text);
|
|
744
930
|
}
|
|
745
931
|
|
|
932
|
+
// src/utils/sanitize.ts
|
|
933
|
+
import { resolve, sep } from "path";
|
|
934
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
935
|
+
function sanitizePathSegment(value) {
|
|
936
|
+
if (!value || typeof value !== "string") {
|
|
937
|
+
throw new Error("Path segment must be a non-empty string");
|
|
938
|
+
}
|
|
939
|
+
if (value === "me") return value;
|
|
940
|
+
if (/[/\\]/.test(value) || value.includes("..")) {
|
|
941
|
+
throw new Error(`Invalid path segment: "${value}"`);
|
|
942
|
+
}
|
|
943
|
+
return encodeURIComponent(value);
|
|
944
|
+
}
|
|
945
|
+
function sanitizeFileName(name) {
|
|
946
|
+
if (!name || typeof name !== "string") {
|
|
947
|
+
throw new Error("File name must be a non-empty string");
|
|
948
|
+
}
|
|
949
|
+
const segments = name.split(/[/\\]/).filter((s) => s.length > 0);
|
|
950
|
+
const base = segments[segments.length - 1] ?? "";
|
|
951
|
+
return base.replace(/[\r\n"]/g, "_");
|
|
952
|
+
}
|
|
953
|
+
function validateLocalPath(filePath, allowedBase) {
|
|
954
|
+
const resolved = resolve(filePath);
|
|
955
|
+
if (allowedBase) {
|
|
956
|
+
const resolvedBase = resolve(allowedBase);
|
|
957
|
+
if (resolved !== resolvedBase && !resolved.startsWith(resolvedBase + sep)) {
|
|
958
|
+
throw new Error(`Path "${filePath}" escapes the allowed directory "${allowedBase}"`);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
return resolved;
|
|
962
|
+
}
|
|
963
|
+
function validateRedirectUrl(location, allowedHosts) {
|
|
964
|
+
let parsed;
|
|
965
|
+
try {
|
|
966
|
+
parsed = new URL(location);
|
|
967
|
+
} catch {
|
|
968
|
+
throw new Error(`Invalid redirect URL: "${location}"`);
|
|
969
|
+
}
|
|
970
|
+
if (parsed.protocol !== "https:") {
|
|
971
|
+
throw new Error(`Redirect URL must use HTTPS: "${location}"`);
|
|
972
|
+
}
|
|
973
|
+
const isAllowed = allowedHosts.some(
|
|
974
|
+
(host) => parsed.hostname === host || parsed.hostname.endsWith("." + host)
|
|
975
|
+
);
|
|
976
|
+
if (!isAllowed) {
|
|
977
|
+
throw new Error(`Redirect to untrusted host: "${parsed.hostname}"`);
|
|
978
|
+
}
|
|
979
|
+
return location;
|
|
980
|
+
}
|
|
981
|
+
function generateSecureState() {
|
|
982
|
+
return randomBytes2(32).toString("hex");
|
|
983
|
+
}
|
|
984
|
+
|
|
746
985
|
// src/api/message.ts
|
|
747
986
|
function buildContent(opts) {
|
|
748
987
|
const type = opts.type ?? "text";
|
|
@@ -750,7 +989,11 @@ function buildContent(opts) {
|
|
|
750
989
|
return { type: "text", text: opts.text };
|
|
751
990
|
}
|
|
752
991
|
if (type === "button") {
|
|
753
|
-
const actions = opts.actions ?
|
|
992
|
+
const actions = opts.actions ? (() => {
|
|
993
|
+
const parsed = JSON.parse(opts.actions);
|
|
994
|
+
if (!Array.isArray(parsed)) throw new Error("actions must be a JSON array");
|
|
995
|
+
return parsed;
|
|
996
|
+
})() : [];
|
|
754
997
|
return {
|
|
755
998
|
type: "button_template",
|
|
756
999
|
contentText: opts.text,
|
|
@@ -758,7 +1001,11 @@ function buildContent(opts) {
|
|
|
758
1001
|
};
|
|
759
1002
|
}
|
|
760
1003
|
if (type === "list") {
|
|
761
|
-
const elements = opts.elements ?
|
|
1004
|
+
const elements = opts.elements ? (() => {
|
|
1005
|
+
const parsed = JSON.parse(opts.elements);
|
|
1006
|
+
if (!Array.isArray(parsed)) throw new Error("elements must be a JSON array");
|
|
1007
|
+
return parsed;
|
|
1008
|
+
})() : [];
|
|
762
1009
|
return {
|
|
763
1010
|
type: "list_template",
|
|
764
1011
|
coverData: { text: opts.text },
|
|
@@ -780,7 +1027,7 @@ async function send(opts) {
|
|
|
780
1027
|
if (opts.to) {
|
|
781
1028
|
const result = await request({
|
|
782
1029
|
method: "POST",
|
|
783
|
-
path: `/bots/${creds.botId}/users/${opts.to}/messages`,
|
|
1030
|
+
path: `/bots/${sanitizePathSegment(creds.botId)}/users/${sanitizePathSegment(opts.to)}/messages`,
|
|
784
1031
|
body,
|
|
785
1032
|
profile
|
|
786
1033
|
});
|
|
@@ -789,7 +1036,7 @@ async function send(opts) {
|
|
|
789
1036
|
if (opts.channel) {
|
|
790
1037
|
const result = await request({
|
|
791
1038
|
method: "POST",
|
|
792
|
-
path: `/bots/${creds.botId}/channels/${opts.channel}/messages`,
|
|
1039
|
+
path: `/bots/${sanitizePathSegment(creds.botId)}/channels/${sanitizePathSegment(opts.channel)}/messages`,
|
|
793
1040
|
body,
|
|
794
1041
|
profile
|
|
795
1042
|
});
|
|
@@ -806,7 +1053,7 @@ async function listMembers(channelId, profile = "default") {
|
|
|
806
1053
|
}
|
|
807
1054
|
const result = await request({
|
|
808
1055
|
method: "GET",
|
|
809
|
-
path: `/bots/${creds.botId}/channels/${channelId}/members`,
|
|
1056
|
+
path: `/bots/${sanitizePathSegment(creds.botId)}/channels/${sanitizePathSegment(channelId)}/members`,
|
|
810
1057
|
profile
|
|
811
1058
|
});
|
|
812
1059
|
return { members: result.members ?? [], responseMetaData: result.responseMetaData };
|
|
@@ -854,20 +1101,25 @@ var messageCommand = new Command4("message").description("Bot message operations
|
|
|
854
1101
|
import { Command as Command5 } from "commander";
|
|
855
1102
|
|
|
856
1103
|
// src/api/directory.ts
|
|
857
|
-
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);
|
|
858
1108
|
const result = await request({
|
|
859
1109
|
method: "GET",
|
|
860
|
-
path:
|
|
1110
|
+
path: `/users?${params.toString()}`,
|
|
861
1111
|
profile
|
|
862
1112
|
});
|
|
863
1113
|
return { users: result.users ?? [], responseMetaData: result.responseMetaData };
|
|
864
1114
|
}
|
|
865
1115
|
|
|
866
1116
|
// src/commands/directory.ts
|
|
867
|
-
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) => {
|
|
868
1118
|
try {
|
|
869
|
-
const
|
|
1119
|
+
const count = opts.count ? Number(opts.count) : 100;
|
|
1120
|
+
const result = await listUsers(count, opts.cursor, opts.profile);
|
|
870
1121
|
const formatted = {
|
|
1122
|
+
nextCursor: result.responseMetaData?.nextCursor ?? null,
|
|
871
1123
|
users: result.users.map((u) => ({
|
|
872
1124
|
userId: u.userId,
|
|
873
1125
|
userName: [u.userName?.lastName, u.userName?.firstName].filter(Boolean).join(" ") || "",
|
|
@@ -890,6 +1142,7 @@ import { Command as Command6 } from "commander";
|
|
|
890
1142
|
import { randomUUID } from "crypto";
|
|
891
1143
|
|
|
892
1144
|
// src/auth/token-user.ts
|
|
1145
|
+
var inFlightRefresh = /* @__PURE__ */ new Map();
|
|
893
1146
|
async function getValidUserToken(profile = "default") {
|
|
894
1147
|
const cached = await loadUserToken(profile);
|
|
895
1148
|
if (!cached) {
|
|
@@ -900,22 +1153,106 @@ async function getValidUserToken(profile = "default") {
|
|
|
900
1153
|
if (cached.expiresAt > Date.now() / 1e3 + 300) {
|
|
901
1154
|
return cached.accessToken;
|
|
902
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
|
+
}
|
|
903
1174
|
const refreshed = await refreshUserToken(cached.refreshToken, profile);
|
|
904
1175
|
await saveUserToken(refreshed, profile);
|
|
905
1176
|
return refreshed.accessToken;
|
|
906
1177
|
}
|
|
907
1178
|
|
|
908
|
-
// src/api/
|
|
909
|
-
var
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
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;
|
|
915
1216
|
}
|
|
916
|
-
async function
|
|
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;
|
|
1250
|
+
}
|
|
1251
|
+
async function handleUserApiError(res) {
|
|
917
1252
|
if (res.status === 401) {
|
|
918
|
-
throw new AuthError(
|
|
1253
|
+
throw new AuthError(
|
|
1254
|
+
"User token expired or revoked. Run `nworks login --user` again."
|
|
1255
|
+
);
|
|
919
1256
|
}
|
|
920
1257
|
let code = "UNKNOWN";
|
|
921
1258
|
let description = `HTTP ${res.status}`;
|
|
@@ -927,6 +1264,9 @@ async function handleError(res) {
|
|
|
927
1264
|
}
|
|
928
1265
|
throw new ApiError(code, description, res.status);
|
|
929
1266
|
}
|
|
1267
|
+
|
|
1268
|
+
// src/api/calendar.ts
|
|
1269
|
+
var BASE_URL2 = "https://www.worksapis.com/v1.0";
|
|
930
1270
|
function generateEventId() {
|
|
931
1271
|
return `event-${randomUUID()}`;
|
|
932
1272
|
}
|
|
@@ -940,12 +1280,12 @@ function normalizeDateTime(dt) {
|
|
|
940
1280
|
async function listEvents(fromDateTime, untilDateTime, userId = "me", profile = "default") {
|
|
941
1281
|
const from = encodeURIComponent(fromDateTime);
|
|
942
1282
|
const until = encodeURIComponent(untilDateTime);
|
|
943
|
-
const url = `${BASE_URL2}/users/${userId}/calendar/events?fromDateTime=${from}&untilDateTime=${until}`;
|
|
1283
|
+
const url = `${BASE_URL2}/users/${sanitizePathSegment(userId)}/calendar/events?fromDateTime=${from}&untilDateTime=${until}`;
|
|
944
1284
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
945
1285
|
console.error(`[nworks] GET ${url}`);
|
|
946
1286
|
}
|
|
947
|
-
const res = await
|
|
948
|
-
if (!res.ok) return
|
|
1287
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
1288
|
+
if (!res.ok) return handleUserApiError(res);
|
|
949
1289
|
const data = await res.json();
|
|
950
1290
|
return { events: data.events ?? [] };
|
|
951
1291
|
}
|
|
@@ -977,12 +1317,12 @@ async function createEvent(opts) {
|
|
|
977
1317
|
eventComponents: [eventComponent],
|
|
978
1318
|
sendNotification: opts.sendNotification ?? false
|
|
979
1319
|
};
|
|
980
|
-
const url = `${BASE_URL2}/users/${userId}/calendar/events`;
|
|
1320
|
+
const url = `${BASE_URL2}/users/${sanitizePathSegment(userId)}/calendar/events`;
|
|
981
1321
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
982
1322
|
console.error(`[nworks] POST ${url}`);
|
|
983
|
-
console.error(`[nworks] Body: ${JSON.stringify(body
|
|
1323
|
+
console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
|
|
984
1324
|
}
|
|
985
|
-
const res = await
|
|
1325
|
+
const res = await userFetch(
|
|
986
1326
|
url,
|
|
987
1327
|
{
|
|
988
1328
|
method: "POST",
|
|
@@ -994,16 +1334,16 @@ async function createEvent(opts) {
|
|
|
994
1334
|
if (res.status === 201) {
|
|
995
1335
|
return await res.json();
|
|
996
1336
|
}
|
|
997
|
-
if (!res.ok) return
|
|
1337
|
+
if (!res.ok) return handleUserApiError(res);
|
|
998
1338
|
return await res.json();
|
|
999
1339
|
}
|
|
1000
1340
|
async function getEvent(eventId, userId = "me", profile = "default") {
|
|
1001
|
-
const url = `${BASE_URL2}/users/${userId}/calendar/events/${eventId}`;
|
|
1341
|
+
const url = `${BASE_URL2}/users/${sanitizePathSegment(userId)}/calendar/events/${sanitizePathSegment(eventId)}`;
|
|
1002
1342
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1003
1343
|
console.error(`[nworks] GET ${url}`);
|
|
1004
1344
|
}
|
|
1005
|
-
const res = await
|
|
1006
|
-
if (!res.ok) return
|
|
1345
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
1346
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1007
1347
|
const data = await res.json();
|
|
1008
1348
|
const event = data.eventComponents[0];
|
|
1009
1349
|
if (!event) throw new ApiError("NOT_FOUND", "Event not found", 404);
|
|
@@ -1041,12 +1381,12 @@ async function updateEvent(opts) {
|
|
|
1041
1381
|
eventComponents: [eventComponent],
|
|
1042
1382
|
sendNotification: opts.sendNotification ?? false
|
|
1043
1383
|
};
|
|
1044
|
-
const url = `${BASE_URL2}/users/${userId}/calendar/events/${opts.eventId}`;
|
|
1384
|
+
const url = `${BASE_URL2}/users/${sanitizePathSegment(userId)}/calendar/events/${sanitizePathSegment(opts.eventId)}`;
|
|
1045
1385
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1046
1386
|
console.error(`[nworks] PUT ${url}`);
|
|
1047
|
-
console.error(`[nworks] Body: ${JSON.stringify(body
|
|
1387
|
+
console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
|
|
1048
1388
|
}
|
|
1049
|
-
const res = await
|
|
1389
|
+
const res = await userFetch(
|
|
1050
1390
|
url,
|
|
1051
1391
|
{
|
|
1052
1392
|
method: "PUT",
|
|
@@ -1055,18 +1395,18 @@ async function updateEvent(opts) {
|
|
|
1055
1395
|
},
|
|
1056
1396
|
profile
|
|
1057
1397
|
);
|
|
1058
|
-
if (!res.ok) return
|
|
1398
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1059
1399
|
}
|
|
1060
1400
|
async function deleteEvent(eventId, userId = "me", sendNotification = false, profile = "default") {
|
|
1061
1401
|
const params = new URLSearchParams();
|
|
1062
1402
|
params.set("sendNotification", String(sendNotification));
|
|
1063
|
-
const url = `${BASE_URL2}/users/${userId}/calendar/events/${eventId}?${params.toString()}`;
|
|
1403
|
+
const url = `${BASE_URL2}/users/${sanitizePathSegment(userId)}/calendar/events/${sanitizePathSegment(eventId)}?${params.toString()}`;
|
|
1064
1404
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1065
1405
|
console.error(`[nworks] DELETE ${url}`);
|
|
1066
1406
|
}
|
|
1067
|
-
const res = await
|
|
1407
|
+
const res = await userFetch(url, { method: "DELETE" }, profile);
|
|
1068
1408
|
if (res.status === 204) return;
|
|
1069
|
-
if (!res.ok) return
|
|
1409
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1070
1410
|
}
|
|
1071
1411
|
|
|
1072
1412
|
// src/commands/calendar.ts
|
|
@@ -1186,32 +1526,19 @@ import { join as join2 } from "path";
|
|
|
1186
1526
|
import { Command as Command7 } from "commander";
|
|
1187
1527
|
|
|
1188
1528
|
// src/api/drive.ts
|
|
1189
|
-
import { readFile as readFile4, stat } from "fs/promises";
|
|
1529
|
+
import { readFile as readFile4, stat as stat2 } from "fs/promises";
|
|
1190
1530
|
import { basename } from "path";
|
|
1191
1531
|
var BASE_URL3 = "https://www.worksapis.com/v1.0";
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
if (res.status === 401) {
|
|
1200
|
-
throw new AuthError("User token expired. Run `nworks login --user --scope file` again.");
|
|
1201
|
-
}
|
|
1202
|
-
let code = "UNKNOWN";
|
|
1203
|
-
let description = `HTTP ${res.status}`;
|
|
1204
|
-
try {
|
|
1205
|
-
const body = await res.json();
|
|
1206
|
-
code = body.code ?? code;
|
|
1207
|
-
description = body.description ?? description;
|
|
1208
|
-
} catch {
|
|
1209
|
-
}
|
|
1210
|
-
throw new ApiError(code, description, res.status);
|
|
1211
|
-
}
|
|
1532
|
+
var MAX_UPLOAD_SIZE = 100 * 1024 * 1024;
|
|
1533
|
+
var ALLOWED_HOSTS = [
|
|
1534
|
+
"storage.worksmobile.com",
|
|
1535
|
+
"apis-storage.worksmobile.com",
|
|
1536
|
+
"www.worksapis.com",
|
|
1537
|
+
"worksapis.com"
|
|
1538
|
+
];
|
|
1212
1539
|
async function listFiles(userId = "me", folderId, count = 20, cursor, profile = "default") {
|
|
1213
|
-
const base = `${BASE_URL3}/users/${userId}/drive/files`;
|
|
1214
|
-
const path = folderId ? `${base}/${folderId}/children` : base;
|
|
1540
|
+
const base = `${BASE_URL3}/users/${sanitizePathSegment(userId)}/drive/files`;
|
|
1541
|
+
const path = folderId ? `${base}/${sanitizePathSegment(folderId)}/children` : base;
|
|
1215
1542
|
const params = new URLSearchParams();
|
|
1216
1543
|
params.set("count", String(count));
|
|
1217
1544
|
if (cursor) params.set("cursor", cursor);
|
|
@@ -1219,21 +1546,25 @@ async function listFiles(userId = "me", folderId, count = 20, cursor, profile =
|
|
|
1219
1546
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1220
1547
|
console.error(`[nworks] GET ${url}`);
|
|
1221
1548
|
}
|
|
1222
|
-
const res = await
|
|
1223
|
-
if (!res.ok) return
|
|
1549
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
1550
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1224
1551
|
const data = await res.json();
|
|
1225
1552
|
return { files: data.files ?? [], responseMetaData: data.responseMetaData };
|
|
1226
1553
|
}
|
|
1227
1554
|
async function uploadFile(localPath, userId = "me", folderId, overwrite = false, profile = "default") {
|
|
1228
1555
|
const fileName = basename(localPath);
|
|
1229
|
-
const
|
|
1556
|
+
const safeName = sanitizeFileName(fileName);
|
|
1557
|
+
const fileStat = await stat2(localPath);
|
|
1230
1558
|
const fileSize = fileStat.size;
|
|
1231
|
-
|
|
1232
|
-
|
|
1559
|
+
if (fileSize > MAX_UPLOAD_SIZE) {
|
|
1560
|
+
throw new ApiError("FILE_TOO_LARGE", `File size (${fileSize} bytes) exceeds maximum allowed (${MAX_UPLOAD_SIZE} bytes)`, 413);
|
|
1561
|
+
}
|
|
1562
|
+
const base = `${BASE_URL3}/users/${sanitizePathSegment(userId)}/drive/files`;
|
|
1563
|
+
const createUrl = folderId ? `${base}/${sanitizePathSegment(folderId)}` : base;
|
|
1233
1564
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1234
1565
|
console.error(`[nworks] POST ${createUrl} (create upload URL)`);
|
|
1235
1566
|
}
|
|
1236
|
-
const createRes = await
|
|
1567
|
+
const createRes = await userFetch(
|
|
1237
1568
|
createUrl,
|
|
1238
1569
|
{
|
|
1239
1570
|
method: "POST",
|
|
@@ -1242,13 +1573,14 @@ async function uploadFile(localPath, userId = "me", folderId, overwrite = false,
|
|
|
1242
1573
|
},
|
|
1243
1574
|
profile
|
|
1244
1575
|
);
|
|
1245
|
-
if (!createRes.ok) return
|
|
1576
|
+
if (!createRes.ok) return handleUserApiError(createRes);
|
|
1246
1577
|
const { uploadUrl } = await createRes.json();
|
|
1578
|
+
validateRedirectUrl(uploadUrl, ALLOWED_HOSTS);
|
|
1247
1579
|
const fileBuffer = await readFile4(localPath);
|
|
1248
1580
|
const boundary = `----nworks${Date.now()}`;
|
|
1249
1581
|
const header = Buffer.from(
|
|
1250
1582
|
`--${boundary}\r
|
|
1251
|
-
Content-Disposition: form-data; name="Filedata"; filename="${
|
|
1583
|
+
Content-Disposition: form-data; name="Filedata"; filename="${safeName}"\r
|
|
1252
1584
|
Content-Type: application/octet-stream\r
|
|
1253
1585
|
\r
|
|
1254
1586
|
`
|
|
@@ -1260,7 +1592,7 @@ Content-Type: application/octet-stream\r
|
|
|
1260
1592
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1261
1593
|
console.error(`[nworks] POST ${uploadUrl} (upload content, ${fileSize} bytes)`);
|
|
1262
1594
|
}
|
|
1263
|
-
const uploadRes = await
|
|
1595
|
+
const uploadRes = await userFetch(
|
|
1264
1596
|
uploadUrl,
|
|
1265
1597
|
{
|
|
1266
1598
|
method: "POST",
|
|
@@ -1269,17 +1601,20 @@ Content-Type: application/octet-stream\r
|
|
|
1269
1601
|
},
|
|
1270
1602
|
profile
|
|
1271
1603
|
);
|
|
1272
|
-
if (!uploadRes.ok) return
|
|
1604
|
+
if (!uploadRes.ok) return handleUserApiError(uploadRes);
|
|
1273
1605
|
return await uploadRes.json();
|
|
1274
1606
|
}
|
|
1275
1607
|
async function uploadBuffer(fileBuffer, fileName, userId = "me", folderId, overwrite = false, profile = "default") {
|
|
1276
1608
|
const fileSize = fileBuffer.length;
|
|
1277
|
-
|
|
1278
|
-
|
|
1609
|
+
if (fileSize > MAX_UPLOAD_SIZE) {
|
|
1610
|
+
throw new ApiError("FILE_TOO_LARGE", `File size (${fileSize} bytes) exceeds maximum allowed (${MAX_UPLOAD_SIZE} bytes)`, 413);
|
|
1611
|
+
}
|
|
1612
|
+
const base = `${BASE_URL3}/users/${sanitizePathSegment(userId)}/drive/files`;
|
|
1613
|
+
const createUrl = folderId ? `${base}/${sanitizePathSegment(folderId)}` : base;
|
|
1279
1614
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1280
1615
|
console.error(`[nworks] POST ${createUrl} (create upload URL for buffer)`);
|
|
1281
1616
|
}
|
|
1282
|
-
const createRes = await
|
|
1617
|
+
const createRes = await userFetch(
|
|
1283
1618
|
createUrl,
|
|
1284
1619
|
{
|
|
1285
1620
|
method: "POST",
|
|
@@ -1288,12 +1623,13 @@ async function uploadBuffer(fileBuffer, fileName, userId = "me", folderId, overw
|
|
|
1288
1623
|
},
|
|
1289
1624
|
profile
|
|
1290
1625
|
);
|
|
1291
|
-
if (!createRes.ok) return
|
|
1626
|
+
if (!createRes.ok) return handleUserApiError(createRes);
|
|
1292
1627
|
const { uploadUrl } = await createRes.json();
|
|
1628
|
+
validateRedirectUrl(uploadUrl, ALLOWED_HOSTS);
|
|
1293
1629
|
const boundary = `----nworks${Date.now()}`;
|
|
1294
1630
|
const header = Buffer.from(
|
|
1295
1631
|
`--${boundary}\r
|
|
1296
|
-
Content-Disposition: form-data; name="Filedata"; filename="${fileName}"\r
|
|
1632
|
+
Content-Disposition: form-data; name="Filedata"; filename="${sanitizeFileName(fileName)}"\r
|
|
1297
1633
|
Content-Type: application/octet-stream\r
|
|
1298
1634
|
\r
|
|
1299
1635
|
`
|
|
@@ -1305,7 +1641,7 @@ Content-Type: application/octet-stream\r
|
|
|
1305
1641
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1306
1642
|
console.error(`[nworks] POST ${uploadUrl} (upload buffer, ${fileSize} bytes)`);
|
|
1307
1643
|
}
|
|
1308
|
-
const uploadRes = await
|
|
1644
|
+
const uploadRes = await userFetch(
|
|
1309
1645
|
uploadUrl,
|
|
1310
1646
|
{
|
|
1311
1647
|
method: "POST",
|
|
@@ -1314,32 +1650,29 @@ Content-Type: application/octet-stream\r
|
|
|
1314
1650
|
},
|
|
1315
1651
|
profile
|
|
1316
1652
|
);
|
|
1317
|
-
if (!uploadRes.ok) return
|
|
1653
|
+
if (!uploadRes.ok) return handleUserApiError(uploadRes);
|
|
1318
1654
|
return await uploadRes.json();
|
|
1319
1655
|
}
|
|
1320
|
-
async function
|
|
1321
|
-
const url = `${BASE_URL3}/users/${userId}/drive/files/${fileId}/download`;
|
|
1656
|
+
async function downloadViaRedirect(url, profile) {
|
|
1322
1657
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1323
1658
|
console.error(`[nworks] GET ${url} (get download URL)`);
|
|
1324
1659
|
}
|
|
1325
|
-
const redirectRes = await
|
|
1660
|
+
const redirectRes = await userFetch(
|
|
1326
1661
|
url,
|
|
1327
1662
|
{ method: "GET", redirect: "manual" },
|
|
1328
1663
|
profile
|
|
1329
1664
|
);
|
|
1330
|
-
if (redirectRes.status === 401) {
|
|
1331
|
-
throw new AuthError("User token expired. Run `nworks login --user --scope file` again.");
|
|
1332
|
-
}
|
|
1333
1665
|
const location = redirectRes.headers.get("location");
|
|
1334
1666
|
if (!location) {
|
|
1335
|
-
if (!redirectRes.ok) return
|
|
1667
|
+
if (!redirectRes.ok) return handleUserApiError(redirectRes);
|
|
1336
1668
|
throw new ApiError("NO_REDIRECT", "No download URL returned", redirectRes.status);
|
|
1337
1669
|
}
|
|
1670
|
+
const safeLocation = validateRedirectUrl(location, ALLOWED_HOSTS);
|
|
1338
1671
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1339
|
-
console.error(`[nworks] GET ${
|
|
1672
|
+
console.error(`[nworks] GET ${safeLocation} (download content)`);
|
|
1340
1673
|
}
|
|
1341
|
-
const downloadRes = await
|
|
1342
|
-
if (!downloadRes.ok) return
|
|
1674
|
+
const downloadRes = await userFetch(safeLocation, { method: "GET" }, profile);
|
|
1675
|
+
if (!downloadRes.ok) return handleUserApiError(downloadRes);
|
|
1343
1676
|
const arrayBuffer = await downloadRes.arrayBuffer();
|
|
1344
1677
|
const buffer = Buffer.from(arrayBuffer);
|
|
1345
1678
|
const disposition = downloadRes.headers.get("content-disposition");
|
|
@@ -1352,6 +1685,42 @@ async function downloadFile(fileId, userId = "me", profile = "default") {
|
|
|
1352
1685
|
}
|
|
1353
1686
|
return { buffer, fileName };
|
|
1354
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
|
+
}
|
|
1355
1724
|
|
|
1356
1725
|
// src/commands/drive.ts
|
|
1357
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) => {
|
|
@@ -1445,37 +1814,90 @@ var downloadCommand = new Command7("download").description("Download a file from
|
|
|
1445
1814
|
cliError(err, opts, "drive");
|
|
1446
1815
|
}
|
|
1447
1816
|
});
|
|
1448
|
-
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);
|
|
1449
1889
|
|
|
1450
1890
|
// src/commands/mail.ts
|
|
1891
|
+
import { writeFile as writeFile3 } from "fs/promises";
|
|
1892
|
+
import { join as join3 } from "path";
|
|
1451
1893
|
import { Command as Command8 } from "commander";
|
|
1452
1894
|
|
|
1453
1895
|
// src/api/mail.ts
|
|
1454
1896
|
var BASE_URL4 = "https://www.worksapis.com/v1.0";
|
|
1455
|
-
async function authedFetch3(url, init, profile) {
|
|
1456
|
-
const token = await getValidUserToken(profile);
|
|
1457
|
-
const headers = new Headers(init.headers);
|
|
1458
|
-
headers.set("Authorization", `Bearer ${token}`);
|
|
1459
|
-
return fetch(url, { ...init, headers });
|
|
1460
|
-
}
|
|
1461
|
-
async function handleError3(res) {
|
|
1462
|
-
if (res.status === 401) {
|
|
1463
|
-
throw new AuthError("User token expired. Run `nworks login --user --scope mail` again.");
|
|
1464
|
-
}
|
|
1465
|
-
let code = "UNKNOWN";
|
|
1466
|
-
let description = `HTTP ${res.status}`;
|
|
1467
|
-
try {
|
|
1468
|
-
const body = await res.json();
|
|
1469
|
-
code = body.code ?? code;
|
|
1470
|
-
description = body.description ?? description;
|
|
1471
|
-
} catch {
|
|
1472
|
-
}
|
|
1473
|
-
throw new ApiError(code, description, res.status);
|
|
1474
|
-
}
|
|
1475
1897
|
async function sendMail(opts) {
|
|
1476
1898
|
const userId = opts.userId ?? "me";
|
|
1477
1899
|
const profile = opts.profile ?? "default";
|
|
1478
|
-
const url = `${BASE_URL4}/users/${userId}/mail`;
|
|
1900
|
+
const url = `${BASE_URL4}/users/${sanitizePathSegment(userId)}/mail`;
|
|
1479
1901
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1480
1902
|
console.error(`[nworks] POST ${url}`);
|
|
1481
1903
|
}
|
|
@@ -1487,7 +1909,7 @@ async function sendMail(opts) {
|
|
|
1487
1909
|
if (opts.cc) body.cc = opts.cc;
|
|
1488
1910
|
if (opts.bcc) body.bcc = opts.bcc;
|
|
1489
1911
|
if (opts.contentType) body.contentType = opts.contentType;
|
|
1490
|
-
const res = await
|
|
1912
|
+
const res = await userFetch(
|
|
1491
1913
|
url,
|
|
1492
1914
|
{
|
|
1493
1915
|
method: "POST",
|
|
@@ -1497,19 +1919,19 @@ async function sendMail(opts) {
|
|
|
1497
1919
|
profile
|
|
1498
1920
|
);
|
|
1499
1921
|
if (res.status === 202) return;
|
|
1500
|
-
if (!res.ok) return
|
|
1922
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1501
1923
|
}
|
|
1502
1924
|
async function listMails(folderId = 0, userId = "me", count = 30, cursor, isUnread, profile = "default") {
|
|
1503
1925
|
const params = new URLSearchParams();
|
|
1504
1926
|
params.set("count", String(count));
|
|
1505
1927
|
if (cursor) params.set("cursor", cursor);
|
|
1506
1928
|
if (isUnread) params.set("isUnread", "true");
|
|
1507
|
-
const url = `${BASE_URL4}/users/${userId}/mail/mailfolders/${folderId}/children?${params.toString()}`;
|
|
1929
|
+
const url = `${BASE_URL4}/users/${sanitizePathSegment(userId)}/mail/mailfolders/${sanitizePathSegment(String(folderId))}/children?${params.toString()}`;
|
|
1508
1930
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1509
1931
|
console.error(`[nworks] GET ${url}`);
|
|
1510
1932
|
}
|
|
1511
|
-
const res = await
|
|
1512
|
-
if (!res.ok) return
|
|
1933
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
1934
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1513
1935
|
const data = await res.json();
|
|
1514
1936
|
return {
|
|
1515
1937
|
mails: data.mails ?? [],
|
|
@@ -1520,14 +1942,26 @@ async function listMails(folderId = 0, userId = "me", count = 30, cursor, isUnre
|
|
|
1520
1942
|
};
|
|
1521
1943
|
}
|
|
1522
1944
|
async function readMail(mailId, userId = "me", profile = "default") {
|
|
1523
|
-
const url = `${BASE_URL4}/users/${userId}/mail/${mailId}`;
|
|
1945
|
+
const url = `${BASE_URL4}/users/${sanitizePathSegment(userId)}/mail/${sanitizePathSegment(String(mailId))}`;
|
|
1524
1946
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1525
1947
|
console.error(`[nworks] GET ${url}`);
|
|
1526
1948
|
}
|
|
1527
|
-
const res = await
|
|
1528
|
-
if (!res.ok) return
|
|
1949
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
1950
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1529
1951
|
return await res.json();
|
|
1530
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
|
+
}
|
|
1531
1965
|
|
|
1532
1966
|
// src/commands/mail.ts
|
|
1533
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) => {
|
|
@@ -1615,41 +2049,46 @@ var readCommand = new Command8("read").description("Read a specific mail (requir
|
|
|
1615
2049
|
cliError(err, opts, "mail");
|
|
1616
2050
|
}
|
|
1617
2051
|
});
|
|
1618
|
-
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);
|
|
1619
2078
|
|
|
1620
2079
|
// src/commands/task.ts
|
|
1621
2080
|
import { Command as Command9 } from "commander";
|
|
1622
2081
|
|
|
1623
2082
|
// src/api/task.ts
|
|
1624
2083
|
var BASE_URL5 = "https://www.worksapis.com/v1.0";
|
|
1625
|
-
async function
|
|
1626
|
-
|
|
1627
|
-
const
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
throw new AuthError("User token expired. Run `nworks login --user --scope task` again.");
|
|
1634
|
-
}
|
|
1635
|
-
let code = "UNKNOWN";
|
|
1636
|
-
let description = `HTTP ${res.status}`;
|
|
1637
|
-
try {
|
|
1638
|
-
const body = await res.json();
|
|
1639
|
-
code = body.code ?? code;
|
|
1640
|
-
description = body.description ?? description;
|
|
1641
|
-
} catch {
|
|
1642
|
-
}
|
|
1643
|
-
throw new ApiError(code, description, res.status);
|
|
1644
|
-
}
|
|
1645
|
-
async function resolveUserId(userId, profile) {
|
|
1646
|
-
if (userId !== "me") return userId;
|
|
1647
|
-
const url = `${BASE_URL5}/users/me`;
|
|
1648
|
-
const res = await authedFetch4(url, { method: "GET" }, profile);
|
|
1649
|
-
if (!res.ok) return handleError4(res);
|
|
1650
|
-
const data = await res.json();
|
|
1651
|
-
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1652
|
-
console.error(`[nworks] Resolved "me" \u2192 ${data.userId}`);
|
|
2084
|
+
async function resolveUserId(userId, profile) {
|
|
2085
|
+
if (userId !== "me") return userId;
|
|
2086
|
+
const url = `${BASE_URL5}/users/${sanitizePathSegment(userId)}`;
|
|
2087
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
2088
|
+
if (!res.ok) return handleUserApiError(res);
|
|
2089
|
+
const data = await res.json();
|
|
2090
|
+
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
2091
|
+
console.error(`[nworks] Resolved "me" \u2192 ${data.userId}`);
|
|
1653
2092
|
}
|
|
1654
2093
|
return data.userId;
|
|
1655
2094
|
}
|
|
@@ -1659,12 +2098,12 @@ async function listTasks(categoryId = "default", userId = "me", count = 50, curs
|
|
|
1659
2098
|
params.set("count", String(count));
|
|
1660
2099
|
params.set("status", status);
|
|
1661
2100
|
if (cursor) params.set("cursor", cursor);
|
|
1662
|
-
const url = `${BASE_URL5}/users/${userId}/tasks?${params.toString()}`;
|
|
2101
|
+
const url = `${BASE_URL5}/users/${sanitizePathSegment(userId)}/tasks?${params.toString()}`;
|
|
1663
2102
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1664
2103
|
console.error(`[nworks] GET ${url}`);
|
|
1665
2104
|
}
|
|
1666
|
-
const res = await
|
|
1667
|
-
if (!res.ok) return
|
|
2105
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
2106
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1668
2107
|
const data = await res.json();
|
|
1669
2108
|
return { tasks: data.tasks ?? [], responseMetaData: data.responseMetaData };
|
|
1670
2109
|
}
|
|
@@ -1683,12 +2122,12 @@ async function createTask(opts) {
|
|
|
1683
2122
|
};
|
|
1684
2123
|
if (opts.dueDate) body.dueDate = opts.dueDate;
|
|
1685
2124
|
if (opts.categoryId) body.categoryId = opts.categoryId;
|
|
1686
|
-
const url = `${BASE_URL5}/users/${userId}/tasks`;
|
|
2125
|
+
const url = `${BASE_URL5}/users/${sanitizePathSegment(userId)}/tasks`;
|
|
1687
2126
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1688
2127
|
console.error(`[nworks] POST ${url}`);
|
|
1689
|
-
console.error(`[nworks] Body: ${JSON.stringify(body
|
|
2128
|
+
console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
|
|
1690
2129
|
}
|
|
1691
|
-
const res = await
|
|
2130
|
+
const res = await userFetch(
|
|
1692
2131
|
url,
|
|
1693
2132
|
{
|
|
1694
2133
|
method: "POST",
|
|
@@ -1700,7 +2139,7 @@ async function createTask(opts) {
|
|
|
1700
2139
|
if (res.status === 201) {
|
|
1701
2140
|
return await res.json();
|
|
1702
2141
|
}
|
|
1703
|
-
if (!res.ok) return
|
|
2142
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1704
2143
|
return await res.json();
|
|
1705
2144
|
}
|
|
1706
2145
|
async function updateTask(opts) {
|
|
@@ -1709,11 +2148,11 @@ async function updateTask(opts) {
|
|
|
1709
2148
|
if (opts.title !== void 0) body.title = opts.title;
|
|
1710
2149
|
if (opts.content !== void 0) body.content = opts.content;
|
|
1711
2150
|
if (opts.dueDate !== void 0) body.dueDate = opts.dueDate;
|
|
1712
|
-
const url = `${BASE_URL5}/tasks/${opts.taskId}`;
|
|
2151
|
+
const url = `${BASE_URL5}/tasks/${sanitizePathSegment(opts.taskId)}`;
|
|
1713
2152
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1714
2153
|
console.error(`[nworks] PATCH ${url}`);
|
|
1715
2154
|
}
|
|
1716
|
-
const res = await
|
|
2155
|
+
const res = await userFetch(
|
|
1717
2156
|
url,
|
|
1718
2157
|
{
|
|
1719
2158
|
method: "PATCH",
|
|
@@ -1722,47 +2161,47 @@ async function updateTask(opts) {
|
|
|
1722
2161
|
},
|
|
1723
2162
|
profile
|
|
1724
2163
|
);
|
|
1725
|
-
if (!res.ok) return
|
|
2164
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1726
2165
|
return await res.json();
|
|
1727
2166
|
}
|
|
1728
2167
|
async function completeTask(taskId, profile = "default") {
|
|
1729
|
-
const url = `${BASE_URL5}/tasks/${taskId}/complete`;
|
|
2168
|
+
const url = `${BASE_URL5}/tasks/${sanitizePathSegment(taskId)}/complete`;
|
|
1730
2169
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1731
2170
|
console.error(`[nworks] POST ${url}`);
|
|
1732
2171
|
}
|
|
1733
|
-
const res = await
|
|
2172
|
+
const res = await userFetch(
|
|
1734
2173
|
url,
|
|
1735
2174
|
{ method: "POST" },
|
|
1736
2175
|
profile
|
|
1737
2176
|
);
|
|
1738
2177
|
if (res.status === 204) return;
|
|
1739
|
-
if (!res.ok) return
|
|
2178
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1740
2179
|
}
|
|
1741
2180
|
async function incompleteTask(taskId, profile = "default") {
|
|
1742
|
-
const url = `${BASE_URL5}/tasks/${taskId}/incomplete`;
|
|
2181
|
+
const url = `${BASE_URL5}/tasks/${sanitizePathSegment(taskId)}/incomplete`;
|
|
1743
2182
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1744
2183
|
console.error(`[nworks] POST ${url}`);
|
|
1745
2184
|
}
|
|
1746
|
-
const res = await
|
|
2185
|
+
const res = await userFetch(
|
|
1747
2186
|
url,
|
|
1748
2187
|
{ method: "POST" },
|
|
1749
2188
|
profile
|
|
1750
2189
|
);
|
|
1751
2190
|
if (res.status === 204) return;
|
|
1752
|
-
if (!res.ok) return
|
|
2191
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1753
2192
|
}
|
|
1754
2193
|
async function deleteTask(taskId, profile = "default") {
|
|
1755
|
-
const url = `${BASE_URL5}/tasks/${taskId}`;
|
|
2194
|
+
const url = `${BASE_URL5}/tasks/${sanitizePathSegment(taskId)}`;
|
|
1756
2195
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1757
2196
|
console.error(`[nworks] DELETE ${url}`);
|
|
1758
2197
|
}
|
|
1759
|
-
const res = await
|
|
2198
|
+
const res = await userFetch(
|
|
1760
2199
|
url,
|
|
1761
2200
|
{ method: "DELETE" },
|
|
1762
2201
|
profile
|
|
1763
2202
|
);
|
|
1764
2203
|
if (res.status === 204) return;
|
|
1765
|
-
if (!res.ok) return
|
|
2204
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1766
2205
|
}
|
|
1767
2206
|
|
|
1768
2207
|
// src/commands/task.ts
|
|
@@ -1885,26 +2324,6 @@ import { Command as Command10 } from "commander";
|
|
|
1885
2324
|
|
|
1886
2325
|
// src/api/board.ts
|
|
1887
2326
|
var BASE_URL6 = "https://www.worksapis.com/v1.0";
|
|
1888
|
-
async function authedFetch5(url, init, profile) {
|
|
1889
|
-
const token = await getValidUserToken(profile);
|
|
1890
|
-
const headers = new Headers(init.headers);
|
|
1891
|
-
headers.set("Authorization", `Bearer ${token}`);
|
|
1892
|
-
return fetch(url, { ...init, headers });
|
|
1893
|
-
}
|
|
1894
|
-
async function handleError5(res) {
|
|
1895
|
-
if (res.status === 401) {
|
|
1896
|
-
throw new AuthError("User token expired. Run `nworks login --user --scope board` again.");
|
|
1897
|
-
}
|
|
1898
|
-
let code = "UNKNOWN";
|
|
1899
|
-
let description = `HTTP ${res.status}`;
|
|
1900
|
-
try {
|
|
1901
|
-
const body = await res.json();
|
|
1902
|
-
code = body.code ?? code;
|
|
1903
|
-
description = body.description ?? description;
|
|
1904
|
-
} catch {
|
|
1905
|
-
}
|
|
1906
|
-
throw new ApiError(code, description, res.status);
|
|
1907
|
-
}
|
|
1908
2327
|
function safeParseJson(text) {
|
|
1909
2328
|
const safe = text.replace(
|
|
1910
2329
|
/"((?:board|post|domain|user)Id)"\s*:\s*(\d{16,})/g,
|
|
@@ -1920,11 +2339,11 @@ async function listBoards(count = 20, cursor, profile = "default") {
|
|
|
1920
2339
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1921
2340
|
console.error(`[nworks] GET ${url}`);
|
|
1922
2341
|
}
|
|
1923
|
-
const res = await
|
|
1924
|
-
if (!res.ok) return
|
|
2342
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
2343
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1925
2344
|
const text = await res.text();
|
|
1926
2345
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1927
|
-
console.error(`[nworks] Response: ${text}`);
|
|
2346
|
+
console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
|
|
1928
2347
|
}
|
|
1929
2348
|
const data = safeParseJson(text);
|
|
1930
2349
|
return { boards: data.boards ?? [], responseMetaData: data.responseMetaData };
|
|
@@ -1933,29 +2352,29 @@ async function listPosts(boardId, count = 20, cursor, profile = "default") {
|
|
|
1933
2352
|
const params = new URLSearchParams();
|
|
1934
2353
|
params.set("count", String(count));
|
|
1935
2354
|
if (cursor) params.set("cursor", cursor);
|
|
1936
|
-
const url = `${BASE_URL6}/boards/${boardId}/posts?${params.toString()}`;
|
|
2355
|
+
const url = `${BASE_URL6}/boards/${sanitizePathSegment(boardId)}/posts?${params.toString()}`;
|
|
1937
2356
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1938
2357
|
console.error(`[nworks] GET ${url}`);
|
|
1939
2358
|
}
|
|
1940
|
-
const res = await
|
|
1941
|
-
if (!res.ok) return
|
|
2359
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
2360
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1942
2361
|
const text = await res.text();
|
|
1943
2362
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1944
|
-
console.error(`[nworks] Response: ${text}`);
|
|
2363
|
+
console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
|
|
1945
2364
|
}
|
|
1946
2365
|
const data = safeParseJson(text);
|
|
1947
2366
|
return { posts: data.posts ?? [], responseMetaData: data.responseMetaData };
|
|
1948
2367
|
}
|
|
1949
2368
|
async function readPost(boardId, postId, profile = "default") {
|
|
1950
|
-
const url = `${BASE_URL6}/boards/${boardId}/posts/${postId}`;
|
|
2369
|
+
const url = `${BASE_URL6}/boards/${sanitizePathSegment(boardId)}/posts/${sanitizePathSegment(postId)}`;
|
|
1951
2370
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1952
2371
|
console.error(`[nworks] GET ${url}`);
|
|
1953
2372
|
}
|
|
1954
|
-
const res = await
|
|
1955
|
-
if (!res.ok) return
|
|
2373
|
+
const res = await userFetch(url, { method: "GET" }, profile);
|
|
2374
|
+
if (!res.ok) return handleUserApiError(res);
|
|
1956
2375
|
const text = await res.text();
|
|
1957
2376
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1958
|
-
console.error(`[nworks] Response: ${text}`);
|
|
2377
|
+
console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
|
|
1959
2378
|
}
|
|
1960
2379
|
return safeParseJson(text);
|
|
1961
2380
|
}
|
|
@@ -1967,12 +2386,12 @@ async function createPost(opts) {
|
|
|
1967
2386
|
};
|
|
1968
2387
|
if (opts.enableComment !== void 0) body.enableComment = opts.enableComment;
|
|
1969
2388
|
if (opts.sendNotifications !== void 0) body.sendNotifications = opts.sendNotifications;
|
|
1970
|
-
const url = `${BASE_URL6}/boards/${opts.boardId}/posts`;
|
|
2389
|
+
const url = `${BASE_URL6}/boards/${sanitizePathSegment(opts.boardId)}/posts`;
|
|
1971
2390
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1972
2391
|
console.error(`[nworks] POST ${url}`);
|
|
1973
|
-
console.error(`[nworks] Body: ${JSON.stringify(body
|
|
2392
|
+
console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
|
|
1974
2393
|
}
|
|
1975
|
-
const res = await
|
|
2394
|
+
const res = await userFetch(
|
|
1976
2395
|
url,
|
|
1977
2396
|
{
|
|
1978
2397
|
method: "POST",
|
|
@@ -1984,11 +2403,11 @@ async function createPost(opts) {
|
|
|
1984
2403
|
if (res.status === 201 || res.ok) {
|
|
1985
2404
|
const text = await res.text();
|
|
1986
2405
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
1987
|
-
console.error(`[nworks] Response: ${text}`);
|
|
2406
|
+
console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
|
|
1988
2407
|
}
|
|
1989
2408
|
return safeParseJson(text);
|
|
1990
2409
|
}
|
|
1991
|
-
return
|
|
2410
|
+
return handleUserApiError(res);
|
|
1992
2411
|
}
|
|
1993
2412
|
|
|
1994
2413
|
// src/commands/board.ts
|
|
@@ -2086,128 +2505,256 @@ var createCommand3 = new Command10("create").description("Create a post in a boa
|
|
|
2086
2505
|
});
|
|
2087
2506
|
var boardCommand = new Command10("board").description("Board operations (requires User OAuth)").addCommand(listCommand5).addCommand(postsCommand).addCommand(readCommand2).addCommand(createCommand3);
|
|
2088
2507
|
|
|
2089
|
-
// src/commands/
|
|
2090
|
-
import { Command as Command12 } from "commander";
|
|
2091
|
-
|
|
2092
|
-
// src/mcp/server.ts
|
|
2093
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2094
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2095
|
-
|
|
2096
|
-
// src/mcp/tools.ts
|
|
2097
|
-
import { z } from "zod";
|
|
2098
|
-
|
|
2099
|
-
// src/commands/doctor.ts
|
|
2508
|
+
// src/commands/contact.ts
|
|
2100
2509
|
import { Command as Command11 } from "commander";
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
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}`);
|
|
2112
2530
|
}
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
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}`);
|
|
2117
2540
|
}
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
} else {
|
|
2127
|
-
results.push({ check: "privateKey", status: "FAIL", detail: `\uD30C\uC77C \uC5C6\uC74C: ${creds.privateKeyPath}` });
|
|
2128
|
-
}
|
|
2129
|
-
} else {
|
|
2130
|
-
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}`);
|
|
2131
2549
|
}
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
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}`);
|
|
2136
2566
|
}
|
|
2137
|
-
const
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
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}`);
|
|
2147
2583
|
}
|
|
2148
|
-
const
|
|
2149
|
-
if (
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
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
|
|
2155
2615
|
});
|
|
2156
|
-
|
|
2157
|
-
|
|
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");
|
|
2158
2626
|
}
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
} else {
|
|
2167
|
-
results.push({ check: "apiConnection", status: "FAIL", detail: `HTTP ${res.status}` });
|
|
2168
|
-
}
|
|
2169
|
-
} catch (e) {
|
|
2170
|
-
results.push({ check: "apiConnection", status: "FAIL", detail: `\uC5F0\uACB0 \uC2E4\uD328: ${e.message}` });
|
|
2171
|
-
}
|
|
2172
|
-
} else if (userToken && userToken.expiresAt > Date.now() / 1e3) {
|
|
2173
|
-
try {
|
|
2174
|
-
const res = await fetch("https://www.worksapis.com/v1.0/users/me", {
|
|
2175
|
-
headers: { Authorization: `Bearer ${userToken.accessToken}` }
|
|
2176
|
-
});
|
|
2177
|
-
if (res.ok) {
|
|
2178
|
-
results.push({ check: "apiConnection", status: "OK", detail: "NAVER WORKS API \uC5F0\uACB0 \uC131\uACF5" });
|
|
2179
|
-
} else {
|
|
2180
|
-
results.push({ check: "apiConnection", status: "FAIL", detail: `HTTP ${res.status}` });
|
|
2181
|
-
}
|
|
2182
|
-
} catch (e) {
|
|
2183
|
-
results.push({ check: "apiConnection", status: "FAIL", detail: `\uC5F0\uACB0 \uC2E4\uD328: ${e.message}` });
|
|
2184
|
-
}
|
|
2185
|
-
} else {
|
|
2186
|
-
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");
|
|
2187
2634
|
}
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
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) => {
|
|
2191
2637
|
try {
|
|
2192
|
-
const
|
|
2193
|
-
if (opts.
|
|
2194
|
-
output(
|
|
2195
|
-
|
|
2196
|
-
console.log("\n nworks doctor\n");
|
|
2197
|
-
for (const r of results) {
|
|
2198
|
-
const icon = r.status === "OK" ? "\u2705" : r.status === "SKIP" ? "\u2796" : "\u274C";
|
|
2199
|
-
console.log(` ${icon} ${r.check.padEnd(16)} ${r.detail}`);
|
|
2200
|
-
}
|
|
2201
|
-
console.log();
|
|
2638
|
+
const payload = JSON.parse(opts.payload);
|
|
2639
|
+
if (opts.dryRun) {
|
|
2640
|
+
output({ dryRun: true, request: payload }, opts);
|
|
2641
|
+
return;
|
|
2202
2642
|
}
|
|
2643
|
+
const result = await createContact(payload, opts.profile);
|
|
2644
|
+
output({ success: true, ...result }, opts);
|
|
2203
2645
|
} catch (err) {
|
|
2204
|
-
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");
|
|
2205
2689
|
}
|
|
2206
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);
|
|
2207
2692
|
|
|
2208
|
-
// src/mcp
|
|
2209
|
-
|
|
2210
|
-
|
|
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,
|
|
2211
2758
|
"nworks_setup",
|
|
2212
2759
|
`NAVER WORKS API \uC778\uC99D \uC815\uBCF4\uB97C \uC124\uC815\uD569\uB2C8\uB2E4.
|
|
2213
2760
|
|
|
@@ -2230,10 +2777,10 @@ function registerTools(server) {
|
|
|
2230
2777
|
|
|
2231
2778
|
OAuth Redirect URI: http://localhost:9876/callback`,
|
|
2232
2779
|
{
|
|
2233
|
-
clientId:
|
|
2234
|
-
serviceAccount:
|
|
2235
|
-
botId:
|
|
2236
|
-
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")
|
|
2237
2784
|
},
|
|
2238
2785
|
async ({ clientId, serviceAccount, botId, domainId }) => {
|
|
2239
2786
|
try {
|
|
@@ -2271,7 +2818,7 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2271
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');
|
|
2272
2819
|
}
|
|
2273
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");
|
|
2274
|
-
const
|
|
2821
|
+
const mask2 = (s) => s.length <= 4 ? "****" : `****${s.slice(-Math.min(4, Math.floor(s.length / 3)))}`;
|
|
2275
2822
|
return {
|
|
2276
2823
|
content: [
|
|
2277
2824
|
{
|
|
@@ -2280,10 +2827,10 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2280
2827
|
success: true,
|
|
2281
2828
|
message: "\uC778\uC99D \uC815\uBCF4\uAC00 \uC800\uC7A5\uB418\uC5C8\uC2B5\uB2C8\uB2E4.",
|
|
2282
2829
|
nextSteps,
|
|
2283
|
-
clientId,
|
|
2284
|
-
clientSecret: `${
|
|
2830
|
+
clientId: mask2(clientId),
|
|
2831
|
+
clientSecret: `${mask2(resolvedSecret)} (\uD658\uACBD\uBCC0\uC218)`,
|
|
2285
2832
|
serviceAccount: serviceAccount ?? null,
|
|
2286
|
-
privateKeyPath: resolvedPrivateKeyPath ? `${
|
|
2833
|
+
privateKeyPath: resolvedPrivateKeyPath ? `${mask2(resolvedPrivateKeyPath)} (\uD658\uACBD\uBCC0\uC218)` : null,
|
|
2287
2834
|
botId: botId ?? null
|
|
2288
2835
|
})
|
|
2289
2836
|
}
|
|
@@ -2297,16 +2844,22 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2297
2844
|
}
|
|
2298
2845
|
}
|
|
2299
2846
|
);
|
|
2300
|
-
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
// src/mcp/tools/message.ts
|
|
2850
|
+
import { z as z3 } from "zod";
|
|
2851
|
+
function registerMessageTools(server) {
|
|
2852
|
+
defineTool(
|
|
2853
|
+
server,
|
|
2301
2854
|
"nworks_message_send",
|
|
2302
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)",
|
|
2303
2856
|
{
|
|
2304
|
-
to:
|
|
2305
|
-
channel:
|
|
2306
|
-
text:
|
|
2307
|
-
type:
|
|
2308
|
-
actions:
|
|
2309
|
-
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)")
|
|
2310
2863
|
},
|
|
2311
2864
|
async ({ to, channel, text, type, actions, elements }) => {
|
|
2312
2865
|
try {
|
|
@@ -2329,11 +2882,12 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2329
2882
|
}
|
|
2330
2883
|
}
|
|
2331
2884
|
);
|
|
2332
|
-
|
|
2885
|
+
defineTool(
|
|
2886
|
+
server,
|
|
2333
2887
|
"nworks_message_members",
|
|
2334
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)",
|
|
2335
2889
|
{
|
|
2336
|
-
channel:
|
|
2890
|
+
channel: z3.string().describe("\uCC44\uB110 channelId")
|
|
2337
2891
|
},
|
|
2338
2892
|
async ({ channel }) => {
|
|
2339
2893
|
try {
|
|
@@ -2349,15 +2903,29 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2349
2903
|
}
|
|
2350
2904
|
}
|
|
2351
2905
|
);
|
|
2352
|
-
|
|
2906
|
+
}
|
|
2907
|
+
|
|
2908
|
+
// src/mcp/tools/directory.ts
|
|
2909
|
+
import { z as z4 } from "zod";
|
|
2910
|
+
function registerDirectoryTools(server) {
|
|
2911
|
+
defineTool(
|
|
2912
|
+
server,
|
|
2353
2913
|
"nworks_directory_members",
|
|
2354
|
-
"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",
|
|
2355
|
-
{
|
|
2356
|
-
|
|
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 }) => {
|
|
2357
2920
|
try {
|
|
2358
|
-
const result = await listUsers();
|
|
2921
|
+
const result = await listUsers(count ?? 100, cursor);
|
|
2359
2922
|
return {
|
|
2360
|
-
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
|
+
}) }]
|
|
2361
2929
|
};
|
|
2362
2930
|
} catch (err) {
|
|
2363
2931
|
return {
|
|
@@ -2367,13 +2935,19 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2367
2935
|
}
|
|
2368
2936
|
}
|
|
2369
2937
|
);
|
|
2370
|
-
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
// src/mcp/tools/calendar.ts
|
|
2941
|
+
import { z as z5 } from "zod";
|
|
2942
|
+
function registerCalendarTools(server) {
|
|
2943
|
+
defineTool(
|
|
2944
|
+
server,
|
|
2371
2945
|
"nworks_calendar_list",
|
|
2372
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",
|
|
2373
2947
|
{
|
|
2374
|
-
fromDateTime:
|
|
2375
|
-
untilDateTime:
|
|
2376
|
-
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)")
|
|
2377
2951
|
},
|
|
2378
2952
|
async ({ fromDateTime, untilDateTime, userId }) => {
|
|
2379
2953
|
try {
|
|
@@ -2402,21 +2976,32 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2402
2976
|
}
|
|
2403
2977
|
}
|
|
2404
2978
|
);
|
|
2405
|
-
|
|
2979
|
+
defineTool(
|
|
2980
|
+
server,
|
|
2406
2981
|
"nworks_calendar_create",
|
|
2407
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)",
|
|
2408
2983
|
{
|
|
2409
|
-
summary:
|
|
2410
|
-
start:
|
|
2411
|
-
end:
|
|
2412
|
-
timeZone:
|
|
2413
|
-
description:
|
|
2414
|
-
location:
|
|
2415
|
-
attendees:
|
|
2416
|
-
sendNotification:
|
|
2417
|
-
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)")
|
|
2418
2993
|
},
|
|
2419
|
-
async ({
|
|
2994
|
+
async ({
|
|
2995
|
+
summary,
|
|
2996
|
+
start,
|
|
2997
|
+
end,
|
|
2998
|
+
timeZone,
|
|
2999
|
+
description,
|
|
3000
|
+
location,
|
|
3001
|
+
attendees,
|
|
3002
|
+
sendNotification,
|
|
3003
|
+
userId
|
|
3004
|
+
}) => {
|
|
2420
3005
|
try {
|
|
2421
3006
|
const result = await createEvent({
|
|
2422
3007
|
summary,
|
|
@@ -2441,21 +3026,32 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2441
3026
|
}
|
|
2442
3027
|
}
|
|
2443
3028
|
);
|
|
2444
|
-
|
|
3029
|
+
defineTool(
|
|
3030
|
+
server,
|
|
2445
3031
|
"nworks_calendar_update",
|
|
2446
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",
|
|
2447
3033
|
{
|
|
2448
|
-
eventId:
|
|
2449
|
-
summary:
|
|
2450
|
-
start:
|
|
2451
|
-
end:
|
|
2452
|
-
timeZone:
|
|
2453
|
-
description:
|
|
2454
|
-
location:
|
|
2455
|
-
sendNotification:
|
|
2456
|
-
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)")
|
|
2457
3043
|
},
|
|
2458
|
-
async ({
|
|
3044
|
+
async ({
|
|
3045
|
+
eventId,
|
|
3046
|
+
summary,
|
|
3047
|
+
start,
|
|
3048
|
+
end,
|
|
3049
|
+
timeZone,
|
|
3050
|
+
description,
|
|
3051
|
+
location,
|
|
3052
|
+
sendNotification,
|
|
3053
|
+
userId
|
|
3054
|
+
}) => {
|
|
2459
3055
|
try {
|
|
2460
3056
|
await updateEvent({
|
|
2461
3057
|
eventId,
|
|
@@ -2479,13 +3075,14 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2479
3075
|
}
|
|
2480
3076
|
}
|
|
2481
3077
|
);
|
|
2482
|
-
|
|
3078
|
+
defineTool(
|
|
3079
|
+
server,
|
|
2483
3080
|
"nworks_calendar_delete",
|
|
2484
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",
|
|
2485
3082
|
{
|
|
2486
|
-
eventId:
|
|
2487
|
-
sendNotification:
|
|
2488
|
-
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)")
|
|
2489
3086
|
},
|
|
2490
3087
|
async ({ eventId, sendNotification, userId }) => {
|
|
2491
3088
|
try {
|
|
@@ -2505,14 +3102,20 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2505
3102
|
}
|
|
2506
3103
|
}
|
|
2507
3104
|
);
|
|
2508
|
-
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
// src/mcp/tools/drive.ts
|
|
3108
|
+
import { z as z6 } from "zod";
|
|
3109
|
+
function registerDriveTools(server) {
|
|
3110
|
+
defineTool(
|
|
3111
|
+
server,
|
|
2509
3112
|
"nworks_drive_list",
|
|
2510
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)",
|
|
2511
3114
|
{
|
|
2512
|
-
userId:
|
|
2513
|
-
folderId:
|
|
2514
|
-
count:
|
|
2515
|
-
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")
|
|
2516
3119
|
},
|
|
2517
3120
|
async ({ userId, folderId, count, cursor }) => {
|
|
2518
3121
|
try {
|
|
@@ -2541,16 +3144,17 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2541
3144
|
}
|
|
2542
3145
|
}
|
|
2543
3146
|
);
|
|
2544
|
-
|
|
3147
|
+
defineTool(
|
|
3148
|
+
server,
|
|
2545
3149
|
"nworks_drive_upload",
|
|
2546
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.",
|
|
2547
3151
|
{
|
|
2548
|
-
content:
|
|
2549
|
-
fileName:
|
|
2550
|
-
filePath:
|
|
2551
|
-
userId:
|
|
2552
|
-
folderId:
|
|
2553
|
-
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)")
|
|
2554
3158
|
},
|
|
2555
3159
|
async ({ content, fileName, filePath, userId, folderId, overwrite }) => {
|
|
2556
3160
|
try {
|
|
@@ -2568,11 +3172,12 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2568
3172
|
overwrite ?? false
|
|
2569
3173
|
);
|
|
2570
3174
|
} else if (filePath) {
|
|
3175
|
+
const safePath = validateLocalPath(filePath);
|
|
2571
3176
|
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
2572
|
-
console.error(`[nworks] MCP upload: filePath=${
|
|
3177
|
+
console.error(`[nworks] MCP upload: filePath=${safePath}`);
|
|
2573
3178
|
}
|
|
2574
3179
|
result = await uploadFile(
|
|
2575
|
-
|
|
3180
|
+
safePath,
|
|
2576
3181
|
userId ?? "me",
|
|
2577
3182
|
folderId,
|
|
2578
3183
|
overwrite ?? false
|
|
@@ -2587,23 +3192,25 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2587
3192
|
content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }]
|
|
2588
3193
|
};
|
|
2589
3194
|
} catch (err) {
|
|
2590
|
-
|
|
2591
|
-
|
|
3195
|
+
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
3196
|
+
console.error(`[nworks] drive upload error: ${err.stack}`);
|
|
3197
|
+
}
|
|
2592
3198
|
return {
|
|
2593
|
-
content: [{ type: "text", text:
|
|
3199
|
+
content: [{ type: "text", text: mcpErrorHint(err, "drive.upload") }],
|
|
2594
3200
|
isError: true
|
|
2595
3201
|
};
|
|
2596
3202
|
}
|
|
2597
3203
|
}
|
|
2598
3204
|
);
|
|
2599
|
-
|
|
3205
|
+
defineTool(
|
|
3206
|
+
server,
|
|
2600
3207
|
"nworks_drive_download",
|
|
2601
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.",
|
|
2602
3209
|
{
|
|
2603
|
-
fileId:
|
|
2604
|
-
outputDir:
|
|
2605
|
-
outputName:
|
|
2606
|
-
userId:
|
|
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)")
|
|
2607
3214
|
},
|
|
2608
3215
|
async ({ fileId, outputDir, outputName, userId }) => {
|
|
2609
3216
|
try {
|
|
@@ -2613,10 +3220,13 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2613
3220
|
);
|
|
2614
3221
|
const fileName = outputName ?? result.fileName ?? fileId;
|
|
2615
3222
|
if (outputDir) {
|
|
2616
|
-
const { writeFile:
|
|
2617
|
-
const { join:
|
|
2618
|
-
const
|
|
2619
|
-
|
|
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);
|
|
2620
3230
|
return {
|
|
2621
3231
|
content: [{ type: "text", text: JSON.stringify({ success: true, fileName, path: outPath, size: result.buffer.length }) }]
|
|
2622
3232
|
};
|
|
@@ -2649,60 +3259,180 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2649
3259
|
}
|
|
2650
3260
|
}
|
|
2651
3261
|
);
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
"
|
|
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)",
|
|
2655
3266
|
{
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
body: z.string().optional().describe("\uBA54\uC77C \uBCF8\uBB38"),
|
|
2659
|
-
cc: z.string().optional().describe("\uCC38\uC870 \uC774\uBA54\uC77C (\uC5EC\uB7EC \uBA85\uC740 ; \uB85C \uAD6C\uBD84)"),
|
|
2660
|
-
bcc: z.string().optional().describe("\uC228\uC740\uCC38\uC870 \uC774\uBA54\uC77C (\uC5EC\uB7EC \uBA85\uC740 ; \uB85C \uAD6C\uBD84)"),
|
|
2661
|
-
contentType: z.enum(["html", "text"]).optional().describe("\uBCF8\uBB38 \uD615\uC2DD (\uAE30\uBCF8: html)"),
|
|
2662
|
-
userId: z.string().optional().describe("\uBC1C\uC2E0\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
|
|
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")
|
|
2663
3269
|
},
|
|
2664
|
-
async ({
|
|
3270
|
+
async ({ count, cursor }) => {
|
|
2665
3271
|
try {
|
|
2666
|
-
await
|
|
2667
|
-
to,
|
|
2668
|
-
subject,
|
|
2669
|
-
body,
|
|
2670
|
-
cc,
|
|
2671
|
-
bcc,
|
|
2672
|
-
contentType,
|
|
2673
|
-
userId: userId ?? "me"
|
|
2674
|
-
});
|
|
3272
|
+
const result = await listSharedDrives(count ?? 20, cursor);
|
|
2675
3273
|
return {
|
|
2676
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
3274
|
+
content: [{ type: "text", text: JSON.stringify({ sharedDrives: result.sharedDrives, count: result.sharedDrives.length, hasMore: !!result.responseMetaData?.nextCursor, nextCursor: result.responseMetaData?.nextCursor ?? null }) }]
|
|
2677
3275
|
};
|
|
2678
3276
|
} catch (err) {
|
|
2679
3277
|
return {
|
|
2680
|
-
content: [{ type: "text", text: mcpErrorHint(err, "
|
|
3278
|
+
content: [{ type: "text", text: mcpErrorHint(err, "drive.sharedrive_list") }],
|
|
2681
3279
|
isError: true
|
|
2682
3280
|
};
|
|
2683
3281
|
}
|
|
2684
3282
|
}
|
|
2685
3283
|
);
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
"
|
|
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)",
|
|
2689
3288
|
{
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
|
|
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")
|
|
2695
3293
|
},
|
|
2696
|
-
async ({ folderId, count, cursor
|
|
3294
|
+
async ({ sharedriveId, folderId, count, cursor }) => {
|
|
2697
3295
|
try {
|
|
2698
|
-
const result = await
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
count ??
|
|
2702
|
-
cursor
|
|
2703
|
-
|
|
2704
|
-
)
|
|
2705
|
-
|
|
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 }) }]
|
|
3312
|
+
};
|
|
3313
|
+
} catch (err) {
|
|
3314
|
+
return {
|
|
3315
|
+
content: [{ type: "text", text: mcpErrorHint(err, "drive.sharedrive_files") }],
|
|
3316
|
+
isError: true
|
|
3317
|
+
};
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
);
|
|
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.",
|
|
3325
|
+
{
|
|
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)")
|
|
3330
|
+
},
|
|
3331
|
+
async ({ sharedriveId, fileId, outputDir, outputName }) => {
|
|
3332
|
+
try {
|
|
3333
|
+
const result = await downloadSharedDriveFile(sharedriveId, fileId);
|
|
3334
|
+
const fileName = outputName ?? result.fileName ?? fileId;
|
|
3335
|
+
if (outputDir) {
|
|
3336
|
+
const { writeFile: writeFile4 } = await import("fs/promises");
|
|
3337
|
+
const { join: join5 } = await import("path");
|
|
3338
|
+
const safeDir = validateLocalPath(outputDir);
|
|
3339
|
+
const safeName = sanitizeFileName(fileName);
|
|
3340
|
+
const outPath = join5(safeDir, safeName);
|
|
3341
|
+
validateLocalPath(outPath, safeDir);
|
|
3342
|
+
await writeFile4(outPath, result.buffer);
|
|
3343
|
+
return {
|
|
3344
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, fileName, path: outPath, size: result.buffer.length }) }]
|
|
3345
|
+
};
|
|
3346
|
+
}
|
|
3347
|
+
const MAX_INLINE_SIZE = 5 * 1024 * 1024;
|
|
3348
|
+
if (result.buffer.length > MAX_INLINE_SIZE) {
|
|
3349
|
+
const sizeMB = (result.buffer.length / (1024 * 1024)).toFixed(1);
|
|
3350
|
+
return {
|
|
3351
|
+
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 }) }],
|
|
3352
|
+
isError: true
|
|
3353
|
+
};
|
|
3354
|
+
}
|
|
3355
|
+
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;
|
|
3356
|
+
const isText = textExtensions.test(fileName);
|
|
3357
|
+
if (isText) {
|
|
3358
|
+
const text = result.buffer.toString("utf-8");
|
|
3359
|
+
return {
|
|
3360
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, fileName, size: result.buffer.length, encoding: "text", content: text }) }]
|
|
3361
|
+
};
|
|
3362
|
+
}
|
|
3363
|
+
const base64 = result.buffer.toString("base64");
|
|
3364
|
+
return {
|
|
3365
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, fileName, size: result.buffer.length, encoding: "base64", content: base64 }) }]
|
|
3366
|
+
};
|
|
3367
|
+
} catch (err) {
|
|
3368
|
+
return {
|
|
3369
|
+
content: [{ type: "text", text: mcpErrorHint(err, "drive.sharedrive_download") }],
|
|
3370
|
+
isError: true
|
|
3371
|
+
};
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
);
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3377
|
+
// src/mcp/tools/mail.ts
|
|
3378
|
+
import { z as z7 } from "zod";
|
|
3379
|
+
function registerMailTools(server) {
|
|
3380
|
+
defineTool(
|
|
3381
|
+
server,
|
|
3382
|
+
"nworks_mail_send",
|
|
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)",
|
|
3384
|
+
{
|
|
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)")
|
|
3392
|
+
},
|
|
3393
|
+
async ({ to, subject, body, cc, bcc, contentType, userId }) => {
|
|
3394
|
+
try {
|
|
3395
|
+
await sendMail({
|
|
3396
|
+
to,
|
|
3397
|
+
subject,
|
|
3398
|
+
body,
|
|
3399
|
+
cc,
|
|
3400
|
+
bcc,
|
|
3401
|
+
contentType,
|
|
3402
|
+
userId: userId ?? "me"
|
|
3403
|
+
});
|
|
3404
|
+
return {
|
|
3405
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, message: "\uBA54\uC77C\uC774 \uC804\uC1A1\uB418\uC5C8\uC2B5\uB2C8\uB2E4 (\uBE44\uB3D9\uAE30 \uCC98\uB9AC)" }) }]
|
|
3406
|
+
};
|
|
3407
|
+
} catch (err) {
|
|
3408
|
+
return {
|
|
3409
|
+
content: [{ type: "text", text: mcpErrorHint(err, "mail.send") }],
|
|
3410
|
+
isError: true
|
|
3411
|
+
};
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
);
|
|
3415
|
+
defineTool(
|
|
3416
|
+
server,
|
|
3417
|
+
"nworks_mail_list",
|
|
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)",
|
|
3419
|
+
{
|
|
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)")
|
|
3425
|
+
},
|
|
3426
|
+
async ({ folderId, count, cursor, isUnread, userId }) => {
|
|
3427
|
+
try {
|
|
3428
|
+
const result = await listMails(
|
|
3429
|
+
folderId ?? 0,
|
|
3430
|
+
userId ?? "me",
|
|
3431
|
+
count ?? 30,
|
|
3432
|
+
cursor,
|
|
3433
|
+
isUnread
|
|
3434
|
+
);
|
|
3435
|
+
const mails = result.mails.map((m) => ({
|
|
2706
3436
|
mailId: m.mailId,
|
|
2707
3437
|
from: m.from.email,
|
|
2708
3438
|
subject: m.subject,
|
|
@@ -2721,12 +3451,13 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2721
3451
|
}
|
|
2722
3452
|
}
|
|
2723
3453
|
);
|
|
2724
|
-
|
|
3454
|
+
defineTool(
|
|
3455
|
+
server,
|
|
2725
3456
|
"nworks_mail_read",
|
|
2726
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)",
|
|
2727
3458
|
{
|
|
2728
|
-
mailId:
|
|
2729
|
-
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)")
|
|
2730
3461
|
},
|
|
2731
3462
|
async ({ mailId, userId }) => {
|
|
2732
3463
|
try {
|
|
@@ -2760,15 +3491,80 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2760
3491
|
}
|
|
2761
3492
|
}
|
|
2762
3493
|
);
|
|
2763
|
-
|
|
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,
|
|
2764
3560
|
"nworks_task_list",
|
|
2765
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)",
|
|
2766
3562
|
{
|
|
2767
|
-
categoryId:
|
|
2768
|
-
status:
|
|
2769
|
-
count:
|
|
2770
|
-
cursor:
|
|
2771
|
-
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)")
|
|
2772
3568
|
},
|
|
2773
3569
|
async ({ categoryId, status, count, cursor, userId }) => {
|
|
2774
3570
|
try {
|
|
@@ -2798,16 +3594,17 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2798
3594
|
}
|
|
2799
3595
|
}
|
|
2800
3596
|
);
|
|
2801
|
-
|
|
3597
|
+
defineTool(
|
|
3598
|
+
server,
|
|
2802
3599
|
"nworks_task_create",
|
|
2803
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)",
|
|
2804
3601
|
{
|
|
2805
|
-
title:
|
|
2806
|
-
content:
|
|
2807
|
-
dueDate:
|
|
2808
|
-
categoryId:
|
|
2809
|
-
assigneeIds:
|
|
2810
|
-
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)")
|
|
2811
3608
|
},
|
|
2812
3609
|
async ({ title, content, dueDate, categoryId, assigneeIds, userId }) => {
|
|
2813
3610
|
try {
|
|
@@ -2830,15 +3627,16 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2830
3627
|
}
|
|
2831
3628
|
}
|
|
2832
3629
|
);
|
|
2833
|
-
|
|
3630
|
+
defineTool(
|
|
3631
|
+
server,
|
|
2834
3632
|
"nworks_task_update",
|
|
2835
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)",
|
|
2836
3634
|
{
|
|
2837
|
-
taskId:
|
|
2838
|
-
status:
|
|
2839
|
-
title:
|
|
2840
|
-
content:
|
|
2841
|
-
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)")
|
|
2842
3640
|
},
|
|
2843
3641
|
async ({ taskId, status, title, content, dueDate }) => {
|
|
2844
3642
|
try {
|
|
@@ -2872,11 +3670,12 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2872
3670
|
}
|
|
2873
3671
|
}
|
|
2874
3672
|
);
|
|
2875
|
-
|
|
3673
|
+
defineTool(
|
|
3674
|
+
server,
|
|
2876
3675
|
"nworks_task_delete",
|
|
2877
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)",
|
|
2878
3677
|
{
|
|
2879
|
-
taskId:
|
|
3678
|
+
taskId: z8.string().describe("\uC0AD\uC81C\uD560 \uD560 \uC77C ID (nworks_task_list\uB85C \uC870\uD68C \uAC00\uB2A5)")
|
|
2880
3679
|
},
|
|
2881
3680
|
async ({ taskId }) => {
|
|
2882
3681
|
try {
|
|
@@ -2892,12 +3691,18 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2892
3691
|
}
|
|
2893
3692
|
}
|
|
2894
3693
|
);
|
|
2895
|
-
|
|
3694
|
+
}
|
|
3695
|
+
|
|
3696
|
+
// src/mcp/tools/board.ts
|
|
3697
|
+
import { z as z9 } from "zod";
|
|
3698
|
+
function registerBoardTools(server) {
|
|
3699
|
+
defineTool(
|
|
3700
|
+
server,
|
|
2896
3701
|
"nworks_board_list",
|
|
2897
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)",
|
|
2898
3703
|
{
|
|
2899
|
-
count:
|
|
2900
|
-
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")
|
|
2901
3706
|
},
|
|
2902
3707
|
async ({ count, cursor }) => {
|
|
2903
3708
|
try {
|
|
@@ -2918,13 +3723,14 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2918
3723
|
}
|
|
2919
3724
|
}
|
|
2920
3725
|
);
|
|
2921
|
-
|
|
3726
|
+
defineTool(
|
|
3727
|
+
server,
|
|
2922
3728
|
"nworks_board_posts",
|
|
2923
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)",
|
|
2924
3730
|
{
|
|
2925
|
-
boardId:
|
|
2926
|
-
count:
|
|
2927
|
-
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")
|
|
2928
3734
|
},
|
|
2929
3735
|
async ({ boardId, count, cursor }) => {
|
|
2930
3736
|
try {
|
|
@@ -2948,12 +3754,13 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2948
3754
|
}
|
|
2949
3755
|
}
|
|
2950
3756
|
);
|
|
2951
|
-
|
|
3757
|
+
defineTool(
|
|
3758
|
+
server,
|
|
2952
3759
|
"nworks_board_read",
|
|
2953
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)",
|
|
2954
3761
|
{
|
|
2955
|
-
boardId:
|
|
2956
|
-
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)")
|
|
2957
3764
|
},
|
|
2958
3765
|
async ({ boardId, postId }) => {
|
|
2959
3766
|
try {
|
|
@@ -2979,15 +3786,16 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
2979
3786
|
}
|
|
2980
3787
|
}
|
|
2981
3788
|
);
|
|
2982
|
-
|
|
3789
|
+
defineTool(
|
|
3790
|
+
server,
|
|
2983
3791
|
"nworks_board_create",
|
|
2984
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)",
|
|
2985
3793
|
{
|
|
2986
|
-
boardId:
|
|
2987
|
-
title:
|
|
2988
|
-
body:
|
|
2989
|
-
enableComment:
|
|
2990
|
-
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)")
|
|
2991
3799
|
},
|
|
2992
3800
|
async ({ boardId, title, body, enableComment, sendNotifications }) => {
|
|
2993
3801
|
try {
|
|
@@ -3009,36 +3817,176 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3009
3817
|
}
|
|
3010
3818
|
}
|
|
3011
3819
|
);
|
|
3012
|
-
|
|
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,
|
|
3013
3974
|
"nworks_login_user",
|
|
3014
|
-
"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).",
|
|
3015
3976
|
{
|
|
3016
|
-
|
|
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")
|
|
3017
3979
|
},
|
|
3018
|
-
async ({ scope }) => {
|
|
3019
|
-
const DEFAULT_SCOPE = "calendar calendar.read file file.read mail mail.read task task.read user.read board board.read";
|
|
3980
|
+
async ({ preset, scope }) => {
|
|
3020
3981
|
try {
|
|
3021
3982
|
const creds = await loadCredentials();
|
|
3022
|
-
const SCOPE_DEPS = {
|
|
3023
|
-
calendar: ["calendar.read"],
|
|
3024
|
-
task: ["user.read"],
|
|
3025
|
-
"task.read": ["user.read"]
|
|
3026
|
-
};
|
|
3027
|
-
const expandScopes = (scopes) => {
|
|
3028
|
-
const expanded = new Set(scopes);
|
|
3029
|
-
for (const s of scopes) {
|
|
3030
|
-
const deps = SCOPE_DEPS[s];
|
|
3031
|
-
if (deps) deps.forEach((d) => expanded.add(d));
|
|
3032
|
-
}
|
|
3033
|
-
return [...expanded];
|
|
3034
|
-
};
|
|
3035
3983
|
const existingToken = await loadUserToken();
|
|
3036
3984
|
const existingScopes = existingToken?.scope?.split(" ").filter(Boolean) ?? [];
|
|
3037
|
-
const requestedScopes =
|
|
3038
|
-
const mergedScopes =
|
|
3039
|
-
const state =
|
|
3985
|
+
const requestedScopes = resolveScopes(preset ?? scope);
|
|
3986
|
+
const mergedScopes = mergeScopes(existingScopes, requestedScopes);
|
|
3987
|
+
const state = generateSecureState();
|
|
3040
3988
|
const authorizeUrl = buildAuthorizeUrl(creds.clientId, mergedScopes, state);
|
|
3041
|
-
startOAuthCallbackServer(creds.clientId, creds.clientSecret).then(
|
|
3989
|
+
startOAuthCallbackServer(creds.clientId, creds.clientSecret, state).then(
|
|
3042
3990
|
(token) => saveUserToken({
|
|
3043
3991
|
accessToken: token.accessToken,
|
|
3044
3992
|
refreshToken: token.refreshToken,
|
|
@@ -3072,7 +4020,8 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3072
4020
|
}
|
|
3073
4021
|
}
|
|
3074
4022
|
);
|
|
3075
|
-
|
|
4023
|
+
defineTool(
|
|
4024
|
+
server,
|
|
3076
4025
|
"nworks_whoami",
|
|
3077
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",
|
|
3078
4027
|
{},
|
|
@@ -3083,9 +4032,10 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3083
4032
|
const userToken = await loadUserToken();
|
|
3084
4033
|
const isValid = token ? token.expiresAt > Date.now() / 1e3 : false;
|
|
3085
4034
|
const userTokenValid = userToken ? userToken.expiresAt > Date.now() / 1e3 : false;
|
|
4035
|
+
const mask2 = (s) => s.length <= 4 ? "****" : `****${s.slice(-4)}`;
|
|
3086
4036
|
const info = {
|
|
3087
4037
|
serviceAccount: creds.serviceAccount ?? null,
|
|
3088
|
-
clientId: creds.clientId,
|
|
4038
|
+
clientId: mask2(creds.clientId),
|
|
3089
4039
|
botId: creds.botId ?? null,
|
|
3090
4040
|
tokenValid: isValid,
|
|
3091
4041
|
userOAuth: userToken ? { valid: userTokenValid, scope: userToken.scope } : null
|
|
@@ -3101,12 +4051,25 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3101
4051
|
}
|
|
3102
4052
|
}
|
|
3103
4053
|
);
|
|
3104
|
-
|
|
4054
|
+
defineTool(
|
|
4055
|
+
server,
|
|
3105
4056
|
"nworks_logout",
|
|
3106
4057
|
"\uC800\uC7A5\uB41C NAVER WORKS \uC778\uC99D \uC815\uBCF4\uC640 \uD1A0\uD070\uC744 \uBAA8\uB450 \uC0AD\uC81C\uD569\uB2C8\uB2E4",
|
|
3107
4058
|
{},
|
|
3108
4059
|
async () => {
|
|
3109
4060
|
try {
|
|
4061
|
+
try {
|
|
4062
|
+
const creds = await loadCredentials();
|
|
4063
|
+
const token = await loadToken();
|
|
4064
|
+
const userToken = await loadUserToken();
|
|
4065
|
+
if (token?.accessToken) {
|
|
4066
|
+
await revokeToken(token.accessToken, creds.clientId, creds.clientSecret);
|
|
4067
|
+
}
|
|
4068
|
+
if (userToken?.refreshToken) {
|
|
4069
|
+
await revokeToken(userToken.refreshToken, creds.clientId, creds.clientSecret);
|
|
4070
|
+
}
|
|
4071
|
+
} catch {
|
|
4072
|
+
}
|
|
3110
4073
|
await clearCredentials();
|
|
3111
4074
|
return {
|
|
3112
4075
|
content: [
|
|
@@ -3127,15 +4090,162 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3127
4090
|
}
|
|
3128
4091
|
}
|
|
3129
4092
|
);
|
|
3130
|
-
|
|
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,
|
|
3131
4228
|
"nworks_doctor",
|
|
3132
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.",
|
|
3133
4230
|
{},
|
|
3134
4231
|
async () => {
|
|
3135
4232
|
try {
|
|
3136
4233
|
const results = await runChecks("default");
|
|
4234
|
+
const maskedResults = results.map((r) => {
|
|
4235
|
+
if (r.check === "credentials" && r.status === "OK") {
|
|
4236
|
+
return { ...r, detail: r.detail.replace(/clientId: .+/, "clientId: ****") };
|
|
4237
|
+
}
|
|
4238
|
+
if (r.check === "privateKey" && r.status === "OK") {
|
|
4239
|
+
return { ...r, detail: "OK (path hidden)" };
|
|
4240
|
+
}
|
|
4241
|
+
if (r.check === "serviceAccount" && r.status === "OK") {
|
|
4242
|
+
const masked = r.detail.length <= 4 ? "****" : `****${r.detail.slice(-4)}`;
|
|
4243
|
+
return { ...r, detail: masked };
|
|
4244
|
+
}
|
|
4245
|
+
return r;
|
|
4246
|
+
});
|
|
3137
4247
|
return {
|
|
3138
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
4248
|
+
content: [{ type: "text", text: JSON.stringify(maskedResults) }]
|
|
3139
4249
|
};
|
|
3140
4250
|
} catch (err) {
|
|
3141
4251
|
return {
|
|
@@ -3147,6 +4257,21 @@ OAuth Redirect URI: http://localhost:9876/callback`,
|
|
|
3147
4257
|
);
|
|
3148
4258
|
}
|
|
3149
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
|
+
|
|
3150
4275
|
// src/mcp/server.ts
|
|
3151
4276
|
async function startMcpServer() {
|
|
3152
4277
|
try {
|
|
@@ -3167,7 +4292,7 @@ async function startMcpServer() {
|
|
|
3167
4292
|
}
|
|
3168
4293
|
const server = new McpServer({
|
|
3169
4294
|
name: "nworks",
|
|
3170
|
-
version:
|
|
4295
|
+
version: VERSION
|
|
3171
4296
|
});
|
|
3172
4297
|
registerTools(server);
|
|
3173
4298
|
const transport = new StdioServerTransport();
|
|
@@ -3175,22 +4300,31 @@ async function startMcpServer() {
|
|
|
3175
4300
|
}
|
|
3176
4301
|
|
|
3177
4302
|
// src/commands/mcp-cmd.ts
|
|
3178
|
-
|
|
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) => {
|
|
3179
4313
|
if (opts.listTools) {
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
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`);
|
|
3185
4321
|
return;
|
|
3186
4322
|
}
|
|
3187
4323
|
await startMcpServer();
|
|
3188
4324
|
});
|
|
3189
4325
|
|
|
3190
4326
|
// src/index.ts
|
|
3191
|
-
var
|
|
3192
|
-
var { version } = require2("../package.json");
|
|
3193
|
-
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);
|
|
3194
4328
|
program.addCommand(loginCommand);
|
|
3195
4329
|
program.addCommand(logoutCommand);
|
|
3196
4330
|
program.addCommand(whoamiCommand);
|
|
@@ -3201,6 +4335,7 @@ program.addCommand(driveCommand);
|
|
|
3201
4335
|
program.addCommand(mailCommand);
|
|
3202
4336
|
program.addCommand(taskCommand);
|
|
3203
4337
|
program.addCommand(boardCommand);
|
|
4338
|
+
program.addCommand(contactCommand);
|
|
3204
4339
|
program.addCommand(mcpCommand);
|
|
3205
4340
|
program.addCommand(doctorCommand);
|
|
3206
4341
|
program.parse();
|