feishu-user-plugin 1.3.5 → 1.3.7

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.
Files changed (56) hide show
  1. package/.claude-plugin/plugin.json +2 -2
  2. package/CHANGELOG.md +22 -0
  3. package/README.md +66 -40
  4. package/package.json +10 -3
  5. package/scripts/check-tool-count.js +15 -0
  6. package/scripts/check-version.js +40 -0
  7. package/scripts/smoke.js +224 -0
  8. package/scripts/sync-claude-md.sh +12 -0
  9. package/scripts/sync-team-skills.sh +22 -0
  10. package/scripts/test-all-tools.js +158 -0
  11. package/skills/feishu-user-plugin/SKILL.md +5 -5
  12. package/skills/feishu-user-plugin/references/CLAUDE.md +152 -96
  13. package/skills/feishu-user-plugin/references/table.md +18 -9
  14. package/src/auth/credentials.js +350 -0
  15. package/src/cli.js +42 -13
  16. package/src/clients/official/base.js +424 -0
  17. package/src/clients/official/bitable.js +269 -0
  18. package/src/clients/official/calendar.js +176 -0
  19. package/src/clients/official/contacts.js +54 -0
  20. package/src/clients/official/docs.js +301 -0
  21. package/src/clients/official/drive.js +77 -0
  22. package/src/clients/official/groups.js +68 -0
  23. package/src/clients/official/im.js +414 -0
  24. package/src/clients/official/index.js +30 -0
  25. package/src/clients/official/okr.js +127 -0
  26. package/src/clients/official/tasks.js +142 -0
  27. package/src/clients/official/uploads.js +260 -0
  28. package/src/clients/official/wiki.js +207 -0
  29. package/src/{client.js → clients/user.js} +23 -17
  30. package/src/doc-blocks.js +20 -5
  31. package/src/index.js +4 -1744
  32. package/src/logger.js +20 -0
  33. package/src/oauth.js +8 -1
  34. package/src/official.js +5 -1734
  35. package/src/prompts/_registry.js +69 -0
  36. package/src/prompts/index.js +54 -0
  37. package/src/server.js +242 -0
  38. package/src/test-all.js +2 -2
  39. package/src/test-comprehensive.js +3 -3
  40. package/src/test-send.js +1 -1
  41. package/src/tools/_registry.js +30 -0
  42. package/src/tools/bitable.js +246 -0
  43. package/src/tools/calendar.js +207 -0
  44. package/src/tools/contacts.js +66 -0
  45. package/src/tools/diagnostics.js +172 -0
  46. package/src/tools/docs.js +158 -0
  47. package/src/tools/drive.js +111 -0
  48. package/src/tools/groups.js +81 -0
  49. package/src/tools/im-read.js +259 -0
  50. package/src/tools/messaging-bot.js +151 -0
  51. package/src/tools/messaging-user.js +292 -0
  52. package/src/tools/okr.js +159 -0
  53. package/src/tools/profile.js +43 -0
  54. package/src/tools/tasks.js +168 -0
  55. package/src/tools/uploads.js +63 -0
  56. package/src/tools/wiki.js +191 -0
@@ -0,0 +1,246 @@
1
+ // src/tools/bitable.js — Bitable (multi-dimensional table) operations.
2
+ //
3
+ // 5 consolidated tools (replaces 19 in v1.3.6):
4
+ // manage_bitable_app — actions: create, copy, get_meta
5
+ // manage_bitable_table — actions: list, create, update, delete
6
+ // manage_bitable_field — actions: list, create, update, delete
7
+ // manage_bitable_view — actions: list, create, delete
8
+ // manage_bitable_record — actions: search, get, create, update, delete (records arg is array; max 500)
9
+ //
10
+ // The action= discriminator routes to the existing client methods unchanged;
11
+ // this file is just a tool-surface compaction.
12
+
13
+ const { text, json } = require('./_registry');
14
+
15
+ const FIELD_TYPE_NOTE = '1=Text, 2=Number, 3=SingleSelect, 4=MultiSelect, 5=DateTime, 7=Checkbox, 11=User, 13=Phone, 15=URL, 17=Attachment, 18=Link, 20=Formula, 21=DuplexLink, 22=Location, 23=GroupChat, 1001=CreateTime, 1002=ModifiedTime, 1003=Creator, 1004=Modifier';
16
+
17
+ const schemas = [
18
+ {
19
+ name: 'manage_bitable_app',
20
+ description: '[Official API] Manage a Bitable app. action=create (new app, optional wiki_space_id to attach), copy (duplicate an existing app), get_meta (read app metadata).',
21
+ inputSchema: {
22
+ type: 'object',
23
+ properties: {
24
+ action: { type: 'string', enum: ['create', 'copy', 'get_meta'], description: 'Operation to perform' },
25
+ app_token: { type: 'string', description: 'Required for copy/get_meta. Native token, wiki node, or Feishu URL.' },
26
+ name: { type: 'string', description: 'New app name. Required for create/copy.' },
27
+ folder_id: { type: 'string', description: 'Destination folder token (optional for create/copy; ignored when wiki_space_id is set).' },
28
+ wiki_space_id: { type: 'string', description: 'Wiki space ID — create the app directly under this space (create only).' },
29
+ wiki_parent_node_token: { type: 'string', description: 'Parent wiki node within the space (optional for create).' },
30
+ },
31
+ required: ['action'],
32
+ },
33
+ },
34
+ {
35
+ name: 'manage_bitable_table',
36
+ description: '[Official API] Manage a table inside a Bitable app. action=list, create (with optional initial fields), update (rename), delete.',
37
+ inputSchema: {
38
+ type: 'object',
39
+ properties: {
40
+ action: { type: 'string', enum: ['list', 'create', 'update', 'delete'], description: 'Operation to perform' },
41
+ app_token: { type: 'string', description: 'Bitable app token (required for all actions). Accepts native token, wiki node, or Feishu URL.' },
42
+ table_id: { type: 'string', description: 'Table ID — required for update/delete.' },
43
+ name: { type: 'string', description: 'Table name — required for create, optional for update (rename).' },
44
+ fields: {
45
+ type: 'array',
46
+ description: `Initial field definitions (create only, optional). Each item: {field_name, type, property?} where type is ${FIELD_TYPE_NOTE}.`,
47
+ items: { type: 'object' },
48
+ },
49
+ },
50
+ required: ['action', 'app_token'],
51
+ },
52
+ },
53
+ {
54
+ name: 'manage_bitable_field',
55
+ description: '[Official API] Manage fields (columns) inside a Bitable table. action=list, create, update (Feishu requires `type` even when only renaming), delete.',
56
+ inputSchema: {
57
+ type: 'object',
58
+ properties: {
59
+ action: { type: 'string', enum: ['list', 'create', 'update', 'delete'], description: 'Operation to perform' },
60
+ app_token: { type: 'string', description: 'Bitable app token. Accepts native token, wiki node, or Feishu URL.' },
61
+ table_id: { type: 'string', description: 'Table ID' },
62
+ field_id: { type: 'string', description: 'Field ID — required for update/delete.' },
63
+ field_name: { type: 'string', description: 'Field display name — required for create, optional for update.' },
64
+ type: { type: 'number', description: `Field type (${FIELD_TYPE_NOTE}). Required for create AND update — Feishu API rejects update without it.` },
65
+ property: { type: 'object', description: 'Field-type-specific properties (optional). E.g. SingleSelect: {options:[{name:"A"},{name:"B"}]}.' },
66
+ },
67
+ required: ['action', 'app_token', 'table_id'],
68
+ },
69
+ },
70
+ {
71
+ name: 'manage_bitable_view',
72
+ description: '[Official API] Manage views inside a Bitable table. action=list, create, delete. (Feishu open API does not expose view update — recreate with a new name to change.)',
73
+ inputSchema: {
74
+ type: 'object',
75
+ properties: {
76
+ action: { type: 'string', enum: ['list', 'create', 'delete'], description: 'Operation to perform' },
77
+ app_token: { type: 'string', description: 'Bitable app token. Accepts native token, wiki node, or Feishu URL.' },
78
+ table_id: { type: 'string', description: 'Table ID' },
79
+ view_id: { type: 'string', description: 'View ID — required for delete.' },
80
+ view_name: { type: 'string', description: 'View name — required for create.' },
81
+ view_type: { type: 'string', description: 'View type for create: grid (default), kanban, gallery, form, gantt, calendar.', default: 'grid' },
82
+ },
83
+ required: ['action', 'app_token', 'table_id'],
84
+ },
85
+ },
86
+ {
87
+ name: 'manage_bitable_record',
88
+ description: '[Official API] Manage records (rows) inside a Bitable table. action=search, get, create, update, delete. create/update/delete accept arrays — single record or up to 500.',
89
+ inputSchema: {
90
+ type: 'object',
91
+ properties: {
92
+ action: { type: 'string', enum: ['search', 'get', 'create', 'update', 'delete'], description: 'Operation to perform' },
93
+ app_token: { type: 'string', description: 'Bitable app token. Accepts native token, wiki node, or Feishu URL.' },
94
+ table_id: { type: 'string', description: 'Table ID' },
95
+ record_id: { type: 'string', description: 'Record ID — required for action=get.' },
96
+ records: {
97
+ type: 'array',
98
+ description: 'Records to write. For create: [{fields:{field_name:value}}]. For update: [{record_id, fields:{...}}]. Single record or up to 500.',
99
+ items: { type: 'object' },
100
+ },
101
+ record_ids: {
102
+ type: 'array',
103
+ description: 'Record IDs to delete. Single ID or up to 500.',
104
+ items: { type: 'string' },
105
+ },
106
+ filter: { type: 'object', description: 'Filter conditions (search only, optional)' },
107
+ sort: { type: 'array', description: 'Sort conditions (search only, optional)' },
108
+ page_size: { type: 'number', description: 'Results per page (search only, default 20)' },
109
+ },
110
+ required: ['action', 'app_token', 'table_id'],
111
+ },
112
+ },
113
+ ];
114
+
115
+ function need(arg, name, action) {
116
+ if (arg === undefined || arg === null || arg === '') {
117
+ throw new Error(`manage_bitable: ${name} required for action=${action}`);
118
+ }
119
+ }
120
+
121
+ const handlers = {
122
+ async manage_bitable_app(args, ctx) {
123
+ const c = ctx.getOfficialClient();
124
+ switch (args.action) {
125
+ case 'create': {
126
+ need(args.name, 'name', 'create');
127
+ const r = await c.createBitable(args.name, args.folder_id, {
128
+ wikiSpaceId: args.wiki_space_id,
129
+ wikiParentNodeToken: args.wiki_parent_node_token,
130
+ });
131
+ const ownership = r.viaUser ? ' (as user)' : ' (as app — UAT unavailable or failed; bitable owned by the app, not you)';
132
+ const wikiNote = r.wikiNodeToken ? `\nWiki node: ${r.wikiNodeToken}`
133
+ : r.wikiAttachTaskId ? `\nWiki attach queued — task_id: ${r.wikiAttachTaskId}`
134
+ : r.wikiAttachError ? `\nWARNING: wiki attach failed — ${r.wikiAttachError}. Bitable exists in drive root/folder.`
135
+ : '';
136
+ const warn = r.fallbackWarning ? `\n\n${r.fallbackWarning}` : '';
137
+ return text(`Bitable created${ownership}: ${r.appToken}\nURL: ${r.url || ''}${wikiNote}${warn}`);
138
+ }
139
+ case 'copy': {
140
+ need(args.app_token, 'app_token', 'copy');
141
+ need(args.name, 'name', 'copy');
142
+ return json(await c.copyBitable(await ctx.resolveDocId(args.app_token), args.name, args.folder_id));
143
+ }
144
+ case 'get_meta': {
145
+ need(args.app_token, 'app_token', 'get_meta');
146
+ return json(await c.getBitableMeta(await ctx.resolveDocId(args.app_token)));
147
+ }
148
+ }
149
+ },
150
+ async manage_bitable_table(args, ctx) {
151
+ const c = ctx.getOfficialClient();
152
+ const appToken = await ctx.resolveDocId(args.app_token);
153
+ switch (args.action) {
154
+ case 'list':
155
+ return json(await c.listBitableTables(appToken));
156
+ case 'create': {
157
+ need(args.name, 'name', 'create');
158
+ const r = await c.createBitableTable(appToken, args.name, args.fields);
159
+ const warn = r.fallbackWarning ? `\n\n${r.fallbackWarning}` : '';
160
+ return text(`Table created: ${r.tableId}${warn}`);
161
+ }
162
+ case 'update': {
163
+ need(args.table_id, 'table_id', 'update');
164
+ need(args.name, 'name', 'update');
165
+ return text(`Table renamed: ${(await c.updateBitableTable(appToken, args.table_id, args.name)).name}`);
166
+ }
167
+ case 'delete': {
168
+ need(args.table_id, 'table_id', 'delete');
169
+ return text(`Table deleted: ${(await c.deleteBitableTable(appToken, args.table_id)).deleted}`);
170
+ }
171
+ }
172
+ },
173
+ async manage_bitable_field(args, ctx) {
174
+ const c = ctx.getOfficialClient();
175
+ const appToken = await ctx.resolveDocId(args.app_token);
176
+ switch (args.action) {
177
+ case 'list':
178
+ return json(await c.listBitableFields(appToken, args.table_id));
179
+ case 'create': {
180
+ need(args.field_name, 'field_name', 'create');
181
+ need(args.type, 'type', 'create');
182
+ const config = { field_name: args.field_name, type: args.type };
183
+ if (args.property) config.property = args.property;
184
+ return json(await c.createBitableField(appToken, args.table_id, config));
185
+ }
186
+ case 'update': {
187
+ need(args.field_id, 'field_id', 'update');
188
+ need(args.type, 'type', 'update');
189
+ const config = {};
190
+ if (args.field_name) config.field_name = args.field_name;
191
+ if (args.type) config.type = args.type;
192
+ if (args.property) config.property = args.property;
193
+ return json(await c.updateBitableField(appToken, args.table_id, args.field_id, config));
194
+ }
195
+ case 'delete': {
196
+ need(args.field_id, 'field_id', 'delete');
197
+ const r = await c.deleteBitableField(appToken, args.table_id, args.field_id);
198
+ return text(r.deleted ? `Field ${r.fieldId} deleted` : `Field deletion returned deleted=${r.deleted}`);
199
+ }
200
+ }
201
+ },
202
+ async manage_bitable_view(args, ctx) {
203
+ const c = ctx.getOfficialClient();
204
+ const appToken = await ctx.resolveDocId(args.app_token);
205
+ switch (args.action) {
206
+ case 'list':
207
+ return json(await c.listBitableViews(appToken, args.table_id));
208
+ case 'create': {
209
+ need(args.view_name, 'view_name', 'create');
210
+ return json(await c.createBitableView(appToken, args.table_id, args.view_name, args.view_type));
211
+ }
212
+ case 'delete': {
213
+ need(args.view_id, 'view_id', 'delete');
214
+ return text(`View deleted: ${(await c.deleteBitableView(appToken, args.table_id, args.view_id)).deleted}`);
215
+ }
216
+ }
217
+ },
218
+ async manage_bitable_record(args, ctx) {
219
+ const c = ctx.getOfficialClient();
220
+ const appToken = await ctx.resolveDocId(args.app_token);
221
+ switch (args.action) {
222
+ case 'search':
223
+ return json(await c.searchBitableRecords(appToken, args.table_id, {
224
+ filter: args.filter, sort: args.sort, pageSize: args.page_size,
225
+ }));
226
+ case 'get': {
227
+ need(args.record_id, 'record_id', 'get');
228
+ return json(await c.getBitableRecord(appToken, args.table_id, args.record_id));
229
+ }
230
+ case 'create': {
231
+ need(args.records, 'records', 'create');
232
+ return json(await c.batchCreateBitableRecords(appToken, args.table_id, args.records));
233
+ }
234
+ case 'update': {
235
+ need(args.records, 'records', 'update');
236
+ return json(await c.batchUpdateBitableRecords(appToken, args.table_id, args.records));
237
+ }
238
+ case 'delete': {
239
+ need(args.record_ids, 'record_ids', 'delete');
240
+ return json(await c.batchDeleteBitableRecords(appToken, args.table_id, args.record_ids));
241
+ }
242
+ }
243
+ },
244
+ };
245
+
246
+ module.exports = { schemas, handlers };
@@ -0,0 +1,207 @@
1
+ // src/tools/calendar.js — Calendar read + write tools.
2
+ //
3
+ // 8 tools (was 3 in v1.3.6): list_calendars, list_calendar_events,
4
+ // get_calendar_event, plus the v1.3.7 write tools:
5
+ // create_calendar_event / update_calendar_event / delete_calendar_event
6
+ // respond_calendar_event / get_freebusy
7
+ // All UAT-first. Write tools require `calendar:calendar.event:write` scope.
8
+
9
+ const { json, text } = require('./_registry');
10
+
11
+ const TIME_NOTE = 'A time object: {timestamp:"<unix-seconds>", timezone?:"Asia/Shanghai"} OR {date:"YYYY-MM-DD"} for all-day events.';
12
+
13
+ const schemas = [
14
+ {
15
+ name: 'list_calendars',
16
+ description: '[Official API + UAT] List the current user\'s calendars (primary + shared + subscribed). Requires UAT — app identity only sees calendars it was explicitly invited to. Requires `calendar:calendar:readonly` scope on the OAuth.',
17
+ inputSchema: {
18
+ type: 'object',
19
+ properties: {
20
+ page_size: { type: 'number', description: 'Items per page (min 50, default 50). Feishu\'s calendar endpoint rejects page_size < 50.' },
21
+ page_token: { type: 'string', description: 'Pagination token' },
22
+ sync_token: { type: 'string', description: 'Incremental sync token (optional)' },
23
+ },
24
+ },
25
+ },
26
+ {
27
+ name: 'list_calendar_events',
28
+ description: '[Official API + UAT] List events in a calendar within an optional time range. Typical usage: first list_calendars to find calendar_id (primary calendar has type="primary"), then list events in e.g. [now, now+7d] (Unix seconds).',
29
+ inputSchema: {
30
+ type: 'object',
31
+ properties: {
32
+ calendar_id: { type: 'string', description: 'Calendar ID from list_calendars' },
33
+ start_time: { type: 'string', description: 'Range start (Unix seconds, optional)' },
34
+ end_time: { type: 'string', description: 'Range end (Unix seconds, optional)' },
35
+ page_size: { type: 'number', description: 'Items per page (default 50)' },
36
+ page_token: { type: 'string', description: 'Pagination token' },
37
+ sync_token: { type: 'string', description: 'Incremental sync token (optional)' },
38
+ },
39
+ required: ['calendar_id'],
40
+ },
41
+ },
42
+ {
43
+ name: 'get_calendar_event',
44
+ description: '[Official API + UAT] Get full details of a single calendar event (summary, description, start/end, attendees, location, attachments, meeting link).',
45
+ inputSchema: {
46
+ type: 'object',
47
+ properties: {
48
+ calendar_id: { type: 'string', description: 'Calendar ID' },
49
+ event_id: { type: 'string', description: 'Event ID from list_calendar_events' },
50
+ },
51
+ required: ['calendar_id', 'event_id'],
52
+ },
53
+ },
54
+ {
55
+ name: 'create_calendar_event',
56
+ description: `[Official API + UAT, v1.3.7] Create a new calendar event. Requires \`calendar:calendar.event:write\` scope (re-run \`npx feishu-user-plugin oauth\` after enabling). The current identity (UAT-first) must have writer or owner permission on the calendar.\n\nTime fields: ${TIME_NOTE}`,
57
+ inputSchema: {
58
+ type: 'object',
59
+ properties: {
60
+ calendar_id: { type: 'string', description: 'Calendar ID (use list_calendars; primary calendar has type="primary").' },
61
+ summary: { type: 'string', description: 'Event title' },
62
+ description: { type: 'string', description: 'Description / notes (optional)' },
63
+ start_time: { type: 'object', description: TIME_NOTE },
64
+ end_time: { type: 'object', description: TIME_NOTE },
65
+ location: { type: 'object', description: 'Optional. {name, address?, latitude?, longitude?}.' },
66
+ visibility: { type: 'string', enum: ['default', 'public', 'private'], description: 'Event visibility (optional)' },
67
+ attendee_ability: { type: 'string', enum: ['none', 'can_see_others', 'can_invite_others', 'can_modify_event'], description: 'What attendees may do (optional)' },
68
+ free_busy_status: { type: 'string', enum: ['busy', 'free'], description: 'Whether this event blocks the calendar (optional)' },
69
+ reminders: { type: 'array', description: 'Reminders before event start (optional). E.g. [{minutes:15}].', items: { type: 'object' } },
70
+ recurrence: { type: 'string', description: 'iCal RRULE recurrence string (optional)' },
71
+ need_notification: { type: 'boolean', description: 'Whether to notify attendees on create (default true)' },
72
+ },
73
+ required: ['calendar_id', 'summary', 'start_time', 'end_time'],
74
+ },
75
+ },
76
+ {
77
+ name: 'update_calendar_event',
78
+ description: '[Official API + UAT, v1.3.7] Patch fields on an existing calendar event. Pass only the fields you want to change. Requires `calendar:calendar.event:write` scope.',
79
+ inputSchema: {
80
+ type: 'object',
81
+ properties: {
82
+ calendar_id: { type: 'string', description: 'Calendar ID' },
83
+ event_id: { type: 'string', description: 'Event ID' },
84
+ summary: { type: 'string', description: 'New title (optional)' },
85
+ description: { type: 'string', description: 'New description (optional)' },
86
+ start_time: { type: 'object', description: TIME_NOTE },
87
+ end_time: { type: 'object', description: TIME_NOTE },
88
+ location: { type: 'object', description: 'New location object (optional)' },
89
+ visibility: { type: 'string', enum: ['default', 'public', 'private'] },
90
+ attendee_ability: { type: 'string', enum: ['none', 'can_see_others', 'can_invite_others', 'can_modify_event'] },
91
+ free_busy_status: { type: 'string', enum: ['busy', 'free'] },
92
+ reminders: { type: 'array', items: { type: 'object' } },
93
+ recurrence: { type: 'string', description: 'RRULE string' },
94
+ need_notification: { type: 'boolean', description: 'Whether to notify attendees of the update' },
95
+ },
96
+ required: ['calendar_id', 'event_id'],
97
+ },
98
+ },
99
+ {
100
+ name: 'delete_calendar_event',
101
+ description: '[Official API + UAT, v1.3.7] Delete a calendar event. Requires `calendar:calendar.event:write` scope.',
102
+ inputSchema: {
103
+ type: 'object',
104
+ properties: {
105
+ calendar_id: { type: 'string', description: 'Calendar ID' },
106
+ event_id: { type: 'string', description: 'Event ID' },
107
+ need_notification: { type: 'boolean', description: 'Whether to notify attendees of the deletion (default true)' },
108
+ meeting_chat_id: { type: 'string', description: 'Optional. If the event has a linked meeting chat, pass its chat_id to also dissolve it.' },
109
+ },
110
+ required: ['calendar_id', 'event_id'],
111
+ },
112
+ },
113
+ {
114
+ name: 'respond_calendar_event',
115
+ description: '[Official API + UAT, v1.3.7] Respond to an event invitation. The current identity must be in the event\'s attendee list. Requires `calendar:calendar.event:write` scope.',
116
+ inputSchema: {
117
+ type: 'object',
118
+ properties: {
119
+ calendar_id: { type: 'string', description: 'Calendar ID' },
120
+ event_id: { type: 'string', description: 'Event ID' },
121
+ rsvp_status: { type: 'string', enum: ['accept', 'decline', 'tentative'], description: 'Your response' },
122
+ },
123
+ required: ['calendar_id', 'event_id', 'rsvp_status'],
124
+ },
125
+ },
126
+ {
127
+ name: 'get_freebusy',
128
+ description: '[Official API + UAT, v1.3.7] Query freebusy windows for one or more users in a time range. Use to find a meeting slot. Requires `calendar:calendar:readonly` (already in default scope set).',
129
+ inputSchema: {
130
+ type: 'object',
131
+ properties: {
132
+ time_min: { type: 'string', description: 'RFC3339 start, e.g. 2026-05-04T09:00:00+08:00' },
133
+ time_max: { type: 'string', description: 'RFC3339 end' },
134
+ user_ids: { type: 'array', description: 'Open IDs to query (use get_login_status / search_contacts to look up).', items: { type: 'string' } },
135
+ room_ids: { type: 'array', description: 'Optional meeting-room IDs.', items: { type: 'string' } },
136
+ include_external_calendar: { type: 'boolean', description: 'Include the user\'s synced external calendars (optional)' },
137
+ only_busy: { type: 'boolean', description: 'Only return busy windows (optional)' },
138
+ },
139
+ required: ['time_min', 'time_max', 'user_ids'],
140
+ },
141
+ },
142
+ ];
143
+
144
+ function eventDataFromArgs(args, isUpdate = false) {
145
+ const data = {};
146
+ if (args.summary !== undefined) data.summary = args.summary;
147
+ if (args.description !== undefined) data.description = args.description;
148
+ if (args.start_time !== undefined) data.start_time = args.start_time;
149
+ if (args.end_time !== undefined) data.end_time = args.end_time;
150
+ if (args.location !== undefined) data.location = args.location;
151
+ if (args.visibility !== undefined) data.visibility = args.visibility;
152
+ if (args.attendee_ability !== undefined) data.attendee_ability = args.attendee_ability;
153
+ if (args.free_busy_status !== undefined) data.free_busy_status = args.free_busy_status;
154
+ if (args.reminders !== undefined) data.reminders = args.reminders;
155
+ if (args.recurrence !== undefined) data.recurrence = args.recurrence;
156
+ if (args.need_notification !== undefined) data.need_notification = args.need_notification;
157
+ if (!isUpdate && data.need_notification === undefined) data.need_notification = true;
158
+ return data;
159
+ }
160
+
161
+ const handlers = {
162
+ async list_calendars(args, ctx) {
163
+ return json(await ctx.getOfficialClient().listCalendars({ pageSize: args.page_size, pageToken: args.page_token, syncToken: args.sync_token }));
164
+ },
165
+ async list_calendar_events(args, ctx) {
166
+ return json(await ctx.getOfficialClient().listCalendarEvents(args.calendar_id, {
167
+ startTime: args.start_time, endTime: args.end_time,
168
+ pageSize: args.page_size, pageToken: args.page_token, syncToken: args.sync_token,
169
+ }));
170
+ },
171
+ async get_calendar_event(args, ctx) {
172
+ return json(await ctx.getOfficialClient().getCalendarEvent(args.calendar_id, args.event_id));
173
+ },
174
+ async create_calendar_event(args, ctx) {
175
+ const r = await ctx.getOfficialClient().createCalendarEvent(args.calendar_id, eventDataFromArgs(args, false));
176
+ const ownership = r.viaUser ? ' (as user)' : ' (as app — UAT unavailable or failed; event organized by the app, not you)';
177
+ const warn = r.fallbackWarning ? `\n\n${r.fallbackWarning}` : '';
178
+ return text(`Event created${ownership}: ${r.event?.event_id || '(no id returned)'}\n${JSON.stringify(r.event, null, 2)}${warn}`);
179
+ },
180
+ async update_calendar_event(args, ctx) {
181
+ const r = await ctx.getOfficialClient().updateCalendarEvent(args.calendar_id, args.event_id, eventDataFromArgs(args, true));
182
+ const ownership = r.viaUser ? ' (as user)' : ' (as app)';
183
+ const warn = r.fallbackWarning ? `\n\n${r.fallbackWarning}` : '';
184
+ return text(`Event updated${ownership}: ${args.event_id}\n${JSON.stringify(r.event, null, 2)}${warn}`);
185
+ },
186
+ async delete_calendar_event(args, ctx) {
187
+ const r = await ctx.getOfficialClient().deleteCalendarEvent(args.calendar_id, args.event_id, {
188
+ needNotification: args.need_notification, meetingChatId: args.meeting_chat_id,
189
+ });
190
+ const warn = r.fallbackWarning ? `\n\n${r.fallbackWarning}` : '';
191
+ return text(`Event deleted: ${args.event_id}${warn}`);
192
+ },
193
+ async respond_calendar_event(args, ctx) {
194
+ const r = await ctx.getOfficialClient().respondCalendarEvent(args.calendar_id, args.event_id, args.rsvp_status);
195
+ const warn = r.fallbackWarning ? `\n\n${r.fallbackWarning}` : '';
196
+ return text(`Responded to event ${args.event_id} as ${args.rsvp_status}${warn}`);
197
+ },
198
+ async get_freebusy(args, ctx) {
199
+ return json(await ctx.getOfficialClient().getFreebusy({
200
+ timeMin: args.time_min, timeMax: args.time_max,
201
+ userIds: args.user_ids, roomIds: args.room_ids,
202
+ includeExternalCalendar: args.include_external_calendar, onlyBusy: args.only_busy,
203
+ }));
204
+ },
205
+ };
206
+
207
+ module.exports = { schemas, handlers };
@@ -0,0 +1,66 @@
1
+ // src/tools/contacts.js — contact lookup + P2P chat creation.
2
+
3
+ const { text, json } = require('./_registry');
4
+
5
+ const schemas = [
6
+ {
7
+ name: 'search_contacts',
8
+ description: '[User Identity] Search Feishu users, bots, or group chats by name. Returns IDs.',
9
+ inputSchema: {
10
+ type: 'object',
11
+ properties: { query: { type: 'string', description: 'Search keyword' } },
12
+ required: ['query'],
13
+ },
14
+ },
15
+ {
16
+ name: 'create_p2p_chat',
17
+ description: '[User Identity] Create or get a P2P (direct message) chat. Returns numeric chat_id.',
18
+ inputSchema: {
19
+ type: 'object',
20
+ properties: { user_id: { type: 'string', description: 'Target user ID from search_contacts' } },
21
+ required: ['user_id'],
22
+ },
23
+ },
24
+ {
25
+ name: 'get_user_info',
26
+ description: '[User Identity] Look up a user\'s display name by user ID.',
27
+ inputSchema: {
28
+ type: 'object',
29
+ properties: {
30
+ user_id: { type: 'string', description: 'User ID' },
31
+ chat_id: { type: 'string', description: 'Chat context (optional)' },
32
+ },
33
+ required: ['user_id'],
34
+ },
35
+ },
36
+ ];
37
+
38
+ const handlers = {
39
+ async search_contacts(args, ctx) {
40
+ const c = await ctx.getUserClient();
41
+ return json(await c.search(args.query));
42
+ },
43
+ async create_p2p_chat(args, ctx) {
44
+ const c = await ctx.getUserClient();
45
+ const chatId = await c.createChat(args.user_id);
46
+ return text(chatId ? `P2P chat: ${chatId}` : 'Failed to create P2P chat');
47
+ },
48
+ async get_user_info(args, ctx) {
49
+ let n = null;
50
+ try {
51
+ const official = ctx.getOfficialClient();
52
+ n = await official.getUserById(args.user_id, 'open_id');
53
+ } catch {}
54
+ if (!n) {
55
+ try {
56
+ const c = await ctx.getUserClient();
57
+ n = await c.getUserName(args.user_id);
58
+ } catch {}
59
+ }
60
+ return text(n
61
+ ? `User ${args.user_id}: ${n}`
62
+ : `Could not resolve user ${args.user_id}. Tried (1) UAT contact API, (2) bot contact API, (3) cookie protobuf cache. Possible causes:\n • External tenant user — contact API can't see them. Use search_contacts with display name + the inferred numeric ID for messaging.\n • App is missing contact:user.base:readonly scope (only blocks bot path; UAT path should still work).\n • UAT not configured — run \`npx feishu-user-plugin oauth\`.`);
63
+ },
64
+ };
65
+
66
+ module.exports = { schemas, handlers };