oh-langfuse 0.1.21 → 0.1.22

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  `oh-langfuse` 是用于给 Claude Code、OpenCode 和 Codex 配置 Langfuse 追踪的命令行工具。它提供交互式安装向导,也支持 `setup` / `check` 直接命令,方便在用户机器上安装、修复和校验配置。
4
4
 
5
- 当前 npm 版本:`0.1.21`
5
+ 当前 npm 版本:`0.1.22`
6
6
 
7
7
  ## 能做什么
8
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-langfuse",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Use npm scripts to configure Claude Code / OpenCode / Codex with Langfuse tracing.",
@@ -20,16 +20,15 @@
20
20
  "scripts/opencode-langfuse-setup.mjs",
21
21
  "scripts/resolve-opencode-cli.mjs",
22
22
  "langfuse_hook.py",
23
- "codex_langfuse_notify.py",
24
- "README.md",
25
- "CODEX_LANGFUSE_PLAN.md",
23
+ "codex_langfuse_notify.py",
24
+ "README.md",
25
+ "CODEX_LANGFUSE_PLAN.md",
26
26
  "setup-langfuse.bat",
27
27
  "setup-langfuse.sh"
28
28
  ],
29
29
  "scripts": {
30
30
  "start": "node bin/cli.js",
31
31
  "check": "node --check bin/cli.js",
32
- "self:verify": "node scripts/real-self-verify.mjs",
33
32
  "pack:check": "npm pack --dry-run",
34
33
  "claude:setup": "node scripts/langfuse-setup.mjs",
35
34
  "claude:check": "node scripts/langfuse-check.mjs",
@@ -14,6 +14,18 @@ function addResult(results, item, ok, detail, fix = "") {
14
14
  results.push({ item, ok, detail, fix });
15
15
  }
16
16
 
17
+ function pythonExecutableInVenv(claudeDir) {
18
+ return process.platform === "win32"
19
+ ? path.join(claudeDir, "langfuse-venv", "Scripts", "python.exe")
20
+ : path.join(claudeDir, "langfuse-venv", "bin", "python");
21
+ }
22
+
23
+ function launcherPath(hooksDir) {
24
+ return process.platform === "win32"
25
+ ? path.join(hooksDir, "run-langfuse-hook.cmd")
26
+ : path.join(hooksDir, "run-langfuse-hook.sh");
27
+ }
28
+
17
29
  function findLangfuseHook(settings) {
18
30
  const stopHooks = settings?.hooks?.Stop;
19
31
  if (!Array.isArray(stopHooks)) return null;
@@ -25,16 +37,14 @@ function findLangfuseHook(settings) {
25
37
  const command = typeof h.command === "string" ? h.command : "";
26
38
  const args = Array.isArray(h.args) ? h.args : [];
27
39
  const joined = [command, ...args].join(" ");
28
- if (joined.includes("langfuse_hook.py")) return { command, args };
40
+ if (joined.includes("langfuse_hook.py") || joined.includes("run-langfuse-hook")) {
41
+ return { command, args };
42
+ }
29
43
  }
30
44
  }
31
45
  return null;
32
46
  }
33
47
 
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
48
  function pathExists(p) {
39
49
  return typeof p === "string" && p.length > 0 && fs.existsSync(p);
40
50
  }
@@ -56,15 +66,17 @@ function main() {
56
66
  const claudeDir = path.join(userHome, ".claude");
57
67
  const hooksDir = path.join(claudeDir, "hooks");
58
68
  const settingsPath = path.join(claudeDir, "settings.json");
69
+ const pyPath = path.join(hooksDir, "langfuse_hook.py");
70
+ const expectedLauncher = launcherPath(hooksDir);
71
+ const expectedPython = pythonExecutableInVenv(claudeDir);
59
72
  const logPath = path.join(claudeDir, "state", "langfuse_hook.log");
60
73
 
61
74
  const results = [];
62
75
 
63
76
  addResult(results, "Claude config dir", fs.existsSync(claudeDir), claudeDir, "Run: npx oh-langfuse@latest setup claude");
64
77
  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
78
  addResult(results, "hook script", fs.existsSync(pyPath), pyPath, "Run setup again to copy langfuse_hook.py.");
79
+ addResult(results, "hook launcher", fs.existsSync(expectedLauncher), expectedLauncher, "Run setup again to create the launcher script.");
68
80
 
69
81
  const settings = readJsonIfExists(settingsPath);
70
82
  addResult(results, "settings.json", !!settings, settingsPath, "Run setup again to write Claude settings.");
@@ -78,7 +90,6 @@ function main() {
78
90
  "CC_LANGFUSE_BASE_URL",
79
91
  "LANGFUSE_BASEURL"
80
92
  ];
81
-
82
93
  const missingEnv = neededEnv.filter((k) => !env || typeof env[k] === "undefined" || env[k] === "");
83
94
  addResult(
84
95
  results,
@@ -99,42 +110,31 @@ function main() {
99
110
  const hook = findLangfuseHook(settings);
100
111
  addResult(results, "hooks.Stop contains Langfuse", !!hook, hook ? "OK" : "missing", "Run setup again to register the Stop hook.");
101
112
 
102
- const execFormOk = hookUsesExecForm(hook);
113
+ const launcherFormOk = !!hook && path.normalize(hook.command) === path.normalize(expectedLauncher) && !hook.args?.length;
103
114
  addResult(
104
115
  results,
105
116
  "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."
117
+ launcherFormOk,
118
+ launcherFormOk ? "launcher command" : hook ? "legacy command form" : "missing",
119
+ "Run setup again. Claude Code should execute the generated run-langfuse-hook launcher directly."
111
120
  );
112
121
 
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
- }
122
+ addResult(
123
+ results,
124
+ "hook python executable",
125
+ pathExists(expectedPython),
126
+ expectedPython,
127
+ "Run setup again to recreate the Python venv."
128
+ );
129
+
130
+ const importCheck = checkPythonLangfuseImport(expectedPython);
131
+ addResult(
132
+ results,
133
+ "python package langfuse",
134
+ importCheck.ok,
135
+ importCheck.detail,
136
+ `Run: ${expectedPython} -m pip install -U langfuse`
137
+ );
138
138
 
139
139
  addResult(results, "hook log path", true, logPath);
140
140
 
@@ -127,15 +127,46 @@ function normalizeWinPathForClaude(p) {
127
127
  return p.replace(/\\/g, "/");
128
128
  }
129
129
 
130
- function quoteCommandArg(s) {
131
- return `"${String(s).replace(/"/g, '\\"')}"`;
132
- }
133
-
134
- function pythonExecutableInVenv(venvDir) {
135
- return process.platform === "win32"
136
- ? path.join(venvDir, "Scripts", "python.exe")
137
- : path.join(venvDir, "bin", "python");
138
- }
130
+ function quoteCommandArg(s) {
131
+ return `"${String(s).replace(/"/g, '\\"')}"`;
132
+ }
133
+
134
+ function cmdQuote(s) {
135
+ return `"${String(s).replace(/"/g, '""')}"`;
136
+ }
137
+
138
+ function shQuote(s) {
139
+ return `'${String(s).replace(/'/g, "'\\''")}'`;
140
+ }
141
+
142
+ function pythonExecutableInVenv(venvDir) {
143
+ return process.platform === "win32"
144
+ ? path.join(venvDir, "Scripts", "python.exe")
145
+ : path.join(venvDir, "bin", "python");
146
+ }
147
+
148
+ async function createHookLauncher({ hooksDir, hookPython, pyPath }) {
149
+ if (process.platform === "win32") {
150
+ const launcher = path.join(hooksDir, "run-langfuse-hook.cmd");
151
+ const content = [
152
+ "@echo off",
153
+ `${cmdQuote(hookPython)} ${cmdQuote(pyPath)}`,
154
+ ""
155
+ ].join(os.EOL);
156
+ await fsp.writeFile(launcher, content, "utf8");
157
+ return launcher;
158
+ }
159
+
160
+ const launcher = path.join(hooksDir, "run-langfuse-hook.sh");
161
+ const content = [
162
+ "#!/usr/bin/env sh",
163
+ `exec ${shQuote(hookPython)} ${shQuote(pyPath)}`,
164
+ ""
165
+ ].join("\n");
166
+ await fsp.writeFile(launcher, content, "utf8");
167
+ fs.chmodSync(launcher, 0o755);
168
+ return launcher;
169
+ }
139
170
 
140
171
  function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.tsinghua.edu.cn/simple" }) {
141
172
  const venvDir = path.join(baseDir, "langfuse-venv");
@@ -241,12 +272,13 @@ async function main() {
241
272
  const nextPyText = setOrReplaceUserId(pyText, userId);
242
273
  if (nextPyText !== pyText) {
243
274
  await fsp.writeFile(pyPath, nextPyText, "utf8");
244
- }
275
+ }
245
276
  const hookPython = createOrUpdateLangfuseVenv({ baseDir: claudeDir, pipIndexUrl });
246
-
247
- // 4) 合并写入 settings.json
248
- const settingsPath = path.join(claudeDir, "settings.json");
249
- const existing = readJsonIfExists(settingsPath) ?? {};
277
+ const hookLauncher = await createHookLauncher({ hooksDir, hookPython, pyPath });
278
+
279
+ // 4) 合并写入 settings.json
280
+ const settingsPath = path.join(claudeDir, "settings.json");
281
+ const existing = readJsonIfExists(settingsPath) ?? {};
250
282
 
251
283
  const desired = {
252
284
  env: {
@@ -267,8 +299,7 @@ async function main() {
267
299
  hooks: [
268
300
  {
269
301
  type: "command",
270
- command: hookPython,
271
- args: [pyPath]
302
+ command: hookLauncher
272
303
  }
273
304
  ]
274
305
  }