pi-openai-toolkit 0.3.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/src/debug.ts ADDED
@@ -0,0 +1,168 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
5
+ import {
6
+ COMPACTION_EXTENSION_ID,
7
+ REDACTED_VALUE,
8
+ type ArtifactContext,
9
+ type ArtifactPaths,
10
+ type ArtifactSessionInfo,
11
+ type DebugArtifactEnvelope,
12
+ type DebugArtifactKind,
13
+ type CompactionConfig,
14
+ type RedactOptions,
15
+ } from "./types";
16
+
17
+ const SENSITIVE_KEY_RE = /(authorization|api[-_]?key|token|secret|password|cookie|set-cookie|signature|credential|oauth|auth|encrypted[_-]?content)/i;
18
+ const BEARER_RE = /\bBearer\s+[A-Za-z0-9._\-+/=]+/gi;
19
+ const OPENAI_KEY_RE = /\bsk-[A-Za-z0-9\-_]+\b/g;
20
+ const HEADER_TOKEN_RE = /\b(x-api-key|api-key|authorization)\b\s*[:=]\s*[^\s,;]+/gi;
21
+ const SENSITIVE_QUERY_RE = /([?&](?:api[-_]?key|access[-_]?token|token|secret|signature|credential|password)=)[^&#\s]*/gi;
22
+
23
+ function ensureDir(dirPath: string) {
24
+ fs.mkdirSync(dirPath, { recursive: true });
25
+ }
26
+
27
+ function toSessionInfo(context: ArtifactContext): ArtifactSessionInfo {
28
+ const maybeExtensionContext = context as Pick<ExtensionContext, "cwd" | "sessionManager">;
29
+ const sessionManager = maybeExtensionContext.sessionManager;
30
+ if (sessionManager) {
31
+ return {
32
+ cwd: context.cwd,
33
+ sessionId: sessionManager.getSessionId(),
34
+ sessionFile: sessionManager.getSessionFile(),
35
+ sessionDir: sessionManager.getSessionDir(),
36
+ };
37
+ }
38
+ return context;
39
+ }
40
+
41
+ function sanitizePathSegment(value: string | undefined, fallback: string): string {
42
+ if (!value) return fallback;
43
+ const normalized = value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
44
+ return normalized.length > 0 ? normalized : fallback;
45
+ }
46
+
47
+ function redactInlineSecrets(value: string, placeholder: string): string {
48
+ return value
49
+ .replace(BEARER_RE, `Bearer ${placeholder}`)
50
+ .replace(OPENAI_KEY_RE, placeholder)
51
+ .replace(HEADER_TOKEN_RE, (_match, key: string) => `${key}: ${placeholder}`)
52
+ .replace(SENSITIVE_QUERY_RE, (_match, prefix: string) => `${prefix}${placeholder}`);
53
+ }
54
+
55
+ function shouldRedactKey(key: string): boolean {
56
+ return SENSITIVE_KEY_RE.test(key);
57
+ }
58
+
59
+ export function redactValue(value: unknown, options: RedactOptions = {}): unknown {
60
+ const placeholder = options.placeholder ?? REDACTED_VALUE;
61
+ const seen = new WeakSet<object>();
62
+
63
+ const visit = (input: unknown): unknown => {
64
+ if (typeof input === "string") {
65
+ return redactInlineSecrets(input, placeholder);
66
+ }
67
+ if (!input || typeof input !== "object") {
68
+ return input;
69
+ }
70
+ if (seen.has(input)) {
71
+ return "[Circular]";
72
+ }
73
+ seen.add(input);
74
+
75
+ if (Array.isArray(input)) {
76
+ return input.map((item) => visit(item));
77
+ }
78
+
79
+ const result: Record<string, unknown> = {};
80
+ for (const [key, item] of Object.entries(input)) {
81
+ result[key] = shouldRedactKey(key) ? placeholder : visit(item);
82
+ }
83
+ return result;
84
+ };
85
+
86
+ return visit(value);
87
+ }
88
+
89
+ export function resolveArtifactPaths(settings: CompactionConfig, context: ArtifactContext): ArtifactPaths {
90
+ const sessionInfo = toSessionInfo(context);
91
+ const rootDir = settings.artifactRoot.startsWith("~/")
92
+ ? path.join(os.homedir(), settings.artifactRoot.slice(2))
93
+ : path.resolve(settings.artifactRoot);
94
+ const sessionDir = path.join(rootDir, "sessions", sanitizePathSegment(sessionInfo.sessionId, "no-session"));
95
+
96
+ return {
97
+ rootDir,
98
+ sessionDir,
99
+ providerRequestsDir: path.join(sessionDir, "provider-requests"),
100
+ compactResponsesDir: path.join(sessionDir, "compact-responses"),
101
+ compactionDir: path.join(sessionDir, "compaction-events"),
102
+ lifecycleDir: path.join(sessionDir, "lifecycle"),
103
+ };
104
+ }
105
+
106
+ function selectArtifactDirectory(paths: ArtifactPaths, kind: DebugArtifactKind): string {
107
+ switch (kind) {
108
+ case "provider-request":
109
+ return paths.providerRequestsDir;
110
+ case "compact-response":
111
+ return paths.compactResponsesDir;
112
+ case "compaction-event":
113
+ return paths.compactionDir;
114
+ case "lifecycle":
115
+ default:
116
+ return paths.lifecycleDir;
117
+ }
118
+ }
119
+
120
+ function shouldWriteArtifact(kind: DebugArtifactKind, settings: CompactionConfig): boolean {
121
+ switch (kind) {
122
+ case "provider-request":
123
+ return settings.logProviderPayloads;
124
+ case "compact-response":
125
+ return settings.logCompactResponses;
126
+ case "compaction-event":
127
+ case "lifecycle":
128
+ return settings.debug;
129
+ default:
130
+ return false;
131
+ }
132
+ }
133
+
134
+ export function writeDebugArtifact(
135
+ kind: DebugArtifactKind,
136
+ data: unknown,
137
+ settings: CompactionConfig,
138
+ context: ArtifactContext,
139
+ ): string | undefined {
140
+ if (!shouldWriteArtifact(kind, settings)) {
141
+ return undefined;
142
+ }
143
+
144
+ const sessionInfo = toSessionInfo(context);
145
+ const paths = resolveArtifactPaths(settings, context);
146
+ const targetDir = selectArtifactDirectory(paths, kind);
147
+ ensureDir(targetDir);
148
+
149
+ const timestamp = new Date().toISOString();
150
+ const fileName = `${timestamp.replace(/[.:]/g, "-")}-${kind}.json`;
151
+ const filePath = path.join(targetDir, fileName);
152
+ const envelope: DebugArtifactEnvelope = {
153
+ extension: COMPACTION_EXTENSION_ID,
154
+ kind,
155
+ timestamp,
156
+ cwd: sessionInfo.cwd,
157
+ sessionId: sessionInfo.sessionId,
158
+ sessionFile: sessionInfo.sessionFile,
159
+ sessionDir: sessionInfo.sessionDir,
160
+ redaction: {
161
+ enabled: settings.redactSensitiveData,
162
+ },
163
+ data: settings.redactSensitiveData ? redactValue(data) : data,
164
+ };
165
+
166
+ fs.writeFileSync(filePath, `${JSON.stringify(envelope, null, 2)}\n`, "utf8");
167
+ return filePath;
168
+ }
@@ -0,0 +1,151 @@
1
+ import type { CompactionEntry, SessionEntry } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ isNativeCompactionDetails,
4
+ isNativeCompactionEntry,
5
+ type NativeCompactionDetails,
6
+ type NativeCompactionEntry,
7
+ type NativeCompactionIdentity,
8
+ } from "./types";
9
+
10
+ export type NativeCompactionEntryMatch = Partial<NativeCompactionIdentity>;
11
+
12
+ export type LatestNativeCompactionResolutionFailureReason =
13
+ | "no-compaction"
14
+ | "latest-compaction-not-native"
15
+ | "latest-native-compaction-mismatch";
16
+
17
+ export type LatestNativeCompactionResolution =
18
+ | {
19
+ ok: true;
20
+ entry: NativeCompactionEntry;
21
+ index: number;
22
+ latestCompactionIndex: number;
23
+ }
24
+ | {
25
+ ok: false;
26
+ reason: LatestNativeCompactionResolutionFailureReason;
27
+ latestCompactionIndex?: number;
28
+ latestCompaction?: CompactionEntry;
29
+ };
30
+
31
+ function entryMatches(entry: NativeCompactionEntry, match: NativeCompactionEntryMatch): boolean {
32
+ const details = entry.details;
33
+ if (!details) {
34
+ return false;
35
+ }
36
+
37
+ return (
38
+ (match.provider === undefined || details.provider === match.provider) &&
39
+ (match.api === undefined || details.api === match.api) &&
40
+ (match.model === undefined || details.model === match.model) &&
41
+ (match.baseUrl === undefined || details.baseUrl === match.baseUrl)
42
+ );
43
+ }
44
+
45
+ export function getNativeCompactionDetails(
46
+ entry: CompactionEntry | SessionEntry | undefined,
47
+ ): NativeCompactionDetails | undefined {
48
+ if (!entry || entry.type !== "compaction") {
49
+ return undefined;
50
+ }
51
+
52
+ return isNativeCompactionDetails(entry.details) ? entry.details : undefined;
53
+ }
54
+
55
+ export function isPersistedNativeCompactionEntry(
56
+ entry: CompactionEntry | SessionEntry | undefined,
57
+ ): entry is NativeCompactionEntry {
58
+ return isNativeCompactionEntry(entry);
59
+ }
60
+
61
+ export function findLatestCompactionEntryIndex(entries: readonly SessionEntry[]): number | undefined {
62
+ for (let index = entries.length - 1; index >= 0; index--) {
63
+ if (entries[index]?.type === "compaction") {
64
+ return index;
65
+ }
66
+ }
67
+
68
+ return undefined;
69
+ }
70
+
71
+ export function findLatestCompactionEntry(entries: readonly SessionEntry[]): CompactionEntry | undefined {
72
+ const index = findLatestCompactionEntryIndex(entries);
73
+ return index === undefined ? undefined : (entries[index] as CompactionEntry);
74
+ }
75
+
76
+ export function findLatestNativeCompactionEntryIndex(
77
+ entries: readonly SessionEntry[],
78
+ match: NativeCompactionEntryMatch = {},
79
+ ): number | undefined {
80
+ for (let index = entries.length - 1; index >= 0; index--) {
81
+ const entry = entries[index];
82
+ if (!isPersistedNativeCompactionEntry(entry)) {
83
+ continue;
84
+ }
85
+
86
+ if (!entryMatches(entry, match)) {
87
+ continue;
88
+ }
89
+
90
+ return index;
91
+ }
92
+
93
+ return undefined;
94
+ }
95
+
96
+ export function findLatestNativeCompactionEntry(
97
+ entries: readonly SessionEntry[],
98
+ match: NativeCompactionEntryMatch = {},
99
+ ): NativeCompactionEntry | undefined {
100
+ const index = findLatestNativeCompactionEntryIndex(entries, match);
101
+ return index === undefined ? undefined : (entries[index] as NativeCompactionEntry);
102
+ }
103
+
104
+ export function findLatestNativeCompactionDetails(
105
+ entries: readonly SessionEntry[],
106
+ match: NativeCompactionEntryMatch = {},
107
+ ): NativeCompactionDetails | undefined {
108
+ return findLatestNativeCompactionEntry(entries, match)?.details;
109
+ }
110
+
111
+ export function resolveLatestNativeCompactionEntry(
112
+ entries: readonly SessionEntry[],
113
+ match: NativeCompactionEntryMatch = {},
114
+ ): LatestNativeCompactionResolution {
115
+ const latestCompactionIndex = findLatestCompactionEntryIndex(entries);
116
+ if (latestCompactionIndex === undefined) {
117
+ return {
118
+ ok: false,
119
+ reason: "no-compaction",
120
+ };
121
+ }
122
+
123
+ const latestCompaction = entries[latestCompactionIndex];
124
+ if (!latestCompaction || latestCompaction.type !== "compaction" || !isPersistedNativeCompactionEntry(latestCompaction)) {
125
+ return {
126
+ ok: false,
127
+ reason: "latest-compaction-not-native",
128
+ latestCompactionIndex,
129
+ latestCompaction:
130
+ latestCompaction && latestCompaction.type === "compaction"
131
+ ? (latestCompaction as CompactionEntry)
132
+ : undefined,
133
+ };
134
+ }
135
+
136
+ if (!entryMatches(latestCompaction, match)) {
137
+ return {
138
+ ok: false,
139
+ reason: "latest-native-compaction-mismatch",
140
+ latestCompactionIndex,
141
+ latestCompaction,
142
+ };
143
+ }
144
+
145
+ return {
146
+ ok: true,
147
+ entry: latestCompaction,
148
+ index: latestCompactionIndex,
149
+ latestCompactionIndex,
150
+ };
151
+ }