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