opencode-agent-ghost-panel 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/README.md +248 -0
- package/__init__.py +338 -0
- package/index.js +170 -0
- package/package.json +25 -0
- package/plugin.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# AGP OpenCode Plugin
|
|
2
|
+
|
|
3
|
+
Agent Ghost Panel 的 OpenCode 插件,提供无缝的终端多路复用集成。
|
|
4
|
+
|
|
5
|
+
## ✨ 特性
|
|
6
|
+
|
|
7
|
+
- 🔌 **自动启动**:OpenCode 启动时自动启动 AGP 守护进程
|
|
8
|
+
- 🛠️ **原生命令**:直接在 OpenCode 中使用 `/diff`、`/logs` 等命令
|
|
9
|
+
- 🌙 **幽灵模式**:Agent 在后台静默运行,不阻塞交互
|
|
10
|
+
- 🎨 **语法高亮**:通过 lumen 实现 git diff 语法高亮
|
|
11
|
+
- 🔄 **交互支持**:支持 ssh、sudo 等交互式命令
|
|
12
|
+
- 📡 **多路复用**:tmux 窗口管理
|
|
13
|
+
|
|
14
|
+
## 📦 安装
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# 1. 安装 AGP
|
|
18
|
+
pip install -e .
|
|
19
|
+
|
|
20
|
+
# 2. 安装 OpenCode 插件
|
|
21
|
+
agp install opencode
|
|
22
|
+
|
|
23
|
+
# 3. 安装 lumen(diff 高亮需要)
|
|
24
|
+
pip install lumen
|
|
25
|
+
|
|
26
|
+
# 4. 重启 OpenCode,插件自动加载
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 🚀 使用方法
|
|
30
|
+
|
|
31
|
+
### 启动 OpenCode + AGP
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# 一键启动 OpenCode + AGP(自动管理 tmux)
|
|
35
|
+
agp opencode
|
|
36
|
+
|
|
37
|
+
# 这将自动:
|
|
38
|
+
# 1. 启动 AGP 守护进程
|
|
39
|
+
# 2. 创建 tmux 会话 (agp-opencode)
|
|
40
|
+
# 3. 启动 OpenCode
|
|
41
|
+
# 4. 加载 AGP 插件
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### OpenCode 命令
|
|
45
|
+
|
|
46
|
+
启动 OpenCode 后,直接在对话中使用以下命令:
|
|
47
|
+
|
|
48
|
+
| 命令 | 别名 | 功能 |
|
|
49
|
+
|------|------|------|
|
|
50
|
+
| `/diff` | `/d` | 在 tmux 新窗口显示 git diff(带高亮) |
|
|
51
|
+
| `/diff <file>` | `/d <file>` | 显示指定文件的 diff |
|
|
52
|
+
| `/logs <file>` | `/l` | 实时监控日志文件 |
|
|
53
|
+
| `/interactive` | `/i` | 打开交互式终端(ssh/sudo 等) |
|
|
54
|
+
| `/status` | `/s` | 查看 AGP 状态 |
|
|
55
|
+
| `/sessions` | `/ss` | 列出所有 tmux 会话 |
|
|
56
|
+
|
|
57
|
+
### 使用示例
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
用户: 查看当前的 git 修改
|
|
61
|
+
Agent: 好的,让我用 AGP 在 diff 窗口显示修改
|
|
62
|
+
[执行 /diff 命令]
|
|
63
|
+
✅ 已在 tmux diff 窗口显示 git diff(带颜色高亮)
|
|
64
|
+
|
|
65
|
+
用户: 需要登录服务器
|
|
66
|
+
Agent: 好的,我来创建一个交互式终端
|
|
67
|
+
[执行 /interactive 命令]
|
|
68
|
+
✅ 交互式终端已创建,您可以输入 SSH 命令
|
|
69
|
+
|
|
70
|
+
用户: 监控应用日志
|
|
71
|
+
Agent: 好的,启动日志监控
|
|
72
|
+
[执行 /logs app.log 命令]
|
|
73
|
+
✅ 日志监控已启动
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## ⌨️ tmux 快捷键
|
|
77
|
+
|
|
78
|
+
在 tmux 窗口中:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
Ctrl+b, n # 下一个窗口
|
|
82
|
+
Ctrl+b, p # 上一个窗口
|
|
83
|
+
Ctrl+b, 0 # 切换到 OpenCode 窗口
|
|
84
|
+
Ctrl+b, 1 # 切换到 diff 窗口
|
|
85
|
+
Ctrl+b, 2 # 切换到 logs 窗口
|
|
86
|
+
Ctrl+b, 3 # 切换到 interactive 窗口
|
|
87
|
+
Ctrl+b, d # 分离会话(保留后台运行)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
状态栏会显示当前窗口的操作提示。
|
|
91
|
+
|
|
92
|
+
## 🏗️ 架构
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
┌─────────────────────────────────────────────────────────┐
|
|
96
|
+
│ OpenCode │
|
|
97
|
+
│ │
|
|
98
|
+
│ ┌───────────────────────────────────────────────┐ │
|
|
99
|
+
│ │ AGP 插件 │ │
|
|
100
|
+
│ │ • /diff → 创建 tmux diff 窗口 │ │
|
|
101
|
+
│ │ • /logs → 创建 tmux logs 窗口 │ │
|
|
102
|
+
│ │ • /interactive → 创建交互式终端 │ │
|
|
103
|
+
│ └───────────────────────────────────────────────┘ │
|
|
104
|
+
└─────────────────────────────────────────────────────────┘
|
|
105
|
+
│
|
|
106
|
+
▼
|
|
107
|
+
┌─────────────────────────────────────────────────────────┐
|
|
108
|
+
│ AGP 守护进程 (WebSocket) │
|
|
109
|
+
│ ws://127.0.0.1:8765 │
|
|
110
|
+
└─────────────────────────────────────────────────────────┘
|
|
111
|
+
│
|
|
112
|
+
▼
|
|
113
|
+
┌─────────────────────────────────────────────────────────┐
|
|
114
|
+
│ Tmux 会话 │
|
|
115
|
+
│ agp-opencode │
|
|
116
|
+
│ │
|
|
117
|
+
│ [opencode] ← OpenCode 主窗口 │
|
|
118
|
+
│ │ │
|
|
119
|
+
│ ├── [diff] ← lumen git diff(语法高亮) │
|
|
120
|
+
│ ├── [logs] ← tail -f 日志监控 │
|
|
121
|
+
│ └── [interactive] ← 交互式终端 │
|
|
122
|
+
└─────────────────────────────────────────────────────────┘
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## 🔧 开发
|
|
126
|
+
|
|
127
|
+
### 添加新命令
|
|
128
|
+
|
|
129
|
+
1. 在 `__init__.py` 中添加命令处理函数:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
async def _cmd_newcommand(args: list) -> Dict[str, Any]:
|
|
133
|
+
"""新命令说明"""
|
|
134
|
+
# 1. 检查依赖
|
|
135
|
+
# 2. 执行 tmux 命令
|
|
136
|
+
# 3. 返回结果
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
"success": True,
|
|
140
|
+
"message": "✅ 命令已执行",
|
|
141
|
+
"details": {...}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
2. 在 `AGPPlugin.execute()` 中添加分支:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
elif cmd in ["newcommand", "nc"]:
|
|
149
|
+
return await _cmd_newcommand(args)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
3. 在 `get_commands()` 中注册命令:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
{
|
|
156
|
+
"name": "newcommand",
|
|
157
|
+
"description": "新命令说明",
|
|
158
|
+
"usage": "/newcommand",
|
|
159
|
+
"aliases": ["nc"]
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### 测试
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
# 安装测试
|
|
167
|
+
agp install opencode
|
|
168
|
+
|
|
169
|
+
# 重启 OpenCode 测试
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## 📝 命令说明
|
|
173
|
+
|
|
174
|
+
### /diff
|
|
175
|
+
|
|
176
|
+
在 tmux 新窗口显示 git diff,使用 lumen 实现语法高亮。
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
/diff # 当前目录的 diff
|
|
180
|
+
/diff src/main.py # 指定文件的 diff
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
特点:
|
|
184
|
+
- 自动创建 tmux diff 窗口
|
|
185
|
+
- 使用 lumen 渲染,带语法高亮
|
|
186
|
+
- 状态栏显示操作提示
|
|
187
|
+
|
|
188
|
+
### /logs
|
|
189
|
+
|
|
190
|
+
实时监控日志文件。
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
/logs app.log # 监控日志文件
|
|
194
|
+
/logs /var/log/app.log # 绝对路径
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
特点:
|
|
198
|
+
- 使用 `tail -f` 实时监控
|
|
199
|
+
- 新窗口显示,不阻塞 OpenCode
|
|
200
|
+
- 可以同时监控多个日志文件
|
|
201
|
+
|
|
202
|
+
### /interactive
|
|
203
|
+
|
|
204
|
+
打开交互式终端,用于需要用户输入的命令。
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
/interactive # 打开空白交互终端
|
|
208
|
+
/interactive ssh user@host # 直接执行 ssh
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
适用场景:
|
|
212
|
+
- SSH 登录远程服务器
|
|
213
|
+
- sudo 命令需要密码
|
|
214
|
+
- 其他需要交互的命令
|
|
215
|
+
|
|
216
|
+
使用后:
|
|
217
|
+
- 输入命令并回车执行
|
|
218
|
+
- 输入 `exit` 退出交互终端
|
|
219
|
+
- 使用 `Ctrl+b, d` 关闭窗口
|
|
220
|
+
|
|
221
|
+
### /status
|
|
222
|
+
|
|
223
|
+
查看 AGP 守护进程状态。
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
/status # 或 /s
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
显示:
|
|
230
|
+
- 守护进程是否运行
|
|
231
|
+
- API 端点地址
|
|
232
|
+
- 会话数量
|
|
233
|
+
|
|
234
|
+
### /sessions
|
|
235
|
+
|
|
236
|
+
列出所有 tmux 会话。
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
/sessions # 或 /ss
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## 📄 许可证
|
|
243
|
+
|
|
244
|
+
MIT License
|
|
245
|
+
|
|
246
|
+
## 🤝 贡献
|
|
247
|
+
|
|
248
|
+
欢迎提交 Issue 或 Pull Request!
|
package/__init__.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AGP Plugin for OpenCode - Agent Ghost Panel 集成
|
|
3
|
+
OpenCode 插件:终端多路复用与交互增强
|
|
4
|
+
|
|
5
|
+
功能:
|
|
6
|
+
- /diff - 在 tmux 新窗口显示 git diff(使用 lumen)
|
|
7
|
+
- /status - 查看 AGP 状态
|
|
8
|
+
- /logs - 监控日志文件
|
|
9
|
+
- /interactive - 打开交互式终端
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import socket
|
|
15
|
+
import subprocess
|
|
16
|
+
from typing import Any, Dict
|
|
17
|
+
|
|
18
|
+
# 插件元数据
|
|
19
|
+
PLUGIN_NAME = "agent-ghost-panel"
|
|
20
|
+
PLUGIN_VERSION = "0.2.0"
|
|
21
|
+
PLUGIN_DESCRIPTION = "基于 Tmux 的终端多路复用与交互增强"
|
|
22
|
+
|
|
23
|
+
# AGP 配置
|
|
24
|
+
AGP_HOST = "127.0.0.1"
|
|
25
|
+
AGP_PORT = 8765
|
|
26
|
+
AGP_TMUX_SESSION = "agp-opencode"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AGPPlugin:
|
|
30
|
+
"""AGP OpenCode 插件"""
|
|
31
|
+
|
|
32
|
+
name = PLUGIN_NAME
|
|
33
|
+
version = PLUGIN_VERSION
|
|
34
|
+
description = PLUGIN_DESCRIPTION
|
|
35
|
+
|
|
36
|
+
# 插件加载时调用
|
|
37
|
+
@staticmethod
|
|
38
|
+
def on_load():
|
|
39
|
+
print("🔌 AGP Plugin loaded: Agent Ghost Panel")
|
|
40
|
+
|
|
41
|
+
# 确保 AGP 守护进程运行
|
|
42
|
+
_ensure_daemon()
|
|
43
|
+
|
|
44
|
+
# 插件卸载时调用
|
|
45
|
+
@staticmethod
|
|
46
|
+
def on_unload():
|
|
47
|
+
print("👋 AGP Plugin unloaded")
|
|
48
|
+
|
|
49
|
+
# 获取命令列表
|
|
50
|
+
@staticmethod
|
|
51
|
+
def get_commands() -> list:
|
|
52
|
+
return [
|
|
53
|
+
{
|
|
54
|
+
"name": "diff",
|
|
55
|
+
"description": "在 tmux 新窗口显示 git diff(带颜色高亮)",
|
|
56
|
+
"usage": "/diff [file]",
|
|
57
|
+
"aliases": ["d"],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"name": "status",
|
|
61
|
+
"description": "查看 AGP 状态",
|
|
62
|
+
"usage": "/status",
|
|
63
|
+
"aliases": ["s"],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "logs",
|
|
67
|
+
"description": "监控日志文件",
|
|
68
|
+
"usage": "/logs <log_file>",
|
|
69
|
+
"aliases": ["l"],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"name": "interactive",
|
|
73
|
+
"description": "打开交互式终端(ssh/sudo 等)",
|
|
74
|
+
"usage": "/interactive [command]",
|
|
75
|
+
"aliases": ["i"],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"name": "sessions",
|
|
79
|
+
"description": "列出所有 tmux 会话",
|
|
80
|
+
"usage": "/sessions",
|
|
81
|
+
"aliases": ["ss"],
|
|
82
|
+
},
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
# 执行命令
|
|
86
|
+
@staticmethod
|
|
87
|
+
async def execute(command: str, args: list) -> Dict[str, Any]:
|
|
88
|
+
"""执行 AGP 命令"""
|
|
89
|
+
cmd = command.lower()
|
|
90
|
+
|
|
91
|
+
if cmd in ["diff", "d"]:
|
|
92
|
+
return await _cmd_diff(args)
|
|
93
|
+
elif cmd in ["status", "s"]:
|
|
94
|
+
return await _cmd_status()
|
|
95
|
+
elif cmd in ["logs", "l"]:
|
|
96
|
+
return await _cmd_logs(args)
|
|
97
|
+
elif cmd in ["interactive", "i"]:
|
|
98
|
+
return await _cmd_interactive(args)
|
|
99
|
+
elif cmd in ["sessions", "ss"]:
|
|
100
|
+
return await _cmd_sessions()
|
|
101
|
+
|
|
102
|
+
return {"success": False, "error": f"Unknown command: {command}"}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ============================================================================
|
|
106
|
+
# 命令实现
|
|
107
|
+
# ============================================================================
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
async def _cmd_diff(args: list) -> Dict[str, Any]:
|
|
111
|
+
"""执行 /diff 命令 - 在 tmux 新窗口显示 diff"""
|
|
112
|
+
file_arg = args[0] if args else None
|
|
113
|
+
|
|
114
|
+
# 检查 tmux
|
|
115
|
+
result = subprocess.run(["which", "tmux"], capture_output=True)
|
|
116
|
+
if result.returncode != 0:
|
|
117
|
+
return {
|
|
118
|
+
"success": False,
|
|
119
|
+
"error": "tmux 未安装",
|
|
120
|
+
"message": "请安装 tmux: brew install tmux",
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# 检查 lumen
|
|
124
|
+
result = subprocess.run(["which", "lumen"], capture_output=True)
|
|
125
|
+
if result.returncode != 0:
|
|
126
|
+
return {
|
|
127
|
+
"success": False,
|
|
128
|
+
"error": "lumen 未安装",
|
|
129
|
+
"message": "请安装 lumen: pip install lumen",
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# 检查 tmux 会话
|
|
133
|
+
result = subprocess.run(["tmux", "has-session", "-t", AGP_TMUX_SESSION], capture_output=True)
|
|
134
|
+
if result.returncode != 0:
|
|
135
|
+
return {
|
|
136
|
+
"success": False,
|
|
137
|
+
"error": "tmux 会话不存在",
|
|
138
|
+
"message": "请先启动 OpenCode: agp opencode",
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# 获取当前工作目录
|
|
142
|
+
cwd = os.getcwd()
|
|
143
|
+
|
|
144
|
+
# 构建命令
|
|
145
|
+
if file_arg:
|
|
146
|
+
cmd = f"cd {file_arg} && lumen diff"
|
|
147
|
+
window_name = f"diff:{os.path.basename(file_arg)}"
|
|
148
|
+
else:
|
|
149
|
+
cmd = "lumen diff"
|
|
150
|
+
window_name = "diff"
|
|
151
|
+
|
|
152
|
+
# 设置状态栏提示
|
|
153
|
+
status_cmd = (
|
|
154
|
+
f"tmux set-option -t {AGP_TMUX_SESSION}:{window_name} status-format "
|
|
155
|
+
f'"#[fg=green]Ctrl+b,n#[default] 返回 | '
|
|
156
|
+
f'Ctrl+b,d#[default] 关闭 | lumen: git diff"'
|
|
157
|
+
)
|
|
158
|
+
subprocess.run(status_cmd, shell=True, capture_output=True)
|
|
159
|
+
|
|
160
|
+
# 创建新窗口并执行 lumen
|
|
161
|
+
subprocess.run(f"tmux new-window -t {AGP_TMUX_SESSION} -n '{window_name}'", capture_output=True)
|
|
162
|
+
subprocess.run(
|
|
163
|
+
f"tmux send-keys -t {AGP_TMUX_SESSION}:{window_name} 'cd {cwd}' C-m", capture_output=True
|
|
164
|
+
)
|
|
165
|
+
subprocess.run(
|
|
166
|
+
f"tmux send-keys -t {AGP_TMUX_SESSION}:{window_name} '{cmd}' C-m", capture_output=True
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
"success": True,
|
|
171
|
+
"message": f"✅ diff 面板已创建: {window_name}",
|
|
172
|
+
"details": {
|
|
173
|
+
"window": window_name,
|
|
174
|
+
"command": cmd,
|
|
175
|
+
"shortcuts": {
|
|
176
|
+
"next_window": "Ctrl+b, n",
|
|
177
|
+
"close_window": "Ctrl+b, d",
|
|
178
|
+
"attach_tmux": "tmux attach -t " + AGP_TMUX_SESSION,
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
async def _cmd_status() -> Dict[str, Any]:
|
|
185
|
+
"""执行 /status 命令"""
|
|
186
|
+
# 检查端口
|
|
187
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
188
|
+
sock.settimeout(1)
|
|
189
|
+
try:
|
|
190
|
+
sock.connect((AGP_HOST, AGP_PORT))
|
|
191
|
+
running = True
|
|
192
|
+
except socket.error:
|
|
193
|
+
running = False
|
|
194
|
+
finally:
|
|
195
|
+
sock.close()
|
|
196
|
+
|
|
197
|
+
if running:
|
|
198
|
+
# 获取详细状态
|
|
199
|
+
import websockets
|
|
200
|
+
import asyncio
|
|
201
|
+
import json
|
|
202
|
+
|
|
203
|
+
async def get_status():
|
|
204
|
+
async with websockets.connect(
|
|
205
|
+
f"ws://{AGP_HOST}:{AGP_PORT}", open_timeout=2, close_timeout=2
|
|
206
|
+
) as ws:
|
|
207
|
+
await ws.send(json.dumps({"action": "status"}))
|
|
208
|
+
return json.loads(await asyncio.wait_for(ws.recv(), timeout=2))
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
status = asyncio.run(get_status())
|
|
212
|
+
return {
|
|
213
|
+
"success": True,
|
|
214
|
+
"message": "✅ AGP 守护进程正在运行",
|
|
215
|
+
"details": {
|
|
216
|
+
"status": status.get("status"),
|
|
217
|
+
"sessions": status.get("session_count", 0),
|
|
218
|
+
"connections": status.get("connections", 0),
|
|
219
|
+
},
|
|
220
|
+
}
|
|
221
|
+
except Exception:
|
|
222
|
+
return {"success": True, "message": "✅ AGP 守护进程正在运行(端口已开放)"}
|
|
223
|
+
else:
|
|
224
|
+
return {"success": False, "error": "AGP 守护进程未运行", "message": "请运行: agp start"}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
async def _cmd_logs(args: list) -> Dict[str, Any]:
|
|
228
|
+
"""执行 /logs 命令"""
|
|
229
|
+
if not args:
|
|
230
|
+
return {"success": False, "error": "缺少日志文件路径", "message": "用法: /logs <log_file>"}
|
|
231
|
+
|
|
232
|
+
log_file = args[0]
|
|
233
|
+
|
|
234
|
+
if not os.path.exists(log_file):
|
|
235
|
+
return {
|
|
236
|
+
"success": False,
|
|
237
|
+
"error": f"文件不存在: {log_file}",
|
|
238
|
+
"message": "请检查日志文件路径",
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
# 创建日志监控窗口
|
|
242
|
+
window_name = f"logs:{os.path.basename(log_file)}"
|
|
243
|
+
|
|
244
|
+
status_cmd = (
|
|
245
|
+
f"tmux set-option -t {AGP_TMUX_SESSION}:{window_name} status-format "
|
|
246
|
+
f'"#[fg=yellow]tail -f {os.path.basename(log_file)}#[default] | Ctrl+b,d 关闭"'
|
|
247
|
+
)
|
|
248
|
+
subprocess.run(status_cmd, shell=True, capture_output=True)
|
|
249
|
+
|
|
250
|
+
subprocess.run(f"tmux new-window -t {AGP_TMUX_SESSION} -n '{window_name}'", capture_output=True)
|
|
251
|
+
subprocess.run(
|
|
252
|
+
f"tmux send-keys -t {AGP_TMUX_SESSION}:{window_name} 'tail -f {log_file}' C-m",
|
|
253
|
+
capture_output=True,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
"success": True,
|
|
258
|
+
"message": f"✅ 日志监控已启动: {log_file}",
|
|
259
|
+
"details": {"window": window_name, "shortcuts": {"close_window": "Ctrl+b, d"}},
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
async def _cmd_interactive(args: list) -> Dict[str, Any]:
|
|
264
|
+
"""执行 /interactive 命令"""
|
|
265
|
+
command = args[0] if args else None
|
|
266
|
+
|
|
267
|
+
window_name = "interactive"
|
|
268
|
+
|
|
269
|
+
status_cmd = (
|
|
270
|
+
f"tmux set-option -t {AGP_TMUX_SESSION}:{window_name} status-format "
|
|
271
|
+
f'"#[fg=cyan]交互式终端#[default] | Ctrl+b,d 关闭 | 输入 exit 退出"'
|
|
272
|
+
)
|
|
273
|
+
subprocess.run(status_cmd, shell=True, capture_output=True)
|
|
274
|
+
|
|
275
|
+
subprocess.run(f"tmux new-window -t {AGP_TMUX_SESSION} -n '{window_name}'", capture_output=True)
|
|
276
|
+
|
|
277
|
+
if command:
|
|
278
|
+
subprocess.run(
|
|
279
|
+
f"tmux send-keys -t {AGP_TMUX_SESSION}:{window_name} '{command}' C-m",
|
|
280
|
+
capture_output=True,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
"success": True,
|
|
285
|
+
"message": "✅ 交互式终端已创建",
|
|
286
|
+
"details": {
|
|
287
|
+
"window": window_name,
|
|
288
|
+
"command": command,
|
|
289
|
+
"shortcuts": {"close_window": "Ctrl+b, d", "send_exit": "输入 exit 然后回车"},
|
|
290
|
+
},
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
async def _cmd_sessions() -> Dict[str, Any]:
|
|
295
|
+
"""执行 /sessions 命令"""
|
|
296
|
+
result = subprocess.run(
|
|
297
|
+
["tmux", "list-sessions", "-F", "#{session_name}"], capture_output=True, text=True
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
if result.returncode == 0 and result.stdout:
|
|
301
|
+
sessions = result.stdout.strip().split("\n")
|
|
302
|
+
return {
|
|
303
|
+
"success": True,
|
|
304
|
+
"message": "✅ tmux 会话列表",
|
|
305
|
+
"details": {"sessions": sessions, "current": AGP_TMUX_SESSION in sessions},
|
|
306
|
+
}
|
|
307
|
+
else:
|
|
308
|
+
return {"success": True, "message": "暂无 tmux 会话", "details": {"sessions": []}}
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
# ============================================================================
|
|
312
|
+
# 辅助函数
|
|
313
|
+
# ============================================================================
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _ensure_daemon():
|
|
317
|
+
"""确保 AGP 守护进程正在运行"""
|
|
318
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
319
|
+
sock.settimeout(1)
|
|
320
|
+
try:
|
|
321
|
+
sock.connect((AGP_HOST, AGP_PORT))
|
|
322
|
+
# 端口已开放,守护进程已在运行
|
|
323
|
+
return True
|
|
324
|
+
except socket.error:
|
|
325
|
+
pass
|
|
326
|
+
finally:
|
|
327
|
+
sock.close()
|
|
328
|
+
|
|
329
|
+
# 尝试启动守护进程
|
|
330
|
+
try:
|
|
331
|
+
result = subprocess.run(["agp", "start"], capture_output=True, timeout=5)
|
|
332
|
+
return result.returncode == 0
|
|
333
|
+
except Exception:
|
|
334
|
+
return False
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# 导出插件类
|
|
338
|
+
plugin = AGPPlugin
|
package/index.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Ghost Panel Plugin for OpenCode
|
|
3
|
+
*
|
|
4
|
+
* 这个插件将 AGP 功能集成到 OpenCode 中。
|
|
5
|
+
* 支持的命令:
|
|
6
|
+
* - /diff - 在 tmux 新窗口显示 git diff
|
|
7
|
+
* - /logs - 监控日志文件
|
|
8
|
+
* - /interactive - 交互式终端
|
|
9
|
+
* - /status - 查看 AGP 状态
|
|
10
|
+
* - /sessions - 列出 tmux 会话
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { tool } from "@opencode-ai/plugin";
|
|
14
|
+
import { spawn } from "child_process";
|
|
15
|
+
|
|
16
|
+
// AGP 配置
|
|
17
|
+
const AGP_PORT = process.env.AGP_PORT || "8765";
|
|
18
|
+
const AGP_HOST = process.env.AGP_HOST || "127.0.0.1";
|
|
19
|
+
|
|
20
|
+
const z = tool.schema;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 执行 AGP 命令
|
|
24
|
+
*/
|
|
25
|
+
function executeAgp(args) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const proc = spawn("agp", args, {
|
|
28
|
+
cwd: process.cwd(),
|
|
29
|
+
env: { ...process.env, AGP_PORT, AGP_HOST }
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
let stdout = "";
|
|
33
|
+
let stderr = "";
|
|
34
|
+
|
|
35
|
+
proc.stdout.on("data", (data) => {
|
|
36
|
+
stdout += data.toString();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
proc.stderr.on("data", (data) => {
|
|
40
|
+
stderr += data.toString();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
proc.on("close", (code) => {
|
|
44
|
+
if (code === 0) {
|
|
45
|
+
resolve(stdout);
|
|
46
|
+
} else {
|
|
47
|
+
reject(new Error(stderr || `Exit code: ${code}`));
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
proc.on("error", (error) => {
|
|
52
|
+
reject(error);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 定义 AGP 工具
|
|
58
|
+
export const agpDiff = tool({
|
|
59
|
+
description: "在 tmux 新窗口显示 git diff(带语法高亮)",
|
|
60
|
+
args: {
|
|
61
|
+
file: z.string().optional().describe("文件路径(可选,默认当前目录)")
|
|
62
|
+
},
|
|
63
|
+
execute: async (args) => {
|
|
64
|
+
try {
|
|
65
|
+
const fileArg = args.file ? ` --file ${args.file}` : "";
|
|
66
|
+
const result = await executeAgp(["diff" + fileArg]);
|
|
67
|
+
return `✅ ${result}`;
|
|
68
|
+
} catch (error) {
|
|
69
|
+
return `❌ 执行失败: ${error}`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export const agpLogs = tool({
|
|
75
|
+
description: "实时监控日志文件",
|
|
76
|
+
args: {
|
|
77
|
+
file: z.string().describe("日志文件路径")
|
|
78
|
+
},
|
|
79
|
+
execute: async (args) => {
|
|
80
|
+
try {
|
|
81
|
+
const result = await executeAgp(["logs", args.file]);
|
|
82
|
+
return `✅ ${result}`;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return `❌ 执行失败: ${error}`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
export const agpInteractive = tool({
|
|
90
|
+
description: "打开交互式终端(用于 ssh、sudo 等需要用户输入的命令)",
|
|
91
|
+
args: {
|
|
92
|
+
command: z.string().optional().describe("初始命令(可选)")
|
|
93
|
+
},
|
|
94
|
+
execute: async (args) => {
|
|
95
|
+
try {
|
|
96
|
+
const cmdArg = args.command ? ` ${args.command}` : "";
|
|
97
|
+
const result = await executeAgp(["interactive" + cmdArg]);
|
|
98
|
+
return `✅ ${result}`;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
return `❌ 执行失败: ${error}`;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
export const agpStatus = tool({
|
|
106
|
+
description: "查看 AGP 守护进程状态",
|
|
107
|
+
args: {},
|
|
108
|
+
execute: async () => {
|
|
109
|
+
try {
|
|
110
|
+
const result = await executeAgp(["status"]);
|
|
111
|
+
return result;
|
|
112
|
+
} catch (error) {
|
|
113
|
+
return `❌ AGP 未运行。请先运行: agp start`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
export const agpSessions = tool({
|
|
119
|
+
description: "列出所有 tmux 会话",
|
|
120
|
+
args: {},
|
|
121
|
+
execute: async () => {
|
|
122
|
+
try {
|
|
123
|
+
const result = await executeAgp(["sessions"]);
|
|
124
|
+
return result;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
return `❌ 执行失败: ${error}`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
export const agpStart = tool({
|
|
132
|
+
description: "启动 AGP 守护进程",
|
|
133
|
+
args: {},
|
|
134
|
+
execute: async () => {
|
|
135
|
+
try {
|
|
136
|
+
const result = await executeAgp(["start"]);
|
|
137
|
+
return result;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
return `❌ 启动失败: ${error}`;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
export const agpStop = tool({
|
|
145
|
+
description: "停止 AGP 守护进程",
|
|
146
|
+
args: {},
|
|
147
|
+
execute: async () => {
|
|
148
|
+
try {
|
|
149
|
+
const result = await executeAgp(["stop"]);
|
|
150
|
+
return result;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
return `❌ 停止失败: ${error}`;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// 导出插件
|
|
158
|
+
export default {
|
|
159
|
+
name: "agent-ghost-panel",
|
|
160
|
+
version: "0.2.0",
|
|
161
|
+
tools: {
|
|
162
|
+
"agp-diff": agpDiff,
|
|
163
|
+
"agp-logs": agpLogs,
|
|
164
|
+
"agp-interactive": agpInteractive,
|
|
165
|
+
"agp-status": agpStatus,
|
|
166
|
+
"agp-sessions": agpSessions,
|
|
167
|
+
"agp-start": agpStart,
|
|
168
|
+
"agp-stop": agpStop
|
|
169
|
+
}
|
|
170
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-agent-ghost-panel",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "基于 Tmux 的终端多路复用与交互增强",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"tmux",
|
|
12
|
+
"terminal",
|
|
13
|
+
"multiplexer",
|
|
14
|
+
"ai-agent",
|
|
15
|
+
"opencode"
|
|
16
|
+
],
|
|
17
|
+
"author": "Your Name",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18.0.0"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@opencode-ai/plugin": "^1.0.0"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/plugin.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-ghost-panel",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "基于 Tmux 的终端多路复用与交互增强",
|
|
5
|
+
"author": "Your Name",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"entry": "__init__.py",
|
|
8
|
+
"plugin_class": "AGPOpenCodePlugin",
|
|
9
|
+
"capabilities": [
|
|
10
|
+
"commands",
|
|
11
|
+
"tools"
|
|
12
|
+
],
|
|
13
|
+
"commands": [
|
|
14
|
+
{
|
|
15
|
+
"name": "diff",
|
|
16
|
+
"description": "在 tmux 新窗口显示 git diff(带颜色高亮)",
|
|
17
|
+
"usage": "/diff [file]",
|
|
18
|
+
"aliases": ["d"]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "status",
|
|
22
|
+
"description": "查看 AGP 状态",
|
|
23
|
+
"usage": "/status",
|
|
24
|
+
"aliases": ["s"]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "logs",
|
|
28
|
+
"description": "监控日志文件",
|
|
29
|
+
"usage": "/logs <log_file>",
|
|
30
|
+
"aliases": ["l"]
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "interactive",
|
|
34
|
+
"description": "打开交互式终端",
|
|
35
|
+
"usage": "/interactive [command]",
|
|
36
|
+
"aliases": ["i"]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "sessions",
|
|
40
|
+
"description": "列出所有 tmux 会话",
|
|
41
|
+
"usage": "/sessions",
|
|
42
|
+
"aliases": ["ss"]
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"requirements": {
|
|
46
|
+
"python": ">=3.10",
|
|
47
|
+
"packages": [
|
|
48
|
+
"agent-ghost-pane"
|
|
49
|
+
],
|
|
50
|
+
"system": {
|
|
51
|
+
"tmux": ">=2.0",
|
|
52
|
+
"lumen": "pip install lumen"
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"configuration": {
|
|
56
|
+
"api_host": "127.0.0.1",
|
|
57
|
+
"api_port": 8765,
|
|
58
|
+
"tmux_session": "agp-opencode"
|
|
59
|
+
}
|
|
60
|
+
}
|