@narumitw/pi-langfuse 0.50.5 → 0.50.6

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/package.json CHANGED
@@ -1,61 +1,57 @@
1
1
  {
2
- "name": "@narumitw/pi-langfuse",
3
- "version": "0.50.5",
4
- "description": "Pi extension that traces LLM generations and tool activity to Langfuse.",
5
- "type": "module",
6
- "license": "MIT",
7
- "private": false,
8
- "keywords": [
9
- "pi-package",
10
- "pi-extension",
11
- "pi",
12
- "langfuse",
13
- "observability",
14
- "tracing"
15
- ],
16
- "files": [
17
- "src",
18
- "dist",
19
- "README.md",
20
- "LICENSE"
21
- ],
22
- "pi": {
23
- "extensions": [
24
- "./dist/index.ts"
25
- ]
26
- },
27
- "piExtension": {
28
- "lifecycle": "stable"
29
- },
30
- "scripts": {
31
- "build": "node scripts/build-runtime.mjs",
32
- "prepack": "npm run build",
33
- "check": "npm run build && biome check .",
34
- "format": "biome check --write .",
35
- "typecheck": "tsc --noEmit"
36
- },
37
- "peerDependencies": {
38
- "@earendil-works/pi-coding-agent": "*"
39
- },
40
- "devDependencies": {
41
- "@biomejs/biome": "2.5.10",
42
- "@earendil-works/pi-coding-agent": "0.84.3",
43
- "@types/node": "26.2.0",
44
- "typescript": "7.0.2",
45
- "esbuild": "0.28.2"
46
- },
47
- "dependencies": {
48
- "@langfuse/otel": "^5.10.1",
49
- "@langfuse/tracing": "^5.10.1",
50
- "@narumitw/pi-tui-kit": "^0.59.0",
51
- "@opentelemetry/api": "^1.9.1",
52
- "@opentelemetry/exporter-trace-otlp-http": "^0.221.0",
53
- "@opentelemetry/sdk-trace-base": "^2.10.0",
54
- "@opentelemetry/sdk-trace-node": "^2.10.0"
55
- },
56
- "repository": {
57
- "type": "git",
58
- "url": "https://github.com/narumiruna/pi-extensions",
59
- "directory": "packages/pi-langfuse"
60
- }
2
+ "name": "@narumitw/pi-langfuse",
3
+ "version": "0.50.6",
4
+ "description": "Pi extension that traces LLM generations and tool activity to Langfuse.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "private": false,
8
+ "keywords": [
9
+ "pi-package",
10
+ "pi-extension",
11
+ "pi",
12
+ "langfuse",
13
+ "observability",
14
+ "tracing"
15
+ ],
16
+ "files": [
17
+ "src",
18
+ "dist",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "pi": {
23
+ "extensions": [
24
+ "./dist/index.ts"
25
+ ]
26
+ },
27
+ "scripts": {
28
+ "build": "node scripts/build-runtime.mjs",
29
+ "prepack": "npm run build",
30
+ "check": "npm run build && biome check .",
31
+ "format": "biome check --write .",
32
+ "typecheck": "tsc --noEmit"
33
+ },
34
+ "peerDependencies": {
35
+ "@earendil-works/pi-coding-agent": "*"
36
+ },
37
+ "devDependencies": {
38
+ "@earendil-works/pi-coding-agent": "0.85.1",
39
+ "@types/node": "26.4.1",
40
+ "typescript": "7.0.2",
41
+ "esbuild": "0.28.2"
42
+ },
43
+ "dependencies": {
44
+ "@langfuse/otel": "^5.11.1",
45
+ "@langfuse/tracing": "^5.11.1",
46
+ "@narumitw/pi-tui-kit": "^0.59.0",
47
+ "@opentelemetry/api": "^1.9.1",
48
+ "@opentelemetry/exporter-trace-otlp-http": "^0.222.0",
49
+ "@opentelemetry/sdk-trace-base": "^2.11.0",
50
+ "@opentelemetry/sdk-trace-node": "^2.11.0"
51
+ },
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "https://github.com/narumiruna/pi-extensions",
55
+ "directory": "packages/pi-langfuse"
56
+ }
61
57
  }
package/src/config.ts CHANGED
@@ -7,287 +7,266 @@ const CONFIG_FILE_NAME = "pi-langfuse.json";
7
7
  export const DEFAULT_BASE_URL = "https://us.cloud.langfuse.com";
8
8
 
9
9
  export interface LangfuseConfig {
10
- publicKey: string;
11
- secretKey: string;
12
- baseUrl: string;
13
- environment?: string;
14
- release?: string;
15
- userId?: string;
16
- captureContent: boolean;
10
+ publicKey: string;
11
+ secretKey: string;
12
+ baseUrl: string;
13
+ environment?: string;
14
+ release?: string;
15
+ userId?: string;
16
+ captureContent: boolean;
17
17
  }
18
18
 
19
19
  export type LangfuseConfigPatch = Partial<LangfuseConfig>;
20
20
 
21
21
  export type LangfuseConfigResult =
22
- | { ok: true; config: LangfuseConfig; path: string; warnings: string[] }
23
- | { ok: false; path: string; warnings: string[]; reason: string };
22
+ | { ok: true; config: LangfuseConfig; path: string; warnings: string[] }
23
+ | { ok: false; path: string; warnings: string[]; reason: string };
24
24
 
25
25
  export function langfuseConfigPath(): string {
26
- return join(getAgentDir(), CONFIG_FILE_NAME);
26
+ return join(getAgentDir(), CONFIG_FILE_NAME);
27
27
  }
28
28
 
29
29
  const configSaveQueues = new Map<string, Promise<void>>();
30
30
 
31
31
  export async function waitForLangfuseConfigWrites(path = langfuseConfigPath()): Promise<void> {
32
- await configSaveQueues.get(path);
32
+ await configSaveQueues.get(path);
33
33
  }
34
34
 
35
- export function writeLangfuseConfig(
36
- patch: LangfuseConfigPatch,
37
- path = langfuseConfigPath(),
38
- ): Promise<LangfuseConfig> {
39
- const previous = configSaveQueues.get(path) ?? Promise.resolve();
40
- const operation = previous.then(() => writeLangfuseConfigNow(patch, path));
41
- const tail = operation.then(
42
- () => undefined,
43
- () => undefined,
44
- );
45
- configSaveQueues.set(path, tail);
46
- void tail.then(() => {
47
- if (configSaveQueues.get(path) === tail) configSaveQueues.delete(path);
48
- });
49
- return operation;
35
+ export function writeLangfuseConfig(patch: LangfuseConfigPatch, path = langfuseConfigPath()): Promise<LangfuseConfig> {
36
+ const previous = configSaveQueues.get(path) ?? Promise.resolve();
37
+ const operation = previous.then(() => writeLangfuseConfigNow(patch, path));
38
+ const tail = operation.then(
39
+ () => undefined,
40
+ () => undefined,
41
+ );
42
+ configSaveQueues.set(path, tail);
43
+ void tail.then(() => {
44
+ if (configSaveQueues.get(path) === tail) configSaveQueues.delete(path);
45
+ });
46
+ return operation;
50
47
  }
51
48
 
52
- async function writeLangfuseConfigNow(
53
- patch: LangfuseConfigPatch,
54
- path: string,
55
- ): Promise<LangfuseConfig> {
56
- const currentDocument = await readLangfuseDocumentForUpdate(path);
57
- const normalized = normalizeLangfuseConfig({ ...currentDocument, ...patch });
58
- if (!normalized.ok) throw new Error(normalized.reason);
59
- const {
60
- publicKey: _publicKey,
61
- secretKey: _secretKey,
62
- baseUrl: _baseUrl,
63
- environment: _environment,
64
- release: _release,
65
- userId: _userId,
66
- captureContent: _captureContent,
67
- ...unknownFields
68
- } = currentDocument;
69
- const nextDocument = { ...unknownFields, ...normalized.config };
49
+ async function writeLangfuseConfigNow(patch: LangfuseConfigPatch, path: string): Promise<LangfuseConfig> {
50
+ const currentDocument = await readLangfuseDocumentForUpdate(path);
51
+ const normalized = normalizeLangfuseConfig({ ...currentDocument, ...patch });
52
+ if (!normalized.ok) throw new Error(normalized.reason);
53
+ const {
54
+ publicKey: _publicKey,
55
+ secretKey: _secretKey,
56
+ baseUrl: _baseUrl,
57
+ environment: _environment,
58
+ release: _release,
59
+ userId: _userId,
60
+ captureContent: _captureContent,
61
+ ...unknownFields
62
+ } = currentDocument;
63
+ const nextDocument = { ...unknownFields, ...normalized.config };
70
64
 
71
- await mkdir(dirname(path), { recursive: true, mode: 0o700 });
72
- const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
73
- try {
74
- await writeFile(tempPath, `${JSON.stringify(nextDocument, null, 2)}\n`, {
75
- encoding: "utf8",
76
- mode: 0o600,
77
- });
78
- await chmod(tempPath, 0o600);
79
- await rename(tempPath, path);
80
- return normalized.config;
81
- } catch (error) {
82
- await rm(tempPath, { force: true }).catch(() => undefined);
83
- throw error;
84
- }
65
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
66
+ const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
67
+ try {
68
+ await writeFile(tempPath, `${JSON.stringify(nextDocument, null, 2)}\n`, {
69
+ encoding: "utf8",
70
+ mode: 0o600,
71
+ });
72
+ await chmod(tempPath, 0o600);
73
+ await rename(tempPath, path);
74
+ return normalized.config;
75
+ } catch (error) {
76
+ await rm(tempPath, { force: true }).catch(() => undefined);
77
+ throw error;
78
+ }
85
79
  }
86
80
 
87
81
  async function readLangfuseDocumentForUpdate(path: string): Promise<Record<string, unknown>> {
88
- let parsed: unknown;
89
- try {
90
- parsed = JSON.parse(await readFile(path, "utf8")) as unknown;
91
- } catch (error) {
92
- if (isNodeError(error) && error.code === "ENOENT") return {};
93
- throw new Error(
94
- `Cannot update ${CONFIG_FILE_NAME} because the existing file is malformed or unreadable; repair it first.`,
95
- );
96
- }
97
- const normalized = normalizeLangfuseConfig(parsed);
98
- if (!normalized.ok) {
99
- throw new Error(
100
- `Cannot update ${CONFIG_FILE_NAME} because the existing file has invalid recognized fields; repair it first.`,
101
- );
102
- }
103
- return { ...(parsed as Record<string, unknown>) };
82
+ let parsed: unknown;
83
+ try {
84
+ parsed = JSON.parse(await readFile(path, "utf8")) as unknown;
85
+ } catch (error) {
86
+ if (isNodeError(error) && error.code === "ENOENT") return {};
87
+ throw new Error(
88
+ `Cannot update ${CONFIG_FILE_NAME} because the existing file is malformed or unreadable; repair it first.`,
89
+ );
90
+ }
91
+ const normalized = normalizeLangfuseConfig(parsed);
92
+ if (!normalized.ok) {
93
+ throw new Error(
94
+ `Cannot update ${CONFIG_FILE_NAME} because the existing file has invalid recognized fields; repair it first.`,
95
+ );
96
+ }
97
+ return { ...(parsed as Record<string, unknown>) };
104
98
  }
105
99
 
106
- export async function loadLangfuseConfig(
107
- path = langfuseConfigPath(),
108
- ): Promise<LangfuseConfigResult> {
109
- await configSaveQueues.get(path);
110
- const warnings: string[] = [];
111
- const permissionFailure = await ensurePrivatePermissions(path, warnings);
112
- if (permissionFailure) {
113
- return {
114
- ok: false,
115
- path,
116
- warnings,
117
- reason: `Refusing to load credentials: ${permissionFailure}`,
118
- };
119
- }
100
+ export async function loadLangfuseConfig(path = langfuseConfigPath()): Promise<LangfuseConfigResult> {
101
+ await configSaveQueues.get(path);
102
+ const warnings: string[] = [];
103
+ const permissionFailure = await ensurePrivatePermissions(path, warnings);
104
+ if (permissionFailure) {
105
+ return {
106
+ ok: false,
107
+ path,
108
+ warnings,
109
+ reason: `Refusing to load credentials: ${permissionFailure}`,
110
+ };
111
+ }
120
112
 
121
- let text: string;
122
- try {
123
- text = await readFile(path, "utf8");
124
- } catch (error) {
125
- if (isNodeError(error) && error.code === "ENOENT") {
126
- return { ok: false, path, warnings, reason: `Configuration file not found: ${path}` };
127
- }
128
- return {
129
- ok: false,
130
- path,
131
- warnings,
132
- reason: `Failed to read ${path}: ${formatError(error)}`,
133
- };
134
- }
113
+ let text: string;
114
+ try {
115
+ text = await readFile(path, "utf8");
116
+ } catch (error) {
117
+ if (isNodeError(error) && error.code === "ENOENT") {
118
+ return { ok: false, path, warnings, reason: `Configuration file not found: ${path}` };
119
+ }
120
+ return {
121
+ ok: false,
122
+ path,
123
+ warnings,
124
+ reason: `Failed to read ${path}: ${formatError(error)}`,
125
+ };
126
+ }
135
127
 
136
- let parsed: unknown;
137
- try {
138
- parsed = JSON.parse(text) as unknown;
139
- } catch {
140
- return {
141
- ok: false,
142
- path,
143
- warnings,
144
- reason: `Failed to parse ${path} as JSON; repair the file before retrying.`,
145
- };
146
- }
128
+ let parsed: unknown;
129
+ try {
130
+ parsed = JSON.parse(text) as unknown;
131
+ } catch {
132
+ return {
133
+ ok: false,
134
+ path,
135
+ warnings,
136
+ reason: `Failed to parse ${path} as JSON; repair the file before retrying.`,
137
+ };
138
+ }
147
139
 
148
- const normalized = normalizeLangfuseConfig(parsed);
149
- if (!normalized.ok) return { ok: false, path, warnings, reason: normalized.reason };
150
- return { ok: true, config: normalized.config, path, warnings };
140
+ const normalized = normalizeLangfuseConfig(parsed);
141
+ if (!normalized.ok) return { ok: false, path, warnings, reason: normalized.reason };
142
+ return { ok: true, config: normalized.config, path, warnings };
151
143
  }
152
144
 
153
145
  export function normalizeLangfuseConfig(
154
- value: unknown,
146
+ value: unknown,
155
147
  ): { ok: true; config: LangfuseConfig } | { ok: false; reason: string } {
156
- if (!value || typeof value !== "object" || Array.isArray(value)) {
157
- return { ok: false, reason: "pi-langfuse.json must contain a JSON object." };
158
- }
159
- const input = value as Record<string, unknown>;
160
- const publicKey = normalizeString(input.publicKey);
161
- const secretKey = normalizeString(input.secretKey);
162
- if (!publicKey) {
163
- return { ok: false, reason: "pi-langfuse.json requires a literal publicKey string." };
164
- }
165
- if (!secretKey) {
166
- return { ok: false, reason: "pi-langfuse.json requires a literal secretKey string." };
167
- }
168
- if (isInterpolation(publicKey)) {
169
- return {
170
- ok: false,
171
- reason:
172
- "pi-langfuse.json publicKey must be literal; environment and command interpolation are not supported.",
173
- };
174
- }
175
- if (isInterpolation(secretKey)) {
176
- return {
177
- ok: false,
178
- reason:
179
- "pi-langfuse.json secretKey must be literal; environment and command interpolation are not supported.",
180
- };
181
- }
148
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
149
+ return { ok: false, reason: "pi-langfuse.json must contain a JSON object." };
150
+ }
151
+ const input = value as Record<string, unknown>;
152
+ const publicKey = normalizeString(input.publicKey);
153
+ const secretKey = normalizeString(input.secretKey);
154
+ if (!publicKey) {
155
+ return { ok: false, reason: "pi-langfuse.json requires a literal publicKey string." };
156
+ }
157
+ if (!secretKey) {
158
+ return { ok: false, reason: "pi-langfuse.json requires a literal secretKey string." };
159
+ }
160
+ if (isInterpolation(publicKey)) {
161
+ return {
162
+ ok: false,
163
+ reason: "pi-langfuse.json publicKey must be literal; environment and command interpolation are not supported.",
164
+ };
165
+ }
166
+ if (isInterpolation(secretKey)) {
167
+ return {
168
+ ok: false,
169
+ reason: "pi-langfuse.json secretKey must be literal; environment and command interpolation are not supported.",
170
+ };
171
+ }
182
172
 
183
- const rawBaseUrl =
184
- input.baseUrl === undefined ? DEFAULT_BASE_URL : normalizeString(input.baseUrl);
185
- if (!rawBaseUrl) {
186
- return { ok: false, reason: "pi-langfuse.json baseUrl must be a non-empty string." };
187
- }
188
- const baseUrl = normalizeBaseUrl(rawBaseUrl);
189
- if (!baseUrl) {
190
- return {
191
- ok: false,
192
- reason:
193
- "pi-langfuse.json baseUrl must use HTTP or HTTPS without credentials, a query, or a fragment.",
194
- };
195
- }
173
+ const rawBaseUrl = input.baseUrl === undefined ? DEFAULT_BASE_URL : normalizeString(input.baseUrl);
174
+ if (!rawBaseUrl) {
175
+ return { ok: false, reason: "pi-langfuse.json baseUrl must be a non-empty string." };
176
+ }
177
+ const baseUrl = normalizeBaseUrl(rawBaseUrl);
178
+ if (!baseUrl) {
179
+ return {
180
+ ok: false,
181
+ reason: "pi-langfuse.json baseUrl must use HTTP or HTTPS without credentials, a query, or a fragment.",
182
+ };
183
+ }
196
184
 
197
- if (input.captureContent !== undefined && typeof input.captureContent !== "boolean") {
198
- return { ok: false, reason: "pi-langfuse.json captureContent must be a boolean." };
199
- }
200
- const environment = optionalString(input.environment, "environment");
201
- if (!environment.ok) return environment;
202
- if (
203
- environment.value &&
204
- (environment.value.length > 40 || !/^(?!langfuse)[a-z0-9_-]+$/u.test(environment.value))
205
- ) {
206
- return {
207
- ok: false,
208
- reason:
209
- "pi-langfuse.json environment must be at most 40 lowercase letters, numbers, hyphens, or underscores and must not start with langfuse.",
210
- };
211
- }
212
- const release = optionalString(input.release, "release");
213
- if (!release.ok) return release;
214
- const userId = optionalString(input.userId, "userId");
215
- if (!userId.ok) return userId;
216
- if (userId.value && userId.value.length > 200) {
217
- return { ok: false, reason: "pi-langfuse.json userId must be at most 200 characters." };
218
- }
185
+ if (input.captureContent !== undefined && typeof input.captureContent !== "boolean") {
186
+ return { ok: false, reason: "pi-langfuse.json captureContent must be a boolean." };
187
+ }
188
+ const environment = optionalString(input.environment, "environment");
189
+ if (!environment.ok) return environment;
190
+ if (environment.value && (environment.value.length > 40 || !/^(?!langfuse)[a-z0-9_-]+$/u.test(environment.value))) {
191
+ return {
192
+ ok: false,
193
+ reason:
194
+ "pi-langfuse.json environment must be at most 40 lowercase letters, numbers, hyphens, or underscores and must not start with langfuse.",
195
+ };
196
+ }
197
+ const release = optionalString(input.release, "release");
198
+ if (!release.ok) return release;
199
+ const userId = optionalString(input.userId, "userId");
200
+ if (!userId.ok) return userId;
201
+ if (userId.value && userId.value.length > 200) {
202
+ return { ok: false, reason: "pi-langfuse.json userId must be at most 200 characters." };
203
+ }
219
204
 
220
- return {
221
- ok: true,
222
- config: {
223
- publicKey,
224
- secretKey,
225
- baseUrl,
226
- ...(environment.value ? { environment: environment.value } : {}),
227
- ...(release.value ? { release: release.value } : {}),
228
- ...(userId.value ? { userId: userId.value } : {}),
229
- captureContent: input.captureContent ?? true,
230
- },
231
- };
205
+ return {
206
+ ok: true,
207
+ config: {
208
+ publicKey,
209
+ secretKey,
210
+ baseUrl,
211
+ ...(environment.value ? { environment: environment.value } : {}),
212
+ ...(release.value ? { release: release.value } : {}),
213
+ ...(userId.value ? { userId: userId.value } : {}),
214
+ captureContent: input.captureContent ?? true,
215
+ },
216
+ };
232
217
  }
233
218
 
234
- async function ensurePrivatePermissions(
235
- path: string,
236
- warnings: string[],
237
- ): Promise<string | undefined> {
238
- try {
239
- const file = await stat(path);
240
- if ((file.mode & 0o777) !== 0o600) {
241
- await chmod(path, 0o600);
242
- const repaired = await stat(path);
243
- if ((repaired.mode & 0o777) !== 0o600) {
244
- throw new Error(`permissions remained ${(repaired.mode & 0o777).toString(8)}`);
245
- }
246
- warnings.push(`Restricted ${path} permissions to 0600.`);
247
- }
248
- } catch (error) {
249
- if (isNodeError(error) && error.code === "ENOENT") return undefined;
250
- const failure = `Failed to enforce 0600 permissions for ${path}: ${formatError(error)}`;
251
- warnings.push(failure);
252
- return failure;
253
- }
254
- return undefined;
219
+ async function ensurePrivatePermissions(path: string, warnings: string[]): Promise<string | undefined> {
220
+ try {
221
+ const file = await stat(path);
222
+ if ((file.mode & 0o777) !== 0o600) {
223
+ await chmod(path, 0o600);
224
+ const repaired = await stat(path);
225
+ if ((repaired.mode & 0o777) !== 0o600) {
226
+ throw new Error(`permissions remained ${(repaired.mode & 0o777).toString(8)}`);
227
+ }
228
+ warnings.push(`Restricted ${path} permissions to 0600.`);
229
+ }
230
+ } catch (error) {
231
+ if (isNodeError(error) && error.code === "ENOENT") return undefined;
232
+ const failure = `Failed to enforce 0600 permissions for ${path}: ${formatError(error)}`;
233
+ warnings.push(failure);
234
+ return failure;
235
+ }
236
+ return undefined;
255
237
  }
256
238
 
257
239
  function normalizeBaseUrl(value: string): string | undefined {
258
- try {
259
- const url = new URL(value);
260
- if (url.protocol !== "http:" && url.protocol !== "https:") return undefined;
261
- if (url.username || url.password || url.search || url.hash) return undefined;
262
- return url.toString().replace(/\/+$/, "");
263
- } catch {
264
- return undefined;
265
- }
240
+ try {
241
+ const url = new URL(value);
242
+ if (url.protocol !== "http:" && url.protocol !== "https:") return undefined;
243
+ if (url.username || url.password || url.search || url.hash) return undefined;
244
+ return url.toString().replace(/\/+$/, "");
245
+ } catch {
246
+ return undefined;
247
+ }
266
248
  }
267
249
 
268
- function optionalString(
269
- value: unknown,
270
- name: string,
271
- ): { ok: true; value?: string } | { ok: false; reason: string } {
272
- if (value === undefined) return { ok: true };
273
- const normalized = normalizeString(value);
274
- return normalized
275
- ? { ok: true, value: normalized }
276
- : { ok: false, reason: `pi-langfuse.json ${name} must be a non-empty string.` };
250
+ function optionalString(value: unknown, name: string): { ok: true; value?: string } | { ok: false; reason: string } {
251
+ if (value === undefined) return { ok: true };
252
+ const normalized = normalizeString(value);
253
+ return normalized
254
+ ? { ok: true, value: normalized }
255
+ : { ok: false, reason: `pi-langfuse.json ${name} must be a non-empty string.` };
277
256
  }
278
257
 
279
258
  function normalizeString(value: unknown): string | undefined {
280
- return typeof value === "string" && value.trim() ? value.trim() : undefined;
259
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
281
260
  }
282
261
 
283
262
  function isInterpolation(value: string): boolean {
284
- return value.startsWith("$") || value.startsWith("!");
263
+ return value.startsWith("$") || value.startsWith("!");
285
264
  }
286
265
 
287
266
  function isNodeError(error: unknown): error is NodeJS.ErrnoException {
288
- return error instanceof Error && "code" in error;
267
+ return error instanceof Error && "code" in error;
289
268
  }
290
269
 
291
270
  function formatError(error: unknown): string {
292
- return error instanceof Error ? error.message : String(error);
271
+ return error instanceof Error ? error.message : String(error);
293
272
  }