feishu-user-plugin 1.1.3 → 1.2.1

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feishu-user-plugin",
3
- "version": "1.1.3",
3
+ "version": "1.2.0",
4
4
  "description": "All-in-one Feishu plugin for Claude Code — send messages as yourself, read chats, manage docs/tables/wiki. 33 tools + 9 skills, 3 auth layers.",
5
5
  "author": {
6
6
  "name": "EthanQC"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "feishu-user-plugin",
3
- "version": "1.1.3",
4
- "description": "All-in-one Feishu plugin for Claude Code — send messages as yourself, read chats, manage docs/tables/wiki. 33 tools + 9 skills, 3 auth layers.",
3
+ "version": "1.2.1",
4
+ "description": "All-in-one Feishu plugin for Claude Code — send messages as yourself, read chats, manage docs/tables/wiki. 46 tools + 9 skills, 3 auth layers.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
7
  "feishu-user-plugin": "src/cli.js"
package/src/cli.js CHANGED
@@ -7,6 +7,7 @@
7
7
  * npx feishu-user-plugin setup → Interactive setup wizard
8
8
  * npx feishu-user-plugin oauth → Run OAuth flow for UAT
9
9
  * npx feishu-user-plugin status → Check auth status
10
+ * npx feishu-user-plugin keepalive → Refresh cookie + UAT (for cron)
10
11
  */
11
12
 
12
13
  const cmd = process.argv[2];
@@ -21,6 +22,9 @@ switch (cmd) {
21
22
  case 'status':
22
23
  checkStatus();
23
24
  break;
25
+ case 'keepalive':
26
+ keepalive();
27
+ break;
24
28
  case 'help':
25
29
  case '--help':
26
30
  case '-h':
@@ -41,6 +45,7 @@ Commands:
41
45
  setup Interactive setup wizard — writes MCP config
42
46
  oauth Run OAuth flow to obtain user_access_token
43
47
  status Check authentication status
48
+ keepalive Refresh cookie + UAT to prevent expiration (for cron jobs)
44
49
  help Show this help
45
50
 
46
51
  Quick Start (team members):
@@ -53,20 +58,85 @@ Quick Start (external users):
53
58
  2. npx feishu-user-plugin setup
54
59
  3. npx feishu-user-plugin oauth
55
60
  4. Restart Claude Code
61
+
62
+ Auto-renewal (optional):
63
+ Add to crontab to keep tokens alive even when Claude Code is closed:
64
+ crontab -e → add: 0 */4 * * * npx feishu-user-plugin keepalive >> /tmp/feishu-keepalive.log 2>&1
56
65
  `);
57
66
  }
58
67
 
68
+ async function keepalive() {
69
+ const { LarkUserClient } = require('./client');
70
+ const { LarkOfficialClient } = require('./official');
71
+ const { findMcpConfig, persistToConfig } = require('./config');
72
+
73
+ const found = findMcpConfig();
74
+ if (!found) {
75
+ console.error('[keepalive] No config found. Run: npx feishu-user-plugin setup');
76
+ process.exit(1);
77
+ }
78
+ const creds = found.serverEnv;
79
+ let ok = true;
80
+
81
+ // 1. Refresh Cookie
82
+ const cookie = creds.LARK_COOKIE;
83
+ if (cookie && cookie !== 'SETUP_NEEDED') {
84
+ try {
85
+ const client = new LarkUserClient(cookie);
86
+ await client.init();
87
+ // init() calls _getCsrfToken which refreshes sl_session
88
+ persistToConfig({ LARK_COOKIE: client.cookieStr });
89
+ console.log(`[keepalive] Cookie refreshed (user: ${client.userName})`);
90
+ } catch (e) {
91
+ console.error(`[keepalive] Cookie refresh FAILED: ${e.message}`);
92
+ ok = false;
93
+ }
94
+ }
95
+
96
+ // 2. Refresh UAT
97
+ const appId = creds.LARK_APP_ID;
98
+ const appSecret = creds.LARK_APP_SECRET;
99
+ const uat = creds.LARK_USER_ACCESS_TOKEN;
100
+ const rt = creds.LARK_USER_REFRESH_TOKEN;
101
+ if (appId && appSecret && uat && uat !== 'SETUP_NEEDED' && rt) {
102
+ try {
103
+ const official = new LarkOfficialClient(appId, appSecret);
104
+ official._uat = uat;
105
+ official._uatRefresh = rt;
106
+ official._uatExpires = 0; // force refresh
107
+ await official._refreshUAT(); // refreshes + persists automatically
108
+ console.log('[keepalive] UAT refreshed');
109
+ } catch (e) {
110
+ console.error(`[keepalive] UAT refresh FAILED: ${e.message}`);
111
+ ok = false;
112
+ }
113
+ }
114
+
115
+ if (ok) {
116
+ console.log('[keepalive] All tokens refreshed successfully');
117
+ }
118
+ process.exit(ok ? 0 : 1);
119
+ }
120
+
59
121
  async function checkStatus() {
60
122
  const { LarkUserClient } = require('./client');
61
123
  const { LarkOfficialClient } = require('./official');
62
- const path = require('path');
63
- require('dotenv').config({ path: path.join(__dirname, '..', '.env') });
124
+ const { findMcpConfig } = require('./config');
125
+
126
+ const found = findMcpConfig();
127
+ const creds = found ? found.serverEnv : {};
64
128
 
65
129
  console.log('=== feishu-user-plugin Auth Status ===\n');
130
+ if (found) {
131
+ console.log(`Config: ${found.configPath}${found.projectPath ? ` (project: ${found.projectPath})` : ''}`);
132
+ } else {
133
+ console.log('Config: NOT FOUND (run: npx feishu-user-plugin setup)');
134
+ }
135
+ console.log('');
66
136
 
67
137
  // Cookie
68
- const cookie = process.env.LARK_COOKIE;
69
- if (cookie) {
138
+ const cookie = creds.LARK_COOKIE;
139
+ if (cookie && cookie !== 'SETUP_NEEDED') {
70
140
  try {
71
141
  const client = new LarkUserClient(cookie);
72
142
  await client.init();
@@ -79,21 +149,24 @@ async function checkStatus() {
79
149
  }
80
150
 
81
151
  // App credentials
82
- const appId = process.env.LARK_APP_ID;
83
- const appSecret = process.env.LARK_APP_SECRET;
152
+ const appId = creds.LARK_APP_ID;
153
+ const appSecret = creds.LARK_APP_SECRET;
84
154
  console.log(`App credentials: ${appId && appSecret ? 'OK' : 'NOT SET'}`);
85
155
 
86
156
  // UAT
87
- const uat = process.env.LARK_USER_ACCESS_TOKEN;
88
- const rt = process.env.LARK_USER_REFRESH_TOKEN;
89
- if (uat) {
157
+ const uat = creds.LARK_USER_ACCESS_TOKEN;
158
+ const rt = creds.LARK_USER_REFRESH_TOKEN;
159
+ if (uat && uat !== 'SETUP_NEEDED') {
90
160
  console.log(`UAT: SET (refresh_token: ${rt ? 'YES' : 'NO'})`);
91
161
  if (appId && appSecret) {
92
162
  const official = new LarkOfficialClient(appId, appSecret);
93
- official.loadUAT();
163
+ // Set UAT fields directly (bypassing loadUAT which reads from process.env)
164
+ official._uat = uat;
165
+ official._uatRefresh = rt || null;
166
+ official._uatExpires = parseInt(creds.LARK_UAT_EXPIRES || '0');
94
167
  try {
95
- const chats = await official.listChatsAsUser({ pageSize: 1 });
96
- console.log(` UAT test: OK (can list chats)`);
168
+ await official.listChatsAsUser({ pageSize: 1 });
169
+ console.log(' UAT test: OK (can list chats)');
97
170
  } catch (e) {
98
171
  console.log(` UAT test: FAILED — ${e.message}`);
99
172
  }
package/src/client.js CHANGED
@@ -90,7 +90,10 @@ class LarkUserClient {
90
90
  this._heartbeatTimer = setInterval(async () => {
91
91
  try {
92
92
  await this._getCsrfToken();
93
- console.error('[feishu-user-plugin] Cookie heartbeat: session refreshed');
93
+ // Lazy require to avoid circular dependency at module load time
94
+ const { persistToConfig } = require('./config');
95
+ persistToConfig({ LARK_COOKIE: this.cookieStr });
96
+ console.error('[feishu-user-plugin] Cookie heartbeat: session refreshed and persisted');
94
97
  } catch (e) {
95
98
  console.error('[feishu-user-plugin] Cookie heartbeat failed:', e.message);
96
99
  }
@@ -252,7 +255,8 @@ class LarkUserClient {
252
255
  const propBuf = this._encode('TextProperty', { content: elem.userId });
253
256
  dictionary[elemId] = { tag: 5, property: propBuf };
254
257
  } else if (elem.tag === 'a') {
255
- const propBuf = this._encode('TextProperty', { content: elem.text || elem.href });
258
+ // Link element: content stores the URL, display text goes through innerText
259
+ const propBuf = this._encode('TextProperty', { content: elem.href || elem.text || '' });
256
260
  dictionary[elemId] = { tag: 6, property: propBuf };
257
261
  }
258
262
  }
package/src/config.js ADDED
@@ -0,0 +1,188 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const SERVER_NAMES = ['feishu-user-plugin', 'feishu'];
5
+
6
+ /**
7
+ * Search an mcpServers object for a feishu-user-plugin entry.
8
+ * Returns { serverName, serverEnv } or null.
9
+ */
10
+ function _findInServers(servers) {
11
+ if (!servers || typeof servers !== 'object') return null;
12
+ for (const name of SERVER_NAMES) {
13
+ if (servers[name]) {
14
+ if (!servers[name].env) servers[name].env = {};
15
+ return { serverName: name, serverEnv: servers[name].env };
16
+ }
17
+ }
18
+ return null;
19
+ }
20
+
21
+ /**
22
+ * Discover the MCP config file containing feishu-user-plugin server entry.
23
+ *
24
+ * Search order:
25
+ * 1. ~/.claude.json — top-level mcpServers
26
+ * 2. ~/.claude.json — projects[*].mcpServers (Claude Code project-level config)
27
+ * 3. ~/.claude/.claude.json — same two-level search
28
+ * 4. <cwd>/.mcp.json — top-level mcpServers (reliable in CLI mode)
29
+ *
30
+ * Returns { configPath, config, serverName, serverEnv, projectPath? } or null.
31
+ */
32
+ function findMcpConfig() {
33
+ const home = process.env.HOME;
34
+ const candidates = [
35
+ ...(home ? [
36
+ path.join(home, '.claude.json'),
37
+ path.join(home, '.claude', '.claude.json'),
38
+ ] : []),
39
+ path.join(process.cwd(), '.mcp.json'),
40
+ ];
41
+
42
+ for (const configPath of candidates) {
43
+ try {
44
+ const raw = fs.readFileSync(configPath, 'utf8');
45
+ const config = JSON.parse(raw);
46
+
47
+ // Strategy 1: top-level mcpServers
48
+ const topLevel = _findInServers(config.mcpServers);
49
+ if (topLevel) {
50
+ return { configPath, config, ...topLevel, projectPath: null };
51
+ }
52
+
53
+ // Strategy 2: projects[*].mcpServers (Claude Code nests project-level config here)
54
+ if (config.projects) {
55
+ for (const [projPath, projConfig] of Object.entries(config.projects)) {
56
+ const nested = _findInServers(projConfig.mcpServers);
57
+ if (nested) {
58
+ return { configPath, config, ...nested, projectPath: projPath };
59
+ }
60
+ }
61
+ }
62
+
63
+ // Strategy 3: .mcp.json uses top-level keys as server names (no mcpServers wrapper)
64
+ const bare = _findInServers(config);
65
+ if (bare) {
66
+ return { configPath, config, ...bare, projectPath: null };
67
+ }
68
+ } catch (e) {
69
+ // Only warn if the file exists but is invalid (not for missing files)
70
+ if (e.code !== 'ENOENT') {
71
+ console.error(`[feishu-user-plugin] Warning: Failed to parse ${configPath}: ${e.message}`);
72
+ }
73
+ }
74
+ }
75
+ return null;
76
+ }
77
+
78
+ /**
79
+ * Read all LARK_* credentials from the discovered MCP config.
80
+ * Returns an object with all env vars, or {} if no config found.
81
+ */
82
+ function readCredentials() {
83
+ const found = findMcpConfig();
84
+ if (!found) return {};
85
+ return { ...found.serverEnv };
86
+ }
87
+
88
+ /**
89
+ * Persist key-value updates into the MCP config's env block.
90
+ * Uses findMcpConfig() to locate the correct entry, then writes back.
91
+ * Returns true if persisted successfully, false otherwise.
92
+ */
93
+ function persistToConfig(updates) {
94
+ try {
95
+ const found = findMcpConfig();
96
+ if (!found) {
97
+ console.error('[feishu-user-plugin] WARNING: No MCP config found. Update your config manually.');
98
+ return false;
99
+ }
100
+
101
+ const { configPath, config, serverName, projectPath } = found;
102
+
103
+ // Navigate to the correct env object
104
+ let env;
105
+ if (projectPath) {
106
+ env = config.projects[projectPath].mcpServers[serverName].env;
107
+ } else if (config.mcpServers?.[serverName]) {
108
+ env = config.mcpServers[serverName].env;
109
+ } else {
110
+ env = config[serverName].env;
111
+ }
112
+
113
+ Object.assign(env, updates);
114
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
115
+ console.error(`[feishu-user-plugin] Config persisted to ${configPath}${projectPath ? ` (project: ${projectPath})` : ''}`);
116
+ return true;
117
+ } catch (e) {
118
+ console.error(`[feishu-user-plugin] Failed to persist config: ${e.message}`);
119
+ return false;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Write a complete feishu-user-plugin MCP server entry to a config file.
125
+ * Used by the setup wizard.
126
+ *
127
+ * If an existing config is found via findMcpConfig(), updates it in-place
128
+ * (preserving its location — top-level or project-level).
129
+ * Otherwise, writes to ~/.claude.json top-level mcpServers.
130
+ *
131
+ * @param {object} env - The env vars to write
132
+ * @param {string} [configPath] - Override the target config file path
133
+ * @param {string} [projectPath] - If writing to a project-level entry
134
+ * @returns {{ configPath: string }} The path that was written
135
+ */
136
+ function writeNewConfig(env, configPath, projectPath) {
137
+ if (!configPath) {
138
+ configPath = path.join(process.env.HOME || '', '.claude.json');
139
+ }
140
+
141
+ if (projectPath) {
142
+ // Verify the project entry still exists; warn if it was removed between discovery and write
143
+ let existing = {};
144
+ try { existing = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch {}
145
+ if (!existing.projects?.[projectPath]) {
146
+ console.error(`[feishu-user-plugin] Warning: project entry "${projectPath}" not found in ${configPath}, writing to top-level mcpServers`);
147
+ projectPath = null;
148
+ }
149
+ }
150
+
151
+ let config = {};
152
+ try {
153
+ config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
154
+ } catch {}
155
+
156
+ const serverEntry = {
157
+ command: 'npx',
158
+ args: ['-y', 'feishu-user-plugin'],
159
+ env,
160
+ };
161
+
162
+ if (projectPath && config.projects?.[projectPath]) {
163
+ // Write into existing project-level config
164
+ if (!config.projects[projectPath].mcpServers) config.projects[projectPath].mcpServers = {};
165
+ config.projects[projectPath].mcpServers['feishu-user-plugin'] = serverEntry;
166
+ if (config.projects[projectPath].mcpServers.feishu) {
167
+ delete config.projects[projectPath].mcpServers.feishu;
168
+ }
169
+ } else if (configPath.endsWith('.mcp.json') && !config.mcpServers) {
170
+ // Bare .mcp.json format: server entries at top level (no mcpServers wrapper)
171
+ config['feishu-user-plugin'] = serverEntry;
172
+ if (config.feishu) {
173
+ delete config.feishu;
174
+ }
175
+ } else {
176
+ // Write to top-level mcpServers (default for ~/.claude.json)
177
+ if (!config.mcpServers) config.mcpServers = {};
178
+ config.mcpServers['feishu-user-plugin'] = serverEntry;
179
+ if (config.mcpServers.feishu) {
180
+ delete config.mcpServers.feishu;
181
+ }
182
+ }
183
+
184
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
185
+ return { configPath };
186
+ }
187
+
188
+ module.exports = { findMcpConfig, readCredentials, persistToConfig, writeNewConfig, SERVER_NAMES };