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,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 existingCliCandidate(candidate, shimDir) {
216
- if (!candidate || isPathInsideDir(candidate, shimDir)) return "";
352
+ function pythonCanImport(pythonCmd, moduleName) {
217
353
  try {
218
- return fs.existsSync(candidate) ? candidate : "";
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 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)
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
- 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 || ""}`;
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
- 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 };
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 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;
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
- throw new Error("Failed to create Python venv. Please install python3-venv first, for example: sudo apt install python3-venv");
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));