pikiclaw 0.2.35
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/LICENSE +21 -0
- package/README.md +315 -0
- package/dist/agent-driver.js +24 -0
- package/dist/bot-command-ui.js +299 -0
- package/dist/bot-commands.js +236 -0
- package/dist/bot-feishu-render.js +527 -0
- package/dist/bot-feishu.js +752 -0
- package/dist/bot-handler.js +115 -0
- package/dist/bot-menu.js +44 -0
- package/dist/bot-streaming.js +165 -0
- package/dist/bot-telegram-directory.js +74 -0
- package/dist/bot-telegram-live-preview.js +192 -0
- package/dist/bot-telegram-render.js +369 -0
- package/dist/bot-telegram.js +789 -0
- package/dist/bot.js +897 -0
- package/dist/channel-base.js +46 -0
- package/dist/channel-feishu.js +873 -0
- package/dist/channel-states.js +3 -0
- package/dist/channel-telegram.js +773 -0
- package/dist/cli-channels.js +24 -0
- package/dist/cli.js +484 -0
- package/dist/code-agent.js +1080 -0
- package/dist/config-validation.js +244 -0
- package/dist/dashboard-ui.js +31 -0
- package/dist/dashboard.js +840 -0
- package/dist/driver-claude.js +520 -0
- package/dist/driver-codex.js +1055 -0
- package/dist/driver-gemini.js +230 -0
- package/dist/mcp-bridge.js +192 -0
- package/dist/mcp-session-server.js +321 -0
- package/dist/onboarding.js +138 -0
- package/dist/process-control.js +259 -0
- package/dist/run.js +275 -0
- package/dist/session-status.js +43 -0
- package/dist/setup-wizard.js +231 -0
- package/dist/user-config.js +195 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pikiclaw contributors
|
|
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
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# pikiclaw
|
|
4
|
+
|
|
5
|
+
**One command. Turn any computer into a world-class productivity machine.**
|
|
6
|
+
|
|
7
|
+
*一行命令,让你的老电脑变成世界顶级生产力。*
|
|
8
|
+
*最好的 IM(Telegram / 飞书)× 最强的 Agent(Claude Code / Codex / Gemini CLI)× 你自己的电脑。*
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npx pikiclaw@latest
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
[](https://www.npmjs.com/package/pikiclaw)
|
|
15
|
+
[](LICENSE)
|
|
16
|
+
[](https://nodejs.org)
|
|
17
|
+
|
|
18
|
+
<!-- TODO: 替换为实际 demo GIF -->
|
|
19
|
+
<!--  -->
|
|
20
|
+
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Why pikiclaw?
|
|
26
|
+
|
|
27
|
+
市面上有很多 IM-to-Agent 桥接方案。它们要么自己造引擎(质量不如官方),要么什么都接(质量参差不齐)。
|
|
28
|
+
|
|
29
|
+
pikiclaw 的思路不同:**只挑最好的,然后把它们组合到极致。**
|
|
30
|
+
|
|
31
|
+
- **最好的 Agent** — Claude Code、Codex CLI、Gemini CLI,都是各家官方出品,不造轮子
|
|
32
|
+
- **最好的 IM** — Telegram(全球)+ 飞书(国内),不追求数量,每个都打磨到位
|
|
33
|
+
- **最好的执行环境** — 你自己的电脑,不是云端沙盒,什么都能干
|
|
34
|
+
|
|
35
|
+
结果就是:你在手机上发一句话,你的电脑就开始干活——小到改一行代码,大到通宵重构整个项目、整理几十份文档、跑完所有测试。
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
你(手机 IM)──→ pikiclaw ──→ 本地 Agent ──→ 你的电脑
|
|
39
|
+
↑ │
|
|
40
|
+
└──────── 流式进度 / 文件 / 截图 ←──────────┘
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
### 准备
|
|
48
|
+
|
|
49
|
+
- Node.js 18+
|
|
50
|
+
- 本机已安装 [`claude`](https://docs.anthropic.com/en/docs/claude-code)、[`codex`](https://github.com/openai/codex) 或 [`gemini`](https://github.com/google-gemini/gemini-cli) 中的任意一个
|
|
51
|
+
- 一个 [Telegram Bot Token](https://t.me/BotFather) 或[飞书应用](https://open.feishu.cn)凭证
|
|
52
|
+
|
|
53
|
+
### 一行启动
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
cd your-workspace/
|
|
57
|
+
npx pikiclaw@latest
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
启动后自动打开 **Web Dashboard**(`localhost:3939`),引导你完成全部配置。也可以用终端向导:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npx pikiclaw@latest --setup
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 开始派活
|
|
67
|
+
|
|
68
|
+
给你的 bot 发消息:
|
|
69
|
+
|
|
70
|
+
> "把 docs/ 目录下所有零散文档整理汇总,提取核心指标,输出一份报告。"
|
|
71
|
+
|
|
72
|
+
**就这样。你的电脑现在是一个随时待命的远程执行中枢。**
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Features
|
|
77
|
+
|
|
78
|
+
### Agent Engines
|
|
79
|
+
|
|
80
|
+
| Agent | 特点 |
|
|
81
|
+
|-------|------|
|
|
82
|
+
| **Claude Code** | Anthropic 官方 CLI · Thinking 展示 · 多模态 · 缓存优化 |
|
|
83
|
+
| **Codex CLI** | OpenAI 官方 CLI · Reasoning 展示 · 计划步骤追踪 · 实时用量 |
|
|
84
|
+
| **Gemini CLI** | Google 官方 CLI · 工具调用 · 流式输出 |
|
|
85
|
+
|
|
86
|
+
通过 `/agents` 随时切换引擎,`/models` 切换模型。
|
|
87
|
+
|
|
88
|
+
### IM Channels
|
|
89
|
+
|
|
90
|
+
| 渠道 | 消息编辑 | 文件上传 | 回调按钮 | 表情回应 | 消息线程 |
|
|
91
|
+
|------|---------|---------|---------|---------|---------|
|
|
92
|
+
| **Telegram** | ✅ | ✅ | ✅ | — | — |
|
|
93
|
+
| **飞书** | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
94
|
+
|
|
95
|
+
两个渠道可以**同时启动**。
|
|
96
|
+
|
|
97
|
+
### Core Capabilities
|
|
98
|
+
|
|
99
|
+
| 能力 | 说明 |
|
|
100
|
+
|------|------|
|
|
101
|
+
| 实时流式输出 | Agent 工作时消息持续更新 |
|
|
102
|
+
| Thinking / Reasoning | 实时查看 Agent 的思考和推理过程 |
|
|
103
|
+
| Token 追踪 | 输入/输出/缓存统计,上下文使用率实时显示 |
|
|
104
|
+
| 产物回传 | 截图、日志、生成文件自动发回 |
|
|
105
|
+
| 长程防休眠 | 系统级防休眠,小时级任务不中断 |
|
|
106
|
+
| 守护进程 | 崩溃自动重启,指数退避(3s → 60s) |
|
|
107
|
+
| 长文本处理 | 超长输出自动拆分或打包为 `.md` |
|
|
108
|
+
| 多会话管理 | 随时切换、恢复历史会话 |
|
|
109
|
+
| 图片/文件输入 | 截图、PDF、文档直接发给 Agent |
|
|
110
|
+
| 项目 Skills | `.pikiclaw/skills/` 自定义技能,兼容 `.claude/commands/` |
|
|
111
|
+
| 安全模式 | 危险操作推送确认卡片,白名单访问控制 |
|
|
112
|
+
| Web Dashboard | 可视化配置、会话浏览、主机监控 |
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Comparison
|
|
117
|
+
|
|
118
|
+
### pikiclaw vs. 其他方案
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
在你的环境里执行
|
|
122
|
+
│
|
|
123
|
+
终端 CLI │ pikiclaw
|
|
124
|
+
(人要守着) │ (人可以走)
|
|
125
|
+
│
|
|
126
|
+
─────────────┼─────────────
|
|
127
|
+
不方便控制 │ 随时随地控制
|
|
128
|
+
│
|
|
129
|
+
SSH+tmux │ 云端 Agent
|
|
130
|
+
(手机上很痛苦) │ (不是你的环境)
|
|
131
|
+
│
|
|
132
|
+
在沙盒/远端执行
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
| | 终端直接跑 | SSH + tmux | 云端 Agent | **pikiclaw** |
|
|
136
|
+
|---|---|---|---|---|
|
|
137
|
+
| 执行环境 | ✅ 本地 | ✅ 本地 | ❌ 沙盒 | ✅ 本地 |
|
|
138
|
+
| 走开后还能跑 | ❌ 合盖就断 | ⚠️ 要配 tmux | ✅ | ✅ 防休眠 + 守护进程 |
|
|
139
|
+
| 手机可控 | ❌ | ⚠️ 打字痛苦 | ✅ | ✅ IM 原生 |
|
|
140
|
+
| 实时看进度 | ✅ 终端 | ⚠️ 得连上去看 | ❌ 多数是黑盒 | ✅ 流式推到聊天 |
|
|
141
|
+
| 结果自动回传 | ❌ | ❌ | ⚠️ 看平台 | ✅ 截图/文件/长文本 |
|
|
142
|
+
| 配置门槛 | 无 | SSH/穿透/tmux | 注册/付费 | `npx` 一行 |
|
|
143
|
+
|
|
144
|
+
### pikiclaw vs. 同类项目
|
|
145
|
+
|
|
146
|
+
| | **pikiclaw** | OpenClaw | cc-connect |
|
|
147
|
+
|---|---|---|---|
|
|
148
|
+
| **理念** | **精选最好的工具,组合到极致** | 开源自主 AI 智能体生态 | 多渠道多端连接器 |
|
|
149
|
+
| **Agent** | Claude Code / Codex / Gemini CLI(官方出品) | 内置 Agent(自接模型) | 多种本地 CLI |
|
|
150
|
+
| **IM** | Telegram + 飞书(深度打磨) | Web / 移动端 | Slack / Discord / LINE 等 |
|
|
151
|
+
| **长程任务** | ✅ 防休眠 · 守护进程 · 异常自愈 | ❌ 偏即时任务 | ❌ 偏短对话 |
|
|
152
|
+
| **产物回传** | ✅ 截图 · 文件 · 长文本打包 | ⚠️ 依赖客户端 | ⚠️ 基础附件 |
|
|
153
|
+
| **流式体验** | ✅ IM 内实时流式 | ✅ | ⚠️ 看桥接能力 |
|
|
154
|
+
| **上手成本** | **一行 `npx`** | 需部署后端 | 需安装服务端 |
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Use Cases
|
|
159
|
+
|
|
160
|
+
pikiclaw 不限于编程。你的 Agent 能做什么,pikiclaw 就能远程调度什么。
|
|
161
|
+
|
|
162
|
+
**工程重构** — "把整个项目从 JS 迁移到 TS,跑测试直到全部通过。搞定告诉我。"
|
|
163
|
+
|
|
164
|
+
**文档处理** — "把 docs/ 下所有零散文档整理汇总,提取核心指标,输出一份报告。"
|
|
165
|
+
|
|
166
|
+
**研究分析** — "下载这 5 篇论文的 PDF,逐篇阅读,写一份 3000 字综述。"
|
|
167
|
+
|
|
168
|
+
**批量任务** — "把 data/ 下所有旧版报表转换成新格式,汇总成一份,确认数据条数。"
|
|
169
|
+
|
|
170
|
+
**巡检自愈** — "跑一下数据同步任务,把报错自动修好,直到全部通过。"
|
|
171
|
+
|
|
172
|
+
**竞品分析** — "分析竞品网站的落地页,把我们的页面改成类似风格,改完截图发我。"
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Commands
|
|
177
|
+
|
|
178
|
+
| 命令 | 说明 |
|
|
179
|
+
|------|------|
|
|
180
|
+
| `/start` | 菜单、当前 Agent 和工作目录 |
|
|
181
|
+
| `/agents` | 切换 Agent |
|
|
182
|
+
| `/models` | 查看并切换模型 |
|
|
183
|
+
| `/sessions` | 查看并切换历史会话 |
|
|
184
|
+
| `/switch` | 浏览并切换工作目录 |
|
|
185
|
+
| `/status` | 状态、会话信息、Token 统计 |
|
|
186
|
+
| `/host` | 主机 CPU / 内存 / 磁盘 / 电量 |
|
|
187
|
+
| `/skills` | 浏览项目 skills |
|
|
188
|
+
| `/restart` | 拉取最新版本并重启 |
|
|
189
|
+
| `/sk_<name>` | 执行项目技能 |
|
|
190
|
+
|
|
191
|
+
> 普通文本直接发给当前 Agent。
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Configuration
|
|
196
|
+
|
|
197
|
+
### 常见用法
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
npx pikiclaw@latest # 自动检测,打开 Dashboard
|
|
201
|
+
npx pikiclaw@latest -a claude # 指定 Agent
|
|
202
|
+
npx pikiclaw@latest -a gemini # 使用 Gemini
|
|
203
|
+
npx pikiclaw@latest -w ~/workspace # 指定工作目录
|
|
204
|
+
npx pikiclaw@latest -m claude-sonnet-4-6 # 指定模型
|
|
205
|
+
npx pikiclaw@latest --safe-mode # 安全模式
|
|
206
|
+
npx pikiclaw@latest --allowed-ids ID # 白名单
|
|
207
|
+
npx pikiclaw@latest --doctor # 检查环境
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### CLI 参数
|
|
211
|
+
|
|
212
|
+
| 参数 | 默认值 | 说明 |
|
|
213
|
+
|------|--------|------|
|
|
214
|
+
| `-t, --token` | — | Bot Token |
|
|
215
|
+
| `-a, --agent` | `codex` | 默认 Agent |
|
|
216
|
+
| `-m, --model` | Agent 默认 | 覆盖模型 |
|
|
217
|
+
| `-w, --workdir` | 已保存或当前目录 | 工作目录 |
|
|
218
|
+
| `--safe-mode` | `false` | Agent 自身权限模型 |
|
|
219
|
+
| `--full-access` | `true` | 无确认执行 |
|
|
220
|
+
| `--allowed-ids` | — | 限制 chat/user ID |
|
|
221
|
+
| `--timeout` | `1800` | 单次请求最大秒数 |
|
|
222
|
+
| `--no-daemon` | — | 禁用守护进程 |
|
|
223
|
+
| `--no-dashboard` | — | 不启动 Dashboard |
|
|
224
|
+
| `--dashboard-port` | `3939` | Dashboard 端口 |
|
|
225
|
+
| `--doctor` | — | 检查环境 |
|
|
226
|
+
| `--setup` | — | Setup Wizard |
|
|
227
|
+
|
|
228
|
+
<details>
|
|
229
|
+
<summary>环境变量</summary>
|
|
230
|
+
|
|
231
|
+
**通用:**
|
|
232
|
+
|
|
233
|
+
| 变量 | 说明 |
|
|
234
|
+
|------|------|
|
|
235
|
+
| `DEFAULT_AGENT` | 默认 Agent |
|
|
236
|
+
| `PIKICLAW_WORKDIR` | 默认工作目录 |
|
|
237
|
+
| `PIKICLAW_TIMEOUT` | 请求超时(秒) |
|
|
238
|
+
| `PIKICLAW_ALLOWED_IDS` | 白名单 |
|
|
239
|
+
| `PIKICLAW_FULL_ACCESS` | 完全访问模式 |
|
|
240
|
+
| `PIKICLAW_RESTART_CMD` | 自定义重启命令 |
|
|
241
|
+
|
|
242
|
+
**Telegram:**
|
|
243
|
+
|
|
244
|
+
| 变量 | 说明 |
|
|
245
|
+
|------|------|
|
|
246
|
+
| `TELEGRAM_BOT_TOKEN` | Bot Token |
|
|
247
|
+
| `TELEGRAM_ALLOWED_CHAT_IDS` | 白名单 |
|
|
248
|
+
|
|
249
|
+
**飞书:**
|
|
250
|
+
|
|
251
|
+
| 变量 | 说明 |
|
|
252
|
+
|------|------|
|
|
253
|
+
| `FEISHU_APP_ID` | App ID |
|
|
254
|
+
| `FEISHU_APP_SECRET` | App Secret |
|
|
255
|
+
| `FEISHU_DOMAIN` | API 域名(默认 `https://open.feishu.cn`) |
|
|
256
|
+
| `FEISHU_ALLOWED_CHAT_IDS` | 白名单 |
|
|
257
|
+
|
|
258
|
+
**Claude:**
|
|
259
|
+
|
|
260
|
+
| 变量 | 说明 |
|
|
261
|
+
|------|------|
|
|
262
|
+
| `CLAUDE_MODEL` | 模型 |
|
|
263
|
+
| `CLAUDE_PERMISSION_MODE` | 权限模式 |
|
|
264
|
+
| `CLAUDE_EXTRA_ARGS` | 额外参数 |
|
|
265
|
+
|
|
266
|
+
**Codex:**
|
|
267
|
+
|
|
268
|
+
| 变量 | 说明 |
|
|
269
|
+
|------|------|
|
|
270
|
+
| `CODEX_MODEL` | 模型 |
|
|
271
|
+
| `CODEX_REASONING_EFFORT` | 推理强度 |
|
|
272
|
+
| `CODEX_FULL_ACCESS` | 完全访问 |
|
|
273
|
+
| `CODEX_EXTRA_ARGS` | 额外参数 |
|
|
274
|
+
|
|
275
|
+
**Gemini:**
|
|
276
|
+
|
|
277
|
+
| 变量 | 说明 |
|
|
278
|
+
|------|------|
|
|
279
|
+
| `GEMINI_MODEL` | 模型 |
|
|
280
|
+
| `GEMINI_EXTRA_ARGS` | 额外参数 |
|
|
281
|
+
|
|
282
|
+
</details>
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## Status
|
|
287
|
+
|
|
288
|
+
| 维度 | 状态 |
|
|
289
|
+
|------|------|
|
|
290
|
+
| IM | Telegram ✅ · 飞书 ✅ · WhatsApp(规划中) |
|
|
291
|
+
| Agent | Claude Code ✅ · Codex CLI ✅ · Gemini CLI ✅ |
|
|
292
|
+
| 面板 | Web Dashboard ✅ |
|
|
293
|
+
| 国际化 | 中文 ✅ · English ✅ |
|
|
294
|
+
| 平台 | macOS ✅ · Linux ✅ |
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Development
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
git clone https://github.com/nicepkg/pikiclaw.git
|
|
302
|
+
cd pikiclaw
|
|
303
|
+
npm install
|
|
304
|
+
echo "TELEGRAM_BOT_TOKEN=your_token" > .env
|
|
305
|
+
set -a && source .env && npx tsx src/cli.ts
|
|
306
|
+
npm test
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
架构详情见 [ARCHITECTURE.md](ARCHITECTURE.md)。
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## License
|
|
314
|
+
|
|
315
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-driver.ts — Agent driver interface and registry.
|
|
3
|
+
*
|
|
4
|
+
* Each CLI agent (claude, codex, gemini, ...) implements AgentDriver.
|
|
5
|
+
* Register with `registerDriver()`, look up with `getDriver()`.
|
|
6
|
+
*/
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Registry
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
const drivers = new Map();
|
|
11
|
+
export function registerDriver(d) { drivers.set(d.id, d); }
|
|
12
|
+
export function getDriver(id) {
|
|
13
|
+
const d = drivers.get(id);
|
|
14
|
+
if (!d)
|
|
15
|
+
throw new Error(`Unknown agent: ${id}. Available: ${[...drivers.keys()].join(', ')}`);
|
|
16
|
+
return d;
|
|
17
|
+
}
|
|
18
|
+
export function hasDriver(id) { return drivers.has(id); }
|
|
19
|
+
export function allDrivers() { return [...drivers.values()]; }
|
|
20
|
+
export function allDriverIds() { return [...drivers.keys()]; }
|
|
21
|
+
export function shutdownAllDrivers() {
|
|
22
|
+
for (const d of drivers.values())
|
|
23
|
+
d.shutdown();
|
|
24
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { normalizeAgent } from './bot.js';
|
|
2
|
+
import { getAgentsListData, getModelsListData, getSessionsPageData, getSkillsListData, modelMatchesSelection, resolveSkillPrompt, } from './bot-commands.js';
|
|
3
|
+
function chunkRows(items, columns) {
|
|
4
|
+
const rows = [];
|
|
5
|
+
const size = Math.max(1, columns);
|
|
6
|
+
for (let i = 0; i < items.length; i += size)
|
|
7
|
+
rows.push(items.slice(i, i + size));
|
|
8
|
+
return rows;
|
|
9
|
+
}
|
|
10
|
+
function buttonStateFromFlags(opts) {
|
|
11
|
+
if (opts.unavailable)
|
|
12
|
+
return 'unavailable';
|
|
13
|
+
if (opts.isRunning)
|
|
14
|
+
return 'running';
|
|
15
|
+
if (opts.isCurrent)
|
|
16
|
+
return 'current';
|
|
17
|
+
return 'default';
|
|
18
|
+
}
|
|
19
|
+
export function encodeCommandAction(action) {
|
|
20
|
+
switch (action.kind) {
|
|
21
|
+
case 'sessions.page':
|
|
22
|
+
return `sp:${Math.max(0, action.page)}`;
|
|
23
|
+
case 'session.new':
|
|
24
|
+
return 'sess:new';
|
|
25
|
+
case 'session.switch':
|
|
26
|
+
return `sess:${action.sessionId}`;
|
|
27
|
+
case 'agent.switch':
|
|
28
|
+
return `ag:${action.agent}`;
|
|
29
|
+
case 'model.switch':
|
|
30
|
+
return `mod:${action.modelId}`;
|
|
31
|
+
case 'effort.set':
|
|
32
|
+
return `eff:${action.effort}`;
|
|
33
|
+
case 'skill.run':
|
|
34
|
+
return `skr:${action.command}`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function decodeCommandAction(data) {
|
|
38
|
+
if (data === 'sess:new')
|
|
39
|
+
return { kind: 'session.new' };
|
|
40
|
+
if (data.startsWith('sp:')) {
|
|
41
|
+
const page = Number.parseInt(data.slice(3), 10);
|
|
42
|
+
if (!Number.isFinite(page) || page < 0)
|
|
43
|
+
return null;
|
|
44
|
+
return { kind: 'sessions.page', page };
|
|
45
|
+
}
|
|
46
|
+
if (data.startsWith('sess:')) {
|
|
47
|
+
const sessionId = data.slice(5);
|
|
48
|
+
if (!sessionId)
|
|
49
|
+
return null;
|
|
50
|
+
return { kind: 'session.switch', sessionId };
|
|
51
|
+
}
|
|
52
|
+
if (data.startsWith('ag:')) {
|
|
53
|
+
try {
|
|
54
|
+
return { kind: 'agent.switch', agent: normalizeAgent(data.slice(3)) };
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (data.startsWith('mod:')) {
|
|
61
|
+
const modelId = data.slice(4);
|
|
62
|
+
if (!modelId)
|
|
63
|
+
return null;
|
|
64
|
+
return { kind: 'model.switch', modelId };
|
|
65
|
+
}
|
|
66
|
+
if (data.startsWith('eff:')) {
|
|
67
|
+
const effort = data.slice(4);
|
|
68
|
+
if (!effort)
|
|
69
|
+
return null;
|
|
70
|
+
return { kind: 'effort.set', effort };
|
|
71
|
+
}
|
|
72
|
+
if (data.startsWith('skr:')) {
|
|
73
|
+
const command = data.slice(4);
|
|
74
|
+
if (!command)
|
|
75
|
+
return null;
|
|
76
|
+
return { kind: 'skill.run', command };
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
export async function buildSessionsCommandView(bot, chatId, page, pageSize = 5) {
|
|
81
|
+
const data = await getSessionsPageData(bot, chatId, page, pageSize);
|
|
82
|
+
const sessionButtons = data.sessions.map(session => [{
|
|
83
|
+
label: session.title,
|
|
84
|
+
action: { kind: 'session.switch', sessionId: session.key },
|
|
85
|
+
state: buttonStateFromFlags({ isCurrent: session.isCurrent, isRunning: session.isRunning }),
|
|
86
|
+
primary: session.isCurrent,
|
|
87
|
+
}]);
|
|
88
|
+
const navRow = [];
|
|
89
|
+
if (data.page > 0)
|
|
90
|
+
navRow.push({ label: `◀ p${data.page}`, action: { kind: 'sessions.page', page: data.page - 1 } });
|
|
91
|
+
navRow.push({ label: '+ New', action: { kind: 'session.new' } });
|
|
92
|
+
if (data.page < data.totalPages - 1)
|
|
93
|
+
navRow.push({ label: `p${data.page + 2} ▶`, action: { kind: 'sessions.page', page: data.page + 1 } });
|
|
94
|
+
return {
|
|
95
|
+
kind: 'sessions',
|
|
96
|
+
title: 'Sessions',
|
|
97
|
+
detail: data.agent,
|
|
98
|
+
metaLines: [`${data.total} total · p${data.page + 1}/${data.totalPages}`],
|
|
99
|
+
items: data.sessions.map(session => ({
|
|
100
|
+
label: session.title,
|
|
101
|
+
detail: session.time,
|
|
102
|
+
state: buttonStateFromFlags({ isCurrent: session.isCurrent, isRunning: session.isRunning }),
|
|
103
|
+
})),
|
|
104
|
+
emptyText: 'No sessions found.',
|
|
105
|
+
helperText: data.totalPages > 1
|
|
106
|
+
? `Use the controls below to switch or turn pages.`
|
|
107
|
+
: 'Use the controls below to switch or start a new session.',
|
|
108
|
+
rows: navRow.length ? [...sessionButtons, navRow] : sessionButtons,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
export function buildAgentsCommandView(bot, chatId) {
|
|
112
|
+
const data = getAgentsListData(bot, chatId);
|
|
113
|
+
const actions = data.agents
|
|
114
|
+
.filter(agent => agent.installed)
|
|
115
|
+
.map(agent => ({
|
|
116
|
+
label: agent.agent,
|
|
117
|
+
action: { kind: 'agent.switch', agent: agent.agent },
|
|
118
|
+
state: buttonStateFromFlags({ isCurrent: agent.isCurrent }),
|
|
119
|
+
primary: agent.isCurrent,
|
|
120
|
+
}));
|
|
121
|
+
return {
|
|
122
|
+
kind: 'agents',
|
|
123
|
+
title: 'Agents',
|
|
124
|
+
metaLines: [],
|
|
125
|
+
items: data.agents.map(agent => ({
|
|
126
|
+
label: agent.agent,
|
|
127
|
+
detail: agent.installed
|
|
128
|
+
? (agent.version ? `Version ${agent.version}` : 'Installed')
|
|
129
|
+
: 'Not installed',
|
|
130
|
+
state: buttonStateFromFlags({ isCurrent: agent.isCurrent, unavailable: !agent.installed }),
|
|
131
|
+
})),
|
|
132
|
+
helperText: 'Use the controls below to switch agents.',
|
|
133
|
+
rows: chunkRows(actions, 3),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
export async function buildModelsCommandView(bot, chatId) {
|
|
137
|
+
const data = await getModelsListData(bot, chatId);
|
|
138
|
+
const models = [...data.models].sort((a, b) => Number(b.isCurrent) - Number(a.isCurrent));
|
|
139
|
+
const modelButtons = models.map(model => ({
|
|
140
|
+
label: model.alias || model.id,
|
|
141
|
+
action: { kind: 'model.switch', modelId: model.id },
|
|
142
|
+
state: buttonStateFromFlags({ isCurrent: model.isCurrent }),
|
|
143
|
+
primary: model.isCurrent,
|
|
144
|
+
}));
|
|
145
|
+
const rows = chunkRows(modelButtons, modelButtons.some(button => button.label.length > 14) ? 1 : 2);
|
|
146
|
+
if (data.effort) {
|
|
147
|
+
rows.push(data.effort.levels.map(level => ({
|
|
148
|
+
label: level.label,
|
|
149
|
+
action: { kind: 'effort.set', effort: level.id },
|
|
150
|
+
state: buttonStateFromFlags({ isCurrent: level.isCurrent }),
|
|
151
|
+
primary: level.isCurrent,
|
|
152
|
+
})));
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
kind: 'models',
|
|
156
|
+
title: 'Models',
|
|
157
|
+
detail: data.agent,
|
|
158
|
+
metaLines: [
|
|
159
|
+
...(data.sources.length ? [`Source: ${data.sources.join(', ')}`] : []),
|
|
160
|
+
...(data.note ? [data.note] : []),
|
|
161
|
+
...(data.effort ? [`Thinking Effort: ${data.effort.current}`] : []),
|
|
162
|
+
],
|
|
163
|
+
items: models.map(model => ({
|
|
164
|
+
label: model.alias || model.id,
|
|
165
|
+
detail: model.alias ? model.id : null,
|
|
166
|
+
state: buttonStateFromFlags({ isCurrent: model.isCurrent }),
|
|
167
|
+
})),
|
|
168
|
+
emptyText: 'No discoverable models found.',
|
|
169
|
+
helperText: data.models.length ? 'Use the controls below to switch models.' : null,
|
|
170
|
+
rows,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
export function buildSkillsCommandView(bot, chatId) {
|
|
174
|
+
const data = getSkillsListData(bot, chatId);
|
|
175
|
+
const buttons = data.skills.map(skill => ({
|
|
176
|
+
label: skill.label,
|
|
177
|
+
action: { kind: 'skill.run', command: skill.command },
|
|
178
|
+
}));
|
|
179
|
+
return {
|
|
180
|
+
kind: 'skills',
|
|
181
|
+
title: 'Skills',
|
|
182
|
+
detail: data.agent,
|
|
183
|
+
metaLines: [`Workdir: ${data.workdir}`],
|
|
184
|
+
items: data.skills.map(skill => ({
|
|
185
|
+
label: skill.label,
|
|
186
|
+
detail: skill.description || `/${skill.command}`,
|
|
187
|
+
})),
|
|
188
|
+
emptyText: 'No project skills found.',
|
|
189
|
+
helperText: data.skills.length ? 'Use the controls below to run a skill.' : null,
|
|
190
|
+
rows: chunkRows(buttons, buttons.some(button => button.label.length > 14) ? 1 : 2),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
export async function executeCommandAction(bot, chatId, action, opts = {}) {
|
|
194
|
+
const sessionsPageSize = opts.sessionsPageSize ?? 5;
|
|
195
|
+
switch (action.kind) {
|
|
196
|
+
case 'sessions.page':
|
|
197
|
+
return {
|
|
198
|
+
kind: 'view',
|
|
199
|
+
view: await buildSessionsCommandView(bot, chatId, action.page, sessionsPageSize),
|
|
200
|
+
callbackText: '',
|
|
201
|
+
};
|
|
202
|
+
case 'session.new':
|
|
203
|
+
bot.resetConversationForChat(chatId);
|
|
204
|
+
return {
|
|
205
|
+
kind: 'notice',
|
|
206
|
+
callbackText: 'New session',
|
|
207
|
+
notice: {
|
|
208
|
+
title: 'New Session',
|
|
209
|
+
detail: 'Send a message to start.',
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
case 'session.switch': {
|
|
213
|
+
const chat = bot.chat(chatId);
|
|
214
|
+
const result = await bot.fetchSessions(chat.agent);
|
|
215
|
+
if (!result.ok)
|
|
216
|
+
return { kind: 'noop', message: 'Failed to load sessions' };
|
|
217
|
+
const session = result.sessions.find(entry => entry.sessionId === action.sessionId);
|
|
218
|
+
if (!session)
|
|
219
|
+
return { kind: 'noop', message: 'Session not found' };
|
|
220
|
+
const runtime = bot.adoptExistingSessionForChat(chatId, session);
|
|
221
|
+
const displayId = session.sessionId || action.sessionId;
|
|
222
|
+
return {
|
|
223
|
+
kind: 'notice',
|
|
224
|
+
callbackText: `Switched: ${displayId.slice(0, 12)}`,
|
|
225
|
+
notice: {
|
|
226
|
+
title: 'Session Switched',
|
|
227
|
+
value: displayId,
|
|
228
|
+
detail: 'Switched successfully',
|
|
229
|
+
valueMode: 'code',
|
|
230
|
+
},
|
|
231
|
+
session: runtime,
|
|
232
|
+
previewSession: { agent: session.agent, sessionId: session.sessionId },
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
case 'agent.switch': {
|
|
236
|
+
const chat = bot.chat(chatId);
|
|
237
|
+
if (chat.agent === action.agent)
|
|
238
|
+
return { kind: 'noop', message: `Already using ${action.agent}` };
|
|
239
|
+
bot.switchAgentForChat(chatId, action.agent);
|
|
240
|
+
return {
|
|
241
|
+
kind: 'notice',
|
|
242
|
+
callbackText: `Switched to ${action.agent}`,
|
|
243
|
+
notice: {
|
|
244
|
+
title: 'Agent',
|
|
245
|
+
value: action.agent,
|
|
246
|
+
detail: 'Session reset',
|
|
247
|
+
valueMode: 'plain',
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
case 'model.switch': {
|
|
252
|
+
const chat = bot.chat(chatId);
|
|
253
|
+
const currentModel = bot.modelForAgent(chat.agent);
|
|
254
|
+
if (modelMatchesSelection(chat.agent, action.modelId, currentModel)) {
|
|
255
|
+
return { kind: 'noop', message: `Already using ${action.modelId}` };
|
|
256
|
+
}
|
|
257
|
+
bot.switchModelForChat(chatId, action.modelId);
|
|
258
|
+
return {
|
|
259
|
+
kind: 'notice',
|
|
260
|
+
callbackText: `Switched to ${action.modelId}`,
|
|
261
|
+
notice: {
|
|
262
|
+
title: 'Model',
|
|
263
|
+
value: action.modelId,
|
|
264
|
+
detail: `${chat.agent} · session reset`,
|
|
265
|
+
valueMode: 'code',
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
case 'effort.set': {
|
|
270
|
+
const chat = bot.chat(chatId);
|
|
271
|
+
const currentEffort = bot.effortForAgent(chat.agent);
|
|
272
|
+
if (action.effort === currentEffort) {
|
|
273
|
+
return { kind: 'noop', message: `Already using ${action.effort} effort` };
|
|
274
|
+
}
|
|
275
|
+
bot.switchEffortForChat(chatId, action.effort);
|
|
276
|
+
return {
|
|
277
|
+
kind: 'notice',
|
|
278
|
+
callbackText: `Effort set to ${action.effort}`,
|
|
279
|
+
notice: {
|
|
280
|
+
title: 'Thinking Effort',
|
|
281
|
+
value: action.effort,
|
|
282
|
+
detail: `${chat.agent} · takes effect on next message`,
|
|
283
|
+
valueMode: 'code',
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
case 'skill.run': {
|
|
288
|
+
const resolved = resolveSkillPrompt(bot, chatId, action.command, '');
|
|
289
|
+
if (!resolved)
|
|
290
|
+
return { kind: 'noop', message: 'Skill not found' };
|
|
291
|
+
return {
|
|
292
|
+
kind: 'skill',
|
|
293
|
+
prompt: resolved.prompt,
|
|
294
|
+
skillName: resolved.skillName,
|
|
295
|
+
callbackText: `Run ${resolved.skillName}`,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|