feishu-user-plugin 1.3.6 → 1.3.8
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/.claude-plugin/plugin.json +2 -2
- package/CHANGELOG.md +71 -0
- package/README.md +72 -41
- package/package.json +10 -3
- package/scripts/capture-feishu-protobuf.js +86 -0
- package/scripts/check-changelog.js +31 -0
- package/scripts/check-docs-sync.js +41 -0
- package/scripts/check-tool-count.js +40 -0
- package/scripts/check-version.js +40 -0
- package/scripts/decode-feishu-protobuf.js +115 -0
- package/scripts/smoke.js +224 -0
- package/scripts/sync-claude-md.sh +12 -0
- package/scripts/sync-server-json.js +71 -0
- package/scripts/sync-team-skills.sh +22 -0
- package/scripts/test-all-tools.js +158 -0
- package/scripts/test-wiki-attach-fallback.js +71 -0
- package/scripts/test-ws-events.js +84 -0
- package/skills/feishu-user-plugin/SKILL.md +5 -5
- package/skills/feishu-user-plugin/references/CLAUDE.md +248 -318
- package/skills/feishu-user-plugin/references/table.md +18 -9
- package/src/auth/cookie.js +30 -0
- package/src/auth/credentials.js +399 -0
- package/src/auth/profile-router.js +248 -0
- package/src/auth/uat.js +231 -0
- package/src/cli.js +45 -13
- package/src/clients/official/base.js +188 -0
- package/src/clients/official/bitable.js +269 -0
- package/src/clients/official/calendar.js +176 -0
- package/src/clients/official/contacts.js +54 -0
- package/src/clients/official/docs.js +301 -0
- package/src/clients/official/drive.js +77 -0
- package/src/clients/official/groups.js +68 -0
- package/src/clients/official/im.js +414 -0
- package/src/clients/official/index.js +30 -0
- package/src/clients/official/okr.js +127 -0
- package/src/clients/official/tasks.js +142 -0
- package/src/clients/official/uploads.js +260 -0
- package/src/clients/official/wiki.js +207 -0
- package/src/{client.js → clients/user.js} +25 -33
- package/src/config.js +13 -8
- package/src/events/event-buffer.js +100 -0
- package/src/events/index.js +5 -0
- package/src/events/ws-server.js +86 -0
- package/src/index.js +4 -1977
- package/src/logger.js +20 -0
- package/src/oauth.js +5 -1
- package/src/official.js +5 -1944
- package/src/prompts/_registry.js +69 -0
- package/src/prompts/index.js +54 -0
- package/src/server.js +305 -0
- package/src/setup.js +16 -1
- package/src/test-all.js +2 -2
- package/src/test-comprehensive.js +3 -3
- package/src/test-send.js +1 -1
- package/src/tools/_registry.js +31 -0
- package/src/tools/bitable.js +246 -0
- package/src/tools/calendar.js +207 -0
- package/src/tools/contacts.js +66 -0
- package/src/tools/diagnostics.js +172 -0
- package/src/tools/docs.js +158 -0
- package/src/tools/drive.js +111 -0
- package/src/tools/events.js +64 -0
- package/src/tools/groups.js +81 -0
- package/src/tools/im-read.js +259 -0
- package/src/tools/messaging-bot.js +151 -0
- package/src/tools/messaging-user.js +292 -0
- package/src/tools/okr.js +159 -0
- package/src/tools/profile.js +74 -0
- package/src/tools/tasks.js +168 -0
- package/src/tools/uploads.js +63 -0
- package/src/tools/wiki.js +191 -0
|
@@ -6,21 +6,29 @@
|
|
|
6
6
|
## 执行步骤
|
|
7
7
|
|
|
8
8
|
### 查询数据
|
|
9
|
-
1. 用 `
|
|
10
|
-
2. 用 `
|
|
11
|
-
3. 用 `
|
|
9
|
+
1. 用 `manage_bitable_table(action=list, app_token=...)` 获取表格列表
|
|
10
|
+
2. 用 `manage_bitable_field(action=list, app_token=..., table_id=...)` 获取字段结构
|
|
11
|
+
3. 用 `manage_bitable_record(action=search, app_token=..., table_id=..., filter?, sort?)` 查询记录
|
|
12
12
|
4. 格式化展示查询结果
|
|
13
13
|
|
|
14
14
|
### 写入数据
|
|
15
|
-
1. 先用 `
|
|
16
|
-
2. 用 `
|
|
15
|
+
1. 先用 `manage_bitable_field(action=list)` 确认字段结构
|
|
16
|
+
2. 用 `manage_bitable_record(action=create, records=[{fields:{...}}])` 创建记录(可批量,最多 500/次)
|
|
17
17
|
```
|
|
18
|
-
|
|
18
|
+
manage_bitable_record({
|
|
19
|
+
action: 'create',
|
|
20
|
+
app_token,
|
|
21
|
+
table_id,
|
|
22
|
+
records: [{ fields: {"状态":"进行中","标题":"新任务"} }]
|
|
23
|
+
})
|
|
19
24
|
```
|
|
20
25
|
|
|
21
26
|
### 更新数据
|
|
22
|
-
1. 先用 `
|
|
23
|
-
2. 用 `
|
|
27
|
+
1. 先用 `manage_bitable_record(action=search)` 定位目标记录的 record_id
|
|
28
|
+
2. 用 `manage_bitable_record(action=update, records=[{record_id, fields:{...}}])` 更新
|
|
29
|
+
|
|
30
|
+
### 删除数据
|
|
31
|
+
- 用 `manage_bitable_record(action=delete, record_ids=[...])`(可批量)
|
|
24
32
|
|
|
25
33
|
## 示例
|
|
26
34
|
- `/table query appXxx` — 列出所有表格
|
|
@@ -28,5 +36,6 @@
|
|
|
28
36
|
- `/table create appXxx tblXxx {"状态":"进行中"}` — 创建记录
|
|
29
37
|
|
|
30
38
|
## 注意
|
|
31
|
-
- 需要知道 app_token(从多维表格 URL
|
|
39
|
+
- 需要知道 app_token(从多维表格 URL 中获取,或调 `manage_bitable_app(action=create)` 新建)
|
|
32
40
|
- 字段名必须与表格中的字段名完全匹配
|
|
41
|
+
- 字段创建/修改 (`manage_bitable_field`) 都必须传 `type`,即使只改 field_name
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// src/auth/cookie.js — cookie heartbeat scheduler.
|
|
2
|
+
//
|
|
3
|
+
// State lives on the LarkUserClient instance (this.cookieStr, this._heartbeatTimer).
|
|
4
|
+
// We expose start/stop functions that take `client` and mutate the timer field.
|
|
5
|
+
// Lifted out of clients/user.js for clarity; called only from there.
|
|
6
|
+
|
|
7
|
+
const HEARTBEAT_INTERVAL_MS = 4 * 60 * 60 * 1000; // 4 hours — sl_session has 12h max
|
|
8
|
+
|
|
9
|
+
function startHeartbeat(client) {
|
|
10
|
+
client._heartbeatTimer = setInterval(async () => {
|
|
11
|
+
try {
|
|
12
|
+
await client._getCsrfToken();
|
|
13
|
+
const { persistToConfig } = require('./credentials');
|
|
14
|
+
persistToConfig({ LARK_COOKIE: client.cookieStr });
|
|
15
|
+
console.error('[feishu-user-plugin] Cookie heartbeat: session refreshed and persisted');
|
|
16
|
+
} catch (e) {
|
|
17
|
+
console.error('[feishu-user-plugin] Cookie heartbeat failed:', e.message);
|
|
18
|
+
}
|
|
19
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
20
|
+
if (client._heartbeatTimer.unref) client._heartbeatTimer.unref();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function stopHeartbeat(client) {
|
|
24
|
+
if (client._heartbeatTimer) {
|
|
25
|
+
clearInterval(client._heartbeatTimer);
|
|
26
|
+
client._heartbeatTimer = null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = { startHeartbeat, stopHeartbeat, HEARTBEAT_INTERVAL_MS };
|
|
@@ -0,0 +1,399 @@
|
|
|
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
|
+
// --- Profile hints (v1.3.8) ---
|
|
331
|
+
//
|
|
332
|
+
// profileHints maps resourceKey → profileName, persisted in credentials.json.
|
|
333
|
+
// Used by src/auth/profile-router.js to remember which profile owns / has
|
|
334
|
+
// access to a given resource. Reads pass through; writes are atomic.
|
|
335
|
+
//
|
|
336
|
+
// resourceKey format: "<kind>:<token>", e.g. "doc:doccnXXX" or "chat:oc_zzz".
|
|
337
|
+
// Caller is responsible for canonicalising tokens (resolveDocId etc).
|
|
338
|
+
|
|
339
|
+
function getProfileHints() {
|
|
340
|
+
const f = _readFile();
|
|
341
|
+
if (!f) return {};
|
|
342
|
+
return { ...f.profileHints };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function setProfileHint(resourceKey, profileName) {
|
|
346
|
+
if (typeof resourceKey !== 'string' || !resourceKey) {
|
|
347
|
+
throw new Error('setProfileHint: resourceKey must be a non-empty string');
|
|
348
|
+
}
|
|
349
|
+
const f = _readFile();
|
|
350
|
+
if (!f) return false;
|
|
351
|
+
if (!f.profiles[profileName]) {
|
|
352
|
+
throw new Error(`setProfileHint: profile "${profileName}" not in credentials.json. Known: ${Object.keys(f.profiles).join(', ')}`);
|
|
353
|
+
}
|
|
354
|
+
if (f.profileHints[resourceKey] === profileName) return true;
|
|
355
|
+
f.profileHints[resourceKey] = profileName;
|
|
356
|
+
_atomicWriteJson(_credentialsPath(), f);
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function clearProfileHint(resourceKey) {
|
|
361
|
+
const f = _readFile();
|
|
362
|
+
if (!f) return false;
|
|
363
|
+
if (resourceKey === undefined) {
|
|
364
|
+
if (Object.keys(f.profileHints).length === 0) return true;
|
|
365
|
+
f.profileHints = {};
|
|
366
|
+
_atomicWriteJson(_credentialsPath(), f);
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
if (!(resourceKey in f.profileHints)) return false;
|
|
370
|
+
delete f.profileHints[resourceKey];
|
|
371
|
+
_atomicWriteJson(_credentialsPath(), f);
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// --- Re-exports for back-compat ---
|
|
376
|
+
|
|
377
|
+
module.exports = {
|
|
378
|
+
// canonical API
|
|
379
|
+
readCanonical,
|
|
380
|
+
getActiveProfileName,
|
|
381
|
+
listProfileNames,
|
|
382
|
+
getActiveProfileEnv,
|
|
383
|
+
setActiveProfile,
|
|
384
|
+
persistProfileUpdate,
|
|
385
|
+
migrate,
|
|
386
|
+
// profile hints (v1.3.8)
|
|
387
|
+
getProfileHints,
|
|
388
|
+
setProfileHint,
|
|
389
|
+
clearProfileHint,
|
|
390
|
+
// back-compat with src/config
|
|
391
|
+
readCredentials,
|
|
392
|
+
persistToConfig,
|
|
393
|
+
findMcpConfig: legacy.findMcpConfig,
|
|
394
|
+
writeNewConfig: legacy.writeNewConfig,
|
|
395
|
+
SERVER_NAMES: legacy.SERVER_NAMES,
|
|
396
|
+
// constants
|
|
397
|
+
SCHEMA_VERSION,
|
|
398
|
+
ENV_KEYS,
|
|
399
|
+
};
|