oh-langfuse 0.1.71 → 0.1.72

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.71",
3
+ "version": "0.1.72",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Use npm scripts to configure Claude Code / OpenCode / Codex with Langfuse tracing.",
@@ -16,6 +16,7 @@
16
16
  "scripts/auto-update-runtime.mjs",
17
17
  "scripts/codex-langfuse-check.mjs",
18
18
  "scripts/codex-langfuse-setup.mjs",
19
+ "scripts/cli-detection-utils.mjs",
19
20
  "scripts/json-utils.mjs",
20
21
  "scripts/langfuse-check.mjs",
21
22
  "scripts/langfuse-setup.mjs",
@@ -0,0 +1,114 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+
6
+ function unique(values) {
7
+ return [...new Set(values.filter(Boolean))];
8
+ }
9
+
10
+ function stripExecutableExtension(name) {
11
+ return String(name || "").replace(/\.(cmd|bat|exe)$/i, "");
12
+ }
13
+
14
+ function isTargetBinary(target, candidate) {
15
+ const base = stripExecutableExtension(path.basename(String(candidate || ""))).toLowerCase();
16
+ return base === String(target || "").toLowerCase();
17
+ }
18
+
19
+ function sortWindowsCliCandidates(candidates) {
20
+ const extPriority = (candidate) => {
21
+ const ext = path.extname(String(candidate || "")).toLowerCase();
22
+ if (ext === ".cmd" || ext === ".bat" || ext === ".exe") return 0;
23
+ return 1;
24
+ };
25
+ return [...candidates].sort((a, b) => extPriority(a) - extPriority(b));
26
+ }
27
+
28
+ export function parseWhereisBinaryOutput(target, stdout) {
29
+ const candidates = [];
30
+ for (const line of String(stdout || "").split(/\r?\n/)) {
31
+ const colon = line.indexOf(":");
32
+ const body = colon === -1 ? line : line.slice(colon + 1);
33
+ for (const token of body.split(/\s+/)) {
34
+ const candidate = token.trim();
35
+ if (candidate && path.isAbsolute(candidate) && isTargetBinary(target, candidate)) {
36
+ candidates.push(candidate);
37
+ }
38
+ }
39
+ }
40
+ return unique(candidates);
41
+ }
42
+
43
+ function isLikelyNodePrefix(name) {
44
+ return /^(node|nodejs)(?:[-_.].*)?$/i.test(String(name || ""));
45
+ }
46
+
47
+ export function listCommonUnixCliCandidates(target, roots = ["/usr/local", "/opt"]) {
48
+ if (process.platform === "win32") return [];
49
+ const candidates = [
50
+ path.join("/usr/local", "bin", target),
51
+ path.join("/usr", "bin", target),
52
+ path.join("/bin", target),
53
+ ];
54
+ for (const root of roots) {
55
+ try {
56
+ if (!fs.existsSync(root)) continue;
57
+ for (const ent of fs.readdirSync(root, { withFileTypes: true })) {
58
+ if (ent.isDirectory() && isLikelyNodePrefix(ent.name)) {
59
+ candidates.push(path.join(root, ent.name, "bin", target));
60
+ }
61
+ }
62
+ } catch {
63
+ // Best effort only; command lookups and other candidates may still work.
64
+ }
65
+ }
66
+ return unique(candidates);
67
+ }
68
+
69
+ export function listSystemCliCandidates(target) {
70
+ if (!target) return [];
71
+ const candidates = [];
72
+ if (process.platform === "win32") {
73
+ const result = spawnSync("where.exe", [target], { encoding: "utf8", windowsHide: true });
74
+ if (result.status === 0) {
75
+ candidates.push(...String(result.stdout || "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean));
76
+ }
77
+ return sortWindowsCliCandidates(unique(candidates));
78
+ }
79
+
80
+ const which = spawnSync("which", ["-a", target], { encoding: "utf8", windowsHide: true });
81
+ if (which.status === 0) {
82
+ candidates.push(...String(which.stdout || "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean));
83
+ }
84
+
85
+ const whereis = spawnSync("whereis", ["-b", target], { encoding: "utf8", windowsHide: true });
86
+ if (whereis.status === 0) {
87
+ candidates.push(...parseWhereisBinaryOutput(target, whereis.stdout));
88
+ }
89
+
90
+ candidates.push(...listCommonUnixCliCandidates(target));
91
+ return unique(candidates);
92
+ }
93
+
94
+ export function candidateExists(candidate) {
95
+ try {
96
+ return Boolean(candidate) && fs.existsSync(candidate);
97
+ } catch {
98
+ return false;
99
+ }
100
+ }
101
+
102
+ export function hasSystemCli(target) {
103
+ return listSystemCliCandidates(target).some(candidateExists);
104
+ }
105
+
106
+ export function defaultWindowsNpmCliCandidates(target) {
107
+ const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
108
+ if (process.platform !== "win32") return [];
109
+ return [
110
+ path.join(appData, "npm", `${target}.cmd`),
111
+ path.join(appData, "npm", `${target}.exe`),
112
+ path.join(appData, "npm", target),
113
+ ];
114
+ }
@@ -2,9 +2,10 @@ import fs from "node:fs";
2
2
  import fsp from "node:fs/promises";
3
3
  import path from "node:path";
4
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";
5
+ import { execFileSync, spawnSync } from "node:child_process";
6
+ import { fileURLToPath } from "node:url";
7
+ import { defaultWindowsNpmCliCandidates, listSystemCliCandidates } from "./cli-detection-utils.mjs";
8
+ import { writeRuntimeInstallRecord } from "./runtime-state-utils.mjs";
8
9
 
9
10
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
11
  const USER_ID_PATTERN = /^[a-z](?:\d{8}|wx\d{7})$/;
@@ -217,19 +218,11 @@ function listLocalCodexCliCandidates() {
217
218
  return candidates;
218
219
  }
219
220
 
220
- function listCliCandidatesFromPath(target) {
221
- const cmd = process.platform === "win32" ? "where.exe" : "which";
222
- const args = process.platform === "win32" ? [target] : ["-a", target];
223
- const result = spawnSync(cmd, args, { encoding: "utf8", windowsHide: true });
224
- if (result.status !== 0) return [];
225
- const candidates = String(result.stdout || "")
226
- .split(/\r?\n/)
227
- .map((line) => line.trim())
228
- .filter(Boolean);
229
- return process.platform === "win32" ? sortWindowsCliCandidates(candidates) : candidates;
230
- }
231
-
232
- function sortWindowsCliCandidates(candidates) {
221
+ function listCliCandidatesFromPath(target) {
222
+ return listSystemCliCandidates(target);
223
+ }
224
+
225
+ function sortWindowsCliCandidates(candidates) {
233
226
  const extPriority = (candidate) => {
234
227
  const ext = path.extname(String(candidate || "")).toLowerCase();
235
228
  if (ext === ".cmd" || ext === ".bat" || ext === ".exe") return 0;
@@ -238,16 +231,13 @@ function sortWindowsCliCandidates(candidates) {
238
231
  return [...candidates].sort((a, b) => extPriority(a) - extPriority(b));
239
232
  }
240
233
 
241
- function resolveAgentCli({ target, preferred = "", shimDir = "" }) {
242
- const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
243
- const candidates = [
244
- preferred,
245
- ...(target === "codex" ? listLocalCodexCliCandidates() : []),
246
- process.platform === "win32" ? path.join(appData, "npm", `${target}.cmd`) : "",
247
- process.platform === "win32" ? path.join(appData, "npm", `${target}.exe`) : "",
248
- process.platform === "win32" ? path.join(appData, "npm", target) : "",
249
- ...listCliCandidatesFromPath(target)
250
- ];
234
+ function resolveAgentCli({ target, preferred = "", shimDir = "" }) {
235
+ const candidates = [
236
+ preferred,
237
+ ...(target === "codex" ? listLocalCodexCliCandidates() : []),
238
+ ...defaultWindowsNpmCliCandidates(target),
239
+ ...listCliCandidatesFromPath(target)
240
+ ];
251
241
  for (const candidate of candidates) {
252
242
  const found = existingCliCandidate(candidate, shimDir);
253
243
  if (found) return found;
@@ -2,9 +2,10 @@ import fs from "node:fs";
2
2
  import fsp from "node:fs/promises";
3
3
  import path from "node:path";
4
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";
5
+ import { execFileSync, spawnSync } from "node:child_process";
6
+ import { fileURLToPath } from "node:url";
7
+ import { defaultWindowsNpmCliCandidates, listSystemCliCandidates } from "./cli-detection-utils.mjs";
8
+ import { writeRuntimeInstallRecord } from "./runtime-state-utils.mjs";
8
9
 
9
10
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
11
  const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
@@ -221,26 +222,16 @@ function existingCliCandidate(candidate, shimDir) {
221
222
  }
222
223
  }
223
224
 
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
- ];
225
+ function listCliCandidatesFromPath(target) {
226
+ return listSystemCliCandidates(target);
227
+ }
228
+
229
+ function resolveAgentCli({ target, preferred = "", shimDir = "" }) {
230
+ const candidates = [
231
+ preferred,
232
+ ...defaultWindowsNpmCliCandidates(target),
233
+ ...listCliCandidatesFromPath(target)
234
+ ];
244
235
  for (const candidate of candidates) {
245
236
  const found = existingCliCandidate(candidate, shimDir);
246
237
  if (found) return found;
@@ -1,7 +1,8 @@
1
1
  import fs from "node:fs";
2
- import path from "node:path";
3
- import os from "node:os";
4
- import { spawnSync } from "node:child_process";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import { spawnSync } from "node:child_process";
5
+ import { listSystemCliCandidates } from "./cli-detection-utils.mjs";
5
6
 
6
7
  /**
7
8
  * OpenCode CLI 的官方安装脚本默认会把二进制放到 ~/.opencode/bin;
@@ -36,10 +37,11 @@ export function resolveOpencodeCli(preferred) {
36
37
  candidates.push(path.join(process.env.APPDATA, "npm", "node_modules", "opencode-windows-x64-baseline", "bin", "opencode.exe"));
37
38
  }
38
39
  candidates.push(path.join(home, "scoop", "shims", "opencode.exe"));
39
- } else {
40
- candidates.push(path.join(home, ".opencode", "bin", "opencode"));
41
- candidates.push(path.join(home, ".local", "bin", "opencode"));
42
- }
40
+ } else {
41
+ candidates.push(path.join(home, ".opencode", "bin", "opencode"));
42
+ candidates.push(path.join(home, ".local", "bin", "opencode"));
43
+ candidates.push(...listSystemCliCandidates("opencode"));
44
+ }
43
45
 
44
46
  for (const c of candidates) {
45
47
  if (c && fs.existsSync(c)) return c;
@@ -1,10 +1,11 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
- import { spawnSync } from "node:child_process";
5
- import { fileURLToPath } from "node:url";
6
- import { buildUpdatePlan, extractVersionFromNpmMetadata } from "./update-utils.mjs";
7
- import { writeRuntimeInstallRecord } from "./runtime-state-utils.mjs";
4
+ import { spawnSync } from "node:child_process";
5
+ import { fileURLToPath } from "node:url";
6
+ import { hasSystemCli } from "./cli-detection-utils.mjs";
7
+ import { buildUpdatePlan, extractVersionFromNpmMetadata } from "./update-utils.mjs";
8
+ import { writeRuntimeInstallRecord } from "./runtime-state-utils.mjs";
8
9
 
9
10
  const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
11
  const packageJson = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
@@ -59,16 +60,21 @@ function readJsonIfExists(p) {
59
60
  }
60
61
  }
61
62
 
62
- function detectInstalledTargets(home = os.homedir()) {
63
- const codexHome = process.env.CODEX_HOME || path.join(home, ".codex");
64
- return {
65
- claude: fs.existsSync(path.join(home, ".claude", "hooks", "langfuse_hook.py")),
66
- opencode:
67
- fs.existsSync(path.join(home, ".config", "opencode", "opencode.json")) ||
68
- fs.existsSync(path.join(home, ".config", "opencode", "plugins", "opencode-plugin-langfuse")),
69
- codex: fs.existsSync(path.join(codexHome, "hooks", "codex_langfuse_notify.py")),
70
- };
71
- }
63
+ function detectInstalledTargets(home = os.homedir()) {
64
+ const codexHome = process.env.CODEX_HOME || path.join(home, ".codex");
65
+ return {
66
+ claude:
67
+ fs.existsSync(path.join(home, ".claude", "hooks", "langfuse_hook.py")) ||
68
+ hasSystemCli("claude"),
69
+ opencode:
70
+ fs.existsSync(path.join(home, ".config", "opencode", "opencode.json")) ||
71
+ fs.existsSync(path.join(home, ".config", "opencode", "plugins", "opencode-plugin-langfuse")) ||
72
+ hasSystemCli("opencode"),
73
+ codex:
74
+ fs.existsSync(path.join(codexHome, "hooks", "codex_langfuse_notify.py")) ||
75
+ hasSystemCli("codex"),
76
+ };
77
+ }
72
78
 
73
79
  function claudeConfig(home) {
74
80
  const settings = readJsonIfExists(path.join(home, ".claude", "settings.json")) || {};