oh-langfuse 0.1.54 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-langfuse",
3
- "version": "0.1.54",
3
+ "version": "0.1.55",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Use npm scripts to configure Claude Code / OpenCode / Codex with Langfuse tracing.",
@@ -59,13 +59,54 @@ function tomlArray(items) {
59
59
  return `[${items.map(tomlString).join(", ")}]`;
60
60
  }
61
61
 
62
- function pythonExecutableInVenv(venvDir) {
63
- return process.platform === "win32"
64
- ? path.join(venvDir, "Scripts", "python.exe")
65
- : path.join(venvDir, "bin", "python");
66
- }
67
-
68
- function shQuote(s) {
62
+ function pythonExecutableInVenv(venvDir) {
63
+ return process.platform === "win32"
64
+ ? path.join(venvDir, "Scripts", "python.exe")
65
+ : path.join(venvDir, "bin", "python");
66
+ }
67
+
68
+ function pythonCanImport(pythonCmd, moduleName) {
69
+ try {
70
+ execFileSync(pythonCmd, ["-c", `import ${moduleName}`], { stdio: "ignore" });
71
+ return true;
72
+ } catch {
73
+ return false;
74
+ }
75
+ }
76
+
77
+ function runPipInstallWithFallback({ pythonCmd, pipIndexUrl }) {
78
+ const attempts = [
79
+ { command: pythonCmd, args: ["-m", "pip", "install", "--user", "-U", "langfuse", "-i", pipIndexUrl] },
80
+ { command: "pip3", args: ["install", "--user", "-U", "langfuse", "-i", pipIndexUrl] },
81
+ { command: "pip", args: ["install", "--user", "-U", "langfuse", "-i", pipIndexUrl] }
82
+ ];
83
+
84
+ const errors = [];
85
+ for (const attempt of attempts) {
86
+ try {
87
+ console.log(`Trying Python package install: ${attempt.command} ${attempt.args.join(" ")}`);
88
+ execFileSync(attempt.command, attempt.args, { stdio: "inherit" });
89
+ return;
90
+ } catch (error) {
91
+ errors.push(`${attempt.command}: ${error?.message || error}`);
92
+ }
93
+ }
94
+
95
+ throw new Error(
96
+ `Failed to install langfuse with system Python/pip. Tried python -m pip, pip3, and pip. Last errors: ${errors.join(" | ")}`
97
+ );
98
+ }
99
+
100
+ function installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl }) {
101
+ console.log("Python venv is unavailable; falling back to system Python user install for langfuse.");
102
+ runPipInstallWithFallback({ pythonCmd, pipIndexUrl });
103
+ if (!pythonCanImport(pythonCmd, "langfuse")) {
104
+ 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");
105
+ }
106
+ return pythonCmd;
107
+ }
108
+
109
+ function shQuote(s) {
69
110
  return `'${String(s).replace(/'/g, "'\"'\"'")}'`;
70
111
  }
71
112
 
@@ -288,14 +329,14 @@ function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.
288
329
 
289
330
  if (!fs.existsSync(venvPython)) {
290
331
  console.log(`Creating Python virtual environment: ${venvDir}`);
291
- try {
292
- execFileSync(pythonCmd, ["-m", "venv", venvDir], { stdio: "inherit" });
293
- } catch (e) {
294
- if (process.platform !== "win32") {
295
- throw new Error("Failed to create Python venv. Please install python3-venv first, for example: sudo apt install python3-venv");
296
- }
297
- throw new Error("Failed to create Python venv. Please confirm Python venv support is available.");
298
- }
332
+ try {
333
+ execFileSync(pythonCmd, ["-m", "venv", venvDir], { stdio: "inherit" });
334
+ } catch (e) {
335
+ if (process.platform !== "win32") {
336
+ return installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl });
337
+ }
338
+ throw new Error("Failed to create Python venv. Please confirm Python venv support is available.");
339
+ }
299
340
  }
300
341
 
301
342
  console.log("Installing/updating Python package in venv: langfuse");
@@ -303,11 +344,14 @@ function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.
303
344
  execFileSync(
304
345
  venvPython,
305
346
  ["-m", "pip", "install", "-U", "langfuse", "-i", pipIndexUrl],
306
- { stdio: "inherit" }
307
- );
308
- } catch (e) {
309
- throw new Error(`Failed to install langfuse in venv: ${venvPython} -m pip install -U langfuse -i ${pipIndexUrl}`);
310
- }
347
+ { stdio: "inherit" }
348
+ );
349
+ } catch (e) {
350
+ if (process.platform !== "win32") {
351
+ return installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl });
352
+ }
353
+ throw new Error(`Failed to install langfuse in venv: ${venvPython} -m pip install -U langfuse -i ${pipIndexUrl}`);
354
+ }
311
355
 
312
356
  return venvPython;
313
357
  }
@@ -343,13 +343,54 @@ function createAgentLauncher({ baseDir, target, executable }) {
343
343
  return launcher;
344
344
  }
345
345
 
346
- function pythonExecutableInVenv(venvDir) {
347
- return process.platform === "win32"
348
- ? path.join(venvDir, "Scripts", "python.exe")
349
- : path.join(venvDir, "bin", "python");
350
- }
351
-
352
- async function createHookLauncher({ hooksDir, hookPython, pyPath }) {
346
+ function pythonExecutableInVenv(venvDir) {
347
+ return process.platform === "win32"
348
+ ? path.join(venvDir, "Scripts", "python.exe")
349
+ : path.join(venvDir, "bin", "python");
350
+ }
351
+
352
+ function pythonCanImport(pythonCmd, moduleName) {
353
+ try {
354
+ execFileSync(pythonCmd, ["-c", `import ${moduleName}`], { stdio: "ignore" });
355
+ return true;
356
+ } catch {
357
+ return false;
358
+ }
359
+ }
360
+
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] }
366
+ ];
367
+
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
+ }
377
+ }
378
+
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
+ );
382
+ }
383
+
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");
389
+ }
390
+ return pythonCmd;
391
+ }
392
+
393
+ async function createHookLauncher({ hooksDir, hookPython, pyPath }) {
353
394
  if (process.platform === "win32") {
354
395
  const launcher = path.join(hooksDir, "run-langfuse-hook.cmd");
355
396
  const content = [
@@ -379,14 +420,14 @@ function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.
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");
@@ -394,11 +435,14 @@ function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.
394
435
  execFileSync(
395
436
  venvPython,
396
437
  ["-m", "pip", "install", "-U", "langfuse", "-i", pipIndexUrl],
397
- { stdio: "inherit" }
398
- );
399
- } catch (e) {
400
- throw new Error(`Failed to install langfuse in venv: ${venvPython} -m pip install -U langfuse -i ${pipIndexUrl}`);
401
- }
438
+ { stdio: "inherit" }
439
+ );
440
+ } catch (e) {
441
+ if (process.platform !== "win32") {
442
+ return installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl });
443
+ }
444
+ throw new Error(`Failed to install langfuse in venv: ${venvPython} -m pip install -U langfuse -i ${pipIndexUrl}`);
445
+ }
402
446
 
403
447
  return venvPython;
404
448
  }
@@ -61,6 +61,7 @@ function main() {
61
61
  const userConfigPath = path.join(home, ".config", "opencode-plugin-langfuse", "config.json");
62
62
  const windowsLauncherPath = path.join(opencodeDir, "launch-opencode-langfuse.cmd");
63
63
  const unixLauncherPath = path.join(opencodeDir, "launch-opencode-langfuse.sh");
64
+ const opencodeCommandShimPath = path.join(opencodeDir, "bin", process.platform === "win32" ? "opencode.cmd" : "opencode");
64
65
 
65
66
  const results = [];
66
67
 
@@ -134,6 +135,14 @@ function main() {
134
135
  "Run setup again and enter userId when prompted."
135
136
  );
136
137
 
138
+ addResult(
139
+ results,
140
+ "opencode command shim",
141
+ fs.existsSync(opencodeCommandShimPath),
142
+ opencodeCommandShimPath,
143
+ "Run setup again after installing OpenCode so the direct opencode command can load Langfuse and auto-update checks."
144
+ );
145
+
137
146
  if (process.platform === "win32") {
138
147
  addResult(
139
148
  results,
@@ -512,12 +512,10 @@ function getPatchedLangfuseDistIndexJs() {
512
512
  " if (userId) spanProcessors.push(createUserIdSpanProcessor(userId));",
513
513
  "",
514
514
  " const sdk = new NodeSDK({ spanProcessors });",
515
- " // Defer SDK start to avoid blocking Opencode launch",
516
- " setImmediate(() => {",
517
- ' try { sdk.start(); }',
518
- ' catch (err) { log("warn", `OTEL SDK start failed: ${err?.message ?? err}`); }',
515
+ " const sdkStartPromise = Promise.resolve().then(() => sdk.start()).catch((err) => {",
516
+ ' log("warn", `OTEL SDK start failed: ${err?.message ?? err}`);',
519
517
  " });",
520
- " const metricsTracer = trace.getTracer('oh-langfuse-opencode-metrics');",
518
+ " const getMetricsTracer = () => trace.getTracer('oh-langfuse-opencode-metrics');",
521
519
  " const knownSkillNames = await collectKnownSkillNames();",
522
520
  " const startupSkillUsages = detectOpencodeSkillUsages(process.argv.join('\\n'), knownSkillNames);",
523
521
  " const messageTextById = new Map();",
@@ -534,21 +532,23 @@ function getPatchedLangfuseDistIndexJs() {
534
532
  ' if (userId) log("info", `LANGFUSE userId configured -> ${userId}`);',
535
533
  ' if (knownSkillNames.length) log("info", `OpenCode skills discovered -> ${knownSkillNames.length}`);',
536
534
  "",
537
- " let shutdownStarted = false;",
538
- " const flush = async (reason) => {",
539
- " try {",
540
- ' log("info", `Flushing OTEL spans on ${reason}`);',
541
- " await processor.forceFlush();",
535
+ " let shutdownStarted = false;",
536
+ " const flush = async (reason) => {",
537
+ " try {",
538
+ ' log("info", `Flushing OTEL spans on ${reason}`);',
539
+ " await sdkStartPromise;",
540
+ " await processor.forceFlush();",
542
541
  " } catch (error) {",
543
542
  ' log("warn", `OTEL forceFlush failed on ${reason}: ${error?.message ?? error}`);',
544
543
  " }",
545
544
  " };",
546
545
  " const shutdown = async (reason) => {",
547
- " if (shutdownStarted) return;",
548
- " shutdownStarted = true;",
549
- " try {",
550
- ' log("info", `Shutting down OTEL SDK on ${reason}`);',
551
- " await sdk.shutdown();",
546
+ " if (shutdownStarted) return;",
547
+ " shutdownStarted = true;",
548
+ " try {",
549
+ ' log("info", `Shutting down OTEL SDK on ${reason}`);',
550
+ " await sdkStartPromise;",
551
+ " await sdk.shutdown();",
552
552
  " } catch (error) {",
553
553
  ' log("warn", `OTEL shutdown failed on ${reason}: ${error?.message ?? error}`);',
554
554
  " }",
@@ -586,7 +586,7 @@ function getPatchedLangfuseDistIndexJs() {
586
586
  " }",
587
587
  " set.add(activity.toolCallId || `${kind}:${set.size + 1}`);",
588
588
  " };",
589
- " const recordInteractionMetric = (event) => {",
589
+ " const recordInteractionMetric = async (event) => {",
590
590
  " const payload = eventPayload(event);",
591
591
  " const part = eventPart(event);",
592
592
  " const partType = part?.type ?? '';",
@@ -617,7 +617,8 @@ function getPatchedLangfuseDistIndexJs() {
617
617
  " const tokenMetrics = tokenMetricsFromPart(part);",
618
618
  " const total = tokenMetrics.total ?? (tokenMetrics.input !== undefined && tokenMetrics.output !== undefined ? tokenMetrics.input + tokenMetrics.output : undefined);",
619
619
  " const tokenAvailable = [tokenMetrics.input, tokenMetrics.output, total, tokenMetrics.cacheRead, tokenMetrics.reasoning].some((value) => value !== undefined);",
620
- " const span = metricsTracer.startSpan('Agent Turn');",
620
+ " await sdkStartPromise;",
621
+ " const span = getMetricsTracer().startSpan('Agent Turn');",
621
622
  " const text = messageTextById.get(messageId) || '';",
622
623
  " const skillUsages = dedupeSkillUsages([...(skillUsagesByMessageId.get(messageId) ?? []), ...(skillUsagesBySessionId.get(sessionId) ?? [])]);",
623
624
  " const interactionId = `opencode:${userId || \"unknown\"}:${sessionId || \"unknown\"}:${messageId}`;",
@@ -673,9 +674,9 @@ function getPatchedLangfuseDistIndexJs() {
673
674
  ' log("warn", "OpenTelemetry experimental feature is disabled in Opencode config - tracing disabled");',
674
675
  " }",
675
676
  " },",
676
- " event: async ({ event }) => {",
677
- " const eventType = pickEventString(event?.name, event?.type, eventPayload(event)?.name, eventPayload(event)?.type);",
678
- " recordInteractionMetric(event);",
677
+ " event: async ({ event }) => {",
678
+ " const eventType = pickEventString(event?.name, event?.type, eventPayload(event)?.name, eventPayload(event)?.type);",
679
+ " await recordInteractionMetric(event);",
679
680
  ' if (eventType === "session.idle" || eventType === "message.updated" || eventType === "message.part.updated" || eventType === "session.updated" || eventType === "session.idle.1" || eventType === "message.updated.1" || eventType === "message.part.updated.1" || eventType === "session.updated.1") {',
680
681
  " await flush(eventType);",
681
682
  " }",
@@ -973,24 +974,28 @@ function npmInstallLooksLikeMissingVersion(result) {
973
974
  return text.includes("etarget") || text.includes("notarget") || text.includes("no matching version found");
974
975
  }
975
976
 
976
- function isOfficialNpmRegistry(registry) {
977
- return /^https?:\/\/registry\.npmjs\.org\/?$/i.test(String(registry || "").trim());
978
- }
979
-
980
- async function runNpmInstallOrThrow({ opencodeDir, pkgName = "opencode-plugin-langfuse", npmRegistry = "" }) {
981
- const npmArgs = ["install", pkgName, "--prefix", opencodeDir, "--package-lock=false", "--no-save", "--audit=false", "--fund=false"];
982
- if (npmRegistry) npmArgs.push("--registry", npmRegistry);
983
- const cliJs = getNpmCliJsPath();
984
- console.log(`使用 npm:${fs.existsSync(cliJs) ? `node ${cliJs}` : getNpmExecutable()}`);
985
- console.log("Installing OpenCode Langfuse plugin. This can take a few minutes on slow networks...");
986
- let r = await runNpmInstallCapture(npmArgs);
987
- if (!r.error && r.status !== 0 && npmInstallLooksLikeMissingVersion(r) && !isOfficialNpmRegistry(npmRegistry)) {
988
- console.error("");
989
- console.error("npm registry appears to be missing a package version. Retrying with https://registry.npmjs.org/ ...");
990
- const retryArgs = ["install", pkgName, "--prefix", opencodeDir, "--package-lock=false", "--no-save", "--audit=false", "--fund=false", "--registry", "https://registry.npmjs.org/"];
991
- r = await runNpmInstallCapture(retryArgs);
992
- if (!r.error && r.status === 0) return;
993
- }
977
+ function isOfficialNpmRegistry(registry) {
978
+ return /^https?:\/\/registry\.npmjs\.org\/?$/i.test(String(registry || "").trim());
979
+ }
980
+
981
+ const OFFICIAL_NPM_REGISTRY = "https://registry.npmjs.org/";
982
+
983
+ async function runNpmInstallOrThrow({ opencodeDir, pkgName = "opencode-plugin-langfuse", npmRegistry = "" }) {
984
+ const npmArgs = ["install", pkgName, "--prefix", opencodeDir, "--package-lock=false", "--no-save", "--audit=false", "--fund=false"];
985
+ const effectiveRegistry = npmRegistry || OFFICIAL_NPM_REGISTRY;
986
+ npmArgs.push("--registry", effectiveRegistry);
987
+ const cliJs = getNpmCliJsPath();
988
+ console.log(`使用 npm:${fs.existsSync(cliJs) ? `node ${cliJs}` : getNpmExecutable()}`);
989
+ console.log(`使用 npm registry:${effectiveRegistry}`);
990
+ console.log("Installing OpenCode Langfuse plugin. This can take a few minutes on slow networks...");
991
+ let r = await runNpmInstallCapture(npmArgs);
992
+ if (!r.error && r.status !== 0 && npmInstallLooksLikeMissingVersion(r) && !isOfficialNpmRegistry(effectiveRegistry)) {
993
+ console.error("");
994
+ console.error(`npm registry appears to be missing a package version. Retrying with ${OFFICIAL_NPM_REGISTRY} ...`);
995
+ const retryArgs = ["install", pkgName, "--prefix", opencodeDir, "--package-lock=false", "--no-save", "--audit=false", "--fund=false", "--registry", OFFICIAL_NPM_REGISTRY];
996
+ r = await runNpmInstallCapture(retryArgs);
997
+ if (!r.error && r.status === 0) return;
998
+ }
994
999
  if (!r.error && r.status === 0) return;
995
1000
  printNpmDiagnostics();
996
1001
  const npmLabel = fs.existsSync(cliJs) ? `node ${cliJs}` : getNpmExecutable();
@@ -1105,7 +1110,7 @@ async function main() {
1105
1110
  throw new Error("缺少参数:--userId=你的工号");
1106
1111
  }
1107
1112
  assertValidUserId(userId);
1108
- const npmRegistry = args.npmRegistry || process.env.OPENCODE_NPM_REGISTRY || process.env.NPM_CONFIG_REGISTRY || "";
1113
+ const npmRegistry = args.npmRegistry || process.env.OPENCODE_NPM_REGISTRY || "";
1109
1114
 
1110
1115
  const home = os.homedir();
1111
1116
  const opencodeDir = path.join(home, ".config", "opencode");