@wu529778790/open-im 1.10.9-beta.21 → 1.10.9-beta.22

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.
@@ -0,0 +1,10 @@
1
+ /**
2
+ * OpenCode Adapter — run tasks through OpenCode CLI (`opencode run`)
3
+ */
4
+ import type { RunCallbacks, RunHandle, RunOptions, ToolAdapter } from './tool-adapter.interface.js';
5
+ export declare class OpenCodeAdapter implements ToolAdapter {
6
+ private cliPath;
7
+ readonly toolId = "opencode";
8
+ constructor(cliPath: string);
9
+ run(prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: RunOptions): RunHandle;
10
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * OpenCode Adapter — run tasks through OpenCode CLI (`opencode run`)
3
+ */
4
+ import { runOpenCode } from '../opencode/cli-runner.js';
5
+ export class OpenCodeAdapter {
6
+ cliPath;
7
+ toolId = 'opencode';
8
+ constructor(cliPath) {
9
+ this.cliPath = cliPath;
10
+ }
11
+ run(prompt, sessionId, workDir, callbacks, options) {
12
+ return runOpenCode(this.cliPath, prompt, sessionId, workDir, {
13
+ onText: callbacks.onText,
14
+ onThinking: callbacks.onThinking,
15
+ onToolUse: callbacks.onToolUse,
16
+ onComplete: (raw) => {
17
+ const result = {
18
+ success: raw.success,
19
+ result: raw.result,
20
+ accumulated: raw.accumulated,
21
+ cost: raw.cost,
22
+ durationMs: raw.durationMs,
23
+ model: raw.model,
24
+ numTurns: raw.numTurns,
25
+ toolStats: raw.toolStats,
26
+ };
27
+ callbacks.onComplete(result);
28
+ },
29
+ onError: (err) => {
30
+ const msg = typeof err === 'string' ? err : String(err);
31
+ const friendly = msg.includes('session not found') ||
32
+ msg.includes('Session not found') ||
33
+ msg.includes('no sessions found')
34
+ ? 'OpenCode 会话已失效,旧 session 已清理。请直接重试当前请求。'
35
+ : msg;
36
+ callbacks.onError(friendly);
37
+ },
38
+ onSessionId: callbacks.onSessionId,
39
+ onSessionInvalid: callbacks.onSessionInvalid,
40
+ }, {
41
+ skipPermissions: options?.skipPermissions,
42
+ model: options?.model,
43
+ });
44
+ }
45
+ }
@@ -2,6 +2,7 @@ import { getConfiguredAiCommands } from '../config.js';
2
2
  import { ClaudeSDKAdapter, configureClaudeSdkSessionIdle } from './claude-sdk-adapter.js';
3
3
  import { CodexAdapter } from './codex-adapter.js';
4
4
  import { CodeBuddyAdapter } from './codebuddy-adapter.js';
5
+ import { OpenCodeAdapter } from './opencode-adapter.js';
5
6
  import { createLogger } from '../logger.js';
6
7
  import { destroyAllLiveChildren } from '../shared/process-kill.js';
7
8
  const log = createLogger('Registry');
@@ -24,6 +25,10 @@ export function initAdapters(config) {
24
25
  log.info('CodeBuddy CLI adapter enabled');
25
26
  adapters.set('codebuddy', new CodeBuddyAdapter(config.codebuddyCliPath));
26
27
  }
28
+ if (aiCommand === 'opencode') {
29
+ log.info('OpenCode CLI adapter enabled');
30
+ adapters.set('opencode', new OpenCodeAdapter(config.opencodeCliPath));
31
+ }
27
32
  }
28
33
  }
29
34
  export function getAdapter(aiCommand) {
@@ -189,12 +189,13 @@ export class CommandHandler {
189
189
  }
190
190
  getAiVersion(aiCommand) {
191
191
  if (aiCommand === 'claude') {
192
- // Claude 使用 SDK,返回 SDK 版本
193
192
  return Promise.resolve('SDK Mode');
194
193
  }
195
194
  const cmd = aiCommand === 'codex'
196
195
  ? this.deps.config.codexCliPath
197
- : this.deps.config.codebuddyCliPath;
196
+ : aiCommand === 'opencode'
197
+ ? this.deps.config.opencodeCliPath
198
+ : this.deps.config.codebuddyCliPath;
198
199
  return new Promise((resolve) => {
199
200
  execFile(cmd, ['--version'], { timeout: 5000 }, (err, stdout) => {
200
201
  resolve(err ? '未知' : (stdout?.toString().trim() || '未知'));
@@ -1,6 +1,6 @@
1
1
  import type { LogLevel } from '../logger.js';
2
2
  export type Platform = 'clawbot' | 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wework' | 'workbuddy';
3
- export type AiCommand = 'claude' | 'codex' | 'codebuddy';
3
+ export type AiCommand = 'claude' | 'codex' | 'codebuddy' | 'opencode';
4
4
  export interface Config {
5
5
  enabledPlatforms: Platform[];
6
6
  telegramBotToken?: string;
@@ -24,6 +24,7 @@ export interface Config {
24
24
  clawbotAllowedUserIds: string[];
25
25
  codexCliPath: string;
26
26
  codebuddyCliPath: string;
27
+ opencodeCliPath: string;
27
28
  /** Claude 访问 API 的代理(如 http://127.0.0.1:7890) */
28
29
  claudeProxy?: string;
29
30
  /** Codex 访问 chatgpt.com 的代理(如 http://127.0.0.1:7890) */
@@ -182,6 +183,9 @@ export interface FileToolCodex {
182
183
  export interface FileToolCodeBuddy {
183
184
  cliPath?: string;
184
185
  }
186
+ export interface FileToolOpenCode {
187
+ cliPath?: string;
188
+ }
185
189
  export interface FileConfig {
186
190
  telegramBotToken?: string;
187
191
  feishuAppId?: string;
@@ -203,6 +207,7 @@ export interface FileConfig {
203
207
  claude?: FileToolClaude;
204
208
  codex?: FileToolCodex;
205
209
  codebuddy?: FileToolCodeBuddy;
210
+ opencode?: FileToolOpenCode;
206
211
  };
207
212
  logDir?: string;
208
213
  logLevel?: LogLevel;
@@ -351,6 +351,7 @@ function buildInitialPayload(file) {
351
351
  claudeProxy: file.tools?.claude?.proxy ?? "",
352
352
  codexCliPath: file.tools?.codex?.cliPath ?? "codex",
353
353
  codebuddyCliPath: file.tools?.codebuddy?.cliPath ?? "codebuddy",
354
+ opencodeCliPath: file.tools?.opencode?.cliPath ?? "opencode",
354
355
  codexProxy: file.tools?.codex?.proxy ?? "",
355
356
  codexApiKey: (() => {
356
357
  if (process.env.OPENAI_API_KEY)
@@ -505,6 +506,7 @@ function createProbeConfig(values) {
505
506
  logLevel: "INFO",
506
507
  telemetry: { enabled: true },
507
508
  codebuddyCliPath: "codebuddy",
509
+ opencodeCliPath: "opencode",
508
510
  platforms: {},
509
511
  ...values,
510
512
  };
@@ -704,6 +706,10 @@ function toFileConfig(payload, existing) {
704
706
  ...existing.tools?.codebuddy,
705
707
  cliPath: clean(payload.ai.codebuddyCliPath) ?? "codebuddy",
706
708
  },
709
+ opencode: {
710
+ ...existing.tools?.opencode,
711
+ cliPath: clean(payload.ai.opencodeCliPath) ?? "opencode",
712
+ },
707
713
  },
708
714
  platforms: {
709
715
  ...existing.platforms,
package/dist/config.js CHANGED
@@ -231,6 +231,8 @@ export function loadConfig() {
231
231
  }
232
232
  }
233
233
  }
234
+ const topencode = file.tools?.opencode ?? {};
235
+ const opencodeCliPath = process.env.OPENCODE_CLI_PATH ?? topencode.cliPath ?? 'opencode';
234
236
  const claudeWorkDir = process.env.CLAUDE_WORK_DIR ?? tc.workDir ?? process.cwd();
235
237
  const skipPermissions = process.env.OPEN_IM_SKIP_PERMISSIONS === 'false'
236
238
  ? false
@@ -563,6 +565,7 @@ export function loadConfig() {
563
565
  clawbotAllowedUserIds,
564
566
  codexCliPath,
565
567
  codebuddyCliPath,
568
+ opencodeCliPath,
566
569
  claudeProxy,
567
570
  codexProxy,
568
571
  claudeWorkDir,
@@ -0,0 +1,32 @@
1
+ /**
2
+ * OpenCode CLI runner — spawns `opencode run` in non-interactive mode.
3
+ *
4
+ * OpenCode outputs plain text to stdout. Session continuation uses `-s <id>`.
5
+ */
6
+ export interface OpenCodeRunCallbacks {
7
+ onText: (accumulated: string) => void;
8
+ onThinking?: (accumulated: string) => void;
9
+ onToolUse?: (toolName: string, toolInput?: Record<string, unknown>) => void;
10
+ onComplete: (result: {
11
+ success: boolean;
12
+ result: string;
13
+ accumulated: string;
14
+ cost: number;
15
+ durationMs: number;
16
+ model?: string;
17
+ numTurns: number;
18
+ toolStats: Record<string, number>;
19
+ }) => void;
20
+ onError: (error: string) => void;
21
+ onSessionId?: (sessionId: string) => void;
22
+ onSessionInvalid?: () => void;
23
+ }
24
+ export interface OpenCodeRunOptions {
25
+ skipPermissions?: boolean;
26
+ model?: string;
27
+ }
28
+ export interface OpenCodeRunHandle {
29
+ abort: () => void;
30
+ }
31
+ export declare function buildOpenCodeArgs(prompt: string, sessionId: string | undefined, workDir: string, options?: OpenCodeRunOptions): string[];
32
+ export declare function runOpenCode(cliPath: string, prompt: string, sessionId: string | undefined, workDir: string, callbacks: OpenCodeRunCallbacks, options?: OpenCodeRunOptions): OpenCodeRunHandle;
@@ -0,0 +1,161 @@
1
+ /**
2
+ * OpenCode CLI runner — spawns `opencode run` in non-interactive mode.
3
+ *
4
+ * OpenCode outputs plain text to stdout. Session continuation uses `-s <id>`.
5
+ */
6
+ import { spawn } from 'node:child_process';
7
+ import { createInterface } from 'node:readline';
8
+ import { createLogger } from '../logger.js';
9
+ import { processEnvForNonClaudeCliChild } from '../config/file-io.js';
10
+ import { killProcessTree, trackChild } from '../shared/process-kill.js';
11
+ const log = createLogger('OpenCodeCli');
12
+ export function buildOpenCodeArgs(prompt, sessionId, workDir, options) {
13
+ const args = ['run'];
14
+ // Working directory
15
+ args.push('--dir', workDir);
16
+ // Session continuation
17
+ if (sessionId) {
18
+ args.push('--session', sessionId);
19
+ }
20
+ // Model override
21
+ if (options?.model) {
22
+ args.push('--model', options.model);
23
+ }
24
+ // Skip permissions
25
+ if (options?.skipPermissions) {
26
+ args.push('--dangerously-skip-permissions');
27
+ }
28
+ // Prompt as positional argument
29
+ args.push(prompt);
30
+ return args;
31
+ }
32
+ export function runOpenCode(cliPath, prompt, sessionId, workDir, callbacks, options) {
33
+ const args = buildOpenCodeArgs(prompt, sessionId, workDir, options);
34
+ const env = processEnvForNonClaudeCliChild();
35
+ log.info(`Spawning OpenCode: path=${cliPath}, cwd=${workDir}, session=${sessionId ?? 'new'}`);
36
+ const child = spawn(cliPath, args, {
37
+ cwd: workDir,
38
+ detached: process.platform !== 'win32',
39
+ stdio: ['pipe', 'pipe', 'pipe'],
40
+ env,
41
+ windowsHide: process.platform === 'win32',
42
+ });
43
+ trackChild(child);
44
+ // Close stdin — prompt is passed as argument
45
+ child.stdin?.end();
46
+ let accumulated = '';
47
+ let completed = false;
48
+ const toolStats = {};
49
+ const startTime = Date.now();
50
+ const rl = createInterface({ input: child.stdout });
51
+ const MAX_STDERR_HEAD = 4 * 1024;
52
+ const MAX_STDERR_TAIL = 6 * 1024;
53
+ let stderrHead = '';
54
+ let stderrTail = '';
55
+ let stderrTotal = 0;
56
+ let stderrHeadFull = false;
57
+ child.stderr?.on('data', (chunk) => {
58
+ const text = chunk.toString();
59
+ stderrTotal += text.length;
60
+ if (!stderrHeadFull) {
61
+ const room = MAX_STDERR_HEAD - stderrHead.length;
62
+ if (room > 0) {
63
+ stderrHead += text.slice(0, room);
64
+ if (stderrHead.length >= MAX_STDERR_HEAD)
65
+ stderrHeadFull = true;
66
+ }
67
+ }
68
+ stderrTail += text;
69
+ if (stderrTail.length > MAX_STDERR_TAIL) {
70
+ stderrTail = stderrTail.slice(-MAX_STDERR_TAIL);
71
+ }
72
+ log.debug(`[stderr] ${text.trimEnd()}`);
73
+ });
74
+ rl.on('line', (line) => {
75
+ if (completed)
76
+ return;
77
+ // OpenCode outputs plain text — accumulate each line
78
+ const trimmed = line.trim();
79
+ if (!trimmed)
80
+ return;
81
+ // Skip spinner lines (ANSI escape sequences or common spinner chars)
82
+ if (/^[⠀-⣿\s⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏▪●◆⬤◉⬤⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]+$/.test(trimmed))
83
+ return;
84
+ accumulated += (accumulated ? '\n' : '') + line;
85
+ callbacks.onText(accumulated);
86
+ });
87
+ // Also capture stdout as a raw stream for binary-like streaming
88
+ child.stdout?.on('data', (chunk) => {
89
+ // The readline interface handles line-by-line, but we also
90
+ // want to capture any partial data for streaming updates
91
+ const text = chunk.toString();
92
+ if (!completed && text.trim()) {
93
+ log.debug(`[stdout chunk] ${text.substring(0, 200)}`);
94
+ }
95
+ });
96
+ let exitCode = null;
97
+ let rlClosed = false;
98
+ let childClosed = false;
99
+ const finalize = () => {
100
+ if (!rlClosed || !childClosed)
101
+ return;
102
+ if (completed)
103
+ return;
104
+ if (exitCode !== null && exitCode !== 0) {
105
+ let errMsg = '';
106
+ if (stderrTotal > 0) {
107
+ if (!stderrHeadFull) {
108
+ errMsg = stderrHead;
109
+ }
110
+ else if (stderrTotal <= MAX_STDERR_HEAD + MAX_STDERR_TAIL) {
111
+ errMsg = stderrHead + stderrTail.slice(stderrTail.length - (stderrTotal - MAX_STDERR_HEAD));
112
+ }
113
+ else {
114
+ errMsg =
115
+ stderrHead +
116
+ `\n\n... (omitted ${stderrTotal - MAX_STDERR_HEAD - MAX_STDERR_TAIL} bytes) ...\n\n` +
117
+ stderrTail;
118
+ }
119
+ }
120
+ if (sessionId &&
121
+ (errMsg.includes('session not found') ||
122
+ errMsg.includes('Session not found') ||
123
+ errMsg.includes('no sessions found'))) {
124
+ callbacks.onSessionInvalid?.();
125
+ }
126
+ callbacks.onError(errMsg || `OpenCode exited with code ${exitCode}`);
127
+ return;
128
+ }
129
+ // Exit code 0 but no onComplete fired yet — treat accumulated text as result
130
+ if (!completed) {
131
+ completed = true;
132
+ callbacks.onComplete({
133
+ success: true,
134
+ result: accumulated,
135
+ accumulated,
136
+ cost: 0,
137
+ durationMs: Date.now() - startTime,
138
+ numTurns: 1,
139
+ toolStats,
140
+ });
141
+ }
142
+ };
143
+ rl.on('close', () => {
144
+ rlClosed = true;
145
+ finalize();
146
+ });
147
+ child.on('close', (code) => {
148
+ exitCode = code;
149
+ childClosed = true;
150
+ finalize();
151
+ });
152
+ child.on('error', (err) => {
153
+ log.error('OpenCode spawn error:', err);
154
+ callbacks.onError(`Failed to start OpenCode: ${err.message}`);
155
+ });
156
+ return {
157
+ abort: () => {
158
+ killProcessTree(child);
159
+ },
160
+ };
161
+ }
@@ -1,4 +1,4 @@
1
- type ToolId = 'claude' | 'codex' | 'codebuddy';
1
+ type ToolId = 'claude' | 'codex' | 'codebuddy' | 'opencode';
2
2
  interface ConvHistoryEntry {
3
3
  convId: string;
4
4
  totalTurns: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.10.9-beta.21",
3
+ "version": "1.10.9-beta.22",
4
4
  "description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, CodeBuddy)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",