@teamclaw/feishu-agent 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,327 @@
1
+ #!/usr/bin/env bun
2
+ import { parseArgs } from "node:util";
3
+ import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
4
+ import { exec } from "node:child_process";
5
+ import { writeFile, mkdir } from "fs/promises";
6
+ import { loadConfig, saveGlobalConfig } from "../../core/config";
7
+ import { FeishuClient } from "../../core/client";
8
+ import { IntrospectionEngine } from "../../core/introspection";
9
+
10
+ interface UserAccessTokenResponse {
11
+ code: number;
12
+ msg: string;
13
+ data?: {
14
+ access_token: string;
15
+ refresh_token: string;
16
+ token_type: string;
17
+ expires_in: number;
18
+ name: string;
19
+ en_name: string;
20
+ avatar: string;
21
+ user_id: string;
22
+ union_id: string;
23
+ };
24
+ }
25
+
26
+ export async function setupCommand() {
27
+ console.log("\n========================================");
28
+ console.log(" Feishu Agent Setup");
29
+ console.log("========================================\n");
30
+
31
+ // Step 1: Get App ID and Secret
32
+ console.log("Step 1: Feishu App Credentials");
33
+ console.log("-".repeat(40));
34
+
35
+ let config = await loadConfig();
36
+ let appId = config.appId;
37
+ let appSecret = config.appSecret;
38
+
39
+ if (!appId) {
40
+ const input = prompt("Enter Feishu App ID:");
41
+ if (!input) {
42
+ console.error("Error: App ID is required.");
43
+ process.exit(1);
44
+ }
45
+ appId = input;
46
+ }
47
+
48
+ if (!appSecret) {
49
+ const input = prompt("Enter Feishu App Secret:");
50
+ if (!input) {
51
+ console.error("Error: App Secret is required.");
52
+ process.exit(1);
53
+ }
54
+ appSecret = input;
55
+ }
56
+
57
+ // Save app credentials
58
+ await saveGlobalConfig({ appId, appSecret });
59
+ await ensureEnvFile(appId, appSecret);
60
+ console.log("App credentials saved.\n");
61
+
62
+ // Step 2: OAuth 2.0 Authorization
63
+ console.log("Step 2: OAuth 2.0 Authorization");
64
+ console.log("-".repeat(40));
65
+ console.log("This will open a browser to authorize with Feishu.");
66
+ console.log("The authorization will grant access to:");
67
+ console.log(" - Your calendar");
68
+ console.log(" - Your events");
69
+ console.log("");
70
+
71
+ const port = 3000;
72
+ const state = generateRandomString(32);
73
+ const redirectUri = `http://localhost:${port}/callback`;
74
+ const encodedRedirectUri = encodeURIComponent(redirectUri);
75
+ const authUrl = `https://open.feishu.cn/open-apis/authen/v1/index?app_id=${appId}&redirect_uri=${encodedRedirectUri}&state=${state}`;
76
+
77
+ console.log("Redirect URI (must be configured in Feishu Console):");
78
+ console.log(` ${redirectUri}\n`);
79
+ console.log("Opening authorization URL...");
80
+ console.log("");
81
+
82
+ // Start local server and open browser
83
+ let authCode: string | null = null;
84
+ const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
85
+ if (req.url?.startsWith("/callback")) {
86
+ const url = new URL(req.url, `http://localhost:${port}`);
87
+ const code = url.searchParams.get("code");
88
+ const receivedState = url.searchParams.get("state");
89
+
90
+ if (!code) {
91
+ res.writeHead(400);
92
+ res.end("No code received");
93
+ return;
94
+ }
95
+
96
+ if (receivedState !== state) {
97
+ res.writeHead(400);
98
+ res.end("State mismatch");
99
+ return;
100
+ }
101
+
102
+ authCode = code;
103
+ res.writeHead(200, { "Content-Type": "text/html" });
104
+ res.end(`
105
+ <html>
106
+ <head><title>Authorization Successful</title></head>
107
+ <body>
108
+ <h1>Authorization Successful!</h1>
109
+ <p>You can close this window and return to the terminal.</p>
110
+ <script>setTimeout(() => window.close(), 3000);</script>
111
+ </body>
112
+ </html>
113
+ `);
114
+ } else {
115
+ res.writeHead(404);
116
+ res.end("Not found");
117
+ }
118
+ });
119
+
120
+ await new Promise<void>((resolve) => {
121
+ server.listen(port, () => {
122
+ openBrowser(authUrl);
123
+ resolve();
124
+ });
125
+ });
126
+
127
+ // Wait for callback with timeout
128
+ const timeout = 5 * 60 * 1000;
129
+ const startTime = Date.now();
130
+
131
+ const waitForAuth = (): Promise<string> => {
132
+ return new Promise((resolve, reject) => {
133
+ const interval = setInterval(() => {
134
+ if (authCode) {
135
+ clearInterval(interval);
136
+ resolve(authCode);
137
+ }
138
+ if (Date.now() - startTime > timeout) {
139
+ clearInterval(interval);
140
+ reject(new Error("Authorization timeout"));
141
+ }
142
+ }, 500);
143
+ });
144
+ };
145
+
146
+ try {
147
+ await waitForAuth();
148
+ } catch (error) {
149
+ console.error("Authorization timeout. Please run setup again.");
150
+ server.close();
151
+ process.exit(1);
152
+ }
153
+
154
+ console.log("Authorization code received, exchanging for access token...");
155
+
156
+ // Exchange code for token
157
+ const tokenResponse = await exchangeCodeForToken(appId, appSecret, authCode!, redirectUri);
158
+
159
+ if (tokenResponse.code !== 0 || !tokenResponse.data) {
160
+ console.error("Failed to get access token:", tokenResponse.msg);
161
+ server.close();
162
+ process.exit(1);
163
+ }
164
+
165
+ const { access_token, refresh_token, user_id, name } = tokenResponse.data;
166
+
167
+ console.log("Access token received!\n");
168
+ console.log(` User ID: ${user_id}`);
169
+ console.log(` Name: ${name}`);
170
+ console.log("");
171
+
172
+ // Save tokens to local config
173
+ await mkdir(".feishu_agent", { recursive: true });
174
+ await writeFile(".feishu_agent/config.json", JSON.stringify({
175
+ appId,
176
+ appSecret,
177
+ userAccessToken: access_token,
178
+ refreshToken: refresh_token,
179
+ userId: user_id,
180
+ }, null, 2));
181
+
182
+ console.log("Configuration saved to .feishu_agent/config.json\n");
183
+
184
+ // Step 3: Get Base Token for Bitable
185
+ console.log("Step 3: Feishu Bitable (多维表格)");
186
+ console.log("-".repeat(40));
187
+
188
+ let baseToken: string | null = null;
189
+ while (!baseToken) {
190
+ const input = prompt("Enter Feishu Base URL (or Base Token): ");
191
+ if (!input) {
192
+ console.error("Error: Base Token is required.");
193
+ process.exit(1);
194
+ }
195
+ baseToken = extractBaseToken(input);
196
+ if (!baseToken) {
197
+ console.error("Invalid input. Could not extract Base Token.");
198
+ console.log("Example: https://xxx.feishu.cn/base/basexxxxxx");
199
+ }
200
+ }
201
+
202
+ console.log(`Detected Base Token: ${baseToken}`);
203
+ console.log("Fetching schema...");
204
+
205
+ const client = new FeishuClient({ appId, appSecret, userAccessToken: access_token });
206
+ const engine = new IntrospectionEngine(client);
207
+
208
+ try {
209
+ const schema = await engine.introspect(baseToken, (msg) => console.log(msg));
210
+ await writeFile(".feishu_agent/schema.json", JSON.stringify(schema, null, 2));
211
+ console.log("Schema saved to .feishu_agent/schema.json\n");
212
+ } catch (error) {
213
+ console.error("Failed to fetch schema:", error instanceof Error ? error.message : String(error));
214
+ // Continue anyway - schema is optional
215
+ }
216
+
217
+ server.close();
218
+
219
+ console.log("========================================");
220
+ console.log(" Setup Complete!");
221
+ console.log("========================================\n");
222
+ console.log("You can now use the following commands:");
223
+ console.log(" feishu-agent calendar list - List your calendars");
224
+ console.log(" feishu-agent calendar event list - List events in a calendar");
225
+ console.log(" feishu-agent todo list - List todos from Bitable");
226
+ console.log(" feishu-agent contact list - List contacts");
227
+ console.log("");
228
+ }
229
+
230
+ function generateRandomString(length: number): string {
231
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
232
+ let result = "";
233
+ for (let i = 0; i < length; i++) {
234
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
235
+ }
236
+ return result;
237
+ }
238
+
239
+ function openBrowser(url: string): void {
240
+ const platform = process.platform;
241
+ let cmd: string;
242
+
243
+ switch (platform) {
244
+ case "win32":
245
+ cmd = `start ${url}`;
246
+ break;
247
+ case "darwin":
248
+ cmd = `open "${url}"`;
249
+ break;
250
+ default:
251
+ cmd = `xdg-open "${url}"`;
252
+ break;
253
+ }
254
+
255
+ exec(cmd, (error) => {
256
+ if (error) {
257
+ console.log("Could not auto-open browser. Please manually visit the URL:");
258
+ console.log(url);
259
+ }
260
+ });
261
+ }
262
+
263
+ async function exchangeCodeForToken(
264
+ appId: string,
265
+ appSecret: string,
266
+ code: string,
267
+ redirectUri: string
268
+ ): Promise<UserAccessTokenResponse> {
269
+ // 使用飞书身份认证 v1 API
270
+ // https://open.feishu.cn/document/ukTMukTMukTM/ukDMz4SMxQjL0MjN
271
+ const response = await fetch("https://open.feishu.cn/open-apis/authen/v1/access_token", {
272
+ method: "POST",
273
+ headers: { "Content-Type": "application/json" },
274
+ body: JSON.stringify({
275
+ grant_type: "authorization_code",
276
+ code: code,
277
+ app_id: appId,
278
+ app_secret: appSecret,
279
+ }),
280
+ });
281
+
282
+ const text = await response.text();
283
+
284
+ // 调试输出:查看原始响应
285
+ console.log("Token exchange response status:", response.status);
286
+ if (response.status !== 200 || text.includes("error")) {
287
+ console.log("Response body:", text);
288
+ }
289
+
290
+ // 处理空响应
291
+ if (!text || text.trim() === "") {
292
+ throw new Error("Empty response from Feishu API");
293
+ }
294
+
295
+ try {
296
+ const data = JSON.parse(text);
297
+ // 如果返回错误,打印详细信息
298
+ if (data.code !== 0) {
299
+ throw new Error(`Feishu API error: ${data.msg || data.message || JSON.stringify(data)}`);
300
+ }
301
+ return data;
302
+ } catch (parseError) {
303
+ throw new Error(`Failed to parse JSON response: ${text.substring(0, 200)}. Error: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
304
+ }
305
+ }
306
+
307
+ function extractBaseToken(input: string): string | null {
308
+ const urlMatch = input.match(/\/base\/([a-zA-Z0-9]+)/);
309
+ if (urlMatch) {
310
+ return urlMatch[1];
311
+ }
312
+ if (/^(base|app)[a-zA-Z0-9]+$/.test(input)) {
313
+ return input;
314
+ }
315
+ return null;
316
+ }
317
+
318
+ async function ensureEnvFile(appId: string, appSecret: string): Promise<void> {
319
+ const envFile = Bun.file(".env");
320
+ if (!(await envFile.exists())) {
321
+ const envContent = `FEISHU_APP_ID=${appId}\nFEISHU_APP_SECRET=${appSecret}\n`;
322
+ await writeFile(".env", envContent);
323
+ console.log("Created .env file with credentials.");
324
+ } else {
325
+ console.log(".env file already exists.");
326
+ }
327
+ }
@@ -0,0 +1,114 @@
1
+ import { Command } from "commander";
2
+ import { FeishuClient } from "../../core/client";
3
+ import { TodoManager } from "../../core/todo";
4
+ import { FeishuConfig } from "../../types";
5
+
6
+ interface TodoOptions {
7
+ title?: string;
8
+ priority?: string;
9
+ recordId?: string;
10
+ }
11
+
12
+ export function createTodoCommands(program: Command, config: FeishuConfig) {
13
+ program
14
+ .command("list")
15
+ .description("List all todos from Bitable")
16
+ .action(async () => {
17
+ await handleList(config);
18
+ });
19
+
20
+ program
21
+ .command("create")
22
+ .description("Create a new todo")
23
+ .requiredOption("--title <string>", "Todo title")
24
+ .option("--priority <string>", "Priority (High, Medium, Low)", "Medium")
25
+ .action(async (options: TodoOptions) => {
26
+ await handleCreate(config, options);
27
+ });
28
+
29
+ program
30
+ .command("done")
31
+ .description("Mark a todo as done")
32
+ .requiredOption("--record-id <string>", "Record ID")
33
+ .action(async (options: TodoOptions) => {
34
+ await handleDone(config, options);
35
+ });
36
+ }
37
+
38
+ async function handleList(config: FeishuConfig) {
39
+ if (!config.appId || !config.appSecret) {
40
+ console.error("Error: Credentials required.");
41
+ process.exit(1);
42
+ }
43
+
44
+ const baseToken = process.env.FEISHU_BASE_TOKEN;
45
+ if (!baseToken) {
46
+ console.error("Error: FEISHU_BASE_TOKEN required.");
47
+ process.exit(1);
48
+ }
49
+
50
+ const client = new FeishuClient(config);
51
+ const todoManager = new TodoManager(client, baseToken);
52
+
53
+ console.log(`Fetching todos from Base: ${baseToken}`);
54
+ const todos = await todoManager.listTodos();
55
+
56
+ if (todos.length > 0) {
57
+ console.table(todos.map((t: any) => ({
58
+ id: t.record_id,
59
+ title: t.fields.Title,
60
+ done: t.fields.Done,
61
+ priority: t.fields.Priority || "N/A"
62
+ })));
63
+ } else {
64
+ console.log("No todos found.");
65
+ }
66
+ }
67
+
68
+ async function handleCreate(config: FeishuConfig, options: TodoOptions) {
69
+ if (!config.appId || !config.appSecret) {
70
+ console.error("Error: Credentials required.");
71
+ process.exit(1);
72
+ }
73
+
74
+ const baseToken = process.env.FEISHU_BASE_TOKEN;
75
+ if (!baseToken) {
76
+ console.error("Error: FEISHU_BASE_TOKEN required.");
77
+ process.exit(1);
78
+ }
79
+
80
+ if (!options?.title) {
81
+ console.error("Error: --title is required.");
82
+ process.exit(1);
83
+ }
84
+
85
+ const client = new FeishuClient(config);
86
+ const todoManager = new TodoManager(client, baseToken);
87
+
88
+ const todo = await todoManager.createTodo(options.title, options.priority || "Medium");
89
+ console.log(`Todo created successfully! ID: ${todo.record_id}`);
90
+ }
91
+
92
+ async function handleDone(config: FeishuConfig, options: TodoOptions) {
93
+ if (!config.appId || !config.appSecret) {
94
+ console.error("Error: Credentials required.");
95
+ process.exit(1);
96
+ }
97
+
98
+ const baseToken = process.env.FEISHU_BASE_TOKEN;
99
+ if (!baseToken) {
100
+ console.error("Error: FEISHU_BASE_TOKEN required.");
101
+ process.exit(1);
102
+ }
103
+
104
+ if (!options?.recordId) {
105
+ console.error("Error: --record-id is required.");
106
+ process.exit(1);
107
+ }
108
+
109
+ const client = new FeishuClient(config);
110
+ const todoManager = new TodoManager(client, baseToken);
111
+
112
+ await todoManager.markDone(options.recordId);
113
+ console.log(`Todo ${options.recordId} marked as done.`);
114
+ }
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env bun
2
+ import { FeishuClient } from "../../core/client";
3
+ import { loadConfig } from "../../core/config";
4
+
5
+ export async function whoamiCommand() {
6
+ console.log("\n=== Feishu Agent - Who Am I ===\n");
7
+
8
+ const config = await loadConfig();
9
+
10
+ if (!config.appId || !config.appSecret) {
11
+ console.error("Error: App credentials not configured.");
12
+ console.error("Run 'feishu-agent setup' to configure credentials.");
13
+ process.exit(1);
14
+ }
15
+
16
+ const client = new FeishuClient({
17
+ appId: config.appId,
18
+ appSecret: config.appSecret,
19
+ userAccessToken: config.userAccessToken,
20
+ });
21
+
22
+ // Check if user has authorized
23
+ if (!client.hasUserToken()) {
24
+ console.log("App credentials:");
25
+ console.log(` App ID: ${config.appId}`);
26
+ console.log("");
27
+ console.log("User authorization: NOT CONFIGURED");
28
+ console.log("");
29
+ console.log("Run 'feishu-agent auth' to authorize with your Feishu account.");
30
+ console.log("This is required for calendar and other user-level operations.");
31
+ return;
32
+ }
33
+
34
+ // Get user info
35
+ try {
36
+ const user = await client.getCurrentUser();
37
+
38
+ if (user) {
39
+ console.log("App credentials:");
40
+ console.log(` App ID: ${config.appId}`);
41
+ console.log("");
42
+ console.log("User authorization: CONFIGURED");
43
+ console.log("");
44
+ console.log("Current user:");
45
+ console.log(` Name: ${user.name}`);
46
+ console.log(` User ID: ${user.user_id}`);
47
+ console.log("");
48
+ console.log("You can use calendar and other user-level commands.");
49
+ } else {
50
+ console.log("App credentials: CONFIGURED");
51
+ console.log("User authorization: CONFIGURED (but unable to fetch user info)");
52
+ console.log("");
53
+ console.log("Note: Token might be expired. Run 'feishu-agent auth' to re-authorize.");
54
+ }
55
+ } catch (error) {
56
+ console.log("App credentials: CONFIGURED");
57
+ console.log("User authorization: CONFIGURED (but verification failed)");
58
+ console.log("");
59
+ console.log(`Error: ${error instanceof Error ? error.message : String(error)}`);
60
+ console.log("");
61
+ console.log("Try running 'feishu-agent auth' to re-authorize.");
62
+ }
63
+ }
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env bun
2
+ import { Command } from "commander";
3
+ import { setupCommand } from "./commands/setup";
4
+ import { authCommand } from "./commands/auth";
5
+ import { whoamiCommand } from "./commands/whoami";
6
+ import { loadConfig } from "../core/config";
7
+ import { createConfigCommands } from "./commands/config";
8
+ import { createCalendarCommands } from "./commands/calendar";
9
+ import { createTodoCommands } from "./commands/todo";
10
+ import { createContactCommands } from "./commands/contact";
11
+
12
+ async function main() {
13
+ const program = new Command();
14
+
15
+ program
16
+ .name("feishu-agent")
17
+ .description("Feishu Agent CLI for AI assistants")
18
+ .version("1.0.0");
19
+
20
+ program
21
+ .command("setup")
22
+ .description("Initialize configuration")
23
+ .action(setupCommand);
24
+
25
+ program
26
+ .command("auth")
27
+ .description("Authenticate with Feishu OAuth")
28
+ .action(authCommand);
29
+
30
+ program
31
+ .command("whoami")
32
+ .description("Show current user info")
33
+ .action(whoamiCommand);
34
+
35
+ // Load config for commands that need it
36
+ const config = await loadConfig();
37
+
38
+ // Config subcommand group
39
+ const configCmd = program
40
+ .command("config")
41
+ .description("Manage configuration");
42
+ createConfigCommands(configCmd);
43
+
44
+ // Calendar subcommand group
45
+ const calendarCmd = program
46
+ .command("calendar")
47
+ .description("Manage calendar events");
48
+ createCalendarCommands(calendarCmd, config);
49
+
50
+ // Todo subcommand group
51
+ const todoCmd = program
52
+ .command("todo")
53
+ .description("Manage todos");
54
+ createTodoCommands(todoCmd, config);
55
+
56
+ // Contact subcommand group
57
+ const contactCmd = program
58
+ .command("contact")
59
+ .description("Manage contacts");
60
+ createContactCommands(contactCmd, config);
61
+
62
+ await program.parseAsync();
63
+ }
64
+
65
+ main().catch((err) => {
66
+ console.error("Error:", err.message);
67
+ process.exit(1);
68
+ });