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 +213 -142
- package/package.json +1 -1
- package/scripts/codex-langfuse-check.mjs +41 -15
- package/scripts/codex-langfuse-setup.mjs +110 -40
- package/scripts/langfuse-setup.mjs +64 -20
- package/scripts/opencode-langfuse-check.mjs +9 -0
- package/scripts/opencode-langfuse-setup.mjs +44 -39
- package/scripts/real-self-verify.mjs +33 -13
package/README.md
CHANGED
|
@@ -1,142 +1,213 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
`oh-langfuse`
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
npx oh-langfuse@latest
|
|
41
|
-
npx oh-langfuse@latest
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
OpenCode
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
npx oh-langfuse@latest
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
@@ -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
|
|
47
|
-
return
|
|
48
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
178
|
-
process.platform === "win32" ? path.join(appData, "npm", `${target}.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
516
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
const
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
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 ||
|
|
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
|
|
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(
|
|
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],
|