oh-langfuse 0.1.54 → 0.1.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,142 +1,213 @@
1
- # oh-langfuse
2
-
3
- `oh-langfuse` 鏄敤浜庣粰 Claude Code銆丱penCode 鍜?Codex 閰嶇疆 Langfuse 杩借釜鐨勫懡浠よ宸ュ叿銆傚畠鎻愪緵浜や簰寮忓畨瑁呭悜瀵硷紝涔熸敮鎸?`setup` / `check` 鐩存帴鍛戒护锛屾柟渚垮湪鐢ㄦ埛鏈哄櫒涓婂畨瑁呫€佷慨澶嶅拰鏍¢獙閰嶇疆銆?
4
- 褰撳墠 npm 鐗堟湰锛歚0.1.41`
5
-
6
- ## 鑳藉仛浠€涔?
7
- - 缁?Claude Code 瀹夎 `Stop` hook锛屾妸浼氳瘽浜嬩欢鍐欏叆 Langfuse銆?- 缁?OpenCode 瀹夎骞?patch `opencode-plugin-langfuse`锛屽紑鍚?OpenTelemetry銆?- 缁?Codex 瀹夎 `notify` hook锛屽閲忚鍙?session JSONL 骞跺啓鍏?Langfuse銆?- 瀹夎瀹屾垚鍚庤嚜鍔ㄦ墽琛屽搴?`check`锛屽敖鏃╁彂鐜伴厤缃己澶便€?- 鏍¢獙鍛樺伐鍙锋牸寮忥紝蹇呴』鍖归厤 `^[a-z](?:\d{8}|wx\d{7})$`锛屼緥濡?`h00613222` 鎴?`hwx1234567`銆?
8
- ## 蹇€熶娇鐢?
9
- 寤鸿濮嬬粓甯?`@latest`锛岄伩鍏?npx 浣跨敤鏈湴鏃х紦瀛橈細
10
-
11
- ```bash
12
- npx oh-langfuse@latest
13
- npx oh-langfuse@latest setup
14
- npx oh-langfuse@latest check
15
- ```
16
-
17
- 涔熷彲浠ユ寚瀹氱洰鏍囷細
18
-
19
- ```bash
20
- npx oh-langfuse@latest setup claude
21
- npx oh-langfuse@latest setup opencode
22
- npx oh-langfuse@latest setup codex
23
-
24
- npx oh-langfuse@latest check claude
25
- npx oh-langfuse@latest check opencode
26
- npx oh-langfuse@latest check codex
27
- ```
28
-
29
- ## 鑷姩鏇存柊
30
-
31
- 瀹夎鎴栨洿鏂?runtime 鍚庯紝宸ュ叿浼氬湪 `~/.config/oh-langfuse/runtime.json` 璁板綍 Claude Code / OpenCode / Codex 褰撳墠鍐欏叆鏈満 runtime 鐨?`oh-langfuse` 鐗堟湰銆?
32
- OpenCode 鐢熸垚鐨?`launch-opencode-langfuse.*` 浼氬湪鍚姩 agent 鍓嶆墽琛屼竴娆℃洿鏂版娴嬶細濡傛灉 npm 涓婄殑 `oh-langfuse@latest` 楂樹簬鏈満 runtime 鐗堟湰锛屼細鎻愮ず鏄惁鏇存柊锛涚敤鎴风‘璁ゅ悗鎵嶄細杩愯 `npx oh-langfuse@latest update opencode`銆侰laude Code 鍜?Codex 涔熶細鐢熸垚瀵瑰簲 launcher锛?
33
- OpenCode setup 杩樹細鐢熸垚 `~/.config/opencode/bin/opencode` / `opencode.cmd` 鍛戒护鍖呰鍣紝骞舵妸璇ョ洰褰曞啓鍏ョ敤鎴?PATH銆傛柊寮€缁堢鍚庣洿鎺ヨ繍琛?`opencode`锛屼細鍏堢敤杞婚噺鑴氭湰妫€娴?`oh-langfuse` runtime 鏄惁闇€瑕佹洿鏂帮紝鍐嶈浆鍙戝埌鐪熷疄 OpenCode CLI銆備负浜嗛伩鍏?Windows 涓嬪湪 agent 鍚姩閾捐矾閲屽祵濂楄繍琛?Node/npm installer 瀵艰嚧鍚姩宕╂簝锛宍opencode` 鍛戒护鍙仛妫€娴嬪拰鎻愮ず锛涢渶瑕佹洿鏂版椂璇峰叧闂?agent 鍚庤繍琛屾彁绀轰腑鐨?`npx oh-langfuse@latest update opencode`銆?
34
- - `~/.claude/launch-claude-langfuse.cmd` / `~/.claude/launch-claude-langfuse.sh`
35
- - `~/.codex/launch-codex-langfuse.cmd` / `~/.codex/launch-codex-langfuse.sh`
36
-
37
- 濡傛灉鐩存帴杩愯绯荤粺鍘熷鐨?`claude` / `codex` 鍛戒护锛屽伐鍏蜂笉浼氬己琛屾浛鎹㈣鍛戒护锛涢渶瑕佷娇鐢ㄤ笂杩?launcher 鎵嶈兘鍋氬埌鍚姩鍓嶆娴嬪苟鎻愮ず鏇存柊銆備篃鍙互鎵嬪姩杩愯锛?
38
- ```bash
39
- npx oh-langfuse@latest auto-update opencode
40
- npx oh-langfuse@latest auto-update claude
41
- npx oh-langfuse@latest auto-update codex
42
- ```
43
-
44
- ## Skill 使用量统计
45
-
46
- 工具会把 skill 使用汇总写入每次交互的 `Agent Turn` observation,不再额外生成 `Skill Use` observation。
47
-
48
- ```text
49
- Observation Name: Agent Turn
50
- metadata.skill_use_count: 本轮 skill 使用总次数
51
- metadata.unique_skill_count: 本轮不同 skill 数量
52
- metadata.repeated_skill_count: 本轮重复 skill 使用次数
53
- metadata.skill_names: 去重后的 skill 名称列表
54
- metadata.skill_names_all: 按使用顺序记录的 skill 名称列表
55
- ```
56
-
57
- Dashboard 中统计全部 skill 使用量可配置:
58
-
59
- ```text
60
- View: Observations
61
- Metric: Sum skill_use_count
62
- Filter: Observation Name equals Agent Turn
63
- ```
64
-
65
- 如果当前 Langfuse Dashboard 支持 metadata 维度,可用 `metadata.skill_names` 或导出数据中的 `metadata.skill_names_all` 按 skill 聚合。`skill_names_json`、`skill_names_all_json` 和 `skill_use_events_json` 不再写入,避免和数组字段重复。
66
- 鏈湴寮€鍙戣繍琛岋細
67
-
68
- ```bash
69
- npm install
70
- npm start
71
- ```
72
-
73
- ## 浜や簰寮忚彍鍗?
74
- 杩愯 `npx oh-langfuse@latest` 浼氭墦寮€浜や簰寮忚彍鍗曪細
75
-
76
- - `Up` / `Down` 绉诲姩閫夐」銆?- 鏁板瓧閿揩閫熷畾浣嶃€?- `Space` 鍕鹃€夋垨鍙栨秷銆?- `Enter` 纭銆?- `q` 鎴?`Esc` 閫€鍑恒€?
77
- 澶氶€夎彍鍗曢粯璁や笉鍕鹃€変换浣曠洰鏍囷紝閬垮厤璇銆傜粓绔笉鏀寔鍘熷鎸夐敭杈撳叆鏃讹紝浼氳嚜鍔ㄩ檷绾т负鏁板瓧杈撳叆妯″紡銆?
78
- ## OpenCode 璇存槑
79
-
80
- OpenCode 瀹夎浼氬啓鍏ワ細
81
-
82
- - `~/.config/opencode/opencode.json`
83
- - `~/.config/opencode/plugins/opencode-plugin-langfuse`
84
- - `~/.config/opencode-plugin-langfuse/config.json`
85
- - Windows 涓嬬殑 `~/.config/opencode/launch-opencode-langfuse.cmd`
86
- - Linux/macOS 涓嬬殑 `~/.config/opencode/launch-opencode-langfuse.sh`
87
-
88
- Windows 涓嬪畨瑁呭櫒浼氬皾璇曞啓鍏ョ敤鎴风骇 `LANGFUSE_PUBLIC_KEY`銆乣LANGFUSE_SECRET_KEY`銆乣LANGFUSE_BASEURL`銆傝繖浜涘彉閲忓褰撳墠宸茬粡鎵撳紑鐨勭粓绔笉浼氱珛鍗崇敓鏁堬紝闇€瑕佹柊寮€缁堢锛屾垨鑰呬娇鐢ㄧ敓鎴愮殑 launcher 鍚姩 OpenCode銆?
89
- `check opencode` 宸叉寜瀹為檯鎯呭喌澶勭悊 Windows锛氬鏋滃綋鍓嶇粓绔繕鐪嬩笉鍒?`LANGFUSE_*`锛屼絾鐢ㄦ埛绾х幆澧冨彉閲忓凡鍐欏叆锛屾垨 launcher 宸茬敓鎴愶紝浼氭樉绀?OpenCode 浠嶆湁鍙敤鐨?Langfuse 鐜鍙橀噺鏉ユ簮锛屼笉鍐嶆妸杩欑鎯呭喌璇垽涓哄畨瑁呭け璐ャ€?
90
- WSL 鐢ㄦ埛璇风洿鎺ュ湪 WSL shell 涓畨瑁呭拰妫€鏌ワ細
91
-
92
- ```bash
93
- npx oh-langfuse@latest setup opencode --userId=h00613222 --yes
94
- npx oh-langfuse@latest check opencode
95
- ```
96
-
97
- 涓嶄細鍋?Windows 鍒?WSL 鐨勮浆鍙戯紱鍦?WSL 涓繍琛屾椂锛岄厤缃啓鍏?WSL 鐢ㄦ埛鑷繁鐨?`$HOME/.config/opencode`銆?
98
- ## Claude Code 璇存槑
99
-
100
- Claude Code 瀹夎浼氾細
101
-
102
- - 瀹夎 `langfuse_hook.py`
103
- - 鍒涘缓 `~/.claude/langfuse-venv`
104
- - 瀹夎 Python 鍖?`langfuse`
105
- - 鍚堝苟鏇存柊 `~/.claude/settings.json`
106
- - 閰嶇疆 `Stop` hook
107
-
108
- Linux 濡傛灉缂哄皯 venv 鏀寔锛岃鏍规嵁鎻愮ず瀹夎 `python3-venv` 鎴栧搴?Python 鐗堟湰鐨?venv 鍖呫€?
109
- ## Codex 璇存槑
110
-
111
- Codex 瀹夎浼氾細
112
-
113
- - 瀹夎 `codex_langfuse_notify.py`
114
- - 鍒涘缓 `~/.codex/langfuse-venv`
115
- - 鍐欏叆 `~/.codex/langfuse/config.json`
116
- - 鏇存柊 `~/.codex/config.toml` 椤跺眰 `notify`
117
-
118
- 閰嶇疆鍚庨渶瑕侀噸鍚?Codex銆俷otify hook 浼氬閲忚鍙?`~/.codex/sessions/**/*.jsonl`锛屾妸鏂扮殑 user銆乤ssistant銆乼ool銆乼oken 浜嬩欢杞崲涓?Langfuse observation锛屽苟鎶婅鍙栧亸绉昏褰曞埌 `~/.codex/langfuse/state.json`銆?
119
- ## 鐜鍙橀噺
120
-
121
- | 鍙橀噺 | 浣滅敤 |
122
- | --- | --- |
123
- | `LANGFUSE_BASEURL` / `LANGFUSE_HOST` | Langfuse 鏈嶅姟鍦板潃 |
124
- | `LANGFUSE_PUBLIC_KEY` | Langfuse public key |
125
- | `LANGFUSE_SECRET_KEY` | Langfuse secret key |
126
- | `LANGFUSE_USER_ID` / `CC_USER_ID` | 鐢ㄦ埛鏍囪瘑锛屽繀椤诲尮閰嶅憳宸ュ彿瑙勫垯 |
127
- | `CODEX_HOME` | 鑷畾涔?Codex home 鐩綍 |
128
- | `OPENCODE_SKIP_PLUGIN_INSTALL` | 璺宠繃 OpenCode npm 鎻掍欢瀹夎 |
129
- | `OPENCODE_NPM_REGISTRY` | OpenCode 鎻掍欢 npm 瀹夎婧?|
130
-
131
- Secret key 涓嶄細鍦ㄤ氦浜掑紡鐣岄潰涓槑鏂囧睍绀恒€?
132
- ## 甯哥敤缁存姢鍛戒护
133
-
134
- ```bash
135
- npm run check
136
- npm pack --dry-run
137
- ```
138
-
139
- 鍙戝竷鍖呮毚闇蹭袱涓懡浠わ細
140
-
141
- - `oh-langfuse`
142
- - `code-tool-langfuse`锛屽吋瀹规棫鍏ュ彛
1
+ # oh-langfuse
2
+
3
+ `oh-langfuse` 是用于给 Claude Code、OpenCode、Codex 配置 Langfuse 追踪的命令行工具。它既提供交互式安装向导,也支持 `setup`、`check`、`update`、`auto-update` 等直接命令。
4
+
5
+ 当前 npm 版本:`0.1.56`
6
+
7
+ ## 能做什么
8
+
9
+ - 为 Claude Code 安装 `Stop` hook,把会话事件写入 Langfuse。
10
+ - 为 OpenCode 安装并 patch `opencode-plugin-langfuse`,开启 OpenTelemetry,并输出 `Agent Turn` 指标。
11
+ - 为 Codex 安装 `notify` hook,增量读取 session JSONL 并写入 Langfuse。
12
+ - 记录本地 runtime 版本,支持启动前更新检查和手动更新。
13
+ - 校验员工号、环境变量、插件、hook、launcher、命令 shim 是否安装完整。
14
+
15
+ 员工号必须匹配:
16
+
17
+ ```text
18
+ ^[a-z](?:\d{8}|wx\d{7})$
19
+ ```
20
+
21
+ 示例:`h00613222`、`hwx1234567`。
22
+
23
+ ## 快速使用
24
+
25
+ 建议始终带 `@latest`:
26
+
27
+ ```bash
28
+ npx oh-langfuse@latest
29
+ npx oh-langfuse@latest setup
30
+ npx oh-langfuse@latest check
31
+ ```
32
+
33
+ 指定目标:
34
+
35
+ ```bash
36
+ npx oh-langfuse@latest setup claude
37
+ npx oh-langfuse@latest setup opencode
38
+ npx oh-langfuse@latest setup codex
39
+
40
+ npx oh-langfuse@latest check claude
41
+ npx oh-langfuse@latest check opencode
42
+ npx oh-langfuse@latest check codex
43
+ ```
44
+
45
+ 更新已安装运行时:
46
+
47
+ ```bash
48
+ npx oh-langfuse@latest update all
49
+ npx oh-langfuse@latest update claude
50
+ npx oh-langfuse@latest update opencode
51
+ npx oh-langfuse@latest update codex
52
+ ```
53
+
54
+ ## 交互式菜单
55
+
56
+ 运行 `npx oh-langfuse@latest` 会打开菜单:
57
+
58
+ - `Setup Langfuse`:选择 Claude Code、OpenCode、Codex 中的一个或多个目标安装。
59
+ - `Update Installed Runtimes`:刷新已安装目标的 hook、plugin、launcher、命令 shim 和 runtime 版本记录。
60
+ - `Check Environment`:检查 Node.js、npm、Python、pip、OpenCode CLI 等基础依赖。
61
+ - `Check Configuration`:检查已写入的目标配置。
62
+
63
+ 多选菜单默认不选中任何目标,避免误写本地配置。
64
+
65
+ ## 自动更新
66
+
67
+ 安装或更新 runtime 后,工具会在:
68
+
69
+ ```text
70
+ ~/.config/oh-langfuse/runtime.json
71
+ ```
72
+
73
+ 记录 Claude Code / OpenCode / Codex 当前写入本机 runtime 的包名和版本。
74
+
75
+ OpenCode setup 会生成直接命令 shim:
76
+
77
+ - Windows:`~/.config/opencode/bin/opencode.cmd`
78
+ - Linux/macOS:`~/.config/opencode/bin/opencode`
79
+
80
+ 新开终端后直接运行 `opencode`,会先做轻量更新检查,再转发到真实 OpenCode CLI。为了避免在 agent 启动链路里嵌套运行完整 installer,shim 默认只提示更新命令;需要更新时关闭 agent 后运行:
81
+
82
+ ```bash
83
+ npx oh-langfuse@latest update opencode
84
+ ```
85
+
86
+ Claude Code Codex 也会生成 launcher:
87
+
88
+ - `~/.claude/launch-claude-langfuse.cmd` / `~/.claude/launch-claude-langfuse.sh`
89
+ - `~/.codex/launch-codex-langfuse.cmd` / `~/.codex/launch-codex-langfuse.sh`
90
+
91
+ 也可以手动触发轻量检查:
92
+
93
+ ```bash
94
+ npx oh-langfuse@latest auto-update opencode
95
+ npx oh-langfuse@latest auto-update claude
96
+ npx oh-langfuse@latest auto-update codex
97
+ ```
98
+
99
+ ## Skill 使用统计
100
+
101
+ 当前版本把 skill 使用汇总写入每次交互的 `Agent Turn` observation,不再额外生成独立 `Skill Use` observation。
102
+
103
+ 常见字段:
104
+
105
+ ```text
106
+ metadata.skill_use_count
107
+ metadata.unique_skill_count
108
+ metadata.repeated_skill_count
109
+ metadata.skill_names
110
+ metadata.skill_names_all
111
+ metadata.skill_invocation_modes
112
+ metadata.skill_agent_paths
113
+ ```
114
+
115
+ Dashboard 统计建议:
116
+
117
+ ```text
118
+ View: Observations
119
+ Filter: Observation Name equals Agent Turn
120
+ Metric: Sum skill_use_count
121
+ ```
122
+
123
+ 如果 Langfuse Dashboard 支持 metadata 维度,可用 `metadata.skill_names` 聚合;导出数据时可用 `metadata.skill_names_all` 保留重复调用顺序。
124
+
125
+ ## 各目标写入内容
126
+
127
+ ### OpenCode
128
+
129
+ 写入或更新:
130
+
131
+ - `~/.config/opencode/opencode.json`
132
+ - `~/.config/opencode/plugins/opencode-plugin-langfuse`
133
+ - `~/.config/opencode-plugin-langfuse/config.json`
134
+ - `~/.config/opencode/launch-opencode-langfuse.cmd` 或 `.sh`
135
+ - `~/.config/opencode/bin/opencode.cmd` 或 `opencode`
136
+
137
+ Windows 下会尝试写入用户级 `LANGFUSE_PUBLIC_KEY`、`LANGFUSE_SECRET_KEY`、`LANGFUSE_BASEURL`、`LANGFUSE_USER_ID`。当前终端不会立即继承用户级环境变量,新开终端或使用 launcher 即可生效。
138
+
139
+ ### Claude Code
140
+
141
+ 写入或更新:
142
+
143
+ - `~/.claude/hooks/langfuse_hook.py`
144
+ - `~/.claude/langfuse-venv`
145
+ - `~/.claude/settings.json`
146
+ - `~/.claude/bin/claude.cmd` 或 `claude`
147
+ - `~/.claude/launch-claude-langfuse.cmd` 或 `.sh`
148
+
149
+ Linux 环境缺少 `python3-venv/ensurepip` 时,安装器会降级尝试 `python3 -m pip --user`、`pip3 --user`、`pip --user` 安装 `langfuse`。
150
+
151
+ ### Codex
152
+
153
+ 写入或更新:
154
+
155
+ - `~/.codex/hooks/codex_langfuse_notify.py`
156
+ - `~/.codex/langfuse-venv`
157
+ - `~/.codex/langfuse/config.json`
158
+ - `~/.codex/config.toml` 顶层 `notify`
159
+ - `~/.codex/bin/codex.cmd` 或 `codex`
160
+ - `~/.codex/launch-codex-langfuse.cmd` 或 `.sh`
161
+
162
+ 配置后需要重启 Codex,让新的 notify 命令加载。notify hook 会增量读取 `~/.codex/sessions/**/*.jsonl`,并把读取偏移记录到 `~/.codex/langfuse/state.json`。
163
+
164
+ ## 环境变量
165
+
166
+ | 变量 | 作用 |
167
+ | --- | --- |
168
+ | `LANGFUSE_BASEURL` / `LANGFUSE_HOST` | Langfuse 服务地址 |
169
+ | `LANGFUSE_PUBLIC_KEY` | Langfuse public key |
170
+ | `LANGFUSE_SECRET_KEY` | Langfuse secret key |
171
+ | `LANGFUSE_USER_ID` / `CC_USER_ID` | 用户标识,必须匹配员工号规则 |
172
+ | `CODEX_HOME` | 自定义 Codex home 目录 |
173
+ | `OPENCODE_SKIP_PLUGIN_INSTALL` | 跳过 OpenCode npm 插件安装 |
174
+ | `OPENCODE_NPM_REGISTRY` | OpenCode 插件 npm 安装 registry |
175
+ | `LANGFUSE_PIP_INDEX_URL` | Python `langfuse` 安装 index |
176
+
177
+ Secret key 不会在交互式界面中明文展示。
178
+
179
+ ## WSL
180
+
181
+ WSL 用户请在 WSL shell 中直接安装和检查:
182
+
183
+ ```bash
184
+ npx oh-langfuse@latest setup opencode --userId=h00613222 --yes
185
+ npx oh-langfuse@latest check opencode
186
+ ```
187
+
188
+ 工具不会做 Windows 到 WSL 的转发;在 WSL 中运行时,配置写入 WSL 用户自己的 `$HOME/.config/opencode`。
189
+
190
+ ## 开发与验证
191
+
192
+ ```bash
193
+ npm run check
194
+ npm test
195
+ npm pack --dry-run
196
+ ```
197
+
198
+ 涉及安装、hook、CLI、OpenCode、Claude Code、Codex、Langfuse、packaging 或 verification 行为的修改,应运行真实闭环验证:
199
+
200
+ ```bash
201
+ npm run self:verify -- --targets=opencode --userId=h00613222
202
+ ```
203
+
204
+ 完整覆盖可运行:
205
+
206
+ ```bash
207
+ npm run self:verify -- --targets=claude,opencode,codex --userId=h00613222
208
+ ```
209
+
210
+ 发布包暴露两个命令:
211
+
212
+ - `oh-langfuse`
213
+ - `code-tool-langfuse`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-langfuse",
3
- "version": "0.1.54",
3
+ "version": "0.1.56",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Use npm scripts to configure Claude Code / OpenCode / Codex with Langfuse tracing.",
@@ -38,14 +38,31 @@ function findLatestSession(sessionsDir) {
38
38
  return latest;
39
39
  }
40
40
 
41
- function commandOk(cmd, args) {
42
- const r = spawnSync(cmd, args, { encoding: "utf8" });
43
- return { ok: !r.error && r.status === 0, detail: (r.stdout || r.stderr || "").trim() };
44
- }
45
-
46
- function venvPython(codexHome) {
47
- return process.platform === "win32"
48
- ? path.join(codexHome, "langfuse-venv", "Scripts", "python.exe")
41
+ function commandOk(cmd, args) {
42
+ const r = spawnSync(cmd, args, { encoding: "utf8" });
43
+ return { ok: !r.error && r.status === 0, detail: (r.stdout || r.stderr || "").trim() };
44
+ }
45
+
46
+ function isWindowsAppsPath(candidate) {
47
+ return String(candidate || "").toLowerCase().includes("\\program files\\windowsapps\\");
48
+ }
49
+
50
+ function readCodexShimTarget(codexHome) {
51
+ if (process.platform !== "win32") return "";
52
+ const shimPath = path.join(codexHome, "bin", "codex.cmd");
53
+ if (!fs.existsSync(shimPath)) return "";
54
+ const text = stripBom(fs.readFileSync(shimPath, "utf8"));
55
+ const matches = text.matchAll(/^\s*call\s+"([^"]+)"/gim);
56
+ for (const match of matches) {
57
+ const base = path.basename(match[1]).toLowerCase();
58
+ if (base === "codex.exe" || base === "codex.cmd" || base === "codex") return match[1];
59
+ }
60
+ return "";
61
+ }
62
+
63
+ function venvPython(codexHome) {
64
+ return process.platform === "win32"
65
+ ? path.join(codexHome, "langfuse-venv", "Scripts", "python.exe")
49
66
  : path.join(codexHome, "langfuse-venv", "bin", "python");
50
67
  }
51
68
 
@@ -68,11 +85,13 @@ function main() {
68
85
  const latestSession = findLatestSession(sessionsDir);
69
86
  const langfuseConfig = readJsonIfExists(langfuseConfigPath);
70
87
  const configText = fs.existsSync(configPath) ? stripBom(fs.readFileSync(configPath, "utf8")) : "";
71
- const hookPython = venvPython(codexHome);
72
- const python = commandOk(process.platform === "win32" ? "python" : "python3", ["--version"]);
73
- const langfuseImport = commandOk(hookPython, ["-c", "import langfuse; print('langfuse ok')"]);
74
-
75
- const logPath = path.join(codexHome, "langfuse", "codex_langfuse_notify.log");
88
+ const hookPython = venvPython(codexHome);
89
+ const python = commandOk(process.platform === "win32" ? "python" : "python3", ["--version"]);
90
+ const langfuseImport = commandOk(hookPython, ["-c", "import langfuse; print('langfuse ok')"]);
91
+ const codexShimTarget = readCodexShimTarget(codexHome);
92
+ const codexShimUsesWindowsApps = !!codexShimTarget && isWindowsAppsPath(codexShimTarget);
93
+
94
+ const logPath = path.join(codexHome, "langfuse", "codex_langfuse_notify.log");
76
95
  const logText = fs.existsSync(logPath) ? stripBom(fs.readFileSync(logPath, "utf8")) : "";
77
96
  const recentLogHasError = /Traceback|ERROR|Exception|Failed/i.test(logText.slice(-4000));
78
97
 
@@ -111,8 +130,15 @@ function main() {
111
130
  "Start a Codex conversation, then check again.",
112
131
  { required: false }
113
132
  );
114
- addResult(results, "Python", python.ok, python.detail || "not found", "Install Python and pip, then rerun setup.");
115
- addResult(results, "Langfuse venv Python", fs.existsSync(hookPython), hookPython, "Run setup again; on Linux install python3-venv if venv creation fails.");
133
+ addResult(results, "Python", python.ok, python.detail || "not found", "Install Python and pip, then rerun setup.");
134
+ addResult(
135
+ results,
136
+ "Codex command shim target",
137
+ !codexShimUsesWindowsApps,
138
+ codexShimTarget || "not installed",
139
+ "Run setup/update again with a fixed oh-langfuse version; WindowsApps Codex paths cannot be called directly."
140
+ );
141
+ addResult(results, "Langfuse venv Python", fs.existsSync(hookPython), hookPython, "Run setup again; on Linux install python3-venv if venv creation fails.");
116
142
  addResult(
117
143
  results,
118
144
  "Python langfuse package",
@@ -59,13 +59,54 @@ function tomlArray(items) {
59
59
  return `[${items.map(tomlString).join(", ")}]`;
60
60
  }
61
61
 
62
- function pythonExecutableInVenv(venvDir) {
63
- return process.platform === "win32"
64
- ? path.join(venvDir, "Scripts", "python.exe")
65
- : path.join(venvDir, "bin", "python");
66
- }
67
-
68
- function shQuote(s) {
62
+ function pythonExecutableInVenv(venvDir) {
63
+ return process.platform === "win32"
64
+ ? path.join(venvDir, "Scripts", "python.exe")
65
+ : path.join(venvDir, "bin", "python");
66
+ }
67
+
68
+ function pythonCanImport(pythonCmd, moduleName) {
69
+ try {
70
+ execFileSync(pythonCmd, ["-c", `import ${moduleName}`], { stdio: "ignore" });
71
+ return true;
72
+ } catch {
73
+ return false;
74
+ }
75
+ }
76
+
77
+ function runPipInstallWithFallback({ pythonCmd, pipIndexUrl }) {
78
+ const attempts = [
79
+ { command: pythonCmd, args: ["-m", "pip", "install", "--user", "-U", "langfuse", "-i", pipIndexUrl] },
80
+ { command: "pip3", args: ["install", "--user", "-U", "langfuse", "-i", pipIndexUrl] },
81
+ { command: "pip", args: ["install", "--user", "-U", "langfuse", "-i", pipIndexUrl] }
82
+ ];
83
+
84
+ const errors = [];
85
+ for (const attempt of attempts) {
86
+ try {
87
+ console.log(`Trying Python package install: ${attempt.command} ${attempt.args.join(" ")}`);
88
+ execFileSync(attempt.command, attempt.args, { stdio: "inherit" });
89
+ return;
90
+ } catch (error) {
91
+ errors.push(`${attempt.command}: ${error?.message || error}`);
92
+ }
93
+ }
94
+
95
+ throw new Error(
96
+ `Failed to install langfuse with system Python/pip. Tried python -m pip, pip3, and pip. Last errors: ${errors.join(" | ")}`
97
+ );
98
+ }
99
+
100
+ function installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl }) {
101
+ console.log("Python venv is unavailable; falling back to system Python user install for langfuse.");
102
+ runPipInstallWithFallback({ pythonCmd, pipIndexUrl });
103
+ if (!pythonCanImport(pythonCmd, "langfuse")) {
104
+ throw new Error("langfuse was installed with pip, but python3 still cannot import it. Install python3-venv and rerun setup, for example: sudo apt install python3-venv");
105
+ }
106
+ return pythonCmd;
107
+ }
108
+
109
+ function shQuote(s) {
69
110
  return `'${String(s).replace(/'/g, "'\"'\"'")}'`;
70
111
  }
71
112
 
@@ -140,19 +181,44 @@ function isPathInsideDir(candidate, dir) {
140
181
  return relative === "" || (relative && !relative.startsWith("..") && !path.isAbsolute(relative));
141
182
  }
142
183
 
143
- function existingCliCandidate(candidate, shimDir) {
144
- if (!candidate || isPathInsideDir(candidate, shimDir)) return "";
145
- try {
146
- return fs.existsSync(candidate) ? candidate : "";
147
- } catch {
148
- return "";
149
- }
150
- }
151
-
152
- function listCliCandidatesFromPath(target) {
153
- const cmd = process.platform === "win32" ? "where.exe" : "which";
154
- const args = process.platform === "win32" ? [target] : ["-a", target];
155
- const result = spawnSync(cmd, args, { encoding: "utf8", windowsHide: true });
184
+ function existingCliCandidate(candidate, shimDir) {
185
+ if (!candidate || isPathInsideDir(candidate, shimDir)) return "";
186
+ if (process.platform === "win32" && isWindowsAppsPath(candidate)) return "";
187
+ try {
188
+ return fs.existsSync(candidate) ? candidate : "";
189
+ } catch {
190
+ return "";
191
+ }
192
+ }
193
+
194
+ function isWindowsAppsPath(candidate) {
195
+ return String(candidate || "").toLowerCase().includes("\\program files\\windowsapps\\");
196
+ }
197
+
198
+ function listLocalCodexCliCandidates() {
199
+ if (process.platform !== "win32") return [];
200
+ const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
201
+ const codexBin = path.join(localAppData, "OpenAI", "Codex", "bin");
202
+ const candidates = [
203
+ process.env.CODEX_CLI_PATH || "",
204
+ path.join(codexBin, "codex.exe"),
205
+ ];
206
+ try {
207
+ if (fs.existsSync(codexBin)) {
208
+ for (const ent of fs.readdirSync(codexBin, { withFileTypes: true })) {
209
+ if (ent.isDirectory()) candidates.push(path.join(codexBin, ent.name, "codex.exe"));
210
+ }
211
+ }
212
+ } catch {
213
+ // Best effort only; PATH candidates are still checked below.
214
+ }
215
+ return candidates;
216
+ }
217
+
218
+ function listCliCandidatesFromPath(target) {
219
+ const cmd = process.platform === "win32" ? "where.exe" : "which";
220
+ const args = process.platform === "win32" ? [target] : ["-a", target];
221
+ const result = spawnSync(cmd, args, { encoding: "utf8", windowsHide: true });
156
222
  if (result.status !== 0) return [];
157
223
  const candidates = String(result.stdout || "")
158
224
  .split(/\r?\n/)
@@ -170,13 +236,14 @@ function sortWindowsCliCandidates(candidates) {
170
236
  return [...candidates].sort((a, b) => extPriority(a) - extPriority(b));
171
237
  }
172
238
 
173
- function resolveAgentCli({ target, preferred = "", shimDir = "" }) {
174
- const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
175
- const candidates = [
176
- preferred,
177
- process.platform === "win32" ? path.join(appData, "npm", `${target}.cmd`) : "",
178
- process.platform === "win32" ? path.join(appData, "npm", `${target}.exe`) : "",
179
- process.platform === "win32" ? path.join(appData, "npm", target) : "",
239
+ function resolveAgentCli({ target, preferred = "", shimDir = "" }) {
240
+ const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
241
+ const candidates = [
242
+ preferred,
243
+ ...(target === "codex" ? listLocalCodexCliCandidates() : []),
244
+ process.platform === "win32" ? path.join(appData, "npm", `${target}.cmd`) : "",
245
+ process.platform === "win32" ? path.join(appData, "npm", `${target}.exe`) : "",
246
+ process.platform === "win32" ? path.join(appData, "npm", target) : "",
180
247
  ...listCliCandidatesFromPath(target)
181
248
  ];
182
249
  for (const candidate of candidates) {
@@ -288,14 +355,14 @@ function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.
288
355
 
289
356
  if (!fs.existsSync(venvPython)) {
290
357
  console.log(`Creating Python virtual environment: ${venvDir}`);
291
- try {
292
- execFileSync(pythonCmd, ["-m", "venv", venvDir], { stdio: "inherit" });
293
- } catch (e) {
294
- if (process.platform !== "win32") {
295
- throw new Error("Failed to create Python venv. Please install python3-venv first, for example: sudo apt install python3-venv");
296
- }
297
- throw new Error("Failed to create Python venv. Please confirm Python venv support is available.");
298
- }
358
+ try {
359
+ execFileSync(pythonCmd, ["-m", "venv", venvDir], { stdio: "inherit" });
360
+ } catch (e) {
361
+ if (process.platform !== "win32") {
362
+ return installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl });
363
+ }
364
+ throw new Error("Failed to create Python venv. Please confirm Python venv support is available.");
365
+ }
299
366
  }
300
367
 
301
368
  console.log("Installing/updating Python package in venv: langfuse");
@@ -303,11 +370,14 @@ function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.
303
370
  execFileSync(
304
371
  venvPython,
305
372
  ["-m", "pip", "install", "-U", "langfuse", "-i", pipIndexUrl],
306
- { stdio: "inherit" }
307
- );
308
- } catch (e) {
309
- throw new Error(`Failed to install langfuse in venv: ${venvPython} -m pip install -U langfuse -i ${pipIndexUrl}`);
310
- }
373
+ { stdio: "inherit" }
374
+ );
375
+ } catch (e) {
376
+ if (process.platform !== "win32") {
377
+ return installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl });
378
+ }
379
+ throw new Error(`Failed to install langfuse in venv: ${venvPython} -m pip install -U langfuse -i ${pipIndexUrl}`);
380
+ }
311
381
 
312
382
  return venvPython;
313
383
  }
@@ -343,13 +343,54 @@ function createAgentLauncher({ baseDir, target, executable }) {
343
343
  return launcher;
344
344
  }
345
345
 
346
- function pythonExecutableInVenv(venvDir) {
347
- return process.platform === "win32"
348
- ? path.join(venvDir, "Scripts", "python.exe")
349
- : path.join(venvDir, "bin", "python");
350
- }
351
-
352
- async function createHookLauncher({ hooksDir, hookPython, pyPath }) {
346
+ function pythonExecutableInVenv(venvDir) {
347
+ return process.platform === "win32"
348
+ ? path.join(venvDir, "Scripts", "python.exe")
349
+ : path.join(venvDir, "bin", "python");
350
+ }
351
+
352
+ function pythonCanImport(pythonCmd, moduleName) {
353
+ try {
354
+ execFileSync(pythonCmd, ["-c", `import ${moduleName}`], { stdio: "ignore" });
355
+ return true;
356
+ } catch {
357
+ return false;
358
+ }
359
+ }
360
+
361
+ function runPipInstallWithFallback({ pythonCmd, pipIndexUrl }) {
362
+ const attempts = [
363
+ { command: pythonCmd, args: ["-m", "pip", "install", "--user", "-U", "langfuse", "-i", pipIndexUrl] },
364
+ { command: "pip3", args: ["install", "--user", "-U", "langfuse", "-i", pipIndexUrl] },
365
+ { command: "pip", args: ["install", "--user", "-U", "langfuse", "-i", pipIndexUrl] }
366
+ ];
367
+
368
+ const errors = [];
369
+ for (const attempt of attempts) {
370
+ try {
371
+ console.log(`Trying Python package install: ${attempt.command} ${attempt.args.join(" ")}`);
372
+ execFileSync(attempt.command, attempt.args, { stdio: "inherit" });
373
+ return;
374
+ } catch (error) {
375
+ errors.push(`${attempt.command}: ${error?.message || error}`);
376
+ }
377
+ }
378
+
379
+ throw new Error(
380
+ `Failed to install langfuse with system Python/pip. Tried python -m pip, pip3, and pip. Last errors: ${errors.join(" | ")}`
381
+ );
382
+ }
383
+
384
+ function installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl }) {
385
+ console.log("Python venv is unavailable; falling back to system Python user install for langfuse.");
386
+ runPipInstallWithFallback({ pythonCmd, pipIndexUrl });
387
+ if (!pythonCanImport(pythonCmd, "langfuse")) {
388
+ throw new Error("langfuse was installed with pip, but python3 still cannot import it. Install python3-venv and rerun setup, for example: sudo apt install python3-venv");
389
+ }
390
+ return pythonCmd;
391
+ }
392
+
393
+ async function createHookLauncher({ hooksDir, hookPython, pyPath }) {
353
394
  if (process.platform === "win32") {
354
395
  const launcher = path.join(hooksDir, "run-langfuse-hook.cmd");
355
396
  const content = [
@@ -379,14 +420,14 @@ function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.
379
420
 
380
421
  if (!fs.existsSync(venvPython)) {
381
422
  console.log(`Creating Python virtual environment: ${venvDir}`);
382
- try {
383
- execFileSync(pythonCmd, ["-m", "venv", venvDir], { stdio: "inherit" });
384
- } catch (e) {
385
- if (process.platform !== "win32") {
386
- throw new Error("Failed to create Python venv. Please install python3-venv first, for example: sudo apt install python3-venv");
387
- }
388
- throw new Error("Failed to create Python venv. Please confirm Python venv support is available.");
389
- }
423
+ try {
424
+ execFileSync(pythonCmd, ["-m", "venv", venvDir], { stdio: "inherit" });
425
+ } catch (e) {
426
+ if (process.platform !== "win32") {
427
+ return installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl });
428
+ }
429
+ throw new Error("Failed to create Python venv. Please confirm Python venv support is available.");
430
+ }
390
431
  }
391
432
 
392
433
  console.log("Installing/updating Python package in venv: langfuse");
@@ -394,11 +435,14 @@ function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.
394
435
  execFileSync(
395
436
  venvPython,
396
437
  ["-m", "pip", "install", "-U", "langfuse", "-i", pipIndexUrl],
397
- { stdio: "inherit" }
398
- );
399
- } catch (e) {
400
- throw new Error(`Failed to install langfuse in venv: ${venvPython} -m pip install -U langfuse -i ${pipIndexUrl}`);
401
- }
438
+ { stdio: "inherit" }
439
+ );
440
+ } catch (e) {
441
+ if (process.platform !== "win32") {
442
+ return installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl });
443
+ }
444
+ throw new Error(`Failed to install langfuse in venv: ${venvPython} -m pip install -U langfuse -i ${pipIndexUrl}`);
445
+ }
402
446
 
403
447
  return venvPython;
404
448
  }
@@ -61,6 +61,7 @@ function main() {
61
61
  const userConfigPath = path.join(home, ".config", "opencode-plugin-langfuse", "config.json");
62
62
  const windowsLauncherPath = path.join(opencodeDir, "launch-opencode-langfuse.cmd");
63
63
  const unixLauncherPath = path.join(opencodeDir, "launch-opencode-langfuse.sh");
64
+ const opencodeCommandShimPath = path.join(opencodeDir, "bin", process.platform === "win32" ? "opencode.cmd" : "opencode");
64
65
 
65
66
  const results = [];
66
67
 
@@ -134,6 +135,14 @@ function main() {
134
135
  "Run setup again and enter userId when prompted."
135
136
  );
136
137
 
138
+ addResult(
139
+ results,
140
+ "opencode command shim",
141
+ fs.existsSync(opencodeCommandShimPath),
142
+ opencodeCommandShimPath,
143
+ "Run setup again after installing OpenCode so the direct opencode command can load Langfuse and auto-update checks."
144
+ );
145
+
137
146
  if (process.platform === "win32") {
138
147
  addResult(
139
148
  results,
@@ -512,12 +512,10 @@ function getPatchedLangfuseDistIndexJs() {
512
512
  " if (userId) spanProcessors.push(createUserIdSpanProcessor(userId));",
513
513
  "",
514
514
  " const sdk = new NodeSDK({ spanProcessors });",
515
- " // Defer SDK start to avoid blocking Opencode launch",
516
- " setImmediate(() => {",
517
- ' try { sdk.start(); }',
518
- ' catch (err) { log("warn", `OTEL SDK start failed: ${err?.message ?? err}`); }',
515
+ " const sdkStartPromise = Promise.resolve().then(() => sdk.start()).catch((err) => {",
516
+ ' log("warn", `OTEL SDK start failed: ${err?.message ?? err}`);',
519
517
  " });",
520
- " const metricsTracer = trace.getTracer('oh-langfuse-opencode-metrics');",
518
+ " const getMetricsTracer = () => trace.getTracer('oh-langfuse-opencode-metrics');",
521
519
  " const knownSkillNames = await collectKnownSkillNames();",
522
520
  " const startupSkillUsages = detectOpencodeSkillUsages(process.argv.join('\\n'), knownSkillNames);",
523
521
  " const messageTextById = new Map();",
@@ -534,21 +532,23 @@ function getPatchedLangfuseDistIndexJs() {
534
532
  ' if (userId) log("info", `LANGFUSE userId configured -> ${userId}`);',
535
533
  ' if (knownSkillNames.length) log("info", `OpenCode skills discovered -> ${knownSkillNames.length}`);',
536
534
  "",
537
- " let shutdownStarted = false;",
538
- " const flush = async (reason) => {",
539
- " try {",
540
- ' log("info", `Flushing OTEL spans on ${reason}`);',
541
- " await processor.forceFlush();",
535
+ " let shutdownStarted = false;",
536
+ " const flush = async (reason) => {",
537
+ " try {",
538
+ ' log("info", `Flushing OTEL spans on ${reason}`);',
539
+ " await sdkStartPromise;",
540
+ " await processor.forceFlush();",
542
541
  " } catch (error) {",
543
542
  ' log("warn", `OTEL forceFlush failed on ${reason}: ${error?.message ?? error}`);',
544
543
  " }",
545
544
  " };",
546
545
  " const shutdown = async (reason) => {",
547
- " if (shutdownStarted) return;",
548
- " shutdownStarted = true;",
549
- " try {",
550
- ' log("info", `Shutting down OTEL SDK on ${reason}`);',
551
- " await sdk.shutdown();",
546
+ " if (shutdownStarted) return;",
547
+ " shutdownStarted = true;",
548
+ " try {",
549
+ ' log("info", `Shutting down OTEL SDK on ${reason}`);',
550
+ " await sdkStartPromise;",
551
+ " await sdk.shutdown();",
552
552
  " } catch (error) {",
553
553
  ' log("warn", `OTEL shutdown failed on ${reason}: ${error?.message ?? error}`);',
554
554
  " }",
@@ -586,7 +586,7 @@ function getPatchedLangfuseDistIndexJs() {
586
586
  " }",
587
587
  " set.add(activity.toolCallId || `${kind}:${set.size + 1}`);",
588
588
  " };",
589
- " const recordInteractionMetric = (event) => {",
589
+ " const recordInteractionMetric = async (event) => {",
590
590
  " const payload = eventPayload(event);",
591
591
  " const part = eventPart(event);",
592
592
  " const partType = part?.type ?? '';",
@@ -617,7 +617,8 @@ function getPatchedLangfuseDistIndexJs() {
617
617
  " const tokenMetrics = tokenMetricsFromPart(part);",
618
618
  " const total = tokenMetrics.total ?? (tokenMetrics.input !== undefined && tokenMetrics.output !== undefined ? tokenMetrics.input + tokenMetrics.output : undefined);",
619
619
  " const tokenAvailable = [tokenMetrics.input, tokenMetrics.output, total, tokenMetrics.cacheRead, tokenMetrics.reasoning].some((value) => value !== undefined);",
620
- " const span = metricsTracer.startSpan('Agent Turn');",
620
+ " await sdkStartPromise;",
621
+ " const span = getMetricsTracer().startSpan('Agent Turn');",
621
622
  " const text = messageTextById.get(messageId) || '';",
622
623
  " const skillUsages = dedupeSkillUsages([...(skillUsagesByMessageId.get(messageId) ?? []), ...(skillUsagesBySessionId.get(sessionId) ?? [])]);",
623
624
  " const interactionId = `opencode:${userId || \"unknown\"}:${sessionId || \"unknown\"}:${messageId}`;",
@@ -673,9 +674,9 @@ function getPatchedLangfuseDistIndexJs() {
673
674
  ' log("warn", "OpenTelemetry experimental feature is disabled in Opencode config - tracing disabled");',
674
675
  " }",
675
676
  " },",
676
- " event: async ({ event }) => {",
677
- " const eventType = pickEventString(event?.name, event?.type, eventPayload(event)?.name, eventPayload(event)?.type);",
678
- " recordInteractionMetric(event);",
677
+ " event: async ({ event }) => {",
678
+ " const eventType = pickEventString(event?.name, event?.type, eventPayload(event)?.name, eventPayload(event)?.type);",
679
+ " await recordInteractionMetric(event);",
679
680
  ' if (eventType === "session.idle" || eventType === "message.updated" || eventType === "message.part.updated" || eventType === "session.updated" || eventType === "session.idle.1" || eventType === "message.updated.1" || eventType === "message.part.updated.1" || eventType === "session.updated.1") {',
680
681
  " await flush(eventType);",
681
682
  " }",
@@ -973,24 +974,28 @@ function npmInstallLooksLikeMissingVersion(result) {
973
974
  return text.includes("etarget") || text.includes("notarget") || text.includes("no matching version found");
974
975
  }
975
976
 
976
- function isOfficialNpmRegistry(registry) {
977
- return /^https?:\/\/registry\.npmjs\.org\/?$/i.test(String(registry || "").trim());
978
- }
979
-
980
- async function runNpmInstallOrThrow({ opencodeDir, pkgName = "opencode-plugin-langfuse", npmRegistry = "" }) {
981
- const npmArgs = ["install", pkgName, "--prefix", opencodeDir, "--package-lock=false", "--no-save", "--audit=false", "--fund=false"];
982
- if (npmRegistry) npmArgs.push("--registry", npmRegistry);
983
- const cliJs = getNpmCliJsPath();
984
- console.log(`使用 npm:${fs.existsSync(cliJs) ? `node ${cliJs}` : getNpmExecutable()}`);
985
- console.log("Installing OpenCode Langfuse plugin. This can take a few minutes on slow networks...");
986
- let r = await runNpmInstallCapture(npmArgs);
987
- if (!r.error && r.status !== 0 && npmInstallLooksLikeMissingVersion(r) && !isOfficialNpmRegistry(npmRegistry)) {
988
- console.error("");
989
- console.error("npm registry appears to be missing a package version. Retrying with https://registry.npmjs.org/ ...");
990
- const retryArgs = ["install", pkgName, "--prefix", opencodeDir, "--package-lock=false", "--no-save", "--audit=false", "--fund=false", "--registry", "https://registry.npmjs.org/"];
991
- r = await runNpmInstallCapture(retryArgs);
992
- if (!r.error && r.status === 0) return;
993
- }
977
+ function isOfficialNpmRegistry(registry) {
978
+ return /^https?:\/\/registry\.npmjs\.org\/?$/i.test(String(registry || "").trim());
979
+ }
980
+
981
+ const OFFICIAL_NPM_REGISTRY = "https://registry.npmjs.org/";
982
+
983
+ async function runNpmInstallOrThrow({ opencodeDir, pkgName = "opencode-plugin-langfuse", npmRegistry = "" }) {
984
+ const npmArgs = ["install", pkgName, "--prefix", opencodeDir, "--package-lock=false", "--no-save", "--audit=false", "--fund=false"];
985
+ const effectiveRegistry = npmRegistry || OFFICIAL_NPM_REGISTRY;
986
+ npmArgs.push("--registry", effectiveRegistry);
987
+ const cliJs = getNpmCliJsPath();
988
+ console.log(`使用 npm:${fs.existsSync(cliJs) ? `node ${cliJs}` : getNpmExecutable()}`);
989
+ console.log(`使用 npm registry:${effectiveRegistry}`);
990
+ console.log("Installing OpenCode Langfuse plugin. This can take a few minutes on slow networks...");
991
+ let r = await runNpmInstallCapture(npmArgs);
992
+ if (!r.error && r.status !== 0 && npmInstallLooksLikeMissingVersion(r) && !isOfficialNpmRegistry(effectiveRegistry)) {
993
+ console.error("");
994
+ console.error(`npm registry appears to be missing a package version. Retrying with ${OFFICIAL_NPM_REGISTRY} ...`);
995
+ const retryArgs = ["install", pkgName, "--prefix", opencodeDir, "--package-lock=false", "--no-save", "--audit=false", "--fund=false", "--registry", OFFICIAL_NPM_REGISTRY];
996
+ r = await runNpmInstallCapture(retryArgs);
997
+ if (!r.error && r.status === 0) return;
998
+ }
994
999
  if (!r.error && r.status === 0) return;
995
1000
  printNpmDiagnostics();
996
1001
  const npmLabel = fs.existsSync(cliJs) ? `node ${cliJs}` : getNpmExecutable();
@@ -1105,7 +1110,7 @@ async function main() {
1105
1110
  throw new Error("缺少参数:--userId=你的工号");
1106
1111
  }
1107
1112
  assertValidUserId(userId);
1108
- const npmRegistry = args.npmRegistry || process.env.OPENCODE_NPM_REGISTRY || process.env.NPM_CONFIG_REGISTRY || "";
1113
+ const npmRegistry = args.npmRegistry || process.env.OPENCODE_NPM_REGISTRY || "";
1109
1114
 
1110
1115
  const home = os.homedir();
1111
1116
  const opencodeDir = path.join(home, ".config", "opencode");
@@ -136,16 +136,36 @@ function printRunResult(label, result) {
136
136
  }
137
137
  }
138
138
 
139
- function findOnPath(names) {
140
- for (const name of names) {
141
- const probe = run(name, ["--version"], { timeoutMs: 15000 });
142
- if (probe.status === 0) return name;
143
- }
144
- return "";
145
- }
146
-
147
- function configFromArgs(args) {
148
- return {
139
+ function findOnPath(names) {
140
+ for (const name of names) {
141
+ const probe = run(name, ["--version"], { timeoutMs: 15000 });
142
+ if (probe.status === 0) return name;
143
+ }
144
+ return "";
145
+ }
146
+
147
+ function localCodexCliCandidates() {
148
+ if (process.platform !== "win32") return ["codex"];
149
+ const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
150
+ const codexBin = path.join(localAppData, "OpenAI", "Codex", "bin");
151
+ const candidates = [
152
+ process.env.CODEX_CLI_PATH || "",
153
+ path.join(codexBin, "codex.exe"),
154
+ ];
155
+ try {
156
+ if (fs.existsSync(codexBin)) {
157
+ for (const ent of fs.readdirSync(codexBin, { withFileTypes: true })) {
158
+ if (ent.isDirectory()) candidates.push(path.join(codexBin, ent.name, "codex.exe"));
159
+ }
160
+ }
161
+ } catch {
162
+ // Fall back to PATH probing below.
163
+ }
164
+ return [...candidates, "codex.cmd", "codex.exe", "codex"].filter(Boolean);
165
+ }
166
+
167
+ function configFromArgs(args) {
168
+ return {
149
169
  baseUrl:
150
170
  String(args.langfuseBaseUrl || args.langfuseHost || args.host || process.env.LANGFUSE_BASEURL || process.env.LANGFUSE_HOST || DEFAULT_LANGFUSE_BASE_URL),
151
171
  publicKey: String(args.publicKey || process.env.LANGFUSE_PUBLIC_KEY || DEFAULT_LANGFUSE_PUBLIC_KEY),
@@ -213,9 +233,9 @@ function triggerOpenCode(prompt, args, env) {
213
233
  return last;
214
234
  }
215
235
 
216
- function triggerCodex(prompt, args, env) {
217
- const cmd = String(args.codexCmd || findOnPath(process.platform === "win32" ? ["codex.cmd", "codex.exe", "codex"] : ["codex"]));
218
- if (!cmd) return { status: 127, stdout: "", stderr: "Codex CLI not found. Set --codexCmd=<path>.", command: "codex", args: [] };
236
+ function triggerCodex(prompt, args, env) {
237
+ const cmd = String(args.codexCmd || findOnPath(localCodexCliCandidates()));
238
+ if (!cmd) return { status: 127, stdout: "", stderr: "Codex CLI not found. Set --codexCmd=<path>.", command: "codex", args: [] };
219
239
  const candidates = [
220
240
  ["exec", prompt],
221
241
  ["exec", "--skip-git-repo-check", prompt],