@wu529778790/open-im 1.10.9-beta.4 → 1.10.9-beta.6

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
@@ -66,6 +66,27 @@ After `start`, the CLI prints the dashboard URL (default **`http://127.0.0.1:392
66
66
 
67
67
  Session state is stored in **`~/.open-im/data/sessions.json`** (per user, not IM chat logs).
68
68
 
69
+ ## Session continuity
70
+
71
+ open-im and Claude Code CLI share the same session storage. When you work in the same directory, you can seamlessly switch between phone and computer.
72
+
73
+ **Phone → Computer:** open-im automatically resumes the latest CLI session in the same directory — no configuration needed.
74
+
75
+ **Computer → Phone:** use `claude --continue` (or `claude -c`) to pick up the conversation that was continued on open-im.
76
+
77
+ ```
78
+ # On computer
79
+ cd /my-project && claude # work as usual, then Ctrl+C
80
+
81
+ # On phone (via IM)
82
+ "help me fix the login bug" # open-im auto-resumes the same session
83
+
84
+ # Back on computer
85
+ claude -c # continues the phone conversation
86
+ ```
87
+
88
+ > **Note:** only one side can be active at a time. Exit the CLI before sending messages from the phone, and vice versa.
89
+
69
90
  ## Configuration
70
91
 
71
92
  ### Per-platform AI
package/README.zh-CN.md CHANGED
@@ -66,6 +66,27 @@ npx @wu529778790/open-im start
66
66
 
67
67
  会话状态保存在 **`~/.open-im/data/sessions.json`**(按用户,与 IM 聊天记录无关)。
68
68
 
69
+ ## 会话接力
70
+
71
+ open-im 和 Claude Code CLI 共享同一份 session 存储。在同一个目录下,手机和电脑可以无缝切换。
72
+
73
+ **手机接电脑:** open-im 自动恢复同目录下最新的 CLI session,无需配置。
74
+
75
+ **电脑接手机:** 使用 `claude --continue`(或 `claude -c`)接上 open-im 端的对话。
76
+
77
+ ```bash
78
+ # 电脑端
79
+ cd /my-project && claude # 正常工作,退出时 Ctrl+C
80
+
81
+ # 手机端(IM 消息)
82
+ "帮我修复登录 bug" # open-im 自动接续同一个 session
83
+
84
+ # 回到电脑端
85
+ claude -c # 接上手机端的对话
86
+ ```
87
+
88
+ > **注意:** 同一时刻只能有一端活跃。从手机发消息前先退出 CLI,反之亦然。
89
+
69
90
  ## 配置说明
70
91
 
71
92
  ### 按平台指定 AI
@@ -9,6 +9,18 @@
9
9
  * 认证:ANTHROPIC_API_KEY 或 CLAUDE_CODE_OAUTH_TOKEN
10
10
  */
11
11
  import type { ToolAdapter, RunCallbacks, RunOptions, RunHandle } from './tool-adapter.interface.js';
12
+ interface ClaudeSessionMeta {
13
+ sessionId: string;
14
+ mtime: number;
15
+ filePath: string;
16
+ size: number;
17
+ }
18
+ /**
19
+ * 扫描 ~/.claude/projects/<encoded-path>/ 找到最新的 CLI session。
20
+ * 只返回 JSONL 文件(排除子目录如 subagents/),按修改时间倒序。
21
+ * @param homeOverride 测试用:覆盖 homedir() 的返回值
22
+ */
23
+ export declare function findLatestClaudeSession(workDir: string, homeOverride?: string): ClaudeSessionMeta | undefined;
12
24
  /**
13
25
  * 由 initAdapters 根据配置调用。ttlMinutes≤0 时关闭空闲回收(仍受 MAX_ACTIVE_SESSIONS 限制)。
14
26
  */
@@ -26,3 +38,4 @@ export declare class ClaudeSDKAdapter implements ToolAdapter {
26
38
  static removeSession(sessionId: string): void;
27
39
  run(prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: RunOptions): RunHandle;
28
40
  }
41
+ export {};
@@ -9,7 +9,8 @@
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';
12
+ import { existsSync, readFileSync, readdirSync, statSync, openSync, readSync, closeSync } from 'fs';
13
+ import { execSync } from 'child_process';
13
14
  import { homedir } from 'os';
14
15
  import { join } from 'path';
15
16
  import { createLogger } from '../logger.js';
@@ -46,6 +47,110 @@ function loadUserPluginSettings() {
46
47
  }
47
48
  // Pre-load user plugin settings to cache Claude Code user preferences
48
49
  loadUserPluginSettings();
50
+ // ── 扫描 Claude Code CLI 的最新 session,支持手机/电脑无缝切换 ──
51
+ /**
52
+ * 将 workDir(如 /Users/mac/github/open-im)转换为 Claude Code 的项目路径编码
53
+ * ~/.claude/projects/-Users-mac-github-open-im/
54
+ */
55
+ function workDirToProjectPath(workDir) {
56
+ // Claude Code 将路径中的 / 替换为 -,leading - 保留
57
+ // /Users/mac/github/open-im → -Users-mac-github-open-im
58
+ return workDir.replace(/\//g, '-');
59
+ }
60
+ /**
61
+ * 检查 CLI 进程是否正在使用某个 session(通过 ps 检测)
62
+ * 注意:仅支持 macOS/Linux,Windows 上会静默返回 false
63
+ */
64
+ function isCliSessionActive(sessionId) {
65
+ try {
66
+ // macOS/Linux: 用 ps 搜索包含该 sessionId 的 claude 进程
67
+ // -F 固定字符串匹配,避免正则意外;-- 防止 sessionId 被误认为 flag
68
+ const result = execSync(`ps -axo pid,command 2>/dev/null | grep -v grep | grep "claude" | grep -F -- "${sessionId}" || true`, { encoding: 'utf-8', timeout: 3000 });
69
+ return result.trim().length > 0;
70
+ }
71
+ catch {
72
+ return false;
73
+ }
74
+ }
75
+ /**
76
+ * 扫描 ~/.claude/projects/<encoded-path>/ 找到最新的 CLI session。
77
+ * 只返回 JSONL 文件(排除子目录如 subagents/),按修改时间倒序。
78
+ * @param homeOverride 测试用:覆盖 homedir() 的返回值
79
+ */
80
+ export function findLatestClaudeSession(workDir, homeOverride) {
81
+ const projectDir = join(homeOverride ?? homedir(), '.claude', 'projects', workDirToProjectPath(workDir));
82
+ if (!existsSync(projectDir)) {
83
+ log.info(`No Claude Code project dir found: ${projectDir}`);
84
+ return undefined;
85
+ }
86
+ try {
87
+ const entries = readdirSync(projectDir, { withFileTypes: true });
88
+ const sessions = [];
89
+ for (const entry of entries) {
90
+ // 只处理 .jsonl 文件,跳过子目录(如 subagents/)和 memory 目录
91
+ if (!entry.isFile() || !entry.name.endsWith('.jsonl'))
92
+ continue;
93
+ const sessionId = entry.name.replace('.jsonl', '');
94
+ // 校验 UUID 格式
95
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(sessionId))
96
+ continue;
97
+ const filePath = join(projectDir, entry.name);
98
+ try {
99
+ const stat = statSync(filePath);
100
+ // 跳过空文件
101
+ if (stat.size === 0)
102
+ continue;
103
+ sessions.push({ sessionId, mtime: stat.mtimeMs, filePath, size: stat.size });
104
+ }
105
+ catch {
106
+ continue;
107
+ }
108
+ }
109
+ if (sessions.length === 0) {
110
+ log.info(`No session files found in ${projectDir}`);
111
+ return undefined;
112
+ }
113
+ // 按修改时间倒序,最新的在前
114
+ sessions.sort((a, b) => b.mtime - a.mtime);
115
+ const latest = sessions[0];
116
+ log.info(`Found latest Claude Code session: ${latest.sessionId} (mtime: ${new Date(latest.mtime).toISOString()}, size: ${latest.size})`);
117
+ return latest;
118
+ }
119
+ catch (err) {
120
+ log.warn(`Failed to scan Claude Code project dir: ${err}`);
121
+ return undefined;
122
+ }
123
+ }
124
+ /**
125
+ * 从 JSONL 文件的第一行提取 sessionId,验证文件内容与文件名一致。
126
+ * 只读取前 4KB,避免大文件全量读入内存。
127
+ */
128
+ function validateSessionFile(filePath, expectedSessionId) {
129
+ let fd;
130
+ try {
131
+ fd = openSync(filePath, 'r');
132
+ const buf = Buffer.alloc(4096);
133
+ const bytesRead = readSync(fd, buf, 0, 4096, 0);
134
+ if (bytesRead === 0)
135
+ return false;
136
+ const firstLine = buf.toString('utf-8', 0, bytesRead).split('\n')[0];
137
+ if (!firstLine)
138
+ return false;
139
+ const firstEntry = JSON.parse(firstLine);
140
+ return firstEntry.sessionId === expectedSessionId;
141
+ }
142
+ catch {
143
+ return false;
144
+ }
145
+ finally {
146
+ if (fd !== undefined) {
147
+ try {
148
+ closeSync(fd);
149
+ }
150
+ catch { /* ignore */ }
151
+ }
152
+ }
153
+ }
49
154
  // 存储所有活跃的 SDKSession 对象,key 为 sessionId
50
155
  // 使用 Map 而不是 Set,因为我们需要通过 sessionId 获取 session
51
156
  const activeSessions = new Map();
@@ -246,6 +351,37 @@ async function getOrCreateSession(sessionId, workDir, model, permissionMode, onS
246
351
  // 恢复失败,创建新会话
247
352
  }
248
353
  }
354
+ // 没有指定 sessionId 时,尝试自动恢复 Claude Code CLI 的最新 session
355
+ // 实现手机/电脑无缝切换:同目录下默认共享同一个对话
356
+ if (!sessionId) {
357
+ const latest = findLatestClaudeSession(workDir);
358
+ if (latest) {
359
+ // 安全检查:如果 CLI 正在使用该 session,不能接管
360
+ if (isCliSessionActive(latest.sessionId)) {
361
+ log.info(`CLI is actively using session ${latest.sessionId}, skipping auto-resume`);
362
+ }
363
+ else {
364
+ // 验证文件内容一致性
365
+ if (validateSessionFile(latest.filePath, latest.sessionId)) {
366
+ try {
367
+ log.info(`Auto-resuming latest CLI session: ${latest.sessionId}`);
368
+ session = unstable_v2_resumeSession(latest.sessionId, sessionOptions);
369
+ activeSessions.set(latest.sessionId, session);
370
+ sessionWorkDirs.set(latest.sessionId, workDir);
371
+ sessionLastUsed.set(latest.sessionId, Date.now());
372
+ log.info(`Successfully auto-resumed CLI session: ${latest.sessionId}`);
373
+ return { session, sessionId: latest.sessionId };
374
+ }
375
+ catch (err) {
376
+ log.warn(`Failed to auto-resume CLI session ${latest.sessionId}, skipping auto-resume: ${err}`);
377
+ }
378
+ }
379
+ else {
380
+ log.warn(`Session file validation failed for ${latest.sessionId}, skipping`);
381
+ }
382
+ }
383
+ }
384
+ }
249
385
  // 创建新会话
250
386
  session = unstable_v2_createSession(sessionOptions);
251
387
  // 新会话的 sessionId 需要从第一个消息中获取
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.10.9-beta.4",
3
+ "version": "1.10.9-beta.6",
4
4
  "description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, CodeBuddy)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",