oh-langfuse 0.1.79 → 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.
@@ -2,28 +2,28 @@ import fs from "node:fs";
2
2
  import fsp from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import os from "node:os";
5
- import { execFileSync, spawnSync } from "node:child_process";
6
- import { fileURLToPath } from "node:url";
7
- import { defaultWindowsNpmCliCandidates, listSystemCliCandidates } from "./cli-detection-utils.mjs";
8
- import { writeRuntimeInstallRecord } from "./runtime-state-utils.mjs";
9
-
10
- const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
11
- const DEFAULT_LANGFUSE_BASE_URL = "https://metrics.openharmonyinsight.cn";
12
- const LEGACY_LANGFUSE_BASE_URLS = new Set([
13
- "http://120.46.221.227:3000",
14
- "https://120.46.221.227:3000",
15
- ]);
16
- const USER_ID_PATTERN = /^[a-z](?:\d{8}|wx\d{7})$/;
17
- const USER_ID_PATTERN_TEXT = "^[a-z](?:\\d{8}|wx\\d{7})$";
18
-
19
- function normalizeLegacyBaseUrl(baseUrl) {
20
- const value = String(baseUrl || "").trim().replace(/\/+$/, "");
21
- return LEGACY_LANGFUSE_BASE_URLS.has(value) ? DEFAULT_LANGFUSE_BASE_URL : baseUrl;
22
- }
23
-
24
- function normalizeUserId(v) {
25
- return String(v || "").trim();
26
- }
5
+ import { execFileSync, spawnSync } from "node:child_process";
6
+ import { fileURLToPath } from "node:url";
7
+ import { defaultWindowsNpmCliCandidates, listSystemCliCandidates } from "./cli-detection-utils.mjs";
8
+ import { writeRuntimeInstallRecord } from "./runtime-state-utils.mjs";
9
+
10
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
11
+ const DEFAULT_LANGFUSE_BASE_URL = "https://metrics.openharmonyinsight.cn";
12
+ const LEGACY_LANGFUSE_BASE_URLS = new Set([
13
+ "http://120.46.221.227:3000",
14
+ "https://120.46.221.227:3000",
15
+ ]);
16
+ const USER_ID_PATTERN = /^[a-z](?:\d{8}|wx\d{7})$/;
17
+ const USER_ID_PATTERN_TEXT = "^[a-z](?:\\d{8}|wx\\d{7})$";
18
+
19
+ function normalizeLegacyBaseUrl(baseUrl) {
20
+ const value = String(baseUrl || "").trim().replace(/\/+$/, "");
21
+ return LEGACY_LANGFUSE_BASE_URLS.has(value) ? DEFAULT_LANGFUSE_BASE_URL : baseUrl;
22
+ }
23
+
24
+ function normalizeUserId(v) {
25
+ return String(v || "").trim();
26
+ }
27
27
 
28
28
  function assertValidUserId(userId) {
29
29
  if (!USER_ID_PATTERN.test(normalizeUserId(userId))) {
@@ -78,63 +78,63 @@ function pythonExecutableInVenv(venvDir) {
78
78
  : path.join(venvDir, "bin", "python");
79
79
  }
80
80
 
81
- function pythonEnv(extraPythonPath = "") {
82
- const env = { ...process.env, PYTHONUTF8: "1" };
83
- if (extraPythonPath) {
84
- env.PYTHONPATH = process.env.PYTHONPATH ? `${extraPythonPath}${path.delimiter}${process.env.PYTHONPATH}` : extraPythonPath;
85
- }
86
- return env;
87
- }
88
-
89
- function pythonCanImport(pythonCmd, moduleName, extraPythonPath = "") {
90
- try {
91
- execFileSync(pythonCmd, ["-c", `import ${moduleName}`], { stdio: "ignore", encoding: "utf8", env: pythonEnv(extraPythonPath) });
92
- return true;
93
- } catch {
94
- return false;
95
- }
96
- }
97
-
98
- function installLangfuseIntoTargetDir({ pythonCmd, baseDir, pipIndexUrl }) {
99
- const targetDir = path.join(baseDir, "langfuse-python");
100
- ensureDir(targetDir);
101
- console.log(`Installing Python package into isolated target: ${targetDir}`);
102
- execFileSync(
103
- pythonCmd,
104
- ["-m", "pip", "install", "--target", targetDir, "--upgrade", "langfuse", "-i", pipIndexUrl],
105
- { stdio: "inherit", encoding: "utf8", env: pythonEnv() }
106
- );
107
- if (!pythonCanImport(pythonCmd, "langfuse", targetDir)) {
108
- throw new Error(`langfuse was installed into ${targetDir}, but ${pythonCmd} still cannot import it with PYTHONPATH.`);
109
- }
110
- return { python: pythonCmd, pythonPath: targetDir };
111
- }
112
-
113
- function runPipInstallWithFallback({ pythonCmd, baseDir, pipIndexUrl }) {
114
- const attempts = [
115
- () => installLangfuseIntoTargetDir({ pythonCmd, baseDir, pipIndexUrl }),
116
- () => installLangfuseIntoTargetDir({ pythonCmd: "python3", baseDir, pipIndexUrl }),
117
- () => installLangfuseIntoTargetDir({ pythonCmd: "python", baseDir, pipIndexUrl })
118
- ];
119
-
120
- const errors = [];
121
- for (const attempt of attempts) {
122
- try {
123
- return attempt();
124
- } catch (error) {
125
- errors.push(error?.message || String(error));
126
- }
127
- }
128
-
129
- throw new Error(
130
- `Failed to install langfuse into isolated Python target. Last errors: ${errors.join(" | ")}`
131
- );
132
- }
133
-
134
- function installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl }) {
135
- console.log("Python venv is unavailable; falling back to isolated PYTHONPATH install for langfuse.");
136
- return runPipInstallWithFallback({ pythonCmd, baseDir, pipIndexUrl });
137
- }
81
+ function pythonEnv(extraPythonPath = "") {
82
+ const env = { ...process.env, PYTHONUTF8: "1" };
83
+ if (extraPythonPath) {
84
+ env.PYTHONPATH = process.env.PYTHONPATH ? `${extraPythonPath}${path.delimiter}${process.env.PYTHONPATH}` : extraPythonPath;
85
+ }
86
+ return env;
87
+ }
88
+
89
+ function pythonCanImport(pythonCmd, moduleName, extraPythonPath = "") {
90
+ try {
91
+ execFileSync(pythonCmd, ["-c", `import ${moduleName}`], { stdio: "ignore", encoding: "utf8", env: pythonEnv(extraPythonPath) });
92
+ return true;
93
+ } catch {
94
+ return false;
95
+ }
96
+ }
97
+
98
+ function installLangfuseIntoTargetDir({ pythonCmd, baseDir, pipIndexUrl }) {
99
+ const targetDir = path.join(baseDir, "langfuse-python");
100
+ ensureDir(targetDir);
101
+ console.log(`Installing Python package into isolated target: ${targetDir}`);
102
+ execFileSync(
103
+ pythonCmd,
104
+ ["-m", "pip", "install", "--target", targetDir, "--upgrade", "langfuse", "-i", pipIndexUrl],
105
+ { stdio: "inherit", encoding: "utf8", env: pythonEnv() }
106
+ );
107
+ if (!pythonCanImport(pythonCmd, "langfuse", targetDir)) {
108
+ throw new Error(`langfuse was installed into ${targetDir}, but ${pythonCmd} still cannot import it with PYTHONPATH.`);
109
+ }
110
+ return { python: pythonCmd, pythonPath: targetDir };
111
+ }
112
+
113
+ function runPipInstallWithFallback({ pythonCmd, baseDir, pipIndexUrl }) {
114
+ const attempts = [
115
+ () => installLangfuseIntoTargetDir({ pythonCmd, baseDir, pipIndexUrl }),
116
+ () => installLangfuseIntoTargetDir({ pythonCmd: "python3", baseDir, pipIndexUrl }),
117
+ () => installLangfuseIntoTargetDir({ pythonCmd: "python", baseDir, pipIndexUrl })
118
+ ];
119
+
120
+ const errors = [];
121
+ for (const attempt of attempts) {
122
+ try {
123
+ return attempt();
124
+ } catch (error) {
125
+ errors.push(error?.message || String(error));
126
+ }
127
+ }
128
+
129
+ throw new Error(
130
+ `Failed to install langfuse into isolated Python target. Last errors: ${errors.join(" | ")}`
131
+ );
132
+ }
133
+
134
+ function installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl }) {
135
+ console.log("Python venv is unavailable; falling back to isolated PYTHONPATH install for langfuse.");
136
+ return runPipInstallWithFallback({ pythonCmd, baseDir, pipIndexUrl });
137
+ }
138
138
 
139
139
  function shQuote(s) {
140
140
  return `'${String(s).replace(/'/g, "'\"'\"'")}'`;
@@ -160,7 +160,7 @@ function writeAutoUpdateHelper(target) {
160
160
  const helperDir = autoUpdateHelperDir();
161
161
  ensureDir(helperDir);
162
162
  const runtimePath = autoUpdateRuntimePath();
163
- const fallbackArgs = ["-y", "--registry=https://registry.npmjs.org/", "oh-langfuse@latest", "auto-update", target, "--skip-check", "--startup-status"];
163
+ const fallbackArgs = ["-y", "--registry=https://registry.npmjs.org/", "oh-langfuse@latest", "auto-update", target, "--skip-check", "--startup-status"];
164
164
 
165
165
  if (process.platform === "win32") {
166
166
  const helper = path.join(helperDir, `oh-langfuse-auto-update-${target}.cmd`);
@@ -175,7 +175,7 @@ function writeAutoUpdateHelper(target) {
175
175
  "exit /b 0",
176
176
  ""
177
177
  ];
178
- fs.writeFileSync(helper, lines.join(os.EOL), "utf8");
178
+ fs.writeFileSync(helper, lines.join(os.EOL), "utf8");
179
179
  return helper;
180
180
  }
181
181
 
@@ -245,11 +245,11 @@ function listLocalCodexCliCandidates() {
245
245
  return candidates;
246
246
  }
247
247
 
248
- function listCliCandidatesFromPath(target) {
249
- return listSystemCliCandidates(target);
250
- }
251
-
252
- function sortWindowsCliCandidates(candidates) {
248
+ function listCliCandidatesFromPath(target) {
249
+ return listSystemCliCandidates(target);
250
+ }
251
+
252
+ function sortWindowsCliCandidates(candidates) {
253
253
  const extPriority = (candidate) => {
254
254
  const ext = path.extname(String(candidate || "")).toLowerCase();
255
255
  if (ext === ".cmd" || ext === ".bat" || ext === ".exe") return 0;
@@ -258,13 +258,13 @@ function sortWindowsCliCandidates(candidates) {
258
258
  return [...candidates].sort((a, b) => extPriority(a) - extPriority(b));
259
259
  }
260
260
 
261
- function resolveAgentCli({ target, preferred = "", shimDir = "" }) {
262
- const candidates = [
263
- preferred,
264
- ...(target === "codex" ? listLocalCodexCliCandidates() : []),
265
- ...defaultWindowsNpmCliCandidates(target),
266
- ...listCliCandidatesFromPath(target)
267
- ];
261
+ function resolveAgentCli({ target, preferred = "", shimDir = "" }) {
262
+ const candidates = [
263
+ preferred,
264
+ ...(target === "codex" ? listLocalCodexCliCandidates() : []),
265
+ ...defaultWindowsNpmCliCandidates(target),
266
+ ...listCliCandidatesFromPath(target)
267
+ ];
268
268
  for (const candidate of candidates) {
269
269
  const found = existingCliCandidate(candidate, shimDir);
270
270
  if (found) return found;
@@ -318,7 +318,7 @@ function writeAgentCommandShim({ baseDir, target, executable, realCli, publicKey
318
318
  "exit /b %ERRORLEVEL%",
319
319
  ""
320
320
  ].filter(Boolean).join(os.EOL);
321
- fs.writeFileSync(shim, content, "utf8");
321
+ fs.writeFileSync(shim, content, "utf8");
322
322
  return { shim, shimDir };
323
323
  }
324
324
 
@@ -350,7 +350,7 @@ function createAgentLauncher({ baseDir, target, executable }) {
350
350
  `${executable} %*`,
351
351
  ""
352
352
  ].join(os.EOL);
353
- fs.writeFileSync(launcher, content, "utf8");
353
+ fs.writeFileSync(launcher, content, "utf8");
354
354
  return launcher;
355
355
  }
356
356
 
@@ -375,13 +375,13 @@ function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.
375
375
  if (!fs.existsSync(venvPython)) {
376
376
  console.log(`Creating Python virtual environment: ${venvDir}`);
377
377
  try {
378
- execFileSync(pythonCmd, ["-m", "venv", venvDir], { stdio: "inherit", encoding: "utf8", env: { ...process.env, PYTHONUTF8: "1" } });
379
- } catch (e) {
380
- if (process.platform !== "win32") {
381
- return installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl });
382
- }
383
- throw new Error("Failed to create Python venv. Please confirm Python venv support is available.");
384
- }
378
+ execFileSync(pythonCmd, ["-m", "venv", venvDir], { stdio: "inherit", encoding: "utf8", env: { ...process.env, PYTHONUTF8: "1" } });
379
+ } catch (e) {
380
+ if (process.platform !== "win32") {
381
+ return installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl });
382
+ }
383
+ throw new Error("Failed to create Python venv. Please confirm Python venv support is available.");
384
+ }
385
385
  }
386
386
 
387
387
  console.log("Installing/updating Python package in venv: langfuse");
@@ -390,31 +390,31 @@ function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.
390
390
  venvPython,
391
391
  ["-m", "pip", "install", "-U", "langfuse", "-i", pipIndexUrl],
392
392
  { stdio: "inherit", encoding: "utf8", env: { ...process.env, PYTHONUTF8: "1" } }
393
- );
394
- } catch (e) {
395
- if (process.platform !== "win32") {
396
- return installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl });
397
- }
398
- throw new Error(`Failed to install langfuse in venv: ${venvPython} -m pip install -U langfuse -i ${pipIndexUrl}`);
399
- }
400
-
401
- if (!pythonCanImport(venvPython, "langfuse")) {
402
- if (process.platform !== "win32") {
403
- return installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl });
404
- }
405
- throw new Error(`langfuse was installed in venv, but cannot be imported by ${venvPython}`);
406
- }
407
-
408
- return { python: venvPython, pythonPath: "" };
409
- }
410
-
411
- function codexNotifyCommand({ python, pythonPath = "", hookPath }) {
412
- if (pythonPath && process.platform !== "win32") {
413
- const script = `if [ -n "\${PYTHONPATH:-}" ]; then export PYTHONPATH=${shQuote(pythonPath)}:"$PYTHONPATH"; else export PYTHONPATH=${shQuote(pythonPath)}; fi; exec ${shQuote(python)} ${shQuote(hookPath)}`;
414
- return ["sh", "-c", script];
415
- }
416
- return [python, hookPath];
417
- }
393
+ );
394
+ } catch (e) {
395
+ if (process.platform !== "win32") {
396
+ return installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl });
397
+ }
398
+ throw new Error(`Failed to install langfuse in venv: ${venvPython} -m pip install -U langfuse -i ${pipIndexUrl}`);
399
+ }
400
+
401
+ if (!pythonCanImport(venvPython, "langfuse")) {
402
+ if (process.platform !== "win32") {
403
+ return installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl });
404
+ }
405
+ throw new Error(`langfuse was installed in venv, but cannot be imported by ${venvPython}`);
406
+ }
407
+
408
+ return { python: venvPython, pythonPath: "" };
409
+ }
410
+
411
+ function codexNotifyCommand({ python, pythonPath = "", hookPath }) {
412
+ if (pythonPath && process.platform !== "win32") {
413
+ const script = `if [ -n "\${PYTHONPATH:-}" ]; then export PYTHONPATH=${shQuote(pythonPath)}:"$PYTHONPATH"; else export PYTHONPATH=${shQuote(pythonPath)}; fi; exec ${shQuote(python)} ${shQuote(hookPath)}`;
414
+ return ["sh", "-c", script];
415
+ }
416
+ return [python, hookPath];
417
+ }
418
418
 
419
419
  function updateCodexNotify(configPath, notifyCommand) {
420
420
  let content = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
@@ -473,14 +473,14 @@ async function main() {
473
473
  args.publicKey || process.env.LANGFUSE_PUBLIC_KEY || "pk-lf-da0c90a7-6e93-4eb7-bb86-c1047c8d187d";
474
474
  const secretKey =
475
475
  args.secretKey || process.env.LANGFUSE_SECRET_KEY || "sk-lf-0269b85d-bfdc-442c-bfa3-e737954e3315";
476
- const baseUrl = normalizeLegacyBaseUrl(
477
- args.langfuseBaseUrl ||
478
- args.langfuseHost ||
479
- args.host ||
480
- process.env.LANGFUSE_BASEURL ||
481
- process.env.LANGFUSE_HOST ||
482
- DEFAULT_LANGFUSE_BASE_URL
483
- );
476
+ const baseUrl = normalizeLegacyBaseUrl(
477
+ args.langfuseBaseUrl ||
478
+ args.langfuseHost ||
479
+ args.host ||
480
+ process.env.LANGFUSE_BASEURL ||
481
+ process.env.LANGFUSE_HOST ||
482
+ DEFAULT_LANGFUSE_BASE_URL
483
+ );
484
484
  const userId = normalizeUserId(args.userId || args.userid || "");
485
485
  if (!userId || typeof userId !== "string") {
486
486
  throw new Error("缺少参数:--userId=你的工号");
@@ -516,11 +516,11 @@ async function main() {
516
516
  userId,
517
517
  });
518
518
 
519
- const pythonCmd = process.platform === "win32" ? "python" : "python3";
520
- const notifyRuntime = args["skip-pip-install"]
521
- ? { python: pythonCmd, pythonPath: "" }
522
- : createOrUpdateLangfuseVenv({ baseDir: codexHome, pipIndexUrl });
523
- updateCodexNotify(configPath, codexNotifyCommand({ python: notifyRuntime.python, pythonPath: notifyRuntime.pythonPath, hookPath: destHook }));
519
+ const pythonCmd = process.platform === "win32" ? "python" : "python3";
520
+ const notifyRuntime = args["skip-pip-install"]
521
+ ? { python: pythonCmd, pythonPath: "" }
522
+ : createOrUpdateLangfuseVenv({ baseDir: codexHome, pipIndexUrl });
523
+ updateCodexNotify(configPath, codexNotifyCommand({ python: notifyRuntime.python, pythonPath: notifyRuntime.pythonPath, hookPath: destHook }));
524
524
  const codexShimDir = path.join(codexHome, "bin");
525
525
  const realCodexCli = resolveAgentCli({ target: "codex", preferred: args.cmd || "", shimDir: codexShimDir });
526
526
  const directShim = writeAgentCommandShim({
@@ -1,99 +1,99 @@
1
- export function stripBom(s) {
2
- if (typeof s !== "string" || s.length === 0) return s;
3
- return s.charCodeAt(0) === 0xfeff ? s.slice(1) : s;
4
- }
5
-
6
- function stripJsonComments(text) {
7
- let out = "";
8
- let inString = false;
9
- let escaped = false;
10
-
11
- for (let i = 0; i < text.length; i += 1) {
12
- const c = text[i];
13
- const n = text[i + 1];
14
-
15
- if (inString) {
16
- out += c;
17
- if (escaped) escaped = false;
18
- else if (c === "\\") escaped = true;
19
- else if (c === '"') inString = false;
20
- continue;
21
- }
22
-
23
- if (c === '"') {
24
- inString = true;
25
- out += c;
26
- continue;
27
- }
28
-
29
- if (c === "/" && n === "/") {
30
- while (i < text.length && text[i] !== "\n" && text[i] !== "\r") i += 1;
31
- if (i < text.length) out += text[i];
32
- continue;
33
- }
34
-
35
- if (c === "/" && n === "*") {
36
- i += 2;
37
- while (i < text.length - 1 && !(text[i] === "*" && text[i + 1] === "/")) {
38
- out += text[i] === "\n" || text[i] === "\r" ? text[i] : " ";
39
- i += 1;
40
- }
41
- i += 1;
42
- continue;
43
- }
44
-
45
- out += c;
46
- }
47
-
48
- return out;
49
- }
50
-
51
- function removeTrailingCommas(text) {
52
- let out = "";
53
- let inString = false;
54
- let escaped = false;
55
-
56
- for (let i = 0; i < text.length; i += 1) {
57
- const c = text[i];
58
-
59
- if (inString) {
60
- out += c;
61
- if (escaped) escaped = false;
62
- else if (c === "\\") escaped = true;
63
- else if (c === '"') inString = false;
64
- continue;
65
- }
66
-
67
- if (c === '"') {
68
- inString = true;
69
- out += c;
70
- continue;
71
- }
72
-
73
- if (c === ",") {
74
- let j = i + 1;
75
- while (j < text.length && /\s/.test(text[j])) j += 1;
76
- if (text[j] === "}" || text[j] === "]") continue;
77
- }
78
-
79
- out += c;
80
- }
81
-
82
- return out;
83
- }
84
-
85
- export function parseJsonRelaxed(text, filePath = "JSON file") {
86
- const raw = stripBom(text);
87
- try {
88
- return JSON.parse(raw);
89
- } catch (strictError) {
90
- const relaxed = removeTrailingCommas(stripJsonComments(raw));
91
- try {
92
- return JSON.parse(relaxed);
93
- } catch (relaxedError) {
94
- throw new Error(
95
- `Cannot parse ${filePath}: ${relaxedError.message}. Original JSON.parse error: ${strictError.message}`
96
- );
97
- }
98
- }
99
- }
1
+ export function stripBom(s) {
2
+ if (typeof s !== "string" || s.length === 0) return s;
3
+ return s.charCodeAt(0) === 0xfeff ? s.slice(1) : s;
4
+ }
5
+
6
+ function stripJsonComments(text) {
7
+ let out = "";
8
+ let inString = false;
9
+ let escaped = false;
10
+
11
+ for (let i = 0; i < text.length; i += 1) {
12
+ const c = text[i];
13
+ const n = text[i + 1];
14
+
15
+ if (inString) {
16
+ out += c;
17
+ if (escaped) escaped = false;
18
+ else if (c === "\\") escaped = true;
19
+ else if (c === '"') inString = false;
20
+ continue;
21
+ }
22
+
23
+ if (c === '"') {
24
+ inString = true;
25
+ out += c;
26
+ continue;
27
+ }
28
+
29
+ if (c === "/" && n === "/") {
30
+ while (i < text.length && text[i] !== "\n" && text[i] !== "\r") i += 1;
31
+ if (i < text.length) out += text[i];
32
+ continue;
33
+ }
34
+
35
+ if (c === "/" && n === "*") {
36
+ i += 2;
37
+ while (i < text.length - 1 && !(text[i] === "*" && text[i + 1] === "/")) {
38
+ out += text[i] === "\n" || text[i] === "\r" ? text[i] : " ";
39
+ i += 1;
40
+ }
41
+ i += 1;
42
+ continue;
43
+ }
44
+
45
+ out += c;
46
+ }
47
+
48
+ return out;
49
+ }
50
+
51
+ function removeTrailingCommas(text) {
52
+ let out = "";
53
+ let inString = false;
54
+ let escaped = false;
55
+
56
+ for (let i = 0; i < text.length; i += 1) {
57
+ const c = text[i];
58
+
59
+ if (inString) {
60
+ out += c;
61
+ if (escaped) escaped = false;
62
+ else if (c === "\\") escaped = true;
63
+ else if (c === '"') inString = false;
64
+ continue;
65
+ }
66
+
67
+ if (c === '"') {
68
+ inString = true;
69
+ out += c;
70
+ continue;
71
+ }
72
+
73
+ if (c === ",") {
74
+ let j = i + 1;
75
+ while (j < text.length && /\s/.test(text[j])) j += 1;
76
+ if (text[j] === "}" || text[j] === "]") continue;
77
+ }
78
+
79
+ out += c;
80
+ }
81
+
82
+ return out;
83
+ }
84
+
85
+ export function parseJsonRelaxed(text, filePath = "JSON file") {
86
+ const raw = stripBom(text);
87
+ try {
88
+ return JSON.parse(raw);
89
+ } catch (strictError) {
90
+ const relaxed = removeTrailingCommas(stripJsonComments(raw));
91
+ try {
92
+ return JSON.parse(relaxed);
93
+ } catch (relaxedError) {
94
+ throw new Error(
95
+ `Cannot parse ${filePath}: ${relaxedError.message}. Original JSON.parse error: ${strictError.message}`
96
+ );
97
+ }
98
+ }
99
+ }