@lark-project/meegle 0.0.3 → 0.0.4

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.
@@ -1,2076 +0,0 @@
1
- #!/usr/bin/env node
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropNames = Object.getOwnPropertyNames;
4
- var __esm = (fn, res) => function __init() {
5
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
- };
7
- var __export = (target, all) => {
8
- for (var name in all)
9
- __defProp(target, name, { get: all[name], enumerable: true });
10
- };
11
-
12
- // src/utils/env.ts
13
- function getEnv(key) {
14
- return process.env[`${PREFIX}${key}`];
15
- }
16
- function getHost() {
17
- return getEnv("HOST");
18
- }
19
- function getUserToken() {
20
- return getEnv("USER_ACCESS_TOKEN");
21
- }
22
- function hasEnvAuth() {
23
- return !!(getEnv("HOST") && getEnv("USER_ACCESS_TOKEN"));
24
- }
25
- var PREFIX;
26
- var init_env = __esm({
27
- "src/utils/env.ts"() {
28
- "use strict";
29
- PREFIX = "MEEGLE_";
30
- }
31
- });
32
-
33
- // src/core/config.ts
34
- import { readFile, writeFile, mkdir } from "fs/promises";
35
- import { join } from "path";
36
- import { homedir } from "os";
37
- function getConfigDir() {
38
- return CONFIG_DIR;
39
- }
40
- function getCacheDir() {
41
- return join(CONFIG_DIR, "cache");
42
- }
43
- async function readRootConfig() {
44
- try {
45
- const raw = await readFile(CONFIG_PATH, "utf-8");
46
- const parsed = JSON.parse(raw);
47
- if (!parsed.profiles) return {};
48
- return parsed;
49
- } catch {
50
- return {};
51
- }
52
- }
53
- async function writeRootConfig(root) {
54
- await mkdir(CONFIG_DIR, { recursive: true, mode: 448 });
55
- await writeFile(CONFIG_PATH, JSON.stringify(root, null, 2), { mode: 384 });
56
- }
57
- async function getCurrentProfileName() {
58
- const root = await readRootConfig();
59
- return root.current ?? "default";
60
- }
61
- async function setCurrentProfileName(name) {
62
- const root = await readRootConfig();
63
- root.current = name;
64
- await writeRootConfig(root);
65
- }
66
- async function loadProfileConfig(profile) {
67
- const root = await readRootConfig();
68
- return root.profiles?.[profile] ?? {};
69
- }
70
- async function saveProfileConfig(profile, config) {
71
- const root = await readRootConfig();
72
- if (!root.profiles) root.profiles = {};
73
- root.profiles[profile] = config;
74
- if (!root.current) root.current = profile;
75
- await writeRootConfig(root);
76
- }
77
- async function getProfileValue(profile, key) {
78
- const config = await loadProfileConfig(profile);
79
- return config[key];
80
- }
81
- async function setProfileValue(profile, key, value) {
82
- const config = await loadProfileConfig(profile);
83
- let parsed = value;
84
- if (typeof value === "string") {
85
- try {
86
- parsed = JSON.parse(value);
87
- } catch {
88
- }
89
- }
90
- config[key] = parsed;
91
- await saveProfileConfig(profile, config);
92
- }
93
- async function listProfiles() {
94
- const root = await readRootConfig();
95
- const current = root.current ?? "default";
96
- const names = Object.keys(root.profiles ?? {});
97
- return { names, current };
98
- }
99
- async function deleteProfile(name) {
100
- const root = await readRootConfig();
101
- if (root.profiles) {
102
- delete root.profiles[name];
103
- }
104
- if (root.current === name) {
105
- const remaining = Object.keys(root.profiles ?? {});
106
- root.current = remaining[0] ?? "default";
107
- }
108
- await writeRootConfig(root);
109
- }
110
- async function loadConfig(profile) {
111
- const name = profile ?? await getCurrentProfileName();
112
- return loadProfileConfig(name);
113
- }
114
- async function saveConfig(config, profile) {
115
- const name = profile ?? await getCurrentProfileName();
116
- await saveProfileConfig(name, config);
117
- }
118
- function sanitizeHost(raw) {
119
- const trimmed = raw.trim();
120
- try {
121
- const url = new URL(trimmed.includes("://") ? trimmed : `https://${trimmed}`);
122
- return url.host;
123
- } catch {
124
- return trimmed;
125
- }
126
- }
127
- function getServerUrl(config) {
128
- const envHost = getEnv("HOST");
129
- const host = sanitizeHost(envHost ?? config.host ?? "meegle.com");
130
- return `https://${host}/mcp_server/v1`;
131
- }
132
- var CONFIG_DIR, CONFIG_PATH;
133
- var init_config = __esm({
134
- "src/core/config.ts"() {
135
- "use strict";
136
- init_env();
137
- CONFIG_DIR = join(homedir(), ".meegle");
138
- CONFIG_PATH = join(CONFIG_DIR, "config.json");
139
- }
140
- });
141
-
142
- // src/core/errors.ts
143
- var MeegleError, ClientError, ServerError, HttpError;
144
- var init_errors = __esm({
145
- "src/core/errors.ts"() {
146
- "use strict";
147
- MeegleError = class extends Error {
148
- code;
149
- exitCode;
150
- suggestion;
151
- constructor(message, code, exitCode, suggestion) {
152
- super(message);
153
- this.name = "MeegleError";
154
- this.code = code;
155
- this.exitCode = exitCode;
156
- this.suggestion = suggestion;
157
- }
158
- toJSON() {
159
- return {
160
- error: this.code,
161
- message: this.message,
162
- ...this.suggestion !== void 0 && { suggestion: this.suggestion }
163
- };
164
- }
165
- };
166
- ClientError = class extends MeegleError {
167
- constructor(message, code, suggestion) {
168
- super(message, code, 1, suggestion);
169
- this.name = "ClientError";
170
- }
171
- };
172
- ServerError = class extends MeegleError {
173
- constructor(message, code, suggestion) {
174
- super(message, code, 2, suggestion);
175
- this.name = "ServerError";
176
- }
177
- };
178
- HttpError = class extends ServerError {
179
- httpStatus;
180
- constructor(message, code, httpStatus, suggestion) {
181
- super(message, code, suggestion);
182
- this.name = "HttpError";
183
- this.httpStatus = httpStatus;
184
- }
185
- };
186
- }
187
- });
188
-
189
- // src/core/profile-validation.ts
190
- function validateProfileName(profile) {
191
- if (!PROFILE_NAME_RE.test(profile)) {
192
- throw new ClientError(
193
- `Profile \u540D\u79F0\u53EA\u80FD\u5305\u542B\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u8FDE\u5B57\u7B26\u548C\u4E0B\u5212\u7EBF\uFF0C\u6536\u5230: "${profile}"`,
194
- "CLIENT_INVALID_PROFILE"
195
- );
196
- }
197
- }
198
- var PROFILE_NAME_RE;
199
- var init_profile_validation = __esm({
200
- "src/core/profile-validation.ts"() {
201
- "use strict";
202
- init_errors();
203
- PROFILE_NAME_RE = /^[a-zA-Z0-9_-]+$/;
204
- }
205
- });
206
-
207
- // src/core/auth/keychain-store.ts
208
- var keychain_store_exports = {};
209
- __export(keychain_store_exports, {
210
- KeychainStore: () => KeychainStore,
211
- SecretToolStore: () => SecretToolStore
212
- });
213
- import { execFile } from "child_process";
214
- import { promisify } from "util";
215
- var exec, SERVICE, KeychainStore, SecretToolStore;
216
- var init_keychain_store = __esm({
217
- "src/core/auth/keychain-store.ts"() {
218
- "use strict";
219
- init_profile_validation();
220
- exec = promisify(execFile);
221
- SERVICE = "meegle-cli";
222
- KeychainStore = class {
223
- account;
224
- constructor(profile = "default") {
225
- validateProfileName(profile);
226
- this.account = profile;
227
- }
228
- async isAvailable() {
229
- try {
230
- await exec("security", ["help"]);
231
- return true;
232
- } catch {
233
- return false;
234
- }
235
- }
236
- async load() {
237
- try {
238
- const { stdout } = await exec("security", [
239
- "find-generic-password",
240
- "-s",
241
- SERVICE,
242
- "-a",
243
- this.account,
244
- "-w"
245
- ]);
246
- return JSON.parse(stdout.trim());
247
- } catch {
248
- return null;
249
- }
250
- }
251
- async save(data) {
252
- const json = JSON.stringify(data);
253
- try {
254
- await exec("security", ["delete-generic-password", "-s", SERVICE, "-a", this.account]);
255
- } catch {
256
- }
257
- await exec("security", [
258
- "add-generic-password",
259
- "-s",
260
- SERVICE,
261
- "-a",
262
- this.account,
263
- "-w",
264
- json
265
- ]);
266
- }
267
- async clear() {
268
- try {
269
- await exec("security", ["delete-generic-password", "-s", SERVICE, "-a", this.account]);
270
- } catch {
271
- }
272
- }
273
- };
274
- SecretToolStore = class {
275
- account;
276
- constructor(profile = "default") {
277
- validateProfileName(profile);
278
- this.account = profile;
279
- }
280
- async isAvailable() {
281
- try {
282
- await exec("which", ["secret-tool"]);
283
- return true;
284
- } catch {
285
- return false;
286
- }
287
- }
288
- async load() {
289
- try {
290
- const { stdout } = await exec("secret-tool", [
291
- "lookup",
292
- "service",
293
- SERVICE,
294
- "account",
295
- this.account
296
- ]);
297
- return JSON.parse(stdout.trim());
298
- } catch {
299
- return null;
300
- }
301
- }
302
- async save(data) {
303
- const json = JSON.stringify(data);
304
- const { spawn } = await import("child_process");
305
- const child = spawn("secret-tool", [
306
- "store",
307
- "--label=meegle-cli",
308
- "service",
309
- SERVICE,
310
- "account",
311
- this.account
312
- ], { stdio: ["pipe", "pipe", "pipe"] });
313
- child.stdin.write(json);
314
- child.stdin.end();
315
- await new Promise((resolve, reject) => {
316
- child.on("close", (code) => code === 0 ? resolve() : reject(new Error(`secret-tool exited ${code}`)));
317
- });
318
- }
319
- async clear() {
320
- try {
321
- await exec("secret-tool", ["clear", "service", SERVICE, "account", this.account]);
322
- } catch {
323
- }
324
- }
325
- };
326
- }
327
- });
328
-
329
- // src/core/auth/file-store.ts
330
- var file_store_exports = {};
331
- __export(file_store_exports, {
332
- FileStore: () => FileStore
333
- });
334
- import {
335
- randomBytes,
336
- createCipheriv,
337
- createDecipheriv,
338
- pbkdf2Sync
339
- } from "crypto";
340
- import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, rm } from "fs/promises";
341
- import { join as join2 } from "path";
342
- import { hostname, userInfo } from "os";
343
- function toHex(b) {
344
- return Buffer.from(b).toString("hex");
345
- }
346
- function fromHex(s) {
347
- return new Uint8Array(Buffer.from(s, "hex"));
348
- }
349
- async function getOrCreateMachineKey(dir) {
350
- const keyPath = join2(dir, MACHINE_KEY_FILE);
351
- try {
352
- return await readFile2(keyPath, "utf-8");
353
- } catch {
354
- const key = randomBytes(32).toString("hex");
355
- await mkdir2(dir, { recursive: true, mode: 448 });
356
- await writeFile2(keyPath, key, { mode: 384 });
357
- return key;
358
- }
359
- }
360
- function deriveKey(salt, machineKey) {
361
- const material = `${hostname()}:${userInfo().username}:${machineKey}:meegle-cli`;
362
- return new Uint8Array(pbkdf2Sync(material, salt, 1e5, 32, "sha256").buffer);
363
- }
364
- var MACHINE_KEY_FILE, FileStore;
365
- var init_file_store = __esm({
366
- "src/core/auth/file-store.ts"() {
367
- "use strict";
368
- init_config();
369
- init_profile_validation();
370
- MACHINE_KEY_FILE = ".machine-key";
371
- FileStore = class {
372
- dir;
373
- filename;
374
- constructor(dir, profile = "default") {
375
- this.dir = dir ?? getConfigDir();
376
- validateProfileName(profile);
377
- this.filename = profile === "default" ? "credentials.enc" : `credentials-${profile}.enc`;
378
- }
379
- get filePath() {
380
- return join2(this.dir, this.filename);
381
- }
382
- async load() {
383
- try {
384
- const machineKey = await getOrCreateMachineKey(this.dir);
385
- const raw = await readFile2(this.filePath, "utf-8");
386
- const file = JSON.parse(raw);
387
- const salt = fromHex(file.salt);
388
- const iv = fromHex(file.iv);
389
- const tag = fromHex(file.tag);
390
- const encrypted = fromHex(file.data);
391
- const key = deriveKey(salt, machineKey);
392
- const decipher = createDecipheriv("aes-256-gcm", key, iv);
393
- decipher.setAuthTag(tag);
394
- const part1 = decipher.update(encrypted);
395
- const part2 = decipher.final();
396
- const decrypted = Buffer.concat([part1, part2]);
397
- return JSON.parse(decrypted.toString("utf-8"));
398
- } catch {
399
- return null;
400
- }
401
- }
402
- async save(data) {
403
- const machineKey = await getOrCreateMachineKey(this.dir);
404
- const salt = new Uint8Array(randomBytes(16).buffer);
405
- const iv = new Uint8Array(randomBytes(12).buffer);
406
- const key = deriveKey(salt, machineKey);
407
- const cipher = createCipheriv("aes-256-gcm", key, iv);
408
- const plaintext = JSON.stringify(data);
409
- const enc1 = cipher.update(plaintext, "utf-8");
410
- const enc2 = cipher.final();
411
- const encrypted = Buffer.concat([enc1, enc2]);
412
- const tag = new Uint8Array(cipher.getAuthTag().buffer);
413
- const file = {
414
- salt: toHex(salt),
415
- iv: toHex(iv),
416
- tag: toHex(tag),
417
- data: encrypted.toString("hex")
418
- };
419
- await mkdir2(this.dir, { recursive: true, mode: 448 });
420
- await writeFile2(this.filePath, JSON.stringify(file, null, 2), { mode: 384 });
421
- }
422
- async clear() {
423
- try {
424
- await rm(this.filePath);
425
- } catch {
426
- }
427
- }
428
- };
429
- }
430
- });
431
-
432
- // src/cli.ts
433
- import { Command } from "commander";
434
-
435
- // src/commands/auth/login.ts
436
- init_config();
437
- init_env();
438
- import { select as select2, input as input2 } from "@inquirer/prompts";
439
-
440
- // src/core/auth/token-store.ts
441
- async function createTokenStore(profile = "default") {
442
- if (process.platform === "darwin") {
443
- const { KeychainStore: KeychainStore2 } = await Promise.resolve().then(() => (init_keychain_store(), keychain_store_exports));
444
- const store = new KeychainStore2(profile);
445
- if (await store.isAvailable()) return store;
446
- }
447
- if (process.platform === "linux") {
448
- const { SecretToolStore: SecretToolStore2 } = await Promise.resolve().then(() => (init_keychain_store(), keychain_store_exports));
449
- const store = new SecretToolStore2(profile);
450
- if (await store.isAvailable()) return store;
451
- }
452
- const { FileStore: FileStore2 } = await Promise.resolve().then(() => (init_file_store(), file_store_exports));
453
- return new FileStore2(void 0, profile);
454
- }
455
-
456
- // src/core/auth/token-manager.ts
457
- init_env();
458
-
459
- // src/core/auth/oauth-discovery.ts
460
- init_errors();
461
- var REQUIRED_ENDPOINTS = [
462
- "issuer",
463
- "authorization_endpoint",
464
- "token_endpoint",
465
- "registration_endpoint"
466
- ];
467
- function validateMetadata(metadata) {
468
- for (const field of REQUIRED_ENDPOINTS) {
469
- const value = metadata[field];
470
- if (typeof value !== "string" || !value.startsWith("https://")) {
471
- throw new ClientError(
472
- `OAuth \u914D\u7F6E\u5B57\u6BB5\u65E0\u6548: ${field}`,
473
- "OAUTH_DISCOVERY_INVALID"
474
- );
475
- }
476
- }
477
- return metadata;
478
- }
479
- async function fetchOAuthMetadata(host, headers) {
480
- const url = `https://${host}/.well-known/oauth-authorization-server`;
481
- const res = await fetch(url, { headers });
482
- if (!res.ok) {
483
- throw new ClientError(
484
- `\u65E0\u6CD5\u83B7\u53D6 OAuth \u914D\u7F6E: ${res.status}`,
485
- "OAUTH_DISCOVERY_FAILED",
486
- `\u8BF7\u786E\u8BA4\u7AD9\u70B9 ${host} \u662F\u5426\u6B63\u786E`
487
- );
488
- }
489
- const data = await res.json();
490
- return validateMetadata(data);
491
- }
492
-
493
- // src/core/auth/token-manager.ts
494
- var TokenManager = class {
495
- store;
496
- host;
497
- constructor(store, host) {
498
- this.store = store;
499
- this.host = host;
500
- }
501
- async getToken() {
502
- const envToken = getUserToken();
503
- if (envToken) return envToken;
504
- const data = await this.store.load();
505
- if (!data) return void 0;
506
- if (data.expiresAt && Date.now() > data.expiresAt) {
507
- const refreshed = await this.refreshToken(data);
508
- if (refreshed) return refreshed.accessToken;
509
- return void 0;
510
- }
511
- return data.accessToken;
512
- }
513
- async refreshToken(tokenData) {
514
- if (!tokenData.refreshToken || !tokenData.clientId || !this.host) return null;
515
- try {
516
- const metadata = await fetchOAuthMetadata(this.host);
517
- const res = await fetch(metadata.token_endpoint, {
518
- method: "POST",
519
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
520
- body: new URLSearchParams({
521
- grant_type: "refresh_token",
522
- refresh_token: tokenData.refreshToken,
523
- client_id: tokenData.clientId
524
- })
525
- });
526
- if (!res.ok) return null;
527
- const data = await res.json();
528
- if (typeof data.access_token !== "string" || !data.access_token) {
529
- return null;
530
- }
531
- const newTokenData = {
532
- accessToken: data.access_token,
533
- refreshToken: (typeof data.refresh_token === "string" ? data.refresh_token : void 0) ?? tokenData.refreshToken,
534
- clientId: tokenData.clientId,
535
- expiresAt: typeof data.expires_in === "number" ? Date.now() + data.expires_in * 1e3 : void 0
536
- };
537
- await this.store.save(newTokenData);
538
- return newTokenData;
539
- } catch {
540
- return null;
541
- }
542
- }
543
- async saveToken(data) {
544
- await this.store.save(data);
545
- }
546
- async clearToken() {
547
- await this.store.clear();
548
- }
549
- async isAuthenticated() {
550
- const token = await this.getToken();
551
- return token !== void 0;
552
- }
553
- getStore() {
554
- return this.store;
555
- }
556
- };
557
-
558
- // src/core/auth/auth-code-flow.ts
559
- import { randomBytes as randomBytes3 } from "crypto";
560
- import open from "open";
561
-
562
- // src/core/auth/oauth-client-registration.ts
563
- init_errors();
564
- import { hostname as hostname2, platform, arch, userInfo as userInfo2 } from "os";
565
- function getClientName() {
566
- const parts = [];
567
- try {
568
- parts.push(userInfo2().username);
569
- } catch {
570
- }
571
- try {
572
- parts.push(hostname2());
573
- } catch {
574
- }
575
- const name = parts.length > 1 ? `${parts[0]}@${parts[1]}` : parts[0];
576
- try {
577
- const sys = `${platform()}/${arch()}`;
578
- return name ? `${name} (${sys})` : `meegle-cli (${sys})`;
579
- } catch {
580
- return name || "meegle-cli";
581
- }
582
- }
583
- async function registerClient(registrationEndpoint, redirectUri, customHeaders) {
584
- const body = {
585
- client_name: getClientName(),
586
- token_endpoint_auth_method: "none"
587
- };
588
- if (redirectUri) {
589
- body.redirect_uris = [redirectUri];
590
- body.grant_types = ["authorization_code", "refresh_token"];
591
- body.response_types = ["code"];
592
- } else {
593
- body.grant_types = ["urn:ietf:params:oauth:grant-type:device_code", "refresh_token"];
594
- }
595
- const res = await fetch(registrationEndpoint, {
596
- method: "POST",
597
- headers: { "Content-Type": "application/json", ...customHeaders },
598
- body: JSON.stringify(body)
599
- });
600
- if (!res.ok) {
601
- throw new ClientError(
602
- `\u5BA2\u6237\u7AEF\u6CE8\u518C\u5931\u8D25: ${res.status}`,
603
- "CLIENT_REGISTRATION_FAILED"
604
- );
605
- }
606
- const data = await res.json();
607
- const payload = (data.client_id ? data : data.data) ?? data;
608
- if (!payload?.client_id) {
609
- const msg = data.msg || data.error?.localizedMessage?.toString() || JSON.stringify(data);
610
- throw new ClientError(
611
- `\u5BA2\u6237\u7AEF\u6CE8\u518C\u5931\u8D25: \u54CD\u5E94\u4E2D\u7F3A\u5C11 client_id (${msg})`,
612
- "CLIENT_REGISTRATION_FAILED"
613
- );
614
- }
615
- return {
616
- client_id: payload.client_id,
617
- client_secret: payload.client_secret ?? ""
618
- };
619
- }
620
-
621
- // src/core/auth/pkce.ts
622
- import { randomBytes as randomBytes2, createHash } from "crypto";
623
- function generateCodeVerifier() {
624
- return randomBytes2(32).toString("base64url");
625
- }
626
- function generateCodeChallenge(verifier) {
627
- return createHash("sha256").update(verifier).digest("base64url");
628
- }
629
-
630
- // src/core/auth/callback-server.ts
631
- init_errors();
632
- import http from "http";
633
- import { URL as URL2 } from "url";
634
- var SUCCESS_HTML = `<!DOCTYPE html><html><body>
635
- <h1>\u6388\u6743\u6210\u529F\uFF01</h1><p>\u53EF\u4EE5\u5173\u95ED\u6B64\u9875\u9762\uFF0C\u8FD4\u56DE\u7EC8\u7AEF\u3002</p>
636
- </body></html>`;
637
- var ERROR_HTML = `<!DOCTYPE html><html><body>
638
- <h1>\u6388\u6743\u9519\u8BEF</h1><p>\u72B6\u6001\u9A8C\u8BC1\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\u3002</p>
639
- </body></html>`;
640
- async function startCallbackServer() {
641
- return new Promise((resolve, reject) => {
642
- const server = http.createServer();
643
- server.listen(0, "127.0.0.1", () => {
644
- const addr = server.address();
645
- if (!addr || typeof addr === "string") {
646
- reject(new Error("Failed to get server address"));
647
- return;
648
- }
649
- const port = addr.port;
650
- const redirectUri = `http://127.0.0.1:${port}/callback`;
651
- const callbackServer = {
652
- port,
653
- redirectUri,
654
- waitForCallback(expectedState) {
655
- let resolveCallback;
656
- let rejectCallback;
657
- const promise = new Promise((res, rej) => {
658
- resolveCallback = res;
659
- rejectCallback = rej;
660
- });
661
- promise.catch(() => {
662
- });
663
- let settled = false;
664
- const settle = (fn) => {
665
- if (settled) return;
666
- settled = true;
667
- clearTimeout(timeout);
668
- fn();
669
- };
670
- const timeout = setTimeout(() => {
671
- settle(() => rejectCallback(new ClientError("\u6388\u6743\u8D85\u65F6\uFF08120\u79D2\uFF09", "OAUTH_TIMEOUT")));
672
- }, 12e4);
673
- const requestHandler = (req, res) => {
674
- const url = new URL2(req.url ?? "", `http://127.0.0.1:${port}`);
675
- if (url.pathname !== "/callback") {
676
- res.writeHead(404);
677
- res.end("Not Found");
678
- return;
679
- }
680
- const code = url.searchParams.get("code");
681
- const state = url.searchParams.get("state");
682
- server.off("request", requestHandler);
683
- if (state !== expectedState) {
684
- res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
685
- res.end(ERROR_HTML, () => {
686
- settle(() => rejectCallback(new ClientError("\u6388\u6743\u72B6\u6001\u4E0D\u5339\u914D", "OAUTH_STATE_MISMATCH")));
687
- });
688
- return;
689
- }
690
- if (!code) {
691
- res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
692
- res.end(ERROR_HTML, () => {
693
- settle(() => rejectCallback(new ClientError("\u672A\u6536\u5230\u6388\u6743\u7801", "OAUTH_NO_CODE")));
694
- });
695
- return;
696
- }
697
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
698
- res.end(SUCCESS_HTML, () => {
699
- settle(() => resolveCallback({ code }));
700
- });
701
- };
702
- server.on("request", requestHandler);
703
- return promise;
704
- },
705
- close() {
706
- if (typeof server.closeAllConnections === "function") {
707
- server.closeAllConnections();
708
- }
709
- server.close();
710
- }
711
- };
712
- resolve(callbackServer);
713
- });
714
- server.on("error", reject);
715
- });
716
- }
717
-
718
- // src/core/auth/auth-code-flow.ts
719
- init_errors();
720
- function buildAuthUrl(endpoint, params) {
721
- const url = new URL(endpoint);
722
- for (const [key, value] of Object.entries(params)) {
723
- url.searchParams.set(key, value);
724
- }
725
- return url.toString();
726
- }
727
- async function exchangeToken(tokenEndpoint, params, customHeaders) {
728
- const res = await fetch(tokenEndpoint, {
729
- method: "POST",
730
- headers: { "Content-Type": "application/x-www-form-urlencoded", ...customHeaders },
731
- body: new URLSearchParams(params)
732
- });
733
- if (!res.ok) {
734
- throw new ServerError(`Token \u4EA4\u6362\u5931\u8D25: ${res.status}`, "TOKEN_EXCHANGE_FAILED");
735
- }
736
- return res.json();
737
- }
738
- async function startAuthCodeFlow(host, customHeaders) {
739
- const metadata = await fetchOAuthMetadata(host, customHeaders);
740
- const server = await startCallbackServer();
741
- try {
742
- const client = await registerClient(metadata.registration_endpoint, server.redirectUri, customHeaders);
743
- const codeVerifier = generateCodeVerifier();
744
- const codeChallenge = generateCodeChallenge(codeVerifier);
745
- const state = randomBytes3(16).toString("hex");
746
- const authUrl = buildAuthUrl(metadata.authorization_endpoint, {
747
- response_type: "code",
748
- client_id: client.client_id,
749
- redirect_uri: server.redirectUri,
750
- code_challenge: codeChallenge,
751
- code_challenge_method: "S256",
752
- state
753
- });
754
- console.log(" \u6B63\u5728\u6253\u5F00\u6D4F\u89C8\u5668\u8FDB\u884C\u6388\u6743...");
755
- console.log(` \u6388\u6743\u5730\u5740: ${authUrl}`);
756
- try {
757
- const cp = await open(authUrl);
758
- console.log(` \u6D4F\u89C8\u5668\u5DF2\u542F\u52A8 (pid: ${cp.pid})`);
759
- } catch (err) {
760
- console.log(` \u65E0\u6CD5\u81EA\u52A8\u6253\u5F00\u6D4F\u89C8\u5668\uFF0C\u8BF7\u624B\u52A8\u8BBF\u95EE\u4E0A\u65B9\u5730\u5740`);
761
- }
762
- const result = await server.waitForCallback(state);
763
- const tokenResponse = await exchangeToken(metadata.token_endpoint, {
764
- grant_type: "authorization_code",
765
- code: result.code,
766
- redirect_uri: server.redirectUri,
767
- client_id: client.client_id,
768
- code_verifier: codeVerifier
769
- }, customHeaders);
770
- return {
771
- accessToken: tokenResponse.access_token,
772
- refreshToken: tokenResponse.refresh_token,
773
- clientId: client.client_id,
774
- expiresAt: tokenResponse.expires_in ? Date.now() + tokenResponse.expires_in * 1e3 : void 0
775
- };
776
- } finally {
777
- server.close();
778
- }
779
- }
780
-
781
- // src/core/auth/device-code-flow.ts
782
- import qrcode from "qrcode-terminal";
783
- init_errors();
784
- function generateQR(url) {
785
- return new Promise((resolve) => {
786
- qrcode.generate(url, { small: true }, (code) => {
787
- resolve(code);
788
- });
789
- });
790
- }
791
- function sleep(ms) {
792
- return new Promise((resolve) => setTimeout(resolve, ms));
793
- }
794
- async function startDeviceCodeInit(host, customHeaders) {
795
- const metadata = await fetchOAuthMetadata(host, customHeaders);
796
- if (!metadata.device_authorization_endpoint) {
797
- throw new ClientError(
798
- "\u5F53\u524D\u7AD9\u70B9\u4E0D\u652F\u6301 Device Code \u6388\u6743",
799
- "DEVICE_CODE_NOT_SUPPORTED"
800
- );
801
- }
802
- const client = await registerClient(metadata.registration_endpoint, void 0, customHeaders);
803
- const deviceRes = await fetch(metadata.device_authorization_endpoint, {
804
- method: "POST",
805
- headers: { "Content-Type": "application/x-www-form-urlencoded", ...customHeaders },
806
- body: new URLSearchParams({ client_id: client.client_id })
807
- });
808
- if (!deviceRes.ok) {
809
- throw new ServerError(
810
- `\u83B7\u53D6\u8BBE\u5907\u6388\u6743\u7801\u5931\u8D25: ${deviceRes.status}`,
811
- "DEVICE_CODE_REQUEST_FAILED"
812
- );
813
- }
814
- const deviceRaw = await deviceRes.json();
815
- const device = deviceRaw.device_code ? deviceRaw : deviceRaw.data;
816
- if (!device?.device_code) {
817
- const msg = deviceRaw.msg || JSON.stringify(deviceRaw);
818
- throw new ServerError(
819
- `\u83B7\u53D6\u8BBE\u5907\u6388\u6743\u7801\u5931\u8D25: \u54CD\u5E94\u4E2D\u7F3A\u5C11 device_code (${msg})`,
820
- "DEVICE_CODE_REQUEST_FAILED"
821
- );
822
- }
823
- return {
824
- ...device,
825
- client_id: client.client_id
826
- };
827
- }
828
- async function pollDeviceCodeOnce(options) {
829
- const metadata = await fetchOAuthMetadata(options.host, options.customHeaders);
830
- const tokenRes = await fetch(metadata.token_endpoint, {
831
- method: "POST",
832
- headers: { "Content-Type": "application/x-www-form-urlencoded", ...options.customHeaders },
833
- body: new URLSearchParams({
834
- grant_type: "urn:ietf:params:oauth:grant-type:device_code",
835
- device_code: options.device_code,
836
- client_id: options.client_id
837
- })
838
- });
839
- const tokenRaw = await tokenRes.json();
840
- const data = tokenRaw.access_token || tokenRaw.error ? tokenRaw : tokenRaw.data ?? tokenRaw;
841
- if (data.access_token) {
842
- return {
843
- status: "ok",
844
- token_data: {
845
- accessToken: data.access_token,
846
- refreshToken: data.refresh_token,
847
- clientId: options.client_id,
848
- expiresAt: data.expires_in ? Date.now() + data.expires_in * 1e3 : void 0
849
- }
850
- };
851
- }
852
- if (data.error === "slow_down") return { status: "slow_down" };
853
- if (data.error === "authorization_pending") return { status: "authorization_pending" };
854
- if (data.error === "expired_token") return { status: "expired_token" };
855
- throw new ServerError(
856
- `Device Code \u6388\u6743\u5931\u8D25: ${data.error ?? "\u672A\u77E5\u9519\u8BEF"}`,
857
- "DEVICE_CODE_FAILED"
858
- );
859
- }
860
- async function startDeviceCodePoll(options) {
861
- let interval = options.interval * 1e3;
862
- const deadline = Date.now() + options.expires_in * 1e3;
863
- while (Date.now() < deadline) {
864
- await sleep(interval);
865
- const result = await pollDeviceCodeOnce(options);
866
- if (result.status === "ok" && result.token_data) {
867
- return result.token_data;
868
- }
869
- if (result.status === "slow_down") {
870
- interval += 5e3;
871
- continue;
872
- }
873
- if (result.status === "authorization_pending") {
874
- continue;
875
- }
876
- if (result.status === "expired_token") {
877
- throw new ClientError("\u6388\u6743\u5DF2\u8D85\u65F6\uFF0C\u8BF7\u91CD\u65B0\u6267\u884C meegle auth login --device-code", "DEVICE_CODE_EXPIRED");
878
- }
879
- }
880
- throw new ClientError("\u6388\u6743\u5DF2\u8D85\u65F6\uFF0C\u8BF7\u91CD\u65B0\u6267\u884C meegle auth login --device-code", "DEVICE_CODE_EXPIRED");
881
- }
882
- async function startDeviceCodeFlow(host, customHeaders) {
883
- const initResult = await startDeviceCodeInit(host, customHeaders);
884
- const qr = await generateQR(initResult.verification_uri_complete);
885
- console.log("\n \u8BF7\u4F7F\u7528\u624B\u673A\u626B\u63CF\u4E8C\u7EF4\u7801\uFF0C\u6216\u5728\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00\u4EE5\u4E0B\u5730\u5740\uFF1A\n");
886
- console.log(qr);
887
- console.log(` \u5730\u5740: ${initResult.verification_uri_complete}`);
888
- console.log(` \u6388\u6743\u7801: ${initResult.user_code}`);
889
- console.log(`
890
- \u7B49\u5F85\u6388\u6743\u4E2D... (\u8D85\u65F6\u65F6\u95F4 ${Math.round(initResult.expires_in / 60)} \u5206\u949F)
891
- `);
892
- return startDeviceCodePoll({
893
- host,
894
- device_code: initResult.device_code,
895
- client_id: initResult.client_id,
896
- interval: initResult.interval,
897
- expires_in: initResult.expires_in,
898
- customHeaders
899
- });
900
- }
901
-
902
- // src/core/first-run.ts
903
- init_env();
904
- init_config();
905
- import { select, input } from "@inquirer/prompts";
906
- var PRESET_HOSTS = [
907
- { name: "\u98DE\u4E66\u9879\u76EE (project.feishu.cn)", value: "project.feishu.cn" },
908
- { name: "Meegle (meegle.com)", value: "meegle.com" },
909
- { name: "\u81EA\u5B9A\u4E49\u57DF\u540D", value: "__custom__" }
910
- ];
911
- function normalizeHost(raw) {
912
- const trimmed = raw.trim();
913
- try {
914
- const url = new URL(trimmed.includes("://") ? trimmed : `https://${trimmed}`);
915
- return url.host;
916
- } catch {
917
- return trimmed;
918
- }
919
- }
920
- async function promptHost() {
921
- const choice = await select({
922
- message: "\u9009\u62E9\u7AD9\u70B9",
923
- choices: PRESET_HOSTS
924
- });
925
- if (choice === "__custom__") {
926
- const raw = await input({
927
- message: "\u8F93\u5165\u7AD9\u70B9\u57DF\u540D\uFF08\u652F\u6301\u7C98\u8D34\u5B8C\u6574 URL\uFF09",
928
- validate: (val) => val.trim() ? true : "\u57DF\u540D\u4E0D\u80FD\u4E3A\u7A7A"
929
- });
930
- return normalizeHost(raw);
931
- }
932
- return choice;
933
- }
934
- async function checkFirstRun(profileName) {
935
- if (hasEnvAuth()) return;
936
- const profile = profileName ?? await getCurrentProfileName();
937
- const config = await loadConfig(profile);
938
- const needsHost = !config.host;
939
- const store = await createTokenStore(profile);
940
- const tokenManager = new TokenManager(store, config.host);
941
- const needsAuth = !await tokenManager.isAuthenticated();
942
- if (!needsHost && !needsAuth) return;
943
- if (!process.stdin.isTTY) {
944
- console.error("\n \u9519\u8BEF\uFF1AMeegle CLI \u672A\u521D\u59CB\u5316\uFF0C\u4E14\u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301\u4EA4\u4E92\u5F0F\u5F15\u5BFC\u3002\n");
945
- console.error(" \u8BF7\u901A\u8FC7\u4EE5\u4E0B\u65B9\u5F0F\u4E4B\u4E00\u5B8C\u6210\u914D\u7F6E\uFF1A");
946
- console.error(" 1. \u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF: MEEGLE_HOST \u548C MEEGLE_USER_ACCESS_TOKEN");
947
- console.error(" 2. \u5728\u4EA4\u4E92\u5F0F\u7EC8\u7AEF\u4E2D\u8FD0\u884C: meegle config init && meegle auth login\n");
948
- process.exit(1);
949
- }
950
- console.log("\n \u6B22\u8FCE\u4F7F\u7528 Meegle CLI\uFF01\u9996\u6B21\u4F7F\u7528\u9700\u8981\u5B8C\u6210\u521D\u59CB\u5316\u3002\n");
951
- if (needsHost) {
952
- const host = await promptHost();
953
- config.host = host;
954
- await saveConfig(config, profile);
955
- console.log(`
956
- \u2713 \u914D\u7F6E\u5DF2\u4FDD\u5B58: host=${host}
957
- `);
958
- }
959
- if (needsAuth) {
960
- const tokenData = await startAuthCodeFlow(config.host);
961
- await tokenManager.saveToken(tokenData);
962
- console.log("\n \u2713 \u767B\u5F55\u6210\u529F\uFF01\n");
963
- }
964
- console.log(" \u521D\u59CB\u5316\u5B8C\u6210\u3002\u8BF7\u91CD\u65B0\u6267\u884C\u60A8\u7684\u547D\u4EE4\u3002\n");
965
- process.exit(0);
966
- }
967
-
968
- // src/commands/auth/login.ts
969
- init_errors();
970
-
971
- // src/output/table.ts
972
- import Table from "cli-table3";
973
- function renderTable(data) {
974
- if (data.length === 0) return "(no data)";
975
- const headers = Object.keys(data[0]);
976
- const table = new Table({ head: headers });
977
- for (const row of data) {
978
- table.push(headers.map((h) => String(row[h] ?? "")));
979
- }
980
- return table.toString();
981
- }
982
-
983
- // src/output/formatter.ts
984
- var VALID_FORMATS = /* @__PURE__ */ new Set(["json", "table", "ndjson"]);
985
- function validateFormat(format) {
986
- if (!VALID_FORMATS.has(format)) {
987
- throw new Error(`\u4E0D\u652F\u6301\u7684\u8F93\u51FA\u683C\u5F0F "${format}"\uFF0C\u53EF\u9009: json, table, ndjson`);
988
- }
989
- return format;
990
- }
991
- function formatOutput(data, format) {
992
- switch (format) {
993
- case "json":
994
- return JSON.stringify(data, null, 2);
995
- case "ndjson":
996
- if (Array.isArray(data)) {
997
- return data.map((item) => JSON.stringify(item)).join("\n");
998
- }
999
- return JSON.stringify(data);
1000
- case "table":
1001
- return formatAsTable(data);
1002
- }
1003
- }
1004
- function formatAsTable(data) {
1005
- if (data === null || data === void 0) return "(no data)";
1006
- if (Array.isArray(data)) {
1007
- if (data.length === 0) return "(no data)";
1008
- if (typeof data[0] === "object" && data[0] !== null) {
1009
- return renderTable(data);
1010
- }
1011
- return data.map(String).join("\n");
1012
- }
1013
- if (typeof data === "object") {
1014
- const entries = Object.entries(data);
1015
- if (entries.length === 0) return "(no data)";
1016
- return renderTable(entries.map(([key, value]) => ({
1017
- key,
1018
- value: typeof value === "object" ? JSON.stringify(value) : value
1019
- })));
1020
- }
1021
- return String(data);
1022
- }
1023
-
1024
- // src/commands/auth/login.ts
1025
- var PRESET_HOSTS2 = [
1026
- { name: "\u98DE\u4E66\u9879\u76EE (project.feishu.cn)", value: "project.feishu.cn" },
1027
- { name: "Meegle (meegle.com)", value: "meegle.com" },
1028
- { name: "\u81EA\u5B9A\u4E49\u57DF\u540D", value: "__custom__" }
1029
- ];
1030
- async function resolveHost(profile, hostFlag) {
1031
- if (hostFlag) {
1032
- const host2 = normalizeHost(hostFlag);
1033
- const config2 = await loadConfig(profile);
1034
- config2.host = host2;
1035
- await saveConfig(config2, profile);
1036
- return host2;
1037
- }
1038
- const config = await loadConfig(profile);
1039
- const envHost = getHost();
1040
- if (envHost) return envHost;
1041
- if (config.host) return config.host;
1042
- const choice = await select2({
1043
- message: "\u9009\u62E9\u7AD9\u70B9",
1044
- choices: PRESET_HOSTS2
1045
- });
1046
- let host;
1047
- if (choice === "__custom__") {
1048
- const raw = await input2({
1049
- message: "\u8F93\u5165\u7AD9\u70B9\u57DF\u540D\uFF08\u652F\u6301\u7C98\u8D34\u5B8C\u6574 URL\uFF09",
1050
- validate: (val) => val.trim() ? true : "\u57DF\u540D\u4E0D\u80FD\u4E3A\u7A7A"
1051
- });
1052
- host = normalizeHost(raw);
1053
- } else {
1054
- host = choice;
1055
- }
1056
- config.host = host;
1057
- await saveConfig(config, profile);
1058
- console.log(`\u2713 \u914D\u7F6E\u5DF2\u4FDD\u5B58: host=${host}`);
1059
- return host;
1060
- }
1061
- function registerLogin(auth) {
1062
- auth.command("login").description("\u767B\u5F55\u98DE\u4E66\u9879\u76EE").option("--device-code", "\u4F7F\u7528 Device Code \u6D41\u7A0B\uFF08\u65E0\u6D4F\u89C8\u5668\u73AF\u5883\uFF09").option("--phase <phase>", "Device Code \u9636\u6BB5: init \u6216 poll").option("--host <host>", "\u6307\u5B9A\u7AD9\u70B9\u57DF\u540D\uFF08\u8DF3\u8FC7\u4EA4\u4E92\u5F0F\u9009\u62E9\uFF09").option("--device-code-value <code>", "poll \u9636\u6BB5\u7684 device_code").option("--client-id <id>", "poll \u9636\u6BB5\u7684 client_id").option("--interval <seconds>", "poll \u8F6E\u8BE2\u95F4\u9694\uFF08\u79D2\uFF09", "5").option("--expires-in <seconds>", "poll \u8D85\u65F6\u65F6\u95F4\uFF08\u79D2\uFF09", "600").option("--once", "poll \u9636\u6BB5\u5355\u6B21\u5C1D\u8BD5\uFF0C\u7ACB\u5373\u8FD4\u56DE\u7ED3\u679C\uFF08\u4E0D\u963B\u585E\uFF09").option("--format <format>", "\u8F93\u51FA\u683C\u5F0F: json, table, ndjson", "table").action(async (options) => {
1063
- const profileName = auth.parent?.opts().profile ?? await getCurrentProfileName();
1064
- const format = validateFormat(options.format);
1065
- const config = await loadConfig(profileName);
1066
- if (options.phase === "init") {
1067
- if (!options.deviceCode) {
1068
- throw new ClientError("--phase \u4EC5\u5728 --device-code \u6A21\u5F0F\u4E0B\u53EF\u7528", "INVALID_OPTION");
1069
- }
1070
- const host2 = await resolveHost(profileName, options.host);
1071
- const result = await startDeviceCodeInit(host2, config.headers);
1072
- console.log(formatOutput(result, format));
1073
- return;
1074
- }
1075
- if (options.phase === "poll") {
1076
- if (!options.deviceCode) {
1077
- throw new ClientError("--phase \u4EC5\u5728 --device-code \u6A21\u5F0F\u4E0B\u53EF\u7528", "INVALID_OPTION");
1078
- }
1079
- if (!options.deviceCodeValue || !options.clientId) {
1080
- throw new ClientError("--phase poll \u9700\u8981 --device-code-value \u548C --client-id", "MISSING_OPTION");
1081
- }
1082
- const host2 = await resolveHost(profileName, options.host);
1083
- if (options.once) {
1084
- const result = await pollDeviceCodeOnce({
1085
- host: host2,
1086
- device_code: options.deviceCodeValue,
1087
- client_id: options.clientId,
1088
- customHeaders: config.headers
1089
- });
1090
- if (result.status === "ok" && result.token_data) {
1091
- const store3 = await createTokenStore(profileName);
1092
- const tokenManager3 = new TokenManager(store3, host2);
1093
- await tokenManager3.saveToken(result.token_data);
1094
- console.log(formatOutput({ status: "ok", message: "\u767B\u5F55\u6210\u529F" }, format));
1095
- } else {
1096
- console.log(formatOutput({ status: result.status }, format));
1097
- }
1098
- return;
1099
- }
1100
- const tokenData2 = await startDeviceCodePoll({
1101
- host: host2,
1102
- device_code: options.deviceCodeValue,
1103
- client_id: options.clientId,
1104
- interval: Number(options.interval),
1105
- expires_in: Number(options.expiresIn),
1106
- customHeaders: config.headers
1107
- });
1108
- const store2 = await createTokenStore(profileName);
1109
- const tokenManager2 = new TokenManager(store2, host2);
1110
- await tokenManager2.saveToken(tokenData2);
1111
- console.log(formatOutput({ status: "ok", message: "\u767B\u5F55\u6210\u529F" }, format));
1112
- return;
1113
- }
1114
- const host = await resolveHost(profileName, options.host);
1115
- const tokenData = options.deviceCode ? await startDeviceCodeFlow(host, config.headers) : await startAuthCodeFlow(host, config.headers);
1116
- const store = await createTokenStore(profileName);
1117
- const tokenManager = new TokenManager(store, host);
1118
- await tokenManager.saveToken(tokenData);
1119
- console.log(`\u2713 [${profileName}] \u767B\u5F55\u6210\u529F\uFF01`);
1120
- });
1121
- }
1122
-
1123
- // src/commands/auth/logout.ts
1124
- init_config();
1125
- function registerLogout(auth) {
1126
- auth.command("logout").description("\u9000\u51FA\u767B\u5F55\uFF0C\u6E05\u9664\u672C\u5730\u51ED\u636E").action(async () => {
1127
- const profileName = auth.parent?.opts().profile;
1128
- const profile = profileName ?? await getCurrentProfileName();
1129
- const store = await createTokenStore(profile);
1130
- await store.clear();
1131
- console.log("\u2713 \u5DF2\u9000\u51FA\u767B\u5F55");
1132
- });
1133
- }
1134
-
1135
- // src/commands/auth/status.ts
1136
- init_env();
1137
- init_config();
1138
- async function buildStatusResult(profile) {
1139
- const config = await loadConfig(profile);
1140
- const host = config.host ?? null;
1141
- if (getUserToken()) {
1142
- return { authenticated: true, host, source: "env", expires_in_minutes: null };
1143
- }
1144
- const store = await createTokenStore(profile);
1145
- const token = await store.load();
1146
- if (!token) {
1147
- return { authenticated: false, host, source: null, expires_in_minutes: null };
1148
- }
1149
- let expiresInMinutes = null;
1150
- if (token.expiresAt) {
1151
- const remaining = token.expiresAt - Date.now();
1152
- expiresInMinutes = remaining > 0 ? Math.floor(remaining / 6e4) : 0;
1153
- }
1154
- return {
1155
- authenticated: true,
1156
- host,
1157
- source: "token_store",
1158
- expires_in_minutes: expiresInMinutes
1159
- };
1160
- }
1161
- function registerStatus(auth) {
1162
- auth.command("status").description("\u67E5\u770B\u5F53\u524D\u767B\u5F55\u72B6\u6001\u548C Token \u6709\u6548\u671F").option("--format <format>", "\u8F93\u51FA\u683C\u5F0F", "table").action(async (options) => {
1163
- const profileName = auth.parent?.opts().profile ?? await getCurrentProfileName();
1164
- const format = validateFormat(options.format);
1165
- const result = await buildStatusResult(profileName);
1166
- if (format === "json") {
1167
- console.log(formatOutput(result, "json"));
1168
- if (!result.authenticated) process.exit(1);
1169
- return;
1170
- }
1171
- if (result.source === "env") {
1172
- console.log("\u5DF2\u767B\u5F55\uFF08\u901A\u8FC7\u73AF\u5883\u53D8\u91CF MEEGLE_USER_ACCESS_TOKEN\uFF09");
1173
- return;
1174
- }
1175
- if (!result.authenticated) {
1176
- console.log("\u672A\u767B\u5F55");
1177
- process.exit(1);
1178
- }
1179
- console.log("\u5DF2\u767B\u5F55");
1180
- if (result.expires_in_minutes !== null) {
1181
- if (result.expires_in_minutes > 0) {
1182
- console.log(`Token \u6709\u6548\u671F\u5269\u4F59: ${result.expires_in_minutes} \u5206\u949F`);
1183
- } else {
1184
- console.log("Token \u5DF2\u8FC7\u671F\uFF0C\u5C06\u5728\u4E0B\u6B21\u4F7F\u7528\u65F6\u81EA\u52A8\u5237\u65B0");
1185
- }
1186
- }
1187
- });
1188
- }
1189
-
1190
- // src/commands/auth/index.ts
1191
- function registerAuthCommands(program) {
1192
- const auth = program.command("auth").description("\u8BA4\u8BC1\u7BA1\u7406");
1193
- registerLogin(auth);
1194
- registerLogout(auth);
1195
- registerStatus(auth);
1196
- }
1197
-
1198
- // src/commands/config/init.ts
1199
- init_config();
1200
- function registerInit(config) {
1201
- config.command("init").description("\u521D\u59CB\u5316\u914D\u7F6E").option("--host <host>", "\u7AD9\u70B9\u57DF\u540D", "meegle.com").action(async (options) => {
1202
- const profile = config.parent?.opts().profile ?? await getCurrentProfileName();
1203
- await saveConfig({ host: options.host }, profile);
1204
- console.log(`\u2713 [${profile}] \u914D\u7F6E\u5DF2\u521D\u59CB\u5316: host=${options.host}`);
1205
- });
1206
- }
1207
-
1208
- // src/commands/config/show.ts
1209
- init_config();
1210
- function registerShow(config) {
1211
- config.command("show").description("\u67E5\u770B\u5F53\u524D\u914D\u7F6E").action(async () => {
1212
- const profile = config.parent?.opts().profile ?? await getCurrentProfileName();
1213
- const cfg = await loadConfig(profile);
1214
- console.log(`Profile: ${profile}`);
1215
- console.log(JSON.stringify(cfg, null, 2));
1216
- });
1217
- }
1218
-
1219
- // src/commands/config/set.ts
1220
- init_config();
1221
- function registerSet(config) {
1222
- config.command("set <key> <value>").description("\u4FEE\u6539\u5F53\u524D profile \u7684\u914D\u7F6E\u9879").action(async (key, value) => {
1223
- const profile = config.parent?.opts().profile ?? await getCurrentProfileName();
1224
- await setProfileValue(profile, key, value);
1225
- console.log(`\u2713 [${profile}] ${key}=${value}`);
1226
- });
1227
- }
1228
-
1229
- // src/commands/config/get.ts
1230
- init_config();
1231
- function registerGet(config) {
1232
- config.command("get <key>").description("\u67E5\u770B\u5F53\u524D profile \u7684\u914D\u7F6E\u9879").action(async (key) => {
1233
- const profile = config.parent?.opts().profile ?? await getCurrentProfileName();
1234
- const value = await getProfileValue(profile, key);
1235
- if (value === void 0) {
1236
- console.log("(\u672A\u8BBE\u7F6E)");
1237
- } else {
1238
- console.log(String(value));
1239
- }
1240
- });
1241
- }
1242
-
1243
- // src/commands/config/profile.ts
1244
- init_config();
1245
- import { select as select3, input as input3 } from "@inquirer/prompts";
1246
- var PRESET_HOSTS3 = [
1247
- { name: "\u98DE\u4E66\u9879\u76EE (project.feishu.cn)", value: "project.feishu.cn" },
1248
- { name: "Meegle (meegle.com)", value: "meegle.com" },
1249
- { name: "\u81EA\u5B9A\u4E49\u57DF\u540D", value: "__custom__" }
1250
- ];
1251
- function registerProfile(config) {
1252
- const profile = config.command("profile").description("\u73AF\u5883\u7BA1\u7406");
1253
- profile.command("list").description("\u5217\u51FA\u6240\u6709 profile").action(async () => {
1254
- const { names, current } = await listProfiles();
1255
- if (names.length === 0) {
1256
- console.log("\u6682\u65E0 profile\uFF0C\u4F7F\u7528 meegle config profile create <name> \u521B\u5EFA");
1257
- return;
1258
- }
1259
- for (const name of names) {
1260
- const marker = name === current ? " (\u5F53\u524D)" : "";
1261
- console.log(` ${name}${marker}`);
1262
- }
1263
- });
1264
- profile.command("current").description("\u663E\u793A\u5F53\u524D profile").action(async () => {
1265
- const name = await getCurrentProfileName();
1266
- console.log(name);
1267
- });
1268
- profile.command("use <name>").description("\u5207\u6362\u5F53\u524D profile").action(async (name) => {
1269
- const { names } = await listProfiles();
1270
- if (!names.includes(name)) {
1271
- console.error(`profile "${name}" \u4E0D\u5B58\u5728`);
1272
- process.exit(1);
1273
- }
1274
- await setCurrentProfileName(name);
1275
- console.log(`\u2713 \u5DF2\u5207\u6362\u5230 ${name}`);
1276
- });
1277
- profile.command("create <name>").description("\u521B\u5EFA\u65B0 profile").action(async (name) => {
1278
- const { names } = await listProfiles();
1279
- if (names.includes(name)) {
1280
- console.error(`profile "${name}" \u5DF2\u5B58\u5728`);
1281
- process.exit(1);
1282
- }
1283
- const choice = await select3({
1284
- message: "\u9009\u62E9\u7AD9\u70B9",
1285
- choices: PRESET_HOSTS3
1286
- });
1287
- let host;
1288
- if (choice === "__custom__") {
1289
- const raw = await input3({
1290
- message: "\u8F93\u5165\u7AD9\u70B9\u57DF\u540D\uFF08\u652F\u6301\u7C98\u8D34\u5B8C\u6574 URL\uFF09",
1291
- validate: (val) => val.trim() ? true : "\u57DF\u540D\u4E0D\u80FD\u4E3A\u7A7A"
1292
- });
1293
- host = normalizeHost(raw);
1294
- } else {
1295
- host = choice;
1296
- }
1297
- await saveConfig({ host }, name);
1298
- console.log(`\u2713 profile "${name}" \u5DF2\u521B\u5EFA (host=${host})`);
1299
- const shouldLogin = await select3({
1300
- message: "\u662F\u5426\u7ACB\u5373\u767B\u5F55\uFF1F",
1301
- choices: [
1302
- { name: "\u662F", value: true },
1303
- { name: "\u5426", value: false }
1304
- ]
1305
- });
1306
- if (shouldLogin) {
1307
- const tokenData = await startAuthCodeFlow(host);
1308
- const store = await createTokenStore(name);
1309
- const tokenManager = new TokenManager(store, host);
1310
- await tokenManager.saveToken(tokenData);
1311
- console.log("\u2713 \u767B\u5F55\u6210\u529F\uFF01");
1312
- }
1313
- await setCurrentProfileName(name);
1314
- console.log(`\u2713 \u5DF2\u5207\u6362\u5230 ${name}`);
1315
- });
1316
- profile.command("delete <name>").description("\u5220\u9664 profile \u53CA\u5176\u51ED\u636E").action(async (name) => {
1317
- const { current } = await listProfiles();
1318
- if (name === current) {
1319
- console.error(`\u4E0D\u80FD\u5220\u9664\u5F53\u524D\u6B63\u5728\u4F7F\u7528\u7684 profile "${name}"\uFF0C\u8BF7\u5148\u5207\u6362`);
1320
- process.exit(1);
1321
- }
1322
- const store = await createTokenStore(name);
1323
- await store.clear();
1324
- await deleteProfile(name);
1325
- console.log(`\u2713 profile "${name}" \u5DF2\u5220\u9664`);
1326
- });
1327
- }
1328
-
1329
- // src/commands/config/index.ts
1330
- function registerConfigCommands(program) {
1331
- const config = program.command("config").description("\u914D\u7F6E\u7BA1\u7406");
1332
- registerInit(config);
1333
- registerShow(config);
1334
- registerSet(config);
1335
- registerGet(config);
1336
- registerProfile(config);
1337
- }
1338
-
1339
- // src/dynamic/params-parser.ts
1340
- init_errors();
1341
- function parseSetFlag(input4) {
1342
- const eqIndex = input4.indexOf("=");
1343
- if (eqIndex === -1) {
1344
- throw new ClientError(
1345
- `--set \u683C\u5F0F\u9519\u8BEF: "${input4}"\uFF0C\u5E94\u4E3A key=value`,
1346
- "INVALID_SET_FLAG"
1347
- );
1348
- }
1349
- const key = input4.slice(0, eqIndex);
1350
- const rawValue = input4.slice(eqIndex + 1);
1351
- let value;
1352
- try {
1353
- const parsed = JSON.parse(rawValue);
1354
- value = typeof parsed === "object" && parsed !== null ? parsed : rawValue;
1355
- } catch {
1356
- value = rawValue;
1357
- }
1358
- return { field_key: key, field_value: value };
1359
- }
1360
- function parseParams(json) {
1361
- try {
1362
- const parsed = JSON.parse(json);
1363
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1364
- throw new Error("not an object");
1365
- }
1366
- return parsed;
1367
- } catch {
1368
- throw new ClientError(
1369
- "--params JSON \u89E3\u6790\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u683C\u5F0F\u662F\u5426\u4E3A\u5408\u6CD5 JSON \u5BF9\u8C61",
1370
- "INVALID_PARAMS_JSON"
1371
- );
1372
- }
1373
- }
1374
- function mergeParams(setFields, params, cliFlags) {
1375
- const base = params ? { ...params } : {};
1376
- for (const [key, value] of Object.entries(cliFlags)) {
1377
- if (value !== void 0) {
1378
- base[key] = value;
1379
- }
1380
- }
1381
- if (setFields.length > 0) {
1382
- const existingFields = Array.isArray(base.fields) ? base.fields : [];
1383
- const setKeys = new Set(setFields.map((f) => f.field_key));
1384
- const kept = existingFields.filter((f) => !setKeys.has(f.field_key));
1385
- base.fields = [...kept, ...setFields];
1386
- }
1387
- return base;
1388
- }
1389
-
1390
- // src/output/fields.ts
1391
- function parseFieldList(input4) {
1392
- if (!input4) return [];
1393
- return input4.split(",").map((f) => f.trim()).filter(Boolean);
1394
- }
1395
- function filterFields(data, fields) {
1396
- if (fields.length === 0) return data;
1397
- if (Array.isArray(data)) {
1398
- return data.map((item) => pickFields(item, fields));
1399
- }
1400
- return pickFields(data, fields);
1401
- }
1402
- function pickFields(obj, fields) {
1403
- if (typeof obj !== "object" || obj === null) return obj;
1404
- const result = {};
1405
- for (const field of fields) {
1406
- if (field.includes(".")) {
1407
- result[field] = resolveDotPath(obj, field.split("."));
1408
- } else if (field in obj) {
1409
- result[field] = obj[field];
1410
- }
1411
- }
1412
- return result;
1413
- }
1414
- function resolveDotPath(current, segments) {
1415
- for (let i = 0; i < segments.length; i++) {
1416
- if (current === null || current === void 0) return void 0;
1417
- if (Array.isArray(current)) {
1418
- const remaining = segments.slice(i);
1419
- return current.map((item) => resolveDotPath(item, remaining));
1420
- }
1421
- if (typeof current === "object") {
1422
- current = current[segments[i]];
1423
- } else {
1424
- return void 0;
1425
- }
1426
- }
1427
- return current;
1428
- }
1429
-
1430
- // src/dynamic/response.ts
1431
- init_errors();
1432
-
1433
- // src/core/logger.ts
1434
- import chalk from "chalk";
1435
- var LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
1436
- var currentLevel = "info";
1437
- function setLogLevel(level) {
1438
- currentLevel = level;
1439
- }
1440
- function shouldLog(level) {
1441
- return LEVELS[level] >= LEVELS[currentLevel];
1442
- }
1443
- var logger = {
1444
- debug: (msg) => {
1445
- if (shouldLog("debug")) console.error(chalk.gray(`[debug] ${msg}`));
1446
- },
1447
- info: (msg) => {
1448
- if (shouldLog("info")) console.error(chalk.blue(`[info] ${msg}`));
1449
- },
1450
- warn: (msg) => {
1451
- if (shouldLog("warn")) console.error(chalk.yellow(`[warn] ${msg}`));
1452
- },
1453
- error: (msg) => {
1454
- if (shouldLog("error")) console.error(chalk.red(`[error] ${msg}`));
1455
- }
1456
- };
1457
-
1458
- // src/dynamic/response.ts
1459
- function unwrapResponse(raw) {
1460
- if (raw === null || raw === void 0) return void 0;
1461
- const response = raw;
1462
- const content = response.content;
1463
- if (!Array.isArray(content)) return void 0;
1464
- if (response.isError) {
1465
- const allTexts = content.filter((e) => e.type === "text" && e.text).map((e) => e.text);
1466
- const message = allTexts.join("\n") || "\u670D\u52A1\u7AEF\u8FD4\u56DE\u9519\u8BEF";
1467
- throw new ServerError(message, "SERVER_CALL_FAILED");
1468
- }
1469
- const dataTexts = [];
1470
- for (const entry of content) {
1471
- if (entry.type !== "text" || !entry.text) continue;
1472
- if (entry.text.startsWith("logid:")) {
1473
- logger.debug(entry.text);
1474
- } else {
1475
- dataTexts.push(entry.text);
1476
- }
1477
- }
1478
- if (dataTexts.length === 0) return void 0;
1479
- const text = dataTexts[0];
1480
- try {
1481
- return JSON.parse(text);
1482
- } catch {
1483
- return text;
1484
- }
1485
- }
1486
-
1487
- // src/dynamic/register.ts
1488
- init_errors();
1489
- var RESOURCE_DESCRIPTIONS = {
1490
- workitem: "\u5DE5\u4F5C\u9879\u57DF \u2014 \u5DE5\u4F5C\u9879\u5B9E\u4F8B\u7684 CRUD \u548C\u6A21\u578B\u914D\u7F6E\uFF0C\u901A\u8FC7 work-item-id \u6807\u8BC6",
1491
- workflow: "\u5DE5\u4F5C\u6D41\u57DF \u2014 \u8282\u70B9\u6D41\u8F6C\u4E0E\u72B6\u6001\u6D41\u8F6C\uFF0C\u4F9D\u8D56 workitem \u7684 work-item-id",
1492
- subtask: "\u5B50\u4EFB\u52A1 \u2014 \u8282\u70B9\u4E0B\u7684\u5B50\u4EFB\u52A1\u64CD\u4F5C\uFF0C\u4F9D\u8D56 workflow \u7684 node-id",
1493
- comment: "\u8BC4\u8BBA\u57DF \u2014 \u8DE8\u5B9E\u4F53\u8BC4\u8BBA\uFF0C\u5F53\u524D\u901A\u8FC7 work-item-id \u5173\u8054",
1494
- workhour: "\u5DE5\u65F6\u57DF \u2014 \u5DE5\u65F6\u8BB0\u5F55\u4E0E\u6392\u671F\uFF0C\u4F9D\u8D56 workitem \u7684 work-item-id",
1495
- relation: "\u5173\u7CFB\u57DF \u2014 \u5DE5\u4F5C\u9879\u4E4B\u95F4\u7684\u5173\u8054\u5173\u7CFB\u5B9A\u4E49\u4E0E\u67E5\u8BE2",
1496
- mywork: "\u5DE5\u4F5C\u53F0\u57DF \u2014 \u8DE8\u7A7A\u95F4\u7684\u4E2A\u4EBA\u5F85\u529E/\u5DF2\u529E",
1497
- view: "\u89C6\u56FE\u57DF \u2014 \u89C6\u56FE\u7684\u521B\u5EFA\u3001\u67E5\u770B\u3001\u66F4\u65B0\u3001\u641C\u7D22",
1498
- chart: "\u5EA6\u91CF\u57DF \u2014 \u56FE\u8868\u67E5\u770B\u4E0E\u5217\u8868\uFF0C\u4F9D\u8D56 view \u7684 view-id",
1499
- team: "\u4EBA\u5458\u57DF \u2014 \u56E2\u961F\u5217\u8868\u4E0E\u6210\u5458\u67E5\u8BE2",
1500
- user: "\u4EBA\u5458\u57DF \u2014 \u7528\u6237\u641C\u7D22\uFF0C\u652F\u6301\u540D\u79F0\u6A21\u7CCA\u5339\u914D",
1501
- project: "\u7A7A\u95F4\u57DF \u2014 \u7A7A\u95F4\u4FE1\u606F\u641C\u7D22"
1502
- };
1503
- var registeredCommands = [];
1504
- function registerDynamicCommands(program, commands, client) {
1505
- registeredCommands = commands;
1506
- const groups = /* @__PURE__ */ new Map();
1507
- for (const cmd of commands) {
1508
- const list = groups.get(cmd.resource) ?? [];
1509
- list.push(cmd);
1510
- groups.set(cmd.resource, list);
1511
- }
1512
- for (const [resource, cmds] of groups) {
1513
- const group = program.command(resource).description(RESOURCE_DESCRIPTIONS[resource] ?? `${resource} \u64CD\u4F5C`);
1514
- for (const cmd of cmds) {
1515
- const flagSummary = buildFlagSummary(cmd);
1516
- const sub = group.command(cmd.method).description(`${cmd.description ?? ""} ${flagSummary}`);
1517
- for (const param of cmd.parameters) {
1518
- if (param.name === "url") continue;
1519
- const kebab = param.name.replace(/_/g, "-");
1520
- let desc = param.description ?? "";
1521
- if (param.required) desc = `[\u5FC5\u586B] ${desc}`;
1522
- if (param.type === "array") {
1523
- const itemType = param.items?.type;
1524
- if (itemType === "object") {
1525
- desc += "\uFF08\u591A\u4E2A JSON \u5BF9\u8C61\uFF0C\u91CD\u590D\u4F20\u5165\uFF09";
1526
- } else if (itemType === "number") {
1527
- desc += "\uFF08\u591A\u4E2A\u6570\u5B57\uFF0C\u9017\u53F7\u5206\u9694\u6216\u91CD\u590D\u4F20\u5165\uFF09";
1528
- } else {
1529
- desc += "\uFF08\u591A\u4E2A\u503C\uFF0C\u9017\u53F7\u5206\u9694\u6216\u91CD\u590D\u4F20\u5165\uFF09";
1530
- }
1531
- }
1532
- if (param.type === "object") desc += "\uFF08JSON \u683C\u5F0F\uFF09";
1533
- const typeLabel = formatTypeLabel(param);
1534
- if (param.type === "boolean") {
1535
- sub.option(`--${kebab}`, desc);
1536
- } else if (param.type === "array") {
1537
- sub.option(`--${kebab} <${typeLabel}>`, desc, collect, []);
1538
- } else {
1539
- sub.option(`--${kebab} <${typeLabel}>`, desc);
1540
- }
1541
- }
1542
- sub.option("--params <json>", "\u5B8C\u6574 JSON \u53C2\u6570\uFF08--set \u548C flag \u4F1A\u8986\u76D6\u540C\u540D\u5B57\u6BB5\uFF09");
1543
- if (cmd.hasFields) {
1544
- sub.option("--set <key=value>", "\u8BBE\u7F6E\u5B57\u6BB5 key=value\uFF0C\u53EF\u91CD\u590D\uFF0Cvalue \u652F\u6301 JSON", collect, []);
1545
- }
1546
- sub.action(async (options) => {
1547
- const { params: rawParams, set: rawSet, ...cliFlags } = options;
1548
- const snakeFlags = {};
1549
- for (const param of cmd.parameters) {
1550
- const camelKey = snakeToCamel(param.name);
1551
- const value = cliFlags[camelKey];
1552
- if (value === void 0) continue;
1553
- if (param.type === "array") {
1554
- const arr = Array.isArray(value) ? value : [value];
1555
- const itemType = param.items?.type;
1556
- const expanded = itemType === "object" ? arr : arr.flatMap((s) => s.split(",").map((v) => v.trim()));
1557
- const filtered = expanded.filter((s) => s !== "");
1558
- if (filtered.length > 0) {
1559
- snakeFlags[param.name] = convertArrayItems(filtered, itemType);
1560
- }
1561
- } else if (param.type === "number" && typeof value === "string") {
1562
- const num = Number(value);
1563
- if (Number.isNaN(num)) {
1564
- throw new ClientError(
1565
- `\u53C2\u6570 --${param.name.replace(/_/g, "-")} \u9700\u8981\u6570\u5B57\uFF0C\u6536\u5230: "${value}"`,
1566
- "CLIENT_INVALID_TYPE"
1567
- );
1568
- }
1569
- snakeFlags[param.name] = num;
1570
- } else {
1571
- snakeFlags[param.name] = value;
1572
- }
1573
- }
1574
- const setFields = (rawSet ?? []).map(parseSetFlag);
1575
- const parsedParams = rawParams ? parseParams(rawParams) : void 0;
1576
- const finalParams = mergeParams(setFields, parsedParams, snakeFlags);
1577
- const missing = cmd.parameters.filter((p) => p.required && p.name !== "url").filter((p) => finalParams[p.name] === void 0 || finalParams[p.name] === null).map((p) => `--${p.name.replace(/_/g, "-")}`);
1578
- if (missing.length > 0) {
1579
- throw new ClientError(
1580
- `\u7F3A\u5C11\u5FC5\u586B\u53C2\u6570: ${missing.join(", ")}`,
1581
- "CLIENT_MISSING_REQUIRED",
1582
- `\u4F7F\u7528 meegle inspect ${cmd.resource}.${cmd.method} \u67E5\u770B\u53C2\u6570\u8BE6\u60C5`
1583
- );
1584
- }
1585
- const raw = await client.callTool(cmd.toolName, finalParams);
1586
- let result = unwrapResponse(raw);
1587
- if (result !== void 0) {
1588
- const rootOpts = program.opts();
1589
- const selectStr = rootOpts.select;
1590
- if (selectStr) {
1591
- const fields = parseFieldList(selectStr);
1592
- result = filterFields(result, fields);
1593
- }
1594
- const format = validateFormat(rootOpts.format ?? "json");
1595
- console.log(formatOutput(result, format));
1596
- }
1597
- });
1598
- }
1599
- }
1600
- }
1601
- function collect(value, previous) {
1602
- return [...previous, value];
1603
- }
1604
- function formatTypeLabel(param) {
1605
- if (param.type === "array" && param.items?.type) {
1606
- return `array<${param.items.type}>`;
1607
- }
1608
- return param.type;
1609
- }
1610
- function convertArrayItems(values, itemType) {
1611
- if (itemType === "number") {
1612
- return values.map((v) => {
1613
- const n = Number(v);
1614
- if (Number.isNaN(n)) {
1615
- throw new ClientError(
1616
- `\u6570\u7EC4\u5143\u7D20\u9700\u8981\u6570\u5B57\uFF0C\u6536\u5230: "${v}"`,
1617
- "CLIENT_INVALID_TYPE"
1618
- );
1619
- }
1620
- return n;
1621
- });
1622
- }
1623
- if (itemType === "object") {
1624
- return values.map((v) => {
1625
- try {
1626
- return JSON.parse(v);
1627
- } catch {
1628
- throw new ClientError(
1629
- `\u65E0\u6CD5\u89E3\u6790 JSON: ${v}`,
1630
- "CLIENT_INVALID_JSON"
1631
- );
1632
- }
1633
- });
1634
- }
1635
- return values;
1636
- }
1637
- function snakeToCamel(s) {
1638
- return s.replace(/_(\w)/g, (_, c) => c.toUpperCase());
1639
- }
1640
- function buildFlagSummary(cmd) {
1641
- const flags = [];
1642
- for (const param of cmd.parameters) {
1643
- if (param.name === "url") continue;
1644
- const kebab = param.name.replace(/_/g, "-");
1645
- flags.push(param.required ? `--${kebab}*` : `--${kebab}`);
1646
- }
1647
- if (cmd.hasFields) flags.push("--set");
1648
- if (flags.length === 0) return "";
1649
- return `[${flags.join(", ")}]`;
1650
- }
1651
-
1652
- // src/commands/inspect.ts
1653
- function registerInspectCommand(program) {
1654
- program.command("inspect [command]").description("\u67E5\u770B\u547D\u4EE4\u7684\u53C2\u6570\u8BE6\u60C5\uFF08\u5982 inspect workitem.create\uFF09").action(async (command) => {
1655
- if (!command) {
1656
- const grouped = /* @__PURE__ */ new Map();
1657
- for (const cmd2 of registeredCommands) {
1658
- const list = grouped.get(cmd2.resource) ?? [];
1659
- list.push(cmd2.method);
1660
- grouped.set(cmd2.resource, list);
1661
- }
1662
- for (const [resource2, methods] of grouped) {
1663
- console.log(`${resource2}: ${methods.join(", ")}`);
1664
- }
1665
- return;
1666
- }
1667
- const parts = command.includes(".") ? command.split(".") : command.split(" ");
1668
- const [resource, method] = parts;
1669
- const cmd = registeredCommands.find(
1670
- (c) => c.resource === resource && c.method === method
1671
- );
1672
- if (!cmd) {
1673
- console.error(`\u672A\u627E\u5230\u547D\u4EE4: ${command}`);
1674
- console.error("\u4F7F\u7528 meegle inspect \u67E5\u770B\u6240\u6709\u53EF\u7528\u547D\u4EE4");
1675
- process.exit(1);
1676
- }
1677
- const params = cmd.parameters.filter((p) => p.name !== "url").map((p) => ({
1678
- flag: `--${p.name.replace(/_/g, "-")}`,
1679
- type: formatTypeLabel(p),
1680
- required: p.required ?? false,
1681
- description: p.description ?? ""
1682
- }));
1683
- if (cmd.hasFields) {
1684
- params.push({
1685
- flag: "--set",
1686
- type: "key=value",
1687
- required: false,
1688
- description: "\u8BBE\u7F6E\u5B57\u6BB5 key=value\uFF0C\u53EF\u91CD\u590D\uFF0Cvalue \u652F\u6301 JSON"
1689
- });
1690
- }
1691
- params.push({
1692
- flag: "--params",
1693
- type: "json",
1694
- required: false,
1695
- description: "\u5B8C\u6574 JSON \u53C2\u6570\uFF08--set \u548C flag \u4F1A\u8986\u76D6\u540C\u540D\u5B57\u6BB5\uFF09"
1696
- });
1697
- console.log(`
1698
- meegle ${cmd.resource} ${cmd.method}`);
1699
- console.log(` ${cmd.description ?? ""}
1700
- `);
1701
- const required = params.filter((p) => p.required);
1702
- const optional = params.filter((p) => !p.required);
1703
- if (required.length > 0) {
1704
- console.log(" \u5FC5\u586B\u53C2\u6570:");
1705
- for (const p of required) {
1706
- console.log(` ${p.flag} <${p.type}>`);
1707
- if (p.description) console.log(` ${p.description}`);
1708
- }
1709
- console.log();
1710
- }
1711
- if (optional.length > 0) {
1712
- console.log(" \u53EF\u9009\u53C2\u6570:");
1713
- for (const p of optional) {
1714
- console.log(` ${p.flag} <${p.type}>`);
1715
- if (p.description) console.log(` ${p.description}`);
1716
- }
1717
- console.log();
1718
- }
1719
- });
1720
- }
1721
-
1722
- // src/commands/index.ts
1723
- function registerStaticCommands(program) {
1724
- registerAuthCommands(program);
1725
- registerConfigCommands(program);
1726
- registerInspectCommand(program);
1727
- }
1728
-
1729
- // src/dynamic/mapper.ts
1730
- var FALLBACK_TABLE = {
1731
- // WorkItem 工作项域 (9)
1732
- create_workitem: { resource: "workitem", method: "create", hasFields: true },
1733
- get_workitem_brief: { resource: "workitem", method: "get" },
1734
- update_field: { resource: "workitem", method: "update", hasFields: true },
1735
- search_by_mql: { resource: "workitem", method: "query" },
1736
- get_workitem_op_record: { resource: "workitem", method: "list-op-records" },
1737
- list_workitem_types: { resource: "workitem", method: "meta-types" },
1738
- get_workitem_field_meta: { resource: "workitem", method: "meta-create-fields" },
1739
- list_workitem_field_config: { resource: "workitem", method: "meta-fields" },
1740
- list_workitem_role_config: { resource: "workitem", method: "meta-roles" },
1741
- // WorkFlow 工作流域 (6)
1742
- transition_node: { resource: "workflow", method: "transition" },
1743
- get_node_detail: { resource: "workflow", method: "get-node" },
1744
- update_node: { resource: "workflow", method: "update-node", hasFields: true },
1745
- list_node_field_config: { resource: "workflow", method: "meta-node-fields" },
1746
- get_transitable_states: { resource: "workflow", method: "list-state-transitions" },
1747
- get_transition_required: { resource: "workflow", method: "list-state-required" },
1748
- // SubTask 子任务 (1)
1749
- update_node_subtask: { resource: "subtask", method: "update", hasFields: true },
1750
- // Comment 评论域 (2)
1751
- add_comment: { resource: "comment", method: "add" },
1752
- list_workitem_comments: { resource: "comment", method: "list" },
1753
- // WorkHour 工时域 (2)
1754
- get_workitem_man_hour_records: { resource: "workhour", method: "list-records" },
1755
- list_schedule: { resource: "workhour", method: "list-schedule" },
1756
- // Relation 关系域 (2)
1757
- list_workitem_relations: { resource: "relation", method: "meta-definitions" },
1758
- list_related_workitems: { resource: "relation", method: "list" },
1759
- // MyWork 工作台域 (1)
1760
- list_todo: { resource: "mywork", method: "todo" },
1761
- // View 视图域 (4)
1762
- create_fixed_view: { resource: "view", method: "create-fixed" },
1763
- get_view_detail: { resource: "view", method: "get" },
1764
- update_fixed_view: { resource: "view", method: "update-fixed" },
1765
- search_view_by_title: { resource: "view", method: "search" },
1766
- // Chart 度量域 (2)
1767
- get_chart_detail: { resource: "chart", method: "get" },
1768
- list_charts: { resource: "chart", method: "list" },
1769
- // UserGroup 人员域 (3)
1770
- list_project_team: { resource: "team", method: "list" },
1771
- list_team_members: { resource: "team", method: "list-members" },
1772
- search_user_info: { resource: "user", method: "search" },
1773
- // Project 空间域 (1)
1774
- search_project_info: { resource: "project", method: "search" }
1775
- };
1776
- function mapTool(tool) {
1777
- if (tool.metadata?.resource && tool.metadata?.method) {
1778
- return {
1779
- resource: tool.metadata.resource,
1780
- method: tool.metadata.method,
1781
- toolName: tool.name,
1782
- description: tool.description,
1783
- parameters: tool.parameters ?? []
1784
- };
1785
- }
1786
- const fallback = FALLBACK_TABLE[tool.name];
1787
- if (fallback) {
1788
- return {
1789
- resource: fallback.resource,
1790
- method: fallback.method,
1791
- toolName: tool.name,
1792
- description: tool.description,
1793
- parameters: tool.parameters ?? [],
1794
- hasFields: fallback.hasFields
1795
- };
1796
- }
1797
- logger.debug(`\u672A\u77E5 tool "${tool.name}" \u4E0D\u5728\u6620\u5C04\u8868\u4E2D\uFF0C\u4F7F\u7528 name parsing \u56DE\u9000`);
1798
- const parts = tool.name.split("_");
1799
- const resource = parts[0];
1800
- const method = parts.slice(1).join("-");
1801
- return {
1802
- resource,
1803
- method: method || "default",
1804
- toolName: tool.name,
1805
- description: tool.description,
1806
- parameters: tool.parameters ?? []
1807
- };
1808
- }
1809
- function mapTools(tools) {
1810
- return tools.map(mapTool);
1811
- }
1812
-
1813
- // src/dynamic/cache.ts
1814
- init_profile_validation();
1815
- import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
1816
- import { join as join3, dirname } from "path";
1817
- var DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
1818
- var ToolCache = class {
1819
- filePath;
1820
- ttlMs;
1821
- constructor(cacheDir, profile = "default", ttlMs = DEFAULT_TTL_MS) {
1822
- validateProfileName(profile);
1823
- const filename = profile === "default" ? "tools.json" : `tools-${profile}.json`;
1824
- this.filePath = join3(cacheDir, filename);
1825
- this.ttlMs = ttlMs;
1826
- }
1827
- async get() {
1828
- try {
1829
- const raw = await readFile3(this.filePath, "utf-8");
1830
- const data = JSON.parse(raw);
1831
- if (!Array.isArray(data.tools) || typeof data.timestamp !== "number") {
1832
- return null;
1833
- }
1834
- const stale = Date.now() - data.timestamp > this.ttlMs;
1835
- return { tools: data.tools, stale };
1836
- } catch {
1837
- return null;
1838
- }
1839
- }
1840
- async set(tools) {
1841
- const data = { timestamp: Date.now(), tools };
1842
- await mkdir3(dirname(this.filePath), { recursive: true });
1843
- await writeFile3(this.filePath, JSON.stringify(data, null, 2));
1844
- }
1845
- };
1846
-
1847
- // src/dynamic/discovery.ts
1848
- async function discoverTools(client) {
1849
- const result = await client.listTools();
1850
- const tools = result?.tools ?? [];
1851
- return tools.map(convertTool);
1852
- }
1853
- function convertTool(entry) {
1854
- const parameters = [];
1855
- const props = entry.inputSchema?.properties ?? {};
1856
- const required = new Set(entry.inputSchema?.required ?? []);
1857
- for (const [name, schema] of Object.entries(props)) {
1858
- const param = {
1859
- name,
1860
- type: schema.type ?? "string",
1861
- description: schema.description,
1862
- required: required.has(name)
1863
- };
1864
- if (schema.type === "array" && schema.items?.type) {
1865
- param.items = { type: schema.items.type };
1866
- }
1867
- parameters.push(param);
1868
- }
1869
- return {
1870
- name: entry.name,
1871
- description: entry.description,
1872
- parameters
1873
- };
1874
- }
1875
-
1876
- // src/cli.ts
1877
- init_config();
1878
- init_errors();
1879
-
1880
- // src/core/mcp-client.ts
1881
- init_errors();
1882
- var McpClient = class {
1883
- baseUrl;
1884
- tokenManager;
1885
- customHeaders;
1886
- requestId = 0;
1887
- constructor(baseUrl, tokenManager, customHeaders) {
1888
- this.baseUrl = baseUrl;
1889
- this.tokenManager = tokenManager;
1890
- this.customHeaders = customHeaders ?? {};
1891
- }
1892
- async call(method, params) {
1893
- try {
1894
- return await this._doCall(method, params);
1895
- } catch (err) {
1896
- if (err instanceof HttpError && err.httpStatus === 401) {
1897
- const store = this.tokenManager.getStore();
1898
- const tokenData = await store.load();
1899
- if (tokenData) {
1900
- const refreshed = await this.tokenManager.refreshToken(tokenData);
1901
- if (refreshed) {
1902
- return await this._doCall(method, params);
1903
- }
1904
- }
1905
- throw new ClientError("\u8BA4\u8BC1\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55", "AUTH_EXPIRED", "meegle auth login");
1906
- }
1907
- throw err;
1908
- }
1909
- }
1910
- async _doCall(method, params) {
1911
- const token = await this.tokenManager.getToken();
1912
- const id = ++this.requestId;
1913
- const body = { jsonrpc: "2.0", id, method, params };
1914
- const headers = {
1915
- "Content-Type": "application/json",
1916
- "User-Agent": "meegle-cli",
1917
- ...this.customHeaders
1918
- };
1919
- if (token) {
1920
- headers["Authorization"] = `Bearer ${token}`;
1921
- }
1922
- const response = await fetch(this.baseUrl, {
1923
- method: "POST",
1924
- headers,
1925
- body: JSON.stringify(body)
1926
- });
1927
- if (!response.ok) {
1928
- throw new HttpError(
1929
- `\u670D\u52A1\u7AEF\u8FD4\u56DE\u9519\u8BEF (${response.status})`,
1930
- "SERVER_HTTP_ERROR",
1931
- response.status
1932
- );
1933
- }
1934
- const data = await response.json();
1935
- if (data.error) {
1936
- throw new ServerError(data.error.message, "SERVER_CALL_FAILED");
1937
- }
1938
- return data.result;
1939
- }
1940
- async listTools() {
1941
- return this.call("tools/list");
1942
- }
1943
- async callTool(name, params) {
1944
- return this.call("tools/call", { name, arguments: params });
1945
- }
1946
- };
1947
-
1948
- // src/cli.ts
1949
- import { readFileSync } from "fs";
1950
- import { join as join4, dirname as dirname2 } from "path";
1951
- import { fileURLToPath } from "url";
1952
- function loadVersion() {
1953
- try {
1954
- const dir = dirname2(fileURLToPath(import.meta.url));
1955
- for (const rel of ["../../package.json", "../package.json"]) {
1956
- try {
1957
- const raw = readFileSync(join4(dir, rel), "utf-8");
1958
- return JSON.parse(raw).version;
1959
- } catch {
1960
- continue;
1961
- }
1962
- }
1963
- } catch {
1964
- }
1965
- return "0.0.0";
1966
- }
1967
- var version = loadVersion();
1968
- var SKIP_FIRST_RUN = ["config", "auth", "help"];
1969
- function getCommandChain(cmd) {
1970
- const chain = [];
1971
- let current = cmd;
1972
- while (current && current.name() !== "meegle") {
1973
- chain.unshift(current.name());
1974
- current = current.parent ?? null;
1975
- }
1976
- return chain;
1977
- }
1978
- function extractProfileFromArgv(argv) {
1979
- for (let i = 0; i < argv.length; i++) {
1980
- if (argv[i] === "--profile" && i + 1 < argv.length) {
1981
- return argv[i + 1];
1982
- }
1983
- if (argv[i]?.startsWith("--profile=")) {
1984
- return argv[i].slice("--profile=".length);
1985
- }
1986
- }
1987
- return void 0;
1988
- }
1989
- async function createProgram() {
1990
- const program = new Command();
1991
- program.name("meegle").description(`Agent-First CLI for Meegle (Lark Project)
1992
-
1993
- \u9886\u57DF\u6A21\u578B:
1994
- workitem\uFF08\u5DE5\u4F5C\u9879\uFF09\u2500 \u6838\u5FC3\u5B9E\u4F53\uFF0C\u901A\u8FC7 work-item-id \u6807\u8BC6
1995
- \u251C\u2500 workflow\uFF08\u5DE5\u4F5C\u6D41\uFF09\u2500 \u8282\u70B9\u6D41\u8F6C\u4E0E\u72B6\u6001\u6D41\u8F6C\uFF0C\u4F9D\u8D56 work-item-id
1996
- \u2502 \u2514\u2500 subtask\uFF08\u5B50\u4EFB\u52A1\uFF09\u2500 \u8282\u70B9\u4E0B\u7684\u5B50\u4EFB\u52A1\uFF0C\u4F9D\u8D56 node-id
1997
- \u251C\u2500 comment\uFF08\u8BC4\u8BBA\uFF09\u2500 \u5DE5\u4F5C\u9879\u7684\u8BC4\u8BBA\uFF0C\u4F9D\u8D56 work-item-id
1998
- \u251C\u2500 workhour\uFF08\u5DE5\u65F6\uFF09\u2500 \u5DE5\u65F6\u8BB0\u5F55\u4E0E\u6392\u671F\uFF0C\u4F9D\u8D56 work-item-id
1999
- \u2514\u2500 relation\uFF08\u5173\u7CFB\uFF09\u2500 \u5DE5\u4F5C\u9879\u4E4B\u95F4\u7684\u5173\u8054\u5173\u7CFB
2000
-
2001
- mywork\uFF08\u5DE5\u4F5C\u53F0\uFF09\u2500 \u8DE8\u7A7A\u95F4\u7684\u4E2A\u4EBA\u5F85\u529E/\u5DF2\u529E
2002
- view\uFF08\u89C6\u56FE\uFF09\u2500 \u56FA\u5B9A\u89C6\u56FE\u3001\u7B5B\u9009\u89C6\u56FE
2003
- chart\uFF08\u5EA6\u91CF\uFF09\u2500 \u89C6\u56FE\u4E0B\u7684\u56FE\u8868\uFF0C\u4F9D\u8D56 view-id
2004
- team / user\uFF08\u4EBA\u5458\uFF09\u2500 \u56E2\u961F\u4E0E\u7528\u6237
2005
- project\uFF08\u7A7A\u95F4\uFF09\u2500 \u7A7A\u95F4\u4FE1\u606F
2006
-
2007
- \u5178\u578B\u64CD\u4F5C\u94FE\u8DEF:
2008
- \u521B\u5EFA\u5DE5\u4F5C\u9879: workitem meta-types \u2192 workitem meta-create-fields \u2192 workitem create
2009
- \u6D41\u8F6C\u8282\u70B9: workflow get-node \u2192 workflow list-state-transitions \u2192 workflow transition
2010
- \u67E5\u770B\u5173\u8054: relation meta-definitions \u2192 relation list`).version(version).option("--format <format>", "\u8F93\u51FA\u683C\u5F0F: json | table | ndjson", "json").option("--select <props>", "\u9009\u53D6\u8F93\u51FA\u5C5E\u6027\uFF0C\u9017\u53F7\u5206\u9694").option("--verbose", "\u663E\u793A\u8BE6\u7EC6\u65E5\u5FD7").option("--profile <name>", "\u4F7F\u7528\u6307\u5B9A profile\uFF08\u4E34\u65F6\uFF0C\u4E0D\u6539\u53D8\u5168\u5C40\uFF09").hook("preAction", async (thisCommand, actionCommand) => {
2011
- const opts = thisCommand.opts();
2012
- if (opts.verbose) {
2013
- setLogLevel("debug");
2014
- }
2015
- const chain = getCommandChain(actionCommand);
2016
- const topCmd = chain[0] ?? "";
2017
- const profileName2 = thisCommand.opts().profile;
2018
- if (!SKIP_FIRST_RUN.includes(topCmd)) {
2019
- await checkFirstRun(profileName2);
2020
- }
2021
- });
2022
- registerStaticCommands(program);
2023
- const profileName = extractProfileFromArgv(process.argv);
2024
- try {
2025
- const config = await loadConfig(profileName);
2026
- const serverUrl = getServerUrl(config);
2027
- const currentProfile = profileName ?? await getCurrentProfileName();
2028
- const store = await createTokenStore(currentProfile);
2029
- const tokenManager = new TokenManager(store, config.host);
2030
- const client = new McpClient(serverUrl, tokenManager, config.headers);
2031
- const cache = new ToolCache(getCacheDir(), currentProfile);
2032
- const cached = await cache.get();
2033
- let tools;
2034
- if (cached) {
2035
- tools = cached.tools;
2036
- if (cached.stale) {
2037
- logger.debug("\u7F13\u5B58\u5DF2\u8FC7\u671F\uFF0C\u540E\u53F0\u5237\u65B0\u4E2D...");
2038
- discoverTools(client).then((fresh) => {
2039
- if (fresh.length > 0) cache.set(fresh);
2040
- }).catch(() => {
2041
- });
2042
- }
2043
- } else {
2044
- logger.debug("\u6B63\u5728\u83B7\u53D6\u547D\u4EE4\u5217\u8868...");
2045
- tools = await discoverTools(client);
2046
- if (tools.length > 0) {
2047
- await cache.set(tools);
2048
- }
2049
- }
2050
- const mapped = mapTools(tools);
2051
- registerDynamicCommands(program, mapped, client);
2052
- logger.debug(`\u5DF2\u52A0\u8F7D ${mapped.length} \u4E2A\u547D\u4EE4`);
2053
- } catch (err) {
2054
- logger.debug(`\u547D\u4EE4\u52A0\u8F7D\u8DF3\u8FC7: ${err}`);
2055
- }
2056
- return program;
2057
- }
2058
- async function run() {
2059
- try {
2060
- const program = await createProgram();
2061
- await program.parseAsync(process.argv);
2062
- } catch (err) {
2063
- if (err instanceof MeegleError) {
2064
- console.error(JSON.stringify(err.toJSON(), null, 2));
2065
- process.exit(err.exitCode);
2066
- }
2067
- console.error(JSON.stringify({
2068
- error: "UNEXPECTED",
2069
- message: err instanceof Error ? err.message : String(err)
2070
- }, null, 2));
2071
- process.exit(1);
2072
- }
2073
- }
2074
-
2075
- // bin/meegle.ts
2076
- run();