oh-langfuse 0.1.60 → 0.1.62

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.
@@ -9,9 +9,10 @@ import { resolveOpencodeCli } from "./resolve-opencode-cli.mjs";
9
9
  const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
10
  const packageJson = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
11
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";
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
+ const OPENCODE_NATIVE_AGENT_TURN_NAMES = new Set(["session.llm:ai.streamText.doStream"]);
15
16
 
16
17
  function parseArgs(argv) {
17
18
  const args = { _: [] };
@@ -136,36 +137,36 @@ function printRunResult(label, result) {
136
137
  }
137
138
  }
138
139
 
139
- function findOnPath(names) {
140
- for (const name of names) {
141
- const probe = run(name, ["--version"], { timeoutMs: 15000 });
142
- if (probe.status === 0) return name;
143
- }
144
- return "";
145
- }
146
-
147
- function localCodexCliCandidates() {
148
- if (process.platform !== "win32") return ["codex"];
149
- const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
150
- const codexBin = path.join(localAppData, "OpenAI", "Codex", "bin");
151
- const candidates = [
152
- process.env.CODEX_CLI_PATH || "",
153
- path.join(codexBin, "codex.exe"),
154
- ];
155
- try {
156
- if (fs.existsSync(codexBin)) {
157
- for (const ent of fs.readdirSync(codexBin, { withFileTypes: true })) {
158
- if (ent.isDirectory()) candidates.push(path.join(codexBin, ent.name, "codex.exe"));
159
- }
160
- }
161
- } catch {
162
- // Fall back to PATH probing below.
163
- }
164
- return [...candidates, "codex.cmd", "codex.exe", "codex"].filter(Boolean);
165
- }
166
-
167
- function configFromArgs(args) {
168
- return {
140
+ function findOnPath(names) {
141
+ for (const name of names) {
142
+ const probe = run(name, ["--version"], { timeoutMs: 15000 });
143
+ if (probe.status === 0) return name;
144
+ }
145
+ return "";
146
+ }
147
+
148
+ function localCodexCliCandidates() {
149
+ if (process.platform !== "win32") return ["codex"];
150
+ const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
151
+ const codexBin = path.join(localAppData, "OpenAI", "Codex", "bin");
152
+ const candidates = [
153
+ process.env.CODEX_CLI_PATH || "",
154
+ path.join(codexBin, "codex.exe"),
155
+ ];
156
+ try {
157
+ if (fs.existsSync(codexBin)) {
158
+ for (const ent of fs.readdirSync(codexBin, { withFileTypes: true })) {
159
+ if (ent.isDirectory()) candidates.push(path.join(codexBin, ent.name, "codex.exe"));
160
+ }
161
+ }
162
+ } catch {
163
+ // Fall back to PATH probing below.
164
+ }
165
+ return [...candidates, "codex.cmd", "codex.exe", "codex"].filter(Boolean);
166
+ }
167
+
168
+ function configFromArgs(args) {
169
+ return {
169
170
  baseUrl:
170
171
  String(args.langfuseBaseUrl || args.langfuseHost || args.host || process.env.LANGFUSE_BASEURL || process.env.LANGFUSE_HOST || DEFAULT_LANGFUSE_BASE_URL),
171
172
  publicKey: String(args.publicKey || process.env.LANGFUSE_PUBLIC_KEY || DEFAULT_LANGFUSE_PUBLIC_KEY),
@@ -233,9 +234,9 @@ function triggerOpenCode(prompt, args, env) {
233
234
  return last;
234
235
  }
235
236
 
236
- function triggerCodex(prompt, args, env) {
237
- const cmd = String(args.codexCmd || findOnPath(localCodexCliCandidates()));
238
- if (!cmd) return { status: 127, stdout: "", stderr: "Codex CLI not found. Set --codexCmd=<path>.", command: "codex", args: [] };
237
+ function triggerCodex(prompt, args, env) {
238
+ const cmd = String(args.codexCmd || findOnPath(localCodexCliCandidates()));
239
+ if (!cmd) return { status: 127, stdout: "", stderr: "Codex CLI not found. Set --codexCmd=<path>.", command: "codex", args: [] };
239
240
  const candidates = [
240
241
  ["exec", prompt],
241
242
  ["exec", "--skip-git-repo-check", prompt],
@@ -322,31 +323,34 @@ function metadataValue(item, key) {
322
323
  const attrs = metadata.attributes || {};
323
324
  const prefixed = `langfuse.observation.metadata.${key}`;
324
325
  if (attrs[prefixed] !== undefined) return attrs[prefixed];
325
- const ohKey = `oh.langfuse.${key}`;
326
- if (attrs[ohKey] !== undefined) return attrs[ohKey];
327
- if (metadata.source === "opencode" || attrs["oh.langfuse.source"] === "opencode") {
328
- if (key === "interaction_id") return `opencode:${metadata.user_id || attrs["oh.langfuse.user_id"] || "unknown"}:${item?.id || item?.traceId || item?.trace_id || "unknown"}`;
329
- if (key === "token_metrics_available") {
330
- return [
331
- "ai.usage.inputTokens",
332
- "ai.usage.outputTokens",
333
- "ai.usage.totalTokens",
326
+ const ohKey = `oh.langfuse.${key}`;
327
+ if (attrs[ohKey] !== undefined) return attrs[ohKey];
328
+ if (metadata.source === "opencode" || attrs["oh.langfuse.source"] === "opencode") {
329
+ const usage = item?.usage || {};
330
+ if (key === "interaction_id") return `opencode:${metadata.user_id || attrs["oh.langfuse.user_id"] || "unknown"}:${item?.id || item?.traceId || item?.trace_id || "unknown"}`;
331
+ if (key === "interaction_count") return 1;
332
+ if (key === "token_metrics_available") {
333
+ return usage.input !== undefined || usage.output !== undefined || usage.total !== undefined || [
334
+ "ai.usage.inputTokens",
335
+ "ai.usage.outputTokens",
336
+ "ai.usage.totalTokens",
334
337
  "ai.usage.promptTokens",
335
338
  "ai.usage.completionTokens",
336
339
  ].some((attrKey) => attrs[attrKey] !== undefined);
337
340
  }
338
- if (key === "tool_call_count" || key === "skill_use_count") return 0;
339
- const tokenAttrMap = {
340
- input_tokens: ["ai.usage.inputTokens", "ai.usage.promptTokens"],
341
- output_tokens: ["ai.usage.outputTokens", "ai.usage.completionTokens"],
342
- total_tokens: ["ai.usage.totalTokens"],
343
- cache_read_tokens: ["ai.usage.cachedInputTokens", "ai.usage.inputTokenDetails.cacheReadTokens"],
344
- reasoning_tokens: ["ai.usage.reasoningTokens", "ai.usage.outputTokenDetails.reasoningTokens"],
345
- };
346
- for (const attrKey of tokenAttrMap[key] || []) {
347
- if (attrs[attrKey] !== undefined) return attrs[attrKey];
348
- }
349
- }
341
+ if (key === "tool_call_count" || key === "skill_use_count") return 0;
342
+ const tokenAttrMap = {
343
+ input_tokens: ["input", "ai.usage.inputTokens", "ai.usage.promptTokens"],
344
+ output_tokens: ["output", "ai.usage.outputTokens", "ai.usage.completionTokens"],
345
+ total_tokens: ["total", "ai.usage.totalTokens"],
346
+ cache_read_tokens: ["ai.usage.cachedInputTokens", "ai.usage.inputTokenDetails.cacheReadTokens"],
347
+ reasoning_tokens: ["ai.usage.reasoningTokens", "ai.usage.outputTokenDetails.reasoningTokens"],
348
+ };
349
+ for (const attrKey of tokenAttrMap[key] || []) {
350
+ if (usage[attrKey] !== undefined) return usage[attrKey];
351
+ if (attrs[attrKey] !== undefined) return attrs[attrKey];
352
+ }
353
+ }
350
354
  return undefined;
351
355
  }
352
356
 
@@ -366,45 +370,51 @@ function metricInteractionId(item, target) {
366
370
  }
367
371
 
368
372
  function isAgentTurnObservation(item) {
369
- if (/^(Claude|OpenCode|Codex) Agent Turn$/.test(String(item?.name || "")) || item?.name === "Agent Turn" || metadataValue(item, "interaction_count") === 1 || metadataValue(item, "interaction_count") === "1") {
373
+ const metadata = item?.metadata || {};
374
+ const attrs = metadata.attributes || {};
375
+ const isOpencode = metadata.source === "opencode" || attrs["oh.langfuse.source"] === "opencode";
376
+ if (/^(Claude|OpenCode|Codex) Agent Turn$/.test(String(item?.name || "")) || item?.name === "Agent Turn" || (!isOpencode && (metadataValue(item, "interaction_count") === 1 || metadataValue(item, "interaction_count") === "1"))) {
370
377
  return true;
371
378
  }
372
- const metadata = item?.metadata || {};
373
- const attrs = metadata.attributes || {};
374
- const isOpencode = metadata.source === "opencode" || attrs["oh.langfuse.source"] === "opencode";
375
- const hasModel = attrs["ai.model.id"] !== undefined || attrs["ai.model.provider"] !== undefined;
376
- const isTool = attrs["ai.toolCall.name"] !== undefined || attrs["ai.toolCall.id"] !== undefined;
379
+ if (isOpencode && OPENCODE_NATIVE_AGENT_TURN_NAMES.has(String(item?.name || ""))) return true;
380
+ const hasModel = attrs["ai.model.id"] !== undefined || attrs["ai.model.provider"] !== undefined;
381
+ const isTool = attrs["ai.toolCall.name"] !== undefined || attrs["ai.toolCall.id"] !== undefined;
377
382
  return isOpencode && hasModel && !isTool && item?.type === "GENERATION";
378
383
  }
379
-
380
- function expectedAgentTurnName(target) {
381
- if (target === "claude") return "Claude Agent Turn";
382
- if (target === "opencode") return "OpenCode Agent Turn";
383
- if (target === "codex") return "Codex Agent Turn";
384
- return "Agent Turn";
385
- }
386
-
387
- async function observationsForTrace(config, traceId, since) {
388
- if (!traceId) return [];
389
- const params = { limit: 100, traceId };
390
- try {
391
- return dataArray(await langfuseGet(config, "/observations", params));
392
- } catch (error) {
393
- if (error.status !== 400) throw error;
394
- }
384
+
385
+ function expectedAgentTurnName(target) {
386
+ if (target === "claude") return "Claude Agent Turn";
387
+ if (target === "opencode") return "OpenCode Agent Turn";
388
+ if (target === "codex") return "Codex Agent Turn";
389
+ return "Agent Turn";
390
+ }
391
+
392
+ async function observationsForTrace(config, traceId, since) {
393
+ if (!traceId) return [];
394
+ const params = { limit: 100, fields: "core,basic,usage", traceId };
395
+ try {
396
+ return dataArray(await langfuseGet(config, "/v2/observations", params));
397
+ } catch (error) {
398
+ if (error.status !== 400 && error.status !== 404) throw error;
399
+ }
400
+ try {
401
+ return dataArray(await langfuseGet(config, "/observations", { limit: 100, traceId }));
402
+ } catch (error) {
403
+ if (error.status !== 400) throw error;
404
+ }
395
405
  const fallback = dataArray(await langfuseGetLenient(config, "/observations", { limit: 100, fromTimestamp: since.toISOString() }));
396
406
  return fallback.filter((item) => item.traceId === traceId || item.trace_id === traceId);
397
407
  }
398
408
 
399
409
  function mergeMetricCandidates(items) {
400
410
  const out = [];
401
- const seen = new Set();
402
- for (const item of items || []) {
403
- if (!item || typeof item !== "object") continue;
404
- const key = [idOf(item), item.name || "", item.traceId || item.trace_id || ""].join(":");
405
- if (seen.has(key)) continue;
406
- seen.add(key);
407
- out.push(item);
411
+ const seen = new Set();
412
+ for (const item of items || []) {
413
+ if (!item || typeof item !== "object") continue;
414
+ const key = idOf(item) || [item.name || "", item.traceId || item.trace_id || ""].join(":");
415
+ if (seen.has(key)) continue;
416
+ seen.add(key);
417
+ out.push(item);
408
418
  }
409
419
  return out;
410
420
  }
@@ -452,17 +462,17 @@ async function verifyMetricObservations(config, found, { since, target, marker =
452
462
  throw new Error(`Metric verification failed for ${target}: Skill Use observations should not be emitted as standalone metrics.`);
453
463
  }
454
464
 
455
- if (!interactions.length) {
456
- throw new Error(`Metric verification failed for ${target}: Agent Turn observation was not found for trace ${traceId || found.id}.`);
465
+ if (!interactions.length) {
466
+ throw new Error(`Metric verification failed for ${target}: Agent Turn observation was not found for trace ${traceId || found.id}.`);
457
467
  }
458
468
 
459
469
  const expectedName = expectedAgentTurnName(target);
460
- if (!interactions.some((item) => item?.name === expectedName)) {
470
+ if (target !== "opencode" && !interactions.some((item) => item?.name === expectedName)) {
461
471
  const names = interactions.map((item) => item?.name || "<unnamed>").join(", ");
462
472
  throw new Error(`Metric verification failed for ${target}: expected observation name ${expectedName}, found ${names}.`);
463
473
  }
464
-
465
- const byInteractionId = new Map();
474
+
475
+ const byInteractionId = new Map();
466
476
  const seenInteractionIds = new Set();
467
477
  for (const item of interactions) {
468
478
  const interactionId = metricInteractionId(item, target);
@@ -477,13 +487,13 @@ async function verifyMetricObservations(config, found, { since, target, marker =
477
487
  throw new Error(`Metric verification failed for ${target}: Agent Turn is missing ${key}.`);
478
488
  }
479
489
  }
480
- if (target === "opencode") {
481
- for (const key of ["interaction_id", "interaction_count", "token_metrics_available", "tool_call_count", "skill_use_count", "input_tokens", "output_tokens", "total_tokens"]) {
482
- if (directMetadataValue(item, key) === undefined) {
483
- throw new Error(`Metric verification failed for ${target}: visible metadata is missing ${key}.`);
484
- }
485
- }
486
- }
490
+ if (target === "opencode") {
491
+ for (const key of ["interaction_id", "interaction_count", "token_metrics_available", "tool_call_count", "skill_use_count", "input_tokens", "output_tokens", "total_tokens"]) {
492
+ if (metadataValue(item, key) === undefined) {
493
+ throw new Error(`Metric verification failed for ${target}: effective metadata is missing ${key}.`);
494
+ }
495
+ }
496
+ }
487
497
  const tokenAvailable = metadataValue(item, "token_metrics_available");
488
498
  for (const tokenKey of ["input_tokens", "output_tokens", "total_tokens"]) {
489
499
  const value = metadataValue(item, tokenKey);
@@ -632,12 +642,12 @@ async function main() {
632
642
  }
633
643
  }
634
644
 
635
- const found = await pollLangfuse(config, marker, { ...args, since, target });
636
- console.log(`[OK] Langfuse marker found for ${target}: ${found.kind} ${found.id || ""}`.trim());
637
- const metrics = await verifyMetricObservations(config, found, { since, target, marker });
638
- console.log(`[OK] Langfuse metrics found for ${target}: ${expectedAgentTurnName(target)} x${metrics.interactionCount}`);
639
- results.push({ target, marker, langfuse: { kind: found.kind, id: found.id || "", traceId: metrics.traceId }, metrics });
640
- }
645
+ const found = await pollLangfuse(config, marker, { ...args, since, target });
646
+ console.log(`[OK] Langfuse marker found for ${target}: ${found.kind} ${found.id || ""}`.trim());
647
+ const metrics = await verifyMetricObservations(config, found, { since, target, marker });
648
+ console.log(`[OK] Langfuse metrics found for ${target}: ${expectedAgentTurnName(target)} x${metrics.interactionCount}`);
649
+ results.push({ target, marker, langfuse: { kind: found.kind, id: found.id || "", traceId: metrics.traceId }, metrics });
650
+ }
641
651
 
642
652
  console.log("");
643
653
  console.log(JSON.stringify({ ok: true, package: packageJson.name, marker, results }, null, 2));
@@ -126,6 +126,7 @@ function runNodeScript(script, args, options = {}) {
126
126
  const result = spawnSync(process.execPath, [path.join(rootDir, "scripts", script), ...args], {
127
127
  cwd: rootDir,
128
128
  stdio: "inherit",
129
+ encoding: "utf8",
129
130
  windowsHide: true,
130
131
  timeout: options.timeoutMs || 900000,
131
132
  });
@@ -254,9 +255,9 @@ async function main() {
254
255
  printUpdateSummary(targets);
255
256
  }
256
257
 
257
- main()
258
- .then(() => process.exit(0))
259
- .catch((error) => {
260
- console.error(`[FAIL] ${error?.message || String(error)}`);
261
- process.exit(1);
262
- });
258
+ main()
259
+ .then(() => process.exit(0))
260
+ .catch((error) => {
261
+ console.error(`[FAIL] ${error?.message || String(error)}`);
262
+ process.exit(1);
263
+ });
@@ -1,162 +1,162 @@
1
- import fs from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { extractVersionFromNpmMetadata } from "./update-utils.mjs";
5
- import { readRuntimeState, runtimeStatePath } from "./runtime-state-utils.mjs";
6
- import {
7
- buildVerifyUpdateResults,
8
- normalizeVerifyTargets,
9
- targetLabel,
10
- verifyUpdateExitCode,
11
- } from "./verify-update-utils.mjs";
12
-
13
- const colorEnabled = process.stdout.isTTY && process.env.NO_COLOR !== "1";
14
- const ansi = (code) => (colorEnabled ? `\x1b[${code}m` : "");
15
- const t = {
16
- reset: ansi(0),
17
- bold: ansi(1),
18
- green: ansi("92"),
19
- gold: ansi("93"),
20
- red: ansi("91"),
21
- cyan: ansi("96"),
22
- };
23
-
24
- function paint(text, ...styles) {
25
- if (!colorEnabled) return text;
26
- return `${styles.join("")}${text}${t.reset}`;
27
- }
28
-
29
- function parseArgs(argv) {
30
- const args = { _: [] };
31
- for (const raw of argv) {
32
- if (!raw.startsWith("--")) {
33
- args._.push(raw);
34
- continue;
35
- }
36
- const eq = raw.indexOf("=");
37
- if (eq === -1) args[raw.slice(2)] = true;
38
- else args[raw.slice(2, eq)] = raw.slice(eq + 1);
39
- }
40
- return args;
41
- }
42
-
43
- function stripBom(s) {
44
- return typeof s === "string" && s.charCodeAt(0) === 0xfeff ? s.slice(1) : s;
45
- }
46
-
47
- function readJsonIfExists(p) {
48
- try {
49
- if (!fs.existsSync(p)) return null;
50
- const text = stripBom(fs.readFileSync(p, "utf8"));
51
- if (!text.trim()) return null;
52
- return JSON.parse(text);
53
- } catch {
54
- return null;
55
- }
56
- }
57
-
58
- function codexHome(home = os.homedir()) {
59
- return process.env.CODEX_HOME || path.join(home, ".codex");
60
- }
61
-
62
- function detectInstalledTargets(home = os.homedir()) {
63
- return {
64
- claude: fs.existsSync(path.join(home, ".claude", "hooks", "langfuse_hook.py")),
65
- opencode:
66
- fs.existsSync(path.join(home, ".config", "opencode", "opencode.json")) ||
67
- fs.existsSync(path.join(home, ".config", "opencode", "plugins", "opencode-plugin-langfuse")),
68
- codex: fs.existsSync(path.join(codexHome(home), "hooks", "codex_langfuse_notify.py")),
69
- };
70
- }
71
-
72
- async function latestVersion(packageName, registry = "https://registry.npmjs.org") {
73
- const base = registry.replace(/\/+$/, "");
74
- const response = await fetch(`${base}/${packageName}`, { headers: { accept: "application/json" } });
75
- if (!response.ok) throw new Error(`npm registry ${response.status} ${response.statusText}`);
76
- return extractVersionFromNpmMetadata(await response.json());
77
- }
78
-
79
- function targetsToVerify(targetArg, installed, runtimeTargets) {
80
- const requested = normalizeVerifyTargets(targetArg);
81
- if (String(targetArg || "all").trim().toLowerCase() !== "all") return { targets: requested, skipped: [] };
82
-
83
- const targets = requested.filter((target) => installed[target] || runtimeTargets[target]);
84
- const skipped = requested
85
- .filter((target) => !installed[target] && !runtimeTargets[target])
86
- .map((target) => ({ target, reason: "not_detected" }));
87
- return { targets, skipped };
88
- }
89
-
90
- function printResult(result) {
91
- const label = targetLabel(result.target);
92
- if (result.status === "current") {
93
- console.log(paint(`[OK] ${label} Langfuse runtime is current: ${result.packageName}@${result.installedVersion}`, t.bold, t.green));
94
- if (result.updatedAt) console.log(`[INFO] ${label} updatedAt: ${result.updatedAt}`);
95
- return;
96
- }
97
- if (result.status === "outdated") {
98
- console.log(
99
- paint(
100
- `[FAIL] ${label} Langfuse runtime is outdated: installed ${result.packageName}@${result.installedVersion}, latest ${result.packageName}@${result.latestVersion}`,
101
- t.bold,
102
- t.red,
103
- ),
104
- );
105
- const fix =
106
- result.packageName === "oh-aicoding-tool"
107
- ? `npx oh-aicoding-tool@latest langfuse update ${result.target}`
108
- : `npx oh-langfuse@latest update ${result.target}`;
109
- console.log(paint(`[FIX] Run: ${fix}`, t.bold, t.cyan));
110
- return;
111
- }
112
- console.log(paint(`[FAIL] ${label} Langfuse runtime record is missing.`, t.bold, t.red));
113
- console.log(`[INFO] Runtime state: ${runtimeStatePath()}`);
114
- console.log(paint(`[FIX] Run: npx oh-langfuse@latest update ${result.target}`, t.bold, t.cyan));
115
- }
116
-
117
- async function main() {
118
- const args = parseArgs(process.argv.slice(2));
119
- const targetArg = args._[0] || "all";
120
- const state = readRuntimeState();
121
- const runtimeTargets = state.targets || {};
122
- const installed = detectInstalledTargets();
123
- const { targets, skipped } = targetsToVerify(targetArg, installed, runtimeTargets);
124
-
125
- for (const item of skipped) {
126
- console.log(paint(`[SKIP] ${targetLabel(item.target)} not installed`, t.bold, t.gold));
127
- }
128
-
129
- if (!targets.length) {
130
- console.log(paint("[FAIL] No installed Langfuse runtime targets were found.", t.bold, t.red));
131
- console.log(paint("[FIX] Run setup or update first: npx oh-langfuse@latest update all", t.bold, t.cyan));
132
- return 2;
133
- }
134
-
135
- let latestVersions = {};
136
- try {
137
- const packageNames = new Set(["oh-langfuse"]);
138
- for (const target of targets) {
139
- const packageName = String(runtimeTargets[target]?.packageName || "oh-langfuse").trim() || "oh-langfuse";
140
- packageNames.add(packageName);
141
- }
142
- for (const packageName of packageNames) {
143
- latestVersions[packageName] = await latestVersion(packageName, args.npmRegistry);
144
- }
145
- } catch (error) {
146
- console.error(paint(`[FAIL] Could not query npm latest version: ${error.message}`, t.bold, t.red));
147
- return 3;
148
- }
149
-
150
- const results = buildVerifyUpdateResults({ targets, runtimeTargets, latestVersions });
151
- for (const result of results) printResult(result);
152
- return verifyUpdateExitCode(results);
153
- }
154
-
155
- main()
156
- .then((code) => {
157
- process.exit(code);
158
- })
159
- .catch((error) => {
160
- console.error(paint(`[FAIL] ${error?.message || String(error)}`, t.bold, t.red));
161
- process.exit(3);
162
- });
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { extractVersionFromNpmMetadata } from "./update-utils.mjs";
5
+ import { readRuntimeState, runtimeStatePath } from "./runtime-state-utils.mjs";
6
+ import {
7
+ buildVerifyUpdateResults,
8
+ normalizeVerifyTargets,
9
+ targetLabel,
10
+ verifyUpdateExitCode,
11
+ } from "./verify-update-utils.mjs";
12
+
13
+ const colorEnabled = process.stdout.isTTY && process.env.NO_COLOR !== "1";
14
+ const ansi = (code) => (colorEnabled ? `\x1b[${code}m` : "");
15
+ const t = {
16
+ reset: ansi(0),
17
+ bold: ansi(1),
18
+ green: ansi("92"),
19
+ gold: ansi("93"),
20
+ red: ansi("91"),
21
+ cyan: ansi("96"),
22
+ };
23
+
24
+ function paint(text, ...styles) {
25
+ if (!colorEnabled) return text;
26
+ return `${styles.join("")}${text}${t.reset}`;
27
+ }
28
+
29
+ function parseArgs(argv) {
30
+ const args = { _: [] };
31
+ for (const raw of argv) {
32
+ if (!raw.startsWith("--")) {
33
+ args._.push(raw);
34
+ continue;
35
+ }
36
+ const eq = raw.indexOf("=");
37
+ if (eq === -1) args[raw.slice(2)] = true;
38
+ else args[raw.slice(2, eq)] = raw.slice(eq + 1);
39
+ }
40
+ return args;
41
+ }
42
+
43
+ function stripBom(s) {
44
+ return typeof s === "string" && s.charCodeAt(0) === 0xfeff ? s.slice(1) : s;
45
+ }
46
+
47
+ function readJsonIfExists(p) {
48
+ try {
49
+ if (!fs.existsSync(p)) return null;
50
+ const text = stripBom(fs.readFileSync(p, "utf8"));
51
+ if (!text.trim()) return null;
52
+ return JSON.parse(text);
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ function codexHome(home = os.homedir()) {
59
+ return process.env.CODEX_HOME || path.join(home, ".codex");
60
+ }
61
+
62
+ function detectInstalledTargets(home = os.homedir()) {
63
+ return {
64
+ claude: fs.existsSync(path.join(home, ".claude", "hooks", "langfuse_hook.py")),
65
+ opencode:
66
+ fs.existsSync(path.join(home, ".config", "opencode", "opencode.json")) ||
67
+ fs.existsSync(path.join(home, ".config", "opencode", "plugins", "opencode-plugin-langfuse")),
68
+ codex: fs.existsSync(path.join(codexHome(home), "hooks", "codex_langfuse_notify.py")),
69
+ };
70
+ }
71
+
72
+ async function latestVersion(packageName, registry = "https://registry.npmjs.org") {
73
+ const base = registry.replace(/\/+$/, "");
74
+ const response = await fetch(`${base}/${packageName}`, { headers: { accept: "application/json" } });
75
+ if (!response.ok) throw new Error(`npm registry ${response.status} ${response.statusText}`);
76
+ return extractVersionFromNpmMetadata(await response.json());
77
+ }
78
+
79
+ function targetsToVerify(targetArg, installed, runtimeTargets) {
80
+ const requested = normalizeVerifyTargets(targetArg);
81
+ if (String(targetArg || "all").trim().toLowerCase() !== "all") return { targets: requested, skipped: [] };
82
+
83
+ const targets = requested.filter((target) => installed[target] || runtimeTargets[target]);
84
+ const skipped = requested
85
+ .filter((target) => !installed[target] && !runtimeTargets[target])
86
+ .map((target) => ({ target, reason: "not_detected" }));
87
+ return { targets, skipped };
88
+ }
89
+
90
+ function printResult(result) {
91
+ const label = targetLabel(result.target);
92
+ if (result.status === "current") {
93
+ console.log(paint(`[OK] ${label} Langfuse runtime is current: ${result.packageName}@${result.installedVersion}`, t.bold, t.green));
94
+ if (result.updatedAt) console.log(`[INFO] ${label} updatedAt: ${result.updatedAt}`);
95
+ return;
96
+ }
97
+ if (result.status === "outdated") {
98
+ console.log(
99
+ paint(
100
+ `[FAIL] ${label} Langfuse runtime is outdated: installed ${result.packageName}@${result.installedVersion}, latest ${result.packageName}@${result.latestVersion}`,
101
+ t.bold,
102
+ t.red,
103
+ ),
104
+ );
105
+ const fix =
106
+ result.packageName === "oh-aicoding-tool"
107
+ ? `npx oh-aicoding-tool@latest langfuse update ${result.target}`
108
+ : `npx oh-langfuse@latest update ${result.target}`;
109
+ console.log(paint(`[FIX] Run: ${fix}`, t.bold, t.cyan));
110
+ return;
111
+ }
112
+ console.log(paint(`[FAIL] ${label} Langfuse runtime record is missing.`, t.bold, t.red));
113
+ console.log(`[INFO] Runtime state: ${runtimeStatePath()}`);
114
+ console.log(paint(`[FIX] Run: npx oh-langfuse@latest update ${result.target}`, t.bold, t.cyan));
115
+ }
116
+
117
+ async function main() {
118
+ const args = parseArgs(process.argv.slice(2));
119
+ const targetArg = args._[0] || "all";
120
+ const state = readRuntimeState();
121
+ const runtimeTargets = state.targets || {};
122
+ const installed = detectInstalledTargets();
123
+ const { targets, skipped } = targetsToVerify(targetArg, installed, runtimeTargets);
124
+
125
+ for (const item of skipped) {
126
+ console.log(paint(`[SKIP] ${targetLabel(item.target)} not installed`, t.bold, t.gold));
127
+ }
128
+
129
+ if (!targets.length) {
130
+ console.log(paint("[FAIL] No installed Langfuse runtime targets were found.", t.bold, t.red));
131
+ console.log(paint("[FIX] Run setup or update first: npx oh-langfuse@latest update all", t.bold, t.cyan));
132
+ return 2;
133
+ }
134
+
135
+ let latestVersions = {};
136
+ try {
137
+ const packageNames = new Set(["oh-langfuse"]);
138
+ for (const target of targets) {
139
+ const packageName = String(runtimeTargets[target]?.packageName || "oh-langfuse").trim() || "oh-langfuse";
140
+ packageNames.add(packageName);
141
+ }
142
+ for (const packageName of packageNames) {
143
+ latestVersions[packageName] = await latestVersion(packageName, args.npmRegistry);
144
+ }
145
+ } catch (error) {
146
+ console.error(paint(`[FAIL] Could not query npm latest version: ${error.message}`, t.bold, t.red));
147
+ return 3;
148
+ }
149
+
150
+ const results = buildVerifyUpdateResults({ targets, runtimeTargets, latestVersions });
151
+ for (const result of results) printResult(result);
152
+ return verifyUpdateExitCode(results);
153
+ }
154
+
155
+ main()
156
+ .then((code) => {
157
+ process.exit(code);
158
+ })
159
+ .catch((error) => {
160
+ console.error(paint(`[FAIL] ${error?.message || String(error)}`, t.bold, t.red));
161
+ process.exit(3);
162
+ });