oh-langfuse 0.1.53 → 0.1.55

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.
@@ -1,180 +1,180 @@
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 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
-
29
- function directPythonHookCommand(pythonPath, pyPath) {
30
- if (process.platform !== "win32") return `${pythonPath} ${pyPath}`;
31
- return `"${String(pythonPath).replace(/"/g, '""')}" "${String(pyPath).replace(/"/g, '""')}"`;
32
- }
33
-
34
- function expectedHookCommands({ launcher, pythonPath, pyPath }) {
35
- if (process.platform !== "win32") return [path.normalize(launcher)];
36
- return [
37
- path.normalize(launcher),
38
- `cmd.exe /d /s /c "${launcher.replace(/"/g, '""')}"`,
39
- directPythonHookCommand(pythonPath, pyPath)
40
- ];
41
- }
42
-
43
- function findLangfuseHook(settings) {
44
- const stopHooks = settings?.hooks?.Stop;
45
- if (!Array.isArray(stopHooks)) return null;
46
- for (const entry of stopHooks) {
47
- const hooks = entry?.hooks;
48
- if (!Array.isArray(hooks)) continue;
49
- for (const h of hooks) {
50
- if (h?.type !== "command") continue;
51
- const command = typeof h.command === "string" ? h.command : "";
52
- const args = Array.isArray(h.args) ? h.args : [];
53
- const joined = [command, ...args].join(" ");
54
- if (joined.includes("langfuse_hook.py") || joined.includes("run-langfuse-hook")) {
55
- return { command, args };
56
- }
57
- }
58
- }
59
- return null;
60
- }
61
-
62
- function pathExists(p) {
63
- return typeof p === "string" && p.length > 0 && fs.existsSync(p);
64
- }
65
-
66
- function checkPythonLangfuseImport(pythonPath) {
67
- if (!pathExists(pythonPath)) return { ok: false, detail: "python executable missing" };
68
- const result = spawnSync(pythonPath, ["-c", "import langfuse; print('OK')"], {
69
- encoding: "utf8",
70
- timeout: 15000,
71
- windowsHide: true
72
- });
73
- if (result.status === 0) return { ok: true, detail: "OK" };
74
- const err = `${result.stderr || result.stdout || result.error?.message || "unknown error"}`.trim();
75
- return { ok: false, detail: err || "import failed" };
76
- }
77
-
78
- function main() {
79
- const userHome = os.homedir();
80
- const claudeDir = path.join(userHome, ".claude");
81
- const hooksDir = path.join(claudeDir, "hooks");
82
- const settingsPath = path.join(claudeDir, "settings.json");
83
- const pyPath = path.join(hooksDir, "langfuse_hook.py");
84
- const expectedLauncher = launcherPath(hooksDir);
85
- const expectedPython = pythonExecutableInVenv(claudeDir);
86
- const logPath = path.join(claudeDir, "state", "langfuse_hook.log");
87
-
88
- const results = [];
89
-
90
- addResult(results, "Claude config dir", fs.existsSync(claudeDir), claudeDir, "Run: npx oh-langfuse@latest setup claude");
91
- addResult(results, "hooks dir", fs.existsSync(hooksDir), hooksDir, "Run setup again to install the hook script.");
92
- addResult(results, "hook script", fs.existsSync(pyPath), pyPath, "Run setup again to copy langfuse_hook.py.");
93
- addResult(results, "hook launcher", fs.existsSync(expectedLauncher), expectedLauncher, "Run setup again to create the launcher script.");
94
-
95
- const settings = readJsonIfExists(settingsPath);
96
- addResult(results, "settings.json", !!settings, settingsPath, "Run setup again to write Claude settings.");
97
-
98
- const env = settings?.env;
99
- const neededEnv = [
100
- "TRACE_TO_LANGFUSE",
101
- "LANGFUSE_PUBLIC_KEY",
102
- "LANGFUSE_SECRET_KEY",
103
- "LANGFUSE_HOST",
104
- "CC_LANGFUSE_BASE_URL",
105
- "LANGFUSE_BASEURL"
106
- ];
107
- const missingEnv = neededEnv.filter((k) => !env || typeof env[k] === "undefined" || env[k] === "");
108
- addResult(
109
- results,
110
- "Langfuse env fields",
111
- missingEnv.length === 0,
112
- missingEnv.length ? `missing: ${missingEnv.join(", ")}` : "OK",
113
- "Run setup again so settings.json contains the Langfuse environment fields."
114
- );
115
-
116
- const userEnvOk = !!(
117
- env?.CC_LANGFUSE_USER_ID ||
118
- env?.CLAUDE_USER_ID ||
119
- env?.CC_USER_ID ||
120
- env?.LANGFUSE_USER_ID
121
- );
122
- addResult(results, "user id env field", userEnvOk, userEnvOk ? "OK" : "missing", "Run setup again and enter a valid employee id.");
123
-
124
- const hook = findLangfuseHook(settings);
125
- addResult(results, "hooks.Stop contains Langfuse", !!hook, hook ? "OK" : "missing", "Run setup again to register the Stop hook.");
126
-
127
- const expectedCommands = expectedHookCommands({ launcher: expectedLauncher, pythonPath: expectedPython, pyPath });
128
- const hookCommand = hook?.command || "";
129
- const launcherFormOk =
130
- !!hook &&
131
- expectedCommands.some((cmd) => path.normalize(hookCommand) === path.normalize(cmd)) &&
132
- !hook.args?.length;
133
- addResult(
134
- results,
135
- "hook command form",
136
- launcherFormOk,
137
- launcherFormOk ? "supported command" : hook ? "legacy command form" : "missing",
138
- "Run setup again. Claude Code should execute the generated Python hook command."
139
- );
140
-
141
- addResult(
142
- results,
143
- "hook python executable",
144
- pathExists(expectedPython),
145
- expectedPython,
146
- "Run setup again to recreate the Python venv."
147
- );
148
-
149
- const importCheck = checkPythonLangfuseImport(expectedPython);
150
- addResult(
151
- results,
152
- "python package langfuse",
153
- importCheck.ok,
154
- importCheck.detail,
155
- `Run: ${expectedPython} -m pip install -U langfuse`
156
- );
157
-
158
- addResult(results, "hook log path", true, logPath);
159
-
160
- const pad = (s, n) => (s.length >= n ? s : s + " ".repeat(n - s.length));
161
- const w = Math.max(...results.map((r) => r.item.length)) + 2;
162
- const failed = [];
163
- for (const r of results) {
164
- const status = r.ok ? "OK " : "BAD";
165
- console.log(`${status} ${pad(r.item, w)} ${r.detail}`);
166
- if (!r.ok) failed.push(r);
167
- }
168
-
169
- if (failed.length) {
170
- console.log("");
171
- console.log("Fix suggestions:");
172
- for (const r of failed) {
173
- if (r.fix) console.log(`- ${r.item}: ${r.fix}`);
174
- }
175
- }
176
-
177
- process.exit(failed.length ? 2 : 0);
178
- }
179
-
180
- main();
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 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
+
29
+ function directPythonHookCommand(pythonPath, pyPath) {
30
+ if (process.platform !== "win32") return `${pythonPath} ${pyPath}`;
31
+ return `"${String(pythonPath).replace(/"/g, '""')}" "${String(pyPath).replace(/"/g, '""')}"`;
32
+ }
33
+
34
+ function expectedHookCommands({ launcher, pythonPath, pyPath }) {
35
+ if (process.platform !== "win32") return [path.normalize(launcher)];
36
+ return [
37
+ path.normalize(launcher),
38
+ `cmd.exe /d /s /c "${launcher.replace(/"/g, '""')}"`,
39
+ directPythonHookCommand(pythonPath, pyPath)
40
+ ];
41
+ }
42
+
43
+ function findLangfuseHook(settings) {
44
+ const stopHooks = settings?.hooks?.Stop;
45
+ if (!Array.isArray(stopHooks)) return null;
46
+ for (const entry of stopHooks) {
47
+ const hooks = entry?.hooks;
48
+ if (!Array.isArray(hooks)) continue;
49
+ for (const h of hooks) {
50
+ if (h?.type !== "command") continue;
51
+ const command = typeof h.command === "string" ? h.command : "";
52
+ const args = Array.isArray(h.args) ? h.args : [];
53
+ const joined = [command, ...args].join(" ");
54
+ if (joined.includes("langfuse_hook.py") || joined.includes("run-langfuse-hook")) {
55
+ return { command, args };
56
+ }
57
+ }
58
+ }
59
+ return null;
60
+ }
61
+
62
+ function pathExists(p) {
63
+ return typeof p === "string" && p.length > 0 && fs.existsSync(p);
64
+ }
65
+
66
+ function checkPythonLangfuseImport(pythonPath) {
67
+ if (!pathExists(pythonPath)) return { ok: false, detail: "python executable missing" };
68
+ const result = spawnSync(pythonPath, ["-c", "import langfuse; print('OK')"], {
69
+ encoding: "utf8",
70
+ timeout: 15000,
71
+ windowsHide: true
72
+ });
73
+ if (result.status === 0) return { ok: true, detail: "OK" };
74
+ const err = `${result.stderr || result.stdout || result.error?.message || "unknown error"}`.trim();
75
+ return { ok: false, detail: err || "import failed" };
76
+ }
77
+
78
+ function main() {
79
+ const userHome = os.homedir();
80
+ const claudeDir = path.join(userHome, ".claude");
81
+ const hooksDir = path.join(claudeDir, "hooks");
82
+ const settingsPath = path.join(claudeDir, "settings.json");
83
+ const pyPath = path.join(hooksDir, "langfuse_hook.py");
84
+ const expectedLauncher = launcherPath(hooksDir);
85
+ const expectedPython = pythonExecutableInVenv(claudeDir);
86
+ const logPath = path.join(claudeDir, "state", "langfuse_hook.log");
87
+
88
+ const results = [];
89
+
90
+ addResult(results, "Claude config dir", fs.existsSync(claudeDir), claudeDir, "Run: npx oh-langfuse@latest setup claude");
91
+ addResult(results, "hooks dir", fs.existsSync(hooksDir), hooksDir, "Run setup again to install the hook script.");
92
+ addResult(results, "hook script", fs.existsSync(pyPath), pyPath, "Run setup again to copy langfuse_hook.py.");
93
+ addResult(results, "hook launcher", fs.existsSync(expectedLauncher), expectedLauncher, "Run setup again to create the launcher script.");
94
+
95
+ const settings = readJsonIfExists(settingsPath);
96
+ addResult(results, "settings.json", !!settings, settingsPath, "Run setup again to write Claude settings.");
97
+
98
+ const env = settings?.env;
99
+ const neededEnv = [
100
+ "TRACE_TO_LANGFUSE",
101
+ "LANGFUSE_PUBLIC_KEY",
102
+ "LANGFUSE_SECRET_KEY",
103
+ "LANGFUSE_HOST",
104
+ "CC_LANGFUSE_BASE_URL",
105
+ "LANGFUSE_BASEURL"
106
+ ];
107
+ const missingEnv = neededEnv.filter((k) => !env || typeof env[k] === "undefined" || env[k] === "");
108
+ addResult(
109
+ results,
110
+ "Langfuse env fields",
111
+ missingEnv.length === 0,
112
+ missingEnv.length ? `missing: ${missingEnv.join(", ")}` : "OK",
113
+ "Run setup again so settings.json contains the Langfuse environment fields."
114
+ );
115
+
116
+ const userEnvOk = !!(
117
+ env?.CC_LANGFUSE_USER_ID ||
118
+ env?.CLAUDE_USER_ID ||
119
+ env?.CC_USER_ID ||
120
+ env?.LANGFUSE_USER_ID
121
+ );
122
+ addResult(results, "user id env field", userEnvOk, userEnvOk ? "OK" : "missing", "Run setup again and enter a valid employee id.");
123
+
124
+ const hook = findLangfuseHook(settings);
125
+ addResult(results, "hooks.Stop contains Langfuse", !!hook, hook ? "OK" : "missing", "Run setup again to register the Stop hook.");
126
+
127
+ const expectedCommands = expectedHookCommands({ launcher: expectedLauncher, pythonPath: expectedPython, pyPath });
128
+ const hookCommand = hook?.command || "";
129
+ const launcherFormOk =
130
+ !!hook &&
131
+ expectedCommands.some((cmd) => path.normalize(hookCommand) === path.normalize(cmd)) &&
132
+ !hook.args?.length;
133
+ addResult(
134
+ results,
135
+ "hook command form",
136
+ launcherFormOk,
137
+ launcherFormOk ? "supported command" : hook ? "legacy command form" : "missing",
138
+ "Run setup again. Claude Code should execute the generated Python hook command."
139
+ );
140
+
141
+ addResult(
142
+ results,
143
+ "hook python executable",
144
+ pathExists(expectedPython),
145
+ expectedPython,
146
+ "Run setup again to recreate the Python venv."
147
+ );
148
+
149
+ const importCheck = checkPythonLangfuseImport(expectedPython);
150
+ addResult(
151
+ results,
152
+ "python package langfuse",
153
+ importCheck.ok,
154
+ importCheck.detail,
155
+ `Run: ${expectedPython} -m pip install -U langfuse`
156
+ );
157
+
158
+ addResult(results, "hook log path", true, logPath);
159
+
160
+ const pad = (s, n) => (s.length >= n ? s : s + " ".repeat(n - s.length));
161
+ const w = Math.max(...results.map((r) => r.item.length)) + 2;
162
+ const failed = [];
163
+ for (const r of results) {
164
+ const status = r.ok ? "OK " : "BAD";
165
+ console.log(`${status} ${pad(r.item, w)} ${r.detail}`);
166
+ if (!r.ok) failed.push(r);
167
+ }
168
+
169
+ if (failed.length) {
170
+ console.log("");
171
+ console.log("Fix suggestions:");
172
+ for (const r of failed) {
173
+ if (r.fix) console.log(`- ${r.item}: ${r.fix}`);
174
+ }
175
+ }
176
+
177
+ process.exit(failed.length ? 2 : 0);
178
+ }
179
+
180
+ main();