@wu529778790/open-im 0.1.0 → 0.1.2

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/.env.example CHANGED
@@ -1,16 +1,16 @@
1
- # Telegram Bot Token (from @BotFather)
2
- TELEGRAM_BOT_TOKEN=
3
-
4
- # Allowed user IDs (comma-separated, empty = allow all in dev)
5
- ALLOWED_USER_IDS=
6
-
7
- # AI tool: claude | codex | cursor
8
- AI_COMMAND=claude
9
-
10
- # Claude CLI (when AI_COMMAND=claude)
11
- CLAUDE_CLI_PATH=claude
12
- CLAUDE_WORK_DIR=.
13
- CLAUDE_SKIP_PERMISSIONS=true
14
- CLAUDE_TIMEOUT_MS=600000
15
- CLAUDE_MODEL=
16
- ALLOWED_BASE_DIRS=
1
+ # Telegram Bot Token (from @BotFather)
2
+ TELEGRAM_BOT_TOKEN=
3
+
4
+ # Allowed user IDs (comma-separated, empty = allow all in dev)
5
+ ALLOWED_USER_IDS=
6
+
7
+ # AI tool: claude | codex | cursor
8
+ AI_COMMAND=claude
9
+
10
+ # Claude CLI (when AI_COMMAND=claude)
11
+ CLAUDE_CLI_PATH=claude
12
+ CLAUDE_WORK_DIR=.
13
+ CLAUDE_SKIP_PERMISSIONS=true
14
+ CLAUDE_TIMEOUT_MS=600000
15
+ CLAUDE_MODEL=
16
+ ALLOWED_BASE_DIRS=
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 wu529778790
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 wu529778790
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,51 +1,76 @@
1
- # open-im
2
-
3
- 多平台 IM 桥接,支持多种 AI CLI 工具(Claude Code、Codex、Cursor 等)。
4
-
5
- ## 功能
6
-
7
- - **多 AI 工具**:通过 `AI_COMMAND` 配置切换 Claude / Codex / Cursor
8
- - **流式输出**:节流更新,Telegram editMessage 实时展示
9
- - **会话管理**:每用户独立 session,`/new` 重置
10
- - **命令**:`/help` `/new` `/cd` `/pwd` `/status`
11
-
12
- ## 快速开始
13
-
14
- ### 快速开始
15
-
16
- ```bash
17
- npm run build
18
- npm run dev # 或 npm start
19
- ```
20
-
21
- 首次运行会进入交互式配置,按提示输入后自动启动服务。配置保存到 `~/.open-im/config.json`。
22
-
23
- ## 配置
24
-
25
- | 变量 | 说明 |
26
- |------|------|
27
- | `TELEGRAM_BOT_TOKEN` | Telegram Bot Token |
28
- | `ALLOWED_USER_IDS` | 白名单用户 ID(逗号分隔,空=所有人) |
29
- | `AI_COMMAND` | `claude` \| `codex` \| `cursor`,默认 `claude` |
30
- | `CLAUDE_CLI_PATH` | Claude CLI 路径,默认 `claude` |
31
- | `CLAUDE_WORK_DIR` | 工作目录 |
32
- | `CLAUDE_SKIP_PERMISSIONS` | 跳过权限确认,默认 `true` |
33
-
34
- ## 项目结构
35
-
36
- ```
37
- src/
38
- ├── adapters/ # ToolAdapter 抽象与实现
39
- │ ├── tool-adapter.interface.ts
40
- │ ├── claude-adapter.ts
41
- │ └── registry.ts
42
- ├── claude/ # Claude CLI 运行与解析
43
- ├── shared/ # ai-task、utils、types
44
- ├── telegram/ # Telegram 事件与消息
45
- ├── session/ # 会话管理
46
- └── commands/ # 命令分发
47
- ```
48
-
49
- ## License
50
-
51
- MIT
1
+ # open-im
2
+
3
+ > 🚀 把你的 AI 助手装进口袋里 - 在 Telegram 随时随地使用 Claude Code
4
+
5
+ 还在受限于终端吗?用手机也能 Coding 了!
6
+
7
+ open-im 是一个轻量级的 IM 桥接工具,让你通过 Telegram 就能使用 Claude Code、Codex、Cursor AI CLI 工具。无论是在咖啡厅、地铁上,还是躺在床上,你的 AI 助手随时在线。
8
+
9
+ ## 为什么选择 open-im
10
+
11
+ - **📱 移动友好** - 告别终端,用手机照样写代码
12
+ - **⚡ 实时流式输出** - AI 思考过程实时可见,像在终端一样流畅
13
+ - **🔒 安全可控** - 支持白名单,只有你能用
14
+ - **🔄 独立会话** - 每个人独立 session,互不干扰
15
+ - **🛠️ 多 AI 支持** - Claude / Codex / Cursor 随意切换
16
+
17
+ ## 🚀 快速开始
18
+
19
+ ### 方式一:npx(无需安装)
20
+
21
+ ```bash
22
+ npx @wu529778790/open-im start
23
+ ```
24
+
25
+ ### 方式二:全局安装(推荐常用用户)
26
+
27
+ ```bash
28
+ npm i @wu529778790/open-im -g
29
+ open-im start
30
+ ```
31
+
32
+ 首次运行会引导你完成配置,30 秒即可搞定。
33
+
34
+ ## 📖 常用命令
35
+
36
+ | 命令 | 说明 |
37
+ |------|------|
38
+ | `open-im start` | 启动服务(后台运行) |
39
+ | `open-im stop` | 停止服务 |
40
+
41
+ ### Telegram 机器人命令
42
+
43
+ | 命令 | 功能 |
44
+ |------|------|
45
+ | `/help` | 查看帮助 |
46
+ | `/new` | 开启新会话 |
47
+ | `/cd <路径>` | 切换工作目录 |
48
+ | `/pwd` | 查看当前目录 |
49
+ | `/status` | 查看运行状态 |
50
+
51
+ ## 💡 使用场景
52
+
53
+ - 🚇 **通勤路上** - 用手机处理简单的代码问题
54
+ - ☕ **咖啡厅** - 没带电脑也能快速调试
55
+ - 🛋️ **沙发模式** - 躺着看 AI 帮你写代码
56
+ - 🌙 **紧急修复** - 半夜收到报警,手机直接处理
57
+
58
+ ## 📦 安装方式
59
+
60
+ ```bash
61
+ # npx(无需安装)
62
+ npx @wu529778790/open-im start
63
+
64
+ # npm 全局安装
65
+ npm i @wu529778790/open-im -g
66
+
67
+ # yarn 全局安装
68
+ yarn global add @wu529778790/open-im
69
+
70
+ # pnpm 全局安装
71
+ pnpm i @wu529778790/open-im -g
72
+ ```
73
+
74
+ ## 📝 License
75
+
76
+ [MIT](LICENSE)
@@ -22,7 +22,7 @@ export function runClaude(cliPath, prompt, sessionId, workDir, callbacks, option
22
22
  env.CC_IM_CHAT_ID = options.chatId;
23
23
  if (options?.hookPort)
24
24
  env.CC_IM_HOOK_PORT = String(options.hookPort);
25
- const child = spawn(cliPath, args, { cwd: workDir, stdio: ['ignore', 'pipe', 'pipe'], env });
25
+ const child = spawn(cliPath, args, { cwd: workDir, stdio: ['ignore', 'pipe', 'pipe'], env, windowsHide: true });
26
26
  log.debug(`Claude CLI spawned: pid=${child.pid}, cwd=${workDir}, session=${sessionId ?? 'new'}`);
27
27
  let accumulated = '';
28
28
  let accumulatedThinking = '';
package/dist/cli.js CHANGED
@@ -1,6 +1,138 @@
1
1
  #!/usr/bin/env node
2
2
  import { main } from './index.js';
3
- main().catch((err) => {
4
- console.error(err);
5
- process.exit(1);
6
- });
3
+ import { spawn, execFileSync } from 'node:child_process';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { dirname, join } from 'node:path';
6
+ import { mkdir, writeFile, rm, readFile } from 'node:fs/promises';
7
+ import { existsSync } from 'node:fs';
8
+ import { platform } from 'node:os';
9
+ import { homedir } from 'node:os';
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+ // PID 文件路径
13
+ const PID_DIR = join(homedir(), '.open-im');
14
+ const PID_FILE = join(PID_DIR, 'daemon.pid');
15
+ const STOP_FILE = join(PID_DIR, 'stop.flag');
16
+ // 保存 PID 到文件
17
+ async function savePid(pid) {
18
+ try {
19
+ await mkdir(PID_DIR, { recursive: true });
20
+ await writeFile(PID_FILE, String(pid), 'utf-8');
21
+ }
22
+ catch (err) {
23
+ console.error('无法保存 PID 文件:', err);
24
+ }
25
+ }
26
+ // 读取 PID 文件
27
+ async function readPid() {
28
+ try {
29
+ if (!existsSync(PID_FILE)) {
30
+ return null;
31
+ }
32
+ const content = await readFile(PID_FILE, 'utf-8');
33
+ return parseInt(content.trim(), 10);
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ // 删除 PID 文件
40
+ async function removePidFile() {
41
+ try {
42
+ if (existsSync(PID_FILE)) {
43
+ await rm(PID_FILE);
44
+ }
45
+ }
46
+ catch {
47
+ // 忽略删除错误
48
+ }
49
+ }
50
+ // 检查进程是否在运行
51
+ async function isProcessRunning(pid) {
52
+ try {
53
+ // 尝试发送信号 0(不杀死进程,只检查是否存在)
54
+ process.kill(pid, 0);
55
+ return true;
56
+ }
57
+ catch {
58
+ return false;
59
+ }
60
+ }
61
+ // 停止服务 - 创建停止标记文件,让服务优雅关闭
62
+ async function stopService() {
63
+ const pid = await readPid();
64
+ if (!pid) {
65
+ console.log('未找到运行中的服务(PID 文件不存在)');
66
+ return;
67
+ }
68
+ const running = await isProcessRunning(pid);
69
+ if (!running) {
70
+ console.log(`服务未运行(进程 ${pid} 不存在)`);
71
+ await removePidFile();
72
+ return;
73
+ }
74
+ try {
75
+ // 创建停止标记文件,服务会定期检查并优雅关闭
76
+ await mkdir(PID_DIR, { recursive: true });
77
+ await writeFile(STOP_FILE, Date.now().toString(), 'utf-8');
78
+ console.log('正在停止服务...');
79
+ // 等待进程退出(最多 10 秒)
80
+ const maxWait = 10000;
81
+ const interval = 200;
82
+ let waited = 0;
83
+ while (waited < maxWait) {
84
+ await new Promise(resolve => setTimeout(resolve, interval));
85
+ if (!(await isProcessRunning(pid))) {
86
+ await removePidFile();
87
+ console.log('服务已停止');
88
+ return;
89
+ }
90
+ waited += interval;
91
+ }
92
+ // 超时后强制终止
93
+ console.log('等待超时,强制终止服务...');
94
+ const isWindows = platform() === 'win32';
95
+ if (isWindows) {
96
+ execFileSync('taskkill', ['/F', '/PID', String(pid)], { stdio: 'ignore' });
97
+ }
98
+ else {
99
+ process.kill(pid, 'SIGKILL');
100
+ }
101
+ await removePidFile();
102
+ console.log('服务已强制停止');
103
+ }
104
+ catch (err) {
105
+ const errorMsg = err instanceof Error ? err.message : String(err);
106
+ console.error(`停止服务失败: ${errorMsg}`);
107
+ await removePidFile();
108
+ }
109
+ }
110
+ const args = process.argv.slice(2);
111
+ if (args[0] === 'stop') {
112
+ stopService().catch((err) => {
113
+ console.error('停止服务时出错:', err);
114
+ process.exit(1);
115
+ });
116
+ }
117
+ else if (args[0] === 'start') {
118
+ // 后台启动 - 跨平台方案
119
+ const distPath = join(__dirname, '..', 'dist', 'index.js');
120
+ // 使用 detached 模式创建独立进程
121
+ const child = spawn(process.execPath, [distPath], {
122
+ detached: true,
123
+ stdio: ['ignore', 'ignore', 'ignore'],
124
+ windowsHide: true // Windows 上隐藏控制台窗口
125
+ });
126
+ // 保存 PID
127
+ await savePid(child.pid);
128
+ // 让子进程独立于父进程
129
+ child.unref();
130
+ console.log(`服务已在后台启动 (PID: ${child.pid})`);
131
+ }
132
+ else {
133
+ // 默认启动(兼容直接运行 open-im)
134
+ main().catch((err) => {
135
+ console.error(err);
136
+ process.exit(1);
137
+ });
138
+ }
package/dist/index.js CHANGED
@@ -8,9 +8,30 @@ import { SessionManager } from './session/session-manager.js';
8
8
  import { loadActiveChats, getActiveChatId, flushActiveChats } from './shared/active-chats.js';
9
9
  import { initLogger, createLogger, closeLogger } from './logger.js';
10
10
  import { createRequire } from 'node:module';
11
+ import { join } from 'node:path';
12
+ import { homedir } from 'node:os';
13
+ import { rm } from 'node:fs/promises';
14
+ import { existsSync } from 'node:fs';
11
15
  const require = createRequire(import.meta.url);
12
16
  const { version: APP_VERSION } = require('../package.json');
13
17
  const log = createLogger('Main');
18
+ // 停止标记文件路径
19
+ const STOP_FILE = join(homedir(), '.open-im', 'stop.flag');
20
+ // 检查是否收到停止信号
21
+ function checkStopSignal() {
22
+ return existsSync(STOP_FILE);
23
+ }
24
+ // 清理停止标记文件
25
+ async function clearStopSignal() {
26
+ try {
27
+ if (existsSync(STOP_FILE)) {
28
+ await rm(STOP_FILE);
29
+ }
30
+ }
31
+ catch {
32
+ // 忽略错误
33
+ }
34
+ }
14
35
  async function sendLifecycleNotification(platform, message) {
15
36
  const chatId = getActiveChatId('telegram');
16
37
  if (!chatId)
@@ -55,7 +76,16 @@ export async function main() {
55
76
  log.info('Shutting down...');
56
77
  const uptimeSec = Math.floor((Date.now() - startedAt) / 1000);
57
78
  const m = Math.floor(uptimeSec / 60);
58
- await sendLifecycleNotification('telegram', `🔴 open-im 服务正在关闭...\n运行时长: ${m}分钟`).catch(() => { });
79
+ try {
80
+ await sendLifecycleNotification('telegram', `🔴 open-im 服务正在关闭...\n运行时长: ${m}分钟`);
81
+ // 等待消息发送完成
82
+ await new Promise(resolve => setTimeout(resolve, 500));
83
+ }
84
+ catch (err) {
85
+ log.debug('Failed to send shutdown notification:', err);
86
+ }
87
+ // 清理停止标记文件
88
+ await clearStopSignal();
59
89
  telegramHandle?.stop();
60
90
  stopTelegram();
61
91
  sessionManager.destroy();
@@ -65,6 +95,13 @@ export async function main() {
65
95
  };
66
96
  process.on('SIGINT', () => shutdown().catch(() => process.exit(1)));
67
97
  process.on('SIGTERM', () => shutdown().catch(() => process.exit(1)));
98
+ // 定期检查停止标记文件(用于 Windows 等无法发送信号的场景)
99
+ const stopCheckInterval = setInterval(() => {
100
+ if (checkStopSignal()) {
101
+ clearInterval(stopCheckInterval);
102
+ shutdown().catch(() => process.exit(1));
103
+ }
104
+ }, 1000);
68
105
  }
69
106
  const isEntry = process.argv[1]?.replace(/\\/g, '/').endsWith('/index.js') || process.argv[1]?.replace(/\\/g, '/').endsWith('/index.ts');
70
107
  if (isEntry) {
package/dist/setup.js CHANGED
@@ -14,12 +14,12 @@ function printManualInstructions(configPath) {
14
14
  console.log(' 2. 创建文件:', configPath);
15
15
  console.log(' 3. 填入以下内容(替换为你的 Token 和用户 ID):');
16
16
  console.log('');
17
- console.log(`{
18
- "telegramBotToken": "你的Bot Token",
19
- "allowedUserIds": ["你的Telegram用户ID"],
20
- "claudeWorkDir": "${process.cwd().replace(/\\/g, '/')}",
21
- "claudeSkipPermissions": true,
22
- "aiCommand": "claude"
17
+ console.log(`{
18
+ "telegramBotToken": "你的Bot Token",
19
+ "allowedUserIds": ["你的Telegram用户ID"],
20
+ "claudeWorkDir": "${process.cwd().replace(/\\/g, '/')}",
21
+ "claudeSkipPermissions": true,
22
+ "aiCommand": "claude"
23
23
  }`);
24
24
  console.log('');
25
25
  console.log('或设置环境变量: TELEGRAM_BOT_TOKEN=xxx 后再运行');
package/package.json CHANGED
@@ -1,52 +1,54 @@
1
- {
2
- "name": "@wu529778790/open-im",
3
- "version": "0.1.0",
4
- "description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, Cursor)",
5
- "type": "module",
6
- "main": "dist/index.js",
7
- "bin": {
8
- "open-im": "dist/cli.js"
9
- },
10
- "files": [
11
- "dist",
12
- ".env.example"
13
- ],
14
- "scripts": {
15
- "build": "tsc",
16
- "dev": "tsx src/index.ts",
17
- "start": "node dist/index.js",
18
- "setup": "node dist/index.js --setup-only",
19
- "prepublishOnly": "npm run build"
20
- },
21
- "keywords": [
22
- "telegram",
23
- "feishu",
24
- "claude",
25
- "claude-code",
26
- "bot",
27
- "bridge"
28
- ],
29
- "license": "MIT",
30
- "repository": {
31
- "type": "git",
32
- "url": "git+https://github.com/wu529778790/open-im.git"
33
- },
34
- "homepage": "https://github.com/wu529778790/open-im#readme",
35
- "bugs": {
36
- "url": "https://github.com/wu529778790/open-im/issues"
37
- },
38
- "dependencies": {
39
- "prompts": "^2.4.2",
40
- "telegraf": "^4.16.3"
41
- },
42
- "devDependencies": {
43
- "@types/node": "^20.0.0",
44
- "@types/prompts": "^2.4.9",
45
- "dotenv": "^16.0.0",
46
- "tsx": "^4.0.0",
47
- "typescript": "^5.0.0"
48
- },
49
- "engines": {
50
- "node": ">=20"
51
- }
52
- }
1
+ {
2
+ "name": "@wu529778790/open-im",
3
+ "version": "0.1.2",
4
+ "description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, Cursor)",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "open-im": "dist/cli.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ ".env.example"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "dev": "tsx src/index.ts",
17
+ "run": "node dist/index.js",
18
+ "start": "node dist/cli.js start",
19
+ "stop": "node dist/cli.js stop",
20
+ "setup": "node dist/index.js --setup-only",
21
+ "prepublishOnly": "npm run build"
22
+ },
23
+ "keywords": [
24
+ "telegram",
25
+ "feishu",
26
+ "claude",
27
+ "claude-code",
28
+ "bot",
29
+ "bridge"
30
+ ],
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/wu529778790/open-im.git"
35
+ },
36
+ "homepage": "https://github.com/wu529778790/open-im#readme",
37
+ "bugs": {
38
+ "url": "https://github.com/wu529778790/open-im/issues"
39
+ },
40
+ "dependencies": {
41
+ "prompts": "^2.4.2",
42
+ "telegraf": "^4.16.3"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^20.0.0",
46
+ "@types/prompts": "^2.4.9",
47
+ "dotenv": "^16.0.0",
48
+ "tsx": "^4.0.0",
49
+ "typescript": "^5.0.0"
50
+ },
51
+ "engines": {
52
+ "node": ">=20"
53
+ }
54
+ }