oh-langfuse 0.1.78 → 0.1.80

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.
@@ -0,0 +1,125 @@
1
+ // Auto-discover OpenCode skill repositories located as a child or sibling of the
2
+ // current working directory, and print an opencode config fragment (JSON, single
3
+ // line) to stdout. The generated opencode shim captures this output into the
4
+ // OPENCODE_CONFIG_CONTENT env var so opencode loads it as an additional merged
5
+ // config, registering those skill folders and allowing all skills regardless of
6
+ // where opencode was launched from.
7
+ //
8
+ // This script is intentionally self-contained (only node built-ins) because it
9
+ // is copied to ~/.config/oh-langfuse/opencode-skills-discover.mjs during setup
10
+ // and invoked standalone via `node <path>` from the generated opencode shim.
11
+ //
12
+ // Discovery rules (matches the typical "launch from a sibling or parent of the
13
+ // skills repo" usage pattern):
14
+ // - cwd/skills, cwd/.opencode/skills, cwd/.opencode/skill (cwd IS a skills repo)
15
+ // - cwd/<child>/skills (cwd is a parent of a skills repo)
16
+ // - ../<sibling>/skills (cwd is a sibling of a skills repo)
17
+ // A `<x>/skills` folder counts when it contains at least one SKILL.md (directly
18
+ // or one level deep, matching the openharmony-skills layout). opencode itself
19
+ // then scans **/SKILL.md inside each registered path.
20
+
21
+ import fs from "node:fs";
22
+ import path from "node:path";
23
+ import os from "node:os";
24
+ import { pathToFileURL } from "node:url";
25
+
26
+ function dirExists(p) {
27
+ try {
28
+ return fs.statSync(p).isDirectory();
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ function hasSkillFile(skillsDir) {
35
+ if (!dirExists(skillsDir)) return false;
36
+ let entries;
37
+ try {
38
+ entries = fs.readdirSync(skillsDir, { withFileTypes: true });
39
+ } catch {
40
+ return false;
41
+ }
42
+ for (const entry of entries) {
43
+ if (entry.isFile() && entry.name === "SKILL.md") return true;
44
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
45
+ try {
46
+ fs.accessSync(path.join(skillsDir, entry.name, "SKILL.md"));
47
+ return true;
48
+ } catch {
49
+ // keep scanning siblings
50
+ }
51
+ }
52
+ return false;
53
+ }
54
+
55
+ function safeReaddir(p) {
56
+ try {
57
+ return fs.readdirSync(p, { withFileTypes: true });
58
+ } catch {
59
+ return [];
60
+ }
61
+ }
62
+
63
+ export function collectSkillRoots(cwd) {
64
+ const roots = new Set();
65
+
66
+ // 1. cwd itself hosts a skills folder (cwd is a skills repo root).
67
+ for (const sub of ["skills", path.join(".opencode", "skills"), path.join(".opencode", "skill")]) {
68
+ const cand = path.join(cwd, sub);
69
+ if (hasSkillFile(cand)) roots.add(path.resolve(cand));
70
+ }
71
+
72
+ // 2. cwd's children host a skills folder (cwd is a parent of skills repos).
73
+ for (const entry of safeReaddir(cwd)) {
74
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
75
+ const cand = path.join(cwd, entry.name, "skills");
76
+ if (hasSkillFile(cand)) roots.add(path.resolve(cand));
77
+ }
78
+
79
+ // 3. cwd's siblings host a skills folder (cwd is a sibling of skills repos).
80
+ const parent = path.dirname(cwd);
81
+ if (parent && parent !== cwd) {
82
+ for (const entry of safeReaddir(parent)) {
83
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
84
+ const cand = path.join(parent, entry.name, "skills");
85
+ if (hasSkillFile(cand)) roots.add(path.resolve(cand));
86
+ }
87
+ }
88
+
89
+ return [...roots];
90
+ }
91
+
92
+ export function buildConfig(cwd) {
93
+ const roots = collectSkillRoots(cwd);
94
+ return {
95
+ $schema: "https://opencode.ai/config.json",
96
+ skills: { paths: roots },
97
+ permission: { skill: { "*": "allow" } }
98
+ };
99
+ }
100
+
101
+ function main() {
102
+ const config = buildConfig(process.cwd());
103
+ // Single line so the calling shim can capture stdout into one env var.
104
+ process.stdout.write(JSON.stringify(config));
105
+ }
106
+
107
+ // Only run main() when invoked directly via `node <path>`, not when imported
108
+ // (the opencode plugin's config hook imports this module to call
109
+ // collectSkillRoots/buildConfig without side effects).
110
+ function isMain() {
111
+ try {
112
+ return import.meta.url === pathToFileURL(process.argv[1] || "").href;
113
+ } catch {
114
+ return false;
115
+ }
116
+ }
117
+
118
+ if (isMain()) {
119
+ try {
120
+ main();
121
+ } catch {
122
+ // Stay silent on failure: if nothing is captured, OPENCODE_CONFIG_CONTENT
123
+ // stays unset and opencode uses its default config loading.
124
+ }
125
+ }
@@ -9,7 +9,11 @@ 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";
12
+ const DEFAULT_LANGFUSE_BASE_URL = "https://metrics.openharmonyinsight.cn";
13
+ const LEGACY_LANGFUSE_BASE_URLS = new Set([
14
+ "http://120.46.221.227:3000",
15
+ "https://120.46.221.227:3000",
16
+ ]);
13
17
  const DEFAULT_LANGFUSE_PUBLIC_KEY = "pk-lf-da0c90a7-6e93-4eb7-bb86-c1047c8d187d";
14
18
  const DEFAULT_LANGFUSE_SECRET_KEY = "sk-lf-0269b85d-bfdc-442c-bfa3-e737954e3315";
15
19
  const OPENCODE_NATIVE_AGENT_TURN_NAMES = new Set(["session.llm:ai.streamText.doStream"]);
@@ -165,10 +169,18 @@ function localCodexCliCandidates() {
165
169
  return [...candidates, "codex.cmd", "codex.exe", "codex"].filter(Boolean);
166
170
  }
167
171
 
172
+ function normalizeLegacyBaseUrl(baseUrl) {
173
+ const value = String(baseUrl || "").trim().replace(/\/+$/, "");
174
+ return LEGACY_LANGFUSE_BASE_URLS.has(value) ? DEFAULT_LANGFUSE_BASE_URL : baseUrl;
175
+ }
176
+
168
177
  function configFromArgs(args) {
169
178
  return {
170
- baseUrl:
171
- String(args.langfuseBaseUrl || args.langfuseHost || args.host || process.env.LANGFUSE_BASEURL || process.env.LANGFUSE_HOST || DEFAULT_LANGFUSE_BASE_URL),
179
+ baseUrl: String(
180
+ normalizeLegacyBaseUrl(
181
+ args.langfuseBaseUrl || args.langfuseHost || args.host || process.env.LANGFUSE_BASEURL || process.env.LANGFUSE_HOST || DEFAULT_LANGFUSE_BASE_URL
182
+ )
183
+ ),
172
184
  publicKey: String(args.publicKey || process.env.LANGFUSE_PUBLIC_KEY || DEFAULT_LANGFUSE_PUBLIC_KEY),
173
185
  secretKey: String(args.secretKey || process.env.LANGFUSE_SECRET_KEY || DEFAULT_LANGFUSE_SECRET_KEY),
174
186
  userId: String(args.userId || args.userid || process.env.LANGFUSE_USER_ID || process.env.CC_USER_ID || "").trim(),
@@ -258,7 +270,7 @@ function basicAuth(publicKey, secretKey) {
258
270
  return `Basic ${Buffer.from(`${publicKey}:${secretKey}`, "utf8").toString("base64")}`;
259
271
  }
260
272
 
261
- async function langfuseGet(config, pathname, params = {}) {
273
+ async function langfuseGet(config, pathname, params = {}) {
262
274
  const url = new URL(`${apiBase(config.baseUrl)}${pathname}`);
263
275
  for (const [key, value] of Object.entries(params)) {
264
276
  if (value !== undefined && value !== null && String(value) !== "") url.searchParams.set(key, String(value));
@@ -278,22 +290,22 @@ async function langfuseGet(config, pathname, params = {}) {
278
290
  error.status = response.status;
279
291
  throw error;
280
292
  }
281
- return await response.json();
282
- }
283
-
284
- function isLangfuseQueryShapeError(error) {
285
- if (!error) return false;
286
- return error.status === 400 || error.status === 422;
287
- }
288
-
289
- async function langfuseGetLenient(config, pathname, params = {}) {
290
- try {
291
- return await langfuseGet(config, pathname, params);
292
- } catch (error) {
293
- if (!isLangfuseQueryShapeError(error)) throw error;
294
- const fallback = {};
295
- for (const key of ["limit", "page", "userId", "fields"]) {
296
- if (params[key] !== undefined) fallback[key] = params[key];
293
+ return await response.json();
294
+ }
295
+
296
+ function isLangfuseQueryShapeError(error) {
297
+ if (!error) return false;
298
+ return error.status === 400 || error.status === 422;
299
+ }
300
+
301
+ async function langfuseGetLenient(config, pathname, params = {}) {
302
+ try {
303
+ return await langfuseGet(config, pathname, params);
304
+ } catch (error) {
305
+ if (!isLangfuseQueryShapeError(error)) throw error;
306
+ const fallback = {};
307
+ for (const key of ["limit", "page", "userId", "fields"]) {
308
+ if (params[key] !== undefined) fallback[key] = params[key];
297
309
  }
298
310
  return await langfuseGet(config, pathname, fallback);
299
311
  }
@@ -359,29 +371,29 @@ function metadataValue(item, key) {
359
371
  return undefined;
360
372
  }
361
373
 
362
- function directMetadataValue(item, key) {
363
- const metadata = item?.metadata || {};
364
- return metadata[key];
365
- }
366
-
367
- function observationIOValue(item, key) {
368
- const metadata = item?.metadata || {};
369
- const attrs = metadata.attributes || {};
370
- for (const value of [
371
- item?.[key],
372
- metadata[key],
373
- attrs[`langfuse.observation.${key}`],
374
- attrs[`langfuse.trace.${key}`],
375
- attrs[`${key}.value`],
376
- ]) {
377
- if (value !== undefined && value !== null && String(value).trim() !== "") return value;
378
- }
379
- return undefined;
380
- }
381
-
382
- function hasMetadataKey(item, key) {
383
- return metadataValue(item, key) !== undefined;
384
- }
374
+ function directMetadataValue(item, key) {
375
+ const metadata = item?.metadata || {};
376
+ return metadata[key];
377
+ }
378
+
379
+ function observationIOValue(item, key) {
380
+ const metadata = item?.metadata || {};
381
+ const attrs = metadata.attributes || {};
382
+ for (const value of [
383
+ item?.[key],
384
+ metadata[key],
385
+ attrs[`langfuse.observation.${key}`],
386
+ attrs[`langfuse.trace.${key}`],
387
+ attrs[`${key}.value`],
388
+ ]) {
389
+ if (value !== undefined && value !== null && String(value).trim() !== "") return value;
390
+ }
391
+ return undefined;
392
+ }
393
+
394
+ function hasMetadataKey(item, key) {
395
+ return metadataValue(item, key) !== undefined;
396
+ }
385
397
 
386
398
  function metricInteractionId(item, target) {
387
399
  return target === "opencode"
@@ -412,18 +424,18 @@ function expectedAgentTurnName(target) {
412
424
  async function observationsForTrace(config, traceId, since) {
413
425
  if (!traceId) return [];
414
426
  const params = { limit: 100, fields: "core,basic,usage", traceId };
415
- try {
416
- return dataArray(await langfuseGet(config, "/v2/observations", params));
417
- } catch (error) {
418
- if (!isLangfuseQueryShapeError(error) && error.status !== 404) throw error;
419
- }
420
- try {
421
- return dataArray(await langfuseGet(config, "/observations", { limit: 100, traceId }));
422
- } catch (error) {
423
- if (!isLangfuseQueryShapeError(error)) throw error;
424
- }
425
- const fallback = dataArray(await langfuseGetLenient(config, "/observations", { limit: 100, fromTimestamp: since.toISOString() }));
426
- return fallback.filter((item) => item.traceId === traceId || item.trace_id === traceId);
427
+ try {
428
+ return dataArray(await langfuseGet(config, "/v2/observations", params));
429
+ } catch (error) {
430
+ if (!isLangfuseQueryShapeError(error) && error.status !== 404) throw error;
431
+ }
432
+ try {
433
+ return dataArray(await langfuseGet(config, "/observations", { limit: 100, traceId }));
434
+ } catch (error) {
435
+ if (!isLangfuseQueryShapeError(error)) throw error;
436
+ }
437
+ const fallback = dataArray(await langfuseGetLenient(config, "/observations", { limit: 100, fromTimestamp: since.toISOString() }));
438
+ return fallback.filter((item) => item.traceId === traceId || item.trace_id === traceId);
427
439
  }
428
440
 
429
441
  function mergeMetricCandidates(items) {
@@ -444,12 +456,12 @@ async function recentMetricCandidates(config, since) {
444
456
  const candidates = [];
445
457
  for (const pathname of ["/traces"]) {
446
458
  for (const params of [{ ...baseParams, userId: config.userId }, baseParams]) {
447
- try {
448
- candidates.push(...dataArray(await langfuseGetLenient(config, pathname, params)));
449
- } catch (error) {
450
- if (error.name === "AbortError" || error.status === 404 || isLangfuseQueryShapeError(error)) continue;
451
- throw error;
452
- }
459
+ try {
460
+ candidates.push(...dataArray(await langfuseGetLenient(config, pathname, params)));
461
+ } catch (error) {
462
+ if (error.name === "AbortError" || error.status === 404 || isLangfuseQueryShapeError(error)) continue;
463
+ throw error;
464
+ }
453
465
  }
454
466
  }
455
467
  return mergeMetricCandidates(candidates);
@@ -527,20 +539,20 @@ async function verifyMetricObservations(config, found, { since, target, marker =
527
539
  throw new Error(`Metric verification failed for ${target}: Agent Turn is missing repo context ${key}.`);
528
540
  }
529
541
  }
530
- if (target === "opencode") {
531
- for (const key of ["interaction_id", "interaction_count", "token_metrics_available", "tool_call_count", "skill_use_count", "input_tokens", "output_tokens", "total_tokens"]) {
532
- if (metadataValue(item, key) === undefined) {
533
- throw new Error(`Metric verification failed for ${target}: effective metadata is missing ${key}.`);
534
- }
535
- }
536
- if (item?.name === expectedName) {
537
- for (const key of ["input", "output"]) {
538
- if (observationIOValue(item, key) === undefined) {
539
- throw new Error(`Metric verification failed for ${target}: ${expectedName} is missing ${key}.`);
540
- }
541
- }
542
- }
543
- }
542
+ if (target === "opencode") {
543
+ for (const key of ["interaction_id", "interaction_count", "token_metrics_available", "tool_call_count", "skill_use_count", "input_tokens", "output_tokens", "total_tokens"]) {
544
+ if (metadataValue(item, key) === undefined) {
545
+ throw new Error(`Metric verification failed for ${target}: effective metadata is missing ${key}.`);
546
+ }
547
+ }
548
+ if (item?.name === expectedName) {
549
+ for (const key of ["input", "output"]) {
550
+ if (observationIOValue(item, key) === undefined) {
551
+ throw new Error(`Metric verification failed for ${target}: ${expectedName} is missing ${key}.`);
552
+ }
553
+ }
554
+ }
555
+ }
544
556
  const tokenAvailable = metadataValue(item, "token_metrics_available");
545
557
  for (const tokenKey of ["input_tokens", "output_tokens", "total_tokens"]) {
546
558
  const value = metadataValue(item, tokenKey);
@@ -569,12 +581,12 @@ async function findLangfuseMarker(config, marker, { since, target }) {
569
581
 
570
582
  for (const params of traceQueries) {
571
583
  let traces;
572
- try {
573
- traces = await langfuseGetLenient(config, "/traces", params);
574
- } catch (error) {
575
- if (error.status === 404 || isLangfuseQueryShapeError(error)) continue;
576
- throw error;
577
- }
584
+ try {
585
+ traces = await langfuseGetLenient(config, "/traces", params);
586
+ } catch (error) {
587
+ if (error.status === 404 || isLangfuseQueryShapeError(error)) continue;
588
+ throw error;
589
+ }
578
590
  for (const trace of dataArray(traces)) {
579
591
  if (containsMarker(trace, marker)) return { kind: "trace-list", target, id: idOf(trace), item: trace };
580
592
  }
@@ -602,10 +614,10 @@ async function findLangfuseMarker(config, marker, { since, target }) {
602
614
  for (const observation of dataArray(observations)) {
603
615
  if (containsMarker(observation, marker)) return { kind: pathname, target, id: idOf(observation), item: observation };
604
616
  }
605
- } catch (error) {
606
- if (error.status === 404 || isLangfuseQueryShapeError(error)) continue;
607
- throw error;
608
- }
617
+ } catch (error) {
618
+ if (error.status === 404 || isLangfuseQueryShapeError(error)) continue;
619
+ throw error;
620
+ }
609
621
  }
610
622
 
611
623
  return null;
@@ -1,71 +1,71 @@
1
1
  import fs from "node:fs";
2
- import path from "node:path";
3
- import os from "node:os";
4
- import { spawnSync } from "node:child_process";
5
- import { listSystemCliCandidates } from "./cli-detection-utils.mjs";
6
-
7
- function isKnownBrokenWindowsOpencodeCandidate(candidate) {
8
- if (process.platform !== "win32") return false;
9
- const normalized = String(candidate || "").replace(/\//g, "\\").toLowerCase();
10
- return normalized.endsWith("\\node_modules\\opencode-ai\\bin\\opencode.exe");
11
- }
12
-
13
- function usableCandidate(candidate) {
14
- return candidate && !isKnownBrokenWindowsOpencodeCandidate(candidate) && fs.existsSync(candidate);
15
- }
16
-
17
- /**
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import { spawnSync } from "node:child_process";
5
+ import { listSystemCliCandidates } from "./cli-detection-utils.mjs";
6
+
7
+ function isKnownBrokenWindowsOpencodeCandidate(candidate) {
8
+ if (process.platform !== "win32") return false;
9
+ const normalized = String(candidate || "").replace(/\//g, "\\").toLowerCase();
10
+ return normalized.endsWith("\\node_modules\\opencode-ai\\bin\\opencode.exe");
11
+ }
12
+
13
+ function usableCandidate(candidate) {
14
+ return candidate && !isKnownBrokenWindowsOpencodeCandidate(candidate) && fs.existsSync(candidate);
15
+ }
16
+
17
+ /**
18
18
  * OpenCode CLI 的官方安装脚本默认会把二进制放到 ~/.opencode/bin;
19
19
  * npm 全局安装则可能出现在 %AppData%\npm\opencode.cmd;PATH 也可能只认识 `opencode`。
20
20
  * @param {string|undefined} preferred --cmd=xxx 或可执行文件路径
21
21
  * @returns {string|null} 可执行的绝对路径,或仅在 PATH 中解析到则用 `opencode`
22
22
  */
23
23
  export function resolveOpencodeCli(preferred) {
24
- if (typeof preferred === "string" && preferred.trim()) {
25
- const p = preferred.trim();
26
- if (usableCandidate(p)) return path.resolve(p);
27
- }
24
+ if (typeof preferred === "string" && preferred.trim()) {
25
+ const p = preferred.trim();
26
+ if (usableCandidate(p)) return path.resolve(p);
27
+ }
28
28
 
29
29
  const home = os.homedir();
30
30
 
31
31
  /** @type {string[]} */
32
- const candidates = [];
33
- if (process.platform === "win32") {
34
- candidates.push(path.join(home, ".opencode", "bin", "opencode.exe"));
35
- if (process.env.APPDATA) {
36
- candidates.push(path.join(process.env.APPDATA, "npm", "node_modules", "opencode-ai", "node_modules", "opencode-windows-x64", "bin", "opencode.exe"));
37
- candidates.push(path.join(process.env.APPDATA, "npm", "node_modules", "opencode-ai", "node_modules", "opencode-windows-x64-baseline", "bin", "opencode.exe"));
38
- candidates.push(path.join(process.env.APPDATA, "npm", "node_modules", "opencode-windows-x64", "bin", "opencode.exe"));
39
- candidates.push(path.join(process.env.APPDATA, "npm", "node_modules", "opencode-windows-x64-baseline", "bin", "opencode.exe"));
40
- }
41
- if (process.env.LOCALAPPDATA) {
42
- candidates.push(path.join(process.env.LOCALAPPDATA, "Programs", "opencode", "opencode.exe"));
43
- }
44
- candidates.push(path.join(home, ".config", "opencode", "bin", "opencode.exe"));
45
- candidates.push(path.join(home, ".config", "opencode", "bin", "opencode.cmd"));
46
- candidates.push(path.join(home, ".opencode", "bin", "opencode.cmd"));
47
- candidates.push(path.join(home, "bin", "opencode.exe"));
48
- if (process.env.APPDATA) {
49
- candidates.push(path.join(process.env.APPDATA, "npm", "opencode.cmd"));
50
- candidates.push(path.join(process.env.APPDATA, "npm", "opencode"));
51
- }
52
- candidates.push(path.join(home, "scoop", "shims", "opencode.exe"));
53
- } else {
54
- candidates.push(path.join(home, ".opencode", "bin", "opencode"));
55
- candidates.push(path.join(home, ".local", "bin", "opencode"));
56
- candidates.push(...listSystemCliCandidates("opencode"));
57
- }
58
-
59
- for (const c of candidates) {
60
- if (usableCandidate(c)) return c;
61
- }
32
+ const candidates = [];
33
+ if (process.platform === "win32") {
34
+ candidates.push(path.join(home, ".opencode", "bin", "opencode.exe"));
35
+ if (process.env.APPDATA) {
36
+ candidates.push(path.join(process.env.APPDATA, "npm", "node_modules", "opencode-ai", "node_modules", "opencode-windows-x64", "bin", "opencode.exe"));
37
+ candidates.push(path.join(process.env.APPDATA, "npm", "node_modules", "opencode-ai", "node_modules", "opencode-windows-x64-baseline", "bin", "opencode.exe"));
38
+ candidates.push(path.join(process.env.APPDATA, "npm", "node_modules", "opencode-windows-x64", "bin", "opencode.exe"));
39
+ candidates.push(path.join(process.env.APPDATA, "npm", "node_modules", "opencode-windows-x64-baseline", "bin", "opencode.exe"));
40
+ }
41
+ if (process.env.LOCALAPPDATA) {
42
+ candidates.push(path.join(process.env.LOCALAPPDATA, "Programs", "opencode", "opencode.exe"));
43
+ }
44
+ candidates.push(path.join(home, ".config", "opencode", "bin", "opencode.exe"));
45
+ candidates.push(path.join(home, ".config", "opencode", "bin", "opencode.cmd"));
46
+ candidates.push(path.join(home, ".opencode", "bin", "opencode.cmd"));
47
+ candidates.push(path.join(home, "bin", "opencode.exe"));
48
+ if (process.env.APPDATA) {
49
+ candidates.push(path.join(process.env.APPDATA, "npm", "opencode.cmd"));
50
+ candidates.push(path.join(process.env.APPDATA, "npm", "opencode"));
51
+ }
52
+ candidates.push(path.join(home, "scoop", "shims", "opencode.exe"));
53
+ } else {
54
+ candidates.push(path.join(home, ".opencode", "bin", "opencode"));
55
+ candidates.push(path.join(home, ".local", "bin", "opencode"));
56
+ candidates.push(...listSystemCliCandidates("opencode"));
57
+ }
58
+
59
+ for (const c of candidates) {
60
+ if (usableCandidate(c)) return c;
61
+ }
62
62
 
63
63
  if (process.platform === "win32") {
64
64
  const r = spawnSync("where.exe", ["opencode"], { encoding: "utf8", windowsHide: true });
65
65
  if (r.status === 0 && r.stdout) {
66
- const line = r.stdout.trim().split(/\r?\n/)[0]?.trim();
67
- if (usableCandidate(line)) return line;
68
- }
66
+ const line = r.stdout.trim().split(/\r?\n/)[0]?.trim();
67
+ if (usableCandidate(line)) return line;
68
+ }
69
69
  } else {
70
70
  const r = spawnSync("which", ["opencode"], { encoding: "utf8" });
71
71
  if (r.status === 0 && r.stdout) {
@@ -1,16 +1,20 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
- import { spawnSync } from "node:child_process";
5
- import { fileURLToPath } from "node:url";
6
- import { hasSystemCli } from "./cli-detection-utils.mjs";
7
- import { buildUpdatePlan, extractVersionFromNpmMetadata } from "./update-utils.mjs";
8
- import { writeRuntimeInstallRecord } from "./runtime-state-utils.mjs";
4
+ import { spawnSync } from "node:child_process";
5
+ import { fileURLToPath } from "node:url";
6
+ import { hasSystemCli } from "./cli-detection-utils.mjs";
7
+ import { buildUpdatePlan, extractVersionFromNpmMetadata } from "./update-utils.mjs";
8
+ import { writeRuntimeInstallRecord } from "./runtime-state-utils.mjs";
9
9
 
10
10
  const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
11
11
  const packageJson = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
12
12
 
13
- const DEFAULT_LANGFUSE_BASE_URL = "http://120.46.221.227:3000";
13
+ const DEFAULT_LANGFUSE_BASE_URL = "https://metrics.openharmonyinsight.cn";
14
+ const LEGACY_LANGFUSE_BASE_URLS = new Set([
15
+ "http://120.46.221.227:3000",
16
+ "https://120.46.221.227:3000",
17
+ ]);
14
18
  const DEFAULT_LANGFUSE_PUBLIC_KEY = "pk-lf-da0c90a7-6e93-4eb7-bb86-c1047c8d187d";
15
19
  const DEFAULT_LANGFUSE_SECRET_KEY = "sk-lf-0269b85d-bfdc-442c-bfa3-e737954e3315";
16
20
 
@@ -60,21 +64,21 @@ function readJsonIfExists(p) {
60
64
  }
61
65
  }
62
66
 
63
- function detectInstalledTargets(home = os.homedir()) {
64
- const codexHome = process.env.CODEX_HOME || path.join(home, ".codex");
65
- return {
66
- claude:
67
- fs.existsSync(path.join(home, ".claude", "hooks", "langfuse_hook.py")) ||
68
- hasSystemCli("claude"),
69
- opencode:
70
- fs.existsSync(path.join(home, ".config", "opencode", "opencode.json")) ||
71
- fs.existsSync(path.join(home, ".config", "opencode", "plugins", "opencode-plugin-langfuse")) ||
72
- hasSystemCli("opencode"),
73
- codex:
74
- fs.existsSync(path.join(codexHome, "hooks", "codex_langfuse_notify.py")) ||
75
- hasSystemCli("codex"),
76
- };
77
- }
67
+ function detectInstalledTargets(home = os.homedir()) {
68
+ const codexHome = process.env.CODEX_HOME || path.join(home, ".codex");
69
+ return {
70
+ claude:
71
+ fs.existsSync(path.join(home, ".claude", "hooks", "langfuse_hook.py")) ||
72
+ hasSystemCli("claude"),
73
+ opencode:
74
+ fs.existsSync(path.join(home, ".config", "opencode", "opencode.json")) ||
75
+ fs.existsSync(path.join(home, ".config", "opencode", "plugins", "opencode-plugin-langfuse")) ||
76
+ hasSystemCli("opencode"),
77
+ codex:
78
+ fs.existsSync(path.join(codexHome, "hooks", "codex_langfuse_notify.py")) ||
79
+ hasSystemCli("codex"),
80
+ };
81
+ }
78
82
 
79
83
  function claudeConfig(home) {
80
84
  const settings = readJsonIfExists(path.join(home, ".claude", "settings.json")) || {};
@@ -108,11 +112,24 @@ function opencodeConfig(home) {
108
112
  };
109
113
  }
110
114
 
115
+ function normalizeLegacyBaseUrl(baseUrl) {
116
+ const value = String(baseUrl || "").trim().replace(/\/+$/, "");
117
+ return LEGACY_LANGFUSE_BASE_URLS.has(value) ? DEFAULT_LANGFUSE_BASE_URL : baseUrl;
118
+ }
119
+
111
120
  function mergedConfig(target, args) {
112
121
  const home = os.homedir();
113
122
  const existing = target === "claude" ? claudeConfig(home) : target === "codex" ? codexConfig(home) : opencodeConfig(home);
123
+ const baseUrl = normalizeLegacyBaseUrl(
124
+ args.langfuseBaseUrl ||
125
+ args.langfuseHost ||
126
+ args.host ||
127
+ existing.baseUrl ||
128
+ process.env.LANGFUSE_BASEURL ||
129
+ DEFAULT_LANGFUSE_BASE_URL
130
+ );
114
131
  const config = {
115
- baseUrl: args.langfuseBaseUrl || args.langfuseHost || args.host || existing.baseUrl || process.env.LANGFUSE_BASEURL || DEFAULT_LANGFUSE_BASE_URL,
132
+ baseUrl,
116
133
  publicKey: args.publicKey || existing.publicKey || process.env.LANGFUSE_PUBLIC_KEY || DEFAULT_LANGFUSE_PUBLIC_KEY,
117
134
  secretKey: args.secretKey || existing.secretKey || process.env.LANGFUSE_SECRET_KEY || DEFAULT_LANGFUSE_SECRET_KEY,
118
135
  userId: args.userId || args.userid || existing.userId || process.env.LANGFUSE_USER_ID || process.env.CC_USER_ID || "",
@@ -57,9 +57,9 @@ echo.
57
57
  call npm run claude:setup -- --userId=%USER_ID% --pyPath=%PY_PATH%
58
58
  if errorlevel 1 exit /b 1
59
59
 
60
- echo.
61
- echo Check:
62
- call npx oh-langfuse@latest check claude
60
+ echo.
61
+ echo Check:
62
+ call npx oh-langfuse@latest check claude
63
63
  exit /b %errorlevel%
64
64
 
65
65
  :OPENCODE
@@ -72,11 +72,11 @@ echo - NO auto-launch: open opencode in a NEW terminal afterwards
72
72
  echo.
73
73
 
74
74
  set "OC_USER_ID="
75
- set /p OC_USER_ID=Enter userId (required) ^>
76
- if "%OC_USER_ID%"=="" (
77
- echo [ERROR] userId is required.
78
- exit /b 1
79
- )
75
+ set /p OC_USER_ID=Enter userId (required) ^>
76
+ if "%OC_USER_ID%"=="" (
77
+ echo [ERROR] userId is required.
78
+ exit /b 1
79
+ )
80
80
 
81
81
  echo.
82
82
  echo Write LANGFUSE_* to Windows user env vars (HKCU)?
@@ -98,7 +98,7 @@ set "OC_CMD="
98
98
  set /p OC_CMD=OpenCode CLI path (optional; press Enter to auto-detect) ^>
99
99
 
100
100
  set "SETUP_ARGS="
101
- set "SETUP_ARGS=%SETUP_ARGS% --userId=%OC_USER_ID%"
101
+ set "SETUP_ARGS=%SETUP_ARGS% --userId=%OC_USER_ID%"
102
102
  if /i "%OC_SET_ENV%"=="n" set "SETUP_ARGS=%SETUP_ARGS% --no-set-env"
103
103
  if /i "%OC_SET_ENV%"=="no" set "SETUP_ARGS=%SETUP_ARGS% --no-set-env"
104
104
  if /i "%OC_SKIP_INSTALL%"=="y" set "SETUP_ARGS=%SETUP_ARGS% --skip-plugin-install"
@@ -120,13 +120,13 @@ if errorlevel 1 goto :OPENCODE_FAIL
120
120
  echo.
121
121
  echo ============================================
122
122
  echo OpenCode setup finished.
123
- echo Next:
124
- echo 1) Open a NEW terminal (HKCU env vars take effect).
125
- echo 2) Run: opencode
126
- echo Optional launcher:
127
- echo %USERPROFILE%\.config\opencode\launch-opencode-langfuse.cmd
128
- echo 3) Check: npx oh-langfuse@latest check opencode
129
- echo ============================================
123
+ echo Next:
124
+ echo 1) Open a NEW terminal (HKCU env vars take effect).
125
+ echo 2) Run: opencode
126
+ echo Optional launcher:
127
+ echo %USERPROFILE%\.config\opencode\launch-opencode-langfuse.cmd
128
+ echo 3) Check: npx oh-langfuse@latest check opencode
129
+ echo ============================================
130
130
  exit /b 0
131
131
 
132
132
  :CODEX
@@ -150,9 +150,9 @@ echo.
150
150
  call npm run codex:setup -- --userId=%CODEX_USER_ID% --pyPath=%CODEX_PY_PATH%
151
151
  if errorlevel 1 exit /b 1
152
152
 
153
- echo.
154
- echo Check:
155
- call npx oh-langfuse@latest check codex
153
+ echo.
154
+ echo Check:
155
+ call npx oh-langfuse@latest check codex
156
156
  exit /b %errorlevel%
157
157
 
158
158
  :NO_NPM