@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,305 @@
1
+ import { Command } from "commander";
2
+ import { FeishuClient } from "../../core/client";
3
+ import { CalendarManager } from "../../core/calendar";
4
+ import { ContactManager } from "../../core/contact";
5
+ import { loadContactCache } from "../../core/config";
6
+ import { FeishuConfig } from "../../types";
7
+
8
+ interface CalendarOptions {
9
+ calendarId?: string;
10
+ summary?: string;
11
+ start?: string;
12
+ end?: string;
13
+ attendee?: string[];
14
+ attendeeName?: string[];
15
+ eventId?: string;
16
+ }
17
+
18
+ export function createCalendarCommands(program: Command, config: FeishuConfig) {
19
+ program
20
+ .command("list")
21
+ .description("List all calendars")
22
+ .action(async () => {
23
+ await handleListCalendars(config);
24
+ });
25
+
26
+ program
27
+ .command("events")
28
+ .description("List events in a calendar")
29
+ .option("--calendar-id <string>", "Specify calendar ID")
30
+ .action(async (options: CalendarOptions) => {
31
+ await handleListEvents(config, options.calendarId);
32
+ });
33
+
34
+ program
35
+ .command("create")
36
+ .description("Create a new event")
37
+ .requiredOption("--summary <string>", "Event title")
38
+ .requiredOption("--start <string>", "Event start time")
39
+ .requiredOption("--end <string>", "Event end time")
40
+ .option("--attendee <ids...>", "User IDs (union_id) to invite")
41
+ .option("--attendee-name <names...>", "Contact names to invite")
42
+ .option("--calendar-id <string>", "Specify calendar ID")
43
+ .action(async (options: CalendarOptions) => {
44
+ await handleCreateEvent(config, options);
45
+ });
46
+
47
+ program
48
+ .command("delete")
49
+ .description("Delete an event")
50
+ .requiredOption("--event-id <string>", "Event ID")
51
+ .option("--calendar-id <string>", "Specify calendar ID")
52
+ .action(async (options: CalendarOptions) => {
53
+ await handleDeleteEvent(config, options);
54
+ });
55
+ }
56
+
57
+ async function handleListCalendars(config: FeishuConfig) {
58
+ if (!config.appId || !config.appSecret) {
59
+ console.error("Error: FEISHU_APP_ID and FEISHU_APP_SECRET must be set.");
60
+ process.exit(1);
61
+ }
62
+ if (!config.userAccessToken) {
63
+ console.error("Error: User authorization required.");
64
+ process.exit(1);
65
+ }
66
+
67
+ const client = new FeishuClient(config);
68
+ const calendarManager = new CalendarManager(client);
69
+
70
+ console.log("\n📅 Your Calendars\n");
71
+ console.log("=".repeat(60));
72
+
73
+ const calendars = await calendarManager.listCalendars();
74
+
75
+ if (!calendars.calendar_list || calendars.calendar_list.length === 0) {
76
+ console.log("No calendars found.");
77
+ return;
78
+ }
79
+
80
+ const primary = calendars.calendar_list.filter(c => c.type === "primary");
81
+ const subscribed = calendars.calendar_list.filter(c => c.type === "shared" || c.type === "exchange");
82
+ const other = calendars.calendar_list.filter(c => c.type !== "primary" && c.type !== "shared" && c.type !== "exchange");
83
+
84
+ if (primary.length > 0) {
85
+ console.log("\n【Primary】");
86
+ primary.forEach(c => {
87
+ console.log(` • ${c.summary}`);
88
+ console.log(` ID: ${c.calendar_id}`);
89
+ console.log(` Role: ${c.role}`);
90
+ });
91
+ }
92
+
93
+ if (subscribed.length > 0) {
94
+ console.log("\n【Subscribed】");
95
+ subscribed.forEach(c => {
96
+ console.log(` • ${c.summary}`);
97
+ console.log(` ID: ${c.calendar_id}`);
98
+ console.log(` Role: ${c.role}`);
99
+ });
100
+ }
101
+
102
+ if (other.length > 0) {
103
+ console.log("\n【Other】");
104
+ other.forEach(c => {
105
+ console.log(` • ${c.summary} (${c.type})`);
106
+ console.log(` ID: ${c.calendar_id}`);
107
+ console.log(` Role: ${c.role}`);
108
+ });
109
+ }
110
+
111
+ console.log("\n" + "=".repeat(60));
112
+ console.log(`Total: ${calendars.calendar_list.length} calendar(s)\n`);
113
+
114
+ if (primary.length > 0) {
115
+ console.log(`Tip: Use --calendar-id "${primary[0].calendar_id}" to list events.`);
116
+ }
117
+ }
118
+
119
+ async function handleListEvents(config: FeishuConfig, calendarId?: string) {
120
+ if (!config.appId || !config.appSecret || !config.userAccessToken) {
121
+ console.error("Error: Authorization required. Run 'feishu-agent auth'.");
122
+ process.exit(1);
123
+ }
124
+
125
+ const client = new FeishuClient(config);
126
+ const calendarManager = new CalendarManager(client);
127
+
128
+ if (!calendarId) {
129
+ const calendars = await calendarManager.listCalendars();
130
+ const primary = calendars.calendar_list?.find(c => c.type === "primary");
131
+ if (primary) {
132
+ calendarId = primary.calendar_id;
133
+ console.log(`Using primary calendar: ${primary.summary}\n`);
134
+ }
135
+ }
136
+
137
+ if (!calendarId) {
138
+ console.error("Error: No calendar available.");
139
+ process.exit(1);
140
+ }
141
+
142
+ console.log(`\n📅 Events\n`);
143
+ console.log("=".repeat(60));
144
+
145
+ const events = await calendarManager.listEvents(calendarId);
146
+ if (!events.items || events.items.length === 0) {
147
+ console.log("No events found.");
148
+ return;
149
+ }
150
+
151
+ const activeEvents = events.items.filter(e => e.status !== "cancelled");
152
+ if (activeEvents.length === 0) {
153
+ console.log("No active events found.");
154
+ return;
155
+ }
156
+
157
+ const contactCache = await loadContactCache();
158
+
159
+ for (const e of activeEvents) {
160
+ const start = e.start_time.timestamp
161
+ ? new Date(parseInt(e.start_time.timestamp) * 1000).toLocaleString()
162
+ : e.start_time.date;
163
+ const end = e.end_time.timestamp
164
+ ? new Date(parseInt(e.end_time.timestamp) * 1000).toLocaleString()
165
+ : e.end_time.date;
166
+
167
+ console.log(`\n📅 ${e.summary || "(No title)"}`);
168
+ console.log(` 🕐 ${start} - ${end}`);
169
+ console.log(` ID: ${e.event_id}`);
170
+ if (e.status && e.status !== "confirmed") {
171
+ console.log(` Status: ${e.status}`);
172
+ }
173
+
174
+ try {
175
+ const attendees = await calendarManager.getEventAttendees(calendarId, e.event_id);
176
+ if (attendees && attendees.length > 0) {
177
+ const attendeeDisplay = attendees.map(a => {
178
+ if (a.type === "user" && a.user_id) {
179
+ // Lookup attendee name from cache by open_id or union_id
180
+ for (const [unionId, entry] of Object.entries(contactCache)) {
181
+ if (entry.open_id === a.user_id || unionId === a.user_id) {
182
+ return `${entry.name}${a.is_optional ? " (optional)" : ""}`;
183
+ }
184
+ }
185
+ return `${a.user_id}${a.is_optional ? " (optional)" : ""}`;
186
+ }
187
+ if (a.type === "chat") return `Chat: ${a.chat_id}`;
188
+ if (a.type === "third_party" && a.third_party_email) {
189
+ return `${a.third_party_email} (external)`;
190
+ }
191
+ return a.type;
192
+ });
193
+ console.log(` 👥 Attendees: ${attendeeDisplay.join(", ")}`);
194
+ }
195
+ } catch (err) {
196
+ // Ignore errors
197
+ }
198
+ }
199
+
200
+ console.log("\n" + "=".repeat(60));
201
+ console.log(`Total: ${activeEvents.length} event(s)\n`);
202
+ }
203
+
204
+ async function handleCreateEvent(config: FeishuConfig, options: CalendarOptions) {
205
+ if (!config.appId || !config.appSecret || !config.userAccessToken) {
206
+ console.error("Error: Authorization required.");
207
+ process.exit(1);
208
+ }
209
+
210
+ const client = new FeishuClient(config);
211
+ const calendarManager = new CalendarManager(client);
212
+ const contactManager = new ContactManager(client);
213
+
214
+ const { summary, start, end, attendee, attendeeName, calendarId } = options;
215
+
216
+ if (!summary || !start || !end) {
217
+ console.error("Error: --summary, --start, and --end are required.");
218
+ process.exit(1);
219
+ }
220
+
221
+ // Resolve attendee names to union_ids
222
+ let attendeeUserIds: string[] = attendee || [];
223
+ if (attendeeName && attendeeName.length > 0) {
224
+ console.log("\n🔍 Resolving attendee names...");
225
+ for (const name of attendeeName) {
226
+ const results = await contactManager.searchUser(name);
227
+ if (results.length === 0) {
228
+ console.error(`Error: No contact found for "${name}".`);
229
+ process.exit(1);
230
+ }
231
+ if (results.length > 1) {
232
+ console.log(` Multiple matches for "${name}":`);
233
+ results.forEach((r, i) => {
234
+ console.log(` ${i + 1}. ${r.name} (${r.email || r.union_id})`);
235
+ });
236
+ console.log(" Using the first match.");
237
+ }
238
+ attendeeUserIds.push(results[0].union_id);
239
+ console.log(` ✓ "${name}" -> ${results[0].name} (${results[0].union_id})`);
240
+ }
241
+ }
242
+
243
+ // Get calendar
244
+ let targetCalendarId = calendarId;
245
+ if (!targetCalendarId) {
246
+ const calendars = await calendarManager.listCalendars();
247
+ const primary = calendars.calendar_list?.find(c => c.type === "primary");
248
+ if (primary) targetCalendarId = primary.calendar_id;
249
+ }
250
+
251
+ if (!targetCalendarId) {
252
+ console.error("Error: No calendar available.");
253
+ process.exit(1);
254
+ }
255
+
256
+ const startTimestamp = Math.floor(new Date(start).getTime() / 1000).toString();
257
+ const endTimestamp = Math.floor(new Date(end).getTime() / 1000).toString();
258
+
259
+ const event = await calendarManager.createEvent(targetCalendarId, {
260
+ summary,
261
+ startTime: { timestamp: startTimestamp },
262
+ endTime: { timestamp: endTimestamp },
263
+ attendeeUserIds: attendeeUserIds.length > 0 ? attendeeUserIds : undefined,
264
+ });
265
+
266
+ console.log("\n✅ Event created!");
267
+ console.log(` Title: ${summary}`);
268
+ console.log(` Time: ${new Date(parseInt(startTimestamp) * 1000).toLocaleString()} - ${new Date(parseInt(endTimestamp) * 1000).toLocaleString()}`);
269
+ console.log(` Calendar ID: ${targetCalendarId}`);
270
+ if (attendeeUserIds.length > 0) {
271
+ console.log(` Attendees: ${attendeeUserIds.join(", ")}`);
272
+ }
273
+ }
274
+
275
+ async function handleDeleteEvent(config: FeishuConfig, options: CalendarOptions) {
276
+ if (!config.appId || !config.appSecret || !config.userAccessToken) {
277
+ console.error("Error: Authorization required.");
278
+ process.exit(1);
279
+ }
280
+
281
+ const client = new FeishuClient(config);
282
+ const calendarManager = new CalendarManager(client);
283
+
284
+ const { eventId, calendarId } = options;
285
+ let targetCalendarId = calendarId;
286
+
287
+ if (!targetCalendarId) {
288
+ const calendars = await calendarManager.listCalendars();
289
+ const primary = calendars.calendar_list?.find(c => c.type === "primary");
290
+ if (primary) targetCalendarId = primary.calendar_id;
291
+ }
292
+
293
+ if (!targetCalendarId) {
294
+ console.error("Error: No calendar available.");
295
+ process.exit(1);
296
+ }
297
+
298
+ if (!eventId) {
299
+ console.error("Error: --event-id is required.");
300
+ process.exit(1);
301
+ }
302
+
303
+ await calendarManager.deleteEvent(targetCalendarId, eventId);
304
+ console.log(`\n✅ Event deleted: ${eventId}\n`);
305
+ }
@@ -0,0 +1,48 @@
1
+ import { Command } from "commander";
2
+ import { saveGlobalConfig, getConfigPath } from "../../core/config";
3
+ import { FeishuConfig } from "../../types";
4
+
5
+ export function createConfigCommands(program: Command) {
6
+ program
7
+ .command("set")
8
+ .description("Set a config value")
9
+ .argument("<key>", "Config key (appId or appSecret)")
10
+ .argument("<value>", "Config value")
11
+ .action(async (key: string, value: string) => {
12
+ if (key !== "appId" && key !== "appSecret") {
13
+ console.error("Error: Key must be 'appId' or 'appSecret'.");
14
+ process.exit(1);
15
+ }
16
+
17
+ await saveGlobalConfig({ [key]: value });
18
+ console.log(`Updated global config: ${key} = ${value}`);
19
+ });
20
+
21
+ program
22
+ .command("get")
23
+ .description("Get a config value")
24
+ .argument("<key>", "Config key")
25
+ .action(async (key: string) => {
26
+ const file = Bun.file(getConfigPath());
27
+ if (await file.exists()) {
28
+ const config = await file.json();
29
+ console.log(config[key] || "(not set)");
30
+ } else {
31
+ console.log("(not set)");
32
+ }
33
+ });
34
+
35
+ program
36
+ .command("list")
37
+ .description("List all config values")
38
+ .action(async () => {
39
+ const f = Bun.file(getConfigPath());
40
+ if (await f.exists()) {
41
+ const c = await f.json();
42
+ console.log("Global Configuration:");
43
+ console.log(JSON.stringify(c, null, 2));
44
+ } else {
45
+ console.log("Global Configuration: (empty)");
46
+ }
47
+ });
48
+ }
@@ -0,0 +1,90 @@
1
+ import { Command } from "commander";
2
+ import { FeishuClient } from "../../core/client";
3
+ import { ContactManager } from "../../core/contact";
4
+ import { FeishuConfig } from "../../types";
5
+
6
+ interface ContactOptions {
7
+ dept?: string;
8
+ }
9
+
10
+ export function createContactCommands(program: Command, config: FeishuConfig) {
11
+ program
12
+ .command("list")
13
+ .description("List users in a department")
14
+ .option("--dept <string>", "Department ID", "0")
15
+ .action(async (options: ContactOptions) => {
16
+ await handleList(config, options.dept);
17
+ });
18
+
19
+ program
20
+ .command("search")
21
+ .description("Search users by name or email")
22
+ .argument("<query>", "Search query")
23
+ .action(async (query: string) => {
24
+ await handleSearch(config, query);
25
+ });
26
+ }
27
+
28
+ async function handleList(config: FeishuConfig, dept: string = "0") {
29
+ if (!config.appId || !config.appSecret) {
30
+ console.error("Error: Credentials required.");
31
+ process.exit(1);
32
+ }
33
+
34
+ const client = new FeishuClient(config);
35
+ const contactManager = new ContactManager(client);
36
+
37
+ console.log(`Fetching users for department: ${dept}`);
38
+ const users = await contactManager.listUsers(dept);
39
+
40
+ if (users.length > 0) {
41
+ users.forEach((u) => {
42
+ console.log(`- Name: ${u.name}${u.email ? ` (${u.email})` : ''}`);
43
+ if (u.user_id && u.union_id && u.user_id !== u.union_id) {
44
+ console.log(` ID: ${u.user_id} / UnionID: ${u.union_id}`);
45
+ } else if (u.user_id) {
46
+ console.log(` ID: ${u.user_id}`);
47
+ } else if (u.union_id) {
48
+ console.log(` UnionID: ${u.union_id}`);
49
+ }
50
+ console.log("");
51
+ });
52
+ console.log(`Total: ${users.length} users`);
53
+ } else {
54
+ console.log("No users found.");
55
+ }
56
+ }
57
+
58
+ async function handleSearch(config: FeishuConfig, query: string) {
59
+ if (!config.appId || !config.appSecret) {
60
+ console.error("Error: Credentials required.");
61
+ process.exit(1);
62
+ }
63
+
64
+ if (!query) {
65
+ console.error("Error: search query is required.");
66
+ process.exit(1);
67
+ }
68
+
69
+ const client = new FeishuClient(config);
70
+ const contactManager = new ContactManager(client);
71
+
72
+ console.log(`Searching for users matching: '${query}'`);
73
+ const users = await contactManager.searchUser(query);
74
+
75
+ if (users.length > 0) {
76
+ users.forEach((u) => {
77
+ console.log(`- Name: ${u.name}${u.email ? ` (${u.email})` : ''}`);
78
+ if (u.user_id && u.union_id && u.user_id !== u.union_id) {
79
+ console.log(` ID: ${u.user_id} / UnionID: ${u.union_id}`);
80
+ } else if (u.user_id) {
81
+ console.log(` ID: ${u.user_id}`);
82
+ } else if (u.union_id) {
83
+ console.log(` UnionID: ${u.union_id}`);
84
+ }
85
+ });
86
+ console.log(`Total: ${users.length} matching users`);
87
+ } else {
88
+ console.log("No matching users found.");
89
+ }
90
+ }
@@ -0,0 +1,128 @@
1
+ import { loadConfig, FeishuConfig } from "../../core/config";
2
+ import { FeishuClient } from "../../core/client";
3
+ import { IntrospectionEngine } from "../../core/introspection";
4
+ import { writeFile, mkdir } from "fs/promises";
5
+
6
+ export async function initCommand(args: string[], cliOptions?: Partial<FeishuConfig>) {
7
+ const target = args[0];
8
+
9
+ // If no target provided, go straight to interactive mode
10
+ if (!target) {
11
+ await initInteractive(undefined, cliOptions);
12
+ return;
13
+ }
14
+
15
+ // Check if it's a Folder URL
16
+ if (target.includes("/drive/folder/")) {
17
+ console.warn("That looks like a Folder URL. Please provide a Base URL (多维表格).");
18
+ await initInteractive(undefined, cliOptions);
19
+ return;
20
+ }
21
+
22
+ const baseToken = extractBaseToken(target);
23
+ if (baseToken) {
24
+ await initInteractive(target, cliOptions);
25
+ } else {
26
+ // Treat as local path (scaffolding) - for now just log
27
+ console.log(`Initializing in local path: ${target}`);
28
+ // TODO: Implement local scaffolding logic if needed
29
+ }
30
+ }
31
+
32
+ async function initInteractive(initialTarget?: string, cliOptions?: Partial<FeishuConfig>) {
33
+ let baseToken: string | null = null;
34
+
35
+ if (initialTarget) {
36
+ baseToken = extractBaseToken(initialTarget);
37
+ }
38
+
39
+ while (!baseToken) {
40
+ const input = prompt("Please enter the Feishu Base URL (or Base Token): ");
41
+
42
+ if (!input) {
43
+ console.error("Error: Base Token is required.");
44
+ process.exit(1);
45
+ }
46
+
47
+ baseToken = extractBaseToken(input);
48
+
49
+ if (!baseToken) {
50
+ console.error("Invalid input. Could not extract Base Token.");
51
+ console.log("To find your Base URL: Open the Base (多维表格) in your browser and copy the link. It usually looks like: https://.../base/basexxxxxx");
52
+ }
53
+ }
54
+
55
+ console.log(`Detected Base Token: ${baseToken}`);
56
+ await runIntrospection(baseToken, cliOptions);
57
+ }
58
+
59
+ async function runIntrospection(baseToken: string, cliOptions?: Partial<FeishuConfig>) {
60
+ // Load configuration
61
+ const config = await loadConfig(cliOptions);
62
+
63
+ // Get App Credentials
64
+ let appId = config.appId;
65
+ if (!appId) {
66
+ const input = prompt("Enter Feishu App ID:");
67
+ if (!input) {
68
+ console.error("Error: App ID is required.");
69
+ process.exit(1);
70
+ }
71
+ appId = input;
72
+ }
73
+
74
+ let appSecret = config.appSecret;
75
+ if (!appSecret) {
76
+ const input = prompt("Enter Feishu App Secret:");
77
+ if (!input) {
78
+ console.error("Error: App Secret is required.");
79
+ process.exit(1);
80
+ }
81
+ appSecret = input;
82
+ }
83
+
84
+ console.log("Fetching schema...");
85
+
86
+ const client = new FeishuClient({
87
+ appId,
88
+ appSecret,
89
+ });
90
+
91
+ try {
92
+ const engine = new IntrospectionEngine(client);
93
+ const schema = await engine.introspect(baseToken, (msg) => console.log(msg));
94
+
95
+ await mkdir(".feishu_agent", { recursive: true });
96
+ await writeFile(".feishu_agent/schema.json", JSON.stringify(schema, null, 2));
97
+ console.log("Success! Schema saved to .feishu_agent/schema.json");
98
+
99
+ // Create .env if it doesn't exist
100
+ const envFile = Bun.file(".env");
101
+ if (!(await envFile.exists())) {
102
+ const envContent = `FEISHU_APP_ID=${appId}\nFEISHU_APP_SECRET=${appSecret}\n`;
103
+ await writeFile(".env", envContent);
104
+ console.log("Created .env file with credentials.");
105
+ } else {
106
+ console.log(".env file already exists, skipping creation.");
107
+ }
108
+
109
+ } catch (error) {
110
+ console.error("Failed to fetch schema:", error instanceof Error ? error.message : String(error));
111
+ process.exit(1);
112
+ }
113
+ }
114
+
115
+ function extractBaseToken(input: string): string | null {
116
+ // 1. Try to match URL pattern: .../base/<token>...
117
+ const urlMatch = input.match(/\/base\/([a-zA-Z0-9]+)/);
118
+ if (urlMatch) {
119
+ return urlMatch[1];
120
+ }
121
+
122
+ // 2. check if input itself looks like a token (starts with 'base' or 'app')
123
+ if (/^(base|app)[a-zA-Z0-9]+$/.test(input)) {
124
+ return input;
125
+ }
126
+
127
+ return null;
128
+ }