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 +1 -1
- package/package.json +4 -5
- package/scripts/langfuse-check.mjs +39 -39
- package/scripts/langfuse-setup.mjs +47 -16
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oh-langfuse",
|
|
3
|
-
"version": "0.1.
|
|
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"))
|
|
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
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
|
135
|
-
return
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
const
|
|
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:
|
|
271
|
-
args: [pyPath]
|
|
302
|
+
command: hookLauncher
|
|
272
303
|
}
|
|
273
304
|
]
|
|
274
305
|
}
|