oh-langfuse 0.1.78 → 0.1.80

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/SELF_VERIFY.md CHANGED
@@ -1,25 +1,25 @@
1
- # Real self verification
2
-
3
- `npm run self:verify` is an end-to-end validation path. It does not only inspect environment variables or local config files.
4
-
5
- The script:
6
-
7
- 1. Runs the real setup command for the selected target unless `--skip-install` is set.
8
- 2. Starts the real Claude Code / OpenCode / Codex CLI with a unique marker.
9
- 3. Polls Langfuse Public API with the configured public/secret key until that marker appears in traces or observations.
10
-
11
- Examples:
12
-
13
- ```bash
14
- npm run self:verify -- --targets=opencode --userId=h00613222
15
- npm run self:verify -- --targets=claude,opencode,codex --userId=h00613222
16
- ```
17
-
18
- Useful options:
19
-
20
- - `--opencodeCmd=<path>`, `--claudeCmd=<path>`, `--codexCmd=<path>`: use a specific CLI binary.
21
- - `--skip-install`: do not run setup before triggering the CLI.
22
- - `--skip-trigger`: only poll Langfuse for an existing marker.
23
- - `--timeoutMs=180000`: adjust Langfuse polling timeout.
24
-
25
- This command writes real user-level tool configuration and sends a real validation trace to Langfuse.
1
+ # Real self verification
2
+
3
+ `npm run self:verify` is an end-to-end validation path. It does not only inspect environment variables or local config files.
4
+
5
+ The script:
6
+
7
+ 1. Runs the real setup command for the selected target unless `--skip-install` is set.
8
+ 2. Starts the real Claude Code / OpenCode / Codex CLI with a unique marker.
9
+ 3. Polls Langfuse Public API with the configured public/secret key until that marker appears in traces or observations.
10
+
11
+ Examples:
12
+
13
+ ```bash
14
+ npm run self:verify -- --targets=opencode --userId=h00613222
15
+ npm run self:verify -- --targets=claude,opencode,codex --userId=h00613222
16
+ ```
17
+
18
+ Useful options:
19
+
20
+ - `--opencodeCmd=<path>`, `--claudeCmd=<path>`, `--codexCmd=<path>`: use a specific CLI binary.
21
+ - `--skip-install`: do not run setup before triggering the CLI.
22
+ - `--skip-trigger`: only poll Langfuse for an existing marker.
23
+ - `--timeoutMs=180000`: adjust Langfuse polling timeout.
24
+
25
+ This command writes real user-level tool configuration and sends a real validation trace to Langfuse.
package/bin/cli.js CHANGED
@@ -1,15 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from "fs";
3
3
  import path from "path";
4
- import readline from "readline";
5
- import { fileURLToPath } from "url";
6
- import { spawnSync } from "child_process";
7
- import { resolveOpencodeCli } from "../scripts/resolve-opencode-cli.mjs";
4
+ import readline from "readline";
5
+ import { fileURLToPath } from "url";
6
+ import { spawnSync } from "child_process";
7
+ import { resolveOpencodeCli } from "../scripts/resolve-opencode-cli.mjs";
8
8
 
9
9
  const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
10
  const scriptsDir = path.join(rootDir, "scripts");
11
11
 
12
- const DEFAULT_LANGFUSE_BASE_URL = "http://120.46.221.227:3000";
12
+ const DEFAULT_LANGFUSE_BASE_URL = "https://metrics.openharmonyinsight.cn";
13
+ const LEGACY_LANGFUSE_BASE_URLS = new Set([
14
+ "http://120.46.221.227:3000",
15
+ "https://120.46.221.227:3000",
16
+ ]);
13
17
  const DEFAULT_LANGFUSE_PUBLIC_KEY = "pk-lf-da0c90a7-6e93-4eb7-bb86-c1047c8d187d";
14
18
  const DEFAULT_LANGFUSE_SECRET_KEY = "sk-lf-0269b85d-bfdc-442c-bfa3-e737954e3315";
15
19
  const USER_ID_PATTERN = /^[a-z](?:\d{8}|wx\d{7})$/;
@@ -119,6 +123,11 @@ function hasValue(v) {
119
123
  return typeof v === "string" && v.trim().length > 0;
120
124
  }
121
125
 
126
+ function normalizeLegacyBaseUrl(baseUrl) {
127
+ const value = String(baseUrl || "").trim().replace(/\/+$/, "");
128
+ return LEGACY_LANGFUSE_BASE_URLS.has(value) ? DEFAULT_LANGFUSE_BASE_URL : baseUrl;
129
+ }
130
+
122
131
  function normalizeUserId(v) {
123
132
  return String(v || "").trim();
124
133
  }
@@ -160,13 +169,13 @@ function detectPython() {
160
169
  return { ok: false, command: null, version: "", pipOk: false, pipVersion: "" };
161
170
  }
162
171
 
163
- function detectOpencode() {
164
- const resolved = resolveOpencodeCli();
165
- const candidates = [resolved, ...(process.platform === "win32" ? ["opencode.cmd", "opencode"] : ["opencode"])].filter(Boolean);
166
- for (const cmd of candidates) {
167
- const r = commandExists(cmd, ["--version"]);
168
- if (r.ok) return { ok: true, command: cmd, version: r.stdout || r.stderr };
169
- }
172
+ function detectOpencode() {
173
+ const resolved = resolveOpencodeCli();
174
+ const candidates = [resolved, ...(process.platform === "win32" ? ["opencode.cmd", "opencode"] : ["opencode"])].filter(Boolean);
175
+ for (const cmd of candidates) {
176
+ const r = commandExists(cmd, ["--version"]);
177
+ if (r.ok) return { ok: true, command: cmd, version: r.stdout || r.stderr };
178
+ }
170
179
  return { ok: false, command: null, version: "" };
171
180
  }
172
181
 
@@ -508,13 +517,13 @@ async function askMultiChoice(rl, label, choices, options = {}) {
508
517
 
509
518
  function langfuseConfig(overrides = {}) {
510
519
  const config = {
511
- baseUrl: envDefault("LANGFUSE_BASEURL", envDefault("LANGFUSE_HOST", DEFAULT_LANGFUSE_BASE_URL)),
520
+ baseUrl: normalizeLegacyBaseUrl(envDefault("LANGFUSE_BASEURL", envDefault("LANGFUSE_HOST", DEFAULT_LANGFUSE_BASE_URL))),
512
521
  publicKey: envDefault("LANGFUSE_PUBLIC_KEY", DEFAULT_LANGFUSE_PUBLIC_KEY),
513
522
  secretKey: envDefault("LANGFUSE_SECRET_KEY", DEFAULT_LANGFUSE_SECRET_KEY),
514
523
  userId: ""
515
524
  };
516
525
  for (const [key, value] of Object.entries(overrides)) {
517
- if (hasValue(value)) config[key] = value;
526
+ if (hasValue(value)) config[key] = key === "baseUrl" ? normalizeLegacyBaseUrl(value) : value;
518
527
  }
519
528
  return config;
520
529
  }
@@ -745,6 +745,155 @@ def discover_known_skills(extra_roots: Optional[List[Path]] = None) -> set:
745
745
  return names
746
746
 
747
747
 
748
+ def collect_skill_roots(cwd: Path) -> list:
749
+ """Return absolute <dir>/skills paths that contain SKILL.md, found among
750
+ cwd itself, cwd's children, and cwd's siblings. Mirrors the OpenCode
751
+ opencode-skills-discover.mjs collectSkillRoots so skills cloned near the
752
+ working directory can be linked into the native skills dir."""
753
+ def _has_skill(d: Path) -> bool:
754
+ try:
755
+ if not d.is_dir():
756
+ return False
757
+ for entry in d.iterdir():
758
+ if entry.name == "SKILL.md" and entry.is_file():
759
+ return True
760
+ if entry.is_dir() and not entry.name.startswith(".") and (entry / "SKILL.md").exists():
761
+ return True
762
+ except Exception:
763
+ pass
764
+ return False
765
+
766
+ candidates = []
767
+ for sub in (Path("skills"), Path(".opencode") / "skills", Path(".opencode") / "skill"):
768
+ candidates.append(cwd / sub)
769
+ try:
770
+ for entry in cwd.iterdir():
771
+ if entry.is_dir() and not entry.name.startswith("."):
772
+ candidates.append(entry / "skills")
773
+ except Exception:
774
+ pass
775
+ parent = cwd.parent
776
+ if parent != cwd:
777
+ try:
778
+ for entry in parent.iterdir():
779
+ if entry.is_dir() and entry.name != cwd.name and not entry.name.startswith("."):
780
+ candidates.append(entry / "skills")
781
+ except Exception:
782
+ pass
783
+
784
+ roots = []
785
+ seen = set()
786
+ for d in candidates:
787
+ try:
788
+ if _has_skill(d):
789
+ key = str(d.resolve())
790
+ if key not in seen:
791
+ seen.add(key)
792
+ roots.append(d.resolve())
793
+ except Exception:
794
+ continue
795
+ return roots
796
+
797
+
798
+ def _skill_link_exists(link: Path) -> bool:
799
+ try:
800
+ return link.exists() or link.is_symlink()
801
+ except Exception:
802
+ return False
803
+
804
+
805
+ def _create_skill_link(target: Path, link: Path) -> bool:
806
+ """Create a junction (Windows, no admin) or symlink (Unix) at link -> target.
807
+ Returns False if link already exists or creation failed.
808
+
809
+ On Windows we always use a directory junction (mklink /J): Codex follows
810
+ junctions but does not follow true symlinks for skill discovery, while
811
+ Claude Code follows both. Junctions do not require admin/developer mode."""
812
+ if _skill_link_exists(link):
813
+ return False
814
+ try:
815
+ link.parent.mkdir(parents=True, exist_ok=True)
816
+ except Exception:
817
+ pass
818
+ if sys.platform == "win32":
819
+ try:
820
+ r = subprocess.run(
821
+ ["cmd", "/c", "mklink", "/J", str(link), str(target)],
822
+ capture_output=True,
823
+ )
824
+ return r.returncode == 0
825
+ except Exception:
826
+ return False
827
+ try:
828
+ os.symlink(str(target), str(link), target_is_directory=True)
829
+ return True
830
+ except OSError:
831
+ return False
832
+
833
+
834
+ def _remove_skill_link(link: Path) -> None:
835
+ """Remove a junction/symlink we created, without touching the target."""
836
+ try:
837
+ if sys.platform == "win32":
838
+ subprocess.run(["cmd", "/c", "rmdir", str(link)], capture_output=True)
839
+ else:
840
+ os.unlink(str(link))
841
+ except Exception:
842
+ pass
843
+
844
+
845
+ def link_nearby_skills(cwd: Path, native_dir: Path, state_file: Path) -> int:
846
+ """Symlink nearby repo skills into native_dir so the agent discovers them on
847
+ the next turn/session. Idempotent; skips names already present (native installs
848
+ or prior links). Cleans stale links we created whose target is gone. Failures
849
+ are swallowed so the metrics hook never breaks. Returns newly-linked count."""
850
+ try:
851
+ native_dir.mkdir(parents=True, exist_ok=True)
852
+ except Exception:
853
+ return 0
854
+ state = {}
855
+ try:
856
+ if state_file.exists():
857
+ state = json.loads(state_file.read_text("utf-8") or "{}")
858
+ except Exception:
859
+ state = {}
860
+ for link_str, target_str in list(state.items()):
861
+ link = Path(link_str)
862
+ target = Path(target_str)
863
+ try:
864
+ if _skill_link_exists(link):
865
+ if not target.exists():
866
+ _remove_skill_link(link)
867
+ state.pop(link_str, None)
868
+ else:
869
+ state.pop(link_str, None)
870
+ except Exception:
871
+ pass
872
+ linked = 0
873
+ for repo_skills_dir in collect_skill_roots(cwd):
874
+ try:
875
+ for skill_dir in repo_skills_dir.iterdir():
876
+ if not skill_dir.is_dir() or skill_dir.name.startswith("."):
877
+ continue
878
+ if not (skill_dir / "SKILL.md").exists():
879
+ continue
880
+ try:
881
+ target = skill_dir.resolve()
882
+ except Exception:
883
+ continue
884
+ link_path = native_dir / skill_dir.name
885
+ if _create_skill_link(target, link_path):
886
+ state[str(link_path)] = str(target)
887
+ linked += 1
888
+ except Exception:
889
+ pass
890
+ try:
891
+ state_file.write_text(json.dumps(state, ensure_ascii=False), "utf-8")
892
+ except Exception:
893
+ pass
894
+ return linked
895
+
896
+
748
897
  def _skill_namespace(name: str) -> str:
749
898
  return name.split(":", 1)[0] if ":" in name else ""
750
899
 
@@ -1377,7 +1526,12 @@ def emit_codex_turn(
1377
1526
  model = first_string(turn.model, meta.get("model"), meta.get("model_provider")) or "codex"
1378
1527
  tool_calls = material.get("tool_calls") or []
1379
1528
  tool_results = material.get("tool_results") or []
1380
- skill_usages = detect_turn_skill_usages(material, discover_known_skills())
1529
+ cwd = Path.cwd()
1530
+ try:
1531
+ known_skills = discover_known_skills(collect_skill_roots(cwd))
1532
+ except Exception:
1533
+ known_skills = discover_known_skills()
1534
+ skill_usages = detect_turn_skill_usages(material, known_skills)
1381
1535
  interaction_id = build_interaction_id("codex", session_id, turn_num)
1382
1536
  skill_use_events = build_skill_use_events(interaction_id, skill_usages)
1383
1537
  interaction_meta = build_interaction_metadata(
@@ -1516,7 +1670,7 @@ def main() -> int:
1516
1670
  or os.environ.get("LANGFUSE_BASEURL")
1517
1671
  or os.environ.get("LANGFUSE_HOST")
1518
1672
  or config.get("baseUrl")
1519
- or "https://cloud.langfuse.com"
1673
+ or "https://metrics.openharmonyinsight.cn"
1520
1674
  )
1521
1675
  user_id = (
1522
1676
  find_value(payload, ("user_id", "userId", "username", "userName"))
@@ -1595,6 +1749,12 @@ def main() -> int:
1595
1749
  langfuse.flush()
1596
1750
  except Exception:
1597
1751
  pass
1752
+ # Best-effort: link nearby repo skills into ~/.codex/skills. Runs
1753
+ # AFTER the Langfuse flush so it can never block or break metrics.
1754
+ try:
1755
+ link_nearby_skills(Path.cwd(), CODEX_DIR / "skills", CODEX_DIR / "skills" / ".oh-linked.json")
1756
+ except Exception:
1757
+ pass
1598
1758
  log("INFO", f"Processed {uploaded} Codex turn(s), total {ss.turn_count} for {session_path}")
1599
1759
  return 0
1600
1760
  except Exception as e:
package/langfuse_hook.py CHANGED
@@ -626,6 +626,156 @@ def discover_known_skills(extra_roots: Optional[List[Path]] = None) -> set:
626
626
  continue
627
627
  return names
628
628
 
629
+
630
+ def collect_skill_roots(cwd: Path) -> list:
631
+ """Return absolute <dir>/skills paths that contain SKILL.md, found among
632
+ cwd itself, cwd's children, and cwd's siblings. Mirrors the OpenCode
633
+ opencode-skills-discover.mjs collectSkillRoots so skills cloned near the
634
+ working directory can be linked into the native skills dir."""
635
+ def _has_skill(d: Path) -> bool:
636
+ try:
637
+ if not d.is_dir():
638
+ return False
639
+ for entry in d.iterdir():
640
+ if entry.name == "SKILL.md" and entry.is_file():
641
+ return True
642
+ if entry.is_dir() and not entry.name.startswith(".") and (entry / "SKILL.md").exists():
643
+ return True
644
+ except Exception:
645
+ pass
646
+ return False
647
+
648
+ candidates = []
649
+ for sub in (Path("skills"), Path(".opencode") / "skills", Path(".opencode") / "skill"):
650
+ candidates.append(cwd / sub)
651
+ try:
652
+ for entry in cwd.iterdir():
653
+ if entry.is_dir() and not entry.name.startswith("."):
654
+ candidates.append(entry / "skills")
655
+ except Exception:
656
+ pass
657
+ parent = cwd.parent
658
+ if parent != cwd:
659
+ try:
660
+ for entry in parent.iterdir():
661
+ if entry.is_dir() and entry.name != cwd.name and not entry.name.startswith("."):
662
+ candidates.append(entry / "skills")
663
+ except Exception:
664
+ pass
665
+
666
+ roots = []
667
+ seen = set()
668
+ for d in candidates:
669
+ try:
670
+ if _has_skill(d):
671
+ key = str(d.resolve())
672
+ if key not in seen:
673
+ seen.add(key)
674
+ roots.append(d.resolve())
675
+ except Exception:
676
+ continue
677
+ return roots
678
+
679
+
680
+ def _skill_link_exists(link: Path) -> bool:
681
+ try:
682
+ return link.exists() or link.is_symlink()
683
+ except Exception:
684
+ return False
685
+
686
+
687
+ def _create_skill_link(target: Path, link: Path) -> bool:
688
+ """Create a junction (Windows, no admin) or symlink (Unix) at link -> target.
689
+ Returns False if link already exists or creation failed.
690
+
691
+ On Windows we always use a directory junction (mklink /J): Codex follows
692
+ junctions but does not follow true symlinks for skill discovery, while
693
+ Claude Code follows both. Junctions do not require admin/developer mode."""
694
+ if _skill_link_exists(link):
695
+ return False
696
+ try:
697
+ link.parent.mkdir(parents=True, exist_ok=True)
698
+ except Exception:
699
+ pass
700
+ if sys.platform == "win32":
701
+ try:
702
+ r = subprocess.run(
703
+ ["cmd", "/c", "mklink", "/J", str(link), str(target)],
704
+ capture_output=True,
705
+ )
706
+ return r.returncode == 0
707
+ except Exception:
708
+ return False
709
+ try:
710
+ os.symlink(str(target), str(link), target_is_directory=True)
711
+ return True
712
+ except OSError:
713
+ return False
714
+
715
+
716
+ def _remove_skill_link(link: Path) -> None:
717
+ """Remove a junction/symlink we created, without touching the target."""
718
+ try:
719
+ if sys.platform == "win32":
720
+ subprocess.run(["cmd", "/c", "rmdir", str(link)], capture_output=True)
721
+ else:
722
+ os.unlink(str(link))
723
+ except Exception:
724
+ pass
725
+
726
+
727
+ def link_nearby_skills(cwd: Path, native_dir: Path, state_file: Path) -> int:
728
+ """Symlink nearby repo skills into native_dir so the agent discovers them on
729
+ the next turn/session. Idempotent; skips names already present (native installs
730
+ or prior links). Cleans stale links we created whose target is gone. Failures
731
+ are swallowed so the metrics hook never breaks. Returns newly-linked count."""
732
+ try:
733
+ native_dir.mkdir(parents=True, exist_ok=True)
734
+ except Exception:
735
+ return 0
736
+ state = {}
737
+ try:
738
+ if state_file.exists():
739
+ state = json.loads(state_file.read_text("utf-8") or "{}")
740
+ except Exception:
741
+ state = {}
742
+ for link_str, target_str in list(state.items()):
743
+ link = Path(link_str)
744
+ target = Path(target_str)
745
+ try:
746
+ if _skill_link_exists(link):
747
+ if not target.exists():
748
+ _remove_skill_link(link)
749
+ state.pop(link_str, None)
750
+ else:
751
+ state.pop(link_str, None)
752
+ except Exception:
753
+ pass
754
+ linked = 0
755
+ for repo_skills_dir in collect_skill_roots(cwd):
756
+ try:
757
+ for skill_dir in repo_skills_dir.iterdir():
758
+ if not skill_dir.is_dir() or skill_dir.name.startswith("."):
759
+ continue
760
+ if not (skill_dir / "SKILL.md").exists():
761
+ continue
762
+ try:
763
+ target = skill_dir.resolve()
764
+ except Exception:
765
+ continue
766
+ link_path = native_dir / skill_dir.name
767
+ if _create_skill_link(target, link_path):
768
+ state[str(link_path)] = str(target)
769
+ linked += 1
770
+ except Exception:
771
+ pass
772
+ try:
773
+ state_file.write_text(json.dumps(state, ensure_ascii=False), "utf-8")
774
+ except Exception:
775
+ pass
776
+ return linked
777
+
778
+
629
779
  def _skill_namespace(name: str) -> str:
630
780
  return name.split(":", 1)[0] if ":" in name else ""
631
781
 
@@ -1067,7 +1217,12 @@ def emit_turn(
1067
1217
  usage_details = get_usage(last_assistant)
1068
1218
 
1069
1219
  tool_calls = _tool_calls_from_assistants(turn.assistant_msgs)
1070
- skill_usages = detect_turn_skill_usages(turn, tool_calls, discover_known_skills())
1220
+ cwd = Path.cwd()
1221
+ try:
1222
+ known_skills = discover_known_skills(collect_skill_roots(cwd))
1223
+ except Exception:
1224
+ known_skills = discover_known_skills()
1225
+ skill_usages = detect_turn_skill_usages(turn, tool_calls, known_skills)
1071
1226
  interaction_id = build_interaction_id("claude", session_id, turn_num)
1072
1227
  skill_use_events = build_skill_use_events(interaction_id, skill_usages)
1073
1228
  interaction_meta = build_interaction_metadata(
@@ -1173,6 +1328,7 @@ def emit_turn(
1173
1328
 
1174
1329
  trace_span.update(output={"role": "assistant", "content": assistant_text})
1175
1330
 
1331
+
1176
1332
  # ----------------- Main -----------------
1177
1333
  def main() -> int:
1178
1334
  start = time.time()
@@ -1183,7 +1339,7 @@ def main() -> int:
1183
1339
 
1184
1340
  public_key = os.environ.get("CC_LANGFUSE_PUBLIC_KEY") or os.environ.get("LANGFUSE_PUBLIC_KEY")
1185
1341
  secret_key = os.environ.get("CC_LANGFUSE_SECRET_KEY") or os.environ.get("LANGFUSE_SECRET_KEY")
1186
- host = os.environ.get("CC_LANGFUSE_BASE_URL") or os.environ.get("LANGFUSE_BASEURL") or "https://cloud.langfuse.com"
1342
+ host = os.environ.get("CC_LANGFUSE_BASE_URL") or os.environ.get("LANGFUSE_BASEURL") or "https://metrics.openharmonyinsight.cn"
1187
1343
 
1188
1344
  if not public_key or not secret_key:
1189
1345
  warn("Missing Langfuse public/secret key in hook environment; exiting.")
@@ -1249,6 +1405,13 @@ def main() -> int:
1249
1405
  warn(f"Langfuse flush failed: {e}")
1250
1406
  pass
1251
1407
 
1408
+ # Best-effort: link nearby repo skills into ~/.claude/skills. Runs
1409
+ # AFTER the Langfuse flush so it can never block or break metrics.
1410
+ try:
1411
+ link_nearby_skills(Path.cwd(), Path.home() / ".claude" / "skills", Path.home() / ".claude" / "skills" / ".oh-linked.json")
1412
+ except Exception:
1413
+ pass
1414
+
1252
1415
  dur = time.time() - start
1253
1416
  info(f"Processed {emitted} turns in {dur:.2f}s (session={session_id})")
1254
1417
  return 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-langfuse",
3
- "version": "0.1.78",
3
+ "version": "0.1.80",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Use npm scripts to configure Claude Code / OpenCode / Codex with Langfuse tracing.",
@@ -23,6 +23,7 @@
23
23
  "scripts/opencode-langfuse-check.mjs",
24
24
  "scripts/opencode-langfuse-run.mjs",
25
25
  "scripts/opencode-langfuse-setup.mjs",
26
+ "scripts/opencode-skills-discover.mjs",
26
27
  "scripts/resolve-opencode-cli.mjs",
27
28
  "scripts/real-self-verify.mjs",
28
29
  "scripts/log-filter-utils.mjs",
@@ -6,10 +6,10 @@ import { fileURLToPath } from "node:url";
6
6
  import { extractVersionFromNpmMetadata, isNewerVersion } from "./update-utils.mjs";
7
7
  import { getRuntimeInstallRecord } from "./runtime-state-utils.mjs";
8
8
 
9
- const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
- const packageJson = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
11
- const ALLOWED_TARGETS = new Set(["claude", "opencode", "codex"]);
12
- const OFFICIAL_NPM_REGISTRY = "https://registry.npmjs.org/";
9
+ const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
+ const packageJson = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
11
+ const ALLOWED_TARGETS = new Set(["claude", "opencode", "codex"]);
12
+ const OFFICIAL_NPM_REGISTRY = "https://registry.npmjs.org/";
13
13
 
14
14
  const colorEnabled = process.stdout.isTTY && process.env.NO_COLOR !== "1";
15
15
  const ansi = (code) => (colorEnabled ? `\x1b[${code}m` : "");
@@ -40,7 +40,7 @@ function parseArgs(argv) {
40
40
  return args;
41
41
  }
42
42
 
43
- async function latestVersion(packageName, registry = OFFICIAL_NPM_REGISTRY) {
43
+ async function latestVersion(packageName, registry = OFFICIAL_NPM_REGISTRY) {
44
44
  const base = registry.replace(/\/+$/, "");
45
45
  const response = await fetch(`${base}/${packageName}`, { headers: { accept: "application/json" } });
46
46
  if (!response.ok) throw new Error(`npm registry ${response.status} ${response.statusText}`);
@@ -106,15 +106,15 @@ function printAlreadyCurrent(target, packageName, version) {
106
106
  console.log(paint(`[OK] ${label} Langfuse \u5df2\u662f\u6700\u65b0\uff1a${packageName}@${version}`, t.bold, t.green));
107
107
  }
108
108
 
109
- function updateCommandForPackage(packageName, target) {
110
- if (packageName === "oh-aicoding-tool") return `npx --registry=${OFFICIAL_NPM_REGISTRY} oh-aicoding-tool@latest langfuse update ${target}`;
111
- return `npx --registry=${OFFICIAL_NPM_REGISTRY} ${packageName}@latest update ${target}`;
112
- }
113
-
114
- function updateArgsForPackage(packageName, target) {
115
- if (packageName === "oh-aicoding-tool") return ["-y", `--registry=${OFFICIAL_NPM_REGISTRY}`, "oh-aicoding-tool@latest", "langfuse", "update", target];
116
- return ["-y", `--registry=${OFFICIAL_NPM_REGISTRY}`, `${packageName}@latest`, "update", target];
117
- }
109
+ function updateCommandForPackage(packageName, target) {
110
+ if (packageName === "oh-aicoding-tool") return `npx --registry=${OFFICIAL_NPM_REGISTRY} oh-aicoding-tool@latest langfuse update ${target}`;
111
+ return `npx --registry=${OFFICIAL_NPM_REGISTRY} ${packageName}@latest update ${target}`;
112
+ }
113
+
114
+ function updateArgsForPackage(packageName, target) {
115
+ if (packageName === "oh-aicoding-tool") return ["-y", `--registry=${OFFICIAL_NPM_REGISTRY}`, "oh-aicoding-tool@latest", "langfuse", "update", target];
116
+ return ["-y", `--registry=${OFFICIAL_NPM_REGISTRY}`, `${packageName}@latest`, "update", target];
117
+ }
118
118
 
119
119
  function runUpdate(target, packageName, args) {
120
120
  const updateArgs = updateArgsForPackage(packageName, target);