remote-claude 0.1.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/.env.example +15 -0
- package/LICENSE +21 -0
- package/README.md +231 -0
- package/bin/cl +20 -0
- package/bin/cla +20 -0
- package/bin/remote-claude +21 -0
- package/client/client.py +251 -0
- package/lark_client/__init__.py +3 -0
- package/lark_client/capture_output.py +91 -0
- package/lark_client/card_builder.py +1114 -0
- package/lark_client/card_service.py +250 -0
- package/lark_client/config.py +22 -0
- package/lark_client/lark_handler.py +841 -0
- package/lark_client/main.py +306 -0
- package/lark_client/output_cleaner.py +222 -0
- package/lark_client/session_bridge.py +195 -0
- package/lark_client/shared_memory_poller.py +364 -0
- package/lark_client/terminal_buffer.py +215 -0
- package/lark_client/terminal_renderer.py +69 -0
- package/package.json +41 -0
- package/pyproject.toml +14 -0
- package/remote_claude.py +518 -0
- package/scripts/check-env.sh +40 -0
- package/scripts/completion.sh +76 -0
- package/scripts/postinstall.sh +76 -0
- package/server/component_parser.py +1113 -0
- package/server/rich_text_renderer.py +301 -0
- package/server/server.py +801 -0
- package/server/shared_state.py +198 -0
- package/stats/__init__.py +38 -0
- package/stats/collector.py +325 -0
- package/stats/machine.py +47 -0
- package/stats/query.py +151 -0
- package/utils/components.py +165 -0
- package/utils/protocol.py +164 -0
- package/utils/session.py +409 -0
- package/uv.lock +703 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""
|
|
2
|
+
终端输出缓冲器 - 正确处理回退和覆盖
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import List
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TerminalBuffer:
|
|
10
|
+
"""
|
|
11
|
+
模拟终端缓冲区,正确处理回退字符和覆盖
|
|
12
|
+
|
|
13
|
+
终端动画原理:
|
|
14
|
+
- \r (carriage return) 回到行首
|
|
15
|
+
- \x1b[K 清除到行尾
|
|
16
|
+
- \x1b[2K 清除整行
|
|
17
|
+
- 动画通过重复 \r + 新内容 实现
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
self.lines: List[str] = [""] # 当前屏幕内容(按行存储)
|
|
22
|
+
self.cursor_line = 0 # 光标所在行
|
|
23
|
+
self.cursor_col = 0 # 光标所在列
|
|
24
|
+
|
|
25
|
+
def write(self, data: str) -> None:
|
|
26
|
+
"""写入数据到缓冲区"""
|
|
27
|
+
# 先移除 ANSI 转义码(颜色等)但保留控制字符
|
|
28
|
+
data = self._strip_ansi_colors(data)
|
|
29
|
+
|
|
30
|
+
i = 0
|
|
31
|
+
while i < len(data):
|
|
32
|
+
char = data[i]
|
|
33
|
+
|
|
34
|
+
if char == '\r':
|
|
35
|
+
# 回车:回到行首
|
|
36
|
+
self.cursor_col = 0
|
|
37
|
+
elif char == '\n':
|
|
38
|
+
# 换行:移动到下一行
|
|
39
|
+
self._new_line()
|
|
40
|
+
elif char == '\x1b':
|
|
41
|
+
# 转义序列
|
|
42
|
+
seq_len = self._handle_escape_sequence(data[i:])
|
|
43
|
+
i += seq_len - 1 # -1 因为循环会 +1
|
|
44
|
+
elif ord(char) >= 32 or char == '\t':
|
|
45
|
+
# 可打印字符或制表符
|
|
46
|
+
self._write_char(char)
|
|
47
|
+
|
|
48
|
+
i += 1
|
|
49
|
+
|
|
50
|
+
def _strip_ansi_colors(self, data: str) -> str:
|
|
51
|
+
"""移除 ANSI 颜色码,但保留控制序列"""
|
|
52
|
+
# 移除颜色码 (SGR)
|
|
53
|
+
data = re.sub(r'\x1b\[[0-9;]*m', '', data)
|
|
54
|
+
# 移除私有模式设置
|
|
55
|
+
data = re.sub(r'\x1b\[\?[0-9]+[hl]', '', data)
|
|
56
|
+
return data
|
|
57
|
+
|
|
58
|
+
def _handle_escape_sequence(self, data: str) -> int:
|
|
59
|
+
"""处理转义序列,返回序列长度"""
|
|
60
|
+
if len(data) < 2:
|
|
61
|
+
return 1
|
|
62
|
+
|
|
63
|
+
if data[1] == '[':
|
|
64
|
+
# CSI 序列
|
|
65
|
+
match = re.match(r'\x1b\[([0-9;]*)([A-Za-z])', data)
|
|
66
|
+
if match:
|
|
67
|
+
params = match.group(1)
|
|
68
|
+
cmd = match.group(2)
|
|
69
|
+
self._handle_csi(params, cmd)
|
|
70
|
+
return len(match.group(0))
|
|
71
|
+
|
|
72
|
+
return 1
|
|
73
|
+
|
|
74
|
+
def _handle_csi(self, params: str, cmd: str) -> None:
|
|
75
|
+
"""处理 CSI 命令"""
|
|
76
|
+
n = int(params) if params.isdigit() else 1
|
|
77
|
+
|
|
78
|
+
if cmd == 'A': # 光标上移
|
|
79
|
+
self.cursor_line = max(0, self.cursor_line - n)
|
|
80
|
+
elif cmd == 'B': # 光标下移
|
|
81
|
+
self.cursor_line = min(len(self.lines) - 1, self.cursor_line + n)
|
|
82
|
+
elif cmd == 'C': # 光标右移
|
|
83
|
+
self.cursor_col += n
|
|
84
|
+
elif cmd == 'D': # 光标左移
|
|
85
|
+
self.cursor_col = max(0, self.cursor_col - n)
|
|
86
|
+
elif cmd == 'K': # 清除行
|
|
87
|
+
if params == '' or params == '0':
|
|
88
|
+
# 清除光标到行尾
|
|
89
|
+
if self.cursor_line < len(self.lines):
|
|
90
|
+
self.lines[self.cursor_line] = self.lines[self.cursor_line][:self.cursor_col]
|
|
91
|
+
elif params == '1':
|
|
92
|
+
# 清除行首到光标
|
|
93
|
+
if self.cursor_line < len(self.lines):
|
|
94
|
+
line = self.lines[self.cursor_line]
|
|
95
|
+
self.lines[self.cursor_line] = ' ' * self.cursor_col + line[self.cursor_col:]
|
|
96
|
+
elif params == '2':
|
|
97
|
+
# 清除整行
|
|
98
|
+
if self.cursor_line < len(self.lines):
|
|
99
|
+
self.lines[self.cursor_line] = ''
|
|
100
|
+
elif cmd == 'J': # 清除屏幕
|
|
101
|
+
if params == '2':
|
|
102
|
+
self.lines = [""]
|
|
103
|
+
self.cursor_line = 0
|
|
104
|
+
self.cursor_col = 0
|
|
105
|
+
elif cmd == 'G': # 光标移动到列
|
|
106
|
+
self.cursor_col = max(0, n - 1)
|
|
107
|
+
|
|
108
|
+
def _new_line(self) -> None:
|
|
109
|
+
"""换行"""
|
|
110
|
+
self.cursor_line += 1
|
|
111
|
+
self.cursor_col = 0
|
|
112
|
+
while len(self.lines) <= self.cursor_line:
|
|
113
|
+
self.lines.append("")
|
|
114
|
+
|
|
115
|
+
def _write_char(self, char: str) -> None:
|
|
116
|
+
"""写入一个字符"""
|
|
117
|
+
# 确保行存在
|
|
118
|
+
while len(self.lines) <= self.cursor_line:
|
|
119
|
+
self.lines.append("")
|
|
120
|
+
|
|
121
|
+
line = self.lines[self.cursor_line]
|
|
122
|
+
|
|
123
|
+
# 如果光标位置超出当前行长度,用空格填充
|
|
124
|
+
if self.cursor_col >= len(line):
|
|
125
|
+
line = line + ' ' * (self.cursor_col - len(line)) + char
|
|
126
|
+
else:
|
|
127
|
+
# 覆盖现有字符
|
|
128
|
+
line = line[:self.cursor_col] + char + line[self.cursor_col + 1:]
|
|
129
|
+
|
|
130
|
+
self.lines[self.cursor_line] = line
|
|
131
|
+
self.cursor_col += 1
|
|
132
|
+
|
|
133
|
+
def get_content(self) -> str:
|
|
134
|
+
"""获取当前缓冲区内容"""
|
|
135
|
+
# 合并所有行,移除尾部空行
|
|
136
|
+
result = []
|
|
137
|
+
for line in self.lines:
|
|
138
|
+
# 移除行尾空格
|
|
139
|
+
result.append(line.rstrip())
|
|
140
|
+
|
|
141
|
+
# 移除尾部空行
|
|
142
|
+
while result and not result[-1]:
|
|
143
|
+
result.pop()
|
|
144
|
+
|
|
145
|
+
return '\n'.join(result)
|
|
146
|
+
|
|
147
|
+
def clear(self) -> None:
|
|
148
|
+
"""清空缓冲区"""
|
|
149
|
+
self.lines = [""]
|
|
150
|
+
self.cursor_line = 0
|
|
151
|
+
self.cursor_col = 0
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def clean_terminal_output(raw_data: bytes, user_input: str = "") -> str:
|
|
155
|
+
"""
|
|
156
|
+
清理终端输出,正确处理回退和覆盖
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
raw_data: 原始终端输出
|
|
160
|
+
user_input: 用户输入(用于过滤回显)
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
清理后的文本
|
|
164
|
+
"""
|
|
165
|
+
text = raw_data.decode('utf-8', errors='replace')
|
|
166
|
+
|
|
167
|
+
# 使用终端缓冲器处理
|
|
168
|
+
buffer = TerminalBuffer()
|
|
169
|
+
buffer.write(text)
|
|
170
|
+
content = buffer.get_content()
|
|
171
|
+
|
|
172
|
+
# 后处理:移除 Claude CLI 界面元素
|
|
173
|
+
lines = content.split('\n')
|
|
174
|
+
clean_lines = []
|
|
175
|
+
|
|
176
|
+
for line in lines:
|
|
177
|
+
stripped = line.strip()
|
|
178
|
+
if not stripped:
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
# 跳过边框和分隔线
|
|
182
|
+
if all(c in '─━═-–—│╭╮╰╯┌┐└┘├┤┬┴┼ ' for c in stripped):
|
|
183
|
+
continue
|
|
184
|
+
if stripped.startswith(('╭', '╰', '│', '┌', '└', '├')):
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
# 跳过界面元素
|
|
188
|
+
if any(x in stripped for x in ['Welcome to', 'Welcome back']):
|
|
189
|
+
continue
|
|
190
|
+
if re.match(r'^Try\s*"', stripped):
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
# 跳过动画文本
|
|
194
|
+
if any(x in stripped.lower() for x in ['evaporating', 'seasoning', 'fiddling', 'thinking']):
|
|
195
|
+
continue
|
|
196
|
+
|
|
197
|
+
# 跳过特殊符号行
|
|
198
|
+
if stripped in ['❯', '>', '$', '⏺', '·', '( )', ';', '()']:
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
# 跳过状态标签
|
|
202
|
+
if stripped in ['问候', 'Basic Math', 'Greeting', 'Code', 'Analysis']:
|
|
203
|
+
continue
|
|
204
|
+
|
|
205
|
+
# 跳过用户输入回显
|
|
206
|
+
if user_input and stripped == user_input:
|
|
207
|
+
continue
|
|
208
|
+
|
|
209
|
+
# 跳过 esc 提示
|
|
210
|
+
if 'esc to' in stripped.lower():
|
|
211
|
+
continue
|
|
212
|
+
|
|
213
|
+
clean_lines.append(stripped)
|
|
214
|
+
|
|
215
|
+
return '\n'.join(clean_lines).strip()
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
终端渲染器 - 使用 pyte 正确模拟终端显示
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import pyte
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TerminalRenderer:
|
|
9
|
+
"""使用 pyte 模拟终端,获取真实的显示内容"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, columns: int = 200, lines: int = 100):
|
|
12
|
+
self.screen = pyte.Screen(columns, lines)
|
|
13
|
+
self.stream = pyte.Stream(self.screen)
|
|
14
|
+
self.stream.use_utf8 = True
|
|
15
|
+
|
|
16
|
+
def feed(self, data: bytes) -> None:
|
|
17
|
+
"""喂入原始终端数据"""
|
|
18
|
+
text = data.decode('utf-8', errors='replace')
|
|
19
|
+
self.stream.feed(text)
|
|
20
|
+
|
|
21
|
+
def get_display(self) -> str:
|
|
22
|
+
"""获取当前终端显示内容"""
|
|
23
|
+
lines = []
|
|
24
|
+
for line in self.screen.display:
|
|
25
|
+
stripped = line.rstrip()
|
|
26
|
+
if stripped: # 只保留非空行
|
|
27
|
+
lines.append(stripped)
|
|
28
|
+
|
|
29
|
+
return '\n'.join(lines)
|
|
30
|
+
|
|
31
|
+
def get_full_display(self) -> str:
|
|
32
|
+
"""获取完整的终端显示(包括空行,用于调试)"""
|
|
33
|
+
lines = []
|
|
34
|
+
for i, line in enumerate(self.screen.display):
|
|
35
|
+
stripped = line.rstrip()
|
|
36
|
+
if stripped:
|
|
37
|
+
lines.append(f"{i:3d}: {stripped}")
|
|
38
|
+
return '\n'.join(lines)
|
|
39
|
+
|
|
40
|
+
def clear(self) -> None:
|
|
41
|
+
"""清空终端"""
|
|
42
|
+
self.screen.reset()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_renderer():
|
|
46
|
+
"""测试渲染器"""
|
|
47
|
+
renderer = TerminalRenderer()
|
|
48
|
+
|
|
49
|
+
# 使用之前捕获的真实输出
|
|
50
|
+
raw_outputs = [
|
|
51
|
+
b'\x1b[?2026h\x1b[2K\x1b[G\x1b[1A\r\x1b[2C\x1b[2Ahello\r\x1b[2B',
|
|
52
|
+
b'\x1b]0;\xe2\x9c\xb3 Greeting\x07',
|
|
53
|
+
b'\x1b[?2026h\r\x1b[3A\x1b[48;2;55;55;55m\x1b[38;2;80;80;80m\xe2\x9d\xaf \x1b[38;2;255;255;255mhello \x1b[39m\x1b[49m',
|
|
54
|
+
b'\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\r\n\x1b[39m\x1b[22m \x1b[38;2;153;153;153m? for shortcuts\x1b[39m',
|
|
55
|
+
b'\x1b[?2026h\r\x1b[6A\x1b[38;2;255;255;255m\xe2\x8f\xba\x1b[1C\x1b[39m\xe4\xbd\xa0\xe5\xa5\xbd\xef\xbc\x81\xe6\x9c\x89\xe4\xbb\x80\xe4\xb9\x88\xe5\x8f\xaf\xe4\xbb\xa5\xe5\xb8\xae\xe4\xbd\xa0\xe7\x9a\x84\xe5\x90\x97\xef\xbc\x9f',
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
for data in raw_outputs:
|
|
59
|
+
renderer.feed(data)
|
|
60
|
+
|
|
61
|
+
result = renderer.get_display()
|
|
62
|
+
print("终端显示内容:")
|
|
63
|
+
print("=" * 60)
|
|
64
|
+
print(result)
|
|
65
|
+
print("=" * 60)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
test_renderer()
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "remote-claude",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "双端共享 Claude CLI 工具",
|
|
5
|
+
"bin": {
|
|
6
|
+
"remote-claude": "bin/remote-claude",
|
|
7
|
+
"cla": "bin/cla",
|
|
8
|
+
"cl": "bin/cl"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"postinstall": "bash scripts/postinstall.sh"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin/",
|
|
15
|
+
"scripts/",
|
|
16
|
+
"remote_claude.py",
|
|
17
|
+
"server/*.py",
|
|
18
|
+
"client/*.py",
|
|
19
|
+
"utils/*.py",
|
|
20
|
+
"lark_client/__init__.py",
|
|
21
|
+
"lark_client/*.py",
|
|
22
|
+
"stats/__init__.py",
|
|
23
|
+
"stats/*.py",
|
|
24
|
+
"pyproject.toml",
|
|
25
|
+
"uv.lock",
|
|
26
|
+
".env.example"
|
|
27
|
+
],
|
|
28
|
+
"os": ["darwin", "linux"],
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/yuyangzi/remote_claude.git"
|
|
33
|
+
},
|
|
34
|
+
"keywords": ["claude", "cli", "terminal", "pty", "lark"],
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=16"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"registry": "https://registry.npmjs.org/"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/pyproject.toml
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "remote-claude"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "双端共享 Claude CLI 工具"
|
|
5
|
+
requires-python = ">=3.9"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"lark-oapi>=1.4.24",
|
|
8
|
+
"python-dotenv>=1.0.0",
|
|
9
|
+
"pyte>=0.8.0",
|
|
10
|
+
"mixpanel>=5.0.0",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[project.scripts]
|
|
14
|
+
remote-claude = "remote_claude:main"
|