oh-langfuse 0.1.19 → 0.1.21
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 +70 -119
- package/langfuse_hook.py +72 -51
- package/package.json +13 -4
- package/scripts/langfuse-check.mjs +161 -90
- package/scripts/langfuse-setup.mjs +16 -15
package/README.md
CHANGED
|
@@ -1,175 +1,126 @@
|
|
|
1
1
|
# oh-langfuse
|
|
2
2
|
|
|
3
|
-
`oh-langfuse`
|
|
4
|
-
Claude Code、OpenCode 和 Codex。它提供终端安装向导、直接 setup/check
|
|
5
|
-
命令,以及不同工具所需的安装脚本和校验脚本。
|
|
3
|
+
`oh-langfuse` 是用于给 Claude Code、OpenCode 和 Codex 配置 Langfuse 追踪的命令行工具。它提供交互式安装向导,也支持 `setup` / `check` 直接命令,方便在用户机器上安装、修复和校验配置。
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
当前 npm 版本:`0.1.21`
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
## 能做什么
|
|
10
8
|
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
9
|
+
- 给 Claude Code 安装 `Stop` hook,把会话事件写入 Langfuse。
|
|
10
|
+
- 给 OpenCode 安装并 patch `opencode-plugin-langfuse`,开启 OpenTelemetry。
|
|
11
|
+
- 给 Codex 安装 `notify` hook,增量读取 session JSONL 并写入 Langfuse。
|
|
12
|
+
- 安装完成后自动执行对应 `check`,尽早发现配置缺失。
|
|
13
|
+
- 校验员工号格式,必须匹配 `^[a-z](?:\d{8}|wx\d{7})$`,例如 `h00613222` 或 `hwx1234567`。
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
`oh-aicoding-tool` 调用。
|
|
15
|
+
## 快速使用
|
|
18
16
|
|
|
19
|
-
|
|
17
|
+
建议始终带 `@latest`,避免 npx 使用本地旧缓存:
|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
```bash
|
|
20
|
+
npx oh-langfuse@latest
|
|
21
|
+
npx oh-langfuse@latest setup
|
|
22
|
+
npx oh-langfuse@latest check
|
|
23
|
+
```
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
| --- | --- |
|
|
25
|
-
| Claude Code | Node.js、npm、Python、pip |
|
|
26
|
-
| OpenCode | Node.js、npm、OpenCode CLI |
|
|
27
|
-
| Codex | Node.js、npm、Python、pip |
|
|
25
|
+
也可以指定目标:
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
```bash
|
|
28
|
+
npx oh-langfuse@latest setup claude
|
|
29
|
+
npx oh-langfuse@latest setup opencode
|
|
30
|
+
npx oh-langfuse@latest setup codex
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
npx oh-langfuse@latest check claude
|
|
33
|
+
npx oh-langfuse@latest check opencode
|
|
34
|
+
npx oh-langfuse@latest check codex
|
|
35
|
+
```
|
|
32
36
|
|
|
33
|
-
|
|
37
|
+
本地开发运行:
|
|
34
38
|
|
|
35
39
|
```bash
|
|
36
40
|
npm install
|
|
37
41
|
npm start
|
|
38
42
|
```
|
|
39
43
|
|
|
40
|
-
|
|
44
|
+
## 交互式菜单
|
|
41
45
|
|
|
42
|
-
|
|
43
|
-
npx oh-langfuse@latest
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
建议始终带 `@latest` 运行,避免 npx 使用本地缓存里的旧版本。
|
|
46
|
+
运行 `npx oh-langfuse@latest` 会打开交互式菜单:
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
- `Up` / `Down` 移动选项。
|
|
49
|
+
- 数字键快速定位。
|
|
50
|
+
- `Space` 勾选或取消。
|
|
51
|
+
- `Enter` 确认。
|
|
52
|
+
- `q` 或 `Esc` 退出。
|
|
49
53
|
|
|
50
|
-
|
|
51
|
-
- 数字键:快速选择;
|
|
52
|
-
- `Space`:多选切换;
|
|
53
|
-
- `Enter`:确认;
|
|
54
|
-
- `q` 或 `Esc`:退出。
|
|
54
|
+
多选菜单默认不勾选任何目标,避免误装。终端不支持原始按键输入时,会自动降级为数字输入模式。
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
## OpenCode 说明
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
OpenCode 安装会写入:
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
oh-langfuse setup codex
|
|
66
|
-
oh-langfuse check
|
|
67
|
-
oh-langfuse check environment
|
|
68
|
-
oh-langfuse check claude
|
|
69
|
-
oh-langfuse check opencode
|
|
70
|
-
oh-langfuse check codex
|
|
71
|
-
```
|
|
60
|
+
- `~/.config/opencode/opencode.json`
|
|
61
|
+
- `~/.config/opencode/plugins/opencode-plugin-langfuse`
|
|
62
|
+
- `~/.config/opencode-plugin-langfuse/config.json`
|
|
63
|
+
- Windows 下的 `~/.config/opencode/launch-opencode-langfuse.cmd`
|
|
64
|
+
- Linux/macOS 下的 `~/.config/opencode/launch-opencode-langfuse.sh`
|
|
72
65
|
|
|
73
|
-
|
|
66
|
+
Windows 下安装器会尝试写入用户级 `LANGFUSE_PUBLIC_KEY`、`LANGFUSE_SECRET_KEY`、`LANGFUSE_BASEURL`。这些变量对当前已经打开的终端不会立即生效,需要新开终端,或者使用生成的 launcher 启动 OpenCode。
|
|
74
67
|
|
|
75
|
-
|
|
76
|
-
oh-langfuse setup --dry-run
|
|
77
|
-
```
|
|
68
|
+
`check opencode` 已按实际情况处理 Windows:如果当前终端还看不到 `LANGFUSE_*`,但用户级环境变量已写入,或 launcher 已生成,会显示 OpenCode 仍有可用的 Langfuse 环境变量来源,不再把这种情况误判为安装失败。
|
|
78
69
|
|
|
79
|
-
|
|
70
|
+
WSL 用户请直接在 WSL shell 中安装和检查:
|
|
80
71
|
|
|
81
72
|
```bash
|
|
82
|
-
|
|
83
|
-
npm run opencode:setup
|
|
84
|
-
npm run codex:setup
|
|
85
|
-
npx oh-langfuse@latest check claude
|
|
73
|
+
npx oh-langfuse@latest setup opencode --userId=h00613222 --yes
|
|
86
74
|
npx oh-langfuse@latest check opencode
|
|
87
|
-
npx oh-langfuse@latest check codex
|
|
88
75
|
```
|
|
89
76
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
### Claude Code
|
|
77
|
+
不会做 Windows 到 WSL 的转发;在 WSL 中运行时,配置写入 WSL 用户自己的 `$HOME/.config/opencode`。
|
|
93
78
|
|
|
94
|
-
|
|
95
|
-
`langfuse`,并把 Langfuse 环境变量和 `Stop` hook 合并写入
|
|
96
|
-
`~/.claude/settings.json`。
|
|
79
|
+
## Claude Code 说明
|
|
97
80
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
安装并 patch `opencode-plugin-langfuse`,在
|
|
101
|
-
`~/.config/opencode/opencode.json` 中启用 OpenTelemetry,写入插件用户配置,
|
|
102
|
-
并可写入用户级 `LANGFUSE_*` 环境变量。Windows 下会生成
|
|
103
|
-
`launch-opencode-langfuse.cmd`,Linux/macOS 下会生成
|
|
104
|
-
`launch-opencode-langfuse.sh`,用于在 shell 环境变量未生效时显式带
|
|
105
|
-
Langfuse 配置启动 OpenCode。安装器可以读取带注释或尾随逗号的
|
|
106
|
-
`opencode.json`,合并配置后会写回标准 JSON。
|
|
107
|
-
|
|
108
|
-
如果你在 WSL 里运行 OpenCode,就在 WSL shell 里直接执行安装和检查命令。
|
|
109
|
-
此时配置会写入 WSL 用户自己的 `$HOME/.config/opencode`:
|
|
110
|
-
|
|
111
|
-
```bash
|
|
112
|
-
npx oh-langfuse@latest setup opencode --userId=h00613222 --yes
|
|
113
|
-
npx oh-langfuse@latest check opencode
|
|
114
|
-
```
|
|
81
|
+
Claude Code 安装会:
|
|
115
82
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
`~/.
|
|
83
|
+
- 安装 `langfuse_hook.py`
|
|
84
|
+
- 创建 `~/.claude/langfuse-venv`
|
|
85
|
+
- 安装 Python 包 `langfuse`
|
|
86
|
+
- 合并更新 `~/.claude/settings.json`
|
|
87
|
+
- 配置 `Stop` hook
|
|
120
88
|
|
|
121
|
-
|
|
122
|
-
再写回标准 JSON。
|
|
89
|
+
Linux 如果缺少 venv 支持,请根据提示安装 `python3-venv` 或对应 Python 版本的 venv 包。
|
|
123
90
|
|
|
124
|
-
|
|
91
|
+
## Codex 说明
|
|
125
92
|
|
|
126
|
-
|
|
127
|
-
包 `langfuse`,把 Langfuse 凭据写入 `~/.codex/langfuse/config.json`,并更新
|
|
128
|
-
`~/.codex/config.toml` 顶层 `notify` 命令。安装后需要重启 Codex。
|
|
93
|
+
Codex 安装会:
|
|
129
94
|
|
|
130
|
-
|
|
131
|
-
`~/.codex/
|
|
132
|
-
|
|
95
|
+
- 安装 `codex_langfuse_notify.py`
|
|
96
|
+
- 创建 `~/.codex/langfuse-venv`
|
|
97
|
+
- 写入 `~/.codex/langfuse/config.json`
|
|
98
|
+
- 更新 `~/.codex/config.toml` 顶层 `notify`
|
|
133
99
|
|
|
134
|
-
|
|
100
|
+
配置后需要重启 Codex。notify hook 会增量读取 `~/.codex/sessions/**/*.jsonl`,把新的 user、assistant、tool、token 事件转换为 Langfuse observation,并把读取偏移记录到 `~/.codex/langfuse/state.json`。
|
|
135
101
|
|
|
136
|
-
|
|
102
|
+
## 环境变量
|
|
137
103
|
|
|
138
104
|
| 变量 | 作用 |
|
|
139
105
|
| --- | --- |
|
|
140
106
|
| `LANGFUSE_BASEURL` / `LANGFUSE_HOST` | Langfuse 服务地址 |
|
|
141
107
|
| `LANGFUSE_PUBLIC_KEY` | Langfuse public key |
|
|
142
108
|
| `LANGFUSE_SECRET_KEY` | Langfuse secret key |
|
|
143
|
-
| `LANGFUSE_USER_ID` / `CC_USER_ID` |
|
|
109
|
+
| `LANGFUSE_USER_ID` / `CC_USER_ID` | 用户标识,必须匹配员工号规则 |
|
|
144
110
|
| `CODEX_HOME` | 自定义 Codex home 目录 |
|
|
145
|
-
| `OPENCODE_SKIP_PLUGIN_INSTALL` | OpenCode
|
|
146
|
-
| `OPENCODE_NPM_REGISTRY` | OpenCode 插件 npm
|
|
147
|
-
|
|
148
|
-
交互界面不会明文打印 secret key。
|
|
111
|
+
| `OPENCODE_SKIP_PLUGIN_INSTALL` | 跳过 OpenCode npm 插件安装 |
|
|
112
|
+
| `OPENCODE_NPM_REGISTRY` | OpenCode 插件 npm 安装源 |
|
|
149
113
|
|
|
150
|
-
|
|
114
|
+
Secret key 不会在交互式界面中明文展示。
|
|
151
115
|
|
|
152
|
-
|
|
153
|
-
| --- | --- |
|
|
154
|
-
| `bin/cli.js` | 交互式和直接命令入口 |
|
|
155
|
-
| `scripts/langfuse-setup.mjs` | Claude Code Langfuse 安装 |
|
|
156
|
-
| `scripts/opencode-langfuse-setup.mjs` | OpenCode Langfuse 安装 |
|
|
157
|
-
| `scripts/codex-langfuse-setup.mjs` | Codex Langfuse 安装 |
|
|
158
|
-
| `scripts/*-check.mjs` | 配置校验脚本 |
|
|
159
|
-
| `langfuse_hook.py` | Claude Code hook exporter |
|
|
160
|
-
| `codex_langfuse_notify.py` | Codex notify exporter |
|
|
161
|
-
|
|
162
|
-
## 发布与维护
|
|
163
|
-
|
|
164
|
-
- npm 包名:`oh-langfuse`
|
|
165
|
-
- 主命令:`oh-langfuse`
|
|
166
|
-
- 兼容命令:`code-tool-langfuse`
|
|
167
|
-
- 发布前建议运行:
|
|
116
|
+
## 常用维护命令
|
|
168
117
|
|
|
169
118
|
```bash
|
|
170
119
|
npm run check
|
|
171
120
|
npm pack --dry-run
|
|
172
121
|
```
|
|
173
122
|
|
|
174
|
-
|
|
175
|
-
|
|
123
|
+
发布包暴露两个命令:
|
|
124
|
+
|
|
125
|
+
- `oh-langfuse`
|
|
126
|
+
- `code-tool-langfuse`,兼容旧入口
|
package/langfuse_hook.py
CHANGED
|
@@ -15,10 +15,18 @@ from pathlib import Path
|
|
|
15
15
|
from typing import Any, Dict, List, Optional, Tuple
|
|
16
16
|
|
|
17
17
|
# --- Langfuse import (fail-open) ---
|
|
18
|
-
try:
|
|
19
|
-
from langfuse import Langfuse, propagate_attributes
|
|
20
|
-
except Exception:
|
|
21
|
-
|
|
18
|
+
try:
|
|
19
|
+
from langfuse import Langfuse, propagate_attributes
|
|
20
|
+
except Exception as e:
|
|
21
|
+
try:
|
|
22
|
+
state_dir = Path.home() / ".claude" / "state"
|
|
23
|
+
state_dir.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
with open(state_dir / "langfuse_hook.log", "a", encoding="utf-8") as f:
|
|
25
|
+
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
26
|
+
f.write(f"{ts} [ERROR] Failed to import langfuse package: {e}\n")
|
|
27
|
+
except Exception:
|
|
28
|
+
pass
|
|
29
|
+
sys.exit(0)
|
|
22
30
|
|
|
23
31
|
# --- Paths ---
|
|
24
32
|
STATE_DIR = Path.home() / ".claude" / "state"
|
|
@@ -325,22 +333,32 @@ def read_new_jsonl(transcript_path: Path, ss: SessionState) -> Tuple[List[Dict[s
|
|
|
325
333
|
text = chunk.decode(errors="replace")
|
|
326
334
|
|
|
327
335
|
combined = ss.buffer + text
|
|
328
|
-
lines = combined.split("\n")
|
|
329
|
-
|
|
330
|
-
ss.
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
line = line.strip()
|
|
336
|
+
lines = combined.split("\n")
|
|
337
|
+
tail = lines[-1]
|
|
338
|
+
ss.offset = new_offset
|
|
339
|
+
|
|
340
|
+
msgs: List[Dict[str, Any]] = []
|
|
341
|
+
for line in lines[:-1]:
|
|
342
|
+
line = line.strip()
|
|
336
343
|
if not line:
|
|
337
344
|
continue
|
|
338
345
|
try:
|
|
339
346
|
msgs.append(json.loads(line))
|
|
340
|
-
except Exception:
|
|
341
|
-
continue
|
|
342
|
-
|
|
343
|
-
|
|
347
|
+
except Exception:
|
|
348
|
+
continue
|
|
349
|
+
|
|
350
|
+
tail = tail.strip()
|
|
351
|
+
if tail:
|
|
352
|
+
try:
|
|
353
|
+
msgs.append(json.loads(tail))
|
|
354
|
+
ss.buffer = ""
|
|
355
|
+
except Exception:
|
|
356
|
+
# Keep a genuinely partial final line for the next hook run.
|
|
357
|
+
ss.buffer = tail
|
|
358
|
+
else:
|
|
359
|
+
ss.buffer = ""
|
|
360
|
+
|
|
361
|
+
return msgs, ss
|
|
344
362
|
|
|
345
363
|
# ----------------- Turn assembly -----------------
|
|
346
364
|
@dataclass
|
|
@@ -520,32 +538,34 @@ def main() -> int:
|
|
|
520
538
|
start = time.time()
|
|
521
539
|
debug("Hook started")
|
|
522
540
|
|
|
523
|
-
if os.environ.get("TRACE_TO_LANGFUSE", "").lower() != "true":
|
|
524
|
-
return 0
|
|
525
|
-
|
|
526
|
-
public_key = os.environ.get("CC_LANGFUSE_PUBLIC_KEY") or os.environ.get("LANGFUSE_PUBLIC_KEY")
|
|
527
|
-
secret_key = os.environ.get("CC_LANGFUSE_SECRET_KEY") or os.environ.get("LANGFUSE_SECRET_KEY")
|
|
528
|
-
host = os.environ.get("CC_LANGFUSE_BASE_URL") or os.environ.get("LANGFUSE_BASEURL") or "https://cloud.langfuse.com"
|
|
529
|
-
|
|
530
|
-
if not public_key or not secret_key:
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
541
|
+
if os.environ.get("TRACE_TO_LANGFUSE", "").lower() != "true":
|
|
542
|
+
return 0
|
|
543
|
+
|
|
544
|
+
public_key = os.environ.get("CC_LANGFUSE_PUBLIC_KEY") or os.environ.get("LANGFUSE_PUBLIC_KEY")
|
|
545
|
+
secret_key = os.environ.get("CC_LANGFUSE_SECRET_KEY") or os.environ.get("LANGFUSE_SECRET_KEY")
|
|
546
|
+
host = os.environ.get("CC_LANGFUSE_BASE_URL") or os.environ.get("LANGFUSE_BASEURL") or "https://cloud.langfuse.com"
|
|
547
|
+
|
|
548
|
+
if not public_key or not secret_key:
|
|
549
|
+
warn("Missing Langfuse public/secret key in hook environment; exiting.")
|
|
550
|
+
return 0
|
|
551
|
+
|
|
552
|
+
payload = read_hook_payload()
|
|
553
|
+
session_id, transcript_path, user_id = extract_session_transcript_and_user(payload)
|
|
554
|
+
|
|
555
|
+
if not session_id or not transcript_path:
|
|
556
|
+
# No structured payload; fail open (do not guess)
|
|
557
|
+
warn("Missing session_id or transcript_path from hook payload; exiting.")
|
|
558
|
+
return 0
|
|
559
|
+
|
|
560
|
+
if not transcript_path.exists():
|
|
561
|
+
warn(f"Transcript path does not exist: {transcript_path}")
|
|
562
|
+
return 0
|
|
563
|
+
|
|
564
|
+
try:
|
|
565
|
+
langfuse = Langfuse(public_key=public_key, secret_key=secret_key, host=host)
|
|
566
|
+
except Exception as e:
|
|
567
|
+
warn(f"Langfuse init failed: {e}")
|
|
568
|
+
return 0
|
|
549
569
|
|
|
550
570
|
try:
|
|
551
571
|
with FileLock(LOCK_FILE):
|
|
@@ -570,20 +590,21 @@ def main() -> int:
|
|
|
570
590
|
for t in turns:
|
|
571
591
|
emitted += 1
|
|
572
592
|
turn_num = ss.turn_count + emitted
|
|
573
|
-
try:
|
|
574
|
-
emit_turn(langfuse, session_id, user_id, turn_num, t, transcript_path)
|
|
575
|
-
except Exception as e:
|
|
576
|
-
|
|
577
|
-
# continue emitting other turns
|
|
593
|
+
try:
|
|
594
|
+
emit_turn(langfuse, session_id, user_id, turn_num, t, transcript_path)
|
|
595
|
+
except Exception as e:
|
|
596
|
+
warn(f"emit_turn failed: {e}")
|
|
597
|
+
# continue emitting other turns
|
|
578
598
|
|
|
579
599
|
ss.turn_count += emitted
|
|
580
600
|
write_session_state(state, key, ss)
|
|
581
601
|
save_state(state)
|
|
582
602
|
|
|
583
|
-
try:
|
|
584
|
-
langfuse.flush()
|
|
585
|
-
except Exception:
|
|
586
|
-
|
|
603
|
+
try:
|
|
604
|
+
langfuse.flush()
|
|
605
|
+
except Exception as e:
|
|
606
|
+
warn(f"Langfuse flush failed: {e}")
|
|
607
|
+
pass
|
|
587
608
|
|
|
588
609
|
dur = time.time() - start
|
|
589
610
|
info(f"Processed {emitted} turns in {dur:.2f}s (session={session_id})")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oh-langfuse",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Use npm scripts to configure Claude Code / OpenCode / Codex with Langfuse tracing.",
|
|
@@ -10,7 +10,15 @@
|
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"bin",
|
|
13
|
-
"scripts",
|
|
13
|
+
"scripts/codex-langfuse-check.mjs",
|
|
14
|
+
"scripts/codex-langfuse-setup.mjs",
|
|
15
|
+
"scripts/json-utils.mjs",
|
|
16
|
+
"scripts/langfuse-check.mjs",
|
|
17
|
+
"scripts/langfuse-setup.mjs",
|
|
18
|
+
"scripts/opencode-langfuse-check.mjs",
|
|
19
|
+
"scripts/opencode-langfuse-run.mjs",
|
|
20
|
+
"scripts/opencode-langfuse-setup.mjs",
|
|
21
|
+
"scripts/resolve-opencode-cli.mjs",
|
|
14
22
|
"langfuse_hook.py",
|
|
15
23
|
"codex_langfuse_notify.py",
|
|
16
24
|
"README.md",
|
|
@@ -20,8 +28,9 @@
|
|
|
20
28
|
],
|
|
21
29
|
"scripts": {
|
|
22
30
|
"start": "node bin/cli.js",
|
|
23
|
-
"check": "node --check bin/cli.js",
|
|
24
|
-
"
|
|
31
|
+
"check": "node --check bin/cli.js",
|
|
32
|
+
"self:verify": "node scripts/real-self-verify.mjs",
|
|
33
|
+
"pack:check": "npm pack --dry-run",
|
|
25
34
|
"claude:setup": "node scripts/langfuse-setup.mjs",
|
|
26
35
|
"claude:check": "node scripts/langfuse-check.mjs",
|
|
27
36
|
"langfuse:setup": "node scripts/langfuse-setup.mjs",
|
|
@@ -1,90 +1,161 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
function readJsonIfExists(p) {
|
|
7
|
+
if (!fs.existsSync(p)) return null;
|
|
8
|
+
const txt = fs.readFileSync(p, "utf8");
|
|
9
|
+
if (!txt.trim()) return null;
|
|
10
|
+
return JSON.parse(txt);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function addResult(results, item, ok, detail, fix = "") {
|
|
14
|
+
results.push({ item, ok, detail, fix });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function findLangfuseHook(settings) {
|
|
18
|
+
const stopHooks = settings?.hooks?.Stop;
|
|
19
|
+
if (!Array.isArray(stopHooks)) return null;
|
|
20
|
+
for (const entry of stopHooks) {
|
|
21
|
+
const hooks = entry?.hooks;
|
|
22
|
+
if (!Array.isArray(hooks)) continue;
|
|
23
|
+
for (const h of hooks) {
|
|
24
|
+
if (h?.type !== "command") continue;
|
|
25
|
+
const command = typeof h.command === "string" ? h.command : "";
|
|
26
|
+
const args = Array.isArray(h.args) ? h.args : [];
|
|
27
|
+
const joined = [command, ...args].join(" ");
|
|
28
|
+
if (joined.includes("langfuse_hook.py")) return { command, args };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function hookUsesExecForm(hook) {
|
|
35
|
+
return !!(hook?.command && Array.isArray(hook.args) && hook.args.some((arg) => String(arg).includes("langfuse_hook.py")));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function pathExists(p) {
|
|
39
|
+
return typeof p === "string" && p.length > 0 && fs.existsSync(p);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function checkPythonLangfuseImport(pythonPath) {
|
|
43
|
+
if (!pathExists(pythonPath)) return { ok: false, detail: "python executable missing" };
|
|
44
|
+
const result = spawnSync(pythonPath, ["-c", "import langfuse; print('OK')"], {
|
|
45
|
+
encoding: "utf8",
|
|
46
|
+
timeout: 15000,
|
|
47
|
+
windowsHide: true
|
|
48
|
+
});
|
|
49
|
+
if (result.status === 0) return { ok: true, detail: "OK" };
|
|
50
|
+
const err = `${result.stderr || result.stdout || result.error?.message || "unknown error"}`.trim();
|
|
51
|
+
return { ok: false, detail: err || "import failed" };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function main() {
|
|
55
|
+
const userHome = os.homedir();
|
|
56
|
+
const claudeDir = path.join(userHome, ".claude");
|
|
57
|
+
const hooksDir = path.join(claudeDir, "hooks");
|
|
58
|
+
const settingsPath = path.join(claudeDir, "settings.json");
|
|
59
|
+
const logPath = path.join(claudeDir, "state", "langfuse_hook.log");
|
|
60
|
+
|
|
61
|
+
const results = [];
|
|
62
|
+
|
|
63
|
+
addResult(results, "Claude config dir", fs.existsSync(claudeDir), claudeDir, "Run: npx oh-langfuse@latest setup claude");
|
|
64
|
+
addResult(results, "hooks dir", fs.existsSync(hooksDir), hooksDir, "Run setup again to install the hook script.");
|
|
65
|
+
|
|
66
|
+
const pyPath = path.join(hooksDir, "langfuse_hook.py");
|
|
67
|
+
addResult(results, "hook script", fs.existsSync(pyPath), pyPath, "Run setup again to copy langfuse_hook.py.");
|
|
68
|
+
|
|
69
|
+
const settings = readJsonIfExists(settingsPath);
|
|
70
|
+
addResult(results, "settings.json", !!settings, settingsPath, "Run setup again to write Claude settings.");
|
|
71
|
+
|
|
72
|
+
const env = settings?.env;
|
|
73
|
+
const neededEnv = [
|
|
74
|
+
"TRACE_TO_LANGFUSE",
|
|
75
|
+
"LANGFUSE_PUBLIC_KEY",
|
|
76
|
+
"LANGFUSE_SECRET_KEY",
|
|
77
|
+
"LANGFUSE_HOST",
|
|
78
|
+
"CC_LANGFUSE_BASE_URL",
|
|
79
|
+
"LANGFUSE_BASEURL"
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
const missingEnv = neededEnv.filter((k) => !env || typeof env[k] === "undefined" || env[k] === "");
|
|
83
|
+
addResult(
|
|
84
|
+
results,
|
|
85
|
+
"Langfuse env fields",
|
|
86
|
+
missingEnv.length === 0,
|
|
87
|
+
missingEnv.length ? `missing: ${missingEnv.join(", ")}` : "OK",
|
|
88
|
+
"Run setup again so settings.json contains the Langfuse environment fields."
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const userEnvOk = !!(
|
|
92
|
+
env?.CC_LANGFUSE_USER_ID ||
|
|
93
|
+
env?.CLAUDE_USER_ID ||
|
|
94
|
+
env?.CC_USER_ID ||
|
|
95
|
+
env?.LANGFUSE_USER_ID
|
|
96
|
+
);
|
|
97
|
+
addResult(results, "user id env field", userEnvOk, userEnvOk ? "OK" : "missing", "Run setup again and enter a valid employee id.");
|
|
98
|
+
|
|
99
|
+
const hook = findLangfuseHook(settings);
|
|
100
|
+
addResult(results, "hooks.Stop contains Langfuse", !!hook, hook ? "OK" : "missing", "Run setup again to register the Stop hook.");
|
|
101
|
+
|
|
102
|
+
const execFormOk = hookUsesExecForm(hook);
|
|
103
|
+
addResult(
|
|
104
|
+
results,
|
|
105
|
+
"hook command form",
|
|
106
|
+
process.platform === "win32" ? execFormOk : !!hook,
|
|
107
|
+
execFormOk ? "exec form command + args" : hook ? "legacy shell-form command" : "missing",
|
|
108
|
+
process.platform === "win32"
|
|
109
|
+
? "Run setup again. Windows should use command plus args so paths are not parsed by Git Bash/PowerShell."
|
|
110
|
+
: "Run setup again to refresh the hook command."
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
if (hook) {
|
|
114
|
+
addResult(
|
|
115
|
+
results,
|
|
116
|
+
"hook python executable",
|
|
117
|
+
pathExists(hook.command),
|
|
118
|
+
hook.command || "missing",
|
|
119
|
+
"Run setup again to recreate the Python venv and write an absolute python path."
|
|
120
|
+
);
|
|
121
|
+
const hookScriptPath = execFormOk ? String(hook.args.find((arg) => String(arg).includes("langfuse_hook.py")) || "") : pyPath;
|
|
122
|
+
addResult(
|
|
123
|
+
results,
|
|
124
|
+
"hook args script",
|
|
125
|
+
pathExists(hookScriptPath),
|
|
126
|
+
hookScriptPath || "missing",
|
|
127
|
+
"Run setup again so the hook args point at langfuse_hook.py."
|
|
128
|
+
);
|
|
129
|
+
const importCheck = checkPythonLangfuseImport(hook.command);
|
|
130
|
+
addResult(
|
|
131
|
+
results,
|
|
132
|
+
"python package langfuse",
|
|
133
|
+
importCheck.ok,
|
|
134
|
+
importCheck.detail,
|
|
135
|
+
`Run: ${hook.command} -m pip install -U langfuse`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
addResult(results, "hook log path", true, logPath);
|
|
140
|
+
|
|
141
|
+
const pad = (s, n) => (s.length >= n ? s : s + " ".repeat(n - s.length));
|
|
142
|
+
const w = Math.max(...results.map((r) => r.item.length)) + 2;
|
|
143
|
+
const failed = [];
|
|
144
|
+
for (const r of results) {
|
|
145
|
+
const status = r.ok ? "OK " : "BAD";
|
|
146
|
+
console.log(`${status} ${pad(r.item, w)} ${r.detail}`);
|
|
147
|
+
if (!r.ok) failed.push(r);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (failed.length) {
|
|
151
|
+
console.log("");
|
|
152
|
+
console.log("Fix suggestions:");
|
|
153
|
+
for (const r of failed) {
|
|
154
|
+
if (r.fix) console.log(`- ${r.item}: ${r.fix}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
process.exit(failed.length ? 2 : 0);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
main();
|
|
@@ -177,11 +177,13 @@ async function main() {
|
|
|
177
177
|
}
|
|
178
178
|
assertValidUserId(userId);
|
|
179
179
|
|
|
180
|
-
const langfuseHost =
|
|
181
|
-
args.
|
|
182
|
-
args.
|
|
183
|
-
|
|
184
|
-
|
|
180
|
+
const langfuseHost =
|
|
181
|
+
args.langfuseBaseUrl ||
|
|
182
|
+
args.langfuseHost ||
|
|
183
|
+
args.host ||
|
|
184
|
+
process.env.LANGFUSE_BASEURL ||
|
|
185
|
+
process.env.LANGFUSE_HOST ||
|
|
186
|
+
"http://120.46.221.227:3000";
|
|
185
187
|
|
|
186
188
|
const publicKey =
|
|
187
189
|
args.publicKey || process.env.LANGFUSE_PUBLIC_KEY || "pk-lf-da0c90a7-6e93-4eb7-bb86-c1047c8d187d";
|
|
@@ -246,10 +248,8 @@ async function main() {
|
|
|
246
248
|
const settingsPath = path.join(claudeDir, "settings.json");
|
|
247
249
|
const existing = readJsonIfExists(settingsPath) ?? {};
|
|
248
250
|
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
const desired = {
|
|
252
|
-
env: {
|
|
251
|
+
const desired = {
|
|
252
|
+
env: {
|
|
253
253
|
TRACE_TO_LANGFUSE: "true",
|
|
254
254
|
LANGFUSE_PUBLIC_KEY: publicKey,
|
|
255
255
|
LANGFUSE_SECRET_KEY: secretKey,
|
|
@@ -265,12 +265,13 @@ async function main() {
|
|
|
265
265
|
Stop: [
|
|
266
266
|
{
|
|
267
267
|
hooks: [
|
|
268
|
-
{
|
|
269
|
-
type: "command",
|
|
270
|
-
command:
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
268
|
+
{
|
|
269
|
+
type: "command",
|
|
270
|
+
command: hookPython,
|
|
271
|
+
args: [pyPath]
|
|
272
|
+
}
|
|
273
|
+
]
|
|
274
|
+
}
|
|
274
275
|
]
|
|
275
276
|
}
|
|
276
277
|
};
|