@wu529778790/open-im 1.9.4-beta.1 → 1.9.4-beta.11
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/dist/access/access-control.js +1 -1
- package/dist/adapters/claude-sdk-adapter.js +51 -30
- package/dist/adapters/claude-sdk-adapter.test.js +50 -0
- package/dist/commands/handler.d.ts +1 -1
- package/dist/commands/handler.js +2 -2
- package/dist/config/credentials.d.ts +27 -0
- package/dist/config/credentials.js +85 -0
- package/dist/config/file-io.d.ts +11 -0
- package/dist/config/file-io.js +176 -0
- package/dist/config/types.d.ts +177 -0
- package/dist/config/types.js +1 -0
- package/dist/config.d.ts +3 -185
- package/dist/config.js +9 -194
- package/dist/dingtalk/api.d.ts +21 -0
- package/dist/dingtalk/api.js +115 -0
- package/dist/dingtalk/client.d.ts +10 -23
- package/dist/dingtalk/client.js +57 -607
- package/dist/dingtalk/event-handler.d.ts +1 -0
- package/dist/dingtalk/event-handler.js +50 -67
- package/dist/dingtalk/message-sender.js +1 -2
- package/dist/dingtalk/streaming-card.d.ts +20 -0
- package/dist/dingtalk/streaming-card.js +300 -0
- package/dist/dingtalk/webhook.d.ts +12 -0
- package/dist/dingtalk/webhook.js +223 -0
- package/dist/feishu/cardkit-manager.js +24 -1
- package/dist/feishu/event-handler.d.ts +1 -0
- package/dist/feishu/event-handler.js +160 -270
- package/dist/index.js +99 -106
- package/dist/platform/create-event-context.d.ts +28 -0
- package/dist/platform/create-event-context.js +30 -0
- package/dist/platform/create-event-context.test.d.ts +1 -0
- package/dist/platform/create-event-context.test.js +91 -0
- package/dist/platform/handle-ai-request.d.ts +108 -0
- package/dist/platform/handle-ai-request.js +169 -0
- package/dist/platform/handle-ai-request.test.d.ts +1 -0
- package/dist/platform/handle-ai-request.test.js +349 -0
- package/dist/platform/handle-text-flow.d.ts +79 -0
- package/dist/platform/handle-text-flow.js +114 -0
- package/dist/platform/handle-text-flow.test.d.ts +1 -0
- package/dist/platform/handle-text-flow.test.js +237 -0
- package/dist/qq/client.js +13 -0
- package/dist/qq/event-handler.d.ts +1 -0
- package/dist/qq/event-handler.js +137 -106
- package/dist/queue/request-queue.d.ts +1 -1
- package/dist/queue/request-queue.js +14 -1
- package/dist/queue/request-queue.test.d.ts +1 -0
- package/dist/queue/request-queue.test.js +114 -0
- package/dist/session/session-manager.js +8 -4
- package/dist/session/session-manager.test.js +92 -4
- package/dist/setup.js +9 -1
- package/dist/shared/ai-task.d.ts +2 -0
- package/dist/shared/ai-task.js +12 -2
- package/dist/shared/ai-task.test.js +127 -0
- package/dist/shared/task-cleanup.d.ts +8 -0
- package/dist/shared/task-cleanup.js +30 -0
- package/dist/shared/task-cleanup.test.d.ts +1 -0
- package/dist/shared/task-cleanup.test.js +74 -0
- package/dist/telegram/event-handler.d.ts +1 -0
- package/dist/telegram/event-handler.js +162 -189
- package/dist/wework/event-handler.d.ts +2 -1
- package/dist/wework/event-handler.js +87 -95
- package/dist/wework/message-sender.js +0 -3
- package/dist/workbuddy/centrifuge-client.d.ts +13 -11
- package/dist/workbuddy/centrifuge-client.js +132 -115
- package/dist/workbuddy/client.js +10 -1
- package/dist/workbuddy/event-handler.d.ts +2 -1
- package/dist/workbuddy/event-handler.js +109 -64
- package/dist/workbuddy/message-sender.d.ts +0 -5
- package/dist/workbuddy/message-sender.js +0 -17
- package/dist/workbuddy/oauth.js +0 -2
- package/dist/workbuddy/types.d.ts +1 -1
- package/package.json +2 -5
- package/dist/qq/event-handler.test.js +0 -68
- package/dist/shared/retry.d.ts +0 -10
- package/dist/shared/retry.js +0 -26
- /package/dist/{qq/event-handler.test.d.ts → adapters/claude-sdk-adapter.test.d.ts} +0 -0
|
@@ -8,7 +8,7 @@ export class AccessControl {
|
|
|
8
8
|
}
|
|
9
9
|
isAllowed(userId) {
|
|
10
10
|
if (this.allowedUserIds.size === 0) {
|
|
11
|
-
log.warn(`Allowing user ${userId}
|
|
11
|
+
log.warn(`SECURITY: Allowing user ${userId} -- no whitelist configured. Set allowedUserIds in config or OPEN_IM_ALLOWED_USER_IDS env var to restrict access.`);
|
|
12
12
|
return true;
|
|
13
13
|
}
|
|
14
14
|
const allowed = this.allowedUserIds.has(userId);
|
|
@@ -9,8 +9,35 @@
|
|
|
9
9
|
* 认证:ANTHROPIC_API_KEY 或 CLAUDE_CODE_OAUTH_TOKEN
|
|
10
10
|
*/
|
|
11
11
|
import { unstable_v2_createSession, unstable_v2_resumeSession } from '@anthropic-ai/claude-agent-sdk';
|
|
12
|
+
import { existsSync, readFileSync } from 'fs';
|
|
13
|
+
import { homedir } from 'os';
|
|
14
|
+
import { join } from 'path';
|
|
12
15
|
import { createLogger } from '../logger.js';
|
|
13
16
|
const log = createLogger('ClaudeSDK');
|
|
17
|
+
function loadUserPluginSettings() {
|
|
18
|
+
try {
|
|
19
|
+
const settingsPath = join(homedir(), '.claude', 'settings.json');
|
|
20
|
+
if (!existsSync(settingsPath))
|
|
21
|
+
return null;
|
|
22
|
+
const content = readFileSync(settingsPath, 'utf-8');
|
|
23
|
+
const settings = JSON.parse(content);
|
|
24
|
+
const result = {};
|
|
25
|
+
if (settings.enabledPlugins)
|
|
26
|
+
result.enabledPlugins = settings.enabledPlugins;
|
|
27
|
+
if (settings.extraKnownMarketplaces)
|
|
28
|
+
result.extraKnownMarketplaces = settings.extraKnownMarketplaces;
|
|
29
|
+
if (Object.keys(result).length === 0)
|
|
30
|
+
return null;
|
|
31
|
+
log.info(`Loaded user plugin settings: plugins=[${Object.keys(result.enabledPlugins ?? {}).join(', ')}]`);
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
log.warn('Failed to read ~/.claude/settings.json for plugin config:', err);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Pre-load user plugin settings to cache Claude Code user preferences
|
|
40
|
+
loadUserPluginSettings();
|
|
14
41
|
// 存储所有活跃的 SDKSession 对象,key 为 sessionId
|
|
15
42
|
// 使用 Map 而不是 Set,因为我们需要通过 sessionId 获取 session
|
|
16
43
|
const activeSessions = new Map();
|
|
@@ -22,6 +49,8 @@ const sessionLastUsed = new Map();
|
|
|
22
49
|
const runningSessions = new Set();
|
|
23
50
|
const SESSION_IDLE_TTL_MS = 30 * 60 * 1000; // 30 分钟未使用则清理
|
|
24
51
|
const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 每 5 分钟检查一次
|
|
52
|
+
const MAX_ACTIVE_SESSIONS = 100;
|
|
53
|
+
let sessionSeq = 0;
|
|
25
54
|
const cleanupInterval = setInterval(() => {
|
|
26
55
|
const now = Date.now();
|
|
27
56
|
for (const [id, lastUsed] of sessionLastUsed) {
|
|
@@ -42,33 +71,17 @@ const cleanupInterval = setInterval(() => {
|
|
|
42
71
|
}
|
|
43
72
|
}, CLEANUP_INTERVAL_MS);
|
|
44
73
|
cleanupInterval.unref(); // 不阻止进程退出
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
continue; // 跳过正在运行任务的 session
|
|
57
|
-
if (now - lastUsed > SESSION_IDLE_TTL_MS) {
|
|
58
|
-
const s = activeSessions.get(id);
|
|
59
|
-
if (s) {
|
|
60
|
-
try {
|
|
61
|
-
s.close();
|
|
62
|
-
}
|
|
63
|
-
catch { /* ignore */ }
|
|
64
|
-
activeSessions.delete(id);
|
|
65
|
-
}
|
|
66
|
-
sessionLastUsed.delete(id);
|
|
67
|
-
log.info(`Lazy cleanup: idle session ${id} (unused ${Math.round((now - lastUsed) / 60000)}min)`);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
// Mutex to serialize process.chdir() calls across concurrent users
|
|
74
|
+
/**
|
|
75
|
+
* Serializes process.chdir() calls across concurrent users.
|
|
76
|
+
*
|
|
77
|
+
* process.chdir() is a process-wide global side effect — only one chdir can
|
|
78
|
+
* be active at a time. The SDK's createSession/resumeSession do not accept a
|
|
79
|
+
* `cwd` parameter, so we must chdir before calling them. This mutex ensures
|
|
80
|
+
* concurrent requests don't race on the working directory.
|
|
81
|
+
*
|
|
82
|
+
* **Limitation:** If the SDK ever supports a `cwd` option, this mutex should
|
|
83
|
+
* be removed entirely.
|
|
84
|
+
*/
|
|
72
85
|
let chdirMutex = Promise.resolve();
|
|
73
86
|
function withChdirMutex(fn) {
|
|
74
87
|
const previous = chdirMutex;
|
|
@@ -108,8 +121,10 @@ function isSessionCorruptionError(msg) {
|
|
|
108
121
|
* @returns SDKSession 对象和实际的 sessionId
|
|
109
122
|
*/
|
|
110
123
|
async function getOrCreateSession(sessionId, workDir, model, permissionMode) {
|
|
111
|
-
lazyCleanupIdleSessions();
|
|
112
124
|
const resolvedModel = model?.trim() || 'claude-opus-4-5';
|
|
125
|
+
if (activeSessions.size >= MAX_ACTIVE_SESSIONS) {
|
|
126
|
+
throw new Error(`Session pool is full (${MAX_ACTIVE_SESSIONS}). Cannot create new session.`);
|
|
127
|
+
}
|
|
113
128
|
const sessionOptions = {
|
|
114
129
|
model: resolvedModel,
|
|
115
130
|
permissionMode,
|
|
@@ -156,7 +171,7 @@ async function getOrCreateSession(sessionId, workDir, model, permissionMode) {
|
|
|
156
171
|
activeSessions.set(tempId, session);
|
|
157
172
|
sessionLastUsed.set(tempId, Date.now());
|
|
158
173
|
log.info(`Created new session (tempId: ${tempId})`);
|
|
159
|
-
return { session, sessionId: tempId };
|
|
174
|
+
return { session, sessionId: tempId, wasReused: false };
|
|
160
175
|
}
|
|
161
176
|
finally {
|
|
162
177
|
if (workDir && workDir !== originalCwd) {
|
|
@@ -260,7 +275,13 @@ export class ClaudeSDKAdapter {
|
|
|
260
275
|
}
|
|
261
276
|
// 获取实际的 sessionId(从 init 消息中)
|
|
262
277
|
if (isSystemInit(msg)) {
|
|
263
|
-
const
|
|
278
|
+
const initMsg = msg;
|
|
279
|
+
// 记录 session 加载的插件和技能
|
|
280
|
+
const pluginNames = initMsg.plugins?.map(p => p.name).join(', ') ?? 'none';
|
|
281
|
+
const skillCount = initMsg.skills?.length ?? 0;
|
|
282
|
+
const toolCount = initMsg.tools?.length ?? 0;
|
|
283
|
+
log.info(`[V2] Init: plugins=[${pluginNames}], skills=${skillCount}, tools=${toolCount}`);
|
|
284
|
+
const newSessionId = initMsg.session_id;
|
|
264
285
|
if (newSessionId && newSessionId !== actualSessionId) {
|
|
265
286
|
// 更新 sessionId 映射
|
|
266
287
|
// 清理 pending 临时 ID(actualSessionId 尚未赋值时用 pendingTempId)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
// Mock the logger before importing the adapter under test
|
|
3
|
+
vi.mock('../logger.js', () => ({
|
|
4
|
+
createLogger: () => ({
|
|
5
|
+
info: vi.fn(),
|
|
6
|
+
warn: vi.fn(),
|
|
7
|
+
error: vi.fn(),
|
|
8
|
+
debug: vi.fn(),
|
|
9
|
+
}),
|
|
10
|
+
}));
|
|
11
|
+
// Mock the Claude Agent SDK
|
|
12
|
+
vi.mock('@anthropic-ai/claude-agent-sdk', () => ({
|
|
13
|
+
unstable_v2_createSession: vi.fn(),
|
|
14
|
+
unstable_v2_resumeSession: vi.fn(),
|
|
15
|
+
}));
|
|
16
|
+
// Import after mocks are set up
|
|
17
|
+
import { ClaudeSDKAdapter } from './claude-sdk-adapter.js';
|
|
18
|
+
describe('ClaudeSDKAdapter', () => {
|
|
19
|
+
let adapter;
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
adapter = new ClaudeSDKAdapter();
|
|
22
|
+
});
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
// Clean up any active sessions/timers created during tests
|
|
25
|
+
ClaudeSDKAdapter.destroy();
|
|
26
|
+
});
|
|
27
|
+
it('implements the ToolAdapter interface', () => {
|
|
28
|
+
expect(adapter).toBeDefined();
|
|
29
|
+
expect(typeof adapter.toolId).toBe('string');
|
|
30
|
+
expect(typeof adapter.run).toBe('function');
|
|
31
|
+
});
|
|
32
|
+
it('has toolId set to claude-sdk', () => {
|
|
33
|
+
expect(adapter.toolId).toBe('claude-sdk');
|
|
34
|
+
});
|
|
35
|
+
it('has a run method that returns a RunHandle', () => {
|
|
36
|
+
const callbacks = {
|
|
37
|
+
onText: vi.fn(),
|
|
38
|
+
onComplete: vi.fn(),
|
|
39
|
+
onError: vi.fn(),
|
|
40
|
+
};
|
|
41
|
+
const handle = adapter.run('test prompt', undefined, '/tmp', callbacks);
|
|
42
|
+
expect(handle).toBeDefined();
|
|
43
|
+
expect(typeof handle.abort).toBe('function');
|
|
44
|
+
// Abort to clean up the background promise
|
|
45
|
+
handle.abort();
|
|
46
|
+
});
|
|
47
|
+
it('stop() (static destroy) does not throw', () => {
|
|
48
|
+
expect(() => ClaudeSDKAdapter.destroy()).not.toThrow();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -18,7 +18,7 @@ export type ClaudeRequestHandler = (userId: string, chatId: string, prompt: stri
|
|
|
18
18
|
export declare class CommandHandler {
|
|
19
19
|
private deps;
|
|
20
20
|
constructor(deps: CommandHandlerDeps);
|
|
21
|
-
dispatch(text: string, chatId: string, userId: string, platform: Platform,
|
|
21
|
+
dispatch(text: string, chatId: string, userId: string, platform: Platform, _handleClaudeRequest: ClaudeRequestHandler): Promise<boolean>;
|
|
22
22
|
private handleHelp;
|
|
23
23
|
private handleNew;
|
|
24
24
|
private handlePwd;
|
package/dist/commands/handler.js
CHANGED
|
@@ -9,7 +9,7 @@ export class CommandHandler {
|
|
|
9
9
|
constructor(deps) {
|
|
10
10
|
this.deps = deps;
|
|
11
11
|
}
|
|
12
|
-
async dispatch(text, chatId, userId, platform,
|
|
12
|
+
async dispatch(text, chatId, userId, platform, _handleClaudeRequest) {
|
|
13
13
|
const t = text.trim();
|
|
14
14
|
if (platform === 'telegram' && t === '/start') {
|
|
15
15
|
await this.deps.sender.sendTextReply(chatId, '欢迎使用 open-im AI CLI 桥接!\n\n发送消息与 AI 交互,输入 /help 查看帮助。');
|
|
@@ -75,7 +75,7 @@ export class CommandHandler {
|
|
|
75
75
|
await this.deps.sender.sendTextReply(chatId, lines.join('\n'));
|
|
76
76
|
return true;
|
|
77
77
|
}
|
|
78
|
-
async handleCd(chatId, userId, dir,
|
|
78
|
+
async handleCd(chatId, userId, dir, _platform) {
|
|
79
79
|
// 如果 dir 为空,显示目录选择界面
|
|
80
80
|
if (!dir) {
|
|
81
81
|
const currentDir = this.deps.sessionManager.getWorkDir(userId);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { FileConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolves a single credential value using the standard priority chain:
|
|
4
|
+
* environment variable → platform file config → legacy file config.
|
|
5
|
+
*/
|
|
6
|
+
export declare function resolveCredential(envKey: string, fileValue?: string, legacyFileValue?: string): string | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* Generic per-platform credential resolution.
|
|
9
|
+
* Returns all resolved credentials and whether the platform should be enabled.
|
|
10
|
+
*/
|
|
11
|
+
export interface ResolvedPlatform {
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
credentials: Record<string, string | undefined>;
|
|
14
|
+
}
|
|
15
|
+
export declare function resolvePlatformCredentials(envKeys: Record<string, string>, fileValues: Record<string, string | undefined>, legacyValues: Record<string, string | undefined>, requiredKeys: string[], enabledFlag?: boolean): ResolvedPlatform;
|
|
16
|
+
/**
|
|
17
|
+
* Extract WorkBuddy credentials, with legacy platforms.wechat migration support.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveWorkBuddyFileConfig(fileConfig: FileConfig): NonNullable<FileConfig['platforms']>['workbuddy'] | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Check if a CLI tool is available at the given path or on PATH.
|
|
22
|
+
*/
|
|
23
|
+
export declare function checkCliAvailable(cliPath: string, toolName: string): void;
|
|
24
|
+
/**
|
|
25
|
+
* Resolve Windows-specific CLI path for npm global installs.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveWindowsCliPath(cliName: string, configuredPath: string): string;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { accessSync, constants } from 'node:fs';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { join, isAbsolute } from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* Resolves a single credential value using the standard priority chain:
|
|
6
|
+
* environment variable → platform file config → legacy file config.
|
|
7
|
+
*/
|
|
8
|
+
export function resolveCredential(envKey, fileValue, legacyFileValue) {
|
|
9
|
+
return process.env[envKey] ?? fileValue ?? legacyFileValue;
|
|
10
|
+
}
|
|
11
|
+
export function resolvePlatformCredentials(envKeys, fileValues, legacyValues, requiredKeys, enabledFlag) {
|
|
12
|
+
const credentials = {};
|
|
13
|
+
for (const [name, envKey] of Object.entries(envKeys)) {
|
|
14
|
+
credentials[name] = resolveCredential(envKey, fileValues[name], legacyValues[name]);
|
|
15
|
+
}
|
|
16
|
+
const hasRequired = requiredKeys.every((key) => credentials[key] !== undefined && credentials[key] !== '');
|
|
17
|
+
return {
|
|
18
|
+
enabled: hasRequired && enabledFlag !== false,
|
|
19
|
+
credentials,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Extract WorkBuddy credentials, with legacy platforms.wechat migration support.
|
|
24
|
+
*/
|
|
25
|
+
export function resolveWorkBuddyFileConfig(fileConfig) {
|
|
26
|
+
const direct = fileConfig.platforms?.workbuddy;
|
|
27
|
+
if (direct)
|
|
28
|
+
return direct;
|
|
29
|
+
const legacyWechat = fileConfig.platforms?.wechat;
|
|
30
|
+
if (legacyWechat?.workbuddyAccessToken && legacyWechat?.workbuddyRefreshToken) {
|
|
31
|
+
return {
|
|
32
|
+
accessToken: legacyWechat.workbuddyAccessToken,
|
|
33
|
+
refreshToken: legacyWechat.workbuddyRefreshToken,
|
|
34
|
+
userId: legacyWechat.userId,
|
|
35
|
+
baseUrl: legacyWechat.workbuddyBaseUrl,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Check if a CLI tool is available at the given path or on PATH.
|
|
42
|
+
*/
|
|
43
|
+
export function checkCliAvailable(cliPath, toolName) {
|
|
44
|
+
if (isAbsolute(cliPath) || cliPath.includes('/') || cliPath.includes('\\')) {
|
|
45
|
+
try {
|
|
46
|
+
accessSync(cliPath, constants.F_OK);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
throw new Error(`${toolName} CLI not found at: ${cliPath}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
const checkCommand = process.platform === 'win32' ? 'where' : 'which';
|
|
54
|
+
try {
|
|
55
|
+
execFileSync(checkCommand, [cliPath], {
|
|
56
|
+
stdio: 'pipe',
|
|
57
|
+
windowsHide: process.platform === 'win32',
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
throw new Error(`${toolName} CLI not found on PATH: ${cliPath}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolve Windows-specific CLI path for npm global installs.
|
|
67
|
+
*/
|
|
68
|
+
export function resolveWindowsCliPath(cliName, configuredPath) {
|
|
69
|
+
if (process.platform !== 'win32' || configuredPath !== cliName)
|
|
70
|
+
return configuredPath;
|
|
71
|
+
const npmPaths = [
|
|
72
|
+
join(process.env.APPDATA || '', 'npm', `${cliName}.cmd`),
|
|
73
|
+
join(process.env.LOCALAPPDATA || '', 'npm', `${cliName}.cmd`),
|
|
74
|
+
];
|
|
75
|
+
for (const p of npmPaths) {
|
|
76
|
+
try {
|
|
77
|
+
accessSync(p, constants.F_OK);
|
|
78
|
+
return p;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
/* try next */
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return configuredPath;
|
|
85
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AiCommand, FileConfig } from './types.js';
|
|
2
|
+
export declare const CONFIG_PATH: string;
|
|
3
|
+
export declare const CODEX_AUTH_PATHS: string[];
|
|
4
|
+
export declare function loadFileConfig(): FileConfig;
|
|
5
|
+
export declare function saveFileConfig(raw: FileConfig): void;
|
|
6
|
+
export declare function getClaudeConfigHome(): string;
|
|
7
|
+
export declare function loadClaudeSettingsEnv(): Record<string, string>;
|
|
8
|
+
export declare function saveClaudeSettingsEnv(env: Record<string, string>): void;
|
|
9
|
+
export declare function normalizeAiCommand(value: unknown, fallback: AiCommand): AiCommand;
|
|
10
|
+
export declare function hasCodexAuth(): boolean;
|
|
11
|
+
export declare function parseCommaSeparated(value: string): string[];
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync, chmodSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { createLogger } from '../logger.js';
|
|
5
|
+
import { APP_HOME } from '../constants.js';
|
|
6
|
+
const log = createLogger('config');
|
|
7
|
+
export const CONFIG_PATH = join(APP_HOME, 'config.json');
|
|
8
|
+
export const CODEX_AUTH_PATHS = [
|
|
9
|
+
join(homedir(), '.codex', 'auth.json'),
|
|
10
|
+
join(homedir(), '.config', 'codex', 'auth.json'),
|
|
11
|
+
join(homedir(), 'AppData', 'Roaming', 'codex', 'auth.json'),
|
|
12
|
+
];
|
|
13
|
+
const OLD_ROOT_KEYS = [
|
|
14
|
+
'claudeWorkDir',
|
|
15
|
+
'claudeTimeoutMs',
|
|
16
|
+
'claudeModel',
|
|
17
|
+
];
|
|
18
|
+
const AI_COMMANDS = ['claude', 'codex', 'codebuddy'];
|
|
19
|
+
// Config cache with mtime tracking
|
|
20
|
+
let cachedConfig = null;
|
|
21
|
+
let cachedClaudeEnv = null;
|
|
22
|
+
function hasOldConfigFormat(raw) {
|
|
23
|
+
const hasOld = OLD_ROOT_KEYS.some((k) => raw[k] !== undefined && raw[k] !== null);
|
|
24
|
+
const hasNew = raw.tools && typeof raw.tools === 'object' && raw.tools.claude;
|
|
25
|
+
return !!hasOld && !hasNew;
|
|
26
|
+
}
|
|
27
|
+
function migrateToNewConfigFormat(raw) {
|
|
28
|
+
const tools = raw.tools || {};
|
|
29
|
+
const tc = tools.claude || {};
|
|
30
|
+
const tcod = tools.codex || {};
|
|
31
|
+
const tcb = tools.codebuddy || {};
|
|
32
|
+
const migrated = { ...raw };
|
|
33
|
+
migrated.tools = {
|
|
34
|
+
claude: {
|
|
35
|
+
...tc,
|
|
36
|
+
workDir: tc.workDir ?? raw.claudeWorkDir ?? process.cwd(),
|
|
37
|
+
proxy: tc.proxy,
|
|
38
|
+
},
|
|
39
|
+
codex: {
|
|
40
|
+
...tcod,
|
|
41
|
+
cliPath: tcod.cliPath ?? 'codex',
|
|
42
|
+
workDir: tcod.workDir ?? raw.claudeWorkDir ?? process.cwd(),
|
|
43
|
+
proxy: tcod.proxy,
|
|
44
|
+
},
|
|
45
|
+
codebuddy: {
|
|
46
|
+
...tcb,
|
|
47
|
+
cliPath: tcb.cliPath ?? 'codebuddy',
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
for (const k of OLD_ROOT_KEYS) {
|
|
51
|
+
delete migrated[k];
|
|
52
|
+
}
|
|
53
|
+
return migrated;
|
|
54
|
+
}
|
|
55
|
+
export function loadFileConfig() {
|
|
56
|
+
try {
|
|
57
|
+
if (!existsSync(CONFIG_PATH))
|
|
58
|
+
return {};
|
|
59
|
+
const stats = statSync(CONFIG_PATH);
|
|
60
|
+
const currentMtime = stats.mtimeMs;
|
|
61
|
+
if (cachedConfig && cachedConfig.mtime === currentMtime) {
|
|
62
|
+
return cachedConfig.config;
|
|
63
|
+
}
|
|
64
|
+
const raw = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
|
|
65
|
+
if (!raw || typeof raw !== 'object')
|
|
66
|
+
return {};
|
|
67
|
+
if (hasOldConfigFormat(raw)) {
|
|
68
|
+
const migrated = migrateToNewConfigFormat(raw);
|
|
69
|
+
const dir = dirname(CONFIG_PATH);
|
|
70
|
+
if (!existsSync(dir))
|
|
71
|
+
mkdirSync(dir, { recursive: true });
|
|
72
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(migrated, null, 2), 'utf-8');
|
|
73
|
+
try {
|
|
74
|
+
chmodSync(CONFIG_PATH, 0o600);
|
|
75
|
+
}
|
|
76
|
+
catch { /* ignore */ }
|
|
77
|
+
cachedConfig = { config: migrated, mtime: currentMtime };
|
|
78
|
+
return migrated;
|
|
79
|
+
}
|
|
80
|
+
cachedConfig = { config: raw, mtime: currentMtime };
|
|
81
|
+
return raw;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return {};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export function saveFileConfig(raw) {
|
|
88
|
+
const dir = dirname(CONFIG_PATH);
|
|
89
|
+
if (!existsSync(dir))
|
|
90
|
+
mkdirSync(dir, { recursive: true });
|
|
91
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(raw, null, 2), 'utf-8');
|
|
92
|
+
cachedConfig = null;
|
|
93
|
+
}
|
|
94
|
+
export function getClaudeConfigHome() {
|
|
95
|
+
return process.env.HOME || process.env.USERPROFILE || homedir();
|
|
96
|
+
}
|
|
97
|
+
export function loadClaudeSettingsEnv() {
|
|
98
|
+
const home = getClaudeConfigHome();
|
|
99
|
+
const paths = [
|
|
100
|
+
join(home, '.claude', 'settings.json'),
|
|
101
|
+
join(home, '.claude.json'),
|
|
102
|
+
];
|
|
103
|
+
for (const p of paths) {
|
|
104
|
+
try {
|
|
105
|
+
if (existsSync(p)) {
|
|
106
|
+
const stats = statSync(p);
|
|
107
|
+
const currentMtime = stats.mtimeMs;
|
|
108
|
+
if (cachedClaudeEnv && cachedClaudeEnv.mtime === currentMtime && cachedClaudeEnv.env) {
|
|
109
|
+
return cachedClaudeEnv.env;
|
|
110
|
+
}
|
|
111
|
+
const raw = JSON.parse(readFileSync(p, 'utf-8'));
|
|
112
|
+
const env = raw?.env;
|
|
113
|
+
if (env && typeof env === 'object') {
|
|
114
|
+
const result = {};
|
|
115
|
+
for (const [k, v] of Object.entries(env)) {
|
|
116
|
+
if (v != null && typeof k === 'string') {
|
|
117
|
+
result[k] = String(v);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
cachedClaudeEnv = { env: result, mtime: currentMtime };
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
/* file not found or parse error, try next path */
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return {};
|
|
130
|
+
}
|
|
131
|
+
export function saveClaudeSettingsEnv(env) {
|
|
132
|
+
const home = getClaudeConfigHome();
|
|
133
|
+
const claudeSettingsPath = join(home, '.claude', 'settings.json');
|
|
134
|
+
const claudeDir = join(home, '.claude');
|
|
135
|
+
try {
|
|
136
|
+
if (!existsSync(claudeDir)) {
|
|
137
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
138
|
+
}
|
|
139
|
+
let existing = {};
|
|
140
|
+
if (existsSync(claudeSettingsPath)) {
|
|
141
|
+
try {
|
|
142
|
+
existing = JSON.parse(readFileSync(claudeSettingsPath, 'utf-8'));
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// file format error, start fresh
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
existing.env = { ...existing.env, ...env };
|
|
149
|
+
writeFileSync(claudeSettingsPath, JSON.stringify(existing, null, 2), 'utf-8');
|
|
150
|
+
cachedClaudeEnv = null;
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
log.error('Failed to save Claude settings:', error);
|
|
154
|
+
throw new Error(`Failed to save Claude settings: ${error instanceof Error ? error.message : String(error)}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export function normalizeAiCommand(value, fallback) {
|
|
158
|
+
return typeof value === 'string' && AI_COMMANDS.includes(value)
|
|
159
|
+
? value
|
|
160
|
+
: fallback;
|
|
161
|
+
}
|
|
162
|
+
export function hasCodexAuth() {
|
|
163
|
+
if (process.env.OPENAI_API_KEY)
|
|
164
|
+
return true;
|
|
165
|
+
return CODEX_AUTH_PATHS.some((p) => {
|
|
166
|
+
try {
|
|
167
|
+
return existsSync(p) && readFileSync(p, 'utf-8').trim().length > 0;
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
export function parseCommaSeparated(value) {
|
|
175
|
+
return value.split(',').map((s) => s.trim()).filter(Boolean);
|
|
176
|
+
}
|