oh-langfuse 0.1.57 → 0.1.59

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  `oh-langfuse` 是用于给 Claude Code、OpenCode、Codex 配置 Langfuse 追踪的命令行工具。它既提供交互式安装向导,也支持 `setup`、`check`、`update`、`auto-update` 等直接命令。
4
4
 
5
- 当前 npm 版本:`0.1.57`
5
+ 当前 npm 版本:`0.1.59`
6
6
 
7
7
  ## 能做什么
8
8
 
@@ -51,6 +51,13 @@ npx oh-langfuse@latest update opencode
51
51
  npx oh-langfuse@latest update codex
52
52
  ```
53
53
 
54
+ 确认是否已经更新到 npm 最新版本:
55
+
56
+ ```bash
57
+ npx oh-langfuse@latest verify-update all
58
+ npx oh-langfuse@latest verify-update opencode
59
+ ```
60
+
54
61
  ## 交互式菜单
55
62
 
56
63
  运行 `npx oh-langfuse@latest` 会打开菜单:
@@ -72,6 +79,23 @@ npx oh-langfuse@latest update codex
72
79
 
73
80
  记录 Claude Code / OpenCode / Codex 当前写入本机 runtime 的包名和版本。
74
81
 
82
+ `verify-update` 会读取这份记录,并对比 npm `oh-langfuse@latest`:
83
+
84
+ ```bash
85
+ npx oh-langfuse@latest verify-update all
86
+ ```
87
+
88
+ 输出示例:
89
+
90
+ ```text
91
+ [OK] OpenCode Langfuse runtime is current: 0.1.59
92
+ [FAIL] Codex Langfuse runtime is outdated: installed 0.1.58, latest 0.1.59
93
+ [FIX] Run: npx oh-langfuse@latest update codex
94
+ [SKIP] Claude not installed
95
+ ```
96
+
97
+ `check` 用于检查配置完整性;`verify-update` 才用于确认本机 runtime 是否已经更新到 npm 最新版本。
98
+
75
99
  OpenCode setup 会生成直接命令 shim:
76
100
 
77
101
  - Windows:`~/.config/opencode/bin/opencode.cmd`
@@ -167,6 +191,8 @@ Linux 环境缺少 `python3-venv/ensurepip` 时,安装器会降级尝试 `pyth
167
191
 
168
192
  配置后需要重启 Codex,让新的 notify 命令加载。notify hook 会增量读取 `~/.codex/sessions/**/*.jsonl`,并把读取偏移记录到 `~/.codex/langfuse/state.json`。
169
193
 
194
+ notify hook 会读取 `~/.codex/langfuse/config.json`(或 `CODEX_HOME/langfuse/config.json`)中的 `baseUrl` / `host`,并把该主机补入 `NO_PROXY`,避免自建 Langfuse 服务被代理干扰。
195
+
170
196
  ## 环境变量
171
197
 
172
198
  | 变量 | 作用 |
package/bin/cli.js CHANGED
@@ -794,11 +794,13 @@ function printHelp() {
794
794
  "oh-langfuse check codex",
795
795
  "oh-langfuse update",
796
796
  "oh-langfuse update all",
797
- "oh-langfuse update claude",
798
- "oh-langfuse update opencode",
799
- "oh-langfuse update codex",
800
- "oh-langfuse auto-update opencode"
801
- ]);
797
+ "oh-langfuse update claude",
798
+ "oh-langfuse update opencode",
799
+ "oh-langfuse update codex",
800
+ "oh-langfuse verify-update all",
801
+ "oh-langfuse verify-update opencode",
802
+ "oh-langfuse auto-update opencode"
803
+ ]);
802
804
  renderSection("Options", [
803
805
  `${paint("--dry-run", t.gold)} Preview actions without writing files or installing packages.`,
804
806
  `${paint("--userId=ID", t.gold)} Provide the Langfuse user id without prompting.`,
@@ -865,17 +867,23 @@ async function main() {
865
867
  ];
866
868
  return runNodeScript("update-langfuse-runtime.mjs", updateArgs, options);
867
869
  }
868
- if (cmd === "auto-update") {
869
- return runNodeScript("auto-update-runtime.mjs", [
870
- target || "all",
871
- ...(hasValue(options.npmRegistry) ? [`--npmRegistry=${options.npmRegistry}`] : []),
872
- ...(hasValue(options.pipIndexUrl) ? [`--pipIndexUrl=${options.pipIndexUrl}`] : []),
870
+ if (cmd === "auto-update") {
871
+ return runNodeScript("auto-update-runtime.mjs", [
872
+ target || "all",
873
+ ...(hasValue(options.npmRegistry) ? [`--npmRegistry=${options.npmRegistry}`] : []),
874
+ ...(hasValue(options.pipIndexUrl) ? [`--pipIndexUrl=${options.pipIndexUrl}`] : []),
873
875
  ...(options.skipCheck ? ["--skip-check"] : []),
874
876
  ...(options.startupStatus ? ["--startup-status"] : []),
875
- ...(options.yes ? ["--yes"] : []),
876
- ], { ...options, quiet: true });
877
- }
878
- if (cmd === "check" && target === "claude") return checkClaude(options);
877
+ ...(options.yes ? ["--yes"] : []),
878
+ ], { ...options, quiet: true });
879
+ }
880
+ if (cmd === "verify-update") {
881
+ return runNodeScript("verify-update-runtime.mjs", [
882
+ target || "all",
883
+ ...(hasValue(options.npmRegistry) ? [`--npmRegistry=${options.npmRegistry}`] : []),
884
+ ], { ...options, quiet: true });
885
+ }
886
+ if (cmd === "check" && target === "claude") return checkClaude(options);
879
887
  if (cmd === "check" && target === "opencode") return checkOpenCode(options);
880
888
  if (cmd === "check" && target === "codex") return checkCodex(options);
881
889
  if (cmd === "check" && target === "environment") {
@@ -17,15 +17,38 @@ from dataclasses import dataclass
17
17
  from datetime import datetime, timezone
18
18
  from pathlib import Path
19
19
  from typing import Any, Dict, List, Optional, Tuple
20
- from urllib.parse import urlparse
21
-
22
-
23
- def configure_langfuse_no_proxy() -> None:
24
- hosts = ["localhost", "127.0.0.1"]
25
- for key in ("LANGFUSE_HOST", "LANGFUSE_BASEURL", "CODEX_LANGFUSE_BASE_URL"):
26
- value = os.environ.get(key)
27
- if not value:
28
- continue
20
+ from urllib.parse import urlparse
21
+
22
+
23
+ def codex_langfuse_config_path() -> Path:
24
+ codex_dir = Path(os.environ.get("CODEX_HOME") or (Path.home() / ".codex"))
25
+ return codex_dir / "langfuse" / "config.json"
26
+
27
+
28
+ def read_langfuse_base_url_from_config() -> Optional[str]:
29
+ try:
30
+ path = codex_langfuse_config_path()
31
+ if not path.exists():
32
+ return None
33
+ data = json.loads(path.read_text(encoding="utf-8-sig"))
34
+ if isinstance(data, dict):
35
+ value = data.get("baseUrl") or data.get("host")
36
+ if isinstance(value, str) and value.strip():
37
+ return value.strip()
38
+ except Exception:
39
+ return None
40
+ return None
41
+
42
+
43
+ def configure_langfuse_no_proxy() -> None:
44
+ hosts = ["localhost", "127.0.0.1"]
45
+ values = [
46
+ *(os.environ.get(key) for key in ("LANGFUSE_HOST", "LANGFUSE_BASEURL", "CODEX_LANGFUSE_BASE_URL")),
47
+ read_langfuse_base_url_from_config(),
48
+ ]
49
+ for value in values:
50
+ if not value:
51
+ continue
29
52
  parsed = urlparse(value if "://" in value else f"http://{value}")
30
53
  if parsed.hostname:
31
54
  hosts.append(parsed.hostname)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-langfuse",
3
- "version": "0.1.57",
3
+ "version": "0.1.59",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Use npm scripts to configure Claude Code / OpenCode / Codex with Langfuse tracing.",
@@ -27,9 +27,11 @@
27
27
  "scripts/log-filter-utils.mjs",
28
28
  "scripts/metrics-utils.mjs",
29
29
  "scripts/runtime-state-utils.mjs",
30
- "scripts/update-langfuse-runtime.mjs",
31
- "scripts/update-utils.mjs",
32
- "langfuse_hook.py",
30
+ "scripts/update-langfuse-runtime.mjs",
31
+ "scripts/update-utils.mjs",
32
+ "scripts/verify-update-runtime.mjs",
33
+ "scripts/verify-update-utils.mjs",
34
+ "langfuse_hook.py",
33
35
  "codex_langfuse_notify.py",
34
36
  "README.md",
35
37
  "SELF_VERIFY.md",
@@ -632,12 +632,12 @@ async function main() {
632
632
  }
633
633
  }
634
634
 
635
- const found = await pollLangfuse(config, marker, { ...args, since, target });
636
- console.log(`[OK] Langfuse marker found for ${target}: ${found.kind} ${found.id || ""}`.trim());
637
- const metrics = await verifyMetricObservations(config, found, { since, target, marker });
638
- console.log(`[OK] Langfuse metrics found for ${target}: Agent Turn x${metrics.interactionCount}`);
639
- results.push({ target, marker, langfuse: { kind: found.kind, id: found.id || "", traceId: metrics.traceId }, metrics });
640
- }
635
+ const found = await pollLangfuse(config, marker, { ...args, since, target });
636
+ console.log(`[OK] Langfuse marker found for ${target}: ${found.kind} ${found.id || ""}`.trim());
637
+ const metrics = await verifyMetricObservations(config, found, { since, target, marker });
638
+ console.log(`[OK] Langfuse metrics found for ${target}: ${expectedAgentTurnName(target)} x${metrics.interactionCount}`);
639
+ results.push({ target, marker, langfuse: { kind: found.kind, id: found.id || "", traceId: metrics.traceId }, metrics });
640
+ }
641
641
 
642
642
  console.log("");
643
643
  console.log(JSON.stringify({ ok: true, package: packageJson.name, marker, results }, null, 2));
@@ -254,7 +254,9 @@ async function main() {
254
254
  printUpdateSummary(targets);
255
255
  }
256
256
 
257
- main().catch((error) => {
258
- console.error(`[FAIL] ${error?.message || String(error)}`);
259
- process.exit(1);
260
- });
257
+ main()
258
+ .then(() => process.exit(0))
259
+ .catch((error) => {
260
+ console.error(`[FAIL] ${error?.message || String(error)}`);
261
+ process.exit(1);
262
+ });
@@ -0,0 +1,162 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { extractVersionFromNpmMetadata } from "./update-utils.mjs";
5
+ import { readRuntimeState, runtimeStatePath } from "./runtime-state-utils.mjs";
6
+ import {
7
+ buildVerifyUpdateResults,
8
+ normalizeVerifyTargets,
9
+ targetLabel,
10
+ verifyUpdateExitCode,
11
+ } from "./verify-update-utils.mjs";
12
+
13
+ const colorEnabled = process.stdout.isTTY && process.env.NO_COLOR !== "1";
14
+ const ansi = (code) => (colorEnabled ? `\x1b[${code}m` : "");
15
+ const t = {
16
+ reset: ansi(0),
17
+ bold: ansi(1),
18
+ green: ansi("92"),
19
+ gold: ansi("93"),
20
+ red: ansi("91"),
21
+ cyan: ansi("96"),
22
+ };
23
+
24
+ function paint(text, ...styles) {
25
+ if (!colorEnabled) return text;
26
+ return `${styles.join("")}${text}${t.reset}`;
27
+ }
28
+
29
+ function parseArgs(argv) {
30
+ const args = { _: [] };
31
+ for (const raw of argv) {
32
+ if (!raw.startsWith("--")) {
33
+ args._.push(raw);
34
+ continue;
35
+ }
36
+ const eq = raw.indexOf("=");
37
+ if (eq === -1) args[raw.slice(2)] = true;
38
+ else args[raw.slice(2, eq)] = raw.slice(eq + 1);
39
+ }
40
+ return args;
41
+ }
42
+
43
+ function stripBom(s) {
44
+ return typeof s === "string" && s.charCodeAt(0) === 0xfeff ? s.slice(1) : s;
45
+ }
46
+
47
+ function readJsonIfExists(p) {
48
+ try {
49
+ if (!fs.existsSync(p)) return null;
50
+ const text = stripBom(fs.readFileSync(p, "utf8"));
51
+ if (!text.trim()) return null;
52
+ return JSON.parse(text);
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ function codexHome(home = os.homedir()) {
59
+ return process.env.CODEX_HOME || path.join(home, ".codex");
60
+ }
61
+
62
+ function detectInstalledTargets(home = os.homedir()) {
63
+ return {
64
+ claude: fs.existsSync(path.join(home, ".claude", "hooks", "langfuse_hook.py")),
65
+ opencode:
66
+ fs.existsSync(path.join(home, ".config", "opencode", "opencode.json")) ||
67
+ fs.existsSync(path.join(home, ".config", "opencode", "plugins", "opencode-plugin-langfuse")),
68
+ codex: fs.existsSync(path.join(codexHome(home), "hooks", "codex_langfuse_notify.py")),
69
+ };
70
+ }
71
+
72
+ async function latestVersion(packageName, registry = "https://registry.npmjs.org") {
73
+ const base = registry.replace(/\/+$/, "");
74
+ const response = await fetch(`${base}/${packageName}`, { headers: { accept: "application/json" } });
75
+ if (!response.ok) throw new Error(`npm registry ${response.status} ${response.statusText}`);
76
+ return extractVersionFromNpmMetadata(await response.json());
77
+ }
78
+
79
+ function targetsToVerify(targetArg, installed, runtimeTargets) {
80
+ const requested = normalizeVerifyTargets(targetArg);
81
+ if (String(targetArg || "all").trim().toLowerCase() !== "all") return { targets: requested, skipped: [] };
82
+
83
+ const targets = requested.filter((target) => installed[target] || runtimeTargets[target]);
84
+ const skipped = requested
85
+ .filter((target) => !installed[target] && !runtimeTargets[target])
86
+ .map((target) => ({ target, reason: "not_detected" }));
87
+ return { targets, skipped };
88
+ }
89
+
90
+ function printResult(result) {
91
+ const label = targetLabel(result.target);
92
+ if (result.status === "current") {
93
+ console.log(paint(`[OK] ${label} Langfuse runtime is current: ${result.packageName}@${result.installedVersion}`, t.bold, t.green));
94
+ if (result.updatedAt) console.log(`[INFO] ${label} updatedAt: ${result.updatedAt}`);
95
+ return;
96
+ }
97
+ if (result.status === "outdated") {
98
+ console.log(
99
+ paint(
100
+ `[FAIL] ${label} Langfuse runtime is outdated: installed ${result.packageName}@${result.installedVersion}, latest ${result.packageName}@${result.latestVersion}`,
101
+ t.bold,
102
+ t.red,
103
+ ),
104
+ );
105
+ const fix =
106
+ result.packageName === "oh-aicoding-tool"
107
+ ? `npx oh-aicoding-tool@latest langfuse update ${result.target}`
108
+ : `npx oh-langfuse@latest update ${result.target}`;
109
+ console.log(paint(`[FIX] Run: ${fix}`, t.bold, t.cyan));
110
+ return;
111
+ }
112
+ console.log(paint(`[FAIL] ${label} Langfuse runtime record is missing.`, t.bold, t.red));
113
+ console.log(`[INFO] Runtime state: ${runtimeStatePath()}`);
114
+ console.log(paint(`[FIX] Run: npx oh-langfuse@latest update ${result.target}`, t.bold, t.cyan));
115
+ }
116
+
117
+ async function main() {
118
+ const args = parseArgs(process.argv.slice(2));
119
+ const targetArg = args._[0] || "all";
120
+ const state = readRuntimeState();
121
+ const runtimeTargets = state.targets || {};
122
+ const installed = detectInstalledTargets();
123
+ const { targets, skipped } = targetsToVerify(targetArg, installed, runtimeTargets);
124
+
125
+ for (const item of skipped) {
126
+ console.log(paint(`[SKIP] ${targetLabel(item.target)} not installed`, t.bold, t.gold));
127
+ }
128
+
129
+ if (!targets.length) {
130
+ console.log(paint("[FAIL] No installed Langfuse runtime targets were found.", t.bold, t.red));
131
+ console.log(paint("[FIX] Run setup or update first: npx oh-langfuse@latest update all", t.bold, t.cyan));
132
+ return 2;
133
+ }
134
+
135
+ let latestVersions = {};
136
+ try {
137
+ const packageNames = new Set(["oh-langfuse"]);
138
+ for (const target of targets) {
139
+ const packageName = String(runtimeTargets[target]?.packageName || "oh-langfuse").trim() || "oh-langfuse";
140
+ packageNames.add(packageName);
141
+ }
142
+ for (const packageName of packageNames) {
143
+ latestVersions[packageName] = await latestVersion(packageName, args.npmRegistry);
144
+ }
145
+ } catch (error) {
146
+ console.error(paint(`[FAIL] Could not query npm latest version: ${error.message}`, t.bold, t.red));
147
+ return 3;
148
+ }
149
+
150
+ const results = buildVerifyUpdateResults({ targets, runtimeTargets, latestVersions });
151
+ for (const result of results) printResult(result);
152
+ return verifyUpdateExitCode(results);
153
+ }
154
+
155
+ main()
156
+ .then((code) => {
157
+ process.exit(code);
158
+ })
159
+ .catch((error) => {
160
+ console.error(paint(`[FAIL] ${error?.message || String(error)}`, t.bold, t.red));
161
+ process.exit(3);
162
+ });
@@ -0,0 +1,60 @@
1
+ import { compareSemver } from "./update-utils.mjs";
2
+
3
+ export const VERIFY_TARGETS = ["claude", "opencode", "codex"];
4
+
5
+ export function targetLabel(target) {
6
+ if (target === "claude") return "Claude";
7
+ if (target === "opencode") return "OpenCode";
8
+ if (target === "codex") return "Codex";
9
+ return target;
10
+ }
11
+
12
+ export function normalizeVerifyTargets(target = "all") {
13
+ const normalized = String(target || "all").trim().toLowerCase();
14
+ if (normalized === "all") return [...VERIFY_TARGETS];
15
+ if (!VERIFY_TARGETS.includes(normalized)) {
16
+ throw new Error(`Unsupported verify-update target: ${target}`);
17
+ }
18
+ return [normalized];
19
+ }
20
+
21
+ export function buildVerifyUpdateResults({
22
+ targets = [],
23
+ runtimeTargets = {},
24
+ latestVersion = "",
25
+ latestVersions = {},
26
+ defaultPackageName = "oh-langfuse",
27
+ } = {}) {
28
+ return targets.map((target) => {
29
+ const record = runtimeTargets?.[target] || {};
30
+ const packageName = String(record.packageName || defaultPackageName).trim() || defaultPackageName;
31
+ const installedVersion = String(record.packageVersion || "").trim();
32
+ const packageLatestVersion = String(latestVersions[packageName] || latestVersion || "").trim();
33
+ const updatedAt = String(record.updatedAt || "").trim();
34
+ if (!installedVersion) {
35
+ return {
36
+ target,
37
+ packageName,
38
+ status: "missing",
39
+ installedVersion: "",
40
+ latestVersion: packageLatestVersion,
41
+ updatedAt,
42
+ };
43
+ }
44
+ const cmp = compareSemver(installedVersion, packageLatestVersion);
45
+ return {
46
+ target,
47
+ packageName,
48
+ status: cmp < 0 ? "outdated" : "current",
49
+ installedVersion,
50
+ latestVersion: packageLatestVersion,
51
+ updatedAt,
52
+ };
53
+ });
54
+ }
55
+
56
+ export function verifyUpdateExitCode(results = []) {
57
+ if (results.some((item) => item.status === "missing")) return 2;
58
+ if (results.some((item) => item.status === "outdated")) return 1;
59
+ return 0;
60
+ }