nworks 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -10
- package/dist/index.js +357 -44
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +143 -1
- package/dist/mcp.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ npm install -g nworks
|
|
|
17
17
|
## Quick Start
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
# 로그인
|
|
20
|
+
# 서비스 계정 로그인
|
|
21
21
|
nworks login \
|
|
22
22
|
--client-id <CLIENT_ID> \
|
|
23
23
|
--client-secret <CLIENT_SECRET> \
|
|
@@ -25,6 +25,9 @@ nworks login \
|
|
|
25
25
|
--private-key <PATH_TO_KEY> \
|
|
26
26
|
--bot-id <BOT_ID>
|
|
27
27
|
|
|
28
|
+
# User OAuth 로그인 (캘린더 등 사용자 API용)
|
|
29
|
+
nworks login --user --scope calendar.read
|
|
30
|
+
|
|
28
31
|
# 인증 확인
|
|
29
32
|
nworks whoami
|
|
30
33
|
|
|
@@ -33,6 +36,9 @@ nworks message send --to <userId> --text "배포 완료했습니다"
|
|
|
33
36
|
|
|
34
37
|
# 조직 구성원 조회
|
|
35
38
|
nworks directory members
|
|
39
|
+
|
|
40
|
+
# 오늘 일정 조회
|
|
41
|
+
nworks calendar list
|
|
36
42
|
```
|
|
37
43
|
|
|
38
44
|
## CLI Commands
|
|
@@ -40,9 +46,11 @@ nworks directory members
|
|
|
40
46
|
### 인증
|
|
41
47
|
|
|
42
48
|
```bash
|
|
43
|
-
nworks login [options]
|
|
44
|
-
nworks
|
|
45
|
-
nworks
|
|
49
|
+
nworks login [options] # 서비스 계정 로그인 (대화형 또는 플래그)
|
|
50
|
+
nworks login --user # User OAuth 로그인 (브라우저)
|
|
51
|
+
nworks login --user --scope calendar.read # scope 지정
|
|
52
|
+
nworks whoami # 인증 상태 확인
|
|
53
|
+
nworks logout # 로그아웃
|
|
46
54
|
```
|
|
47
55
|
|
|
48
56
|
### 메시지 (Bot API)
|
|
@@ -72,6 +80,21 @@ nworks message members --channel <channelId>
|
|
|
72
80
|
nworks directory members # 조직 구성원 목록
|
|
73
81
|
```
|
|
74
82
|
|
|
83
|
+
### 캘린더 (User OAuth 필요)
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# 오늘 일정 조회
|
|
87
|
+
nworks calendar list
|
|
88
|
+
|
|
89
|
+
# 기간 지정
|
|
90
|
+
nworks calendar list --from 2026-03-01T00:00:00+09:00 --until 2026-03-31T23:59:59+09:00
|
|
91
|
+
|
|
92
|
+
# 특정 사용자의 일정
|
|
93
|
+
nworks calendar list --user <userId>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
> **Note**: 캘린더 API는 User OAuth가 필요합니다. 먼저 `nworks login --user --scope calendar.read`를 실행하세요.
|
|
97
|
+
|
|
75
98
|
### MCP 서버
|
|
76
99
|
|
|
77
100
|
```bash
|
|
@@ -105,17 +128,20 @@ nworks mcp --list-tools # 등록된 tool 목록
|
|
|
105
128
|
|
|
106
129
|
[NAVER WORKS Developer Console](https://dev.worksmobile.com)에서 앱의 OAuth Scope를 추가해야 합니다.
|
|
107
130
|
|
|
108
|
-
| Scope | 용도 | 필요한 명령어 |
|
|
109
|
-
|
|
110
|
-
| `bot` | Bot 메시지 전송 | `message send` |
|
|
111
|
-
| `bot.read` | Bot 채널/구성원 조회 | `message members` |
|
|
112
|
-
| `user.read` | 조직 구성원 조회 | `directory members` |
|
|
131
|
+
| Scope | 용도 | 인증 방식 | 필요한 명령어 |
|
|
132
|
+
|-------|------|----------|--------------|
|
|
133
|
+
| `bot` | Bot 메시지 전송 | Service Account | `message send` |
|
|
134
|
+
| `bot.read` | Bot 채널/구성원 조회 | Service Account | `message members` |
|
|
135
|
+
| `user.read` | 조직 구성원 조회 | Service Account | `directory members` |
|
|
136
|
+
| `calendar.read` | 캘린더 일정 조회 | User OAuth | `calendar list` |
|
|
113
137
|
|
|
114
138
|
> **Tip**: scope를 변경한 후에는 토큰을 재발급해야 합니다.
|
|
115
139
|
> ```bash
|
|
116
140
|
> nworks logout && nworks login ...
|
|
117
141
|
> ```
|
|
118
142
|
|
|
143
|
+
> **Developer Console 설정**: User OAuth를 사용하려면 Developer Console에서 Redirect URL에 `http://localhost:9876/callback`을 등록해야 합니다.
|
|
144
|
+
|
|
119
145
|
## 사용 시나리오
|
|
120
146
|
|
|
121
147
|
### CI/CD 배포 알림
|
|
@@ -158,8 +184,10 @@ NWORKS_VERBOSE=1 # optional, 디버그 로깅
|
|
|
158
184
|
|
|
159
185
|
## Roadmap
|
|
160
186
|
|
|
161
|
-
-
|
|
187
|
+
- ~~**v0.1** — 메시지, 조직 구성원, MCP 서버~~
|
|
188
|
+
- ~~**v0.2** — 캘린더 일정 조회 + User OAuth~~
|
|
162
189
|
- **v0.3** — 드라이브 파일 조회/업로드 (`nworks drive`)
|
|
190
|
+
- **v0.4** — 게시판, 메일 (`nworks board`, `nworks mail`)
|
|
163
191
|
|
|
164
192
|
## License
|
|
165
193
|
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ var __export = (target, all) => {
|
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
// src/index.ts
|
|
9
|
-
import { Command as
|
|
9
|
+
import { Command as Command8 } from "commander";
|
|
10
10
|
|
|
11
11
|
// src/commands/login.ts
|
|
12
12
|
import { Command } from "commander";
|
|
@@ -42,6 +42,7 @@ var ApiError = class extends Error {
|
|
|
42
42
|
var CONFIG_DIR = join(homedir(), ".config", "nworks");
|
|
43
43
|
var CREDENTIALS_PATH = join(CONFIG_DIR, "credentials.json");
|
|
44
44
|
var TOKEN_PATH = join(CONFIG_DIR, "token.json");
|
|
45
|
+
var USER_TOKEN_PATH = join(CONFIG_DIR, "user-token.json");
|
|
45
46
|
async function ensureConfigDir() {
|
|
46
47
|
if (!existsSync(CONFIG_DIR)) {
|
|
47
48
|
await mkdir(CONFIG_DIR, { recursive: true });
|
|
@@ -112,6 +113,29 @@ async function saveToken(token, profile = "default") {
|
|
|
112
113
|
tokens[profile] = token;
|
|
113
114
|
await writeFile(TOKEN_PATH, JSON.stringify(tokens, null, 2), "utf-8");
|
|
114
115
|
}
|
|
116
|
+
async function loadUserToken(profile = "default") {
|
|
117
|
+
if (!existsSync(USER_TOKEN_PATH)) return null;
|
|
118
|
+
const raw = await readFile(USER_TOKEN_PATH, "utf-8");
|
|
119
|
+
const tokens = JSON.parse(raw);
|
|
120
|
+
const entry = tokens[profile];
|
|
121
|
+
if (!entry) return null;
|
|
122
|
+
return {
|
|
123
|
+
accessToken: String(entry["accessToken"]),
|
|
124
|
+
refreshToken: String(entry["refreshToken"]),
|
|
125
|
+
expiresAt: Number(entry["expiresAt"]),
|
|
126
|
+
scope: String(entry["scope"] ?? "")
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
async function saveUserToken(token, profile = "default") {
|
|
130
|
+
await ensureConfigDir();
|
|
131
|
+
let tokens = {};
|
|
132
|
+
if (existsSync(USER_TOKEN_PATH)) {
|
|
133
|
+
const raw = await readFile(USER_TOKEN_PATH, "utf-8");
|
|
134
|
+
tokens = JSON.parse(raw);
|
|
135
|
+
}
|
|
136
|
+
tokens[profile] = token;
|
|
137
|
+
await writeFile(USER_TOKEN_PATH, JSON.stringify(tokens, null, 2), "utf-8");
|
|
138
|
+
}
|
|
115
139
|
async function clearCredentials(profile = "default") {
|
|
116
140
|
if (existsSync(CREDENTIALS_PATH)) {
|
|
117
141
|
const raw = await readFile(CREDENTIALS_PATH, "utf-8");
|
|
@@ -129,6 +153,12 @@ async function clearCredentials(profile = "default") {
|
|
|
129
153
|
delete tokens[profile];
|
|
130
154
|
await writeFile(TOKEN_PATH, JSON.stringify(tokens, null, 2), "utf-8");
|
|
131
155
|
}
|
|
156
|
+
if (existsSync(USER_TOKEN_PATH)) {
|
|
157
|
+
const raw = await readFile(USER_TOKEN_PATH, "utf-8");
|
|
158
|
+
const tokens = JSON.parse(raw);
|
|
159
|
+
delete tokens[profile];
|
|
160
|
+
await writeFile(USER_TOKEN_PATH, JSON.stringify(tokens, null, 2), "utf-8");
|
|
161
|
+
}
|
|
132
162
|
}
|
|
133
163
|
|
|
134
164
|
// src/auth/jwt.ts
|
|
@@ -184,6 +214,124 @@ async function refreshToken(profile = "default") {
|
|
|
184
214
|
return tokenData.accessToken;
|
|
185
215
|
}
|
|
186
216
|
|
|
217
|
+
// src/auth/oauth-user.ts
|
|
218
|
+
import { createServer } from "http";
|
|
219
|
+
import { URL as URL2 } from "url";
|
|
220
|
+
var AUTH_URL2 = "https://auth.worksmobile.com/oauth2/v2.0/authorize";
|
|
221
|
+
var TOKEN_URL = "https://auth.worksmobile.com/oauth2/v2.0/token";
|
|
222
|
+
var REDIRECT_PORT = 9876;
|
|
223
|
+
var REDIRECT_URI = `http://localhost:${REDIRECT_PORT}/callback`;
|
|
224
|
+
function buildAuthorizeUrl(clientId, scope, state) {
|
|
225
|
+
const params = new URLSearchParams({
|
|
226
|
+
client_id: clientId,
|
|
227
|
+
redirect_uri: REDIRECT_URI,
|
|
228
|
+
scope,
|
|
229
|
+
response_type: "code",
|
|
230
|
+
state
|
|
231
|
+
});
|
|
232
|
+
return `${AUTH_URL2}?${params.toString()}`;
|
|
233
|
+
}
|
|
234
|
+
async function startUserOAuthFlow(scope, profile = "default") {
|
|
235
|
+
const creds = await loadCredentials(profile);
|
|
236
|
+
const code = await waitForAuthCode();
|
|
237
|
+
return exchangeCodeForToken(code, creds.clientId, creds.clientSecret);
|
|
238
|
+
}
|
|
239
|
+
function waitForAuthCode() {
|
|
240
|
+
return new Promise((resolve, reject) => {
|
|
241
|
+
const timeout = setTimeout(() => {
|
|
242
|
+
server.close();
|
|
243
|
+
reject(new AuthError("OAuth login timed out (120s). Try again."));
|
|
244
|
+
}, 12e4);
|
|
245
|
+
const server = createServer((req, res) => {
|
|
246
|
+
const url2 = new URL2(req.url ?? "/", `http://localhost:${REDIRECT_PORT}`);
|
|
247
|
+
if (url2.pathname !== "/callback") {
|
|
248
|
+
res.writeHead(404);
|
|
249
|
+
res.end("Not found");
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const code = url2.searchParams.get("code");
|
|
253
|
+
const error48 = url2.searchParams.get("error");
|
|
254
|
+
if (error48) {
|
|
255
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
256
|
+
res.end("<h2>\uB85C\uADF8\uC778 \uC2E4\uD328</h2><p>\uC774 \uCC3D\uC744 \uB2EB\uC544\uB3C4 \uB429\uB2C8\uB2E4.</p>");
|
|
257
|
+
clearTimeout(timeout);
|
|
258
|
+
server.close();
|
|
259
|
+
reject(new AuthError(`OAuth error: ${error48}`));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (!code) {
|
|
263
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
264
|
+
res.end("<h2>\uC798\uBABB\uB41C \uC694\uCCAD</h2><p>Authorization code\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.</p>");
|
|
265
|
+
clearTimeout(timeout);
|
|
266
|
+
server.close();
|
|
267
|
+
reject(new AuthError("Missing authorization code in callback."));
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
271
|
+
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>");
|
|
272
|
+
clearTimeout(timeout);
|
|
273
|
+
server.close();
|
|
274
|
+
resolve(code);
|
|
275
|
+
});
|
|
276
|
+
server.listen(REDIRECT_PORT, () => {
|
|
277
|
+
});
|
|
278
|
+
server.on("error", (err) => {
|
|
279
|
+
clearTimeout(timeout);
|
|
280
|
+
reject(new AuthError(`Failed to start callback server on port ${REDIRECT_PORT}: ${err.message}`));
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
async function exchangeCodeForToken(code, clientId, clientSecret) {
|
|
285
|
+
const body = new URLSearchParams({
|
|
286
|
+
grant_type: "authorization_code",
|
|
287
|
+
code,
|
|
288
|
+
client_id: clientId,
|
|
289
|
+
client_secret: clientSecret,
|
|
290
|
+
redirect_uri: REDIRECT_URI
|
|
291
|
+
});
|
|
292
|
+
const res = await fetch(TOKEN_URL, {
|
|
293
|
+
method: "POST",
|
|
294
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
295
|
+
body: body.toString()
|
|
296
|
+
});
|
|
297
|
+
if (!res.ok) {
|
|
298
|
+
const text = await res.text();
|
|
299
|
+
throw new AuthError(`Token exchange failed (${res.status}): ${text}`);
|
|
300
|
+
}
|
|
301
|
+
const data = await res.json();
|
|
302
|
+
return {
|
|
303
|
+
accessToken: data.access_token,
|
|
304
|
+
refreshToken: data.refresh_token,
|
|
305
|
+
expiresAt: Math.floor(Date.now() / 1e3) + Number(data.expires_in),
|
|
306
|
+
scope: data.scope
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
async function refreshUserToken(refreshToken2, profile = "default") {
|
|
310
|
+
const creds = await loadCredentials(profile);
|
|
311
|
+
const body = new URLSearchParams({
|
|
312
|
+
grant_type: "refresh_token",
|
|
313
|
+
refresh_token: refreshToken2,
|
|
314
|
+
client_id: creds.clientId,
|
|
315
|
+
client_secret: creds.clientSecret
|
|
316
|
+
});
|
|
317
|
+
const res = await fetch(TOKEN_URL, {
|
|
318
|
+
method: "POST",
|
|
319
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
320
|
+
body: body.toString()
|
|
321
|
+
});
|
|
322
|
+
if (!res.ok) {
|
|
323
|
+
const text = await res.text();
|
|
324
|
+
throw new AuthError(`Token refresh failed (${res.status}): ${text}`);
|
|
325
|
+
}
|
|
326
|
+
const data = await res.json();
|
|
327
|
+
return {
|
|
328
|
+
accessToken: data.access_token,
|
|
329
|
+
refreshToken: data.refresh_token ?? refreshToken2,
|
|
330
|
+
expiresAt: Math.floor(Date.now() / 1e3) + Number(data.expires_in),
|
|
331
|
+
scope: data.scope
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
187
335
|
// src/output/format.ts
|
|
188
336
|
function output(data, opts = {}) {
|
|
189
337
|
const useJson = opts.json || !process.stdout.isTTY;
|
|
@@ -254,6 +402,7 @@ function errorOutput(error48, opts = {}) {
|
|
|
254
402
|
}
|
|
255
403
|
|
|
256
404
|
// src/commands/login.ts
|
|
405
|
+
import { randomBytes } from "crypto";
|
|
257
406
|
async function prompt(question) {
|
|
258
407
|
const rl = createInterface({
|
|
259
408
|
input: process.stdin,
|
|
@@ -266,53 +415,86 @@ async function prompt(question) {
|
|
|
266
415
|
rl.close();
|
|
267
416
|
}
|
|
268
417
|
}
|
|
269
|
-
var loginCommand = new Command("login").description("Authenticate with NAVER WORKS").option("--client-id <id>", "Client ID").option("--client-secret <secret>", "Client Secret").option("--service-account <account>", "Service Account ID").option("--private-key <path>", "Path to private key file (.key)").option("--bot-id <id>", "Bot ID").option("--domain-id <id>", "Domain ID (optional)").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
|
|
418
|
+
var loginCommand = new Command("login").description("Authenticate with NAVER WORKS").option("--user", "User OAuth login (opens browser)").option("--scope <scope>", "OAuth scope for user login", "calendar.read").option("--client-id <id>", "Client ID").option("--client-secret <secret>", "Client Secret").option("--service-account <account>", "Service Account ID").option("--private-key <path>", "Path to private key file (.key)").option("--bot-id <id>", "Bot ID").option("--domain-id <id>", "Domain ID (optional)").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
|
|
270
419
|
try {
|
|
271
|
-
let clientId = opts.clientId;
|
|
272
|
-
let clientSecret = opts.clientSecret;
|
|
273
|
-
let serviceAccount = opts.serviceAccount;
|
|
274
|
-
let privateKeyPath = opts.privateKey;
|
|
275
|
-
let botId = opts.botId;
|
|
276
|
-
const domainId = opts.domainId;
|
|
277
420
|
const profile = opts.profile;
|
|
278
|
-
if (
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
if (!privateKeyPath)
|
|
284
|
-
privateKeyPath = await prompt("Private Key Path: ");
|
|
285
|
-
if (!botId) botId = await prompt("Bot ID: ");
|
|
286
|
-
}
|
|
287
|
-
if (!clientId || !clientSecret || !serviceAccount || !privateKeyPath || !botId) {
|
|
288
|
-
throw new Error(
|
|
289
|
-
"Missing required fields. Use flags or run interactively."
|
|
290
|
-
);
|
|
291
|
-
}
|
|
292
|
-
if (!existsSync2(privateKeyPath)) {
|
|
293
|
-
throw new Error(`Private key file not found: ${privateKeyPath}`);
|
|
294
|
-
}
|
|
295
|
-
await readFile3(privateKeyPath, "utf-8");
|
|
296
|
-
const creds = {
|
|
297
|
-
clientId,
|
|
298
|
-
clientSecret,
|
|
299
|
-
serviceAccount,
|
|
300
|
-
privateKeyPath,
|
|
301
|
-
botId,
|
|
302
|
-
domainId
|
|
303
|
-
};
|
|
304
|
-
await saveCredentials(creds, profile);
|
|
305
|
-
await refreshToken(profile);
|
|
306
|
-
output(
|
|
307
|
-
{ success: true, message: `Logged in as ${serviceAccount}`, profile },
|
|
308
|
-
opts
|
|
309
|
-
);
|
|
421
|
+
if (opts.user) {
|
|
422
|
+
await handleUserLogin(opts.scope, profile, opts);
|
|
423
|
+
} else {
|
|
424
|
+
await handleServiceAccountLogin(opts);
|
|
425
|
+
}
|
|
310
426
|
} catch (err) {
|
|
311
427
|
const error48 = err;
|
|
312
428
|
errorOutput({ message: error48.message }, opts);
|
|
313
429
|
process.exitCode = 1;
|
|
314
430
|
}
|
|
315
431
|
});
|
|
432
|
+
async function handleUserLogin(scope, profile, opts) {
|
|
433
|
+
const creds = await loadCredentials(profile);
|
|
434
|
+
const state = randomBytes(16).toString("hex");
|
|
435
|
+
const authorizeUrl = buildAuthorizeUrl(creds.clientId, scope, state);
|
|
436
|
+
console.error(`
|
|
437
|
+
Opening browser for NAVER WORKS login...`);
|
|
438
|
+
console.error(`If the browser does not open, visit this URL:
|
|
439
|
+
`);
|
|
440
|
+
console.error(` ${authorizeUrl}
|
|
441
|
+
`);
|
|
442
|
+
const { exec } = await import("child_process");
|
|
443
|
+
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
444
|
+
exec(`${openCmd} "${authorizeUrl}"`);
|
|
445
|
+
const tokenData = await startUserOAuthFlow(scope, profile);
|
|
446
|
+
await saveUserToken(tokenData, profile);
|
|
447
|
+
output(
|
|
448
|
+
{
|
|
449
|
+
success: true,
|
|
450
|
+
message: "User OAuth login successful",
|
|
451
|
+
scope: tokenData.scope,
|
|
452
|
+
profile
|
|
453
|
+
},
|
|
454
|
+
opts
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
async function handleServiceAccountLogin(opts) {
|
|
458
|
+
let clientId = opts.clientId;
|
|
459
|
+
let clientSecret = opts.clientSecret;
|
|
460
|
+
let serviceAccount = opts.serviceAccount;
|
|
461
|
+
let privateKeyPath = opts.privateKey;
|
|
462
|
+
let botId = opts.botId;
|
|
463
|
+
const domainId = opts.domainId;
|
|
464
|
+
const profile = opts.profile;
|
|
465
|
+
if (process.stdin.isTTY) {
|
|
466
|
+
if (!clientId) clientId = await prompt("Client ID: ");
|
|
467
|
+
if (!clientSecret) clientSecret = await prompt("Client Secret: ");
|
|
468
|
+
if (!serviceAccount)
|
|
469
|
+
serviceAccount = await prompt("Service Account: ");
|
|
470
|
+
if (!privateKeyPath)
|
|
471
|
+
privateKeyPath = await prompt("Private Key Path: ");
|
|
472
|
+
if (!botId) botId = await prompt("Bot ID: ");
|
|
473
|
+
}
|
|
474
|
+
if (!clientId || !clientSecret || !serviceAccount || !privateKeyPath || !botId) {
|
|
475
|
+
throw new Error(
|
|
476
|
+
"Missing required fields. Use flags or run interactively."
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
if (!existsSync2(privateKeyPath)) {
|
|
480
|
+
throw new Error(`Private key file not found: ${privateKeyPath}`);
|
|
481
|
+
}
|
|
482
|
+
await readFile3(privateKeyPath, "utf-8");
|
|
483
|
+
const creds = {
|
|
484
|
+
clientId,
|
|
485
|
+
clientSecret,
|
|
486
|
+
serviceAccount,
|
|
487
|
+
privateKeyPath,
|
|
488
|
+
botId,
|
|
489
|
+
domainId
|
|
490
|
+
};
|
|
491
|
+
await saveCredentials(creds, profile);
|
|
492
|
+
await refreshToken(profile);
|
|
493
|
+
output(
|
|
494
|
+
{ success: true, message: `Logged in as ${serviceAccount}`, profile },
|
|
495
|
+
opts
|
|
496
|
+
);
|
|
497
|
+
}
|
|
316
498
|
|
|
317
499
|
// src/commands/logout.ts
|
|
318
500
|
import { Command as Command2 } from "commander";
|
|
@@ -550,9 +732,103 @@ var membersCommand2 = new Command5("members").description("List organization mem
|
|
|
550
732
|
});
|
|
551
733
|
var directoryCommand = new Command5("directory").description("Directory (organization) operations").addCommand(membersCommand2);
|
|
552
734
|
|
|
553
|
-
// src/commands/
|
|
735
|
+
// src/commands/calendar.ts
|
|
554
736
|
import { Command as Command6 } from "commander";
|
|
555
737
|
|
|
738
|
+
// src/auth/token-user.ts
|
|
739
|
+
async function getValidUserToken(profile = "default") {
|
|
740
|
+
const cached2 = await loadUserToken(profile);
|
|
741
|
+
if (!cached2) {
|
|
742
|
+
throw new AuthError(
|
|
743
|
+
"User OAuth token not found. Run `nworks login --user` first."
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
if (cached2.expiresAt > Date.now() / 1e3 + 300) {
|
|
747
|
+
return cached2.accessToken;
|
|
748
|
+
}
|
|
749
|
+
const refreshed = await refreshUserToken(cached2.refreshToken, profile);
|
|
750
|
+
await saveUserToken(refreshed, profile);
|
|
751
|
+
return refreshed.accessToken;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// src/api/calendar.ts
|
|
755
|
+
var BASE_URL2 = "https://www.worksapis.com/v1.0";
|
|
756
|
+
async function listEvents(fromDateTime, untilDateTime, userId = "me", profile = "default") {
|
|
757
|
+
const token = await getValidUserToken(profile);
|
|
758
|
+
const from = encodeURIComponent(fromDateTime);
|
|
759
|
+
const until = encodeURIComponent(untilDateTime);
|
|
760
|
+
const url2 = `${BASE_URL2}/users/${userId}/calendar/events?fromDateTime=${from}&untilDateTime=${until}`;
|
|
761
|
+
if (process.env["NWORKS_VERBOSE"] === "1") {
|
|
762
|
+
console.error(`[nworks] GET ${url2}`);
|
|
763
|
+
}
|
|
764
|
+
const res = await fetch(url2, {
|
|
765
|
+
method: "GET",
|
|
766
|
+
headers: {
|
|
767
|
+
Authorization: `Bearer ${token}`,
|
|
768
|
+
"Content-Type": "application/json"
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
if (res.status === 401) {
|
|
772
|
+
throw new AuthError("User token expired. Run `nworks login --user` again.");
|
|
773
|
+
}
|
|
774
|
+
if (!res.ok) {
|
|
775
|
+
let code = "UNKNOWN";
|
|
776
|
+
let description = `HTTP ${res.status}`;
|
|
777
|
+
try {
|
|
778
|
+
const errorBody = await res.json();
|
|
779
|
+
code = errorBody.code ?? code;
|
|
780
|
+
description = errorBody.description ?? description;
|
|
781
|
+
} catch {
|
|
782
|
+
}
|
|
783
|
+
throw new ApiError(code, description, res.status);
|
|
784
|
+
}
|
|
785
|
+
const data = await res.json();
|
|
786
|
+
return { events: data.events ?? [] };
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// src/commands/calendar.ts
|
|
790
|
+
function todayRange() {
|
|
791
|
+
const now = /* @__PURE__ */ new Date();
|
|
792
|
+
const yyyy = now.getFullYear();
|
|
793
|
+
const mm = String(now.getMonth() + 1).padStart(2, "0");
|
|
794
|
+
const dd = String(now.getDate()).padStart(2, "0");
|
|
795
|
+
return {
|
|
796
|
+
from: `${yyyy}-${mm}-${dd}T00:00:00+09:00`,
|
|
797
|
+
until: `${yyyy}-${mm}-${dd}T23:59:59+09:00`
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
var listCommand = new Command6("list").description("List calendar events (requires User OAuth with calendar.read scope)").option("--user <userId>", "Target user ID (default: me)").option("--from <dateTime>", "Start (YYYY-MM-DDThh:mm:ss+09:00, default: today 00:00)").option("--until <dateTime>", "End (YYYY-MM-DDThh:mm:ss+09:00, default: today 23:59)").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
|
|
801
|
+
try {
|
|
802
|
+
const defaults = todayRange();
|
|
803
|
+
const from = opts.from ?? defaults.from;
|
|
804
|
+
const until = opts.until ?? defaults.until;
|
|
805
|
+
const userId = opts.user ?? "me";
|
|
806
|
+
const result = await listEvents(
|
|
807
|
+
from,
|
|
808
|
+
until,
|
|
809
|
+
userId,
|
|
810
|
+
opts.profile
|
|
811
|
+
);
|
|
812
|
+
const events = result.events.flatMap(
|
|
813
|
+
(e) => e.eventComponents.map((c) => ({
|
|
814
|
+
summary: c.summary,
|
|
815
|
+
start: c.start.dateTime ? `${c.start.dateTime} (${c.start.timeZone ?? ""})` : c.start.date ?? "",
|
|
816
|
+
end: c.end.dateTime ? `${c.end.dateTime} (${c.end.timeZone ?? ""})` : c.end.date ?? "",
|
|
817
|
+
location: c.location ?? ""
|
|
818
|
+
}))
|
|
819
|
+
);
|
|
820
|
+
output({ events, count: events.length }, opts);
|
|
821
|
+
} catch (err) {
|
|
822
|
+
const error48 = err;
|
|
823
|
+
errorOutput({ code: error48.code, message: error48.message }, opts);
|
|
824
|
+
process.exitCode = 1;
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
var calendarCommand = new Command6("calendar").description("Calendar operations (requires User OAuth)").addCommand(listCommand);
|
|
828
|
+
|
|
829
|
+
// src/commands/mcp-cmd.ts
|
|
830
|
+
import { Command as Command7 } from "commander";
|
|
831
|
+
|
|
556
832
|
// src/mcp/server.ts
|
|
557
833
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
558
834
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -14400,6 +14676,41 @@ function registerTools(server) {
|
|
|
14400
14676
|
}
|
|
14401
14677
|
}
|
|
14402
14678
|
);
|
|
14679
|
+
server.tool(
|
|
14680
|
+
"nworks_calendar_list",
|
|
14681
|
+
"\uC0AC\uC6A9\uC790\uC758 \uCE98\uB9B0\uB354 \uC77C\uC815\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4 (User OAuth calendar.read scope \uD544\uC694. \uBA3C\uC800 nworks login --user \uD544\uC694)",
|
|
14682
|
+
{
|
|
14683
|
+
fromDateTime: external_exports.string().describe("\uC2DC\uC791 \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss+09:00)"),
|
|
14684
|
+
untilDateTime: external_exports.string().describe("\uC885\uB8CC \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss+09:00)"),
|
|
14685
|
+
userId: external_exports.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
|
|
14686
|
+
},
|
|
14687
|
+
async ({ fromDateTime, untilDateTime, userId }) => {
|
|
14688
|
+
try {
|
|
14689
|
+
const result = await listEvents(
|
|
14690
|
+
fromDateTime,
|
|
14691
|
+
untilDateTime,
|
|
14692
|
+
userId ?? "me"
|
|
14693
|
+
);
|
|
14694
|
+
const events = result.events.flatMap(
|
|
14695
|
+
(e) => e.eventComponents.map((c) => ({
|
|
14696
|
+
summary: c.summary,
|
|
14697
|
+
start: c.start.dateTime ?? c.start.date ?? "",
|
|
14698
|
+
end: c.end.dateTime ?? c.end.date ?? "",
|
|
14699
|
+
location: c.location ?? ""
|
|
14700
|
+
}))
|
|
14701
|
+
);
|
|
14702
|
+
return {
|
|
14703
|
+
content: [{ type: "text", text: JSON.stringify({ events, count: events.length }) }]
|
|
14704
|
+
};
|
|
14705
|
+
} catch (err) {
|
|
14706
|
+
const error48 = err;
|
|
14707
|
+
return {
|
|
14708
|
+
content: [{ type: "text", text: `Error: ${error48.message}` }],
|
|
14709
|
+
isError: true
|
|
14710
|
+
};
|
|
14711
|
+
}
|
|
14712
|
+
}
|
|
14713
|
+
);
|
|
14403
14714
|
server.tool(
|
|
14404
14715
|
"nworks_whoami",
|
|
14405
14716
|
"\uD604\uC7AC \uC778\uC99D\uB41C NAVER WORKS \uACC4\uC815 \uC815\uBCF4\uB97C \uD655\uC778\uD569\uB2C8\uB2E4",
|
|
@@ -14433,7 +14744,7 @@ function registerTools(server) {
|
|
|
14433
14744
|
async function startMcpServer() {
|
|
14434
14745
|
const server = new McpServer({
|
|
14435
14746
|
name: "nworks",
|
|
14436
|
-
version: "0.
|
|
14747
|
+
version: "0.2.0"
|
|
14437
14748
|
});
|
|
14438
14749
|
registerTools(server);
|
|
14439
14750
|
const transport = new StdioServerTransport();
|
|
@@ -14441,11 +14752,12 @@ async function startMcpServer() {
|
|
|
14441
14752
|
}
|
|
14442
14753
|
|
|
14443
14754
|
// src/commands/mcp-cmd.ts
|
|
14444
|
-
var mcpCommand = new
|
|
14755
|
+
var mcpCommand = new Command7("mcp").description("Start MCP server (stdio transport)").option("--list-tools", "List available MCP tools").action(async (opts) => {
|
|
14445
14756
|
if (opts.listTools) {
|
|
14446
14757
|
console.log("nworks_message_send \u2014 Send message to user or channel");
|
|
14447
14758
|
console.log("nworks_message_members \u2014 List channel members");
|
|
14448
14759
|
console.log("nworks_directory_members \u2014 List organization members");
|
|
14760
|
+
console.log("nworks_calendar_list \u2014 List calendar events (User OAuth)");
|
|
14449
14761
|
console.log("nworks_whoami \u2014 Show auth status");
|
|
14450
14762
|
return;
|
|
14451
14763
|
}
|
|
@@ -14453,12 +14765,13 @@ var mcpCommand = new Command6("mcp").description("Start MCP server (stdio transp
|
|
|
14453
14765
|
});
|
|
14454
14766
|
|
|
14455
14767
|
// src/index.ts
|
|
14456
|
-
var program = new
|
|
14768
|
+
var program = new Command8().name("nworks").description("NAVER WORKS CLI \u2014 built for humans and AI agents").version("0.1.0").option("--json", "Always output JSON").option("-v, --verbose", "Debug logging").option("--dry-run", "Print request without calling API").option("-p, --profile <name>", "Profile name", "default");
|
|
14457
14769
|
program.addCommand(loginCommand);
|
|
14458
14770
|
program.addCommand(logoutCommand);
|
|
14459
14771
|
program.addCommand(whoamiCommand);
|
|
14460
14772
|
program.addCommand(messageCommand);
|
|
14461
14773
|
program.addCommand(directoryCommand);
|
|
14774
|
+
program.addCommand(calendarCommand);
|
|
14462
14775
|
program.addCommand(mcpCommand);
|
|
14463
14776
|
program.parse();
|
|
14464
14777
|
//# sourceMappingURL=index.js.map
|