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,350 @@
1
+ // src/auth/credentials.js — single-source-of-truth credentials API.
2
+ //
3
+ // Reads from `~/.feishu-user-plugin/credentials.json` (created by the `migrate`
4
+ // CLI subcommand, schema documented at docs/CREDENTIALS-FORMAT.md). Falls back
5
+ // to legacy MCP-config discovery (src/config) when the file is absent so v1.3.6
6
+ // users have zero behaviour change until they opt in.
7
+ //
8
+ // What this owns:
9
+ // - credentials.json read / write (atomic, 0600 perms)
10
+ // - profile lookup for the running MCP server
11
+ // - persistence target for cookie heartbeat + UAT refresh
12
+ //
13
+ // What it does NOT own:
14
+ // - Profile switching mechanics (lives in src/server.js — this module just
15
+ // exposes `setActiveProfile` for the handler to call).
16
+ // - Cookie heartbeat (still lives in src/clients/user.js, calls
17
+ // `persistToConfig` here).
18
+ // - UAT refresh + cross-process file lock (still lives in
19
+ // src/clients/official/base.js, calls `readCredentials` + `persistToConfig`
20
+ // here). Plan to extract into src/auth/{cookie,uat}.js once stable.
21
+ //
22
+ // Public API (stable for callers):
23
+ // - readCredentials() → flat env block of the active profile (back-compat
24
+ // drop-in for src/config::readCredentials)
25
+ // - persistToConfig(updates) → writes the updates onto the active profile's
26
+ // env block; falls back to legacy mcpServers persistence when no
27
+ // credentials.json exists (back-compat drop-in)
28
+ // - readCanonical() → full {version, active, profiles, profileHints} object,
29
+ // or null if no credentials.json yet
30
+ // - getActiveProfileEnv(name?) → env block for a named profile (defaults to
31
+ // the active one), with legacy LARK_PROFILES_JSON / process.env fallback
32
+ // - getActiveProfileName() → string
33
+ // - listProfileNames() → string[] (always includes "default")
34
+ // - setActiveProfile(name) → atomic write of the `active` field
35
+ // - migrate({ dryRun }) → CLI helper; reads legacy config and writes
36
+ // credentials.json
37
+ //
38
+ // Re-exports for callers still on the legacy-only paths:
39
+ // - findMcpConfig, writeNewConfig, SERVER_NAMES (from src/config)
40
+
41
+ const fs = require('fs');
42
+ const os = require('os');
43
+ const path = require('path');
44
+
45
+ const legacy = require('../config');
46
+
47
+ // --- Constants ---
48
+
49
+ const SCHEMA_VERSION = 1;
50
+ const ENV_KEYS = [
51
+ 'LARK_COOKIE',
52
+ 'LARK_APP_ID',
53
+ 'LARK_APP_SECRET',
54
+ 'LARK_USER_ACCESS_TOKEN',
55
+ 'LARK_USER_REFRESH_TOKEN',
56
+ 'LARK_UAT_EXPIRES',
57
+ ];
58
+
59
+ // --- Path resolution ---
60
+
61
+ function _credentialsDir() {
62
+ return path.join(os.homedir(), '.feishu-user-plugin');
63
+ }
64
+
65
+ function _credentialsPath() {
66
+ return path.join(_credentialsDir(), 'credentials.json');
67
+ }
68
+
69
+ // --- Atomic file IO ---
70
+
71
+ function _atomicWriteJson(filePath, obj) {
72
+ const dir = path.dirname(filePath);
73
+ try { fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); } catch (_) {}
74
+ // chmod the dir if it pre-existed with looser perms
75
+ try { fs.chmodSync(dir, 0o700); } catch (_) {}
76
+ const tmpPath = filePath + '.tmp.' + process.pid;
77
+ fs.writeFileSync(tmpPath, JSON.stringify(obj, null, 2) + '\n', { mode: 0o600 });
78
+ fs.renameSync(tmpPath, filePath);
79
+ // chmod again post-rename in case of umask interference
80
+ try { fs.chmodSync(filePath, 0o600); } catch (_) {}
81
+ }
82
+
83
+ function _readFile() {
84
+ try {
85
+ const raw = fs.readFileSync(_credentialsPath(), 'utf8');
86
+ const parsed = JSON.parse(raw);
87
+ if (typeof parsed !== 'object' || parsed === null) return null;
88
+ if (parsed.version !== SCHEMA_VERSION) {
89
+ console.error(`[feishu-user-plugin] credentials.json schema version ${parsed.version} unsupported (expected ${SCHEMA_VERSION}). Ignoring file, falling back to legacy config.`);
90
+ return null;
91
+ }
92
+ if (!parsed.profiles || typeof parsed.profiles !== 'object') return null;
93
+ if (typeof parsed.active !== 'string' || !parsed.profiles[parsed.active]) {
94
+ console.error(`[feishu-user-plugin] credentials.json has invalid active profile "${parsed.active}". Ignoring.`);
95
+ return null;
96
+ }
97
+ if (!parsed.profileHints) parsed.profileHints = {};
98
+ return parsed;
99
+ } catch (e) {
100
+ if (e.code !== 'ENOENT') {
101
+ console.error(`[feishu-user-plugin] credentials.json read failed: ${e.message}. Falling back to legacy config.`);
102
+ }
103
+ return null;
104
+ }
105
+ }
106
+
107
+ // --- Public API ---
108
+
109
+ function readCanonical() {
110
+ return _readFile();
111
+ }
112
+
113
+ function getActiveProfileName() {
114
+ const f = _readFile();
115
+ return f ? f.active : 'default';
116
+ }
117
+
118
+ function listProfileNames() {
119
+ const f = _readFile();
120
+ if (f) return Object.keys(f.profiles);
121
+ // Legacy: default + LARK_PROFILES_JSON keys
122
+ let extras = [];
123
+ try {
124
+ const raw = process.env.LARK_PROFILES_JSON;
125
+ if (raw) extras = Object.keys(JSON.parse(raw) || {});
126
+ } catch (_) {}
127
+ return ['default', ...extras];
128
+ }
129
+
130
+ function getActiveProfileEnv(name) {
131
+ const f = _readFile();
132
+ const target = name || (f ? f.active : 'default');
133
+
134
+ if (f) {
135
+ const profile = f.profiles[target];
136
+ if (!profile) {
137
+ throw new Error(`Profile "${target}" not found in credentials.json. Available: ${Object.keys(f.profiles).join(', ')}`);
138
+ }
139
+ return _normalizeEnv(profile);
140
+ }
141
+
142
+ // Legacy paths: default reads process.env directly; named profiles come from LARK_PROFILES_JSON.
143
+ if (target === 'default') {
144
+ const env = {};
145
+ for (const k of ENV_KEYS) if (process.env[k] !== undefined) env[k] = process.env[k];
146
+ return env;
147
+ }
148
+ let map = {};
149
+ try {
150
+ const raw = process.env.LARK_PROFILES_JSON;
151
+ if (raw) map = JSON.parse(raw) || {};
152
+ } catch (e) {
153
+ throw new Error(`LARK_PROFILES_JSON parse failed: ${e.message}`);
154
+ }
155
+ const profile = map[target];
156
+ if (!profile) {
157
+ throw new Error(`Profile "${target}" not found. Available: ${['default', ...Object.keys(map)].join(', ')}`);
158
+ }
159
+ return _normalizeEnv(profile);
160
+ }
161
+
162
+ // Coerce numeric LARK_UAT_EXPIRES → string so it round-trips through env-var
163
+ // callers (process.env always returns strings).
164
+ function _normalizeEnv(profile) {
165
+ const out = {};
166
+ for (const k of ENV_KEYS) {
167
+ if (profile[k] === undefined || profile[k] === null) continue;
168
+ out[k] = typeof profile[k] === 'number' ? String(profile[k]) : profile[k];
169
+ }
170
+ return out;
171
+ }
172
+
173
+ function setActiveProfile(name) {
174
+ const f = _readFile();
175
+ if (!f) {
176
+ throw new Error('No credentials.json — run `npx feishu-user-plugin migrate --confirm` to create one.');
177
+ }
178
+ if (!f.profiles[name]) {
179
+ throw new Error(`Profile "${name}" not found in credentials.json. Available: ${Object.keys(f.profiles).join(', ')}`);
180
+ }
181
+ f.active = name;
182
+ _atomicWriteJson(_credentialsPath(), f);
183
+ }
184
+
185
+ function persistProfileUpdate(profileName, updates) {
186
+ const f = _readFile();
187
+ if (!f) return false;
188
+ if (!f.profiles[profileName]) {
189
+ console.error(`[feishu-user-plugin] persistProfileUpdate: profile "${profileName}" not found in credentials.json`);
190
+ return false;
191
+ }
192
+ // LARK_UAT_EXPIRES sometimes comes through as string; preserve number when possible.
193
+ const normalized = {};
194
+ for (const [k, v] of Object.entries(updates)) {
195
+ if (v === undefined || v === null) continue;
196
+ if (k === 'LARK_UAT_EXPIRES' && typeof v === 'string') {
197
+ const n = parseInt(v, 10);
198
+ normalized[k] = Number.isFinite(n) ? n : v;
199
+ } else {
200
+ normalized[k] = v;
201
+ }
202
+ }
203
+ Object.assign(f.profiles[profileName], normalized);
204
+ _atomicWriteJson(_credentialsPath(), f);
205
+ return true;
206
+ }
207
+
208
+ // Back-compat drop-in for src/config::readCredentials. Resolution order:
209
+ // 1. credentials.json (active profile)
210
+ // 2. process.env.LARK_* (MCP-server context — harness injects env at spawn)
211
+ // 3. legacy mcpServers discovery via src/config (CLI context where the
212
+ // caller process did not get the env block)
213
+ // The order matters: smoke.js + the MCP server want the in-process env to
214
+ // win over disk discovery (so the diff baseline matches the spawn env).
215
+ // CLI commands like `status` and `keepalive` have no env, so they fall
216
+ // through to the legacy reader.
217
+ function readCredentials() {
218
+ const f = _readFile();
219
+ if (f) {
220
+ return _normalizeEnv(f.profiles[f.active]);
221
+ }
222
+ const env = {};
223
+ for (const k of ENV_KEYS) if (process.env[k] !== undefined) env[k] = process.env[k];
224
+ if (Object.keys(env).length > 0) return env;
225
+ return legacy.readCredentials();
226
+ }
227
+
228
+ // Back-compat drop-in for src/config::persistToConfig. Routes writes to:
229
+ // - credentials.json (active profile) when the file exists
230
+ // - legacy mcpServers env block otherwise
231
+ function persistToConfig(updates) {
232
+ const f = _readFile();
233
+ if (f) {
234
+ return persistProfileUpdate(f.active, updates);
235
+ }
236
+ return legacy.persistToConfig(updates);
237
+ }
238
+
239
+ // --- Migration (called by `npx feishu-user-plugin migrate`) ---
240
+
241
+ function migrate({ dryRun = true } = {}) {
242
+ const filePath = _credentialsPath();
243
+ const existing = _readFile();
244
+ if (existing) {
245
+ console.log(`credentials.json already exists at ${filePath}`);
246
+ console.log(`active profile: ${existing.active}`);
247
+ console.log(`profiles: ${Object.keys(existing.profiles).join(', ')}`);
248
+ console.log('');
249
+ console.log('No migration needed. To re-create from harness configs, delete the file first:');
250
+ console.log(` rm ${filePath}`);
251
+ return { ok: true, alreadyMigrated: true };
252
+ }
253
+
254
+ // Discover legacy creds
255
+ const found = legacy.findMcpConfig();
256
+ if (!found) {
257
+ console.error('No MCP config found. Run `npx feishu-user-plugin setup` first.');
258
+ return { ok: false, reason: 'no-config' };
259
+ }
260
+
261
+ const defaultProfile = {};
262
+ for (const k of ENV_KEYS) {
263
+ if (found.serverEnv[k] !== undefined && found.serverEnv[k] !== null) {
264
+ defaultProfile[k] = k === 'LARK_UAT_EXPIRES' ? parseInt(found.serverEnv[k], 10) || 0 : found.serverEnv[k];
265
+ }
266
+ }
267
+
268
+ // Merge LARK_PROFILES_JSON if present
269
+ const profiles = { default: defaultProfile };
270
+ const rawExtras = found.serverEnv.LARK_PROFILES_JSON;
271
+ if (rawExtras) {
272
+ try {
273
+ const parsed = JSON.parse(rawExtras);
274
+ for (const [name, env] of Object.entries(parsed)) {
275
+ if (name === 'default') {
276
+ console.error(`[migrate] Skipping LARK_PROFILES_JSON entry "default" (collision with primary profile).`);
277
+ continue;
278
+ }
279
+ const cleaned = {};
280
+ for (const k of ENV_KEYS) {
281
+ if (env[k] !== undefined && env[k] !== null) {
282
+ cleaned[k] = k === 'LARK_UAT_EXPIRES' ? parseInt(env[k], 10) || 0 : env[k];
283
+ }
284
+ }
285
+ profiles[name] = cleaned;
286
+ }
287
+ } catch (e) {
288
+ console.error(`[migrate] LARK_PROFILES_JSON parse failed: ${e.message}. Skipping extra profiles.`);
289
+ }
290
+ }
291
+
292
+ const credentials = {
293
+ version: SCHEMA_VERSION,
294
+ active: 'default',
295
+ profiles,
296
+ profileHints: {},
297
+ };
298
+
299
+ console.log(`Source: ${found.configPath}${found.projectPath ? ` (project: ${found.projectPath})` : ''}`);
300
+ console.log(`Target: ${filePath}`);
301
+ console.log(`Profiles found: ${Object.keys(profiles).join(', ')}`);
302
+ console.log('');
303
+ for (const [name, env] of Object.entries(profiles)) {
304
+ console.log(` [${name}]`);
305
+ for (const k of ENV_KEYS) {
306
+ if (env[k] === undefined) continue;
307
+ const display = k.includes('SECRET') || k.includes('TOKEN') || k.includes('COOKIE')
308
+ ? `${String(env[k]).slice(0, 12)}…(${String(env[k]).length} chars)`
309
+ : env[k];
310
+ console.log(` ${k}: ${display}`);
311
+ }
312
+ }
313
+ console.log('');
314
+
315
+ if (dryRun) {
316
+ console.log('Dry run — no file written. Re-run with `--confirm` to persist.');
317
+ return { ok: true, dryRun: true, credentials };
318
+ }
319
+
320
+ _atomicWriteJson(filePath, credentials);
321
+ console.log(`✓ Wrote ${filePath} (mode 0600)`);
322
+ console.log('');
323
+ console.log('Next steps:');
324
+ console.log(' 1. Restart Claude Code / Codex so the MCP server adopts the new credentials source.');
325
+ console.log(' 2. Existing harness env blocks remain untouched as a fallback.');
326
+ console.log(' 3. To start fresh: delete the file and re-run migrate.');
327
+ return { ok: true, credentials };
328
+ }
329
+
330
+ // --- Re-exports for back-compat ---
331
+
332
+ module.exports = {
333
+ // canonical API
334
+ readCanonical,
335
+ getActiveProfileName,
336
+ listProfileNames,
337
+ getActiveProfileEnv,
338
+ setActiveProfile,
339
+ persistProfileUpdate,
340
+ migrate,
341
+ // back-compat with src/config
342
+ readCredentials,
343
+ persistToConfig,
344
+ findMcpConfig: legacy.findMcpConfig,
345
+ writeNewConfig: legacy.writeNewConfig,
346
+ SERVER_NAMES: legacy.SERVER_NAMES,
347
+ // constants
348
+ SCHEMA_VERSION,
349
+ ENV_KEYS,
350
+ };
package/src/cli.js CHANGED
@@ -25,6 +25,17 @@ switch (cmd) {
25
25
  case 'keepalive':
26
26
  keepalive();
27
27
  break;
28
+ case 'list-prompts': {
29
+ const { listPrompts } = require('./prompts');
30
+ for (const p of listPrompts()) {
31
+ console.log(`/${p.name} — ${p.description}`);
32
+ for (const a of (p.arguments || [])) console.log(` - ${a.name}${a.required ? ' (required)' : ''}: ${a.description}`);
33
+ }
34
+ break;
35
+ }
36
+ case 'migrate':
37
+ migrate();
38
+ break;
28
39
  case 'help':
29
40
  case '--help':
30
41
  case '-h':
@@ -46,6 +57,9 @@ Commands:
46
57
  oauth Run OAuth flow to obtain user_access_token
47
58
  status Check authentication status
48
59
  keepalive Refresh cookie + UAT to prevent expiration (for cron jobs)
60
+ migrate One-time consolidation: copy creds from harness configs into
61
+ ~/.feishu-user-plugin/credentials.json (single source of truth).
62
+ Dry-run by default. Add --confirm to actually write.
49
63
  help Show this help
50
64
 
51
65
  Setup options:
@@ -70,17 +84,23 @@ Auto-renewal (optional):
70
84
  `);
71
85
  }
72
86
 
87
+ function migrate() {
88
+ const { migrate: runMigrate } = require('./auth/credentials');
89
+ const confirm = process.argv.includes('--confirm');
90
+ const result = runMigrate({ dryRun: !confirm });
91
+ process.exit(result.ok ? 0 : 1);
92
+ }
93
+
73
94
  async function keepalive() {
74
- const { LarkUserClient } = require('./client');
75
- const { LarkOfficialClient } = require('./official');
76
- const { findMcpConfig, persistToConfig } = require('./config');
95
+ const { LarkUserClient } = require('./clients/user');
96
+ const { LarkOfficialClient } = require('./clients/official');
97
+ const { readCredentials, persistToConfig } = require('./auth/credentials');
77
98
 
78
- const found = findMcpConfig();
79
- if (!found) {
80
- console.error('[keepalive] No config found. Run: npx feishu-user-plugin setup');
99
+ const creds = readCredentials();
100
+ if (!creds.LARK_COOKIE && !creds.LARK_APP_ID) {
101
+ console.error('[keepalive] No credentials found. Run: npx feishu-user-plugin setup');
81
102
  process.exit(1);
82
103
  }
83
- const creds = found.serverEnv;
84
104
  let ok = true;
85
105
 
86
106
  // 1. Refresh Cookie
@@ -124,18 +144,27 @@ async function keepalive() {
124
144
  }
125
145
 
126
146
  async function checkStatus() {
127
- const { LarkUserClient } = require('./client');
128
- const { LarkOfficialClient } = require('./official');
147
+ const { LarkUserClient } = require('./clients/user');
148
+ const { LarkOfficialClient } = require('./clients/official');
129
149
  const { findMcpConfig } = require('./config');
150
+ const { readCanonical, getActiveProfileName, listProfileNames, readCredentials } = require('./auth/credentials');
130
151
 
152
+ const canonical = readCanonical();
131
153
  const found = findMcpConfig();
132
- const creds = found ? found.serverEnv : {};
154
+ const creds = readCredentials();
133
155
 
134
156
  console.log('=== feishu-user-plugin Auth Status ===\n');
135
- if (found) {
136
- console.log(`Config: ${found.configPath}${found.projectPath ? ` (project: ${found.projectPath})` : ''}`);
157
+ if (canonical) {
158
+ const path = require('path');
159
+ const os = require('os');
160
+ console.log(`Source: ${path.join(os.homedir(), '.feishu-user-plugin', 'credentials.json')} (canonical)`);
161
+ console.log(`Active profile: ${getActiveProfileName()}`);
162
+ console.log(`Available profiles: ${listProfileNames().join(', ')}`);
163
+ } else if (found) {
164
+ console.log(`Source: ${found.configPath}${found.projectPath ? ` (project: ${found.projectPath})` : ''} (legacy)`);
165
+ console.log('Tip: run `npx feishu-user-plugin migrate --confirm` to consolidate creds into ~/.feishu-user-plugin/credentials.json.');
137
166
  } else {
138
- console.log('Config: NOT FOUND (run: npx feishu-user-plugin setup)');
167
+ console.log('Source: NOT FOUND (run: npx feishu-user-plugin setup)');
139
168
  }
140
169
  console.log('');
141
170