oh-langfuse 0.1.55 → 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 +46 -20
- 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",
|
|
@@ -181,19 +181,44 @@ function isPathInsideDir(candidate, dir) {
|
|
|
181
181
|
return relative === "" || (relative && !relative.startsWith("..") && !path.isAbsolute(relative));
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
function existingCliCandidate(candidate, shimDir) {
|
|
185
|
-
if (!candidate || isPathInsideDir(candidate, shimDir)) return "";
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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 });
|
|
197
222
|
if (result.status !== 0) return [];
|
|
198
223
|
const candidates = String(result.stdout || "")
|
|
199
224
|
.split(/\r?\n/)
|
|
@@ -211,13 +236,14 @@ function sortWindowsCliCandidates(candidates) {
|
|
|
211
236
|
return [...candidates].sort((a, b) => extPriority(a) - extPriority(b));
|
|
212
237
|
}
|
|
213
238
|
|
|
214
|
-
function resolveAgentCli({ target, preferred = "", shimDir = "" }) {
|
|
215
|
-
const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
|
|
216
|
-
const candidates = [
|
|
217
|
-
preferred,
|
|
218
|
-
|
|
219
|
-
process.platform === "win32" ? path.join(appData, "npm", `${target}.
|
|
220
|
-
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) : "",
|
|
221
247
|
...listCliCandidatesFromPath(target)
|
|
222
248
|
];
|
|
223
249
|
for (const candidate of candidates) {
|
|
@@ -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],
|