@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.
package/README.md ADDED
@@ -0,0 +1,178 @@
1
+ # Feishu Agent
2
+
3
+ Feishu Agent is a TypeScript/Node.js middleware layer for Feishu (Lark) API integration, designed for AI agents via MCP protocol.
4
+
5
+ ## Features
6
+
7
+ - 📅 Calendar management (list calendars, events, create/delete events)
8
+ - ✅ Todo management via Bitable
9
+ - 👥 Contact management (list users, search by name/email)
10
+ - 🔐 OAuth 2.0 authentication support
11
+ - 🚀 CLI interface with commander
12
+
13
+ ## Installation
14
+
15
+ ### Global Install (Recommended)
16
+
17
+ ```bash
18
+ bun add -g @teamclaw/feishu-agent
19
+ ```
20
+
21
+ After installation, you can use the `feishu_agent` command directly:
22
+
23
+ ```bash
24
+ feishu_agent calendar list
25
+ ```
26
+
27
+ ### Run with bunx (No Install)
28
+
29
+ ```bash
30
+ bunx @teamclaw/feishu-agent calendar list
31
+ ```
32
+
33
+ ### Local Development
34
+
35
+ ```bash
36
+ bun install
37
+ ```
38
+
39
+ ## Quick Start
40
+
41
+ ### 1. Setup
42
+
43
+ Run the setup command to configure your Feishu app credentials:
44
+
45
+ ```bash
46
+ feishu_agent setup
47
+ ```
48
+
49
+ Or export environment variables:
50
+ ```bash
51
+ export FEISHU_APP_ID=cli_xxx
52
+ export FEISHU_APP_SECRET=xxx
53
+ ```
54
+
55
+ ### 2. Authenticate
56
+
57
+ ```bash
58
+ feishu_agent auth
59
+ ```
60
+
61
+ This will open a browser window for OAuth 2.0 authorization.
62
+
63
+ ## Usage
64
+
65
+ ### Calendar
66
+
67
+ ```bash
68
+ # List calendars
69
+ feishu_agent calendar list
70
+
71
+ # List events
72
+ feishu_agent calendar events
73
+
74
+ # Create event
75
+ feishu_agent calendar create --summary "Meeting" --start "2026-03-01 14:00" --end "2026-03-01 15:00"
76
+
77
+ # Create event with attendees
78
+ feishu_agent calendar create --summary "Meeting" --start "2026-03-01 14:00" --end "2026-03-01 15:00" --attendee-name "张三"
79
+
80
+ # Delete event
81
+ feishu_agent calendar delete --event-id "xxx"
82
+ ```
83
+
84
+ ### Todo
85
+
86
+ ```bash
87
+ # List todos (requires FEISHU_BASE_TOKEN env)
88
+ feishu_agent todo list
89
+
90
+ # Create todo
91
+ feishu_agent todo create --title "Task" --priority "High"
92
+
93
+ # Mark todo as done
94
+ feishu_agent todo done --record-id "xxx"
95
+ ```
96
+
97
+ ### Contact
98
+
99
+ ```bash
100
+ # List users
101
+ feishu_agent contact list --dept "0"
102
+
103
+ # Search users
104
+ feishu_agent contact search "张三"
105
+ ```
106
+
107
+ ### Configuration
108
+
109
+ ```bash
110
+ # Set config
111
+ feishu_agent config set appId cli_xxx
112
+
113
+ # Get config
114
+ feishu_agent config get appId
115
+
116
+ # List all config
117
+ feishu_agent config list
118
+ ```
119
+
120
+ ## Commands
121
+
122
+ ```
123
+ feishu_agent <command> [options]
124
+
125
+ Commands:
126
+ setup Initialize configuration
127
+ auth Authenticate with Feishu OAuth
128
+ whoami Show current user info
129
+ config Manage configuration
130
+ calendar Manage calendar events
131
+ todo Manage todos
132
+ contact Manage contacts
133
+ ```
134
+
135
+ ## Build
136
+
137
+ ```bash
138
+ # Build binary
139
+ bun run build
140
+ ```
141
+
142
+ ## Test
143
+
144
+ ```bash
145
+ bun test
146
+ ```
147
+
148
+ ## Architecture
149
+
150
+ ```
151
+ src/
152
+ ├── core/ # Business logic - Feishu API wrappers
153
+ │ ├── client.ts # HTTP client with auth
154
+ │ ├── auth-manager.ts # Token lifecycle management
155
+ │ ├── config.ts # Global config management
156
+ │ ├── calendar.ts # Calendar API
157
+ │ ├── contact.ts # Contact API
158
+ │ └── todo.ts # Bitable Todo API
159
+ ├── index.ts # Main entry point with CLI router
160
+ └── types/ # TypeScript interfaces
161
+ ```
162
+
163
+ ## Configuration
164
+
165
+ Global config is stored at `~/.feishu-agent/config.json`:
166
+
167
+ ```json
168
+ {
169
+ "appId": "cli_xxx",
170
+ "appSecret": "xxx",
171
+ "userAccessToken": "xxx",
172
+ "refreshToken": "xxx"
173
+ }
174
+ ```
175
+
176
+ ## License
177
+
178
+ MIT
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@teamclaw/feishu-agent",
3
+ "version": "1.0.0",
4
+ "description": "Feishu Agent CLI for AI assistants",
5
+ "type": "module",
6
+ "private": false,
7
+ "bin": {
8
+ "feishu_agent": "src/index.ts"
9
+ },
10
+ "scripts": {
11
+ "test": "bun test",
12
+ "start:mcp": "bun run src/mcp/server.ts",
13
+ "start": "bun run src/index.ts",
14
+ "build": "bun build ./src/index.ts --compile --outfile=feishu_agent"
15
+ },
16
+ "devDependencies": {
17
+ "@types/bun": "latest"
18
+ },
19
+ "peerDependencies": {
20
+ "typescript": "^5"
21
+ },
22
+ "dependencies": {
23
+ "@modelcontextprotocol/sdk": "^1.27.1",
24
+ "commander": "^14.0.3",
25
+ "zod": "^4.3.6"
26
+ },
27
+ "engines": {
28
+ "bun": ">=1.0.0"
29
+ },
30
+ "files": [
31
+ "src",
32
+ "README.md"
33
+ ],
34
+ "keywords": [
35
+ "feishu",
36
+ "lark",
37
+ "mcp",
38
+ "ai",
39
+ "assistant",
40
+ "calendar",
41
+ "todo",
42
+ "cli"
43
+ ],
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/teamclaw/feishu-agent.git"
47
+ },
48
+ "license": "MIT"
49
+ }
@@ -0,0 +1,309 @@
1
+ #!/usr/bin/env bun
2
+ import { parseArgs } from "node:util";
3
+ import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
4
+ import { loadConfig, saveGlobalConfig } from "../../core/config";
5
+
6
+ interface UserAccessTokenResponse {
7
+ code: number;
8
+ msg: string;
9
+ data?: {
10
+ access_token: string;
11
+ refresh_token: string;
12
+ token_type: string;
13
+ expires_in: number;
14
+ name: string;
15
+ en_name: string;
16
+ avatar: string;
17
+ user_id: string;
18
+ union_id: string;
19
+ };
20
+ }
21
+
22
+ export async function authCommand(args: string[]) {
23
+ const { values } = parseArgs({
24
+ args,
25
+ strict: false,
26
+ options: {
27
+ port: { type: "string", default: "3000" },
28
+ },
29
+ });
30
+
31
+ // Load global config
32
+ const config = await loadConfig();
33
+ const appId = config.appId;
34
+ const appSecret = config.appSecret;
35
+ const port = parseInt(values["port"] as string || "3000", 10);
36
+
37
+ if (!appId || !appSecret) {
38
+ console.error("Error: FEISHU_APP_ID and FEISHU_APP_SECRET must be set.");
39
+ console.error("Run 'feishu-agent setup' or export environment variables.");
40
+ process.exit(1);
41
+ }
42
+
43
+ console.log("\n=== Feishu OAuth 2.0 Authorization ===\n");
44
+ console.log("Step 1: Generating authorization URL...\n");
45
+
46
+ // Generate a random state for security
47
+ const state = generateRandomString(32);
48
+ const redirectUri = `http://localhost:${port}/callback`;
49
+
50
+ // URL encode the redirect_uri
51
+ const encodedRedirectUri = encodeURIComponent(redirectUri);
52
+
53
+ // Construct the authorization URL
54
+ // Note: Scopes must be configured in Feishu Developer Console, not in the URL
55
+ const authUrl = `https://open.feishu.cn/open-apis/authen/v1/index?app_id=${appId}&redirect_uri=${encodedRedirectUri}&state=${state}`;
56
+
57
+ console.log("Redirect URI (configure this in Feishu Developer Console):");
58
+ console.log(` ${redirectUri}\n`);
59
+ console.log("Authorization URL:");
60
+ console.log(` ${authUrl}\n`);
61
+ console.log("Required permissions (configure in Feishu Developer Console):");
62
+ console.log(" - calendar:calendar (查看和管理用户的日历)");
63
+ console.log(" - calendar:event (管理用户日历下的日程)");
64
+ console.log("\nInstructions:");
65
+ console.log(" 1. Make sure the redirect URI is configured in your Feishu app settings (安全设置)");
66
+ console.log(" 2. Make sure the required permissions are enabled in your app (权限管理)");
67
+ console.log(" 3. Click the URL above or paste it in your browser");
68
+ console.log(" 4. Authorize the application");
69
+ console.log(" 5. You will be redirected to localhost and the token will be captured\n");
70
+
71
+ // Store auth info for verification
72
+ let authCode: string | null = null;
73
+ let receivedState: string | null = null;
74
+ let authResult: { success: boolean; message: string } | null = null;
75
+
76
+ // Create local server to receive callback
77
+ const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
78
+ if (req.url?.startsWith("/callback")) {
79
+ const url = new URL(req.url, `http://localhost:${port}`);
80
+ const code = url.searchParams.get("code");
81
+ receivedState = url.searchParams.get("state");
82
+ const error = url.searchParams.get("error");
83
+
84
+ if (error) {
85
+ authResult = { success: false, message: `Authorization error: ${error}` };
86
+ res.writeHead(200, { "Content-Type": "text/html" });
87
+ res.end(`
88
+ <html>
89
+ <head><title>Authorization Failed</title></head>
90
+ <body>
91
+ <h1>Authorization Failed</h1>
92
+ <p>Error: ${error}</p>
93
+ <p>You can close this window.</p>
94
+ </body>
95
+ </html>
96
+ `);
97
+ server.close();
98
+ return;
99
+ }
100
+
101
+ if (!code) {
102
+ authResult = { success: false, message: "No authorization code received" };
103
+ res.writeHead(200, { "Content-Type": "text/html" });
104
+ res.end(`
105
+ <html>
106
+ <head><title>Authorization Failed</title></head>
107
+ <body>
108
+ <h1>Authorization Failed</h1>
109
+ <p>No authorization code received.</p>
110
+ <p>You can close this window.</p>
111
+ </body>
112
+ </html>
113
+ `);
114
+ server.close();
115
+ return;
116
+ }
117
+
118
+ authCode = code;
119
+
120
+ // Verify state
121
+ if (receivedState !== state) {
122
+ authResult = { success: false, message: "State mismatch - possible CSRF attack" };
123
+ res.writeHead(200, { "Content-Type": "text/html" });
124
+ res.end(`
125
+ <html>
126
+ <head><title>Authorization Failed</title></head>
127
+ <body>
128
+ <h1>Authorization Failed</h1>
129
+ <p>State verification failed. Please try again.</p>
130
+ <p>You can close this window.</p>
131
+ </body>
132
+ </html>
133
+ `);
134
+ server.close();
135
+ return;
136
+ }
137
+
138
+ // Show success page
139
+ res.writeHead(200, { "Content-Type": "text/html" });
140
+ res.end(`
141
+ <html>
142
+ <head><title>Authorization Successful</title></head>
143
+ <body>
144
+ <h1>Authorization Successful!</h1>
145
+ <p>You can close this window and return to the terminal.</p>
146
+ <script>setTimeout(() => window.close(), 5000);</script>
147
+ </body>
148
+ </html>
149
+ `);
150
+ } else {
151
+ res.writeHead(404);
152
+ res.end("Not found");
153
+ }
154
+ });
155
+
156
+ server.listen(port, () => {
157
+ console.log(`Step 2: Local server started on http://localhost:${port}`);
158
+ console.log(`Step 3: Opening authorization URL in your browser...\n`);
159
+
160
+ // Try to open the browser
161
+ openBrowser(authUrl);
162
+ });
163
+
164
+ // Wait for callback with timeout
165
+ const timeout = 5 * 60 * 1000; // 5 minutes
166
+ const startTime = Date.now();
167
+
168
+ const checkInterval = setInterval(async () => {
169
+ if (authCode) {
170
+ clearInterval(checkInterval);
171
+ clearTimeout(timeoutId);
172
+
173
+ console.log("\nStep 4: Authorization code received, exchanging for access token...\n");
174
+
175
+ try {
176
+ const tokenData = await exchangeCodeForToken(appId, appSecret, authCode, redirectUri);
177
+
178
+ if (tokenData.code === 0 && tokenData.data) {
179
+ console.log("========================================");
180
+ console.log(" SUCCESS! User Access Token Received");
181
+ console.log("========================================\n");
182
+ console.log(` User ID: ${tokenData.data.user_id}`);
183
+ console.log(` Name: ${tokenData.data.name}`);
184
+ console.log(` Union ID: ${tokenData.data.union_id}`);
185
+ console.log(` Access Token: ${tokenData.data.access_token}`);
186
+ console.log(` Expires In: ${tokenData.data.expires_in} seconds`);
187
+ console.log(` Refresh Token: ${tokenData.data.refresh_token}`);
188
+ console.log("\n========================================\n");
189
+
190
+ // Save to global config
191
+ try {
192
+ await saveGlobalConfig({
193
+ userAccessToken: tokenData.data.access_token,
194
+ refreshToken: tokenData.data.refresh_token,
195
+ });
196
+ console.log("Tokens saved to ~/.feishu-agent/config.json\n");
197
+ console.log("You can now use calendar and other commands that require user authorization.");
198
+ } catch (saveError) {
199
+ console.error("Warning: Failed to save tokens to config file:");
200
+ console.error(saveError);
201
+ console.log("\nPlease save the tokens manually:");
202
+ console.log(` export FEISHU_USER_ACCESS_TOKEN="${tokenData.data.access_token}"`);
203
+ console.log(` export FEISHU_REFRESH_TOKEN="${tokenData.data.refresh_token}"`);
204
+ }
205
+ console.log("\nNote: Token will expire in", Math.floor(tokenData.data.expires_in / 60), "minutes.");
206
+ console.log("Token refresh is automatic. Just run 'feishu-agent auth' again when it expires.\n");
207
+ } else {
208
+ console.error("Failed to exchange code for token:");
209
+ console.error(tokenData.msg || "Unknown error");
210
+ }
211
+ } catch (error) {
212
+ console.error("Error exchanging token:", error);
213
+ } finally {
214
+ server.close();
215
+ process.exit(authResult?.success !== false ? 0 : 1);
216
+ }
217
+ }
218
+
219
+ // Check timeout
220
+ if (Date.now() - startTime > timeout) {
221
+ clearInterval(checkInterval);
222
+ console.error("\nAuthorization timed out after 5 minutes.");
223
+ console.error("Please run the command again and complete authorization.");
224
+ server.close();
225
+ process.exit(1);
226
+ }
227
+ }, 500);
228
+
229
+ const timeoutId = setTimeout(() => {
230
+ clearInterval(checkInterval);
231
+ console.error("\nAuthorization timed out.");
232
+ server.close();
233
+ process.exit(1);
234
+ }, timeout);
235
+ }
236
+
237
+ function generateRandomString(length: number): string {
238
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
239
+ let result = "";
240
+ for (let i = 0; i < length; i++) {
241
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
242
+ }
243
+ return result;
244
+ }
245
+
246
+ function openBrowser(url: string): void {
247
+ const platform = process.platform;
248
+ let cmd: string;
249
+
250
+ switch (platform) {
251
+ case "win32":
252
+ cmd = `start ${url}`;
253
+ break;
254
+ case "darwin":
255
+ cmd = `open "${url}"`;
256
+ break;
257
+ default:
258
+ cmd = `xdg-open "${url}"`;
259
+ break;
260
+ }
261
+
262
+ import("node:child_process").then(({ exec }) => {
263
+ exec(cmd, (error) => {
264
+ if (error) {
265
+ console.log("Could not auto-open browser. Please manually visit the URL:");
266
+ console.log(url);
267
+ }
268
+ });
269
+ });
270
+ }
271
+
272
+ async function exchangeCodeForToken(
273
+ appId: string,
274
+ appSecret: string,
275
+ code: string,
276
+ redirectUri: string
277
+ ): Promise<UserAccessTokenResponse> {
278
+ // 使用飞书身份认证 v1 API
279
+ // https://open.feishu.cn/document/ukTMukTMukTM/ukDMz4SMxQjL0MjN
280
+ const response = await fetch("https://open.feishu.cn/open-apis/authen/v1/access_token", {
281
+ method: "POST",
282
+ headers: { "Content-Type": "application/json" },
283
+ body: JSON.stringify({
284
+ grant_type: "authorization_code",
285
+ code: code,
286
+ app_id: appId,
287
+ app_secret: appSecret,
288
+ }),
289
+ });
290
+
291
+ const text = await response.text();
292
+ console.log("Token exchange response status:", response.status);
293
+
294
+ // 处理空响应
295
+ if (!text || text.trim() === "") {
296
+ throw new Error("Empty response from Feishu API");
297
+ }
298
+
299
+ try {
300
+ const data = JSON.parse(text);
301
+ if (data.code !== 0) {
302
+ throw new Error(`Feishu API error: ${data.msg || data.message || JSON.stringify(data)}`);
303
+ }
304
+ return data;
305
+ } catch (parseError) {
306
+ console.log("Raw response:", text.substring(0, 500));
307
+ throw new Error(`Failed to parse JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
308
+ }
309
+ }