实现邮件回复到 Claude Code CLI 的 PTY 模式

- 基于 relay-injection.md 方案一实现
- 使用 node-pty 管理 Claude Code 进程
- 支持从邮件回复中提取命令并注入到 CLI
- 添加完整的测试工具和使用文档
- 支持多种邮件格式和命令输入方式

功能特性:
- 邮件监听和命令提取
- PTY 进程管理和命令注入
- 会话管理和过期控制
- 安全验证(发件人白名单)
- 详细的日志和错误处理

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
panda 2025-07-27 02:34:32 +08:00
parent 45576e5a3e
commit ba4a6343aa
9 changed files with 2252 additions and 3 deletions

30
.env.example Normal file
View File

@ -0,0 +1,30 @@
# TaskPing Email Relay Configuration
# 邮件接收配置
IMAP_HOST=imap.gmail.com
IMAP_PORT=993
IMAP_SECURE=true
IMAP_USER=your-email@gmail.com
IMAP_PASS=your-app-password
# 邮件安全设置
# 允许的发件人列表(逗号分隔)
ALLOWED_SENDERS=jessy@example.com,trusted@company.com
# 路由与会话存储
SESSION_MAP_PATH=/Users/jessytsui/dev/TaskPing/src/data/session-map.json
# 模式选择pty 或 tmux
INJECTION_MODE=pty
# Claude CLI 路径可选默认使用系统PATH中的claude
CLAUDE_CLI_PATH=claude
# 日志级别debug, info, warn, error
LOG_LEVEL=info
# 是否记录PTY输出调试用
PTY_OUTPUT_LOG=false
# tmux 配置如果使用tmux模式
TMUX_SESSION_PREFIX=taskping

274
docs/EMAIL_REPLY_GUIDE.md Normal file
View File

@ -0,0 +1,274 @@
# TaskPing 邮件回复功能使用指南
## 概述
TaskPing 的邮件回复功能允许您通过回复邮件的方式向 Claude Code CLI 发送命令。当您在移动设备或其他电脑上收到 TaskPing 的任务提醒邮件时,可以直接回复邮件来控制 Claude Code 继续执行任务。
## 工作原理
1. **发送提醒**: Claude Code 在任务暂停时发送包含会话 Token 的提醒邮件
2. **邮件监听**: PTY Relay 服务持续监听您的收件箱
3. **命令提取**: 从回复邮件中提取命令内容
4. **命令注入**: 通过 node-pty 将命令注入到对应的 Claude Code 会话
## 快速开始
### 1. 配置邮件账号
复制环境配置文件:
```bash
cp .env.example .env
```
编辑 `.env` 文件,填入您的邮件配置:
```env
# Gmail 示例
IMAP_HOST=imap.gmail.com
IMAP_PORT=993
IMAP_SECURE=true
IMAP_USER=your-email@gmail.com
IMAP_PASS=your-app-password # 使用应用专用密码
# 安全设置
ALLOWED_SENDERS=your-email@gmail.com,trusted@company.com
```
### 2. 启动 PTY Relay 服务
```bash
npm run relay:pty
# 或者
./start-relay-pty.js
```
### 3. 测试邮件解析
运行测试工具验证配置:
```bash
npm run relay:test
```
## 使用方法
### 邮件格式要求
#### 主题格式
邮件主题必须包含 TaskPing Token支持以下格式
- `[TaskPing #TOKEN]` - 推荐格式
- `[TaskPing TOKEN]`
- `TaskPing: TOKEN`
例如:
- `Re: [TaskPing #ABC123] 任务等待您的指示`
- `回复: TaskPing: XYZ789`
#### 命令格式
支持三种命令输入方式:
1. **直接输入**(最简单)
```
继续执行
```
2. **CMD 前缀**(明确标识)
```
CMD: npm run build
```
3. **代码块**(复杂命令)
````
```
git add .
git commit -m "Update features"
git push
```
````
### 示例场景
#### 场景 1: 简单确认
收到邮件:
```
主题: [TaskPing #TASK001] 是否继续部署到生产环境?
```
回复:
```
yes
```
#### 场景 2: 执行具体命令
收到邮件:
```
主题: [TaskPing #BUILD123] 构建失败,请输入修复命令
```
回复:
```
CMD: npm install missing-package
```
#### 场景 3: 多行命令
收到邮件:
```
主题: [TaskPing #DEPLOY456] 准备部署,请确认步骤
```
回复:
````
执行以下命令:
```
npm run test
npm run build
npm run deploy
```
````
## 高级配置
### 会话管理
会话映射文件 `session-map.json` 结构:
```json
{
"TOKEN123": {
"type": "pty",
"createdAt": 1234567890,
"expiresAt": 1234654290,
"cwd": "/path/to/project",
"description": "构建项目 X"
}
}
```
### 环境变量说明
| 变量名 | 说明 | 默认值 |
|--------|------|--------|
| `IMAP_HOST` | IMAP 服务器地址 | 必需 |
| `IMAP_PORT` | IMAP 端口 | 993 |
| `IMAP_SECURE` | 使用 SSL/TLS | true |
| `IMAP_USER` | 邮箱账号 | 必需 |
| `IMAP_PASS` | 邮箱密码 | 必需 |
| `ALLOWED_SENDERS` | 允许的发件人列表 | 空(接受所有) |
| `SESSION_MAP_PATH` | 会话映射文件路径 | ./src/data/session-map.json |
| `CLAUDE_CLI_PATH` | Claude CLI 路径 | claude |
| `LOG_LEVEL` | 日志级别 | info |
| `PTY_OUTPUT_LOG` | 记录 PTY 输出 | false |
### 邮件服务器配置示例
#### Gmail
1. 启用 IMAP: 设置 → 转发和 POP/IMAP → 启用 IMAP
2. 生成应用专用密码: 账号设置 → 安全 → 应用专用密码
```env
IMAP_HOST=imap.gmail.com
IMAP_PORT=993
IMAP_SECURE=true
```
#### Outlook/Office 365
```env
IMAP_HOST=outlook.office365.com
IMAP_PORT=993
IMAP_SECURE=true
```
#### QQ 邮箱
```env
IMAP_HOST=imap.qq.com
IMAP_PORT=993
IMAP_SECURE=true
```
## 安全注意事项
1. **发件人验证**: 始终配置 `ALLOWED_SENDERS` 以限制谁可以发送命令
2. **命令过滤**: 系统会自动过滤危险命令(如 `rm -rf`
3. **会话过期**: 会话有过期时间,过期后无法接受命令
4. **邮件加密**: 使用 SSL/TLS 加密的 IMAP 连接
5. **密码安全**: 使用应用专用密码而非主密码
## 故障排查
### 常见问题
1. **无法连接到邮件服务器**
- 检查 IMAP 是否已启用
- 验证服务器地址和端口
- 确认防火墙设置
2. **邮件未被处理**
- 检查发件人是否在白名单中
- 验证邮件主题格式
- 查看日志中的错误信息
3. **命令未执行**
- 确认 Claude Code 进程正在运行
- 检查会话是否已过期
- 验证命令格式是否正确
### 查看日志
```bash
# 启动时查看详细日志
LOG_LEVEL=debug npm run relay:pty
# 查看 PTY 输出
PTY_OUTPUT_LOG=true npm run relay:pty
```
### 测试命令
```bash
# 测试邮件解析
npm run relay:test
# 手动启动(调试模式)
INJECTION_MODE=pty LOG_LEVEL=debug node src/relay/relay-pty.js
```
## 最佳实践
1. **使用专用邮箱**: 为 TaskPing 创建专用邮箱账号
2. **定期清理**: 定期清理过期会话和已处理邮件
3. **命令简洁**: 保持命令简短明确
4. **及时回复**: 在会话过期前回复邮件
5. **安全优先**: 不要在邮件中包含敏感信息
## 集成到现有项目
如果您想将邮件回复功能集成到现有的 Claude Code 工作流:
1. 在发送提醒时创建会话:
```javascript
const token = generateToken();
const session = {
type: 'pty',
createdAt: Math.floor(Date.now() / 1000),
expiresAt: Math.floor((Date.now() + 3600000) / 1000),
cwd: process.cwd()
};
saveSession(token, session);
```
2. 在邮件主题中包含 Token
```javascript
const subject = `[TaskPing #${token}] ${taskDescription}`;
```
3. 启动 PTY Relay 服务监听回复
## 相关文档
- [邮件配置指南](./EMAIL_GUIDE.md)
- [快速邮件设置](./QUICK_EMAIL_SETUP.md)
- [系统架构说明](./EMAIL_ARCHITECTURE.md)
## 支持
如有问题,请查看:
- 项目 Issue: https://github.com/JessyTsui/TaskPing/issues
- 详细日志: `LOG_LEVEL=debug npm run relay:pty`

588
package-lock.json generated
View File

@ -15,9 +15,15 @@
"win32" "win32"
], ],
"dependencies": { "dependencies": {
"dotenv": "^17.2.1",
"execa": "^9.6.0",
"imapflow": "^1.0.191",
"mailparser": "^3.7.4", "mailparser": "^3.7.4",
"node-imap": "^0.9.6", "node-imap": "^0.9.6",
"node-pty": "^1.0.0",
"nodemailer": "^7.0.5", "nodemailer": "^7.0.5",
"pino": "^9.7.0",
"pino-pretty": "^13.0.0",
"uuid": "^11.1.0" "uuid": "^11.1.0"
}, },
"bin": { "bin": {
@ -25,11 +31,15 @@
"taskping-install": "install.js", "taskping-install": "install.js",
"taskping-notify": "hook-notify.js" "taskping-notify": "hook-notify.js"
}, },
"devDependencies": {},
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@sec-ant/readable-stream": {
"version": "0.4.1",
"resolved": "https://registry.npmmirror.com/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
"integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="
},
"node_modules/@selderee/plugin-htmlparser2": { "node_modules/@selderee/plugin-htmlparser2": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmmirror.com/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", "resolved": "https://registry.npmmirror.com/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz",
@ -42,6 +52,51 @@
"url": "https://ko-fi.com/killymxi" "url": "https://ko-fi.com/killymxi"
} }
}, },
"node_modules/@sindresorhus/merge-streams": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
"integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/atomic-sleep": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
"integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/colorette": {
"version": "2.0.20",
"resolved": "https://registry.npmmirror.com/colorette/-/colorette-2.0.20.tgz",
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/dateformat": {
"version": "4.6.3",
"resolved": "https://registry.npmmirror.com/dateformat/-/dateformat-4.6.3.tgz",
"integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==",
"engines": {
"node": "*"
}
},
"node_modules/deepmerge": { "node_modules/deepmerge": {
"version": "4.3.1", "version": "4.3.1",
"resolved": "https://registry.npmmirror.com/deepmerge/-/deepmerge-4.3.1.tgz", "resolved": "https://registry.npmmirror.com/deepmerge/-/deepmerge-4.3.1.tgz",
@ -101,6 +156,17 @@
"url": "https://github.com/fb55/domutils?sponsor=1" "url": "https://github.com/fb55/domutils?sponsor=1"
} }
}, },
"node_modules/dotenv": {
"version": "17.2.1",
"resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-17.2.1.tgz",
"integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/encoding-japanese": { "node_modules/encoding-japanese": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmmirror.com/encoding-japanese/-/encoding-japanese-2.2.0.tgz", "resolved": "https://registry.npmmirror.com/encoding-japanese/-/encoding-japanese-2.2.0.tgz",
@ -109,6 +175,14 @@
"node": ">=8.10.0" "node": ">=8.10.0"
} }
}, },
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/entities": { "node_modules/entities": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
@ -120,6 +194,78 @@
"url": "https://github.com/fb55/entities?sponsor=1" "url": "https://github.com/fb55/entities?sponsor=1"
} }
}, },
"node_modules/execa": {
"version": "9.6.0",
"resolved": "https://registry.npmmirror.com/execa/-/execa-9.6.0.tgz",
"integrity": "sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==",
"dependencies": {
"@sindresorhus/merge-streams": "^4.0.0",
"cross-spawn": "^7.0.6",
"figures": "^6.1.0",
"get-stream": "^9.0.0",
"human-signals": "^8.0.1",
"is-plain-obj": "^4.1.0",
"is-stream": "^4.0.1",
"npm-run-path": "^6.0.0",
"pretty-ms": "^9.2.0",
"signal-exit": "^4.1.0",
"strip-final-newline": "^4.0.0",
"yoctocolors": "^2.1.1"
},
"engines": {
"node": "^18.19.0 || >=20.5.0"
},
"funding": {
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
"node_modules/fast-copy": {
"version": "3.0.2",
"resolved": "https://registry.npmmirror.com/fast-copy/-/fast-copy-3.0.2.tgz",
"integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ=="
},
"node_modules/fast-redact": {
"version": "3.5.0",
"resolved": "https://registry.npmmirror.com/fast-redact/-/fast-redact-3.5.0.tgz",
"integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==",
"engines": {
"node": ">=6"
}
},
"node_modules/fast-safe-stringify": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="
},
"node_modules/figures": {
"version": "6.1.0",
"resolved": "https://registry.npmmirror.com/figures/-/figures-6.1.0.tgz",
"integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==",
"dependencies": {
"is-unicode-supported": "^2.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-stream": {
"version": "9.0.1",
"resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-9.0.1.tgz",
"integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==",
"dependencies": {
"@sec-ant/readable-stream": "^0.4.1",
"is-stream": "^4.0.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/he": { "node_modules/he": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz", "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz",
@ -128,6 +274,11 @@
"he": "bin/he" "he": "bin/he"
} }
}, },
"node_modules/help-me": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/help-me/-/help-me-5.0.0.tgz",
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="
},
"node_modules/html-to-text": { "node_modules/html-to-text": {
"version": "9.0.5", "version": "9.0.5",
"resolved": "https://registry.npmmirror.com/html-to-text/-/html-to-text-9.0.5.tgz", "resolved": "https://registry.npmmirror.com/html-to-text/-/html-to-text-9.0.5.tgz",
@ -161,6 +312,14 @@
"entities": "^4.4.0" "entities": "^4.4.0"
} }
}, },
"node_modules/human-signals": {
"version": "8.0.1",
"resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-8.0.1.tgz",
"integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==",
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz",
@ -172,11 +331,90 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/imapflow": {
"version": "1.0.191",
"resolved": "https://registry.npmmirror.com/imapflow/-/imapflow-1.0.191.tgz",
"integrity": "sha512-cjGj5RYOZe6L/B1sn/yaSK0HJt/6OH5KVvuZ4wi4eJwm+3DUZT5rFljAqJH7Nc/UlOqdeCH2Tm47/Aa5apOkrg==",
"dependencies": {
"encoding-japanese": "2.2.0",
"iconv-lite": "0.6.3",
"libbase64": "1.3.0",
"libmime": "5.3.7",
"libqp": "2.1.1",
"mailsplit": "5.4.5",
"nodemailer": "7.0.5",
"pino": "9.7.0",
"socks": "2.8.5"
}
},
"node_modules/inherits": { "node_modules/inherits": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
}, },
"node_modules/ip-address": {
"version": "9.0.5",
"resolved": "https://registry.npmmirror.com/ip-address/-/ip-address-9.0.5.tgz",
"integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
"dependencies": {
"jsbn": "1.1.0",
"sprintf-js": "^1.1.3"
},
"engines": {
"node": ">= 12"
}
},
"node_modules/is-plain-obj": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
"integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-stream": {
"version": "4.0.1",
"resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-4.0.1.tgz",
"integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-unicode-supported": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
"integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
},
"node_modules/joycon": {
"version": "3.1.1",
"resolved": "https://registry.npmmirror.com/joycon/-/joycon-3.1.1.tgz",
"integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==",
"engines": {
"node": ">=10"
}
},
"node_modules/jsbn": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/jsbn/-/jsbn-1.1.0.tgz",
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="
},
"node_modules/leac": { "node_modules/leac": {
"version": "0.6.0", "version": "0.6.0",
"resolved": "https://registry.npmmirror.com/leac/-/leac-0.6.0.tgz", "resolved": "https://registry.npmmirror.com/leac/-/leac-0.6.0.tgz",
@ -249,6 +487,19 @@
"libqp": "2.1.1" "libqp": "2.1.1"
} }
}, },
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/nan": {
"version": "2.23.0",
"resolved": "https://registry.npmmirror.com/nan/-/nan-2.23.0.tgz",
"integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ=="
},
"node_modules/node-imap": { "node_modules/node-imap": {
"version": "0.9.6", "version": "0.9.6",
"resolved": "https://registry.npmmirror.com/node-imap/-/node-imap-0.9.6.tgz", "resolved": "https://registry.npmmirror.com/node-imap/-/node-imap-0.9.6.tgz",
@ -261,6 +512,15 @@
"node": ">=0.8.0" "node": ">=0.8.0"
} }
}, },
"node_modules/node-pty": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/node-pty/-/node-pty-1.0.0.tgz",
"integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==",
"hasInstallScript": true,
"dependencies": {
"nan": "^2.17.0"
}
},
"node_modules/nodemailer": { "node_modules/nodemailer": {
"version": "7.0.5", "version": "7.0.5",
"resolved": "https://registry.npmmirror.com/nodemailer/-/nodemailer-7.0.5.tgz", "resolved": "https://registry.npmmirror.com/nodemailer/-/nodemailer-7.0.5.tgz",
@ -269,6 +529,59 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/npm-run-path": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-6.0.0.tgz",
"integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==",
"dependencies": {
"path-key": "^4.0.0",
"unicorn-magic": "^0.3.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/npm-run-path/node_modules/path-key": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/path-key/-/path-key-4.0.0.tgz",
"integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/on-exit-leak-free": {
"version": "2.1.2",
"resolved": "https://registry.npmmirror.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
"integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/parse-ms": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/parse-ms/-/parse-ms-4.0.0.tgz",
"integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/parseley": { "node_modules/parseley": {
"version": "0.12.1", "version": "0.12.1",
"resolved": "https://registry.npmmirror.com/parseley/-/parseley-0.12.1.tgz", "resolved": "https://registry.npmmirror.com/parseley/-/parseley-0.12.1.tgz",
@ -281,6 +594,14 @@
"url": "https://ko-fi.com/killymxi" "url": "https://ko-fi.com/killymxi"
} }
}, },
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"engines": {
"node": ">=8"
}
},
"node_modules/peberminta": { "node_modules/peberminta": {
"version": "0.9.0", "version": "0.9.0",
"resolved": "https://registry.npmmirror.com/peberminta/-/peberminta-0.9.0.tgz", "resolved": "https://registry.npmmirror.com/peberminta/-/peberminta-0.9.0.tgz",
@ -289,6 +610,101 @@
"url": "https://ko-fi.com/killymxi" "url": "https://ko-fi.com/killymxi"
} }
}, },
"node_modules/pino": {
"version": "9.7.0",
"resolved": "https://registry.npmmirror.com/pino/-/pino-9.7.0.tgz",
"integrity": "sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg==",
"dependencies": {
"atomic-sleep": "^1.0.0",
"fast-redact": "^3.1.1",
"on-exit-leak-free": "^2.1.0",
"pino-abstract-transport": "^2.0.0",
"pino-std-serializers": "^7.0.0",
"process-warning": "^5.0.0",
"quick-format-unescaped": "^4.0.3",
"real-require": "^0.2.0",
"safe-stable-stringify": "^2.3.1",
"sonic-boom": "^4.0.1",
"thread-stream": "^3.0.0"
},
"bin": {
"pino": "bin.js"
}
},
"node_modules/pino-abstract-transport": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz",
"integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==",
"dependencies": {
"split2": "^4.0.0"
}
},
"node_modules/pino-pretty": {
"version": "13.0.0",
"resolved": "https://registry.npmmirror.com/pino-pretty/-/pino-pretty-13.0.0.tgz",
"integrity": "sha512-cQBBIVG3YajgoUjo1FdKVRX6t9XPxwB9lcNJVD5GCnNM4Y6T12YYx8c6zEejxQsU0wrg9TwmDulcE9LR7qcJqA==",
"dependencies": {
"colorette": "^2.0.7",
"dateformat": "^4.6.3",
"fast-copy": "^3.0.2",
"fast-safe-stringify": "^2.1.1",
"help-me": "^5.0.0",
"joycon": "^3.1.1",
"minimist": "^1.2.6",
"on-exit-leak-free": "^2.1.0",
"pino-abstract-transport": "^2.0.0",
"pump": "^3.0.0",
"secure-json-parse": "^2.4.0",
"sonic-boom": "^4.0.1",
"strip-json-comments": "^3.1.1"
},
"bin": {
"pino-pretty": "bin.js"
}
},
"node_modules/pino-std-serializers": {
"version": "7.0.0",
"resolved": "https://registry.npmmirror.com/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz",
"integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA=="
},
"node_modules/pretty-ms": {
"version": "9.2.0",
"resolved": "https://registry.npmmirror.com/pretty-ms/-/pretty-ms-9.2.0.tgz",
"integrity": "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==",
"dependencies": {
"parse-ms": "^4.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/process-warning": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/process-warning/-/process-warning-5.0.0.tgz",
"integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
]
},
"node_modules/pump": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.3.tgz",
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/punycode.js": { "node_modules/punycode.js": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmmirror.com/punycode.js/-/punycode.js-2.3.1.tgz", "resolved": "https://registry.npmmirror.com/punycode.js/-/punycode.js-2.3.1.tgz",
@ -297,6 +713,11 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/quick-format-unescaped": {
"version": "4.0.4",
"resolved": "https://registry.npmmirror.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="
},
"node_modules/readable-stream": { "node_modules/readable-stream": {
"version": "3.6.2", "version": "3.6.2",
"resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz",
@ -310,6 +731,14 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/real-require": {
"version": "0.2.0",
"resolved": "https://registry.npmmirror.com/real-require/-/real-require-0.2.0.tgz",
"integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
"engines": {
"node": ">= 12.13.0"
}
},
"node_modules/safe-buffer": { "node_modules/safe-buffer": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz",
@ -329,11 +758,24 @@
} }
] ]
}, },
"node_modules/safe-stable-stringify": {
"version": "2.5.0",
"resolved": "https://registry.npmmirror.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
"engines": {
"node": ">=10"
}
},
"node_modules/safer-buffer": { "node_modules/safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
}, },
"node_modules/secure-json-parse": {
"version": "2.7.0",
"resolved": "https://registry.npmmirror.com/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
"integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="
},
"node_modules/selderee": { "node_modules/selderee": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmmirror.com/selderee/-/selderee-0.11.0.tgz", "resolved": "https://registry.npmmirror.com/selderee/-/selderee-0.11.0.tgz",
@ -353,6 +795,79 @@
"semver": "bin/semver" "semver": "bin/semver"
} }
}, },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"engines": {
"node": ">=8"
}
},
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmmirror.com/smart-buffer/-/smart-buffer-4.2.0.tgz",
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/socks": {
"version": "2.8.5",
"resolved": "https://registry.npmmirror.com/socks/-/socks-2.8.5.tgz",
"integrity": "sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww==",
"dependencies": {
"ip-address": "^9.0.5",
"smart-buffer": "^4.2.0"
},
"engines": {
"node": ">= 10.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/sonic-boom": {
"version": "4.2.0",
"resolved": "https://registry.npmmirror.com/sonic-boom/-/sonic-boom-4.2.0.tgz",
"integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==",
"dependencies": {
"atomic-sleep": "^1.0.0"
}
},
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmmirror.com/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/sprintf-js": {
"version": "1.1.3",
"resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.1.3.tgz",
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="
},
"node_modules/string_decoder": { "node_modules/string_decoder": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz",
@ -361,6 +876,36 @@
"safe-buffer": "~5.2.0" "safe-buffer": "~5.2.0"
} }
}, },
"node_modules/strip-final-newline": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-4.0.0.tgz",
"integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/thread-stream": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/thread-stream/-/thread-stream-3.1.0.tgz",
"integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==",
"dependencies": {
"real-require": "^0.2.0"
}
},
"node_modules/tlds": { "node_modules/tlds": {
"version": "1.259.0", "version": "1.259.0",
"resolved": "https://registry.npmmirror.com/tlds/-/tlds-1.259.0.tgz", "resolved": "https://registry.npmmirror.com/tlds/-/tlds-1.259.0.tgz",
@ -374,6 +919,17 @@
"resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-2.1.0.tgz", "resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-2.1.0.tgz",
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==" "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="
}, },
"node_modules/unicorn-magic": {
"version": "0.3.0",
"resolved": "https://registry.npmmirror.com/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
"integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/utf7": { "node_modules/utf7": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmmirror.com/utf7/-/utf7-1.0.2.tgz", "resolved": "https://registry.npmmirror.com/utf7/-/utf7-1.0.2.tgz",
@ -398,6 +954,36 @@
"bin": { "bin": {
"uuid": "dist/esm/bin/uuid" "uuid": "dist/esm/bin/uuid"
} }
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/yoctocolors": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/yoctocolors/-/yoctocolors-2.1.1.tgz",
"integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
} }
} }
} }

View File

@ -13,7 +13,10 @@
"daemon:stop": "node taskping.js daemon stop", "daemon:stop": "node taskping.js daemon stop",
"daemon:status": "node taskping.js daemon status", "daemon:status": "node taskping.js daemon status",
"test:clipboard": "node test-clipboard.js", "test:clipboard": "node test-clipboard.js",
"start": "node email-automation.js" "start": "node email-automation.js",
"relay:pty": "node start-relay-pty.js",
"relay:test": "node test-email-reply.js",
"relay:start": "INJECTION_MODE=pty node src/relay/relay-pty.js"
}, },
"bin": { "bin": {
"taskping-install": "./install.js", "taskping-install": "./install.js",
@ -50,9 +53,15 @@
}, },
"homepage": "https://github.com/TaskPing/TaskPing#readme", "homepage": "https://github.com/TaskPing/TaskPing#readme",
"dependencies": { "dependencies": {
"dotenv": "^17.2.1",
"execa": "^9.6.0",
"imapflow": "^1.0.191",
"mailparser": "^3.7.4", "mailparser": "^3.7.4",
"node-imap": "^0.9.6", "node-imap": "^0.9.6",
"node-pty": "^1.0.0",
"nodemailer": "^7.0.5", "nodemailer": "^7.0.5",
"pino": "^9.7.0",
"pino-pretty": "^13.0.0",
"uuid": "^11.1.0" "uuid": "^11.1.0"
}, },
"files": [ "files": [

447
relay-injection.md Normal file
View File

@ -0,0 +1,447 @@
# TaskPing — 邮件回复写回 Claude Code CLI执行手册
> 目标读者:能执行脚本的 AI/自动化代理
> 运行环境Linux/macOS建议Node.js ≥ 18Claude Code 已安装并可在终端运行 `claude`
---
## 1. 背景与目标
**项目背景**:用户在本机通过 Claude CodeCLI进行编码。当前已实现
- 通过 hooks 在任务结束/等待下一步时**发出邮件提醒**
- 用户在手机上**回复邮件**
- 本机有一个常驻“中继/Relay”进程轮询收件箱能够拿到该回复。
**当前卡点****把邮件正文写回到对应的 Claude Code CLI 会话,并回车执行**。
**本文档目的**:为 AI/自动化代理提供两种可执行的落地方案:
1) **子进程/PTY 模式**:用 `node-pty` 驱动一个 Claude Code 子进程,直接向伪终端写入并回车;
2) **tmux 模式**:让 Claude Code 运行在指定 `tmux` pane收到邮件后用 `tmux send-keys` 注入文本并回车。
两种方案都要求:
- 能**路由**到正确的 Claude 会话;
- 能**清洗**邮件正文,仅保留要注入的命令;
- 有**安全**与**幂等**控制;
- 有**可观测性**(日志/Tracing
---
## 2. 全局约束与安全要求
- **会话识别**:优先按 `In-Reply-To``Message-ID` 匹配;回退使用 `Subject` 中的 token形如 `[TaskPing #ABC123]`)。
- **白名单**:仅允许来自配置的发件人域/邮箱(通过 SPF/DKIM/DMARC 验证后再放行)。
- **正文提取**:只取**最新回复**去除历史引用、签名、HTML 标签、图片占位。支持三种输入:
- 纯文本一行命令;
- 代码块 ``` 包起来的命令(优先级最高);
- 主题/首行以 `CMD:` 前缀标识。
- **幂等**:用 `Message-ID``gmailThreadId` 去重(重复投递不再次注入)。
- **限流**:同一会话每分钟最多 1 条;单条长度上限(例如 8KB
- **日志**:记录 token、会话、pane/pty id、注入摘要前 120 字符),屏蔽隐私。
---
## 3. 公共依赖与配置
### 3.1 依赖Node.js
```bash
npm i imapflow mailparser node-pty pino dotenv execa
# tmux 方案需要系统安装 tmuxmacOS: brew install tmuxDebian/Ubuntu: apt-get install tmux
```
### 3.2 环境变量(`.env`
```bash
# 邮件接收
IMAP_HOST=imap.example.com
IMAP_PORT=993
IMAP_SECURE=true
IMAP_USER=bot@example.com
IMAP_PASS=********
# 邮件路由安全
ALLOWED_SENDERS=jessy@example.com,panda@company.com
# 路由与会话存储JSON 文件路径)
SESSION_MAP_PATH=/var/lib/taskping/session-map.json
# 模式选择pty 或 tmux
INJECTION_MODE=pty
# tmux 方案可选:默认 session/pane 名称前缀
TMUX_SESSION_PREFIX=taskping
```
### 3.3 会话映射(`SESSION_MAP_PATH`
结构示例:
```json
{
"ABC123": {
"type": "pty",
"ptyId": "b4c1...",
"createdAt": 1732672100,
"expiresAt": 1732679300,
"messageId": "<CA+abc@mail.example.com>",
"imapUid": 12345
},
"XYZ789": {
"type": "tmux",
"session": "taskping-XYZ789",
"pane": "taskping-XYZ789.0",
"createdAt": 1732672200,
"expiresAt": 1732679400,
"messageId": "<CA+xyz@mail.example.com>"
}
}
```
> 注:会话映射由**发送提醒时**创建(包含 token 与目标 CLI 会话信息)。
---
## 4. 方案一:子进程/PTY 模式(推荐)
### 4.1 适用场景
- 由中继程序**直接管理** Claude Code 生命周期;
- 需要最稳定的注入通道(不依赖额外终端复用器)。
### 4.2 实现思路
- 用 `node-pty` `spawn('claude', [...])` 启动 CLI
- 将返回的 `pty` 与生成的 token 绑定,写入 `SESSION_MAP_PATH`
- 收到邮件 → 路由 token → 清洗正文 → `pty.write(cmd + '\r')`
- 监控 `pty.onData`,必要时截取摘要回传提醒(可选)。
### 4.3 关键代码骨架(`relay-pty.js`
```js
import { ImapFlow } from 'imapflow';
import { simpleParser } from 'mailparser';
import pino from 'pino';
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { spawn as spawnPty } from 'node-pty';
import dotenv from 'dotenv';
dotenv.config();
const log = pino({ level: 'info' });
const SESS_PATH = process.env.SESSION_MAP_PATH;
function loadSessions() {
if (!existsSync(SESS_PATH)) return {};
return JSON.parse(readFileSync(SESS_PATH, 'utf8'));
}
function saveSessions(map) {
writeFileSync(SESS_PATH, JSON.stringify(map, null, 2));
}
function normalizeAllowlist() {
return (process.env.ALLOWED_SENDERS || '')
.split(',')
.map(s => s.trim().toLowerCase())
.filter(Boolean);
}
const ALLOW = new Set(normalizeAllowlist());
function isAllowed(addressObj) {
const list = []
.concat(addressObj?.value || [])
.map(a => (a.address || '').toLowerCase());
return list.some(addr => ALLOW.has(addr));
}
function extractTokenFromSubject(subject = '') {
const m = subject.match(/\[TaskPing\s+#([A-Za-z0-9_-]+)\]/);
return m ? m[1] : null;
}
function stripReply(text = '') {
// 优先识别 ``` 块
const codeBlock = text.match(/```[\s\S]*?```/);
if (codeBlock) {
return codeBlock[0].replace(/```/g, '').trim();
}
// 取首行或以 CMD: 前缀
const firstLine = text.split(/\r?\n/).find(l => l.trim().length > 0) || '';
const cmdPrefix = firstLine.match(/^CMD:\s*(.+)$/i);
const candidate = (cmdPrefix ? cmdPrefix[1] : firstLine).trim();
// 去除引用与签名
return candidate
.replace(/^>.*$/gm, '')
.replace(/^--\s+[\s\S]*$/m, '')
.slice(0, 8192) // 长度限制
.trim();
}
async function handleMailMessage(source) {
const parsed = await simpleParser(source);
if (!isAllowed(parsed.from)) {
log.warn({ from: parsed.from?.text }, 'sender not allowed');
return;
}
const subject = parsed.subject || '';
const token = extractTokenFromSubject(subject);
if (!token) {
log.warn({ subject }, 'no token in subject');
return;
}
const sessions = loadSessions();
const sess = sessions[token];
if (!sess || sess.expiresAt * 1000 < Date.now()) {
log.warn({ token }, 'session not found or expired');
return;
}
if (sess.type !== 'pty' || !sess.ptyId) {
log.error({ token }, 'session is not pty type');
return;
}
const cmd = stripReply(parsed.text || parsed.html || '');
if (!cmd) {
log.warn({ token }, 'empty command after strip');
return;
}
// 取得 pty 实例:这里演示为“按需重建/保持单例”两种策略之一
const pty = getOrRestorePty(sess);
log.info({ token, cmd: cmd.slice(0, 120) }, 'inject command');
pty.write(cmd + '\r');
}
const PTY_POOL = new Map();
function getOrRestorePty(sess) {
if (PTY_POOL.has(sess.ptyId)) return PTY_POOL.get(sess.ptyId);
// 如果需要重建,会话应当保存启动参数;这里演示简化为新起一个 claude
const shell = 'claude'; // 或绝对路径
const pty = spawnPty(shell, [], {
name: 'xterm-color',
cols: 120,
rows: 32
});
PTY_POOL.set(sess.ptyId, pty);
pty.onData(d => process.stdout.write(d)); // 可替换为日志/回传
pty.onExit(() => PTY_POOL.delete(sess.ptyId));
return pty;
}
async function startImap() {
const client = new ImapFlow({
host: process.env.IMAP_HOST,
port: Number(process.env.IMAP_PORT || 993),
secure: process.env.IMAP_SECURE === 'true',
auth: { user: process.env.IMAP_USER, pass: process.env.IMAP_PASS }
});
await client.connect();
const lock = await client.getMailboxLock('INBOX');
try {
for await (const msg of client.monitor()) {
if (msg.type === 'exists') {
const { uid } = msg;
const { source } = await client.download('INBOX', uid);
const chunks = [];
for await (const c of source) chunks.push(c);
await handleMailMessage(Buffer.concat(chunks));
}
}
} finally {
lock.release();
}
}
if (process.env.INJECTION_MODE === 'pty') {
startImap().catch(err => {
console.error(err);
process.exit(1);
});
}
```
> 说明:生产化时,建议在**发送提醒**一侧创建会话并持久化 `ptyId` 与参数;这里演示了“按需复原”的简化路径。
### 4.4 启动
```bash
node relay-pty.js
```
### 4.5 验收清单
- [ ] 未授权邮箱无法触发注入;
- [ ] 合法邮件写入后Claude Code 能立即进入下一步;
- [ ] 重复转发的同一邮件不会二次注入;
- [ ] 过期会话拒绝注入;
- [ ] 大段/HTML/带签名的邮件能正确抽取命令。
---
## 5. 方案二tmux 模式(简单稳妥)
### 5.1 适用场景
- 你已经在 `tmux` 里运行 Claude Code
- 希望由外部进程向**指定 pane** 注入按键,不改变现有启动流程。
### 5.2 实现思路
- 为每个 token 创建/记录一个 `tmux` session 与 pane`session = taskping-<token>`
- Claude Code 在该 pane 里运行;
- 收到邮件 → 定位到 pane → `tmux send-keys -t <pane> "<cmd>" Enter`
### 5.3 管理脚本(`tmux-utils.sh`
```bash
#!/usr/bin/env bash
set -euo pipefail
SESSION="$1" # 例如 taskping-ABC123
CMD="${2:-}" # 可选:一次性注入命令
if ! tmux has-session -t "${SESSION}" 2>/dev/null; then
tmux new-session -d -s "${SESSION}"
tmux rename-window -t "${SESSION}:0" "claude"
tmux send-keys -t "${SESSION}:0" "claude" C-m
sleep 0.5
fi
if [ -n "${CMD}" ]; then
tmux send-keys -t "${SESSION}:0" "${CMD}" C-m
fi
# 输出 pane 目标名,供上层程序记录
echo "${SESSION}.0"
```
### 5.4 注入程序(`relay-tmux.js`
```js
import { ImapFlow } from 'imapflow';
import { simpleParser } from 'mailparser';
import pino from 'pino';
import { readFileSync, writeFileSync, existsSync } from 'fs';
import dotenv from 'dotenv';
import { execa } from 'execa';
dotenv.config();
const log = pino({ level: 'info' });
const SESS_PATH = process.env.SESSION_MAP_PATH;
function loadSessions() {
if (!existsSync(SESS_PATH)) return {};
return JSON.parse(readFileSync(SESS_PATH, 'utf8'));
}
function extractTokenFromSubject(subject = '') {
const m = subject.match(/\[TaskPing\s+#([A-Za-z0-9_-]+)\]/);
return m ? m[1] : null;
}
function stripReply(text = '') {
const codeBlock = text.match(/```[\s\S]*?```/);
if (codeBlock) return codeBlock[0].replace(/```/g, '').trim();
const firstLine = text.split(/\r?\n/).find(l => l.trim().length > 0) || '';
const cmdPrefix = firstLine.match(/^CMD:\s*(.+)$/i);
return (cmdPrefix ? cmdPrefix[1] : firstLine).trim().slice(0, 8192);
}
async function injectToTmux(sessionName, cmd) {
const utils = new URL('./tmux-utils.sh', import.meta.url).pathname;
const { stdout } = await execa('bash', [utils, sessionName, cmd], { stdio: 'pipe' });
return stdout.trim(); // 返回 pane 目标名
}
async function startImap() {
const client = new ImapFlow({
host: process.env.IMAP_HOST,
port: Number(process.env.IMAP_PORT || 993),
secure: process.env.IMAP_SECURE === 'true',
auth: { user: process.env.IMAP_USER, pass: process.env.IMAP_PASS }
});
await client.connect();
await client.mailboxOpen('INBOX');
for await (const msg of client.monitor()) {
if (msg.type !== 'exists') continue;
const { uid } = msg;
const { source } = await client.download('INBOX', uid);
const chunks = [];
for await (const c of source) chunks.push(c);
const parsed = await simpleParser(Buffer.concat(chunks));
const token = extractTokenFromSubject(parsed.subject || '');
if (!token) continue;
const cmd = stripReply(parsed.text || parsed.html || '');
if (!cmd) continue;
const sessionName = `${process.env.TMUX_SESSION_PREFIX || 'taskping'}-${token}`;
const pane = await injectToTmux(sessionName, cmd);
console.log(`[inject] ${sessionName} -> ${pane} :: ${cmd.slice(0, 120)}`);
}
}
if (process.env.INJECTION_MODE === 'tmux') {
startImap().catch(err => {
console.error(err);
process.exit(1);
});
}
```
### 5.5 启动
```bash
chmod +x tmux-utils.sh
INJECTION_MODE=tmux node relay-tmux.js
```
### 5.6 验收清单
- [ ] `tmux` 中能看到 Claude 进程始终在运行;
- [ ] 邮件回复注入后,当前 pane 光标位置正确、命令无截断;
- [ ] 会话名与 token 一一对应,过期会话自动清理;
- [ ] `tmux` 重启或 SSH 断连后自动恢复。
---
## 6. 选择建议与权衡
| 维度 | 子进程/PTY | tmux |
|---|---|---|
| 复杂度 | 中 | 低 |
| 稳定性 | 高(直连伪终端) | 高(依赖 tmux 自愈) |
| 多会话并发 | 简单(多 PTY | 简单(多 pane/session |
| 恢复/重连 | 需自管 | `tmux` 自带 |
| 可移植性 | 受 pty/Windows 影响 | Linux/macOS 友好 |
| 外部可观测 | 需自行实现 | `tmux capture-pane` 可用 |
**建议**:桌面或服务器长期常驻,优先 **PTY 模式**;已广泛使用 `tmux` 的团队,直接 **tmux 模式**
---
## 7. 测试用例AI 执行)
1. **路由正确性**:构造两个 tokenA、B分别回复不同命令验证注入不串台。
2. **HTML 邮件**:从 iOS/Android 客户端发送富文本,确认能抽取纯文本命令。
3. **代码块优先**:正文包含代码块和普通文本时,优先选用代码块内容。
4. **幂等**:同一封邮件重复投递三次,仅执行一次。
5. **过期处理**:将 `expiresAt` 回退到过去,验证拒绝注入。
6. **白名单**:非授权邮箱发送,日志报警且不注入。
7. **超长命令**:超过 8KB 截断并拒绝,返回提示。
---
## 8. 运行与守护
- 建议用 `pm2`/`systemd` 管理 `relay-pty.js``relay-tmux.js`
- 每 20 分钟向 IMAP 发送 NOOPImapFlow monitor 已处理),断线自动重连;
- 定时清理过期会话与孤儿 `tmux` session / PTY。
---
## 9. 故障排查
- **注入后无响应**检查会话路由是否匹配token、pane/pty确认 Claude 是否在**等待输入**的状态。
- **乱码/截断**:调大 pty 列数tmux 保证 pane 有焦点且无行编辑冲突。
- **重复执行**:确认以 `Message-ID` 去重;避免多客户端造成“已读后又转发”。
- **权限问题**`tmux` 由同一用户启动Node 进程需与 `tmux` 同一用户。
---
## 10. 清单AI 请按序执行)
1. 安装依赖与 `tmux`(如需)。
2. 写入 `.env`IMAP/白名单/模式)。
3. 准备并加载 `SESSION_MAP_PATH`(由发送提醒侧写入)。
4. 选择 **INJECTION_MODE=pty****tmux**,启动对应 `relay-*.js`
5. 发送带 `[TaskPing #TOKEN]` 的提醒邮件,收到后**回复命令**进行回归测试。
6. 通过测试清单后,接入 hooks开启常驻与告警监控。

View File

@ -0,0 +1,13 @@
{
"id": "8003453f-8d95-48c1-992f-9d4fab001c85",
"created": "2025-07-26T18:22:53.439Z",
"expires": "2025-07-27T18:22:53.439Z",
"notification": {
"type": "completed",
"project": "TaskPing",
"message": "[TaskPing] 任务已完成Claude正在等待下一步指令"
},
"status": "waiting",
"commandCount": 0,
"maxCommands": 10
}

507
src/relay/relay-pty.js Normal file
View File

@ -0,0 +1,507 @@
/**
* Relay PTY Service
* 使用 node-pty 管理 Claude Code 进程并注入邮件命令
*/
const { ImapFlow } = require('imapflow');
const { simpleParser } = require('mailparser');
const pino = require('pino');
const { readFileSync, writeFileSync, existsSync } = require('fs');
const { spawn: spawnPty } = require('node-pty');
const dotenv = require('dotenv');
const path = require('path');
// 加载环境变量
dotenv.config();
// 初始化日志
const log = pino({
level: process.env.LOG_LEVEL || 'info',
transport: {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'HH:MM:ss.l',
ignore: 'pid,hostname'
}
}
});
// 会话映射文件路径
const SESS_PATH = process.env.SESSION_MAP_PATH || path.join(__dirname, '../data/session-map.json');
// PTY 池,管理活跃的 Claude Code 进程
const PTY_POOL = new Map();
// 已处理的邮件ID集合用于去重
const PROCESSED_MESSAGES = new Set();
// 加载会话映射
function loadSessions() {
if (!existsSync(SESS_PATH)) return {};
try {
return JSON.parse(readFileSync(SESS_PATH, 'utf8'));
} catch (error) {
log.error({ error }, 'Failed to load session map');
return {};
}
}
// 保存会话映射
function saveSessions(map) {
try {
const dir = path.dirname(SESS_PATH);
if (!existsSync(dir)) {
require('fs').mkdirSync(dir, { recursive: true });
}
writeFileSync(SESS_PATH, JSON.stringify(map, null, 2));
} catch (error) {
log.error({ error }, 'Failed to save session map');
}
}
// 标准化允许的发件人列表
function normalizeAllowlist() {
return (process.env.ALLOWED_SENDERS || '')
.split(',')
.map(s => s.trim().toLowerCase())
.filter(Boolean);
}
const ALLOW = new Set(normalizeAllowlist());
// 检查发件人是否在白名单中
function isAllowed(addressObj) {
if (!addressObj) return false;
const list = []
.concat(addressObj.value || [])
.map(a => (a.address || '').toLowerCase());
return list.some(addr => ALLOW.has(addr));
}
// 从主题中提取 TaskPing token
function extractTokenFromSubject(subject = '') {
// 支持多种格式: [TaskPing #TOKEN], [TaskPing TOKEN], TaskPing: TOKEN
const patterns = [
/\[TaskPing\s+#([A-Za-z0-9_-]+)\]/,
/\[TaskPing\s+([A-Za-z0-9_-]+)\]/,
/TaskPing:\s*([A-Za-z0-9_-]+)/i
];
for (const pattern of patterns) {
const match = subject.match(pattern);
if (match) return match[1];
}
return null;
}
// 从会话ID中提取 token (支持向后兼容)
function extractTokenFromSessionId(sessionId) {
if (!sessionId) return null;
// 如果sessionId本身就是token格式
if (/^[A-Za-z0-9_-]+$/.test(sessionId)) {
return sessionId;
}
// 从UUID格式的sessionId中提取token如果有映射
const sessions = loadSessions();
for (const [token, session] of Object.entries(sessions)) {
if (session.sessionId === sessionId) {
return token;
}
}
return null;
}
// 清理邮件回复内容,提取命令
function stripReply(text = '') {
// 优先识别 ``` 代码块
const codeBlock = text.match(/```[\s\S]*?```/);
if (codeBlock) {
return codeBlock[0].replace(/```/g, '').trim();
}
// 查找 CMD: 前缀的命令
const cmdMatch = text.match(/^CMD:\s*(.+)$/im);
if (cmdMatch) {
return cmdMatch[1].trim();
}
// 清理邮件内容,移除引用和签名
const lines = text.split(/\r?\n/);
const cleanLines = [];
for (const line of lines) {
// 检测邮件引用开始标记
if (line.match(/^>+\s*/) || // 引用标记
line.includes('-----Original Message-----') ||
line.includes('--- Original Message ---') ||
line.includes('在') && line.includes('写道:') ||
line.includes('On') && line.includes('wrote:') ||
line.includes('会话ID:') ||
line.includes('Session ID:')) {
break;
}
// 检测邮件签名
if (line.match(/^--\s*$/) ||
line.includes('Sent from') ||
line.includes('发自我的')) {
break;
}
cleanLines.push(line);
}
// 获取有效内容的第一行
const cleanText = cleanLines.join('\n').trim();
const firstLine = cleanText.split(/\r?\n/).find(l => l.trim().length > 0) || '';
// 长度限制
return firstLine.slice(0, 8192).trim();
}
// 处理邮件消息
async function handleMailMessage(source, uid) {
try {
const parsed = await simpleParser(source);
// 检查是否已处理过
const messageId = parsed.messageId;
if (messageId && PROCESSED_MESSAGES.has(messageId)) {
log.debug({ messageId }, 'Message already processed, skipping');
return;
}
// 验证发件人
if (!isAllowed(parsed.from)) {
log.warn({ from: parsed.from?.text }, 'Sender not allowed');
return;
}
// 提取 token
const subject = parsed.subject || '';
let token = extractTokenFromSubject(subject);
// 如果主题中没有token尝试从邮件头或正文中提取
if (!token) {
// 检查自定义邮件头
const sessionIdHeader = parsed.headers?.get('x-taskping-session-id');
if (sessionIdHeader) {
token = extractTokenFromSessionId(sessionIdHeader);
}
// 从正文中查找会话ID
if (!token) {
const bodyText = parsed.text || '';
const sessionMatch = bodyText.match(/会话ID:\s*([a-f0-9-]{36})/i);
if (sessionMatch) {
token = extractTokenFromSessionId(sessionMatch[1]);
}
}
}
if (!token) {
log.warn({ subject }, 'No token found in email');
return;
}
// 加载会话信息
const sessions = loadSessions();
const sess = sessions[token];
if (!sess) {
log.warn({ token }, 'Session not found');
return;
}
// 检查会话是否过期
if (sess.expiresAt && sess.expiresAt * 1000 < Date.now()) {
log.warn({ token }, 'Session expired');
// 清理过期会话
delete sessions[token];
saveSessions(sessions);
return;
}
// 提取命令
const cmd = stripReply(parsed.text || parsed.html || '');
if (!cmd) {
log.warn({ token }, 'Empty command after stripping');
return;
}
// 获取或创建 PTY
const pty = await getOrCreatePty(token, sess);
if (!pty) {
log.error({ token }, 'Failed to get PTY');
return;
}
// 注入命令
log.info({
token,
command: cmd.slice(0, 120),
from: parsed.from?.text
}, 'Injecting command to Claude Code');
// 发送命令并按回车
pty.write(cmd + '\r');
// 标记邮件为已处理
if (messageId) {
PROCESSED_MESSAGES.add(messageId);
}
// 更新会话状态
sess.lastCommand = cmd;
sess.lastCommandAt = Date.now();
sess.commandCount = (sess.commandCount || 0) + 1;
sessions[token] = sess;
saveSessions(sessions);
} catch (error) {
log.error({ error, uid }, 'Error handling mail message');
}
}
// 获取或创建 PTY 实例
async function getOrCreatePty(token, session) {
// 检查是否已有 PTY 实例
if (PTY_POOL.has(token)) {
const pty = PTY_POOL.get(token);
// 检查 PTY 是否还活着
try {
// node-pty 没有直接的 isAlive 方法,但我们可以尝试写入空字符来测试
pty.write('');
return pty;
} catch (error) {
log.warn({ token }, 'PTY is dead, removing from pool');
PTY_POOL.delete(token);
}
}
// 创建新的 PTY 实例
try {
const shell = process.env.CLAUDE_CLI_PATH || 'claude';
const args = [];
// 如果会话有特定的工作目录,设置它
const cwd = session.cwd || process.cwd();
log.info({ token, shell, cwd }, 'Spawning new Claude Code PTY');
const pty = spawnPty(shell, args, {
name: 'xterm-256color',
cols: 120,
rows: 40,
cwd: cwd,
env: process.env
});
// 存储 PTY 实例
PTY_POOL.set(token, pty);
// 设置输出处理(可选:记录到文件或发送通知)
pty.onData((data) => {
// 可以在这里添加输出日志或通知逻辑
if (process.env.PTY_OUTPUT_LOG === 'true') {
log.debug({ token, output: data.slice(0, 200) }, 'PTY output');
}
});
// 处理 PTY 退出
pty.onExit(({ exitCode, signal }) => {
log.info({ token, exitCode, signal }, 'PTY exited');
PTY_POOL.delete(token);
// 更新会话状态
const sessions = loadSessions();
if (sessions[token]) {
sessions[token].ptyExited = true;
sessions[token].ptyExitedAt = Date.now();
saveSessions(sessions);
}
});
// 等待 Claude Code 初始化
await new Promise(resolve => setTimeout(resolve, 1000));
return pty;
} catch (error) {
log.error({ error, token }, 'Failed to spawn PTY');
return null;
}
}
// 启动 IMAP 监听
async function startImap() {
const client = new ImapFlow({
host: process.env.IMAP_HOST,
port: Number(process.env.IMAP_PORT || 993),
secure: process.env.IMAP_SECURE === 'true',
auth: {
user: process.env.IMAP_USER,
pass: process.env.IMAP_PASS
},
logger: false // 禁用 ImapFlow 的内置日志
});
try {
await client.connect();
log.info('Connected to IMAP server');
// 打开收件箱
const lock = await client.getMailboxLock('INBOX');
try {
// 获取最近的未读邮件
const messages = await client.search({ seen: false });
if (messages.length > 0) {
log.info(`Found ${messages.length} unread messages`);
for (const uid of messages) {
const { source } = await client.download(uid, '1', { uid: true });
const chunks = [];
for await (const chunk of source) {
chunks.push(chunk);
}
await handleMailMessage(Buffer.concat(chunks), uid);
// 标记为已读
await client.messageFlagsAdd(uid, ['\\Seen'], { uid: true });
}
}
// 监听新邮件
log.info('Starting IMAP monitor...');
for await (const msg of client.idle()) {
if (msg.path === 'INBOX' && msg.type === 'exists') {
log.debug({ count: msg.count }, 'New message notification');
// 获取最新的邮件
const messages = await client.search({ seen: false });
for (const uid of messages) {
const { source } = await client.download(uid, '1', { uid: true });
const chunks = [];
for await (const chunk of source) {
chunks.push(chunk);
}
await handleMailMessage(Buffer.concat(chunks), uid);
// 标记为已读
await client.messageFlagsAdd(uid, ['\\Seen'], { uid: true });
}
}
}
} finally {
lock.release();
}
} catch (error) {
log.error({ error }, 'IMAP error');
throw error;
} finally {
await client.logout();
}
}
// 清理过期会话和孤儿 PTY
function cleanupSessions() {
const sessions = loadSessions();
const now = Date.now();
let cleaned = 0;
for (const [token, session] of Object.entries(sessions)) {
// 清理过期会话
if (session.expiresAt && session.expiresAt * 1000 < now) {
delete sessions[token];
cleaned++;
// 终止相关的 PTY
if (PTY_POOL.has(token)) {
const pty = PTY_POOL.get(token);
try {
pty.kill();
} catch (error) {
log.warn({ token, error }, 'Failed to kill PTY');
}
PTY_POOL.delete(token);
}
}
}
if (cleaned > 0) {
log.info({ cleaned }, 'Cleaned up expired sessions');
saveSessions(sessions);
}
}
// 主函数
async function main() {
// 验证配置
const required = ['IMAP_HOST', 'IMAP_USER', 'IMAP_PASS'];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
log.error({ missing }, 'Missing required environment variables');
process.exit(1);
}
// 显示配置信息
log.info({
mode: 'pty',
imapHost: process.env.IMAP_HOST,
imapUser: process.env.IMAP_USER,
allowedSenders: Array.from(ALLOW),
sessionMapPath: SESS_PATH
}, 'Starting relay-pty service');
// 定期清理
setInterval(cleanupSessions, 5 * 60 * 1000); // 每5分钟清理一次
// 处理退出信号
process.on('SIGINT', () => {
log.info('Shutting down...');
// 终止所有 PTY
for (const [token, pty] of PTY_POOL.entries()) {
try {
pty.kill();
} catch (error) {
log.warn({ token }, 'Failed to kill PTY on shutdown');
}
}
process.exit(0);
});
// 启动 IMAP 监听
while (true) {
try {
await startImap();
} catch (error) {
log.error({ error }, 'IMAP connection lost, retrying in 30s...');
await new Promise(resolve => setTimeout(resolve, 30000));
}
}
}
// 仅在作为主模块运行时启动
if (require.main === module) {
main().catch(error => {
log.error({ error }, 'Fatal error');
process.exit(1);
});
}
module.exports = {
handleMailMessage,
getOrCreatePty,
extractTokenFromSubject,
stripReply
};

152
start-relay-pty.js Executable file
View File

@ -0,0 +1,152 @@
#!/usr/bin/env node
/**
* TaskPing PTY Relay 启动脚本
* 启动基于 node-pty 的邮件命令中继服务
*/
const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
// 检查环境配置
function checkConfig() {
const envPath = path.join(__dirname, '.env');
if (!fs.existsSync(envPath)) {
console.error('❌ 错误: 未找到 .env 配置文件');
console.log('\n请先复制 .env.example 到 .env 并配置您的邮件信息:');
console.log(' cp .env.example .env');
console.log(' 然后编辑 .env 文件填入您的邮件配置\n');
process.exit(1);
}
// 加载环境变量
require('dotenv').config();
// 检查必需的配置
const required = ['IMAP_HOST', 'IMAP_USER', 'IMAP_PASS'];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
console.error('❌ 错误: 缺少必需的环境变量:');
missing.forEach(key => console.log(` - ${key}`));
console.log('\n请编辑 .env 文件并填入所有必需的配置\n');
process.exit(1);
}
console.log('✅ 配置检查通过');
console.log(`📧 IMAP服务器: ${process.env.IMAP_HOST}`);
console.log(`👤 邮件账号: ${process.env.IMAP_USER}`);
console.log(`🔒 白名单发件人: ${process.env.ALLOWED_SENDERS || '(未设置,将接受所有邮件)'}`);
console.log(`💾 会话存储路径: ${process.env.SESSION_MAP_PATH || '(使用默认路径)'}`);
console.log('');
}
// 创建会话示例
function createExampleSession() {
const sessionMapPath = process.env.SESSION_MAP_PATH || path.join(__dirname, 'src/data/session-map.json');
const sessionDir = path.dirname(sessionMapPath);
// 确保目录存在
if (!fs.existsSync(sessionDir)) {
fs.mkdirSync(sessionDir, { recursive: true });
}
// 如果会话文件不存在,创建一个示例
if (!fs.existsSync(sessionMapPath)) {
const exampleToken = 'TEST123';
const exampleSession = {
[exampleToken]: {
type: 'pty',
createdAt: Math.floor(Date.now() / 1000),
expiresAt: Math.floor((Date.now() + 24 * 60 * 60 * 1000) / 1000), // 24小时后过期
cwd: process.cwd(),
description: '测试会话 - 发送邮件时主题包含 [TaskPing #TEST123]'
}
};
fs.writeFileSync(sessionMapPath, JSON.stringify(exampleSession, null, 2));
console.log(`📝 已创建示例会话文件: ${sessionMapPath}`);
console.log(`🔑 测试Token: ${exampleToken}`);
console.log(' 发送测试邮件时,主题中包含: [TaskPing #TEST123]');
console.log('');
}
}
// 启动服务
function startService() {
console.log('🚀 正在启动 TaskPing PTY Relay 服务...\n');
const relayPath = path.join(__dirname, 'src/relay/relay-pty.js');
// 使用 node 直接运行,这样可以看到完整的日志输出
const relay = spawn('node', [relayPath], {
stdio: 'inherit',
env: {
...process.env,
INJECTION_MODE: 'pty'
}
});
// 处理退出
process.on('SIGINT', () => {
console.log('\n⏹ 正在停止服务...');
relay.kill('SIGINT');
process.exit(0);
});
relay.on('error', (error) => {
console.error('❌ 启动失败:', error.message);
process.exit(1);
});
relay.on('exit', (code, signal) => {
if (signal) {
console.log(`\n服务已停止 (信号: ${signal})`);
} else if (code !== 0) {
console.error(`\n服务异常退出 (代码: ${code})`);
process.exit(code);
}
});
}
// 显示使用说明
function showInstructions() {
console.log('📖 使用说明:');
console.log('1. 在 Claude Code 中执行任务时,会发送包含 Token 的提醒邮件');
console.log('2. 回复该邮件,内容为要执行的命令');
console.log('3. 支持的命令格式:');
console.log(' - 直接输入命令文本');
console.log(' - 使用 CMD: 前缀,如 "CMD: 继续"');
console.log(' - 使用代码块包裹,如:');
console.log(' ```');
console.log(' 你的命令');
console.log(' ```');
console.log('4. 系统会自动提取命令并注入到对应的 Claude Code 会话中');
console.log('\n⌨ 按 Ctrl+C 停止服务\n');
console.log('━'.repeat(60) + '\n');
}
// 主函数
function main() {
console.log('╔══════════════════════════════════════════════════════════╗');
console.log('║ TaskPing PTY Relay Service ║');
console.log('║ 邮件命令中继服务 - 基于 node-pty 的 PTY 模式 ║');
console.log('╚══════════════════════════════════════════════════════════╝\n');
// 检查配置
checkConfig();
// 创建示例会话
createExampleSession();
// 显示使用说明
showInstructions();
// 启动服务
startService();
}
// 运行
main();

231
test-email-reply.js Executable file
View File

@ -0,0 +1,231 @@
#!/usr/bin/env node
/**
* TaskPing 邮件回复测试工具
* 用于测试邮件命令提取和 PTY 注入功能
*/
const path = require('path');
const fs = require('fs');
// 加载 relay-pty 模块
const {
extractTokenFromSubject,
stripReply,
handleMailMessage
} = require('./src/relay/relay-pty');
// 测试用例
const testCases = [
{
name: '基本命令',
email: {
subject: 'Re: [TaskPing #TEST123] 任务等待您的指示',
from: { text: 'user@example.com', value: [{ address: 'user@example.com' }] },
text: '继续执行\n\n> 原始邮件内容...'
},
expectedToken: 'TEST123',
expectedCommand: '继续执行'
},
{
name: 'CMD前缀',
email: {
subject: 'Re: TaskPing: ABC789',
from: { text: 'user@example.com', value: [{ address: 'user@example.com' }] },
text: 'CMD: npm run build\n\n发自我的iPhone'
},
expectedToken: 'ABC789',
expectedCommand: 'npm run build'
},
{
name: '代码块',
email: {
subject: 'Re: [TaskPing #XYZ456]',
from: { text: 'user@example.com', value: [{ address: 'user@example.com' }] },
text: '这是我的命令:\n\n```\ngit add .\ngit commit -m "Update"\n```\n\n谢谢'
},
expectedToken: 'XYZ456',
expectedCommand: 'git add .\ngit commit -m "Update"'
},
{
name: '复杂邮件引用',
email: {
subject: 'Re: [TaskPing #TASK999] 请输入下一步操作',
from: { text: 'boss@company.com', value: [{ address: 'boss@company.com' }] },
text: `yes, please continue
--
Best regards,
Boss
On 2024-01-01, TaskPing wrote:
> 任务已完成第一步
> 会话ID: 12345-67890
> 请回复您的下一步指示`
},
expectedToken: 'TASK999',
expectedCommand: 'yes, please continue'
},
{
name: 'HTML邮件转纯文本',
email: {
subject: 'Re: [TaskPing #HTML123]',
from: { text: 'user@example.com', value: [{ address: 'user@example.com' }] },
html: '<div>运行测试套件</div><br><blockquote>原始邮件...</blockquote>',
text: '运行测试套件\n\n> 原始邮件...'
},
expectedToken: 'HTML123',
expectedCommand: '运行测试套件'
}
];
// 运行测试
function runTests() {
console.log('🧪 TaskPing 邮件解析测试\n');
let passed = 0;
let failed = 0;
testCases.forEach((testCase, index) => {
console.log(`测试 ${index + 1}: ${testCase.name}`);
try {
// 测试 Token 提取
const token = extractTokenFromSubject(testCase.email.subject);
if (token === testCase.expectedToken) {
console.log(` ✅ Token提取正确: ${token}`);
} else {
console.log(` ❌ Token提取错误: 期望 "${testCase.expectedToken}", 实际 "${token}"`);
failed++;
console.log('');
return;
}
// 测试命令提取
const command = stripReply(testCase.email.text || testCase.email.html);
if (command === testCase.expectedCommand) {
console.log(` ✅ 命令提取正确: "${command}"`);
passed++;
} else {
console.log(` ❌ 命令提取错误:`);
console.log(` 期望: "${testCase.expectedCommand}"`);
console.log(` 实际: "${command}"`);
failed++;
}
} catch (error) {
console.log(` ❌ 测试出错: ${error.message}`);
failed++;
}
console.log('');
});
// 显示结果
console.log('━'.repeat(50));
console.log(`测试完成: ${passed} 通过, ${failed} 失败`);
if (failed === 0) {
console.log('\n✅ 所有测试通过!');
} else {
console.log('\n❌ 部分测试失败,请检查实现');
}
}
// 测试实际邮件处理
async function testEmailProcessing() {
console.log('\n\n📧 测试邮件处理流程\n');
// 设置测试环境变量
process.env.ALLOWED_SENDERS = 'user@example.com,boss@company.com';
process.env.SESSION_MAP_PATH = path.join(__dirname, 'test-session-map.json');
// 创建测试会话
const testSessions = {
'TEST123': {
type: 'pty',
createdAt: Math.floor(Date.now() / 1000),
expiresAt: Math.floor((Date.now() + 3600000) / 1000),
cwd: process.cwd()
}
};
fs.writeFileSync(process.env.SESSION_MAP_PATH, JSON.stringify(testSessions, null, 2));
console.log('✅ 创建测试会话文件');
// 模拟邮件消息
const { simpleParser } = require('mailparser');
const testEmail = `From: user@example.com
To: taskping@example.com
Subject: Re: [TaskPing #TEST123] 测试
Content-Type: text/plain; charset=utf-8
这是测试命令
> 原始邮件内容...`;
try {
console.log('🔄 处理模拟邮件...');
// 注意:实际的 handleMailMessage 会尝试创建 PTY
// 在测试环境中可能会失败,这是预期的
await handleMailMessage(Buffer.from(testEmail), 'test-uid-123');
console.log('✅ 邮件处理流程完成注意PTY创建可能失败这在测试中是正常的');
} catch (error) {
console.log(`⚠️ 邮件处理出错(预期的): ${error.message}`);
}
// 清理测试文件
if (fs.existsSync(process.env.SESSION_MAP_PATH)) {
fs.unlinkSync(process.env.SESSION_MAP_PATH);
console.log('🧹 清理测试文件');
}
}
// 显示集成说明
function showIntegrationGuide() {
console.log('\n\n📚 集成指南\n');
console.log('1. 配置邮件服务器信息:');
console.log(' 编辑 .env 文件,填入 IMAP 配置');
console.log('');
console.log('2. 设置白名单发件人:');
console.log(' ALLOWED_SENDERS=your-email@gmail.com');
console.log('');
console.log('3. 创建会话映射:');
console.log(' 当发送提醒邮件时,在 session-map.json 中添加:');
console.log(' {');
console.log(' "YOUR_TOKEN": {');
console.log(' "type": "pty",');
console.log(' "createdAt": 1234567890,');
console.log(' "expiresAt": 1234567890,');
console.log(' "cwd": "/path/to/project"');
console.log(' }');
console.log(' }');
console.log('');
console.log('4. 启动服务:');
console.log(' ./start-relay-pty.js');
console.log('');
console.log('5. 发送测试邮件:');
console.log(' 主题包含 [TaskPing #YOUR_TOKEN]');
console.log(' 正文为要执行的命令');
}
// 主函数
async function main() {
console.log('╔══════════════════════════════════════════════════════════╗');
console.log('║ TaskPing Email Reply Test Suite ║');
console.log('╚══════════════════════════════════════════════════════════╝\n');
// 运行单元测试
runTests();
// 测试邮件处理
await testEmailProcessing();
// 显示集成指南
showIntegrationGuide();
}
// 运行测试
main().catch(console.error);