oh-langfuse 0.1.51 → 0.1.53

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/bin/cli.js CHANGED
@@ -830,6 +830,7 @@ async function main() {
830
830
  noSetEnv: !!args["no-set-env"],
831
831
  skipPluginInstall: !!(args["skip-plugin-install"] || args.skipNpmInstall),
832
832
  skipCheck: !!args["skip-check"],
833
+ startupStatus: !!(args["startup-status"] || args.startupStatus),
833
834
  cmd: args.cmd || "",
834
835
  configOverrides: {
835
836
  baseUrl: args.langfuseBaseUrl || args.langfuseHost || args.host || "",
@@ -870,6 +871,7 @@ async function main() {
870
871
  ...(hasValue(options.npmRegistry) ? [`--npmRegistry=${options.npmRegistry}`] : []),
871
872
  ...(hasValue(options.pipIndexUrl) ? [`--pipIndexUrl=${options.pipIndexUrl}`] : []),
872
873
  ...(options.skipCheck ? ["--skip-check"] : []),
874
+ ...(options.startupStatus ? ["--startup-status"] : []),
873
875
  ...(options.yes ? ["--yes"] : []),
874
876
  ], { ...options, quiet: true });
875
877
  }
package/langfuse_hook.py CHANGED
@@ -924,7 +924,7 @@ def emit_turn(
924
924
 
925
925
  # Tool observations
926
926
  for tc in tool_calls:
927
- in_obj = tc["input"]
927
+ in_obj = tc["input"]
928
928
  # truncate tool input if it's a large string payload
929
929
  if isinstance(in_obj, str):
930
930
  in_obj, in_meta = truncate_text(in_obj)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-langfuse",
3
- "version": "0.1.51",
3
+ "version": "0.1.53",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Use npm scripts to configure Claude Code / OpenCode / Codex with Langfuse tracing.",
@@ -93,6 +93,16 @@ function printUpdateCommand(target, updateCommand) {
93
93
  console.log(`${paint("[CMD]", t.bold, t.cyan)} ${updateCommand}`);
94
94
  }
95
95
 
96
+ function shouldPrintStartupStatus(args, env = process.env) {
97
+ const raw = String(env.OH_LANGFUSE_AUTO_UPDATE_STATUS || "").trim().toLowerCase();
98
+ return !!(args["startup-status"] || args.startupStatus || /^(1|true|yes|on)$/i.test(raw));
99
+ }
100
+
101
+ function printAlreadyCurrent(target, version) {
102
+ const label = targetLabel(target);
103
+ console.log(paint(`[OK] ${label} Langfuse \u5df2\u662f\u6700\u65b0\uff1a${packageJson.name}@${version}`, t.bold, t.green));
104
+ }
105
+
96
106
  function runUpdate(target, args) {
97
107
  const updateArgs = ["-y", "oh-langfuse@latest", "update", target];
98
108
  if (args["skip-check"]) updateArgs.push("--skip-check");
@@ -134,8 +144,10 @@ async function main() {
134
144
  const record = getRuntimeInstallRecord(target);
135
145
  const installedVersion = record?.packageVersion || record?.version || "";
136
146
  const needsUpdate = installedVersion ? isNewerVersion(latest, installedVersion) : isNewerVersion(latest, packageJson.version);
137
- if (!needsUpdate && installedVersion) return 0;
138
- if (!needsUpdate && !installedVersion) return 0;
147
+ if (!needsUpdate) {
148
+ if (shouldPrintStartupStatus(args)) printAlreadyCurrent(target, installedVersion || packageJson.version);
149
+ return 0;
150
+ }
139
151
 
140
152
  const message = installedVersion
141
153
  ? `oh-langfuse ${target} runtime update available: ${installedVersion} -> ${latest}.`
@@ -89,7 +89,7 @@ function writeAutoUpdateHelper(target) {
89
89
  const helperDir = autoUpdateHelperDir();
90
90
  ensureDir(helperDir);
91
91
  const runtimePath = autoUpdateRuntimePath();
92
- const fallbackArgs = ["-y", "oh-langfuse@latest", "auto-update", target, "--skip-check"];
92
+ const fallbackArgs = ["-y", "oh-langfuse@latest", "auto-update", target, "--skip-check", "--startup-status"];
93
93
 
94
94
  if (process.platform === "win32") {
95
95
  const helper = path.join(helperDir, `oh-langfuse-auto-update-${target}.cmd`);
@@ -97,7 +97,7 @@ function writeAutoUpdateHelper(target) {
97
97
  "@echo off",
98
98
  "REM Auto-generated by scripts/codex-langfuse-setup.mjs",
99
99
  `if exist ${cmdQuote(runtimePath)} (`,
100
- ` call ${cmdQuote(process.execPath)} ${cmdQuote(runtimePath)} ${target} --skip-check %*`,
100
+ ` call ${cmdQuote(process.execPath)} ${cmdQuote(runtimePath)} ${target} --skip-check --startup-status %*`,
101
101
  ") else (",
102
102
  ` call npx.cmd ${fallbackArgs.map(cmdQuote).join(" ")} %*`,
103
103
  ")",
@@ -114,7 +114,7 @@ function writeAutoUpdateHelper(target) {
114
114
  "# Auto-generated by scripts/codex-langfuse-setup.mjs",
115
115
  "set +e",
116
116
  `if [ -f ${shQuote(runtimePath)} ]; then`,
117
- ` ${shQuote(process.execPath)} ${shQuote(runtimePath)} ${shQuote(target)} --skip-check "$@"`,
117
+ ` ${shQuote(process.execPath)} ${shQuote(runtimePath)} ${shQuote(target)} --skip-check --startup-status "$@"`,
118
118
  "else",
119
119
  ` npx ${fallbackArgs.map(shQuote).join(" ")} "$@"`,
120
120
  "fi",
@@ -161,7 +161,7 @@ function writeAutoUpdateHelper(target) {
161
161
  const helperDir = autoUpdateHelperDir();
162
162
  ensureDir(helperDir);
163
163
  const runtimePath = autoUpdateRuntimePath();
164
- const fallbackArgs = ["-y", "oh-langfuse@latest", "auto-update", target, "--skip-check"];
164
+ const fallbackArgs = ["-y", "oh-langfuse@latest", "auto-update", target, "--skip-check", "--startup-status"];
165
165
 
166
166
  if (process.platform === "win32") {
167
167
  const helper = path.join(helperDir, `oh-langfuse-auto-update-${target}.cmd`);
@@ -169,7 +169,7 @@ function writeAutoUpdateHelper(target) {
169
169
  "@echo off",
170
170
  "REM Auto-generated by scripts/langfuse-setup.mjs",
171
171
  `if exist ${cmdQuote(runtimePath)} (`,
172
- ` call ${cmdQuote(process.execPath)} ${cmdQuote(runtimePath)} ${target} --skip-check %*`,
172
+ ` call ${cmdQuote(process.execPath)} ${cmdQuote(runtimePath)} ${target} --skip-check --startup-status %*`,
173
173
  ") else (",
174
174
  ` call npx.cmd ${fallbackArgs.map(cmdQuote).join(" ")} %*`,
175
175
  ")",
@@ -186,7 +186,7 @@ function writeAutoUpdateHelper(target) {
186
186
  "# Auto-generated by scripts/langfuse-setup.mjs",
187
187
  "set +e",
188
188
  `if [ -f ${shQuote(runtimePath)} ]; then`,
189
- ` ${shQuote(process.execPath)} ${shQuote(runtimePath)} ${shQuote(target)} --skip-check "$@"`,
189
+ ` ${shQuote(process.execPath)} ${shQuote(runtimePath)} ${shQuote(target)} --skip-check --startup-status "$@"`,
190
190
  "else",
191
191
  ` npx ${fallbackArgs.map(shQuote).join(" ")} "$@"`,
192
192
  "fi",
@@ -312,15 +312,17 @@ function getPatchedLangfuseDistIndexJs() {
312
312
  "const skillAgentPath = (detectedBy) => {",
313
313
  " if (detectedBy === 'tool_call') return 'opencode_skill_tool';",
314
314
  " if (detectedBy === 'slash_command') return 'opencode_slash_prompt';",
315
+ " if (detectedBy === 'natural_language_request') return 'opencode_prompt_skill_name';",
315
316
  " if (detectedBy === 'skill_file_path') return 'skill_file_path';",
316
317
  " return detectedBy || 'metadata';",
317
318
  "};",
318
319
  "const skillInvocationMode = (detectedBy) => {",
319
320
  " if (detectedBy === 'slash_command') return 'explicit_request';",
321
+ " if (detectedBy === 'natural_language_request') return 'implicit_request';",
320
322
  " if (detectedBy === 'tool_call' || detectedBy === 'plugin_event') return 'implicit';",
321
323
  " return 'detected';",
322
324
  "};",
323
- "const skillEventType = (detectedBy) => detectedBy === 'slash_command' ? 'requested' : (detectedBy === 'tool_call' || detectedBy === 'plugin_event' ? 'invoked' : 'detected');",
325
+ "const skillEventType = (detectedBy) => (detectedBy === 'slash_command' || detectedBy === 'natural_language_request') ? 'requested' : (detectedBy === 'tool_call' || detectedBy === 'plugin_event' ? 'invoked' : 'detected');",
324
326
  "const skillIdSegment = (name) => String(name || 'unknown').trim().replace(/[^A-Za-z0-9_.:-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 96) || 'unknown';",
325
327
  "",
326
328
  "const escapeRegExp = (value) => String(value).replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');",
@@ -373,6 +375,18 @@ function getPatchedLangfuseDistIndexJs() {
373
375
  " return out;",
374
376
  "};",
375
377
  "",
378
+ "const detectPromptSkillRequests = (haystack, skills) => {",
379
+ " const text = String(haystack || '');",
380
+ " if (!text.trim()) return [];",
381
+ " const out = [];",
382
+ " for (const skillName of skills) {",
383
+ " const escaped = escapeRegExp(skillName);",
384
+ " const pattern = new RegExp(`(?:请\\\\s*)?(?:使用|调用|启用|采用)\\\\s*[\\\"'“”‘’]?${escaped}(?=$|[\\\\s,,。;;::、])|\\\\b(?:use|invoke|run|apply)\\\\s+[\\\"']?${escaped}(?=$|[\\\\s,,。;;::])`, 'i');",
385
+ " if (pattern.test(text)) out.push({ name: skillName, skill_namespace: skillNamespace(skillName), detected_by: 'natural_language_request', skill_call_id: '' });",
386
+ " }",
387
+ " return out;",
388
+ "};",
389
+ "",
376
390
  "const detectOpencodeSkillUsages = (source, knownSkills = []) => {",
377
391
  " const skills = normalizeSkillNames(knownSkills);",
378
392
  " if (skills.length === 0) return [];",
@@ -393,6 +407,12 @@ function getPatchedLangfuseDistIndexJs() {
393
407
  " }",
394
408
  " found.push(...explicit);",
395
409
  " const haystack = collectStrings(source).join('\\n');",
410
+ " const promptSeen = new Set(explicit.map((usage) => usage.name));",
411
+ " for (const usage of detectPromptSkillRequests(haystack, skills)) {",
412
+ " if (promptSeen.has(usage.name)) continue;",
413
+ " promptSeen.add(usage.name);",
414
+ " found.push(usage);",
415
+ " }",
396
416
  " const pathSeen = new Set();",
397
417
  " for (const match of haystack.matchAll(/(?:^|[\"'\\s])(?:[A-Za-z]:)?[^\"'\\n\\r]*[\\\\/]+([^\\\\/\"'\\n\\r]+)[\\\\/]+SKILL\\.md(?=$|[\"'\\s])/gi)) {",
398
418
  " const skillName = match[1];",
@@ -495,9 +515,11 @@ function getPatchedLangfuseDistIndexJs() {
495
515
  " sdk.start();",
496
516
  " const metricsTracer = trace.getTracer('oh-langfuse-opencode-metrics');",
497
517
  " const knownSkillNames = await collectKnownSkillNames();",
518
+ " const startupSkillUsages = detectOpencodeSkillUsages(process.argv.join('\\n'), knownSkillNames);",
498
519
  " const messageTextById = new Map();",
499
520
  " const skillUsagesByMessageId = new Map();",
500
521
  " const skillUsagesBySessionId = new Map();",
522
+ " const startupSkillSessionIds = new Set();",
501
523
  " const toolCallIdsByMessageId = new Map();",
502
524
  " const toolCallIdsBySessionId = new Map();",
503
525
  " const toolResultIdsByMessageId = new Map();",
@@ -566,6 +588,10 @@ function getPatchedLangfuseDistIndexJs() {
566
588
  " const partType = part?.type ?? '';",
567
589
  " const sessionId = pickEventString(part?.sessionID, part?.sessionId, payload?.sessionID, payload?.sessionId, payload?.session?.id, event?.sessionID, event?.sessionId);",
568
590
  " const messageId = pickEventString(part?.messageID, part?.messageId, payload?.messageID, payload?.messageId, payload?.message?.id, event?.messageID, event?.messageId);",
591
+ " if (sessionId && startupSkillUsages.length && !startupSkillSessionIds.has(sessionId)) {",
592
+ " startupSkillSessionIds.add(sessionId);",
593
+ " rememberSkillUsages(skillUsagesBySessionId, sessionId, startupSkillUsages);",
594
+ " }",
569
595
  " const skillDetectionSources = [part, payload, part?.state?.input, part?.input, payload?.input, part?.state?.metadata, payload?.metadata, part?.state?.file, part?.file];",
570
596
  " const eventSkillUsages = detectOpencodeSkillUsages(skillDetectionSources, knownSkillNames);",
571
597
  " rememberSkillUsages(skillUsagesByMessageId, messageId, eventSkillUsages);",
@@ -708,7 +734,7 @@ function writeAutoUpdateHelper(target) {
708
734
  const helperDir = autoUpdateHelperDir();
709
735
  ensureDir(helperDir);
710
736
  const runtimePath = autoUpdateRuntimePath();
711
- const fallbackArgs = ["-y", "oh-langfuse@latest", "auto-update", target, "--skip-check"];
737
+ const fallbackArgs = ["-y", "oh-langfuse@latest", "auto-update", target, "--skip-check", "--startup-status"];
712
738
 
713
739
  if (process.platform === "win32") {
714
740
  const helper = path.join(helperDir, `oh-langfuse-auto-update-${target}.cmd`);
@@ -716,7 +742,7 @@ function writeAutoUpdateHelper(target) {
716
742
  "@echo off",
717
743
  "REM Auto-generated by scripts/opencode-langfuse-setup.mjs",
718
744
  `if exist ${cmdQuote(runtimePath)} (`,
719
- ` call ${cmdQuote(process.execPath)} ${cmdQuote(runtimePath)} ${target} --skip-check %*`,
745
+ ` call ${cmdQuote(process.execPath)} ${cmdQuote(runtimePath)} ${target} --skip-check --startup-status %*`,
720
746
  ") else (",
721
747
  ` call npx.cmd ${fallbackArgs.map(cmdQuote).join(" ")} %*`,
722
748
  ")",
@@ -733,7 +759,7 @@ function writeAutoUpdateHelper(target) {
733
759
  "# Auto-generated by scripts/opencode-langfuse-setup.mjs",
734
760
  "set +e",
735
761
  `if [ -f ${shQuote(runtimePath)} ]; then`,
736
- ` ${shQuote(process.execPath)} ${shQuote(runtimePath)} ${shQuote(target)} --skip-check "$@"`,
762
+ ` ${shQuote(process.execPath)} ${shQuote(runtimePath)} ${shQuote(target)} --skip-check --startup-status "$@"`,
737
763
  "else",
738
764
  ` npx ${fallbackArgs.map(shQuote).join(" ")} "$@"`,
739
765
  "fi",
@@ -339,6 +339,12 @@ function hasMetadataKey(item, key) {
339
339
  return metadataValue(item, key) !== undefined;
340
340
  }
341
341
 
342
+ function metricInteractionId(item, target) {
343
+ return target === "opencode"
344
+ ? directMetadataValue(item, "interaction_id") || metadataValue(item, "interaction_id")
345
+ : metadataValue(item, "interaction_id");
346
+ }
347
+
342
348
  function isAgentTurnObservation(item) {
343
349
  if (item?.name === "Agent Turn" || metadataValue(item, "interaction_count") === 1 || metadataValue(item, "interaction_count") === "1") {
344
350
  return true;
@@ -363,33 +369,76 @@ async function observationsForTrace(config, traceId, since) {
363
369
  return fallback.filter((item) => item.traceId === traceId || item.trace_id === traceId);
364
370
  }
365
371
 
366
- async function verifyMetricObservations(config, found, { since, target }) {
372
+ function mergeMetricCandidates(items) {
373
+ const out = [];
374
+ const seen = new Set();
375
+ for (const item of items || []) {
376
+ if (!item || typeof item !== "object") continue;
377
+ const key = [idOf(item), item.name || "", item.traceId || item.trace_id || ""].join(":");
378
+ if (seen.has(key)) continue;
379
+ seen.add(key);
380
+ out.push(item);
381
+ }
382
+ return out;
383
+ }
384
+
385
+ async function recentMetricCandidates(config, since) {
386
+ const baseParams = { limit: 100, fromTimestamp: since.toISOString() };
387
+ const candidates = [];
388
+ for (const pathname of ["/traces"]) {
389
+ for (const params of [{ ...baseParams, userId: config.userId }, baseParams]) {
390
+ try {
391
+ candidates.push(...dataArray(await langfuseGetLenient(config, pathname, params)));
392
+ } catch (error) {
393
+ if (error.name === "AbortError" || error.status === 404) continue;
394
+ throw error;
395
+ }
396
+ }
397
+ }
398
+ return mergeMetricCandidates(candidates);
399
+ }
400
+
401
+ async function verifyMetricObservations(config, found, { since, target, marker = "" }) {
367
402
  const traceId = traceIdOfFound(found);
368
- let observations = await observationsForTrace(config, traceId, since);
403
+ let observations = mergeMetricCandidates([...(await observationsForTrace(config, traceId, since)), found?.item]);
369
404
 
370
405
  if (!observations.length && Array.isArray(found?.item?.observations)) {
371
- observations = found.item.observations.filter((item) => typeof item === "object");
406
+ observations = mergeMetricCandidates(found.item.observations.filter((item) => typeof item === "object"));
407
+ } else if (Array.isArray(found?.item?.observations)) {
408
+ observations = mergeMetricCandidates([
409
+ ...observations,
410
+ ...found.item.observations.filter((item) => typeof item === "object"),
411
+ ]);
412
+ }
413
+
414
+ let skillUseObservations = observations.filter((item) => item?.name === "Skill Use");
415
+ let interactions = observations.filter(isAgentTurnObservation);
416
+
417
+ if (!interactions.length) {
418
+ const recentRelated = (await recentMetricCandidates(config, since)).filter((item) => marker && containsMarker(item, marker));
419
+ observations = mergeMetricCandidates([...observations, ...recentRelated]);
420
+ skillUseObservations = observations.filter((item) => item?.name === "Skill Use");
421
+ interactions = observations.filter(isAgentTurnObservation);
372
422
  }
373
423
 
374
- const skillUseObservations = observations.filter((item) => item?.name === "Skill Use");
375
424
  if (skillUseObservations.length) {
376
- throw new Error(`Metric verification failed for ${target}: Skill Use observations should not be emitted anymore, found ${skillUseObservations.length}.`);
425
+ throw new Error(`Metric verification failed for ${target}: Skill Use observations should not be emitted as standalone metrics.`);
377
426
  }
378
427
 
379
- const interactions = observations.filter(isAgentTurnObservation);
380
428
  if (!interactions.length) {
381
429
  throw new Error(`Metric verification failed for ${target}: Agent Turn observation was not found for trace ${traceId || found.id}.`);
382
430
  }
383
431
 
384
432
  const byInteractionId = new Map();
433
+ const seenInteractionIds = new Set();
385
434
  for (const item of interactions) {
386
- const interactionId = target === "opencode"
387
- ? directMetadataValue(item, "interaction_id") || metadataValue(item, "interaction_id")
388
- : metadataValue(item, "interaction_id");
435
+ const interactionId = metricInteractionId(item, target);
389
436
  if (!interactionId) {
390
437
  throw new Error(`Metric verification failed for ${target}: Agent Turn is missing interaction_id.`);
391
438
  }
392
- byInteractionId.set(interactionId, (byInteractionId.get(interactionId) || 0) + 1);
439
+ if (seenInteractionIds.has(interactionId)) continue;
440
+ seenInteractionIds.add(interactionId);
441
+ byInteractionId.set(interactionId, 1);
393
442
  for (const key of ["user_id", "token_metrics_available", "tool_call_count", "skill_use_count", "metrics_schema_version"]) {
394
443
  if (!hasMetadataKey(item, key)) {
395
444
  throw new Error(`Metric verification failed for ${target}: Agent Turn is missing ${key}.`);
@@ -415,12 +464,6 @@ async function verifyMetricObservations(config, found, { since, target }) {
415
464
  }
416
465
  }
417
466
 
418
- for (const [interactionId, count] of byInteractionId.entries()) {
419
- if (count !== 1) {
420
- throw new Error(`Metric verification failed for ${target}: interaction_id ${interactionId} appeared ${count} times.`);
421
- }
422
- }
423
-
424
467
  return { traceId, interactionCount: interactions.length };
425
468
  }
426
469
 
@@ -558,7 +601,7 @@ async function main() {
558
601
 
559
602
  const found = await pollLangfuse(config, marker, { ...args, since, target });
560
603
  console.log(`[OK] Langfuse marker found for ${target}: ${found.kind} ${found.id || ""}`.trim());
561
- const metrics = await verifyMetricObservations(config, found, { since, target });
604
+ const metrics = await verifyMetricObservations(config, found, { since, target, marker });
562
605
  console.log(`[OK] Langfuse metrics found for ${target}: Agent Turn x${metrics.interactionCount}`);
563
606
  results.push({ target, marker, langfuse: { kind: found.kind, id: found.id || "", traceId: metrics.traceId }, metrics });
564
607
  }