@narumitw/pi-langfuse 0.50.6 → 0.51.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 +82 -4
- package/dist/chunks/chunk-IASLJKKB.js +316 -0
- package/dist/chunks/chunk-IASLJKKB.js.map +7 -0
- package/dist/chunks/runtime-6MIKLWNJ.js +352 -0
- package/dist/chunks/runtime-6MIKLWNJ.js.map +7 -0
- package/dist/config.d.ts +33 -0
- package/dist/git.d.ts +5 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1551 -0
- package/dist/index.js.map +7 -0
- package/dist/index.ts +588 -479
- package/dist/index.ts.map +4 -4
- package/dist/langfuse.d.ts +13 -0
- package/dist/pi-session.d.ts +43 -0
- package/dist/public-api.d.ts +4 -0
- package/dist/runtime-core.d.ts +26 -0
- package/dist/runtime.d.ts +22 -0
- package/dist/sanitizer.d.ts +2 -0
- package/dist/tracing.d.ts +202 -0
- package/package.json +11 -3
- package/src/git.ts +60 -0
- package/src/index.ts +9 -0
- package/src/langfuse.ts +88 -292
- package/src/pi-session.ts +570 -0
- package/src/public-api.ts +17 -0
- package/src/runtime-core.ts +113 -0
- package/src/runtime.ts +234 -51
- package/src/sanitizer.ts +10 -7
- package/src/tracing.ts +52 -14
- package/dist/chunks/runtime-X75FRK2E.js +0 -215
- package/dist/chunks/runtime-X75FRK2E.js.map +0 -7
package/src/langfuse.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionCommandContext
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import {
|
|
3
3
|
DEFAULT_BASE_URL,
|
|
4
4
|
type LangfuseConfig,
|
|
@@ -8,7 +8,11 @@ import {
|
|
|
8
8
|
normalizeLangfuseConfig,
|
|
9
9
|
writeLangfuseConfig,
|
|
10
10
|
} from "./config.js";
|
|
11
|
-
import {
|
|
11
|
+
import { createPiLangfuseSessionController, type PiLangfuseSessionController } from "./pi-session.js";
|
|
12
|
+
import { createLangfuseRuntimeFromBackend } from "./runtime-core.js";
|
|
13
|
+
import type { GitMetadata, TraceBackend } from "./tracing.js";
|
|
14
|
+
|
|
15
|
+
export { resolveGitMetadata } from "./git.js";
|
|
12
16
|
|
|
13
17
|
interface ExtensionDependencies {
|
|
14
18
|
loadConfig(path?: string): Promise<LangfuseConfigResult>;
|
|
@@ -25,19 +29,13 @@ const HELP_ACTION = "Show setup and privacy help";
|
|
|
25
29
|
export function createLangfuseExtension(dependencies: Partial<ExtensionDependencies> = {}): (pi: ExtensionAPI) => void {
|
|
26
30
|
const loadConfig = dependencies.loadConfig ?? loadLangfuseConfig;
|
|
27
31
|
const writeConfig = dependencies.writeConfig ?? writeLangfuseConfig;
|
|
28
|
-
const
|
|
29
|
-
dependencies.createBackend ??
|
|
30
|
-
(async (config) => {
|
|
31
|
-
const { createProductionBackend } = await import("./runtime.js");
|
|
32
|
-
return createProductionBackend(config);
|
|
33
|
-
});
|
|
32
|
+
const resolveInjectedGitMetadata = dependencies.resolveGitMetadata;
|
|
34
33
|
|
|
35
34
|
return function langfuse(pi: ExtensionAPI) {
|
|
36
|
-
const resolveGit =
|
|
37
|
-
dependencies.resolveGitMetadata ??
|
|
38
|
-
((cwd: string) => resolveGitMetadata((command, args, options) => pi.exec(command, args, options), cwd));
|
|
39
|
-
let recorder: TraceRecorder | undefined;
|
|
40
35
|
let activeConfig: LangfuseConfig | undefined;
|
|
36
|
+
let loadingConfig: LangfuseConfig | undefined;
|
|
37
|
+
let runtimeConfigForShutdown: LangfuseConfig | undefined;
|
|
38
|
+
let shutdownConfig: LangfuseConfig | undefined;
|
|
41
39
|
let configPath: string | undefined;
|
|
42
40
|
let initializationError: string | undefined;
|
|
43
41
|
let hasStoredConfig = false;
|
|
@@ -45,7 +43,80 @@ export function createLangfuseExtension(dependencies: Partial<ExtensionDependenc
|
|
|
45
43
|
let sessionGeneration = 0;
|
|
46
44
|
let menuController = new AbortController();
|
|
47
45
|
let configWriteQueue = Promise.resolve();
|
|
48
|
-
|
|
46
|
+
|
|
47
|
+
const tracing: PiLangfuseSessionController = createPiLangfuseSessionController({
|
|
48
|
+
onSessionStart() {
|
|
49
|
+
sessionGeneration += 1;
|
|
50
|
+
menuController.abort(new DOMException("Langfuse session replaced", "AbortError"));
|
|
51
|
+
menuController = new AbortController();
|
|
52
|
+
activeConfig = undefined;
|
|
53
|
+
loadingConfig = undefined;
|
|
54
|
+
shutdownConfig = undefined;
|
|
55
|
+
configPath = undefined;
|
|
56
|
+
initializationError = undefined;
|
|
57
|
+
hasStoredConfig = false;
|
|
58
|
+
configurationNotice = undefined;
|
|
59
|
+
},
|
|
60
|
+
onSessionShutdown() {
|
|
61
|
+
sessionGeneration += 1;
|
|
62
|
+
menuController.abort(new DOMException("Langfuse session shut down", "AbortError"));
|
|
63
|
+
shutdownConfig = activeConfig ?? runtimeConfigForShutdown ?? loadingConfig;
|
|
64
|
+
activeConfig = undefined;
|
|
65
|
+
},
|
|
66
|
+
async resolveSession(ctx, isCurrent) {
|
|
67
|
+
await configWriteQueue;
|
|
68
|
+
if (!isCurrent()) return undefined;
|
|
69
|
+
const result = await loadConfig();
|
|
70
|
+
if (!isCurrent()) return undefined;
|
|
71
|
+
configPath = result.path;
|
|
72
|
+
for (const warning of result.warnings) ctx.ui.notify(warning, "warning");
|
|
73
|
+
if (!result.ok) {
|
|
74
|
+
initializationError = formatConfigError(result);
|
|
75
|
+
ctx.ui.notify(initializationError, "warning");
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
hasStoredConfig = true;
|
|
80
|
+
loadingConfig = result.config;
|
|
81
|
+
const createBackend = dependencies.createBackend;
|
|
82
|
+
const ownsRuntime = createBackend !== undefined;
|
|
83
|
+
const runtime = createBackend
|
|
84
|
+
? createLangfuseRuntimeFromBackend(await createBackend(result.config))
|
|
85
|
+
: await (await import("./runtime.js")).createLangfuseRuntime({ config: result.config, env: false });
|
|
86
|
+
if (isCurrent() || !runtimeConfigForShutdown) runtimeConfigForShutdown = result.config;
|
|
87
|
+
if (isCurrent()) {
|
|
88
|
+
activeConfig = result.config;
|
|
89
|
+
loadingConfig = undefined;
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
runtime,
|
|
93
|
+
releaseIfStale: async (reason) => {
|
|
94
|
+
if (ownsRuntime || reason === "quit") await runtime.shutdown();
|
|
95
|
+
},
|
|
96
|
+
options: {
|
|
97
|
+
...(result.config.userId ? { userId: result.config.userId } : {}),
|
|
98
|
+
captureContent: result.config.captureContent,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
...(resolveInjectedGitMetadata
|
|
103
|
+
? { resolveGitMetadata: (_pi: ExtensionAPI, cwd: string) => resolveInjectedGitMetadata(cwd) }
|
|
104
|
+
: {}),
|
|
105
|
+
onInitializationError(error, ctx) {
|
|
106
|
+
const config = loadingConfig;
|
|
107
|
+
loadingConfig = undefined;
|
|
108
|
+
initializationError = `Langfuse tracing could not start: ${formatError(error, config)}`;
|
|
109
|
+
ctx.ui.notify(initializationError, "warning");
|
|
110
|
+
},
|
|
111
|
+
beforeSessionDispose: async () => {
|
|
112
|
+
await configWriteQueue;
|
|
113
|
+
},
|
|
114
|
+
onShutdownError(error, ctx) {
|
|
115
|
+
ctx.ui.notify(`Langfuse shutdown export failed: ${formatError(error, shutdownConfig)}`, "error");
|
|
116
|
+
},
|
|
117
|
+
flushOnReplacement: true,
|
|
118
|
+
shutdownRuntimeOnQuit: true,
|
|
119
|
+
});
|
|
49
120
|
|
|
50
121
|
async function showLangfuseMenu(ctx: ExtensionCommandContext) {
|
|
51
122
|
if (!ctx.hasUI) {
|
|
@@ -69,7 +140,7 @@ export function createLangfuseExtension(dependencies: Partial<ExtensionDependenc
|
|
|
69
140
|
.split("\n")
|
|
70
141
|
.slice(1),
|
|
71
142
|
items: [
|
|
72
|
-
...(
|
|
143
|
+
...(tracing.active ? [{ id: "flush", label: FLUSH_ACTION, action: "flush" as const }] : []),
|
|
73
144
|
{
|
|
74
145
|
id: "configure",
|
|
75
146
|
label: hasStoredConfig ? UPDATE_ACTION : SET_UP_ACTION,
|
|
@@ -82,14 +153,13 @@ export function createLangfuseExtension(dependencies: Partial<ExtensionDependenc
|
|
|
82
153
|
},
|
|
83
154
|
actions: {
|
|
84
155
|
flush: async () => {
|
|
85
|
-
const menuRecorder = recorder;
|
|
86
156
|
const menuConfig = activeConfig;
|
|
87
|
-
if (!
|
|
157
|
+
if (!tracing.active) {
|
|
88
158
|
ctx.ui.notify("Langfuse tracing is not enabled for this session.", "warning");
|
|
89
159
|
return { kind: "close" };
|
|
90
160
|
}
|
|
91
161
|
try {
|
|
92
|
-
await
|
|
162
|
+
await tracing.flush();
|
|
93
163
|
if (menuGeneration !== sessionGeneration) return { kind: "close" };
|
|
94
164
|
ctx.ui.notify("Langfuse traces flushed for this session.", "info");
|
|
95
165
|
} catch (error) {
|
|
@@ -146,284 +216,10 @@ export function createLangfuseExtension(dependencies: Partial<ExtensionDependenc
|
|
|
146
216
|
handler: async (_args, ctx) => showLangfuseMenu(ctx),
|
|
147
217
|
});
|
|
148
218
|
|
|
149
|
-
|
|
150
|
-
const generation = ++sessionGeneration;
|
|
151
|
-
menuController.abort(new DOMException("Langfuse session replaced", "AbortError"));
|
|
152
|
-
menuController = new AbortController();
|
|
153
|
-
recorder = undefined;
|
|
154
|
-
activeConfig = undefined;
|
|
155
|
-
configPath = undefined;
|
|
156
|
-
initializationError = undefined;
|
|
157
|
-
hasStoredConfig = false;
|
|
158
|
-
configurationNotice = undefined;
|
|
159
|
-
nextAttemptReason = undefined;
|
|
160
|
-
|
|
161
|
-
await configWriteQueue;
|
|
162
|
-
if (generation !== sessionGeneration) return;
|
|
163
|
-
const result = await loadConfig();
|
|
164
|
-
if (generation !== sessionGeneration) return;
|
|
165
|
-
configPath = result.path;
|
|
166
|
-
for (const warning of result.warnings) ctx.ui.notify(warning, "warning");
|
|
167
|
-
if (!result.ok) {
|
|
168
|
-
initializationError = formatConfigError(result);
|
|
169
|
-
ctx.ui.notify(initializationError, "warning");
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
hasStoredConfig = true;
|
|
174
|
-
try {
|
|
175
|
-
const backend = await createBackend(result.config);
|
|
176
|
-
if (generation !== sessionGeneration) {
|
|
177
|
-
await backend.shutdown().catch(() => undefined);
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
activeConfig = result.config;
|
|
181
|
-
recorder = new TraceRecorder(backend, {
|
|
182
|
-
sessionId: ctx.sessionManager.getSessionId(),
|
|
183
|
-
...(result.config.userId ? { userId: result.config.userId } : {}),
|
|
184
|
-
cwd: ctx.cwd,
|
|
185
|
-
mode: ctx.mode,
|
|
186
|
-
captureContent: result.config.captureContent,
|
|
187
|
-
});
|
|
188
|
-
} catch (error) {
|
|
189
|
-
if (generation !== sessionGeneration) return;
|
|
190
|
-
initializationError = `Langfuse tracing could not start: ${formatError(error, result.config)}`;
|
|
191
|
-
ctx.ui.notify(initializationError, "warning");
|
|
192
|
-
}
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
pi.on("before_agent_start", async (event, ctx) => {
|
|
196
|
-
nextAttemptReason = undefined;
|
|
197
|
-
const activeRecorder = recorder;
|
|
198
|
-
if (!activeRecorder) return;
|
|
199
|
-
const git = await resolveGit(ctx.cwd).catch(() => undefined);
|
|
200
|
-
if (recorder !== activeRecorder) return;
|
|
201
|
-
activeRecorder.beginAgent({
|
|
202
|
-
prompt: event.prompt,
|
|
203
|
-
images: event.images,
|
|
204
|
-
model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id, api: ctx.model.api } : undefined,
|
|
205
|
-
git,
|
|
206
|
-
snapshot: contextSnapshot(ctx),
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
pi.on("agent_start", () => {
|
|
211
|
-
if (!recorder) return;
|
|
212
|
-
recorder.beginAttempt(nextAttemptReason ? { reason: nextAttemptReason } : undefined);
|
|
213
|
-
nextAttemptReason = undefined;
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
pi.on("turn_start", (event, ctx) => {
|
|
217
|
-
if (!recorder) return;
|
|
218
|
-
ensureActiveRun(recorder, ctx);
|
|
219
|
-
recorder.beginTurn(event.turnIndex);
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
pi.on("before_provider_request", (event, ctx) => {
|
|
223
|
-
if (!recorder) return;
|
|
224
|
-
ensureActiveRun(recorder, ctx);
|
|
225
|
-
recorder.beginGeneration({
|
|
226
|
-
payload: event.payload,
|
|
227
|
-
payloadStage: "before_provider_request",
|
|
228
|
-
model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id, api: ctx.model.api } : undefined,
|
|
229
|
-
thinkingLevel: pi.getThinkingLevel(),
|
|
230
|
-
});
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
pi.on("after_provider_response", (event) => {
|
|
234
|
-
recorder?.recordProviderResponse(event.status, event.headers);
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
pi.on("message_update", (event) => {
|
|
238
|
-
if (isRealOutputDelta(event.assistantMessageEvent)) recorder?.markGenerationFirstOutput();
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
pi.on("message_end", (event) => {
|
|
242
|
-
if (event.message.role === "assistant") recorder?.markGenerationEnd();
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
pi.on("turn_end", (event) => {
|
|
246
|
-
if (event.message.role === "assistant") recorder?.finishAssistant(event.message);
|
|
247
|
-
recorder?.finishTurn(event.turnIndex, {
|
|
248
|
-
message: event.message,
|
|
249
|
-
toolResultCount: event.toolResults.length,
|
|
250
|
-
});
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
pi.on("tool_execution_start", (event) => {
|
|
254
|
-
recorder?.beginTool(event.toolCallId, event.toolName, event.args);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
pi.on("tool_execution_update", (event) => {
|
|
258
|
-
recorder?.recordToolProgress(event.toolCallId);
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
pi.on("tool_result", (event) => {
|
|
262
|
-
recorder?.recordToolInput(event.toolCallId, event.input);
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
pi.on("tool_execution_end", (event) => {
|
|
266
|
-
recorder?.finishTool(event.toolCallId, {
|
|
267
|
-
content: event.result.content,
|
|
268
|
-
details: event.result.details,
|
|
269
|
-
isError: event.isError,
|
|
270
|
-
});
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
pi.on("agent_end", (event) => {
|
|
274
|
-
const message = findLastAssistant(event.messages);
|
|
275
|
-
recorder?.finishAttempt(message);
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
pi.on("session_before_compact", (event) => {
|
|
279
|
-
recorder?.beginCompaction({
|
|
280
|
-
reason: event.reason,
|
|
281
|
-
willRetry: event.willRetry,
|
|
282
|
-
tokensBefore: event.preparation.tokensBefore,
|
|
283
|
-
messagesToSummarize: event.preparation.messagesToSummarize.length,
|
|
284
|
-
turnPrefixMessages: event.preparation.turnPrefixMessages.length,
|
|
285
|
-
branchEntries: event.branchEntries.length,
|
|
286
|
-
isSplitTurn: event.preparation.isSplitTurn,
|
|
287
|
-
});
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
pi.on("session_compact", (event) => {
|
|
291
|
-
const entry = event.compactionEntry as typeof event.compactionEntry & {
|
|
292
|
-
usage?: Parameters<TraceRecorder["finishCompaction"]>[0]["usage"];
|
|
293
|
-
};
|
|
294
|
-
recorder?.finishCompaction({
|
|
295
|
-
reason: event.reason,
|
|
296
|
-
willRetry: event.willRetry,
|
|
297
|
-
fromExtension: event.fromExtension,
|
|
298
|
-
tokensBefore: entry.tokensBefore,
|
|
299
|
-
details: entry.details,
|
|
300
|
-
usage: entry.usage,
|
|
301
|
-
});
|
|
302
|
-
if (event.willRetry && recorder?.hasActiveTrace()) nextAttemptReason = "post_compaction";
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
pi.on("agent_settled", (_event, ctx) => {
|
|
306
|
-
recorder?.settle(contextSnapshot(ctx));
|
|
307
|
-
nextAttemptReason = undefined;
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
pi.on("session_shutdown", async (event, ctx) => {
|
|
311
|
-
const generation = ++sessionGeneration;
|
|
312
|
-
menuController.abort(new DOMException("Langfuse session shut down", "AbortError"));
|
|
313
|
-
const activeRecorder = recorder;
|
|
314
|
-
const shutdownConfig = activeConfig;
|
|
315
|
-
const pendingConfigWrite = configWriteQueue;
|
|
316
|
-
const shutdownSnapshot = activeRecorder ? contextSnapshot(ctx) : undefined;
|
|
317
|
-
recorder = undefined;
|
|
318
|
-
activeConfig = undefined;
|
|
319
|
-
await pendingConfigWrite;
|
|
320
|
-
if (!activeRecorder) return;
|
|
321
|
-
try {
|
|
322
|
-
if (event.reason === "quit") await activeRecorder.shutdown(shutdownSnapshot);
|
|
323
|
-
else {
|
|
324
|
-
activeRecorder.interrupt(`Pi session ended before settlement (${event.reason}).`, shutdownSnapshot);
|
|
325
|
-
await activeRecorder.flush();
|
|
326
|
-
}
|
|
327
|
-
} catch (error) {
|
|
328
|
-
if (generation !== sessionGeneration) return;
|
|
329
|
-
ctx.ui.notify(`Langfuse shutdown export failed: ${formatError(error, shutdownConfig)}`, "error");
|
|
330
|
-
}
|
|
331
|
-
});
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function ensureActiveRun(recorder: TraceRecorder, ctx: ExtensionContext): void {
|
|
336
|
-
if (!recorder.hasActiveTrace()) {
|
|
337
|
-
recorder.beginAgent({
|
|
338
|
-
prompt: "[automatic continuation]",
|
|
339
|
-
model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id, api: ctx.model.api } : undefined,
|
|
340
|
-
snapshot: contextSnapshot(ctx),
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
if (!recorder.hasActiveAttempt()) recorder.beginAttempt();
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function contextSnapshot(ctx: ExtensionContext): ContextSnapshot {
|
|
347
|
-
return {
|
|
348
|
-
leafId: typeof ctx.sessionManager.getLeafId === "function" ? ctx.sessionManager.getLeafId() : undefined,
|
|
349
|
-
contextUsage: ctx.getContextUsage(),
|
|
219
|
+
tracing.extension(pi);
|
|
350
220
|
};
|
|
351
221
|
}
|
|
352
222
|
|
|
353
|
-
function findLastAssistant<T extends { role?: string }>(messages: readonly T[]): T | undefined {
|
|
354
|
-
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
355
|
-
const message = messages[index];
|
|
356
|
-
if (message?.role === "assistant") return message;
|
|
357
|
-
}
|
|
358
|
-
return undefined;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function isRealOutputDelta(event: { type: string; delta?: unknown }): boolean {
|
|
362
|
-
return (
|
|
363
|
-
(event.type === "text_delta" || event.type === "thinking_delta" || event.type === "toolcall_delta") &&
|
|
364
|
-
typeof event.delta === "string" &&
|
|
365
|
-
event.delta.length > 0
|
|
366
|
-
);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const GIT_LOOKUP_TIMEOUT_MS = 1_000;
|
|
370
|
-
const MAX_GIT_BRANCH_LENGTH = 256;
|
|
371
|
-
|
|
372
|
-
type GitExecutor = ExtensionAPI["exec"];
|
|
373
|
-
|
|
374
|
-
export async function resolveGitMetadata(exec: GitExecutor, cwd: string): Promise<GitMetadata | undefined> {
|
|
375
|
-
const [branchResult, commit] = await Promise.all([resolveGitBranch(exec, cwd), resolveGitCommit(exec, cwd)]);
|
|
376
|
-
if (!branchResult.resolved) return undefined;
|
|
377
|
-
if (branchResult.branch) {
|
|
378
|
-
return {
|
|
379
|
-
branch: branchResult.branch,
|
|
380
|
-
...(commit ? { commit } : {}),
|
|
381
|
-
detached: false,
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
return commit ? { commit, detached: true } : undefined;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
async function resolveGitBranch(exec: GitExecutor, cwd: string): Promise<{ resolved: boolean; branch?: string }> {
|
|
388
|
-
try {
|
|
389
|
-
const result = await exec("git", ["symbolic-ref", "--quiet", "--short", "HEAD"], {
|
|
390
|
-
cwd,
|
|
391
|
-
timeout: GIT_LOOKUP_TIMEOUT_MS,
|
|
392
|
-
});
|
|
393
|
-
if (result.killed) return { resolved: false };
|
|
394
|
-
if (result.code === 1) return { resolved: true };
|
|
395
|
-
if (result.code !== 0) return { resolved: false };
|
|
396
|
-
const branch = normalizeGitBranch(result.stdout);
|
|
397
|
-
return branch ? { resolved: true, branch } : { resolved: false };
|
|
398
|
-
} catch {
|
|
399
|
-
return { resolved: false };
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
async function resolveGitCommit(exec: GitExecutor, cwd: string): Promise<string | undefined> {
|
|
404
|
-
try {
|
|
405
|
-
const result = await exec("git", ["rev-parse", "--verify", "--short=12", "HEAD"], {
|
|
406
|
-
cwd,
|
|
407
|
-
timeout: GIT_LOOKUP_TIMEOUT_MS,
|
|
408
|
-
});
|
|
409
|
-
if (result.code !== 0 || result.killed) return undefined;
|
|
410
|
-
const commit = result.stdout.trim();
|
|
411
|
-
return /^[0-9a-f]{4,64}$/iu.test(commit) ? commit.toLowerCase() : undefined;
|
|
412
|
-
} catch {
|
|
413
|
-
return undefined;
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function normalizeGitBranch(value: string): string | undefined {
|
|
418
|
-
const branch = value.trim();
|
|
419
|
-
if (!branch || branch.length > MAX_GIT_BRANCH_LENGTH) return undefined;
|
|
420
|
-
for (const character of branch) {
|
|
421
|
-
const code = character.codePointAt(0) ?? 0;
|
|
422
|
-
if (code <= 31 || code === 127) return undefined;
|
|
423
|
-
}
|
|
424
|
-
return branch;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
223
|
async function promptForConfig(
|
|
428
224
|
ctx: ExtensionCommandContext,
|
|
429
225
|
current: LangfuseConfig | undefined,
|