@wu529778790/open-im 1.2.4-beta.3 → 1.3.1-beta.0

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 CHANGED
@@ -55,6 +55,7 @@ open-im start
55
55
  | `ALLOWED_USER_IDS` | 白名单(逗号分隔,空=所有人) |
56
56
  | `CLAUDE_WORK_DIR` | 工作目录,默认当前目录 |
57
57
  | `ALLOWED_BASE_DIRS` | 允许访问的目录(逗号分隔) |
58
+ | `CURSOR_API_KEY` | Cursor Agent API Key(使用 cursor 时必填,或先运行 agent login) |
58
59
 
59
60
  ### Claude API 配置
60
61
 
@@ -160,3 +161,5 @@ EOF
160
161
  **飞书卡片报错**:未配置卡片回调,使用命令替代:`/mode ask`、`/mode yolo`
161
162
 
162
163
  **企业微信收不到通知**:需先发一条消息给机器人,才能接收启动通知
164
+
165
+ **Cursor 报 Authentication required**:需先认证。方式 1:在终端运行 `agent login`;方式 2:在 `~/.open-im/config.json` 的 `env` 中添加 `"CURSOR_API_KEY": "你的 API Key"`
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Cursor Adapter - 通过 Cursor Agent CLI 执行任务
3
+ * 需要预先安装: curl https://cursor.com/install -fsSL | bash
4
+ */
5
+ import type { ToolAdapter, RunCallbacks, RunOptions, RunHandle } from './tool-adapter.interface.js';
6
+ export declare class CursorAdapter implements ToolAdapter {
7
+ private cliPath;
8
+ readonly toolId = "cursor";
9
+ constructor(cliPath: string);
10
+ run(prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: RunOptions): RunHandle;
11
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Cursor Adapter - 通过 Cursor Agent CLI 执行任务
3
+ * 需要预先安装: curl https://cursor.com/install -fsSL | bash
4
+ */
5
+ import { runCursor } from '../cursor/cli-runner.js';
6
+ import { createLogger } from '../logger.js';
7
+ const log = createLogger('CursorAdapter');
8
+ export class CursorAdapter {
9
+ cliPath;
10
+ toolId = 'cursor';
11
+ constructor(cliPath) {
12
+ this.cliPath = cliPath;
13
+ }
14
+ run(prompt, sessionId, workDir, callbacks, options) {
15
+ const opts = {
16
+ skipPermissions: options?.skipPermissions,
17
+ permissionMode: options?.permissionMode,
18
+ timeoutMs: options?.timeoutMs,
19
+ model: options?.model,
20
+ chatId: options?.chatId,
21
+ hookPort: options?.hookPort,
22
+ };
23
+ return runCursor(this.cliPath, prompt, sessionId, workDir, {
24
+ onText: callbacks.onText,
25
+ onThinking: callbacks.onThinking,
26
+ onToolUse: callbacks.onToolUse,
27
+ onComplete: (raw) => {
28
+ const result = {
29
+ success: raw.success,
30
+ result: raw.result,
31
+ accumulated: raw.accumulated,
32
+ cost: raw.cost,
33
+ durationMs: raw.durationMs,
34
+ model: raw.model,
35
+ numTurns: raw.numTurns,
36
+ toolStats: raw.toolStats,
37
+ };
38
+ callbacks.onComplete(result);
39
+ },
40
+ onError: (err) => {
41
+ const msg = typeof err === 'string' ? err : String(err);
42
+ const friendly = msg.includes('Authentication required') || msg.includes('agent login')
43
+ ? 'Cursor 需要先登录。请在终端运行 agent login,或在 ~/.open-im/config.json 的 env 中添加 "CURSOR_API_KEY"。'
44
+ : msg;
45
+ callbacks.onError(friendly);
46
+ },
47
+ onSessionId: callbacks.onSessionId,
48
+ }, opts);
49
+ }
50
+ }
@@ -1,5 +1,6 @@
1
1
  import { ClaudeAdapter } from './claude-adapter.js';
2
2
  import { ClaudeSDKAdapter } from './claude-sdk-adapter.js';
3
+ import { CursorAdapter } from './cursor-adapter.js';
3
4
  const adapters = new Map();
4
5
  export function initAdapters(config) {
5
6
  adapters.clear();
@@ -16,6 +17,10 @@ export function initAdapters(config) {
16
17
  }));
17
18
  }
18
19
  }
20
+ else if (config.aiCommand === 'cursor') {
21
+ console.log('🖱️ 使用 Cursor Agent CLI 适配器');
22
+ adapters.set('cursor', new CursorAdapter(config.cursorCliPath));
23
+ }
19
24
  }
20
25
  export function getAdapter(aiCommand) {
21
26
  return adapters.get(aiCommand);
@@ -31,7 +31,7 @@ export declare class CommandHandler {
31
31
  private handleCd;
32
32
  private handleAllow;
33
33
  private handleDeny;
34
- private getClaudeVersion;
34
+ private getAiVersion;
35
35
  }
36
36
  /**
37
37
  * 列出目录并返回目录信息
@@ -107,7 +107,7 @@ export class CommandHandler {
107
107
  return true;
108
108
  }
109
109
  async handleStatus(chatId, userId) {
110
- const version = await this.getClaudeVersion();
110
+ const version = await this.getAiVersion();
111
111
  const workDir = this.deps.sessionManager.getWorkDir(userId);
112
112
  const convId = this.deps.sessionManager.getConvId(userId);
113
113
  const sessionId = this.deps.sessionManager.getSessionIdForConv(userId, convId);
@@ -166,9 +166,10 @@ export class CommandHandler {
166
166
  }
167
167
  return true;
168
168
  }
169
- getClaudeVersion() {
169
+ getAiVersion() {
170
+ const cmd = this.deps.config.aiCommand === 'cursor' ? this.deps.config.cursorCliPath : this.deps.config.claudeCliPath;
170
171
  return new Promise((resolve) => {
171
- execFile(this.deps.config.claudeCliPath, ['--version'], { timeout: 5000 }, (err, stdout) => {
172
+ execFile(cmd, ['--version'], { timeout: 5000 }, (err, stdout) => {
172
173
  resolve(err ? '未知' : (stdout?.toString().trim() || '未知'));
173
174
  });
174
175
  });
package/dist/config.d.ts CHANGED
@@ -24,6 +24,7 @@ export interface Config {
24
24
  weworkAllowedUserIds: string[];
25
25
  aiCommand: AiCommand;
26
26
  claudeCliPath: string;
27
+ cursorCliPath: string;
27
28
  claudeWorkDir: string;
28
29
  allowedBaseDirs: string[];
29
30
  claudeSkipPermissions: boolean;
package/dist/config.js CHANGED
@@ -183,6 +183,18 @@ export function loadConfig() {
183
183
  // 5. AI / 工作目录 / 安全配置
184
184
  const aiCommand = (process.env.AI_COMMAND ?? file.aiCommand ?? 'claude');
185
185
  const claudeCliPath = process.env.CLAUDE_CLI_PATH ?? file.claudeCliPath ?? 'claude';
186
+ let cursorCliPath = process.env.CURSOR_CLI_PATH ?? file.cursorCliPath ?? 'agent';
187
+ // Windows: spawn 无法解析 .cmd,需使用完整路径(Cursor 默认安装在 %LOCALAPPDATA%\cursor-agent\agent.cmd)
188
+ if (process.platform === 'win32' && cursorCliPath === 'agent') {
189
+ const winAgentPath = join(process.env.LOCALAPPDATA || '', 'cursor-agent', 'agent.cmd');
190
+ try {
191
+ accessSync(winAgentPath, constants.F_OK);
192
+ cursorCliPath = winAgentPath;
193
+ }
194
+ catch {
195
+ /* 使用默认 agent,由后续 where 校验 */
196
+ }
197
+ }
186
198
  const claudeWorkDir = process.env.CLAUDE_WORK_DIR ?? file.claudeWorkDir ?? process.cwd();
187
199
  const allowedBaseDirs = process.env.ALLOWED_BASE_DIRS !== undefined
188
200
  ? parseCommaSeparated(process.env.ALLOWED_BASE_DIRS)
@@ -235,7 +247,46 @@ export function loadConfig() {
235
247
  throw new Error(errorMsg);
236
248
  }
237
249
  }
238
- // 7. 校验 Claude CLI(SDK 模式不需要 CLI)
250
+ // 7. 校验 Cursor CLI(使用 cursor 时)
251
+ if (aiCommand === 'cursor') {
252
+ if (isAbsolute(cursorCliPath) || cursorCliPath.includes('/') || cursorCliPath.includes('\\')) {
253
+ try {
254
+ accessSync(cursorCliPath, constants.F_OK);
255
+ }
256
+ catch {
257
+ throw new Error(`Cursor CLI 不可执行: ${cursorCliPath}`);
258
+ }
259
+ }
260
+ else {
261
+ const checkCommand = process.platform === 'win32' ? 'where' : 'which';
262
+ try {
263
+ execFileSync(checkCommand, [cursorCliPath], { stdio: 'pipe' });
264
+ }
265
+ catch {
266
+ const installGuide = [
267
+ '',
268
+ '━━━ Cursor CLI 未安装 ━━━',
269
+ '',
270
+ '使用 Cursor 需要先安装 Cursor Agent CLI。',
271
+ '',
272
+ '安装方法:',
273
+ '',
274
+ ' macOS/Linux: curl https://cursor.com/install -fsSL | bash',
275
+ ' Windows: irm \'https://cursor.com/install?win32=true\' | iex',
276
+ '',
277
+ '安装后运行 agent --version 验证。',
278
+ '',
279
+ ].join('\n');
280
+ throw new Error(installGuide);
281
+ }
282
+ }
283
+ // 提示 Cursor 认证:需 agent login 或 CURSOR_API_KEY
284
+ if (!process.env.CURSOR_API_KEY) {
285
+ console.warn('\n⚠ Cursor 模式:未检测到 CURSOR_API_KEY。首次使用请先运行 agent login,\n' +
286
+ ' 或在 ~/.open-im/config.json 的 env 中添加 "CURSOR_API_KEY": "你的 API Key"。\n');
287
+ }
288
+ }
289
+ // 8. 校验 Claude CLI(SDK 模式不需要 CLI)
239
290
  if (aiCommand === 'claude' && !useSdkMode) {
240
291
  if (isAbsolute(claudeCliPath) || claudeCliPath.includes('/') || claudeCliPath.includes('\\')) {
241
292
  try {
@@ -352,6 +403,7 @@ export function loadConfig() {
352
403
  weworkAllowedUserIds,
353
404
  aiCommand,
354
405
  claudeCliPath,
406
+ cursorCliPath,
355
407
  claudeWorkDir,
356
408
  allowedBaseDirs,
357
409
  claudeSkipPermissions,
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Cursor CLI Runner - 解析 Cursor Agent 的 stream-json 输出
3
+ * 参考: https://cursor.com/docs/cli/reference/output-format
4
+ */
5
+ export interface CursorRunCallbacks {
6
+ onText: (accumulated: string) => void;
7
+ onThinking?: (accumulated: string) => void;
8
+ onToolUse?: (toolName: string, toolInput?: Record<string, unknown>) => void;
9
+ onComplete: (result: {
10
+ success: boolean;
11
+ result: string;
12
+ accumulated: string;
13
+ cost: number;
14
+ durationMs: number;
15
+ model?: string;
16
+ numTurns: number;
17
+ toolStats: Record<string, number>;
18
+ }) => void;
19
+ onError: (error: string) => void;
20
+ onSessionId?: (sessionId: string) => void;
21
+ }
22
+ export interface CursorRunOptions {
23
+ skipPermissions?: boolean;
24
+ permissionMode?: 'default' | 'acceptEdits' | 'plan';
25
+ timeoutMs?: number;
26
+ model?: string;
27
+ chatId?: string;
28
+ hookPort?: number;
29
+ }
30
+ export interface CursorRunHandle {
31
+ abort: () => void;
32
+ }
33
+ export declare function runCursor(cliPath: string, prompt: string, sessionId: string | undefined, workDir: string, callbacks: CursorRunCallbacks, options?: CursorRunOptions): CursorRunHandle;
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Cursor CLI Runner - 解析 Cursor Agent 的 stream-json 输出
3
+ * 参考: https://cursor.com/docs/cli/reference/output-format
4
+ */
5
+ import { spawn } from 'node:child_process';
6
+ import { createInterface } from 'node:readline';
7
+ import { createLogger } from '../logger.js';
8
+ const log = createLogger('CursorCli');
9
+ /** 从 Cursor tool_call 事件提取工具名和参数 */
10
+ function extractToolFromCursorEvent(event) {
11
+ const toolCall = event.tool_call;
12
+ if (!toolCall || typeof toolCall !== 'object')
13
+ return null;
14
+ const keys = Object.keys(toolCall).filter((k) => k !== 'result');
15
+ if (keys.length === 0)
16
+ return null;
17
+ const key = keys[0];
18
+ const val = toolCall[key];
19
+ if (!val)
20
+ return null;
21
+ let name = key;
22
+ if (key === 'readToolCall')
23
+ name = 'Read';
24
+ else if (key === 'writeToolCall')
25
+ name = 'Write';
26
+ else if (key === 'editToolCall')
27
+ name = 'Edit';
28
+ else if (key === 'bashToolCall')
29
+ name = 'Bash';
30
+ else if (key === 'grepToolCall')
31
+ name = 'Grep';
32
+ else if (key === 'globToolCall')
33
+ name = 'Glob';
34
+ else if (key === 'webSearchToolCall')
35
+ name = 'WebSearch';
36
+ else if (key === 'webFetchToolCall')
37
+ name = 'WebFetch';
38
+ else if (key === 'function') {
39
+ const fn = val;
40
+ name = fn.name ?? 'unknown';
41
+ try {
42
+ const input = fn.arguments ? JSON.parse(fn.arguments) : undefined;
43
+ return { name, input };
44
+ }
45
+ catch {
46
+ return { name };
47
+ }
48
+ }
49
+ const args = val.args;
50
+ return { name, input: args };
51
+ }
52
+ export function runCursor(cliPath, prompt, sessionId, workDir, callbacks, options) {
53
+ const args = ['-p', '--output-format', 'stream-json', '--stream-partial-output'];
54
+ if (options?.skipPermissions) {
55
+ args.push('--force');
56
+ }
57
+ else if (options?.permissionMode === 'plan') {
58
+ args.push('--plan');
59
+ }
60
+ else if (options?.permissionMode === 'acceptEdits') {
61
+ args.push('--trust');
62
+ }
63
+ if (options?.model)
64
+ args.push('--model', options.model);
65
+ if (sessionId)
66
+ args.push('--resume', sessionId);
67
+ args.push('--workspace', workDir);
68
+ args.push('--', prompt);
69
+ const env = {};
70
+ for (const [k, v] of Object.entries(process.env)) {
71
+ if (v !== undefined)
72
+ env[k] = v;
73
+ }
74
+ if (options?.chatId)
75
+ env.CC_IM_CHAT_ID = options.chatId;
76
+ if (options?.hookPort)
77
+ env.CC_IM_HOOK_PORT = String(options.hookPort);
78
+ log.info(`Spawning Cursor CLI: path=${cliPath}, cwd=${workDir}, session=${sessionId ?? 'new'}`);
79
+ // Windows: .cmd 需通过 cmd.exe 执行,否则 spawn 报 ENOENT
80
+ const isCmd = process.platform === 'win32' && /\.cmd$/i.test(cliPath);
81
+ const spawnCmd = isCmd ? 'cmd.exe' : cliPath;
82
+ const spawnArgs = isCmd ? ['/c', cliPath, ...args] : args;
83
+ const child = spawn(spawnCmd, spawnArgs, {
84
+ cwd: workDir,
85
+ stdio: ['ignore', 'pipe', 'pipe'],
86
+ env,
87
+ windowsHide: process.platform === 'win32',
88
+ });
89
+ let accumulated = '';
90
+ let completed = false;
91
+ let model = '';
92
+ const toolStats = {};
93
+ const MAX_TIMEOUT = 2_147_483_647;
94
+ const timeoutMs = options?.timeoutMs && options.timeoutMs > 0
95
+ ? Math.min(options.timeoutMs, MAX_TIMEOUT)
96
+ : 0;
97
+ let timeoutHandle = null;
98
+ if (timeoutMs > 0) {
99
+ timeoutHandle = setTimeout(() => {
100
+ if (!completed && !child.killed) {
101
+ completed = true;
102
+ log.warn(`Cursor CLI timeout after ${timeoutMs}ms, killing pid=${child.pid}`);
103
+ child.kill('SIGTERM');
104
+ callbacks.onError(`执行超时(${timeoutMs}ms),已终止进程`);
105
+ }
106
+ }, timeoutMs);
107
+ }
108
+ const MAX_STDERR_HEAD = 4 * 1024;
109
+ const MAX_STDERR_TAIL = 6 * 1024;
110
+ let stderrHead = '';
111
+ let stderrTail = '';
112
+ let stderrTotal = 0;
113
+ let stderrHeadFull = false;
114
+ child.stderr?.on('data', (chunk) => {
115
+ const text = chunk.toString();
116
+ stderrTotal += text.length;
117
+ if (!stderrHeadFull) {
118
+ const room = MAX_STDERR_HEAD - stderrHead.length;
119
+ if (room > 0) {
120
+ stderrHead += text.slice(0, room);
121
+ if (stderrHead.length >= MAX_STDERR_HEAD)
122
+ stderrHeadFull = true;
123
+ }
124
+ }
125
+ stderrTail += text;
126
+ if (stderrTail.length > MAX_STDERR_TAIL) {
127
+ stderrTail = stderrTail.slice(-MAX_STDERR_TAIL);
128
+ }
129
+ });
130
+ const rl = createInterface({ input: child.stdout });
131
+ rl.on('line', (line) => {
132
+ const trimmed = line.trim();
133
+ if (!trimmed)
134
+ return;
135
+ let event;
136
+ try {
137
+ event = JSON.parse(trimmed);
138
+ }
139
+ catch {
140
+ return;
141
+ }
142
+ const type = event.type;
143
+ if (type === 'system' && event.subtype === 'init') {
144
+ model = event.model ?? '';
145
+ const sid = event.session_id;
146
+ if (sid)
147
+ callbacks.onSessionId?.(sid);
148
+ return;
149
+ }
150
+ if (type === 'assistant') {
151
+ const msg = event.message;
152
+ const content = msg?.content;
153
+ if (Array.isArray(content)) {
154
+ for (const block of content) {
155
+ if (block?.type === 'text' && block.text) {
156
+ accumulated += block.text;
157
+ callbacks.onText(accumulated);
158
+ }
159
+ }
160
+ }
161
+ return;
162
+ }
163
+ if (type === 'tool_call') {
164
+ const subtype = event.subtype;
165
+ if (subtype === 'started') {
166
+ const tool = extractToolFromCursorEvent(event);
167
+ if (tool) {
168
+ toolStats[tool.name] = (toolStats[tool.name] || 0) + 1;
169
+ callbacks.onToolUse?.(tool.name, tool.input);
170
+ }
171
+ }
172
+ return;
173
+ }
174
+ if (type === 'result' && event.subtype === 'success') {
175
+ completed = true;
176
+ if (timeoutHandle)
177
+ clearTimeout(timeoutHandle);
178
+ const result = event.result ?? '';
179
+ if (!accumulated && result)
180
+ accumulated = result;
181
+ callbacks.onComplete({
182
+ success: true,
183
+ result,
184
+ accumulated,
185
+ cost: 0,
186
+ durationMs: event.duration_ms ?? 0,
187
+ model,
188
+ numTurns: 0,
189
+ toolStats,
190
+ });
191
+ }
192
+ });
193
+ let exitCode = null;
194
+ let rlClosed = false;
195
+ let childClosed = false;
196
+ const finalize = () => {
197
+ if (!rlClosed || !childClosed)
198
+ return;
199
+ if (timeoutHandle)
200
+ clearTimeout(timeoutHandle);
201
+ if (!completed) {
202
+ if (exitCode !== null && exitCode !== 0) {
203
+ let errMsg = '';
204
+ if (stderrTotal > 0) {
205
+ if (!stderrHeadFull) {
206
+ errMsg = stderrHead;
207
+ }
208
+ else if (stderrTotal <= MAX_STDERR_HEAD + MAX_STDERR_TAIL) {
209
+ errMsg = stderrHead + stderrTail.slice(stderrTail.length - (stderrTotal - MAX_STDERR_HEAD));
210
+ }
211
+ else {
212
+ errMsg =
213
+ stderrHead +
214
+ `\n\n... (省略 ${stderrTotal - MAX_STDERR_HEAD - MAX_STDERR_TAIL} 字节) ...\n\n` +
215
+ stderrTail;
216
+ }
217
+ }
218
+ callbacks.onError(errMsg || `Cursor CLI exited with code ${exitCode}`);
219
+ }
220
+ else {
221
+ callbacks.onComplete({
222
+ success: true,
223
+ result: accumulated,
224
+ accumulated,
225
+ cost: 0,
226
+ durationMs: 0,
227
+ model,
228
+ numTurns: 0,
229
+ toolStats,
230
+ });
231
+ }
232
+ }
233
+ };
234
+ child.on('close', (code) => {
235
+ log.info(`Cursor CLI closed: exitCode=${code}, pid=${child.pid}`);
236
+ exitCode = code;
237
+ childClosed = true;
238
+ finalize();
239
+ });
240
+ rl.on('close', () => {
241
+ rlClosed = true;
242
+ finalize();
243
+ });
244
+ child.on('error', (err) => {
245
+ const errorCode = err.code;
246
+ log.error(`Cursor CLI spawn error: ${err.message}, code=${errorCode}, path=${cliPath}`);
247
+ if (timeoutHandle)
248
+ clearTimeout(timeoutHandle);
249
+ if (!completed) {
250
+ completed = true;
251
+ callbacks.onError(`Failed to start Cursor CLI: ${err.message}`);
252
+ }
253
+ childClosed = true;
254
+ finalize();
255
+ });
256
+ return {
257
+ abort: () => {
258
+ if (timeoutHandle)
259
+ clearTimeout(timeoutHandle);
260
+ rl.close();
261
+ if (!child.killed)
262
+ child.kill('SIGTERM');
263
+ },
264
+ };
265
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.2.4-beta.3",
3
+ "version": "1.3.1-beta.0",
4
4
  "description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, Cursor)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",