oh-langfuse 0.1.23 → 0.1.25

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.23`
5
+ 当前 npm 版本:`0.1.25`
6
6
 
7
7
  ## 能做什么
8
8
 
package/SELF_VERIFY.md ADDED
@@ -0,0 +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.
package/bin/cli.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import { createInterface } from "node:readline/promises";
5
- import { fileURLToPath } from "node:url";
6
- import { spawnSync } from "node:child_process";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import readline from "readline";
5
+ import { fileURLToPath } from "url";
6
+ import { spawnSync } from "child_process";
7
7
 
8
8
  const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
9
9
  const scriptsDir = path.join(rootDir, "scripts");
@@ -13,6 +13,31 @@ const DEFAULT_LANGFUSE_PUBLIC_KEY = "pk-lf-da0c90a7-6e93-4eb7-bb86-c1047c8d187d"
13
13
  const DEFAULT_LANGFUSE_SECRET_KEY = "sk-lf-0269b85d-bfdc-442c-bfa3-e737954e3315";
14
14
  const USER_ID_PATTERN = /^[a-z](?:\d{8}|wx\d{7})$/;
15
15
  const USER_ID_PATTERN_TEXT = "^[a-z](?:\\d{8}|wx\\d{7})$";
16
+
17
+ function nodeMajorVersion() {
18
+ const raw = process.versions && process.versions.node ? process.versions.node : "0.0.0";
19
+ return Number.parseInt(raw.split(".")[0], 10) || 0;
20
+ }
21
+
22
+ function assertSupportedNode() {
23
+ if (nodeMajorVersion() >= 16) return;
24
+ console.error("oh-langfuse requires Node.js >= 16.");
25
+ console.error(`Current Node.js: ${process.version}`);
26
+ console.error("Please upgrade Node.js, then run: npx oh-langfuse@latest");
27
+ process.exit(1);
28
+ }
29
+
30
+ function createPromptInterface(options) {
31
+ const rl = readline.createInterface(options);
32
+ return {
33
+ question(query) {
34
+ return new Promise((resolve) => rl.question(query, resolve));
35
+ },
36
+ close() {
37
+ rl.close();
38
+ },
39
+ };
40
+ }
16
41
 
17
42
  const colorEnabled = process.stdout.isTTY && process.env.NO_COLOR !== "1";
18
43
  const ansi = (code) => (colorEnabled ? `\x1b[${code}m` : "");
@@ -260,7 +285,7 @@ function runNodeScript(name, args = [], { dryRun = false } = {}) {
260
285
  console.log(paint("Running installer...", t.bold, t.teal));
261
286
  console.log(paint("─".repeat(Math.min(terminalWidth(), 64)), t.panel));
262
287
  const r = spawnSync(process.execPath, [target, ...args], { stdio: "inherit" });
263
- return r.status ?? (r.error ? 1 : 0);
288
+ return r.status != null ? r.status : r.error ? 1 : 0;
264
289
  }
265
290
 
266
291
  async function askText(rl, label, { defaultValue = "", required = false, validate = null, invalidMessage = "" } = {}) {
@@ -294,7 +319,7 @@ async function askYesNo(rl, label, { defaultValue = false } = {}) {
294
319
 
295
320
  function rawKeySeq(raw) {
296
321
  if (Buffer.isBuffer(raw)) return raw.toString("latin1");
297
- return String(raw ?? "");
322
+ return String(raw == null ? "" : raw);
298
323
  }
299
324
 
300
325
  function parseRawKey(raw) {
@@ -680,8 +705,8 @@ async function checkMenu(rl, options) {
680
705
  return claude || opencode || codex;
681
706
  }
682
707
 
683
- async function interactiveMain(options) {
684
- const rl = createInterface({ input: process.stdin, output: process.stdout });
708
+ async function interactiveMain(options) {
709
+ const rl = createPromptInterface({ input: process.stdin, output: process.stdout });
685
710
  try {
686
711
  const action = await askChoice(
687
712
  rl,
@@ -727,9 +752,9 @@ async function setupLangfuseMenu(rl, options) {
727
752
  if (!targets.length) return 0;
728
753
  const config = await collectSharedConfig(rl, options);
729
754
  let code = 0;
730
- if (targets.includes("claude")) code ||= await setupClaude(rl, { ...options, config });
731
- if (targets.includes("opencode")) code ||= await setupOpenCode(rl, { ...options, config });
732
- if (targets.includes("codex")) code ||= await setupCodex(rl, { ...options, config });
755
+ if (targets.includes("claude")) code = code || await setupClaude(rl, { ...options, config });
756
+ if (targets.includes("opencode")) code = code || await setupOpenCode(rl, { ...options, config });
757
+ if (targets.includes("codex")) code = code || await setupCodex(rl, { ...options, config });
733
758
  return code;
734
759
  }
735
760
 
@@ -791,7 +816,7 @@ async function main() {
791
816
 
792
817
  if (!cmd) return await interactiveMain(options);
793
818
 
794
- const rl = createInterface({ input: process.stdin, output: process.stdout });
819
+ const rl = createPromptInterface({ input: process.stdin, output: process.stdout });
795
820
  try {
796
821
  if (cmd === "setup" && target === "claude") return await setupClaude(rl, options);
797
822
  if (cmd === "setup" && target === "opencode") return await setupOpenCode(rl, options);
@@ -815,9 +840,11 @@ async function main() {
815
840
  return 1;
816
841
  }
817
842
 
818
- main()
819
- .then((code) => process.exit(code))
820
- .catch((err) => {
821
- console.error(paint(err?.message || String(err), t.red));
822
- process.exit(1);
823
- });
843
+ assertSupportedNode();
844
+
845
+ main()
846
+ .then((code) => process.exit(code))
847
+ .catch((err) => {
848
+ console.error(paint((err && err.message) || String(err), t.red));
849
+ process.exit(1);
850
+ });
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "oh-langfuse",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "private": false,
5
5
  "type": "module",
6
- "description": "Use npm scripts to configure Claude Code / OpenCode / Codex with Langfuse tracing.",
7
- "bin": {
6
+ "description": "Use npm scripts to configure Claude Code / OpenCode / Codex with Langfuse tracing.",
7
+ "engines": {
8
+ "node": ">=16"
9
+ },
10
+ "bin": {
8
11
  "oh-langfuse": "bin/cli.js",
9
12
  "code-tool-langfuse": "bin/cli.js"
10
13
  },
@@ -18,10 +21,12 @@
18
21
  "scripts/opencode-langfuse-check.mjs",
19
22
  "scripts/opencode-langfuse-run.mjs",
20
23
  "scripts/opencode-langfuse-setup.mjs",
21
- "scripts/resolve-opencode-cli.mjs",
24
+ "scripts/resolve-opencode-cli.mjs",
25
+ "scripts/real-self-verify.mjs",
22
26
  "langfuse_hook.py",
23
27
  "codex_langfuse_notify.py",
24
- "README.md",
28
+ "README.md",
29
+ "SELF_VERIFY.md",
25
30
  "CODEX_LANGFUSE_PLAN.md",
26
31
  "setup-langfuse.bat",
27
32
  "setup-langfuse.sh"
@@ -42,8 +47,9 @@
42
47
  "opencode:langfuse:run": "node scripts/opencode-langfuse-run.mjs",
43
48
  "codex:setup": "node scripts/codex-langfuse-setup.mjs",
44
49
  "codex:check": "node scripts/codex-langfuse-check.mjs",
45
- "codex:langfuse:setup": "node scripts/codex-langfuse-setup.mjs",
46
- "codex:langfuse:check": "node scripts/codex-langfuse-check.mjs"
50
+ "codex:langfuse:setup": "node scripts/codex-langfuse-setup.mjs",
51
+ "codex:langfuse:check": "node scripts/codex-langfuse-check.mjs",
52
+ "self:verify": "node scripts/real-self-verify.mjs"
47
53
  },
48
54
  "dependencies": {}
49
55
  }
@@ -26,6 +26,14 @@ function launcherPath(hooksDir) {
26
26
  : path.join(hooksDir, "run-langfuse-hook.sh");
27
27
  }
28
28
 
29
+ function expectedHookCommands(launcher) {
30
+ if (process.platform !== "win32") return [path.normalize(launcher)];
31
+ return [
32
+ path.normalize(launcher),
33
+ `cmd.exe /d /s /c "${launcher.replace(/"/g, '""')}"`
34
+ ];
35
+ }
36
+
29
37
  function findLangfuseHook(settings) {
30
38
  const stopHooks = settings?.hooks?.Stop;
31
39
  if (!Array.isArray(stopHooks)) return null;
@@ -110,7 +118,12 @@ function main() {
110
118
  const hook = findLangfuseHook(settings);
111
119
  addResult(results, "hooks.Stop contains Langfuse", !!hook, hook ? "OK" : "missing", "Run setup again to register the Stop hook.");
112
120
 
113
- const launcherFormOk = !!hook && path.normalize(hook.command) === path.normalize(expectedLauncher) && !hook.args?.length;
121
+ const expectedCommands = expectedHookCommands(expectedLauncher);
122
+ const hookCommand = hook?.command || "";
123
+ const launcherFormOk =
124
+ !!hook &&
125
+ expectedCommands.some((cmd) => path.normalize(hookCommand) === path.normalize(cmd)) &&
126
+ !hook.args?.length;
114
127
  addResult(
115
128
  results,
116
129
  "hook command form",
@@ -135,6 +135,11 @@ function cmdQuote(s) {
135
135
  return `"${String(s).replace(/"/g, '""')}"`;
136
136
  }
137
137
 
138
+ function hookCommandForClaude(launcher) {
139
+ if (process.platform !== "win32") return launcher;
140
+ return `cmd.exe /d /s /c ${cmdQuote(launcher)}`;
141
+ }
142
+
138
143
  function shQuote(s) {
139
144
  return `'${String(s).replace(/'/g, "'\\''")}'`;
140
145
  }
@@ -299,7 +304,7 @@ async function main() {
299
304
  hooks: [
300
305
  {
301
306
  type: "command",
302
- command: hookLauncher
307
+ command: hookCommandForClaude(hookLauncher)
303
308
  }
304
309
  ]
305
310
  }
@@ -0,0 +1,433 @@
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
+ maxBuffer: 20 * 1024 * 1024,
100
+ windowsHide: true,
101
+ shell: shellNeeded(command),
102
+ });
103
+ return {
104
+ command,
105
+ args,
106
+ status: result.status ?? (result.error ? 1 : 0),
107
+ stdout: result.stdout || "",
108
+ stderr: result.stderr || "",
109
+ error: result.error,
110
+ signal: result.signal,
111
+ };
112
+ }
113
+
114
+ function printRunResult(label, result) {
115
+ const status = result.status === 0 ? "OK" : "FAIL";
116
+ console.log(`[${status}] ${label}: ${result.command} ${result.args.join(" ")}`);
117
+ const text = `${result.stdout}\n${result.stderr}`.trim();
118
+ if (text) {
119
+ const clipped = text.length > 5000 ? `${text.slice(0, 5000)}\n...<clipped>` : text;
120
+ console.log(clipped);
121
+ }
122
+ }
123
+
124
+ function findOnPath(names) {
125
+ for (const name of names) {
126
+ const probe = run(name, ["--version"], { timeoutMs: 15000 });
127
+ if (probe.status === 0) return name;
128
+ }
129
+ return "";
130
+ }
131
+
132
+ function configFromArgs(args) {
133
+ return {
134
+ baseUrl:
135
+ String(args.langfuseBaseUrl || args.langfuseHost || args.host || process.env.LANGFUSE_BASEURL || process.env.LANGFUSE_HOST || DEFAULT_LANGFUSE_BASE_URL),
136
+ publicKey: String(args.publicKey || process.env.LANGFUSE_PUBLIC_KEY || DEFAULT_LANGFUSE_PUBLIC_KEY),
137
+ secretKey: String(args.secretKey || process.env.LANGFUSE_SECRET_KEY || DEFAULT_LANGFUSE_SECRET_KEY),
138
+ userId: String(args.userId || args.userid || process.env.LANGFUSE_USER_ID || process.env.CC_USER_ID || "").trim(),
139
+ };
140
+ }
141
+
142
+ function setupTarget(target, config, args) {
143
+ const cli = path.join(rootDir, "bin", "cli.js");
144
+ const setupArgs = [
145
+ cli,
146
+ "setup",
147
+ target,
148
+ "--yes",
149
+ `--userId=${config.userId}`,
150
+ `--langfuseBaseUrl=${config.baseUrl}`,
151
+ `--publicKey=${config.publicKey}`,
152
+ `--secretKey=${config.secretKey}`,
153
+ ];
154
+ if (args.npmRegistry) setupArgs.push(`--npmRegistry=${args.npmRegistry}`);
155
+ if (args.pipIndexUrl) setupArgs.push(`--pipIndexUrl=${args.pipIndexUrl}`);
156
+ if (args["skip-plugin-install"]) setupArgs.push("--skip-plugin-install");
157
+ if (args["no-set-env"]) setupArgs.push("--no-set-env");
158
+ const targetCmd = target === "opencode" ? args.opencodeCmd || args.cmd : args.cmd;
159
+ if (targetCmd) setupArgs.push(`--cmd=${targetCmd}`);
160
+ const result = run(process.execPath, setupArgs, { stdio: "inherit", timeoutMs: Number(args.setupTimeoutMs || 600000) });
161
+ return result.status;
162
+ }
163
+
164
+ function validationPrompt(marker, target, customPrompt) {
165
+ if (customPrompt) return String(customPrompt).replaceAll("{marker}", marker).replaceAll("{target}", target);
166
+ return [
167
+ `This is an automated real self-verification run for ${packageJson.name}.`,
168
+ `Validation marker: ${marker}`,
169
+ "Reply with the validation marker exactly once and do not run tools.",
170
+ ].join("\n");
171
+ }
172
+
173
+ function triggerClaude(prompt, args, env) {
174
+ const cmd = String(args.claudeCmd || findOnPath(process.platform === "win32" ? ["claude.cmd", "claude"] : ["claude"]));
175
+ if (!cmd) return { status: 127, stdout: "", stderr: "Claude CLI not found. Set --claudeCmd=<path>.", command: "claude", args: [] };
176
+ const candidates = [
177
+ ["-p", prompt],
178
+ ["--print", prompt],
179
+ ];
180
+ let last = null;
181
+ for (const candidate of candidates) {
182
+ last = run(cmd, candidate, { env, timeoutMs: Number(args.triggerTimeoutMs || 300000) });
183
+ if (last.status === 0) return last;
184
+ }
185
+ return last;
186
+ }
187
+
188
+ function triggerOpenCode(prompt, args, env) {
189
+ const cmd = resolveOpencodeCli(args.opencodeCmd || args.cmd);
190
+ if (!cmd) return { status: 127, stdout: "", stderr: "OpenCode CLI not found. Set --opencodeCmd=<path>.", command: "opencode", args: [] };
191
+ const candidates = [
192
+ ["run", "--format", "json", "--print-logs", "--log-level", "INFO", prompt],
193
+ ["run", "--format", "json", prompt],
194
+ ["run", prompt],
195
+ [prompt],
196
+ ];
197
+ let last = null;
198
+ for (const candidate of candidates) {
199
+ last = run(cmd, candidate, { env, timeoutMs: Number(args.triggerTimeoutMs || 300000) });
200
+ if (last.status === 0) return last;
201
+ }
202
+ return last;
203
+ }
204
+
205
+ function triggerCodex(prompt, args, env) {
206
+ const cmd = String(args.codexCmd || findOnPath(process.platform === "win32" ? ["codex.cmd", "codex"] : ["codex"]));
207
+ if (!cmd) return { status: 127, stdout: "", stderr: "Codex CLI not found. Set --codexCmd=<path>.", command: "codex", args: [] };
208
+ const candidates = [
209
+ ["exec", prompt],
210
+ ["exec", "--skip-git-repo-check", prompt],
211
+ ];
212
+ let last = null;
213
+ for (const candidate of candidates) {
214
+ last = run(cmd, candidate, { env, timeoutMs: Number(args.triggerTimeoutMs || 300000) });
215
+ if (last.status === 0) return last;
216
+ }
217
+ return last;
218
+ }
219
+
220
+ function apiBase(baseUrl) {
221
+ const trimmed = String(baseUrl || "").replace(/\/+$/, "");
222
+ return trimmed.endsWith("/api/public") ? trimmed : `${trimmed}/api/public`;
223
+ }
224
+
225
+ function basicAuth(publicKey, secretKey) {
226
+ return `Basic ${Buffer.from(`${publicKey}:${secretKey}`, "utf8").toString("base64")}`;
227
+ }
228
+
229
+ async function langfuseGet(config, pathname, params = {}) {
230
+ const url = new URL(`${apiBase(config.baseUrl)}${pathname}`);
231
+ for (const [key, value] of Object.entries(params)) {
232
+ if (value !== undefined && value !== null && String(value) !== "") url.searchParams.set(key, String(value));
233
+ }
234
+ const controller = new AbortController();
235
+ const timeout = setTimeout(() => controller.abort(), Number(process.env.LANGFUSE_VERIFY_REQUEST_TIMEOUT_MS || 15000));
236
+ const response = await fetch(url, {
237
+ headers: {
238
+ accept: "application/json",
239
+ authorization: basicAuth(config.publicKey, config.secretKey),
240
+ },
241
+ signal: controller.signal,
242
+ }).finally(() => clearTimeout(timeout));
243
+ if (!response.ok) {
244
+ const body = await response.text().catch(() => "");
245
+ const error = new Error(`Langfuse API ${response.status} ${response.statusText}: ${body.slice(0, 500)}`);
246
+ error.status = response.status;
247
+ throw error;
248
+ }
249
+ return await response.json();
250
+ }
251
+
252
+ async function langfuseGetLenient(config, pathname, params = {}) {
253
+ try {
254
+ return await langfuseGet(config, pathname, params);
255
+ } catch (error) {
256
+ if (error.status !== 400) throw error;
257
+ const fallback = {};
258
+ for (const key of ["limit", "page", "userId", "fields"]) {
259
+ if (params[key] !== undefined) fallback[key] = params[key];
260
+ }
261
+ return await langfuseGet(config, pathname, fallback);
262
+ }
263
+ }
264
+
265
+ function dataArray(value) {
266
+ if (Array.isArray(value)) return value;
267
+ if (Array.isArray(value?.data)) return value.data;
268
+ if (Array.isArray(value?.items)) return value.items;
269
+ return [];
270
+ }
271
+
272
+ function containsMarker(value, marker) {
273
+ try {
274
+ return JSON.stringify(value).includes(marker);
275
+ } catch {
276
+ return false;
277
+ }
278
+ }
279
+
280
+ function idOf(value) {
281
+ return value?.id || value?.traceId || value?.trace_id || "";
282
+ }
283
+
284
+ async function findLangfuseMarker(config, marker, { since, target }) {
285
+ const baseParams = {
286
+ limit: 100,
287
+ fromTimestamp: since.toISOString(),
288
+ };
289
+ const traceQueries = [
290
+ { ...baseParams, userId: config.userId },
291
+ baseParams,
292
+ ];
293
+
294
+ for (const params of traceQueries) {
295
+ let traces;
296
+ try {
297
+ traces = await langfuseGetLenient(config, "/traces", params);
298
+ } catch (error) {
299
+ if (error.status === 404) continue;
300
+ throw error;
301
+ }
302
+ for (const trace of dataArray(traces)) {
303
+ if (containsMarker(trace, marker)) return { kind: "trace-list", target, id: idOf(trace), item: trace };
304
+ }
305
+ for (const trace of dataArray(traces).slice(0, 25)) {
306
+ const id = idOf(trace);
307
+ if (!id) continue;
308
+ try {
309
+ const detail = await langfuseGet(config, `/traces/${encodeURIComponent(id)}`);
310
+ if (containsMarker(detail, marker)) return { kind: "trace-detail", target, id, item: detail };
311
+ } catch (error) {
312
+ if (error.status !== 404) throw error;
313
+ }
314
+ }
315
+ }
316
+
317
+ const observationQueries = [
318
+ ["/observations", { ...baseParams, userId: config.userId }],
319
+ ["/observations", baseParams],
320
+ ["/v2/observations", { ...baseParams, fields: "core,basic,usage", userId: config.userId }],
321
+ ["/v2/observations", { ...baseParams, fields: "core,basic,usage" }],
322
+ ];
323
+ for (const [pathname, params] of observationQueries) {
324
+ try {
325
+ const observations = await langfuseGetLenient(config, pathname, params);
326
+ for (const observation of dataArray(observations)) {
327
+ if (containsMarker(observation, marker)) return { kind: pathname, target, id: idOf(observation), item: observation };
328
+ }
329
+ } catch (error) {
330
+ if (error.status === 404 || error.status === 400) continue;
331
+ throw error;
332
+ }
333
+ }
334
+
335
+ return null;
336
+ }
337
+
338
+ async function pollLangfuse(config, marker, options) {
339
+ const timeoutMs = Number(options.timeoutMs || 180000);
340
+ const pollMs = Number(options.pollMs || 5000);
341
+ const deadline = Date.now() + timeoutMs;
342
+ const since = options.since || new Date(Date.now() - 10 * 60 * 1000);
343
+ let lastError = null;
344
+
345
+ while (Date.now() <= deadline) {
346
+ try {
347
+ const found = await findLangfuseMarker(config, marker, { since, target: options.target });
348
+ if (found) return found;
349
+ console.log(`[WAIT] Langfuse has not returned marker yet (${marker}).`);
350
+ } catch (error) {
351
+ lastError = error;
352
+ console.log(`[WAIT] Langfuse query failed: ${error.message}`);
353
+ }
354
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
355
+ }
356
+
357
+ if (lastError) throw new Error(`Timed out waiting for marker. Last Langfuse error: ${lastError.message}`);
358
+ throw new Error(`Timed out waiting for marker in Langfuse: ${marker}`);
359
+ }
360
+
361
+ async function main() {
362
+ const args = parseArgs(process.argv.slice(2));
363
+ if (args.help || args.h) {
364
+ printHelp();
365
+ return 0;
366
+ }
367
+
368
+ const config = configFromArgs(args);
369
+ if (!config.userId) throw new Error("Missing --userId or LANGFUSE_USER_ID. Real verification needs a user id to install and filter traces.");
370
+ const targets = splitTargets(args.targets || args.target || args._[0]);
371
+ const marker = String(args.marker || makeMarker());
372
+ const since = new Date();
373
+ const results = [];
374
+
375
+ console.log(`[INFO] Package: ${packageJson.name}@${packageJson.version}`);
376
+ console.log(`[INFO] Targets: ${targets.join(", ")}`);
377
+ console.log(`[INFO] Langfuse: ${config.baseUrl}`);
378
+ console.log(`[INFO] User ID: ${config.userId}`);
379
+ console.log(`[INFO] Marker: ${marker}`);
380
+ console.log("");
381
+
382
+ for (const target of targets) {
383
+ if (!args["skip-install"]) {
384
+ const code = setupTarget(target, config, args);
385
+ if (code !== 0) throw new Error(`Setup failed for ${target} with exit code ${code}.`);
386
+ }
387
+
388
+ const prompt = validationPrompt(marker, target, args.prompt);
389
+ const env = {
390
+ TRACE_TO_LANGFUSE: "true",
391
+ LANGFUSE_PUBLIC_KEY: config.publicKey,
392
+ LANGFUSE_SECRET_KEY: config.secretKey,
393
+ LANGFUSE_BASEURL: config.baseUrl,
394
+ LANGFUSE_HOST: config.baseUrl,
395
+ LANGFUSE_USER_ID: config.userId,
396
+ CC_USER_ID: config.userId,
397
+ CC_LANGFUSE_USER_ID: config.userId,
398
+ CODEX_LANGFUSE_USER_ID: config.userId,
399
+ OHAI_REAL_SELF_VERIFY_MARKER: marker,
400
+ OHAI_REAL_SELF_VERIFY_TARGET: target,
401
+ };
402
+
403
+ if (!args["skip-trigger"]) {
404
+ const trigger =
405
+ target === "claude"
406
+ ? triggerClaude(prompt, args, env)
407
+ : target === "opencode"
408
+ ? triggerOpenCode(prompt, args, env)
409
+ : triggerCodex(prompt, args, env);
410
+ printRunResult(`${target} trigger`, trigger);
411
+ if (trigger.status !== 0 && !args["allow-trigger-failure"]) {
412
+ throw new Error(`${target} trigger failed before Langfuse polling. Use --allow-trigger-failure to poll anyway.`);
413
+ }
414
+ }
415
+
416
+ const found = await pollLangfuse(config, marker, { ...args, since, target });
417
+ console.log(`[OK] Langfuse marker found for ${target}: ${found.kind} ${found.id || ""}`.trim());
418
+ results.push({ target, marker, langfuse: { kind: found.kind, id: found.id || "" } });
419
+ }
420
+
421
+ console.log("");
422
+ console.log(JSON.stringify({ ok: true, package: packageJson.name, marker, results }, null, 2));
423
+ return 0;
424
+ }
425
+
426
+ main()
427
+ .then((code) => {
428
+ process.exitCode = code;
429
+ })
430
+ .catch((error) => {
431
+ console.error(`[FAIL] ${error?.message || String(error)}`);
432
+ process.exitCode = 1;
433
+ });