@narumitw/pi-langfuse 0.51.1 → 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.
- package/README.md +46 -3
- package/dist/chunks/chunk-N7WSLB5Q.ts +88 -0
- package/dist/chunks/chunk-N7WSLB5Q.ts.map +7 -0
- package/dist/chunks/{runtime-6ZHLVC3A.ts → runtime-GLRAF7FE.ts} +2 -2
- package/dist/chunks/runtime-GLRAF7FE.ts.map +7 -0
- package/dist/config.d.ts +1 -1
- package/dist/durable-projection.d.ts +9 -0
- package/dist/durable-recorder.d.ts +49 -0
- package/dist/durable.d.ts +25 -0
- package/dist/durable.js +638 -0
- package/dist/durable.js.map +7 -0
- package/dist/index.js +248 -159
- package/dist/index.js.map +4 -4
- package/dist/index.ts +244 -16
- package/dist/index.ts.map +4 -4
- package/dist/library-chunks/chunk-B64FXCNE.js +84 -0
- package/dist/library-chunks/chunk-B64FXCNE.js.map +7 -0
- package/dist/library-chunks/chunk-ETFOPFEP.js +10 -0
- package/dist/library-chunks/chunk-ETFOPFEP.js.map +7 -0
- package/dist/library-chunks/chunk-JPFN6GGS.js +153 -0
- package/dist/library-chunks/chunk-JPFN6GGS.js.map +7 -0
- package/dist/library-chunks/{runtime-A6SQ6VEH.js → runtime-6KYOZVOE.js} +5 -3
- package/dist/library-chunks/runtime-6KYOZVOE.js.map +7 -0
- package/dist/public-api.d.ts +1 -3
- package/dist/runtime-api.d.ts +3 -0
- package/dist/runtime-defaults.d.ts +1 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/sanitizer.d.ts +1 -1
- package/docs/durable.md +126 -0
- package/package.json +29 -9
- package/src/config.ts +4 -1
- package/src/durable-projection.ts +106 -0
- package/src/durable-recorder.ts +493 -0
- package/src/durable.ts +195 -0
- package/src/public-api.ts +6 -12
- package/src/runtime-api.ts +8 -0
- package/src/runtime-core.ts +2 -1
- package/src/runtime-defaults.ts +1 -0
- package/src/runtime.ts +2 -1
- package/src/sanitizer.ts +16 -4
- package/dist/chunks/chunk-DBQAWNKG.ts +0 -316
- package/dist/chunks/chunk-DBQAWNKG.ts.map +0 -7
- package/dist/chunks/runtime-6ZHLVC3A.ts.map +0 -7
- package/dist/library-chunks/chunk-SAJ76IYU.js +0 -316
- package/dist/library-chunks/chunk-SAJ76IYU.js.map +0 -7
- 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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} from "
|
|
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 (
|
|
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-
|
|
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: ${
|
|
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: ${
|
|
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: ${
|
|
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: ${
|
|
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
|
|
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/
|
|
1769
|
+
// src/runtime-api.ts
|
|
1542
1770
|
async function createLangfuseRuntime(options = {}) {
|
|
1543
|
-
const runtime = await import("./chunks/runtime-
|
|
1771
|
+
const runtime = await import("./chunks/runtime-GLRAF7FE.ts");
|
|
1544
1772
|
return runtime.createLangfuseRuntime(options);
|
|
1545
1773
|
}
|
|
1546
1774
|
export {
|