feishu-user-plugin 1.2.1 → 1.3.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.
- package/README.md +103 -39
- package/package.json +5 -3
- package/scripts/confirm-version.js +28 -0
- package/scripts/mcp_stdio_bridge.js +97 -0
- package/skills/feishu-user-plugin/references/CLAUDE.md +338 -61
- package/src/cli.js +12 -7
- package/src/config.js +202 -27
- package/src/index.js +429 -55
- package/src/oauth.js +2 -1
- package/src/official.js +313 -1
- package/src/setup.js +19 -3
package/src/config.js
CHANGED
|
@@ -3,6 +3,118 @@ const path = require('path');
|
|
|
3
3
|
|
|
4
4
|
const SERVER_NAMES = ['feishu-user-plugin', 'feishu'];
|
|
5
5
|
|
|
6
|
+
// --- Atomic file write ---
|
|
7
|
+
// Writes to a tmp file then renames, preventing partial reads / race conditions
|
|
8
|
+
// with Claude Code (which also reads/writes ~/.claude.json).
|
|
9
|
+
function _atomicWrite(filePath, content) {
|
|
10
|
+
const tmpPath = filePath + '.tmp.' + process.pid;
|
|
11
|
+
fs.writeFileSync(tmpPath, content);
|
|
12
|
+
fs.renameSync(tmpPath, filePath);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// --- Minimal TOML helpers (only handles MCP server config structure) ---
|
|
16
|
+
|
|
17
|
+
function _tomlEscape(s) {
|
|
18
|
+
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Read key="value" pairs from a TOML section.
|
|
23
|
+
* Returns { key: value } or null if section not found.
|
|
24
|
+
*/
|
|
25
|
+
function _readTomlSection(content, sectionPath) {
|
|
26
|
+
const header = `[${sectionPath}]`;
|
|
27
|
+
const idx = content.indexOf(header);
|
|
28
|
+
if (idx === -1) return null;
|
|
29
|
+
|
|
30
|
+
const afterHeader = content.slice(idx + header.length);
|
|
31
|
+
const nextSection = afterHeader.search(/^\[/m);
|
|
32
|
+
const sectionBody = nextSection === -1 ? afterHeader : afterHeader.slice(0, nextSection);
|
|
33
|
+
|
|
34
|
+
const result = {};
|
|
35
|
+
for (const line of sectionBody.split('\n')) {
|
|
36
|
+
// String value: key = "value"
|
|
37
|
+
const strMatch = line.match(/^(\w+)\s*=\s*"(.*)"/);
|
|
38
|
+
if (strMatch) { result[strMatch[1]] = strMatch[2]; continue; }
|
|
39
|
+
// Array value: key = ["a", "b"]
|
|
40
|
+
const arrMatch = line.match(/^(\w+)\s*=\s*\[(.*)\]/);
|
|
41
|
+
if (arrMatch) {
|
|
42
|
+
result[arrMatch[1]] = arrMatch[2].split(',').map(s => s.trim().replace(/^"|"$/g, ''));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Update or append key-value pairs in a TOML section.
|
|
50
|
+
* Creates the section if it doesn't exist.
|
|
51
|
+
*/
|
|
52
|
+
function _updateTomlSection(content, sectionPath, updates) {
|
|
53
|
+
const header = `[${sectionPath}]`;
|
|
54
|
+
const idx = content.indexOf(header);
|
|
55
|
+
|
|
56
|
+
if (idx === -1) {
|
|
57
|
+
// Append new section
|
|
58
|
+
let block = '\n' + header + '\n';
|
|
59
|
+
for (const [k, v] of Object.entries(updates)) {
|
|
60
|
+
block += `${k} = "${_tomlEscape(v)}"\n`;
|
|
61
|
+
}
|
|
62
|
+
return content.trimEnd() + '\n' + block;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Find section boundaries
|
|
66
|
+
const afterHeader = idx + header.length;
|
|
67
|
+
const rest = content.slice(afterHeader);
|
|
68
|
+
const nextSection = rest.search(/^\[/m);
|
|
69
|
+
const sectionEnd = nextSection === -1 ? content.length : afterHeader + nextSection;
|
|
70
|
+
let sectionBody = content.slice(afterHeader, sectionEnd);
|
|
71
|
+
|
|
72
|
+
for (const [k, v] of Object.entries(updates)) {
|
|
73
|
+
const escaped = _tomlEscape(v);
|
|
74
|
+
const keyRegex = new RegExp(`^${k}\\s*=\\s*".*"`, 'm');
|
|
75
|
+
if (keyRegex.test(sectionBody)) {
|
|
76
|
+
sectionBody = sectionBody.replace(keyRegex, `${k} = "${escaped}"`);
|
|
77
|
+
} else {
|
|
78
|
+
sectionBody = sectionBody.trimEnd() + `\n${k} = "${escaped}"\n`;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return content.slice(0, afterHeader) + sectionBody + content.slice(sectionEnd);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Generate a complete TOML MCP server entry.
|
|
87
|
+
*/
|
|
88
|
+
function _generateTomlServerEntry(serverName, env) {
|
|
89
|
+
const section = `mcp_servers.${serverName}`;
|
|
90
|
+
// Codex uses Content-Length framing; our server uses newline-delimited JSON.
|
|
91
|
+
// The bridge script translates between the two automatically.
|
|
92
|
+
// Resolve bridge path: prefer local repo, fall back to npx-installed package.
|
|
93
|
+
const localBridge = path.join(__dirname, '..', 'scripts', 'mcp_stdio_bridge.js');
|
|
94
|
+
let block = `[${section}]\n`;
|
|
95
|
+
block += `command = "node"\n`;
|
|
96
|
+
block += `args = ["${_tomlEscape(localBridge)}"]\n\n`;
|
|
97
|
+
block += `[${section}.env]\n`;
|
|
98
|
+
for (const [k, v] of Object.entries(env)) {
|
|
99
|
+
block += `${k} = "${_tomlEscape(v)}"\n`;
|
|
100
|
+
}
|
|
101
|
+
return block;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Remove all TOML sections matching a server name.
|
|
106
|
+
*/
|
|
107
|
+
function _removeTomlServer(content, serverName) {
|
|
108
|
+
// Remove [mcp_servers.<name>] and [mcp_servers.<name>.*] sections
|
|
109
|
+
const pattern = new RegExp(
|
|
110
|
+
`\\[mcp_servers\\.${serverName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[^\\]]*\\][^\\[]*`,
|
|
111
|
+
'g'
|
|
112
|
+
);
|
|
113
|
+
return content.replace(pattern, '');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// --- JSON config helpers ---
|
|
117
|
+
|
|
6
118
|
/**
|
|
7
119
|
* Search an mcpServers object for a feishu-user-plugin entry.
|
|
8
120
|
* Returns { serverName, serverEnv } or null.
|
|
@@ -22,16 +134,19 @@ function _findInServers(servers) {
|
|
|
22
134
|
* Discover the MCP config file containing feishu-user-plugin server entry.
|
|
23
135
|
*
|
|
24
136
|
* Search order:
|
|
25
|
-
* 1. ~/.claude.json — top-level mcpServers
|
|
26
|
-
* 2. ~/.claude.json — projects[*].mcpServers (Claude Code project-level
|
|
137
|
+
* 1. ~/.claude.json — top-level mcpServers (Claude Code)
|
|
138
|
+
* 2. ~/.claude.json — projects[*].mcpServers (Claude Code project-level)
|
|
27
139
|
* 3. ~/.claude/.claude.json — same two-level search
|
|
28
|
-
* 4. <cwd>/.mcp.json — top-level mcpServers
|
|
140
|
+
* 4. <cwd>/.mcp.json — top-level mcpServers
|
|
141
|
+
* 5. ~/.codex/config.toml — Codex MCP config
|
|
29
142
|
*
|
|
30
143
|
* Returns { configPath, config, serverName, serverEnv, projectPath? } or null.
|
|
31
144
|
*/
|
|
32
145
|
function findMcpConfig() {
|
|
33
146
|
const home = process.env.HOME;
|
|
34
|
-
|
|
147
|
+
|
|
148
|
+
// --- JSON candidates ---
|
|
149
|
+
const jsonCandidates = [
|
|
35
150
|
...(home ? [
|
|
36
151
|
path.join(home, '.claude.json'),
|
|
37
152
|
path.join(home, '.claude', '.claude.json'),
|
|
@@ -39,7 +154,7 @@ function findMcpConfig() {
|
|
|
39
154
|
path.join(process.cwd(), '.mcp.json'),
|
|
40
155
|
];
|
|
41
156
|
|
|
42
|
-
for (const configPath of
|
|
157
|
+
for (const configPath of jsonCandidates) {
|
|
43
158
|
try {
|
|
44
159
|
const raw = fs.readFileSync(configPath, 'utf8');
|
|
45
160
|
const config = JSON.parse(raw);
|
|
@@ -72,6 +187,25 @@ function findMcpConfig() {
|
|
|
72
187
|
}
|
|
73
188
|
}
|
|
74
189
|
}
|
|
190
|
+
|
|
191
|
+
// --- Codex TOML ---
|
|
192
|
+
if (home) {
|
|
193
|
+
const codexConfig = path.join(home, '.codex', 'config.toml');
|
|
194
|
+
try {
|
|
195
|
+
const raw = fs.readFileSync(codexConfig, 'utf8');
|
|
196
|
+
for (const name of SERVER_NAMES) {
|
|
197
|
+
const env = _readTomlSection(raw, `mcp_servers.${name}.env`);
|
|
198
|
+
if (env) {
|
|
199
|
+
return { configPath: codexConfig, config: null, serverName: name, serverEnv: env, projectPath: null };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} catch (e) {
|
|
203
|
+
if (e.code !== 'ENOENT') {
|
|
204
|
+
console.error(`[feishu-user-plugin] Warning: Failed to parse ${codexConfig}: ${e.message}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
75
209
|
return null;
|
|
76
210
|
}
|
|
77
211
|
|
|
@@ -87,7 +221,7 @@ function readCredentials() {
|
|
|
87
221
|
|
|
88
222
|
/**
|
|
89
223
|
* Persist key-value updates into the MCP config's env block.
|
|
90
|
-
* Uses findMcpConfig() to locate the correct entry, then writes back.
|
|
224
|
+
* Uses findMcpConfig() to locate the correct entry, then writes back atomically.
|
|
91
225
|
* Returns true if persisted successfully, false otherwise.
|
|
92
226
|
*/
|
|
93
227
|
function persistToConfig(updates) {
|
|
@@ -100,6 +234,17 @@ function persistToConfig(updates) {
|
|
|
100
234
|
|
|
101
235
|
const { configPath, config, serverName, projectPath } = found;
|
|
102
236
|
|
|
237
|
+
// --- TOML path ---
|
|
238
|
+
if (configPath.endsWith('.toml')) {
|
|
239
|
+
let content = '';
|
|
240
|
+
try { content = fs.readFileSync(configPath, 'utf8'); } catch {}
|
|
241
|
+
content = _updateTomlSection(content, `mcp_servers.${serverName}.env`, updates);
|
|
242
|
+
_atomicWrite(configPath, content);
|
|
243
|
+
console.error(`[feishu-user-plugin] Config persisted to ${configPath}`);
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// --- JSON path ---
|
|
103
248
|
// Navigate to the correct env object
|
|
104
249
|
let env;
|
|
105
250
|
if (projectPath) {
|
|
@@ -111,7 +256,7 @@ function persistToConfig(updates) {
|
|
|
111
256
|
}
|
|
112
257
|
|
|
113
258
|
Object.assign(env, updates);
|
|
114
|
-
|
|
259
|
+
_atomicWrite(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
115
260
|
console.error(`[feishu-user-plugin] Config persisted to ${configPath}${projectPath ? ` (project: ${projectPath})` : ''}`);
|
|
116
261
|
return true;
|
|
117
262
|
} catch (e) {
|
|
@@ -124,22 +269,33 @@ function persistToConfig(updates) {
|
|
|
124
269
|
* Write a complete feishu-user-plugin MCP server entry to a config file.
|
|
125
270
|
* Used by the setup wizard.
|
|
126
271
|
*
|
|
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
272
|
* @param {object} env - The env vars to write
|
|
132
|
-
* @param {
|
|
133
|
-
*
|
|
134
|
-
* @returns {{ configPath: string }}
|
|
273
|
+
* @param {object} [options] - { configPath, projectPath, client }
|
|
274
|
+
* client: 'claude' (default) | 'codex' | 'both'
|
|
275
|
+
* @returns {{ configPath: string, codexConfigPath?: string }}
|
|
135
276
|
*/
|
|
136
|
-
function writeNewConfig(env, configPath, projectPath) {
|
|
277
|
+
function writeNewConfig(env, configPath, projectPath, client) {
|
|
278
|
+
const results = {};
|
|
279
|
+
|
|
280
|
+
// --- Claude Code (JSON) ---
|
|
281
|
+
if (client !== 'codex') {
|
|
282
|
+
results.configPath = _writeClaudeConfig(env, configPath, projectPath);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// --- Codex (TOML) ---
|
|
286
|
+
if (client === 'codex' || client === 'both') {
|
|
287
|
+
results.codexConfigPath = _writeCodexConfig(env);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return results;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function _writeClaudeConfig(env, configPath, projectPath) {
|
|
137
294
|
if (!configPath) {
|
|
138
295
|
configPath = path.join(process.env.HOME || '', '.claude.json');
|
|
139
296
|
}
|
|
140
297
|
|
|
141
298
|
if (projectPath) {
|
|
142
|
-
// Verify the project entry still exists; warn if it was removed between discovery and write
|
|
143
299
|
let existing = {};
|
|
144
300
|
try { existing = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch {}
|
|
145
301
|
if (!existing.projects?.[projectPath]) {
|
|
@@ -160,29 +316,48 @@ function writeNewConfig(env, configPath, projectPath) {
|
|
|
160
316
|
};
|
|
161
317
|
|
|
162
318
|
if (projectPath && config.projects?.[projectPath]) {
|
|
163
|
-
// Write into existing project-level config
|
|
164
319
|
if (!config.projects[projectPath].mcpServers) config.projects[projectPath].mcpServers = {};
|
|
165
320
|
config.projects[projectPath].mcpServers['feishu-user-plugin'] = serverEntry;
|
|
166
321
|
if (config.projects[projectPath].mcpServers.feishu) {
|
|
167
322
|
delete config.projects[projectPath].mcpServers.feishu;
|
|
168
323
|
}
|
|
169
324
|
} else if (configPath.endsWith('.mcp.json') && !config.mcpServers) {
|
|
170
|
-
// Bare .mcp.json format: server entries at top level (no mcpServers wrapper)
|
|
171
325
|
config['feishu-user-plugin'] = serverEntry;
|
|
172
|
-
if (config.feishu)
|
|
173
|
-
delete config.feishu;
|
|
174
|
-
}
|
|
326
|
+
if (config.feishu) delete config.feishu;
|
|
175
327
|
} else {
|
|
176
|
-
// Write to top-level mcpServers (default for ~/.claude.json)
|
|
177
328
|
if (!config.mcpServers) config.mcpServers = {};
|
|
178
329
|
config.mcpServers['feishu-user-plugin'] = serverEntry;
|
|
179
|
-
if (config.mcpServers.feishu)
|
|
180
|
-
delete config.mcpServers.feishu;
|
|
181
|
-
}
|
|
330
|
+
if (config.mcpServers.feishu) delete config.mcpServers.feishu;
|
|
182
331
|
}
|
|
183
332
|
|
|
184
|
-
|
|
185
|
-
return
|
|
333
|
+
_atomicWrite(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
334
|
+
return configPath;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function _writeCodexConfig(env) {
|
|
338
|
+
const home = process.env.HOME || '';
|
|
339
|
+
const codexDir = path.join(home, '.codex');
|
|
340
|
+
const configPath = path.join(codexDir, 'config.toml');
|
|
341
|
+
|
|
342
|
+
// Ensure ~/.codex/ exists
|
|
343
|
+
if (!fs.existsSync(codexDir)) {
|
|
344
|
+
fs.mkdirSync(codexDir, { recursive: true });
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
let content = '';
|
|
348
|
+
try { content = fs.readFileSync(configPath, 'utf8'); } catch {}
|
|
349
|
+
|
|
350
|
+
// Remove existing feishu entries
|
|
351
|
+
for (const name of SERVER_NAMES) {
|
|
352
|
+
content = _removeTomlServer(content, name);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Append new entry
|
|
356
|
+
content = content.trimEnd() + '\n\n' + _generateTomlServerEntry('feishu-user-plugin', env);
|
|
357
|
+
|
|
358
|
+
_atomicWrite(configPath, content);
|
|
359
|
+
console.error(`[feishu-user-plugin] Codex config written to ${configPath}`);
|
|
360
|
+
return configPath;
|
|
186
361
|
}
|
|
187
362
|
|
|
188
363
|
module.exports = { findMcpConfig, readCredentials, persistToConfig, writeNewConfig, SERVER_NAMES };
|