dores-codex 4.0.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/.codex/bin/dores-hook-dispatch +43 -0
- package/.codex/hooks.json +62 -0
- package/INSTALL.md +37 -0
- package/README.md +231 -0
- package/codex-plugin-setup +14 -0
- package/codex-plugin-uninstall +14 -0
- package/install.sh +5 -0
- package/lib/client-event.js +144 -0
- package/lib/dores-client.js +76 -0
- package/lib/hook-runner.js +101 -0
- package/lib/hook-shared.js +362 -0
- package/lib/post-tool-use-hook.js +27 -0
- package/lib/pre-tool-use-hook.js +23 -0
- package/lib/setup-hooks.js +193 -0
- package/lib/startup-hook.js +21 -0
- package/lib/stop-hook.js +38 -0
- package/lib/uninstall-hooks.js +88 -0
- package/lib/user-prompt-submit-hook.js +20 -0
- package/package.json +39 -0
- package/uninstall.sh +5 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
hook_event="${1:-}"
|
|
5
|
+
project_root="${2:-${PROJECT_PATH:-}}"
|
|
6
|
+
|
|
7
|
+
if [[ -z "$hook_event" ]]; then
|
|
8
|
+
printf 'usage: dores-hook-dispatch <hook-event> [project-root]\n' >&2
|
|
9
|
+
exit 1
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
if [[ -z "$project_root" ]]; then
|
|
13
|
+
project_root="$PWD"
|
|
14
|
+
while [[ "$project_root" != "/" && ! -f "$project_root/.codex/hooks.json" ]]; do
|
|
15
|
+
project_root="$(dirname "$project_root")"
|
|
16
|
+
done
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
if [[ ! -f "$project_root/lib/hook-runner.js" ]]; then
|
|
20
|
+
printf 'hook runner not found under %s\n' "$project_root" >&2
|
|
21
|
+
exit 1
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
node_bin="$(command -v node || true)"
|
|
25
|
+
if [[ -z "$node_bin" ]]; then
|
|
26
|
+
node_bin="$(command -v nodejs || true)"
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
if [[ -z "$node_bin" ]]; then
|
|
30
|
+
printf 'node runtime not found in PATH\n' >&2
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
runtime_dir="${CODEX_PLUGIN_RUNTIME_DIR:-$project_root/runtime}"
|
|
35
|
+
log_file="${CODEX_PLUGIN_HOOK_LOG_FILE:-$runtime_dir/codex-plugin.log}"
|
|
36
|
+
|
|
37
|
+
exec env \
|
|
38
|
+
CODEX_PLUGIN_HOME="${CODEX_PLUGIN_HOME:-$project_root}" \
|
|
39
|
+
CODEX_PLUGIN_HOOK_DIRECT_LOG="1" \
|
|
40
|
+
CODEX_PLUGIN_HOOK_LOG_FILE="$log_file" \
|
|
41
|
+
CODEX_PLUGIN_RUNTIME_DIR="$runtime_dir" \
|
|
42
|
+
PROJECT_PATH="$project_root" \
|
|
43
|
+
"$node_bin" "$project_root/lib/hook-runner.js" "$hook_event" "$project_root"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "startup|resume",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "bash -lc 'root=\"$PWD\"; while [ \"$root\" != \"/\" ] && [ ! -f \"$root/.codex/hooks.json\" ]; do root=\"$(dirname \"$root\")\"; done; exec \"$root/.codex/bin/dores-hook-dispatch\" SessionStart \"$root\"'",
|
|
10
|
+
"statusMessage": "Dispatching session start hook"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"UserPromptSubmit": [
|
|
16
|
+
{
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "bash -lc 'root=\"$PWD\"; while [ \"$root\" != \"/\" ] && [ ! -f \"$root/.codex/hooks.json\" ]; do root=\"$(dirname \"$root\")\"; done; exec \"$root/.codex/bin/dores-hook-dispatch\" UserPromptSubmit \"$root\"'",
|
|
21
|
+
"statusMessage": "Dispatching prompt hook"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"PreToolUse": [
|
|
27
|
+
{
|
|
28
|
+
"matcher": "Bash",
|
|
29
|
+
"hooks": [
|
|
30
|
+
{
|
|
31
|
+
"type": "command",
|
|
32
|
+
"command": "bash -lc 'root=\"$PWD\"; while [ \"$root\" != \"/\" ] && [ ! -f \"$root/.codex/hooks.json\" ]; do root=\"$(dirname \"$root\")\"; done; exec \"$root/.codex/bin/dores-hook-dispatch\" PreToolUse \"$root\"'",
|
|
33
|
+
"statusMessage": "Dispatching Bash pre-hook"
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"PostToolUse": [
|
|
39
|
+
{
|
|
40
|
+
"matcher": "Bash",
|
|
41
|
+
"hooks": [
|
|
42
|
+
{
|
|
43
|
+
"type": "command",
|
|
44
|
+
"command": "bash -lc 'root=\"$PWD\"; while [ \"$root\" != \"/\" ] && [ ! -f \"$root/.codex/hooks.json\" ]; do root=\"$(dirname \"$root\")\"; done; exec \"$root/.codex/bin/dores-hook-dispatch\" PostToolUse \"$root\"'",
|
|
45
|
+
"statusMessage": "Dispatching Bash post-hook"
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
"Stop": [
|
|
51
|
+
{
|
|
52
|
+
"hooks": [
|
|
53
|
+
{
|
|
54
|
+
"type": "command",
|
|
55
|
+
"command": "bash -lc 'root=\"$PWD\"; while [ \"$root\" != \"/\" ] && [ ! -f \"$root/.codex/hooks.json\" ]; do root=\"$(dirname \"$root\")\"; done; exec \"$root/.codex/bin/dores-hook-dispatch\" Stop \"$root\"'",
|
|
56
|
+
"statusMessage": "Dispatching stop hook"
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
}
|
package/INSTALL.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Install Notes
|
|
2
|
+
|
|
3
|
+
推荐安装流程:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
env npm_config_cache=/tmp/dores-codex-npm-cache npm pack
|
|
7
|
+
npm install -g ./dores-codex-4.0.0.tgz
|
|
8
|
+
codex-plugin-setup
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
如果已经发布到 npm 仓库,也可以直接安装:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g dores-codex@4.0.0
|
|
15
|
+
codex-plugin-setup
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`codex-plugin-setup` 会自动:
|
|
19
|
+
|
|
20
|
+
- 确保 `~/.codex/config.toml` 里启用了 `codex_hooks = true`
|
|
21
|
+
- 把本插件的 hooks 安装到 `~/.codex/hooks.json`
|
|
22
|
+
|
|
23
|
+
setup 完成后,直接运行原生 `codex` 即可触发全局 hooks。
|
|
24
|
+
|
|
25
|
+
默认 websocket 地址:
|
|
26
|
+
|
|
27
|
+
```text
|
|
28
|
+
ws://127.0.0.1:8765
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
如果需要覆盖:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
export CODEX_PLUGIN_APP_WS_URL=ws://127.0.0.1:8765
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
更完整的安装、触发和排查说明见 [README.md](./README.md)。
|
package/README.md
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# dores-codex
|
|
2
|
+
|
|
3
|
+
`dores-codex` 是一个只基于原生 Codex 官方 hooks 的插件包。
|
|
4
|
+
它不会替换 `codex` 命令本身,也不会走桥接层;它做的事情是:
|
|
5
|
+
|
|
6
|
+
- 把原生 Codex hooks 事件转成 websocket 消息
|
|
7
|
+
- 默认发送到 `ws://127.0.0.1:8765`
|
|
8
|
+
- 通过 `codex-plugin-setup` 把 hooks 安装到 `~/.codex/hooks.json`
|
|
9
|
+
- 让你在任意目录启动原生 `codex` 时都能触发这套全局 hooks
|
|
10
|
+
|
|
11
|
+
## 支持的原生 Hooks
|
|
12
|
+
|
|
13
|
+
当前包只处理官方这 5 类 hooks:
|
|
14
|
+
|
|
15
|
+
- `SessionStart`
|
|
16
|
+
- `UserPromptSubmit`
|
|
17
|
+
- `PreToolUse`
|
|
18
|
+
- `PostToolUse`
|
|
19
|
+
- `Stop`
|
|
20
|
+
|
|
21
|
+
对应 websocket `event_type` 映射如下:
|
|
22
|
+
|
|
23
|
+
- `SessionStart` -> `StartWork` 或 `ManualWorkRecovery`
|
|
24
|
+
- `UserPromptSubmit` -> `Statistics`
|
|
25
|
+
- `PreToolUse` -> `Statistics`
|
|
26
|
+
- `PostToolUse` -> `Statistics` 或 `AutoDebugError`
|
|
27
|
+
- `Stop` -> `EndWork`
|
|
28
|
+
|
|
29
|
+
不再支持的旧能力:
|
|
30
|
+
|
|
31
|
+
- 桥接 `bridge`
|
|
32
|
+
- `notify` 驱动的 `TaskComplete`
|
|
33
|
+
- 自定义 wrapper `codex`
|
|
34
|
+
- HTTP 回调链路
|
|
35
|
+
|
|
36
|
+
## 1. 打包本地 npm 包
|
|
37
|
+
|
|
38
|
+
当前版本号是 `4.0.0`。
|
|
39
|
+
|
|
40
|
+
在项目根目录执行:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
env npm_config_cache=/tmp/dores-codex-npm-cache npm pack
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
执行后会生成本地包:
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
dores-codex-4.0.0.tgz
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 2. 安装 npm 包
|
|
53
|
+
|
|
54
|
+
推荐全局安装。这样 setup 写入的 hooks 会稳定指向全局安装路径,后续你在任何目录执行原生 `codex` 都能触发。
|
|
55
|
+
|
|
56
|
+
如果你已经发布到 npm 仓库:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm install -g dores-codex@4.0.0
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
如果你使用本地打包出来的 tgz:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npm install -g ./dores-codex-4.0.0.tgz
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
如果你只是临时测试,也可以本地安装:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm install ./dores-codex-4.0.0.tgz
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
但本地安装不推荐长期使用,因为 hooks 会指向当前项目里的 `node_modules/dores-codex`,项目移动或删除后路径就会失效。
|
|
75
|
+
|
|
76
|
+
## 3. 执行 Setup
|
|
77
|
+
|
|
78
|
+
安装完包以后,**不是直接执行 `codex` 就会生效**。
|
|
79
|
+
你还需要先执行一次 setup,把 hooks 安装到 Codex 会读取的全局位置:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
codex-plugin-setup
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
这个命令会做两件事:
|
|
86
|
+
|
|
87
|
+
1. 确保 `~/.codex/config.toml` 里启用了:
|
|
88
|
+
|
|
89
|
+
```toml
|
|
90
|
+
[features]
|
|
91
|
+
codex_hooks = true
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
2. 把本插件管理的 hooks 写入:
|
|
95
|
+
|
|
96
|
+
```text
|
|
97
|
+
~/.codex/hooks.json
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
写入的是带绝对路径的命令,所以 setup 完成后,你在任意目录执行原生 `codex`,都能加载这套全局 hooks。
|
|
101
|
+
|
|
102
|
+
如果你本来就有自己的 `~/.codex/hooks.json`,`codex-plugin-setup` 会保留已有内容,只更新它自己管理的条目。
|
|
103
|
+
本插件管理的 hook 组会带有 `dores-codex:` 前缀,便于后续卸载和重装。
|
|
104
|
+
|
|
105
|
+
## 4. WebSocket 地址
|
|
106
|
+
|
|
107
|
+
默认 websocket 地址:
|
|
108
|
+
|
|
109
|
+
```text
|
|
110
|
+
ws://127.0.0.1:8765
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
如果你的服务端不是这个地址,可以在启动 `codex` 之前覆盖:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
export CODEX_PLUGIN_APP_WS_URL=ws://127.0.0.1:8765
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
如果你不设置,默认就会连 `ws://127.0.0.1:8765`。
|
|
120
|
+
|
|
121
|
+
## 5. 如何触发原生 Codex hooks
|
|
122
|
+
|
|
123
|
+
setup 完成后,直接启动原生 `codex`:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
codex
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
然后在交互里输入内容,不同场景会触发不同 hooks。
|
|
130
|
+
|
|
131
|
+
### 场景 A:只测试会话开始和结束
|
|
132
|
+
|
|
133
|
+
输入:
|
|
134
|
+
|
|
135
|
+
```text
|
|
136
|
+
1+1=?
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
通常会触发:
|
|
140
|
+
|
|
141
|
+
- `SessionStart`
|
|
142
|
+
- `UserPromptSubmit`
|
|
143
|
+
- `Stop`
|
|
144
|
+
|
|
145
|
+
因为这类问题通常不需要调用 Bash。
|
|
146
|
+
|
|
147
|
+
### 场景 B:测试 Bash 成功执行
|
|
148
|
+
|
|
149
|
+
输入:
|
|
150
|
+
|
|
151
|
+
```text
|
|
152
|
+
Use Bash to run the command pwd exactly once, then reply with only the resulting directory path.
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
通常会触发:
|
|
156
|
+
|
|
157
|
+
- `SessionStart`
|
|
158
|
+
- `UserPromptSubmit`
|
|
159
|
+
- `PreToolUse`
|
|
160
|
+
- `PostToolUse`
|
|
161
|
+
- `Stop`
|
|
162
|
+
|
|
163
|
+
### 场景 C:测试 Bash 失败事件
|
|
164
|
+
|
|
165
|
+
输入:
|
|
166
|
+
|
|
167
|
+
```text
|
|
168
|
+
Use Bash to run the command false exactly once, then reply with only the word failed.
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
通常会触发:
|
|
172
|
+
|
|
173
|
+
- `SessionStart`
|
|
174
|
+
- `UserPromptSubmit`
|
|
175
|
+
- `PreToolUse`
|
|
176
|
+
- `PostToolUse`
|
|
177
|
+
- `Stop`
|
|
178
|
+
|
|
179
|
+
其中 `PostToolUse` 会因为非零退出码映射成 `AutoDebugError`。
|
|
180
|
+
|
|
181
|
+
## 6. 如何判断 hooks 是否真的触发
|
|
182
|
+
|
|
183
|
+
可以看两个地方:
|
|
184
|
+
|
|
185
|
+
- 你的 websocket 服务端是否收到消息
|
|
186
|
+
- 本地日志文件是否有 hook 执行记录
|
|
187
|
+
|
|
188
|
+
默认日志路径在 setup 所安装的包目录运行时对应的:
|
|
189
|
+
|
|
190
|
+
```text
|
|
191
|
+
<package-root>/runtime/codex-plugin.log
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
如果 websocket 没收到消息,先检查:
|
|
195
|
+
|
|
196
|
+
- `codex-plugin-setup` 是否执行过
|
|
197
|
+
- `~/.codex/config.toml` 里是否真的启用了 `codex_hooks = true`
|
|
198
|
+
- `~/.codex/hooks.json` 是否存在
|
|
199
|
+
- websocket 服务是否真的监听在 `ws://127.0.0.1:8765`
|
|
200
|
+
- 你运行的是原生 `codex`,不是别的 wrapper
|
|
201
|
+
|
|
202
|
+
## 7. 卸载
|
|
203
|
+
|
|
204
|
+
删除这套全局 hooks:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
codex-plugin-uninstall
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
这个命令会从 `~/.codex/hooks.json` 里移除本插件写入的 managed hooks。
|
|
211
|
+
它不会自动把 `config.toml` 里的 `codex_hooks` 改回去。
|
|
212
|
+
|
|
213
|
+
如果你还想彻底移除包:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
npm uninstall -g dores-codex
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## 8. 开发校验
|
|
220
|
+
|
|
221
|
+
如果你在源码仓库里改了代码,可以先校验:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
npm run check
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
再重新打包:
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
env npm_config_cache=/tmp/dores-codex-npm-cache npm pack
|
|
231
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
script_path="${BASH_SOURCE[0]}"
|
|
5
|
+
while [[ -L "$script_path" ]]; do
|
|
6
|
+
script_dir="$(cd "$(dirname "$script_path")" && pwd)"
|
|
7
|
+
script_path="$(readlink "$script_path")"
|
|
8
|
+
if [[ "$script_path" != /* ]]; then
|
|
9
|
+
script_path="$script_dir/$script_path"
|
|
10
|
+
fi
|
|
11
|
+
done
|
|
12
|
+
script_dir="$(cd "$(dirname "$script_path")" && pwd)"
|
|
13
|
+
|
|
14
|
+
exec node "$script_dir/lib/setup-hooks.js" "$@"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
script_path="${BASH_SOURCE[0]}"
|
|
5
|
+
while [[ -L "$script_path" ]]; do
|
|
6
|
+
script_dir="$(cd "$(dirname "$script_path")" && pwd)"
|
|
7
|
+
script_path="$(readlink "$script_path")"
|
|
8
|
+
if [[ "$script_path" != /* ]]; then
|
|
9
|
+
script_path="$script_dir/$script_path"
|
|
10
|
+
fi
|
|
11
|
+
done
|
|
12
|
+
script_dir="$(cd "$(dirname "$script_path")" && pwd)"
|
|
13
|
+
|
|
14
|
+
exec node "$script_dir/lib/uninstall-hooks.js" "$@"
|
package/install.sh
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
function splitWords(value) {
|
|
2
|
+
return String(value ?? "")
|
|
3
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
4
|
+
.split(/[^A-Za-z0-9]+/)
|
|
5
|
+
.filter(Boolean);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function toPascalCase(value) {
|
|
9
|
+
return splitWords(value)
|
|
10
|
+
.map((part) => part[0].toUpperCase() + part.slice(1).toLowerCase())
|
|
11
|
+
.join("");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function firstString(...values) {
|
|
15
|
+
for (const value of values) {
|
|
16
|
+
if (typeof value !== "string") {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const trimmed = value.trim();
|
|
21
|
+
if (trimmed) {
|
|
22
|
+
return trimmed;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeTurnStatus(payload = null) {
|
|
30
|
+
return firstString(
|
|
31
|
+
payload?.status,
|
|
32
|
+
payload?.turn?.status,
|
|
33
|
+
payload?.params?.turn?.status,
|
|
34
|
+
payload?.params?.status?.type
|
|
35
|
+
).toLowerCase();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeEventIde(value) {
|
|
39
|
+
const normalized = String(value ?? "").trim().toLowerCase();
|
|
40
|
+
return normalized || "codex";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeHookEventName(event, payload = null) {
|
|
44
|
+
return firstString(payload?.hook_event_name, event, payload?.event);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeStartSource(payload = null) {
|
|
48
|
+
return firstString(payload?.start_source, payload?.raw?.source).toLowerCase();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizeExitCode(payload = null) {
|
|
52
|
+
return Number.isFinite(payload?.exit_code) ? payload.exit_code : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function resolveClientEventType(event, payload = null) {
|
|
56
|
+
const hookEventName = normalizeHookEventName(event, payload);
|
|
57
|
+
|
|
58
|
+
switch (hookEventName) {
|
|
59
|
+
case "SessionStart":
|
|
60
|
+
return normalizeStartSource(payload) === "resume"
|
|
61
|
+
? "ManualWorkRecovery"
|
|
62
|
+
: "StartWork";
|
|
63
|
+
case "UserPromptSubmit":
|
|
64
|
+
case "PreToolUse":
|
|
65
|
+
return "Statistics";
|
|
66
|
+
case "PostToolUse": {
|
|
67
|
+
const exitCode = normalizeExitCode(payload);
|
|
68
|
+
return exitCode != null && exitCode !== 0 ? "AutoDebugError" : "Statistics";
|
|
69
|
+
}
|
|
70
|
+
case "Stop":
|
|
71
|
+
return "EndWork";
|
|
72
|
+
default:
|
|
73
|
+
return `Codex${toPascalCase(hookEventName || "hook")}`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function resolveClientEventPhase(event, payload = null) {
|
|
78
|
+
const eventType = resolveClientEventType(event, payload);
|
|
79
|
+
|
|
80
|
+
switch (eventType) {
|
|
81
|
+
case "StartWork":
|
|
82
|
+
return "start";
|
|
83
|
+
case "ManualWorkRecovery":
|
|
84
|
+
return "recovery";
|
|
85
|
+
case "Statistics":
|
|
86
|
+
return "progress";
|
|
87
|
+
case "AutoDebugError":
|
|
88
|
+
return "error";
|
|
89
|
+
case "EndWork":
|
|
90
|
+
return "stop";
|
|
91
|
+
default:
|
|
92
|
+
return "custom";
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function buildClientEventData(event, payload = null) {
|
|
97
|
+
return {
|
|
98
|
+
command: payload?.command ?? null,
|
|
99
|
+
cwd: payload?.cwd ?? null,
|
|
100
|
+
event_phase: resolveClientEventPhase(event, payload),
|
|
101
|
+
exit_code: payload?.exit_code ?? null,
|
|
102
|
+
hook_event: normalizeHookEventName(event, payload) || null,
|
|
103
|
+
hook_event_name: payload?.hook_event_name ?? null,
|
|
104
|
+
input_text: payload?.input_text ?? null,
|
|
105
|
+
last_assistant_message:
|
|
106
|
+
payload?.last_assistant_message ??
|
|
107
|
+
payload?.raw?.["last-assistant-message"] ??
|
|
108
|
+
null,
|
|
109
|
+
message: payload?.message ?? null,
|
|
110
|
+
model: payload?.model ?? null,
|
|
111
|
+
ok: payload?.ok ?? null,
|
|
112
|
+
reason: payload?.reason ?? null,
|
|
113
|
+
session_id: payload?.session_id ?? null,
|
|
114
|
+
source: payload?.source ?? null,
|
|
115
|
+
start_source: payload?.start_source ?? null,
|
|
116
|
+
status: firstString(payload?.status, normalizeTurnStatus(payload)) || null,
|
|
117
|
+
stop_hook_active:
|
|
118
|
+
typeof payload?.stop_hook_active === "boolean"
|
|
119
|
+
? payload.stop_hook_active
|
|
120
|
+
: null,
|
|
121
|
+
timestamp: payload?.timestamp ?? new Date().toISOString(),
|
|
122
|
+
transcript_path: payload?.transcript_path ?? null,
|
|
123
|
+
tool_name: payload?.tool_name ?? null,
|
|
124
|
+
tool_use_id: payload?.tool_use_id ?? null,
|
|
125
|
+
turn_id: payload?.turn_id ?? null,
|
|
126
|
+
payload: payload ?? {}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function buildClientEventMessage(
|
|
131
|
+
event,
|
|
132
|
+
payload,
|
|
133
|
+
{ eventIde = "codex", source = "codex-native-hook" } = {}
|
|
134
|
+
) {
|
|
135
|
+
return {
|
|
136
|
+
type: "client_event",
|
|
137
|
+
source,
|
|
138
|
+
data: {
|
|
139
|
+
event_type: resolveClientEventType(event, payload),
|
|
140
|
+
event_ide: normalizeEventIde(eventIde),
|
|
141
|
+
event_data: buildClientEventData(event, payload)
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_DORES_CLIENT_WS_URL = "ws://127.0.0.1:8765";
|
|
4
|
+
export const DORES_CLIENT_UNAVAILABLE_MESSAGE =
|
|
5
|
+
"dores客户端未启动,请启动后再重试当前命令";
|
|
6
|
+
|
|
7
|
+
export class DoresClientUnavailableError extends Error {
|
|
8
|
+
constructor(
|
|
9
|
+
message = DORES_CLIENT_UNAVAILABLE_MESSAGE,
|
|
10
|
+
{ cause = null, url = DEFAULT_DORES_CLIENT_WS_URL } = {}
|
|
11
|
+
) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "DoresClientUnavailableError";
|
|
14
|
+
this.code = "dores_client_unavailable";
|
|
15
|
+
this.url = url;
|
|
16
|
+
if (cause) {
|
|
17
|
+
this.cause = cause;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readErrorMessage(error) {
|
|
23
|
+
return error instanceof Error ? error.message.trim() : String(error ?? "").trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isDoresClientUnavailableError(error) {
|
|
27
|
+
return (
|
|
28
|
+
error instanceof DoresClientUnavailableError ||
|
|
29
|
+
error?.code === "dores_client_unavailable" ||
|
|
30
|
+
error?.code === "live2d_client_unavailable"
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function looksLikeDoresClientConnectionFailure(message) {
|
|
35
|
+
if (!message) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
/failed to connect/i.test(message) ||
|
|
41
|
+
/websocket connection failed/i.test(message) ||
|
|
42
|
+
/timed out while connecting/i.test(message) ||
|
|
43
|
+
/socket closed before/i.test(message) ||
|
|
44
|
+
/econnrefused/i.test(message)
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function normalizeDoresClientConnectionError(
|
|
49
|
+
error,
|
|
50
|
+
{ url = DEFAULT_DORES_CLIENT_WS_URL } = {}
|
|
51
|
+
) {
|
|
52
|
+
if (isDoresClientUnavailableError(error)) {
|
|
53
|
+
return error;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const message = readErrorMessage(error);
|
|
57
|
+
if (looksLikeDoresClientConnectionFailure(message)) {
|
|
58
|
+
return new DoresClientUnavailableError(undefined, {
|
|
59
|
+
cause: error instanceof Error ? error : null,
|
|
60
|
+
url
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return error instanceof Error ? error : new Error(message || String(error));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function formatDoresClientErrorMessage(
|
|
68
|
+
error,
|
|
69
|
+
{ fallbackMessage = "操作失败,请稍后重试。" } = {}
|
|
70
|
+
) {
|
|
71
|
+
if (isDoresClientUnavailableError(error)) {
|
|
72
|
+
return error.message || DORES_CLIENT_UNAVAILABLE_MESSAGE;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return readErrorMessage(error) || fallbackMessage;
|
|
76
|
+
}
|