oh-langfuse 0.1.20 → 0.1.22

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` 直接命令,方便在用户机器上安装、修复和校验配置。
4
4
 
5
- 当前 npm 版本:`0.1.20`
5
+ 当前 npm 版本:`0.1.22`
6
6
 
7
7
  ## 能做什么
8
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-langfuse",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Use npm scripts to configure Claude Code / OpenCode / Codex with Langfuse tracing.",
@@ -10,18 +10,26 @@
10
10
  },
11
11
  "files": [
12
12
  "bin",
13
- "scripts",
13
+ "scripts/codex-langfuse-check.mjs",
14
+ "scripts/codex-langfuse-setup.mjs",
15
+ "scripts/json-utils.mjs",
16
+ "scripts/langfuse-check.mjs",
17
+ "scripts/langfuse-setup.mjs",
18
+ "scripts/opencode-langfuse-check.mjs",
19
+ "scripts/opencode-langfuse-run.mjs",
20
+ "scripts/opencode-langfuse-setup.mjs",
21
+ "scripts/resolve-opencode-cli.mjs",
14
22
  "langfuse_hook.py",
15
- "codex_langfuse_notify.py",
16
- "README.md",
17
- "CODEX_LANGFUSE_PLAN.md",
23
+ "codex_langfuse_notify.py",
24
+ "README.md",
25
+ "CODEX_LANGFUSE_PLAN.md",
18
26
  "setup-langfuse.bat",
19
27
  "setup-langfuse.sh"
20
28
  ],
21
29
  "scripts": {
22
30
  "start": "node bin/cli.js",
23
- "check": "node --check bin/cli.js",
24
- "pack:check": "npm pack --dry-run",
31
+ "check": "node --check bin/cli.js",
32
+ "pack:check": "npm pack --dry-run",
25
33
  "claude:setup": "node scripts/langfuse-setup.mjs",
26
34
  "claude:check": "node scripts/langfuse-check.mjs",
27
35
  "langfuse:setup": "node scripts/langfuse-setup.mjs",
@@ -14,6 +14,18 @@ function addResult(results, item, ok, detail, fix = "") {
14
14
  results.push({ item, ok, detail, fix });
15
15
  }
16
16
 
17
+ function pythonExecutableInVenv(claudeDir) {
18
+ return process.platform === "win32"
19
+ ? path.join(claudeDir, "langfuse-venv", "Scripts", "python.exe")
20
+ : path.join(claudeDir, "langfuse-venv", "bin", "python");
21
+ }
22
+
23
+ function launcherPath(hooksDir) {
24
+ return process.platform === "win32"
25
+ ? path.join(hooksDir, "run-langfuse-hook.cmd")
26
+ : path.join(hooksDir, "run-langfuse-hook.sh");
27
+ }
28
+
17
29
  function findLangfuseHook(settings) {
18
30
  const stopHooks = settings?.hooks?.Stop;
19
31
  if (!Array.isArray(stopHooks)) return null;
@@ -25,16 +37,14 @@ function findLangfuseHook(settings) {
25
37
  const command = typeof h.command === "string" ? h.command : "";
26
38
  const args = Array.isArray(h.args) ? h.args : [];
27
39
  const joined = [command, ...args].join(" ");
28
- if (joined.includes("langfuse_hook.py")) return { command, args };
40
+ if (joined.includes("langfuse_hook.py") || joined.includes("run-langfuse-hook")) {
41
+ return { command, args };
42
+ }
29
43
  }
30
44
  }
31
45
  return null;
32
46
  }
33
47
 
34
- function hookUsesExecForm(hook) {
35
- return !!(hook?.command && Array.isArray(hook.args) && hook.args.some((arg) => String(arg).includes("langfuse_hook.py")));
36
- }
37
-
38
48
  function pathExists(p) {
39
49
  return typeof p === "string" && p.length > 0 && fs.existsSync(p);
40
50
  }
@@ -56,15 +66,17 @@ function main() {
56
66
  const claudeDir = path.join(userHome, ".claude");
57
67
  const hooksDir = path.join(claudeDir, "hooks");
58
68
  const settingsPath = path.join(claudeDir, "settings.json");
69
+ const pyPath = path.join(hooksDir, "langfuse_hook.py");
70
+ const expectedLauncher = launcherPath(hooksDir);
71
+ const expectedPython = pythonExecutableInVenv(claudeDir);
59
72
  const logPath = path.join(claudeDir, "state", "langfuse_hook.log");
60
73
 
61
74
  const results = [];
62
75
 
63
76
  addResult(results, "Claude config dir", fs.existsSync(claudeDir), claudeDir, "Run: npx oh-langfuse@latest setup claude");
64
77
  addResult(results, "hooks dir", fs.existsSync(hooksDir), hooksDir, "Run setup again to install the hook script.");
65
-
66
- const pyPath = path.join(hooksDir, "langfuse_hook.py");
67
78
  addResult(results, "hook script", fs.existsSync(pyPath), pyPath, "Run setup again to copy langfuse_hook.py.");
79
+ addResult(results, "hook launcher", fs.existsSync(expectedLauncher), expectedLauncher, "Run setup again to create the launcher script.");
68
80
 
69
81
  const settings = readJsonIfExists(settingsPath);
70
82
  addResult(results, "settings.json", !!settings, settingsPath, "Run setup again to write Claude settings.");
@@ -78,7 +90,6 @@ function main() {
78
90
  "CC_LANGFUSE_BASE_URL",
79
91
  "LANGFUSE_BASEURL"
80
92
  ];
81
-
82
93
  const missingEnv = neededEnv.filter((k) => !env || typeof env[k] === "undefined" || env[k] === "");
83
94
  addResult(
84
95
  results,
@@ -99,42 +110,31 @@ function main() {
99
110
  const hook = findLangfuseHook(settings);
100
111
  addResult(results, "hooks.Stop contains Langfuse", !!hook, hook ? "OK" : "missing", "Run setup again to register the Stop hook.");
101
112
 
102
- const execFormOk = hookUsesExecForm(hook);
113
+ const launcherFormOk = !!hook && path.normalize(hook.command) === path.normalize(expectedLauncher) && !hook.args?.length;
103
114
  addResult(
104
115
  results,
105
116
  "hook command form",
106
- process.platform === "win32" ? execFormOk : !!hook,
107
- execFormOk ? "exec form command + args" : hook ? "legacy shell-form command" : "missing",
108
- process.platform === "win32"
109
- ? "Run setup again. Windows should use command plus args so paths are not parsed by Git Bash/PowerShell."
110
- : "Run setup again to refresh the hook command."
117
+ launcherFormOk,
118
+ launcherFormOk ? "launcher command" : hook ? "legacy command form" : "missing",
119
+ "Run setup again. Claude Code should execute the generated run-langfuse-hook launcher directly."
111
120
  );
112
121
 
113
- if (hook) {
114
- addResult(
115
- results,
116
- "hook python executable",
117
- pathExists(hook.command),
118
- hook.command || "missing",
119
- "Run setup again to recreate the Python venv and write an absolute python path."
120
- );
121
- const hookScriptPath = execFormOk ? String(hook.args.find((arg) => String(arg).includes("langfuse_hook.py")) || "") : pyPath;
122
- addResult(
123
- results,
124
- "hook args script",
125
- pathExists(hookScriptPath),
126
- hookScriptPath || "missing",
127
- "Run setup again so the hook args point at langfuse_hook.py."
128
- );
129
- const importCheck = checkPythonLangfuseImport(hook.command);
130
- addResult(
131
- results,
132
- "python package langfuse",
133
- importCheck.ok,
134
- importCheck.detail,
135
- `Run: ${hook.command} -m pip install -U langfuse`
136
- );
137
- }
122
+ addResult(
123
+ results,
124
+ "hook python executable",
125
+ pathExists(expectedPython),
126
+ expectedPython,
127
+ "Run setup again to recreate the Python venv."
128
+ );
129
+
130
+ const importCheck = checkPythonLangfuseImport(expectedPython);
131
+ addResult(
132
+ results,
133
+ "python package langfuse",
134
+ importCheck.ok,
135
+ importCheck.detail,
136
+ `Run: ${expectedPython} -m pip install -U langfuse`
137
+ );
138
138
 
139
139
  addResult(results, "hook log path", true, logPath);
140
140
 
@@ -127,15 +127,46 @@ function normalizeWinPathForClaude(p) {
127
127
  return p.replace(/\\/g, "/");
128
128
  }
129
129
 
130
- function quoteCommandArg(s) {
131
- return `"${String(s).replace(/"/g, '\\"')}"`;
132
- }
133
-
134
- function pythonExecutableInVenv(venvDir) {
135
- return process.platform === "win32"
136
- ? path.join(venvDir, "Scripts", "python.exe")
137
- : path.join(venvDir, "bin", "python");
138
- }
130
+ function quoteCommandArg(s) {
131
+ return `"${String(s).replace(/"/g, '\\"')}"`;
132
+ }
133
+
134
+ function cmdQuote(s) {
135
+ return `"${String(s).replace(/"/g, '""')}"`;
136
+ }
137
+
138
+ function shQuote(s) {
139
+ return `'${String(s).replace(/'/g, "'\\''")}'`;
140
+ }
141
+
142
+ function pythonExecutableInVenv(venvDir) {
143
+ return process.platform === "win32"
144
+ ? path.join(venvDir, "Scripts", "python.exe")
145
+ : path.join(venvDir, "bin", "python");
146
+ }
147
+
148
+ async function createHookLauncher({ hooksDir, hookPython, pyPath }) {
149
+ if (process.platform === "win32") {
150
+ const launcher = path.join(hooksDir, "run-langfuse-hook.cmd");
151
+ const content = [
152
+ "@echo off",
153
+ `${cmdQuote(hookPython)} ${cmdQuote(pyPath)}`,
154
+ ""
155
+ ].join(os.EOL);
156
+ await fsp.writeFile(launcher, content, "utf8");
157
+ return launcher;
158
+ }
159
+
160
+ const launcher = path.join(hooksDir, "run-langfuse-hook.sh");
161
+ const content = [
162
+ "#!/usr/bin/env sh",
163
+ `exec ${shQuote(hookPython)} ${shQuote(pyPath)}`,
164
+ ""
165
+ ].join("\n");
166
+ await fsp.writeFile(launcher, content, "utf8");
167
+ fs.chmodSync(launcher, 0o755);
168
+ return launcher;
169
+ }
139
170
 
140
171
  function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.tsinghua.edu.cn/simple" }) {
141
172
  const venvDir = path.join(baseDir, "langfuse-venv");
@@ -241,12 +272,13 @@ async function main() {
241
272
  const nextPyText = setOrReplaceUserId(pyText, userId);
242
273
  if (nextPyText !== pyText) {
243
274
  await fsp.writeFile(pyPath, nextPyText, "utf8");
244
- }
275
+ }
245
276
  const hookPython = createOrUpdateLangfuseVenv({ baseDir: claudeDir, pipIndexUrl });
246
-
247
- // 4) 合并写入 settings.json
248
- const settingsPath = path.join(claudeDir, "settings.json");
249
- const existing = readJsonIfExists(settingsPath) ?? {};
277
+ const hookLauncher = await createHookLauncher({ hooksDir, hookPython, pyPath });
278
+
279
+ // 4) 合并写入 settings.json
280
+ const settingsPath = path.join(claudeDir, "settings.json");
281
+ const existing = readJsonIfExists(settingsPath) ?? {};
250
282
 
251
283
  const desired = {
252
284
  env: {
@@ -267,8 +299,7 @@ async function main() {
267
299
  hooks: [
268
300
  {
269
301
  type: "command",
270
- command: hookPython,
271
- args: [pyPath]
302
+ command: hookLauncher
272
303
  }
273
304
  ]
274
305
  }
@@ -1,414 +0,0 @@
1
- import fs from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import crypto from "node:crypto";
5
- import { spawnSync } from "node:child_process";
6
- import { fileURLToPath } from "node:url";
7
- import { resolveOpencodeCli } from "./resolve-opencode-cli.mjs";
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
-
12
- const DEFAULT_LANGFUSE_BASE_URL = "http://120.46.221.227:3000";
13
- const DEFAULT_LANGFUSE_PUBLIC_KEY = "pk-lf-da0c90a7-6e93-4eb7-bb86-c1047c8d187d";
14
- const DEFAULT_LANGFUSE_SECRET_KEY = "sk-lf-0269b85d-bfdc-442c-bfa3-e737954e3315";
15
-
16
- function parseArgs(argv) {
17
- const args = { _: [] };
18
- for (let i = 0; i < argv.length; i += 1) {
19
- const raw = argv[i];
20
- if (!raw.startsWith("--")) {
21
- args._.push(raw);
22
- continue;
23
- }
24
- const eq = raw.indexOf("=");
25
- if (eq !== -1) {
26
- args[raw.slice(2, eq)] = raw.slice(eq + 1);
27
- continue;
28
- }
29
- const key = raw.slice(2);
30
- const next = argv[i + 1];
31
- if (next && !next.startsWith("--")) {
32
- args[key] = next;
33
- i += 1;
34
- } else {
35
- args[key] = true;
36
- }
37
- }
38
- return args;
39
- }
40
-
41
- function printHelp() {
42
- console.log(`${packageJson.name} real self verification`);
43
- console.log("");
44
- console.log("Usage:");
45
- console.log(" npm run self:verify -- --targets=opencode --userId=h00613222");
46
- console.log(" node scripts/real-self-verify.mjs --targets=claude,opencode,codex --userId=h00613222");
47
- console.log("");
48
- console.log("What it does:");
49
- console.log(" 1. Runs the real setup command for each target unless --skip-install is set.");
50
- console.log(" 2. Starts the actual Claude/OpenCode/Codex CLI with a unique validation marker.");
51
- console.log(" 3. Polls Langfuse Public API until that marker appears in traces/observations.");
52
- console.log("");
53
- console.log("Options:");
54
- console.log(" --targets=opencode,claude,codex");
55
- console.log(" --userId=<id> Required unless LANGFUSE_USER_ID is set.");
56
- console.log(" --langfuseBaseUrl=<url> Defaults to package default or LANGFUSE_BASEURL.");
57
- console.log(" --publicKey=<key> Defaults to package default or LANGFUSE_PUBLIC_KEY.");
58
- console.log(" --secretKey=<key> Defaults to package default or LANGFUSE_SECRET_KEY.");
59
- console.log(" --marker=<text> Override generated validation marker.");
60
- console.log(" --timeoutMs=180000 Langfuse polling timeout.");
61
- console.log(" --triggerTimeoutMs=300000 Per CLI invocation timeout.");
62
- console.log(" --skip-install Do not run setup first.");
63
- console.log(" --skip-trigger Only install and poll for a pre-existing marker.");
64
- console.log(" --allow-trigger-failure Poll Langfuse even if the CLI command exits non-zero.");
65
- console.log(" --opencodeCmd=<path> Override OpenCode CLI path.");
66
- console.log(" --claudeCmd=<path> Override Claude CLI path.");
67
- console.log(" --codexCmd=<path> Override Codex CLI path.");
68
- console.log(" --prompt=<text> Override the validation prompt.");
69
- }
70
-
71
- function splitTargets(raw) {
72
- const value = String(raw || "opencode").trim();
73
- const out = value
74
- .split(/[, ]+/)
75
- .map((x) => x.trim().toLowerCase())
76
- .filter(Boolean);
77
- const allowed = new Set(["claude", "opencode", "codex"]);
78
- for (const target of out) {
79
- if (!allowed.has(target)) throw new Error(`Unsupported target: ${target}`);
80
- }
81
- return out.length ? [...new Set(out)] : ["opencode"];
82
- }
83
-
84
- function makeMarker() {
85
- return `real-self-verify-${packageJson.name}-${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
86
- }
87
-
88
- function shellNeeded(command) {
89
- return process.platform === "win32" && /\.(cmd|bat)$/i.test(command);
90
- }
91
-
92
- function run(command, args, options = {}) {
93
- const result = spawnSync(command, args, {
94
- cwd: options.cwd || rootDir,
95
- env: { ...process.env, ...(options.env || {}) },
96
- encoding: "utf8",
97
- stdio: options.stdio || "pipe",
98
- timeout: options.timeoutMs,
99
- windowsHide: true,
100
- shell: shellNeeded(command),
101
- });
102
- return {
103
- command,
104
- args,
105
- status: result.status ?? (result.error ? 1 : 0),
106
- stdout: result.stdout || "",
107
- stderr: result.stderr || "",
108
- error: result.error,
109
- signal: result.signal,
110
- };
111
- }
112
-
113
- function printRunResult(label, result) {
114
- const status = result.status === 0 ? "OK" : "FAIL";
115
- console.log(`[${status}] ${label}: ${result.command} ${result.args.join(" ")}`);
116
- const text = `${result.stdout}\n${result.stderr}`.trim();
117
- if (text) {
118
- const clipped = text.length > 5000 ? `${text.slice(0, 5000)}\n...<clipped>` : text;
119
- console.log(clipped);
120
- }
121
- }
122
-
123
- function findOnPath(names) {
124
- for (const name of names) {
125
- const probe = run(name, ["--version"], { timeoutMs: 15000 });
126
- if (probe.status === 0) return name;
127
- }
128
- return "";
129
- }
130
-
131
- function configFromArgs(args) {
132
- return {
133
- baseUrl:
134
- String(args.langfuseBaseUrl || args.langfuseHost || args.host || process.env.LANGFUSE_BASEURL || process.env.LANGFUSE_HOST || DEFAULT_LANGFUSE_BASE_URL),
135
- publicKey: String(args.publicKey || process.env.LANGFUSE_PUBLIC_KEY || DEFAULT_LANGFUSE_PUBLIC_KEY),
136
- secretKey: String(args.secretKey || process.env.LANGFUSE_SECRET_KEY || DEFAULT_LANGFUSE_SECRET_KEY),
137
- userId: String(args.userId || args.userid || process.env.LANGFUSE_USER_ID || process.env.CC_USER_ID || "").trim(),
138
- };
139
- }
140
-
141
- function setupTarget(target, config, args) {
142
- const cli = path.join(rootDir, "bin", "cli.js");
143
- const setupArgs = [
144
- cli,
145
- "setup",
146
- target,
147
- "--yes",
148
- `--userId=${config.userId}`,
149
- `--langfuseBaseUrl=${config.baseUrl}`,
150
- `--publicKey=${config.publicKey}`,
151
- `--secretKey=${config.secretKey}`,
152
- ];
153
- if (args.npmRegistry) setupArgs.push(`--npmRegistry=${args.npmRegistry}`);
154
- if (args.pipIndexUrl) setupArgs.push(`--pipIndexUrl=${args.pipIndexUrl}`);
155
- if (args["skip-plugin-install"]) setupArgs.push("--skip-plugin-install");
156
- if (args["no-set-env"]) setupArgs.push("--no-set-env");
157
- if (args.cmd) setupArgs.push(`--cmd=${args.cmd}`);
158
- const result = run(process.execPath, setupArgs, { stdio: "inherit", timeoutMs: Number(args.setupTimeoutMs || 600000) });
159
- return result.status;
160
- }
161
-
162
- function validationPrompt(marker, target, customPrompt) {
163
- if (customPrompt) return String(customPrompt).replaceAll("{marker}", marker).replaceAll("{target}", target);
164
- return [
165
- `This is an automated real self-verification run for ${packageJson.name}.`,
166
- `Validation marker: ${marker}`,
167
- "Reply with the validation marker exactly once and do not run tools.",
168
- ].join("\n");
169
- }
170
-
171
- function triggerClaude(prompt, args, env) {
172
- const cmd = String(args.claudeCmd || findOnPath(process.platform === "win32" ? ["claude.cmd", "claude"] : ["claude"]));
173
- if (!cmd) return { status: 127, stdout: "", stderr: "Claude CLI not found. Set --claudeCmd=<path>.", command: "claude", args: [] };
174
- const candidates = [
175
- ["-p", prompt],
176
- ["--print", prompt],
177
- ];
178
- let last = null;
179
- for (const candidate of candidates) {
180
- last = run(cmd, candidate, { env, timeoutMs: Number(args.triggerTimeoutMs || 300000) });
181
- if (last.status === 0) return last;
182
- }
183
- return last;
184
- }
185
-
186
- function triggerOpenCode(prompt, args, env) {
187
- const cmd = resolveOpencodeCli(args.opencodeCmd || args.cmd);
188
- if (!cmd) return { status: 127, stdout: "", stderr: "OpenCode CLI not found. Set --opencodeCmd=<path>.", command: "opencode", args: [] };
189
- const candidates = [
190
- ["run", prompt],
191
- ["run", "--print", prompt],
192
- [prompt],
193
- ];
194
- let last = null;
195
- for (const candidate of candidates) {
196
- last = run(cmd, candidate, { env, timeoutMs: Number(args.triggerTimeoutMs || 300000) });
197
- if (last.status === 0) return last;
198
- }
199
- return last;
200
- }
201
-
202
- function triggerCodex(prompt, args, env) {
203
- const cmd = String(args.codexCmd || findOnPath(process.platform === "win32" ? ["codex.cmd", "codex"] : ["codex"]));
204
- if (!cmd) return { status: 127, stdout: "", stderr: "Codex CLI not found. Set --codexCmd=<path>.", command: "codex", args: [] };
205
- const candidates = [
206
- ["exec", prompt],
207
- ["exec", "--skip-git-repo-check", prompt],
208
- ];
209
- let last = null;
210
- for (const candidate of candidates) {
211
- last = run(cmd, candidate, { env, timeoutMs: Number(args.triggerTimeoutMs || 300000) });
212
- if (last.status === 0) return last;
213
- }
214
- return last;
215
- }
216
-
217
- function apiBase(baseUrl) {
218
- const trimmed = String(baseUrl || "").replace(/\/+$/, "");
219
- return trimmed.endsWith("/api/public") ? trimmed : `${trimmed}/api/public`;
220
- }
221
-
222
- function basicAuth(publicKey, secretKey) {
223
- return `Basic ${Buffer.from(`${publicKey}:${secretKey}`, "utf8").toString("base64")}`;
224
- }
225
-
226
- async function langfuseGet(config, pathname, params = {}) {
227
- const url = new URL(`${apiBase(config.baseUrl)}${pathname}`);
228
- for (const [key, value] of Object.entries(params)) {
229
- if (value !== undefined && value !== null && String(value) !== "") url.searchParams.set(key, String(value));
230
- }
231
- const response = await fetch(url, {
232
- headers: {
233
- accept: "application/json",
234
- authorization: basicAuth(config.publicKey, config.secretKey),
235
- },
236
- });
237
- if (!response.ok) {
238
- const body = await response.text().catch(() => "");
239
- const error = new Error(`Langfuse API ${response.status} ${response.statusText}: ${body.slice(0, 500)}`);
240
- error.status = response.status;
241
- throw error;
242
- }
243
- return await response.json();
244
- }
245
-
246
- function dataArray(value) {
247
- if (Array.isArray(value)) return value;
248
- if (Array.isArray(value?.data)) return value.data;
249
- if (Array.isArray(value?.items)) return value.items;
250
- return [];
251
- }
252
-
253
- function containsMarker(value, marker) {
254
- try {
255
- return JSON.stringify(value).includes(marker);
256
- } catch {
257
- return false;
258
- }
259
- }
260
-
261
- function idOf(value) {
262
- return value?.id || value?.traceId || value?.trace_id || "";
263
- }
264
-
265
- async function findLangfuseMarker(config, marker, { since, target }) {
266
- const baseParams = {
267
- limit: 100,
268
- fromTimestamp: since.toISOString(),
269
- };
270
- const traceQueries = [
271
- { ...baseParams, userId: config.userId },
272
- baseParams,
273
- ];
274
-
275
- for (const params of traceQueries) {
276
- let traces;
277
- try {
278
- traces = await langfuseGet(config, "/traces", params);
279
- } catch (error) {
280
- if (error.status === 404) continue;
281
- throw error;
282
- }
283
- for (const trace of dataArray(traces)) {
284
- if (containsMarker(trace, marker)) return { kind: "trace-list", target, id: idOf(trace), item: trace };
285
- }
286
- for (const trace of dataArray(traces).slice(0, 25)) {
287
- const id = idOf(trace);
288
- if (!id) continue;
289
- try {
290
- const detail = await langfuseGet(config, `/traces/${encodeURIComponent(id)}`);
291
- if (containsMarker(detail, marker)) return { kind: "trace-detail", target, id, item: detail };
292
- } catch (error) {
293
- if (error.status !== 404) throw error;
294
- }
295
- }
296
- }
297
-
298
- const observationQueries = [
299
- ["/observations", { ...baseParams, userId: config.userId }],
300
- ["/observations", baseParams],
301
- ["/v2/observations", { ...baseParams, fields: "core,basic,usage", userId: config.userId }],
302
- ["/v2/observations", { ...baseParams, fields: "core,basic,usage" }],
303
- ];
304
- for (const [pathname, params] of observationQueries) {
305
- try {
306
- const observations = await langfuseGet(config, pathname, params);
307
- for (const observation of dataArray(observations)) {
308
- if (containsMarker(observation, marker)) return { kind: pathname, target, id: idOf(observation), item: observation };
309
- }
310
- } catch (error) {
311
- if (error.status === 404 || error.status === 400) continue;
312
- throw error;
313
- }
314
- }
315
-
316
- return null;
317
- }
318
-
319
- async function pollLangfuse(config, marker, options) {
320
- const timeoutMs = Number(options.timeoutMs || 180000);
321
- const pollMs = Number(options.pollMs || 5000);
322
- const deadline = Date.now() + timeoutMs;
323
- const since = options.since || new Date(Date.now() - 10 * 60 * 1000);
324
- let lastError = null;
325
-
326
- while (Date.now() <= deadline) {
327
- try {
328
- const found = await findLangfuseMarker(config, marker, { since, target: options.target });
329
- if (found) return found;
330
- console.log(`[WAIT] Langfuse has not returned marker yet (${marker}).`);
331
- } catch (error) {
332
- lastError = error;
333
- console.log(`[WAIT] Langfuse query failed: ${error.message}`);
334
- }
335
- await new Promise((resolve) => setTimeout(resolve, pollMs));
336
- }
337
-
338
- if (lastError) throw new Error(`Timed out waiting for marker. Last Langfuse error: ${lastError.message}`);
339
- throw new Error(`Timed out waiting for marker in Langfuse: ${marker}`);
340
- }
341
-
342
- async function main() {
343
- const args = parseArgs(process.argv.slice(2));
344
- if (args.help || args.h) {
345
- printHelp();
346
- return 0;
347
- }
348
-
349
- const config = configFromArgs(args);
350
- if (!config.userId) throw new Error("Missing --userId or LANGFUSE_USER_ID. Real verification needs a user id to install and filter traces.");
351
- const targets = splitTargets(args.targets || args.target || args._[0]);
352
- const marker = String(args.marker || makeMarker());
353
- const since = new Date();
354
- const results = [];
355
-
356
- console.log(`[INFO] Package: ${packageJson.name}@${packageJson.version}`);
357
- console.log(`[INFO] Targets: ${targets.join(", ")}`);
358
- console.log(`[INFO] Langfuse: ${config.baseUrl}`);
359
- console.log(`[INFO] User ID: ${config.userId}`);
360
- console.log(`[INFO] Marker: ${marker}`);
361
- console.log("");
362
-
363
- for (const target of targets) {
364
- if (!args["skip-install"]) {
365
- const code = setupTarget(target, config, args);
366
- if (code !== 0) throw new Error(`Setup failed for ${target} with exit code ${code}.`);
367
- }
368
-
369
- const prompt = validationPrompt(marker, target, args.prompt);
370
- const env = {
371
- TRACE_TO_LANGFUSE: "true",
372
- LANGFUSE_PUBLIC_KEY: config.publicKey,
373
- LANGFUSE_SECRET_KEY: config.secretKey,
374
- LANGFUSE_BASEURL: config.baseUrl,
375
- LANGFUSE_HOST: config.baseUrl,
376
- LANGFUSE_USER_ID: config.userId,
377
- CC_USER_ID: config.userId,
378
- CC_LANGFUSE_USER_ID: config.userId,
379
- CODEX_LANGFUSE_USER_ID: config.userId,
380
- OHAI_REAL_SELF_VERIFY_MARKER: marker,
381
- OHAI_REAL_SELF_VERIFY_TARGET: target,
382
- };
383
-
384
- if (!args["skip-trigger"]) {
385
- const trigger =
386
- target === "claude"
387
- ? triggerClaude(prompt, args, env)
388
- : target === "opencode"
389
- ? triggerOpenCode(prompt, args, env)
390
- : triggerCodex(prompt, args, env);
391
- printRunResult(`${target} trigger`, trigger);
392
- if (trigger.status !== 0 && !args["allow-trigger-failure"]) {
393
- throw new Error(`${target} trigger failed before Langfuse polling. Use --allow-trigger-failure to poll anyway.`);
394
- }
395
- }
396
-
397
- const found = await pollLangfuse(config, marker, { ...args, since, target });
398
- console.log(`[OK] Langfuse marker found for ${target}: ${found.kind} ${found.id || ""}`.trim());
399
- results.push({ target, marker, langfuse: { kind: found.kind, id: found.id || "" } });
400
- }
401
-
402
- console.log("");
403
- console.log(JSON.stringify({ ok: true, package: packageJson.name, marker, results }, null, 2));
404
- return 0;
405
- }
406
-
407
- main()
408
- .then((code) => {
409
- process.exitCode = code;
410
- })
411
- .catch((error) => {
412
- console.error(`[FAIL] ${error?.message || String(error)}`);
413
- process.exitCode = 1;
414
- });