@narumitw/pi-langfuse 0.51.2 → 0.52.0

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.
Files changed (46) hide show
  1. package/README.md +46 -3
  2. package/dist/chunks/chunk-N7WSLB5Q.ts +88 -0
  3. package/dist/chunks/chunk-N7WSLB5Q.ts.map +7 -0
  4. package/dist/chunks/{runtime-6ZHLVC3A.ts → runtime-GLRAF7FE.ts} +2 -2
  5. package/dist/chunks/runtime-GLRAF7FE.ts.map +7 -0
  6. package/dist/config.d.ts +1 -1
  7. package/dist/durable-projection.d.ts +9 -0
  8. package/dist/durable-recorder.d.ts +49 -0
  9. package/dist/durable.d.ts +25 -0
  10. package/dist/durable.js +638 -0
  11. package/dist/durable.js.map +7 -0
  12. package/dist/index.js +248 -159
  13. package/dist/index.js.map +4 -4
  14. package/dist/index.ts +244 -16
  15. package/dist/index.ts.map +4 -4
  16. package/dist/library-chunks/chunk-B64FXCNE.js +84 -0
  17. package/dist/library-chunks/chunk-B64FXCNE.js.map +7 -0
  18. package/dist/library-chunks/chunk-ETFOPFEP.js +10 -0
  19. package/dist/library-chunks/chunk-ETFOPFEP.js.map +7 -0
  20. package/dist/library-chunks/chunk-JPFN6GGS.js +153 -0
  21. package/dist/library-chunks/chunk-JPFN6GGS.js.map +7 -0
  22. package/dist/library-chunks/{runtime-A6SQ6VEH.js → runtime-6KYOZVOE.js} +5 -3
  23. package/dist/library-chunks/runtime-6KYOZVOE.js.map +7 -0
  24. package/dist/public-api.d.ts +1 -3
  25. package/dist/runtime-api.d.ts +3 -0
  26. package/dist/runtime-defaults.d.ts +1 -0
  27. package/dist/runtime.d.ts +1 -1
  28. package/dist/sanitizer.d.ts +1 -1
  29. package/docs/durable.md +126 -0
  30. package/package.json +22 -2
  31. package/src/config.ts +4 -1
  32. package/src/durable-projection.ts +106 -0
  33. package/src/durable-recorder.ts +493 -0
  34. package/src/durable.ts +195 -0
  35. package/src/public-api.ts +6 -12
  36. package/src/runtime-api.ts +8 -0
  37. package/src/runtime-core.ts +2 -1
  38. package/src/runtime-defaults.ts +1 -0
  39. package/src/runtime.ts +2 -1
  40. package/src/sanitizer.ts +16 -4
  41. package/dist/chunks/chunk-DBQAWNKG.ts +0 -316
  42. package/dist/chunks/chunk-DBQAWNKG.ts.map +0 -7
  43. package/dist/chunks/runtime-6ZHLVC3A.ts.map +0 -7
  44. package/dist/library-chunks/chunk-SAJ76IYU.js +0 -316
  45. package/dist/library-chunks/chunk-SAJ76IYU.js.map +0 -7
  46. package/dist/library-chunks/runtime-A6SQ6VEH.js.map +0 -7
package/dist/index.ts CHANGED
@@ -3,11 +3,235 @@
3
3
  import {
4
4
  DEFAULT_BASE_URL,
5
5
  createLangfuseRuntimeFromBackend,
6
- getLangfuseRuntimeInternal,
7
- loadLangfuseConfig,
8
- normalizeLangfuseConfig,
9
- writeLangfuseConfig
10
- } from "./chunks/chunk-DBQAWNKG.ts";
6
+ getLangfuseRuntimeInternal
7
+ } from "./chunks/chunk-N7WSLB5Q.ts";
8
+
9
+ // src/config.ts
10
+ import { randomUUID } from "node:crypto";
11
+ import { chmod, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
12
+ import { dirname, join } from "node:path";
13
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
14
+ var CONFIG_FILE_NAME = "pi-langfuse.json";
15
+ function langfuseConfigPath() {
16
+ return join(getAgentDir(), CONFIG_FILE_NAME);
17
+ }
18
+ var configSaveQueues = /* @__PURE__ */ new Map();
19
+ function writeLangfuseConfig(patch, path = langfuseConfigPath()) {
20
+ const previous = configSaveQueues.get(path) ?? Promise.resolve();
21
+ const operation = previous.then(() => writeLangfuseConfigNow(patch, path));
22
+ const tail = operation.then(
23
+ () => void 0,
24
+ () => void 0
25
+ );
26
+ configSaveQueues.set(path, tail);
27
+ void tail.then(() => {
28
+ if (configSaveQueues.get(path) === tail) configSaveQueues.delete(path);
29
+ });
30
+ return operation;
31
+ }
32
+ async function writeLangfuseConfigNow(patch, path) {
33
+ const currentDocument = await readLangfuseDocumentForUpdate(path);
34
+ const normalized = normalizeLangfuseConfig({ ...currentDocument, ...patch });
35
+ if (!normalized.ok) throw new Error(normalized.reason);
36
+ const {
37
+ publicKey: _publicKey,
38
+ secretKey: _secretKey,
39
+ baseUrl: _baseUrl,
40
+ environment: _environment,
41
+ release: _release,
42
+ userId: _userId,
43
+ captureContent: _captureContent,
44
+ ...unknownFields
45
+ } = currentDocument;
46
+ const nextDocument = { ...unknownFields, ...normalized.config };
47
+ await mkdir(dirname(path), { recursive: true, mode: 448 });
48
+ const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
49
+ try {
50
+ await writeFile(tempPath, `${JSON.stringify(nextDocument, null, 2)}
51
+ `, {
52
+ encoding: "utf8",
53
+ mode: 384
54
+ });
55
+ await chmod(tempPath, 384);
56
+ await rename(tempPath, path);
57
+ return normalized.config;
58
+ } catch (error) {
59
+ await rm(tempPath, { force: true }).catch(() => void 0);
60
+ throw error;
61
+ }
62
+ }
63
+ async function readLangfuseDocumentForUpdate(path) {
64
+ let parsed;
65
+ try {
66
+ parsed = JSON.parse(await readFile(path, "utf8"));
67
+ } catch (error) {
68
+ if (isNodeError(error) && error.code === "ENOENT") return {};
69
+ throw new Error(
70
+ `Cannot update ${CONFIG_FILE_NAME} because the existing file is malformed or unreadable; repair it first.`
71
+ );
72
+ }
73
+ const normalized = normalizeLangfuseConfig(parsed);
74
+ if (!normalized.ok) {
75
+ throw new Error(
76
+ `Cannot update ${CONFIG_FILE_NAME} because the existing file has invalid recognized fields; repair it first.`
77
+ );
78
+ }
79
+ return { ...parsed };
80
+ }
81
+ async function loadLangfuseConfig(path = langfuseConfigPath()) {
82
+ await configSaveQueues.get(path);
83
+ const warnings = [];
84
+ const permissionFailure = await ensurePrivatePermissions(path, warnings);
85
+ if (permissionFailure) {
86
+ return {
87
+ ok: false,
88
+ path,
89
+ warnings,
90
+ reason: `Refusing to load credentials: ${permissionFailure}`
91
+ };
92
+ }
93
+ let text;
94
+ try {
95
+ text = await readFile(path, "utf8");
96
+ } catch (error) {
97
+ if (isNodeError(error) && error.code === "ENOENT") {
98
+ return { ok: false, path, warnings, reason: `Configuration file not found: ${path}` };
99
+ }
100
+ return {
101
+ ok: false,
102
+ path,
103
+ warnings,
104
+ reason: `Failed to read ${path}: ${formatError(error)}`
105
+ };
106
+ }
107
+ let parsed;
108
+ try {
109
+ parsed = JSON.parse(text);
110
+ } catch {
111
+ return {
112
+ ok: false,
113
+ path,
114
+ warnings,
115
+ reason: `Failed to parse ${path} as JSON; repair the file before retrying.`
116
+ };
117
+ }
118
+ const normalized = normalizeLangfuseConfig(parsed);
119
+ if (!normalized.ok) return { ok: false, path, warnings, reason: normalized.reason };
120
+ return { ok: true, config: normalized.config, path, warnings };
121
+ }
122
+ function normalizeLangfuseConfig(value) {
123
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
124
+ return { ok: false, reason: "pi-langfuse.json must contain a JSON object." };
125
+ }
126
+ const input = value;
127
+ const publicKey = normalizeString(input.publicKey);
128
+ const secretKey = normalizeString(input.secretKey);
129
+ if (!publicKey) {
130
+ return { ok: false, reason: "pi-langfuse.json requires a literal publicKey string." };
131
+ }
132
+ if (!secretKey) {
133
+ return { ok: false, reason: "pi-langfuse.json requires a literal secretKey string." };
134
+ }
135
+ if (isInterpolation(publicKey)) {
136
+ return {
137
+ ok: false,
138
+ reason: "pi-langfuse.json publicKey must be literal; environment and command interpolation are not supported."
139
+ };
140
+ }
141
+ if (isInterpolation(secretKey)) {
142
+ return {
143
+ ok: false,
144
+ reason: "pi-langfuse.json secretKey must be literal; environment and command interpolation are not supported."
145
+ };
146
+ }
147
+ const rawBaseUrl = input.baseUrl === void 0 ? DEFAULT_BASE_URL : normalizeString(input.baseUrl);
148
+ if (!rawBaseUrl) {
149
+ return { ok: false, reason: "pi-langfuse.json baseUrl must be a non-empty string." };
150
+ }
151
+ const baseUrl = normalizeBaseUrl(rawBaseUrl);
152
+ if (!baseUrl) {
153
+ return {
154
+ ok: false,
155
+ reason: "pi-langfuse.json baseUrl must use HTTP or HTTPS without credentials, a query, or a fragment."
156
+ };
157
+ }
158
+ if (input.captureContent !== void 0 && typeof input.captureContent !== "boolean") {
159
+ return { ok: false, reason: "pi-langfuse.json captureContent must be a boolean." };
160
+ }
161
+ const environment = optionalString(input.environment, "environment");
162
+ if (!environment.ok) return environment;
163
+ if (environment.value && (environment.value.length > 40 || !/^(?!langfuse)[a-z0-9_-]+$/u.test(environment.value))) {
164
+ return {
165
+ ok: false,
166
+ reason: "pi-langfuse.json environment must be at most 40 lowercase letters, numbers, hyphens, or underscores and must not start with langfuse."
167
+ };
168
+ }
169
+ const release = optionalString(input.release, "release");
170
+ if (!release.ok) return release;
171
+ const userId = optionalString(input.userId, "userId");
172
+ if (!userId.ok) return userId;
173
+ if (userId.value && userId.value.length > 200) {
174
+ return { ok: false, reason: "pi-langfuse.json userId must be at most 200 characters." };
175
+ }
176
+ return {
177
+ ok: true,
178
+ config: {
179
+ publicKey,
180
+ secretKey,
181
+ baseUrl,
182
+ ...environment.value ? { environment: environment.value } : {},
183
+ ...release.value ? { release: release.value } : {},
184
+ ...userId.value ? { userId: userId.value } : {},
185
+ captureContent: input.captureContent ?? true
186
+ }
187
+ };
188
+ }
189
+ async function ensurePrivatePermissions(path, warnings) {
190
+ try {
191
+ const file = await stat(path);
192
+ if ((file.mode & 511) !== 384) {
193
+ await chmod(path, 384);
194
+ const repaired = await stat(path);
195
+ if ((repaired.mode & 511) !== 384) {
196
+ throw new Error(`permissions remained ${(repaired.mode & 511).toString(8)}`);
197
+ }
198
+ warnings.push(`Restricted ${path} permissions to 0600.`);
199
+ }
200
+ } catch (error) {
201
+ if (isNodeError(error) && error.code === "ENOENT") return void 0;
202
+ const failure = `Failed to enforce 0600 permissions for ${path}: ${formatError(error)}`;
203
+ warnings.push(failure);
204
+ return failure;
205
+ }
206
+ return void 0;
207
+ }
208
+ function normalizeBaseUrl(value) {
209
+ try {
210
+ const url = new URL(value);
211
+ if (url.protocol !== "http:" && url.protocol !== "https:") return void 0;
212
+ if (url.username || url.password || url.search || url.hash) return void 0;
213
+ return url.toString().replace(/\/+$/, "");
214
+ } catch {
215
+ return void 0;
216
+ }
217
+ }
218
+ function optionalString(value, name) {
219
+ if (value === void 0) return { ok: true };
220
+ const normalized = normalizeString(value);
221
+ return normalized ? { ok: true, value: normalized } : { ok: false, reason: `pi-langfuse.json ${name} must be a non-empty string.` };
222
+ }
223
+ function normalizeString(value) {
224
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
225
+ }
226
+ function isInterpolation(value) {
227
+ return value.startsWith("$") || value.startsWith("!");
228
+ }
229
+ function isNodeError(error) {
230
+ return error instanceof Error && "code" in error;
231
+ }
232
+ function formatError(error) {
233
+ return error instanceof Error ? error.message : String(error);
234
+ }
11
235
 
12
236
  // src/git.ts
13
237
  var GIT_LOOKUP_TIMEOUT_MS = 1e3;
@@ -71,8 +295,8 @@ var TRUNCATED = "[truncated: content budget exceeded]";
71
295
  var BASE64_DATA_URI = /data:[^,\s"'`]*;base64,[^\s"'`]*/gi;
72
296
  var BASE64_DATA_URI_OMITTED = "[base64 data URI omitted]";
73
297
  var OPAQUE_SIGNATURE_KEYS = /* @__PURE__ */ new Set(["thinkingSignature", "textSignature", "thoughtSignature"]);
74
- function sanitizeTraceValue(value) {
75
- const budget = { remaining: MAX_CAPTURE_BYTES };
298
+ function sanitizeTraceValue(value, redactCredentialFields = false) {
299
+ const budget = { remaining: MAX_CAPTURE_BYTES, redactCredentialFields };
76
300
  const sanitized = sanitize(value, /* @__PURE__ */ new WeakSet(), 0, budget);
77
301
  return serializedBytes(sanitized) <= MAX_CAPTURE_BYTES ? sanitized : TRUNCATED;
78
302
  }
@@ -135,7 +359,11 @@ function sanitize(value, active, depth, budget) {
135
359
  const sanitizedKey = sanitizeString(key, keyBudget);
136
360
  budget.remaining -= byteLength(sanitizedKey) + 4;
137
361
  const property = readProperty(record, key);
138
- if (redactData && key === "data") {
362
+ if (budget.redactCredentialFields && /^(?:(?:x[-_])?api[-_]?(?:key|token|secret)|authorization|proxy[-_]?authorization|cookies?|password|credentials?|secret|token|bearer|client[-_]?secret|private[-_]?key|secret[-_]?key|public[-_]?key|access[-_]?token|refresh[-_]?token)$/iu.test(
363
+ key
364
+ )) {
365
+ output[sanitizedKey] = consume("[credential field omitted]", budget);
366
+ } else if (redactData && key === "data") {
139
367
  output[sanitizedKey] = consume("[base64 omitted]", budget);
140
368
  } else if (!property.ok) output[sanitizedKey] = consume("[unreadable property]", budget);
141
369
  else output[sanitizedKey] = sanitize(property.value, active, depth + 1, budget);
@@ -1317,7 +1545,7 @@ function createLangfuseExtension(dependencies = {}) {
1317
1545
  loadingConfig = result.config;
1318
1546
  const createBackend = dependencies.createBackend;
1319
1547
  const ownsRuntime = createBackend !== void 0;
1320
- const runtime = createBackend ? createLangfuseRuntimeFromBackend(await createBackend(result.config)) : await (await import("./chunks/runtime-6ZHLVC3A.ts")).createLangfuseRuntime({ config: result.config, env: false });
1548
+ const runtime = createBackend ? createLangfuseRuntimeFromBackend(await createBackend(result.config)) : await (await import("./chunks/runtime-GLRAF7FE.ts")).createLangfuseRuntime({ config: result.config, env: false });
1321
1549
  if (isCurrent() || !runtimeConfigForShutdown) runtimeConfigForShutdown = result.config;
1322
1550
  if (isCurrent()) {
1323
1551
  activeConfig = result.config;
@@ -1338,14 +1566,14 @@ function createLangfuseExtension(dependencies = {}) {
1338
1566
  onInitializationError(error, ctx) {
1339
1567
  const config = loadingConfig;
1340
1568
  loadingConfig = void 0;
1341
- initializationError = `Langfuse tracing could not start: ${formatError(error, config)}`;
1569
+ initializationError = `Langfuse tracing could not start: ${formatError2(error, config)}`;
1342
1570
  ctx.ui.notify(initializationError, "warning");
1343
1571
  },
1344
1572
  beforeSessionDispose: async () => {
1345
1573
  await configWriteQueue;
1346
1574
  },
1347
1575
  onShutdownError(error, ctx) {
1348
- ctx.ui.notify(`Langfuse shutdown export failed: ${formatError(error, shutdownConfig)}`, "error");
1576
+ ctx.ui.notify(`Langfuse shutdown export failed: ${formatError2(error, shutdownConfig)}`, "error");
1349
1577
  },
1350
1578
  flushOnReplacement: true,
1351
1579
  shutdownRuntimeOnQuit: true
@@ -1392,7 +1620,7 @@ function createLangfuseExtension(dependencies = {}) {
1392
1620
  ctx.ui.notify("Langfuse traces flushed for this session.", "info");
1393
1621
  } catch (error) {
1394
1622
  if (menuGeneration !== sessionGeneration) return { kind: "close" };
1395
- ctx.ui.notify(`Langfuse flush failed: ${formatError(error, menuConfig)}`, "error");
1623
+ ctx.ui.notify(`Langfuse flush failed: ${formatError2(error, menuConfig)}`, "error");
1396
1624
  }
1397
1625
  return { kind: "close" };
1398
1626
  },
@@ -1422,7 +1650,7 @@ function createLangfuseExtension(dependencies = {}) {
1422
1650
  );
1423
1651
  } catch (error) {
1424
1652
  if (menuGeneration !== sessionGeneration) return { kind: "close" };
1425
- ctx.ui.notify(`Failed to save Langfuse config: ${formatError(error, next)}`, "error");
1653
+ ctx.ui.notify(`Failed to save Langfuse config: ${formatError2(error, next)}`, "error");
1426
1654
  }
1427
1655
  return { kind: "close" };
1428
1656
  },
@@ -1528,7 +1756,7 @@ function formatConfigError(result) {
1528
1756
  const setupHint = result.reason.startsWith("Configuration file not found:") ? " Run /langfuse and choose Set up Langfuse." : "";
1529
1757
  return `Langfuse tracing is disabled: ${result.reason}${setupHint}`;
1530
1758
  }
1531
- function formatError(error, config) {
1759
+ function formatError2(error, config) {
1532
1760
  let message = error instanceof Error ? error.message : String(error);
1533
1761
  const secrets = [config?.publicKey, config?.secretKey].filter((secret) => Boolean(secret)).sort((left, right) => right.length - left.length);
1534
1762
  for (const secret of secrets) {
@@ -1538,9 +1766,9 @@ function formatError(error, config) {
1538
1766
  }
1539
1767
  var langfuse_default = createLangfuseExtension();
1540
1768
 
1541
- // src/public-api.ts
1769
+ // src/runtime-api.ts
1542
1770
  async function createLangfuseRuntime(options = {}) {
1543
- const runtime = await import("./chunks/runtime-6ZHLVC3A.ts");
1771
+ const runtime = await import("./chunks/runtime-GLRAF7FE.ts");
1544
1772
  return runtime.createLangfuseRuntime(options);
1545
1773
  }
1546
1774
  export {