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.
- package/README.md +142 -142
- package/bin/cli.js +425 -425
- package/codex_langfuse_notify.py +517 -517
- package/langfuse_hook.py +581 -581
- package/package.json +1 -1
- package/scripts/auto-update-runtime.mjs +190 -190
- package/scripts/codex-langfuse-check.mjs +81 -81
- package/scripts/codex-langfuse-setup.mjs +358 -314
- package/scripts/langfuse-check.mjs +180 -180
- package/scripts/langfuse-setup.mjs +370 -326
- package/scripts/log-filter-utils.mjs +26 -26
- package/scripts/metrics-utils.mjs +377 -377
- package/scripts/opencode-langfuse-check.mjs +9 -0
- package/scripts/opencode-langfuse-setup.mjs +944 -935
- package/scripts/real-self-verify.mjs +621 -621
- package/scripts/runtime-state-utils.mjs +53 -53
- package/scripts/update-langfuse-runtime.mjs +260 -260
- package/scripts/update-utils.mjs +73 -73
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import fsp from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
import { execFileSync, spawnSync } from "node:child_process";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { writeRuntimeInstallRecord } from "./runtime-state-utils.mjs";
|
|
8
|
-
|
|
9
|
-
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
|
-
const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
|
|
11
|
-
|
|
12
|
-
const USER_ID_PATTERN = /^[a-z](?:\d{8}|wx\d{7})$/;
|
|
13
|
-
const USER_ID_PATTERN_TEXT = "^[a-z](?:\\d{8}|wx\\d{7})$";
|
|
14
|
-
|
|
15
|
-
function normalizeUserId(v) {
|
|
16
|
-
return String(v || "").trim();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function assertValidUserId(userId) {
|
|
20
|
-
if (!USER_ID_PATTERN.test(normalizeUserId(userId))) {
|
|
21
|
-
throw new Error(`工号格式不正确:--userId 必须匹配 ${USER_ID_PATTERN_TEXT},例如 h00613222 或 hwx1234567`);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { writeRuntimeInstallRecord } from "./runtime-state-utils.mjs";
|
|
8
|
+
|
|
9
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
|
|
11
|
+
|
|
12
|
+
const USER_ID_PATTERN = /^[a-z](?:\d{8}|wx\d{7})$/;
|
|
13
|
+
const USER_ID_PATTERN_TEXT = "^[a-z](?:\\d{8}|wx\\d{7})$";
|
|
14
|
+
|
|
15
|
+
function normalizeUserId(v) {
|
|
16
|
+
return String(v || "").trim();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function assertValidUserId(userId) {
|
|
20
|
+
if (!USER_ID_PATTERN.test(normalizeUserId(userId))) {
|
|
21
|
+
throw new Error(`工号格式不正确:--userId 必须匹配 ${USER_ID_PATTERN_TEXT},例如 h00613222 或 hwx1234567`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
24
|
|
|
25
25
|
function parseArgs(argv) {
|
|
26
26
|
const args = {};
|
|
@@ -132,271 +132,315 @@ function normalizeWinPathForClaude(p) {
|
|
|
132
132
|
return p.replace(/\\/g, "/");
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
function quoteCommandArg(s) {
|
|
136
|
-
return `"${String(s).replace(/"/g, '\\"')}"`;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function cmdQuote(s) {
|
|
140
|
-
return `"${String(s).replace(/"/g, '""')}"`;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function hookCommandForClaude({ launcher, hookPython, pyPath }) {
|
|
144
|
-
if (process.platform !== "win32") return launcher;
|
|
145
|
-
return `${cmdQuote(hookPython)} ${cmdQuote(pyPath)}`;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function shQuote(s) {
|
|
149
|
-
return `'${String(s).replace(/'/g, "'\\''")}'`;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function autoUpdateRuntimePath() {
|
|
153
|
-
return path.join(packageRoot, "scripts", "auto-update-runtime.mjs");
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function autoUpdateHelperDir() {
|
|
157
|
-
return path.join(os.homedir(), ".config", "oh-langfuse", "bin");
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function writeAutoUpdateHelper(target) {
|
|
161
|
-
const helperDir = autoUpdateHelperDir();
|
|
162
|
-
ensureDir(helperDir);
|
|
163
|
-
const runtimePath = autoUpdateRuntimePath();
|
|
164
|
-
const fallbackArgs = ["-y", "oh-langfuse@latest", "auto-update", target, "--skip-check", "--startup-status"];
|
|
165
|
-
|
|
166
|
-
if (process.platform === "win32") {
|
|
167
|
-
const helper = path.join(helperDir, `oh-langfuse-auto-update-${target}.cmd`);
|
|
168
|
-
const lines = [
|
|
169
|
-
"@echo off",
|
|
170
|
-
"REM Auto-generated by scripts/langfuse-setup.mjs",
|
|
171
|
-
`if exist ${cmdQuote(runtimePath)} (`,
|
|
172
|
-
` call ${cmdQuote(process.execPath)} ${cmdQuote(runtimePath)} ${target} --skip-check --startup-status %*`,
|
|
173
|
-
") else (",
|
|
174
|
-
` call npx.cmd ${fallbackArgs.map(cmdQuote).join(" ")} %*`,
|
|
175
|
-
")",
|
|
176
|
-
"exit /b 0",
|
|
177
|
-
""
|
|
178
|
-
];
|
|
179
|
-
fs.writeFileSync(helper, lines.join(os.EOL), "utf8");
|
|
180
|
-
return helper;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const helper = path.join(helperDir, `oh-langfuse-auto-update-${target}`);
|
|
184
|
-
const lines = [
|
|
185
|
-
"#!/usr/bin/env sh",
|
|
186
|
-
"# Auto-generated by scripts/langfuse-setup.mjs",
|
|
187
|
-
"set +e",
|
|
188
|
-
`if [ -f ${shQuote(runtimePath)} ]; then`,
|
|
189
|
-
` ${shQuote(process.execPath)} ${shQuote(runtimePath)} ${shQuote(target)} --skip-check --startup-status "$@"`,
|
|
190
|
-
"else",
|
|
191
|
-
` npx ${fallbackArgs.map(shQuote).join(" ")} "$@"`,
|
|
192
|
-
"fi",
|
|
193
|
-
"exit 0",
|
|
194
|
-
""
|
|
195
|
-
];
|
|
196
|
-
fs.writeFileSync(helper, lines.join("\n"), "utf8");
|
|
197
|
-
fs.chmodSync(helper, 0o755);
|
|
198
|
-
return helper;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function windowsAutoUpdateCommand(target) {
|
|
202
|
-
return `call ${cmdQuote(writeAutoUpdateHelper(target))}`;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function unixAutoUpdateCommand(target) {
|
|
206
|
-
return `${shQuote(writeAutoUpdateHelper(target))} || true`;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function isPathInsideDir(candidate, dir) {
|
|
210
|
-
if (!candidate || !dir) return false;
|
|
211
|
-
const relative = path.relative(path.resolve(dir), path.resolve(candidate));
|
|
212
|
-
return relative === "" || (relative && !relative.startsWith("..") && !path.isAbsolute(relative));
|
|
135
|
+
function quoteCommandArg(s) {
|
|
136
|
+
return `"${String(s).replace(/"/g, '\\"')}"`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function cmdQuote(s) {
|
|
140
|
+
return `"${String(s).replace(/"/g, '""')}"`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function hookCommandForClaude({ launcher, hookPython, pyPath }) {
|
|
144
|
+
if (process.platform !== "win32") return launcher;
|
|
145
|
+
return `${cmdQuote(hookPython)} ${cmdQuote(pyPath)}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function shQuote(s) {
|
|
149
|
+
return `'${String(s).replace(/'/g, "'\\''")}'`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function autoUpdateRuntimePath() {
|
|
153
|
+
return path.join(packageRoot, "scripts", "auto-update-runtime.mjs");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function autoUpdateHelperDir() {
|
|
157
|
+
return path.join(os.homedir(), ".config", "oh-langfuse", "bin");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function writeAutoUpdateHelper(target) {
|
|
161
|
+
const helperDir = autoUpdateHelperDir();
|
|
162
|
+
ensureDir(helperDir);
|
|
163
|
+
const runtimePath = autoUpdateRuntimePath();
|
|
164
|
+
const fallbackArgs = ["-y", "oh-langfuse@latest", "auto-update", target, "--skip-check", "--startup-status"];
|
|
165
|
+
|
|
166
|
+
if (process.platform === "win32") {
|
|
167
|
+
const helper = path.join(helperDir, `oh-langfuse-auto-update-${target}.cmd`);
|
|
168
|
+
const lines = [
|
|
169
|
+
"@echo off",
|
|
170
|
+
"REM Auto-generated by scripts/langfuse-setup.mjs",
|
|
171
|
+
`if exist ${cmdQuote(runtimePath)} (`,
|
|
172
|
+
` call ${cmdQuote(process.execPath)} ${cmdQuote(runtimePath)} ${target} --skip-check --startup-status %*`,
|
|
173
|
+
") else (",
|
|
174
|
+
` call npx.cmd ${fallbackArgs.map(cmdQuote).join(" ")} %*`,
|
|
175
|
+
")",
|
|
176
|
+
"exit /b 0",
|
|
177
|
+
""
|
|
178
|
+
];
|
|
179
|
+
fs.writeFileSync(helper, lines.join(os.EOL), "utf8");
|
|
180
|
+
return helper;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const helper = path.join(helperDir, `oh-langfuse-auto-update-${target}`);
|
|
184
|
+
const lines = [
|
|
185
|
+
"#!/usr/bin/env sh",
|
|
186
|
+
"# Auto-generated by scripts/langfuse-setup.mjs",
|
|
187
|
+
"set +e",
|
|
188
|
+
`if [ -f ${shQuote(runtimePath)} ]; then`,
|
|
189
|
+
` ${shQuote(process.execPath)} ${shQuote(runtimePath)} ${shQuote(target)} --skip-check --startup-status "$@"`,
|
|
190
|
+
"else",
|
|
191
|
+
` npx ${fallbackArgs.map(shQuote).join(" ")} "$@"`,
|
|
192
|
+
"fi",
|
|
193
|
+
"exit 0",
|
|
194
|
+
""
|
|
195
|
+
];
|
|
196
|
+
fs.writeFileSync(helper, lines.join("\n"), "utf8");
|
|
197
|
+
fs.chmodSync(helper, 0o755);
|
|
198
|
+
return helper;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function windowsAutoUpdateCommand(target) {
|
|
202
|
+
return `call ${cmdQuote(writeAutoUpdateHelper(target))}`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function unixAutoUpdateCommand(target) {
|
|
206
|
+
return `${shQuote(writeAutoUpdateHelper(target))} || true`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function isPathInsideDir(candidate, dir) {
|
|
210
|
+
if (!candidate || !dir) return false;
|
|
211
|
+
const relative = path.relative(path.resolve(dir), path.resolve(candidate));
|
|
212
|
+
return relative === "" || (relative && !relative.startsWith("..") && !path.isAbsolute(relative));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function existingCliCandidate(candidate, shimDir) {
|
|
216
|
+
if (!candidate || isPathInsideDir(candidate, shimDir)) return "";
|
|
217
|
+
try {
|
|
218
|
+
return fs.existsSync(candidate) ? candidate : "";
|
|
219
|
+
} catch {
|
|
220
|
+
return "";
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function listCliCandidatesFromPath(target) {
|
|
225
|
+
const cmd = process.platform === "win32" ? "where.exe" : "which";
|
|
226
|
+
const args = process.platform === "win32" ? [target] : ["-a", target];
|
|
227
|
+
const result = spawnSync(cmd, args, { encoding: "utf8", windowsHide: true });
|
|
228
|
+
if (result.status !== 0) return [];
|
|
229
|
+
return String(result.stdout || "")
|
|
230
|
+
.split(/\r?\n/)
|
|
231
|
+
.map((line) => line.trim())
|
|
232
|
+
.filter(Boolean);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function resolveAgentCli({ target, preferred = "", shimDir = "" }) {
|
|
236
|
+
const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
|
|
237
|
+
const candidates = [
|
|
238
|
+
preferred,
|
|
239
|
+
process.platform === "win32" ? path.join(appData, "npm", `${target}.cmd`) : "",
|
|
240
|
+
process.platform === "win32" ? path.join(appData, "npm", `${target}.exe`) : "",
|
|
241
|
+
process.platform === "win32" ? path.join(appData, "npm", target) : "",
|
|
242
|
+
...listCliCandidatesFromPath(target)
|
|
243
|
+
];
|
|
244
|
+
for (const candidate of candidates) {
|
|
245
|
+
const found = existingCliCandidate(candidate, shimDir);
|
|
246
|
+
if (found) return found;
|
|
247
|
+
}
|
|
248
|
+
return "";
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function prependWindowsUserPath(dir) {
|
|
252
|
+
if (process.platform !== "win32") return false;
|
|
253
|
+
const cmd = [
|
|
254
|
+
"$ErrorActionPreference = 'Stop';",
|
|
255
|
+
`$dir = ${psQuote(dir)};`,
|
|
256
|
+
"$current = [Environment]::GetEnvironmentVariable('Path', 'User');",
|
|
257
|
+
"$parts = @();",
|
|
258
|
+
"if ($current) { $parts = $current -split ';' | Where-Object { $_ -and $_.Trim() } }",
|
|
259
|
+
"$exists = $false;",
|
|
260
|
+
"foreach ($part in $parts) { if ([string]::Equals($part.TrimEnd('\\'), $dir.TrimEnd('\\'), [StringComparison]::OrdinalIgnoreCase)) { $exists = $true } }",
|
|
261
|
+
"if (-not $exists) {",
|
|
262
|
+
" $next = (@($dir) + $parts) -join ';';",
|
|
263
|
+
" [Environment]::SetEnvironmentVariable('Path', $next, 'User');",
|
|
264
|
+
"}"
|
|
265
|
+
].join(" ");
|
|
266
|
+
const result = spawnSync("powershell", ["-NoProfile", "-Command", cmd], { stdio: "inherit", windowsHide: true });
|
|
267
|
+
if (result.status !== 0) throw new Error("Failed to prepend Claude Langfuse shim to the user PATH.");
|
|
268
|
+
|
|
269
|
+
const currentParts = String(process.env.PATH || "").split(path.delimiter);
|
|
270
|
+
const normalized = dir.replace(/[\\/]+$/, "").toLowerCase();
|
|
271
|
+
if (!currentParts.some((part) => part && part.replace(/[\\/]+$/, "").toLowerCase() === normalized)) {
|
|
272
|
+
process.env.PATH = `${dir}${path.delimiter}${process.env.PATH || ""}`;
|
|
273
|
+
}
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function writeAgentCommandShim({ baseDir, target, executable, realCli, publicKey, secretKey, baseUrl, userId }) {
|
|
278
|
+
if (!realCli) return null;
|
|
279
|
+
const shimDir = path.join(baseDir, "bin");
|
|
280
|
+
ensureDir(shimDir);
|
|
281
|
+
if (process.platform === "win32") {
|
|
282
|
+
const shim = path.join(shimDir, `${executable}.cmd`);
|
|
283
|
+
const envPrefix = `OH_LANGFUSE_${target.toUpperCase()}_SHIM`;
|
|
284
|
+
const content = [
|
|
285
|
+
"@echo off",
|
|
286
|
+
"REM Auto-generated by scripts/langfuse-setup.mjs",
|
|
287
|
+
`set ${envPrefix}=1`,
|
|
288
|
+
`set LANGFUSE_PUBLIC_KEY=${publicKey}`,
|
|
289
|
+
`set LANGFUSE_SECRET_KEY=${secretKey}`,
|
|
290
|
+
`set LANGFUSE_BASEURL=${baseUrl}`,
|
|
291
|
+
userId ? `set LANGFUSE_USER_ID=${userId}` : null,
|
|
292
|
+
windowsAutoUpdateCommand(target),
|
|
293
|
+
`call ${cmdQuote(realCli)} %*`,
|
|
294
|
+
"exit /b %ERRORLEVEL%",
|
|
295
|
+
""
|
|
296
|
+
].filter(Boolean).join(os.EOL);
|
|
297
|
+
fs.writeFileSync(shim, content, "utf8");
|
|
298
|
+
return { shim, shimDir };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const shim = path.join(shimDir, executable);
|
|
302
|
+
const lines = [
|
|
303
|
+
"#!/usr/bin/env sh",
|
|
304
|
+
"# Auto-generated by scripts/langfuse-setup.mjs",
|
|
305
|
+
"set -eu",
|
|
306
|
+
`export OH_LANGFUSE_${target.toUpperCase()}_SHIM=1`,
|
|
307
|
+
`export LANGFUSE_PUBLIC_KEY=${shQuote(publicKey)}`,
|
|
308
|
+
`export LANGFUSE_SECRET_KEY=${shQuote(secretKey)}`,
|
|
309
|
+
`export LANGFUSE_BASEURL=${shQuote(baseUrl)}`,
|
|
310
|
+
userId ? `export LANGFUSE_USER_ID=${shQuote(userId)}` : null,
|
|
311
|
+
unixAutoUpdateCommand(target),
|
|
312
|
+
`exec ${shQuote(realCli)} "$@"`,
|
|
313
|
+
""
|
|
314
|
+
].filter(Boolean);
|
|
315
|
+
fs.writeFileSync(shim, lines.join("\n"), "utf8");
|
|
316
|
+
fs.chmodSync(shim, 0o755);
|
|
317
|
+
return { shim, shimDir };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function createAgentLauncher({ baseDir, target, executable }) {
|
|
321
|
+
if (process.platform === "win32") {
|
|
322
|
+
const launcher = path.join(baseDir, `launch-${target}-langfuse.cmd`);
|
|
323
|
+
const content = [
|
|
324
|
+
"@echo off",
|
|
325
|
+
windowsAutoUpdateCommand(target),
|
|
326
|
+
`${executable} %*`,
|
|
327
|
+
""
|
|
328
|
+
].join(os.EOL);
|
|
329
|
+
fs.writeFileSync(launcher, content, "utf8");
|
|
330
|
+
return launcher;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const launcher = path.join(baseDir, `launch-${target}-langfuse.sh`);
|
|
334
|
+
const content = [
|
|
335
|
+
"#!/usr/bin/env sh",
|
|
336
|
+
"set -eu",
|
|
337
|
+
unixAutoUpdateCommand(target),
|
|
338
|
+
`exec ${shQuote(executable)} "$@"`,
|
|
339
|
+
""
|
|
340
|
+
].join("\n");
|
|
341
|
+
fs.writeFileSync(launcher, content, "utf8");
|
|
342
|
+
fs.chmodSync(launcher, 0o755);
|
|
343
|
+
return launcher;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function pythonExecutableInVenv(venvDir) {
|
|
347
|
+
return process.platform === "win32"
|
|
348
|
+
? path.join(venvDir, "Scripts", "python.exe")
|
|
349
|
+
: path.join(venvDir, "bin", "python");
|
|
213
350
|
}
|
|
214
351
|
|
|
215
|
-
function
|
|
216
|
-
if (!candidate || isPathInsideDir(candidate, shimDir)) return "";
|
|
352
|
+
function pythonCanImport(pythonCmd, moduleName) {
|
|
217
353
|
try {
|
|
218
|
-
|
|
354
|
+
execFileSync(pythonCmd, ["-c", `import ${moduleName}`], { stdio: "ignore" });
|
|
355
|
+
return true;
|
|
219
356
|
} catch {
|
|
220
|
-
return
|
|
357
|
+
return false;
|
|
221
358
|
}
|
|
222
359
|
}
|
|
223
360
|
|
|
224
|
-
function
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
return String(result.stdout || "")
|
|
230
|
-
.split(/\r?\n/)
|
|
231
|
-
.map((line) => line.trim())
|
|
232
|
-
.filter(Boolean);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function resolveAgentCli({ target, preferred = "", shimDir = "" }) {
|
|
236
|
-
const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
|
|
237
|
-
const candidates = [
|
|
238
|
-
preferred,
|
|
239
|
-
process.platform === "win32" ? path.join(appData, "npm", `${target}.cmd`) : "",
|
|
240
|
-
process.platform === "win32" ? path.join(appData, "npm", `${target}.exe`) : "",
|
|
241
|
-
process.platform === "win32" ? path.join(appData, "npm", target) : "",
|
|
242
|
-
...listCliCandidatesFromPath(target)
|
|
361
|
+
function runPipInstallWithFallback({ pythonCmd, pipIndexUrl }) {
|
|
362
|
+
const attempts = [
|
|
363
|
+
{ command: pythonCmd, args: ["-m", "pip", "install", "--user", "-U", "langfuse", "-i", pipIndexUrl] },
|
|
364
|
+
{ command: "pip3", args: ["install", "--user", "-U", "langfuse", "-i", pipIndexUrl] },
|
|
365
|
+
{ command: "pip", args: ["install", "--user", "-U", "langfuse", "-i", pipIndexUrl] }
|
|
243
366
|
];
|
|
244
|
-
for (const candidate of candidates) {
|
|
245
|
-
const found = existingCliCandidate(candidate, shimDir);
|
|
246
|
-
if (found) return found;
|
|
247
|
-
}
|
|
248
|
-
return "";
|
|
249
|
-
}
|
|
250
367
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
"foreach ($part in $parts) { if ([string]::Equals($part.TrimEnd('\\'), $dir.TrimEnd('\\'), [StringComparison]::OrdinalIgnoreCase)) { $exists = $true } }",
|
|
261
|
-
"if (-not $exists) {",
|
|
262
|
-
" $next = (@($dir) + $parts) -join ';';",
|
|
263
|
-
" [Environment]::SetEnvironmentVariable('Path', $next, 'User');",
|
|
264
|
-
"}"
|
|
265
|
-
].join(" ");
|
|
266
|
-
const result = spawnSync("powershell", ["-NoProfile", "-Command", cmd], { stdio: "inherit", windowsHide: true });
|
|
267
|
-
if (result.status !== 0) throw new Error("Failed to prepend Claude Langfuse shim to the user PATH.");
|
|
268
|
-
|
|
269
|
-
const currentParts = String(process.env.PATH || "").split(path.delimiter);
|
|
270
|
-
const normalized = dir.replace(/[\\/]+$/, "").toLowerCase();
|
|
271
|
-
if (!currentParts.some((part) => part && part.replace(/[\\/]+$/, "").toLowerCase() === normalized)) {
|
|
272
|
-
process.env.PATH = `${dir}${path.delimiter}${process.env.PATH || ""}`;
|
|
368
|
+
const errors = [];
|
|
369
|
+
for (const attempt of attempts) {
|
|
370
|
+
try {
|
|
371
|
+
console.log(`Trying Python package install: ${attempt.command} ${attempt.args.join(" ")}`);
|
|
372
|
+
execFileSync(attempt.command, attempt.args, { stdio: "inherit" });
|
|
373
|
+
return;
|
|
374
|
+
} catch (error) {
|
|
375
|
+
errors.push(`${attempt.command}: ${error?.message || error}`);
|
|
376
|
+
}
|
|
273
377
|
}
|
|
274
|
-
return true;
|
|
275
|
-
}
|
|
276
378
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
ensureDir(shimDir);
|
|
281
|
-
if (process.platform === "win32") {
|
|
282
|
-
const shim = path.join(shimDir, `${executable}.cmd`);
|
|
283
|
-
const envPrefix = `OH_LANGFUSE_${target.toUpperCase()}_SHIM`;
|
|
284
|
-
const content = [
|
|
285
|
-
"@echo off",
|
|
286
|
-
"REM Auto-generated by scripts/langfuse-setup.mjs",
|
|
287
|
-
`set ${envPrefix}=1`,
|
|
288
|
-
`set LANGFUSE_PUBLIC_KEY=${publicKey}`,
|
|
289
|
-
`set LANGFUSE_SECRET_KEY=${secretKey}`,
|
|
290
|
-
`set LANGFUSE_BASEURL=${baseUrl}`,
|
|
291
|
-
userId ? `set LANGFUSE_USER_ID=${userId}` : null,
|
|
292
|
-
windowsAutoUpdateCommand(target),
|
|
293
|
-
`call ${cmdQuote(realCli)} %*`,
|
|
294
|
-
"exit /b %ERRORLEVEL%",
|
|
295
|
-
""
|
|
296
|
-
].filter(Boolean).join(os.EOL);
|
|
297
|
-
fs.writeFileSync(shim, content, "utf8");
|
|
298
|
-
return { shim, shimDir };
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const shim = path.join(shimDir, executable);
|
|
302
|
-
const lines = [
|
|
303
|
-
"#!/usr/bin/env sh",
|
|
304
|
-
"# Auto-generated by scripts/langfuse-setup.mjs",
|
|
305
|
-
"set -eu",
|
|
306
|
-
`export OH_LANGFUSE_${target.toUpperCase()}_SHIM=1`,
|
|
307
|
-
`export LANGFUSE_PUBLIC_KEY=${shQuote(publicKey)}`,
|
|
308
|
-
`export LANGFUSE_SECRET_KEY=${shQuote(secretKey)}`,
|
|
309
|
-
`export LANGFUSE_BASEURL=${shQuote(baseUrl)}`,
|
|
310
|
-
userId ? `export LANGFUSE_USER_ID=${shQuote(userId)}` : null,
|
|
311
|
-
unixAutoUpdateCommand(target),
|
|
312
|
-
`exec ${shQuote(realCli)} "$@"`,
|
|
313
|
-
""
|
|
314
|
-
].filter(Boolean);
|
|
315
|
-
fs.writeFileSync(shim, lines.join("\n"), "utf8");
|
|
316
|
-
fs.chmodSync(shim, 0o755);
|
|
317
|
-
return { shim, shimDir };
|
|
379
|
+
throw new Error(
|
|
380
|
+
`Failed to install langfuse with system Python/pip. Tried python -m pip, pip3, and pip. Last errors: ${errors.join(" | ")}`
|
|
381
|
+
);
|
|
318
382
|
}
|
|
319
383
|
|
|
320
|
-
function
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
windowsAutoUpdateCommand(target),
|
|
326
|
-
`${executable} %*`,
|
|
327
|
-
""
|
|
328
|
-
].join(os.EOL);
|
|
329
|
-
fs.writeFileSync(launcher, content, "utf8");
|
|
330
|
-
return launcher;
|
|
384
|
+
function installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl }) {
|
|
385
|
+
console.log("Python venv is unavailable; falling back to system Python user install for langfuse.");
|
|
386
|
+
runPipInstallWithFallback({ pythonCmd, pipIndexUrl });
|
|
387
|
+
if (!pythonCanImport(pythonCmd, "langfuse")) {
|
|
388
|
+
throw new Error("langfuse was installed with pip, but python3 still cannot import it. Install python3-venv and rerun setup, for example: sudo apt install python3-venv");
|
|
331
389
|
}
|
|
332
|
-
|
|
333
|
-
const launcher = path.join(baseDir, `launch-${target}-langfuse.sh`);
|
|
334
|
-
const content = [
|
|
335
|
-
"#!/usr/bin/env sh",
|
|
336
|
-
"set -eu",
|
|
337
|
-
unixAutoUpdateCommand(target),
|
|
338
|
-
`exec ${shQuote(executable)} "$@"`,
|
|
339
|
-
""
|
|
340
|
-
].join("\n");
|
|
341
|
-
fs.writeFileSync(launcher, content, "utf8");
|
|
342
|
-
fs.chmodSync(launcher, 0o755);
|
|
343
|
-
return launcher;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function pythonExecutableInVenv(venvDir) {
|
|
347
|
-
return process.platform === "win32"
|
|
348
|
-
? path.join(venvDir, "Scripts", "python.exe")
|
|
349
|
-
: path.join(venvDir, "bin", "python");
|
|
390
|
+
return pythonCmd;
|
|
350
391
|
}
|
|
351
392
|
|
|
352
393
|
async function createHookLauncher({ hooksDir, hookPython, pyPath }) {
|
|
353
|
-
if (process.platform === "win32") {
|
|
354
|
-
const launcher = path.join(hooksDir, "run-langfuse-hook.cmd");
|
|
355
|
-
const content = [
|
|
356
|
-
"@echo off",
|
|
357
|
-
`${cmdQuote(hookPython)} ${cmdQuote(pyPath)}`,
|
|
358
|
-
""
|
|
359
|
-
].join(os.EOL);
|
|
360
|
-
await fsp.writeFile(launcher, content, "utf8");
|
|
361
|
-
return launcher;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const launcher = path.join(hooksDir, "run-langfuse-hook.sh");
|
|
365
|
-
const content = [
|
|
366
|
-
"#!/usr/bin/env sh",
|
|
367
|
-
`exec ${shQuote(hookPython)} ${shQuote(pyPath)}`,
|
|
368
|
-
""
|
|
369
|
-
].join("\n");
|
|
370
|
-
await fsp.writeFile(launcher, content, "utf8");
|
|
371
|
-
fs.chmodSync(launcher, 0o755);
|
|
372
|
-
return launcher;
|
|
373
|
-
}
|
|
394
|
+
if (process.platform === "win32") {
|
|
395
|
+
const launcher = path.join(hooksDir, "run-langfuse-hook.cmd");
|
|
396
|
+
const content = [
|
|
397
|
+
"@echo off",
|
|
398
|
+
`${cmdQuote(hookPython)} ${cmdQuote(pyPath)}`,
|
|
399
|
+
""
|
|
400
|
+
].join(os.EOL);
|
|
401
|
+
await fsp.writeFile(launcher, content, "utf8");
|
|
402
|
+
return launcher;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const launcher = path.join(hooksDir, "run-langfuse-hook.sh");
|
|
406
|
+
const content = [
|
|
407
|
+
"#!/usr/bin/env sh",
|
|
408
|
+
`exec ${shQuote(hookPython)} ${shQuote(pyPath)}`,
|
|
409
|
+
""
|
|
410
|
+
].join("\n");
|
|
411
|
+
await fsp.writeFile(launcher, content, "utf8");
|
|
412
|
+
fs.chmodSync(launcher, 0o755);
|
|
413
|
+
return launcher;
|
|
414
|
+
}
|
|
374
415
|
|
|
375
|
-
function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.tsinghua.edu.cn/simple" }) {
|
|
416
|
+
function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.tsinghua.edu.cn/simple" }) {
|
|
376
417
|
const venvDir = path.join(baseDir, "langfuse-venv");
|
|
377
418
|
const pythonCmd = process.platform === "win32" ? "python" : "python3";
|
|
378
419
|
const venvPython = pythonExecutableInVenv(venvDir);
|
|
379
420
|
|
|
380
421
|
if (!fs.existsSync(venvPython)) {
|
|
381
422
|
console.log(`Creating Python virtual environment: ${venvDir}`);
|
|
382
|
-
try {
|
|
383
|
-
execFileSync(pythonCmd, ["-m", "venv", venvDir], { stdio: "inherit" });
|
|
384
|
-
} catch (e) {
|
|
385
|
-
if (process.platform !== "win32") {
|
|
386
|
-
|
|
387
|
-
}
|
|
388
|
-
throw new Error("Failed to create Python venv. Please confirm Python venv support is available.");
|
|
389
|
-
}
|
|
423
|
+
try {
|
|
424
|
+
execFileSync(pythonCmd, ["-m", "venv", venvDir], { stdio: "inherit" });
|
|
425
|
+
} catch (e) {
|
|
426
|
+
if (process.platform !== "win32") {
|
|
427
|
+
return installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl });
|
|
428
|
+
}
|
|
429
|
+
throw new Error("Failed to create Python venv. Please confirm Python venv support is available.");
|
|
430
|
+
}
|
|
390
431
|
}
|
|
391
432
|
|
|
392
433
|
console.log("Installing/updating Python package in venv: langfuse");
|
|
393
434
|
try {
|
|
394
435
|
execFileSync(
|
|
395
436
|
venvPython,
|
|
396
|
-
["-m", "pip", "install", "-U", "langfuse", "-i", pipIndexUrl],
|
|
437
|
+
["-m", "pip", "install", "-U", "langfuse", "-i", pipIndexUrl],
|
|
397
438
|
{ stdio: "inherit" }
|
|
398
439
|
);
|
|
399
440
|
} catch (e) {
|
|
441
|
+
if (process.platform !== "win32") {
|
|
442
|
+
return installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl });
|
|
443
|
+
}
|
|
400
444
|
throw new Error(`Failed to install langfuse in venv: ${venvPython} -m pip install -U langfuse -i ${pipIndexUrl}`);
|
|
401
445
|
}
|
|
402
446
|
|
|
@@ -406,25 +450,25 @@ function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.
|
|
|
406
450
|
async function main() {
|
|
407
451
|
const args = parseArgs(process.argv.slice(2));
|
|
408
452
|
|
|
409
|
-
const userId = normalizeUserId(args.userId || args.userid);
|
|
410
|
-
if (!userId || typeof userId !== "string") {
|
|
411
|
-
throw new Error("缺少参数:--userId=你的工号");
|
|
412
|
-
}
|
|
413
|
-
assertValidUserId(userId);
|
|
453
|
+
const userId = normalizeUserId(args.userId || args.userid);
|
|
454
|
+
if (!userId || typeof userId !== "string") {
|
|
455
|
+
throw new Error("缺少参数:--userId=你的工号");
|
|
456
|
+
}
|
|
457
|
+
assertValidUserId(userId);
|
|
414
458
|
|
|
415
|
-
const langfuseHost =
|
|
416
|
-
args.langfuseBaseUrl ||
|
|
417
|
-
args.langfuseHost ||
|
|
418
|
-
args.host ||
|
|
419
|
-
process.env.LANGFUSE_BASEURL ||
|
|
420
|
-
process.env.LANGFUSE_HOST ||
|
|
421
|
-
"http://120.46.221.227:3000";
|
|
459
|
+
const langfuseHost =
|
|
460
|
+
args.langfuseBaseUrl ||
|
|
461
|
+
args.langfuseHost ||
|
|
462
|
+
args.host ||
|
|
463
|
+
process.env.LANGFUSE_BASEURL ||
|
|
464
|
+
process.env.LANGFUSE_HOST ||
|
|
465
|
+
"http://120.46.221.227:3000";
|
|
422
466
|
|
|
423
467
|
const publicKey =
|
|
424
468
|
args.publicKey || process.env.LANGFUSE_PUBLIC_KEY || "pk-lf-da0c90a7-6e93-4eb7-bb86-c1047c8d187d";
|
|
425
|
-
const secretKey =
|
|
426
|
-
args.secretKey || process.env.LANGFUSE_SECRET_KEY || "sk-lf-0269b85d-bfdc-442c-bfa3-e737954e3315";
|
|
427
|
-
const pipIndexUrl = args.pipIndexUrl || process.env.LANGFUSE_PIP_INDEX_URL || "https://pypi.tuna.tsinghua.edu.cn/simple";
|
|
469
|
+
const secretKey =
|
|
470
|
+
args.secretKey || process.env.LANGFUSE_SECRET_KEY || "sk-lf-0269b85d-bfdc-442c-bfa3-e737954e3315";
|
|
471
|
+
const pipIndexUrl = args.pipIndexUrl || process.env.LANGFUSE_PIP_INDEX_URL || "https://pypi.tuna.tsinghua.edu.cn/simple";
|
|
428
472
|
if (!publicKey || !secretKey) {
|
|
429
473
|
throw new Error("缺少 Langfuse Key:请提供 --publicKey=... --secretKey=...(或设置环境变量 LANGFUSE_PUBLIC_KEY/LANGFUSE_SECRET_KEY)");
|
|
430
474
|
}
|
|
@@ -434,12 +478,12 @@ async function main() {
|
|
|
434
478
|
process.env.CC_LANGFUSE_ZIP_URL ||
|
|
435
479
|
"https://gitcode.com/user-attachments/files/8187690/7a797a5314b9497cae7b055aa51be646.zip";
|
|
436
480
|
const zipPath = args.zipPath || args.zippath || process.env.CC_LANGFUSE_ZIP_PATH;
|
|
437
|
-
const bundledPyPath = path.join(packageRoot, "langfuse_hook.py");
|
|
438
|
-
const pyPathArg =
|
|
439
|
-
args.pyPath ||
|
|
440
|
-
args.pypath ||
|
|
441
|
-
process.env.CC_LANGFUSE_PY_PATH ||
|
|
442
|
-
(fs.existsSync(bundledPyPath) ? bundledPyPath : "");
|
|
481
|
+
const bundledPyPath = path.join(packageRoot, "langfuse_hook.py");
|
|
482
|
+
const pyPathArg =
|
|
483
|
+
args.pyPath ||
|
|
484
|
+
args.pypath ||
|
|
485
|
+
process.env.CC_LANGFUSE_PY_PATH ||
|
|
486
|
+
(fs.existsSync(bundledPyPath) ? bundledPyPath : "");
|
|
443
487
|
|
|
444
488
|
const userHome = os.homedir();
|
|
445
489
|
const claudeDir = path.join(userHome, ".claude");
|
|
@@ -481,36 +525,36 @@ async function main() {
|
|
|
481
525
|
const nextPyText = setOrReplaceUserId(pyText, userId);
|
|
482
526
|
if (nextPyText !== pyText) {
|
|
483
527
|
await fsp.writeFile(pyPath, nextPyText, "utf8");
|
|
484
|
-
}
|
|
485
|
-
const hookPython = createOrUpdateLangfuseVenv({ baseDir: claudeDir, pipIndexUrl });
|
|
486
|
-
const hookLauncher = await createHookLauncher({ hooksDir, hookPython, pyPath });
|
|
487
|
-
const claudeShimDir = path.join(claudeDir, "bin");
|
|
488
|
-
const realClaudeCli = resolveAgentCli({ target: "claude", preferred: args.cmd || "", shimDir: claudeShimDir });
|
|
489
|
-
const directShim = writeAgentCommandShim({
|
|
490
|
-
baseDir: claudeDir,
|
|
491
|
-
target: "claude",
|
|
492
|
-
executable: "claude",
|
|
493
|
-
realCli: realClaudeCli,
|
|
494
|
-
publicKey,
|
|
495
|
-
secretKey,
|
|
496
|
-
baseUrl: langfuseHost,
|
|
497
|
-
userId
|
|
498
|
-
});
|
|
499
|
-
if (directShim?.shimDir) {
|
|
500
|
-
prependWindowsUserPath(directShim.shimDir);
|
|
501
|
-
}
|
|
502
|
-
const agentLauncher = createAgentLauncher({
|
|
503
|
-
baseDir: claudeDir,
|
|
504
|
-
target: "claude",
|
|
505
|
-
executable: realClaudeCli ? cmdQuote(realClaudeCli) : "claude"
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
// 4) 合并写入 settings.json
|
|
509
|
-
const settingsPath = path.join(claudeDir, "settings.json");
|
|
510
|
-
const existing = readJsonIfExists(settingsPath) ?? {};
|
|
511
|
-
|
|
512
|
-
const desired = {
|
|
513
|
-
env: {
|
|
528
|
+
}
|
|
529
|
+
const hookPython = createOrUpdateLangfuseVenv({ baseDir: claudeDir, pipIndexUrl });
|
|
530
|
+
const hookLauncher = await createHookLauncher({ hooksDir, hookPython, pyPath });
|
|
531
|
+
const claudeShimDir = path.join(claudeDir, "bin");
|
|
532
|
+
const realClaudeCli = resolveAgentCli({ target: "claude", preferred: args.cmd || "", shimDir: claudeShimDir });
|
|
533
|
+
const directShim = writeAgentCommandShim({
|
|
534
|
+
baseDir: claudeDir,
|
|
535
|
+
target: "claude",
|
|
536
|
+
executable: "claude",
|
|
537
|
+
realCli: realClaudeCli,
|
|
538
|
+
publicKey,
|
|
539
|
+
secretKey,
|
|
540
|
+
baseUrl: langfuseHost,
|
|
541
|
+
userId
|
|
542
|
+
});
|
|
543
|
+
if (directShim?.shimDir) {
|
|
544
|
+
prependWindowsUserPath(directShim.shimDir);
|
|
545
|
+
}
|
|
546
|
+
const agentLauncher = createAgentLauncher({
|
|
547
|
+
baseDir: claudeDir,
|
|
548
|
+
target: "claude",
|
|
549
|
+
executable: realClaudeCli ? cmdQuote(realClaudeCli) : "claude"
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// 4) 合并写入 settings.json
|
|
553
|
+
const settingsPath = path.join(claudeDir, "settings.json");
|
|
554
|
+
const existing = readJsonIfExists(settingsPath) ?? {};
|
|
555
|
+
|
|
556
|
+
const desired = {
|
|
557
|
+
env: {
|
|
514
558
|
TRACE_TO_LANGFUSE: "true",
|
|
515
559
|
LANGFUSE_PUBLIC_KEY: publicKey,
|
|
516
560
|
LANGFUSE_SECRET_KEY: secretKey,
|
|
@@ -526,34 +570,34 @@ async function main() {
|
|
|
526
570
|
Stop: [
|
|
527
571
|
{
|
|
528
572
|
hooks: [
|
|
529
|
-
{
|
|
530
|
-
type: "command",
|
|
531
|
-
command: hookCommandForClaude({ launcher: hookLauncher, hookPython, pyPath })
|
|
532
|
-
}
|
|
533
|
-
]
|
|
534
|
-
}
|
|
573
|
+
{
|
|
574
|
+
type: "command",
|
|
575
|
+
command: hookCommandForClaude({ launcher: hookLauncher, hookPython, pyPath })
|
|
576
|
+
}
|
|
577
|
+
]
|
|
578
|
+
}
|
|
535
579
|
]
|
|
536
580
|
}
|
|
537
581
|
};
|
|
538
582
|
|
|
539
583
|
const merged = deepMerge(existing, desired);
|
|
540
584
|
ensureDir(claudeDir);
|
|
541
|
-
writeJsonPretty(settingsPath, merged);
|
|
542
|
-
const runtimeState = writeRuntimeInstallRecord("claude", {
|
|
543
|
-
packageName: packageJson.name,
|
|
544
|
-
packageVersion: packageJson.version,
|
|
545
|
-
});
|
|
546
|
-
console.log(`Runtime version recorded: ${runtimeState}`);
|
|
547
|
-
console.log(`已更新:${settingsPath}`);
|
|
548
|
-
console.log(`Agent launcher with update check: ${agentLauncher}`);
|
|
549
|
-
if (directShim) {
|
|
550
|
-
console.log(`Direct Claude command shim ready: ${directShim.shim}`);
|
|
551
|
-
console.log(`Real Claude CLI: ${realClaudeCli}`);
|
|
552
|
-
} else {
|
|
553
|
-
console.log("Claude CLI not found; skipped direct claude command shim.");
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
}
|
|
585
|
+
writeJsonPretty(settingsPath, merged);
|
|
586
|
+
const runtimeState = writeRuntimeInstallRecord("claude", {
|
|
587
|
+
packageName: packageJson.name,
|
|
588
|
+
packageVersion: packageJson.version,
|
|
589
|
+
});
|
|
590
|
+
console.log(`Runtime version recorded: ${runtimeState}`);
|
|
591
|
+
console.log(`已更新:${settingsPath}`);
|
|
592
|
+
console.log(`Agent launcher with update check: ${agentLauncher}`);
|
|
593
|
+
if (directShim) {
|
|
594
|
+
console.log(`Direct Claude command shim ready: ${directShim.shim}`);
|
|
595
|
+
console.log(`Real Claude CLI: ${realClaudeCli}`);
|
|
596
|
+
} else {
|
|
597
|
+
console.log("Claude CLI not found; skipped direct claude command shim.");
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
}
|
|
557
601
|
|
|
558
602
|
main().catch((err) => {
|
|
559
603
|
console.error(err?.message || String(err));
|