pi-codebuddy-sdk 0.1.10 → 0.1.12

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,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-codebuddy-sdk",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "CodeBuddy Agent SDK provider for Pi — native tool execution, 29+ models, thinking controls, session pool",
5
5
  "author": "slahser",
6
6
  "license": "MIT",
package/src/provider.ts CHANGED
@@ -1,3 +1,12 @@
1
+ /**
2
+ * CodeBuddy Provider — stateless, lightweight.
3
+ *
4
+ * Uses sdk.query() (one-shot) — no context building, no session pool,
5
+ * no tool boundary. Pi manages conversation, CodeBuddy is just a model backend.
6
+ *
7
+ * Architecture follows pi-openai-compatible / pi-anthropic pattern:
8
+ * extract latest user message → send to SDK → stream events back to Pi.
9
+ */
1
10
  import {
2
11
  createAssistantMessageEventStream,
3
12
  type Api,
@@ -16,8 +25,6 @@ import type {
16
25
  Effort,
17
26
  RawMessageStreamEvent,
18
27
  } from "@tencent-ai/agent-sdk";
19
- import { buildFullMessage, buildDeltaMessage } from "./context.js";
20
- import { acquireSession, evictSession } from "./session-pool.js";
21
28
 
22
29
  type CodebuddySdk = typeof import("@tencent-ai/agent-sdk");
23
30
 
@@ -70,13 +77,33 @@ function extractUsageUpdate(msg: CbMessage): Partial<AssistantMessage> | null {
70
77
  return null;
71
78
  }
72
79
 
80
+ // ── extract latest user message ──
81
+
82
+ function extractLatestUserText(context: Context): string {
83
+ const messages = context.messages;
84
+ for (let i = messages.length - 1; i >= 0; i--) {
85
+ if (messages[i].role === "user") {
86
+ const content = (messages[i] as any).content;
87
+ if (typeof content === "string") return content;
88
+ if (Array.isArray(content)) {
89
+ const texts = content
90
+ .filter((c: any) => c?.type === "text")
91
+ .map((c: any) => c.text)
92
+ .join("\n");
93
+ if (texts) return texts;
94
+ }
95
+ }
96
+ }
97
+ return "Continue.";
98
+ }
99
+
73
100
  // ── thinking config mapping ──
74
101
 
75
102
  const DEFAULT_THINKING_BUDGETS: Record<string, number> = {
76
103
  minimal: 1600,
77
104
  low: 4000,
78
- medium: 0, // adaptive
79
- high: 0, // adaptive
105
+ medium: 0,
106
+ high: 0,
80
107
  xhigh: 32000,
81
108
  };
82
109
 
@@ -114,53 +141,35 @@ export function streamCodebuddy(
114
141
  const partial = makePartial(model);
115
142
 
116
143
  queueMicrotask(async () => {
117
- let session: Awaited<ReturnType<CodebuddySdk["unstable_v2_createSession"]>> | undefined;
118
- let isFirst = false;
119
-
120
144
  try {
121
145
  stream.push({ type: "start", partial });
122
146
 
123
147
  const sdk = await loadCodebuddySdk();
148
+ const userText = extractLatestUserText(context);
124
149
 
125
150
  const { thinking, effort } = buildThinkingConfig(
126
151
  options?.reasoning,
127
152
  options?.thinkingBudgets,
128
153
  );
129
154
 
130
- // Always request partial messages so we get stream_event deltas for real streaming.
131
- // Non-streaming-capable models will still emit assistant messages as fallback.
132
- const includePartial = true;
133
-
134
- const cwd = process.cwd();
135
-
136
- const { session: pooled, message: userMessage, isFirst: first } =
137
- await acquireSession(cwd, sdk, context, buildFullMessage, buildDeltaMessage, {
155
+ const q = sdk.query({
156
+ prompt: userText,
157
+ options: {
138
158
  model: model.id,
139
159
  permissionMode: "bypassPermissions",
140
160
  maxTurns: 100,
141
161
  thinking,
142
162
  effort,
143
- includePartialMessages: includePartial,
144
- });
145
-
146
- session = pooled;
147
- isFirst = first;
148
-
149
- let msgCount = 0; // track content blocks for empty-response detection
150
- let hasStreamEvents = false; // true once we see first stream_event
151
-
152
- // Per-index tracking for stream_event delta accumulation
153
- type BlockKind = 'text' | 'thinking' | 'tool_use' | 'redacted_thinking';
154
- const blockKind = new Map<number, BlockKind>();
155
- const blockAccum = new Map<number, string>();
156
- const blockMeta = new Map<number, { id: string; name: string; signature?: string }>();
163
+ includePartialMessages: true,
164
+ },
165
+ });
157
166
 
158
167
  // ── abort handling ──
159
168
  let aborted = false;
160
169
  const onAbort = async () => {
161
170
  if (aborted) return;
162
171
  aborted = true;
163
- try { await session!.interrupt(); } catch { /* best-effort */ }
172
+ try { await q.interrupt(); } catch { /* best-effort */ }
164
173
  partial.stopReason = "aborted";
165
174
  stream.push({ type: "error", reason: "aborted", error: partial });
166
175
  };
@@ -172,10 +181,14 @@ export function streamCodebuddy(
172
181
  }
173
182
  }
174
183
 
175
- // ── send prompt ──
176
- await session.send(userMessage);
184
+ // ── per-index block tracking ──
185
+ type BlockKind = 'text' | 'thinking' | 'tool_use' | 'redacted_thinking';
186
+ const blockKind = new Map<number, BlockKind>();
187
+ const blockAccum = new Map<number, string>();
188
+ const blockMeta = new Map<number, { id: string; name: string; signature?: string }>();
189
+
190
+ let hasStreamEvents = false;
177
191
 
178
- // ── helpers for stream_event block tracking ──
179
192
  function onBlockStart(idx: number, kind: BlockKind, initText: string, meta?: { id: string; name: string; signature?: string }) {
180
193
  blockKind.set(idx, kind);
181
194
  blockAccum.set(idx, initText);
@@ -186,22 +199,18 @@ export function streamCodebuddy(
186
199
  partial.content = [...partial.content, tc];
187
200
  stream.push({ type: 'thinking_start', contentIndex: idx, partial });
188
201
  stream.push({ type: 'thinking_end', contentIndex: idx, content: '[redacted]', partial });
189
- msgCount++;
190
202
  } else if (kind === 'thinking') {
191
203
  const tc: ThinkingContent = { type: 'thinking', thinking: initText, thinkingSignature: meta?.signature };
192
204
  partial.content = [...partial.content, tc];
193
205
  stream.push({ type: 'thinking_start', contentIndex: idx, partial });
194
- msgCount++;
195
206
  } else if (kind === 'text') {
196
207
  const tb: TextContent = { type: 'text', text: initText };
197
208
  partial.content = [...partial.content, tb];
198
209
  stream.push({ type: 'text_start', contentIndex: idx, partial });
199
- msgCount++;
200
210
  } else if (kind === 'tool_use' && meta) {
201
211
  const toolCall: ToolCall = { type: 'toolCall', id: meta.id, name: meta.name, arguments: {} };
202
212
  partial.content = [...partial.content, toolCall];
203
213
  stream.push({ type: 'toolcall_start', contentIndex: idx, partial });
204
- msgCount++;
205
214
  }
206
215
  }
207
216
 
@@ -229,7 +238,7 @@ export function streamCodebuddy(
229
238
  const blocks = [...partial.content];
230
239
  try {
231
240
  blocks[idx] = { ...blocks[idx], arguments: JSON.parse(newJson) };
232
- } catch { /* partial JSON — keep previous */ }
241
+ } catch { /* partial JSON */ }
233
242
  partial.content = blocks;
234
243
  stream.push({ type: 'toolcall_delta', contentIndex: idx, delta: deltaValue, partial } as any);
235
244
  }
@@ -248,8 +257,8 @@ export function streamCodebuddy(
248
257
  }
249
258
  }
250
259
 
251
- // ── iterate responses ──
252
- for await (const msg of session.stream()) {
260
+ // ── iterate ──
261
+ for await (const msg of q) {
253
262
  if (aborted) return;
254
263
 
255
264
  if (msg.type === "system") {
@@ -257,7 +266,6 @@ export function streamCodebuddy(
257
266
  continue;
258
267
  }
259
268
 
260
- // Primary path: token-level streaming via stream_event
261
269
  if (msg.type === "stream_event") {
262
270
  hasStreamEvents = true;
263
271
  const evt = msg.event;
@@ -279,21 +287,19 @@ export function streamCodebuddy(
279
287
  if (d.type === 'text_delta') onBlockDelta(evt.index, 'text_delta', d.text);
280
288
  else if (d.type === 'thinking_delta') onBlockDelta(evt.index, 'thinking_delta', d.thinking);
281
289
  else if (d.type === 'input_json_delta') onBlockDelta(evt.index, 'input_json_delta', d.partial_json);
282
- // ponytail: signature_delta not surfaced to Pi — signature only sent at block start
283
290
  } else if (evt.type === "content_block_stop") {
284
291
  onBlockStop(evt.index);
285
292
  }
286
293
  continue;
287
294
  }
288
295
 
289
- // Fallback: full assistant messages (used when model doesn't support streaming)
296
+ // Fallback: full assistant messages
290
297
  if (msg.type === "assistant" && !hasStreamEvents) {
291
- msgCount++;
292
298
  let ci = 0;
293
299
  for (const block of msg.message.content) {
294
300
  const idx = ci++;
295
301
  if (block.type === "thinking") {
296
- onBlockStart(idx, 'thinking', block.thinking, { id: '', name: '', signature: undefined });
302
+ onBlockStart(idx, 'thinking', block.thinking);
297
303
  onBlockStop(idx);
298
304
  } else if (block.type === "redacted_thinking") {
299
305
  onBlockStart(idx, 'redacted_thinking', '[redacted]', { id: '', name: '', signature: block.data });
@@ -319,24 +325,13 @@ export function streamCodebuddy(
319
325
  partial.stopReason = "error";
320
326
  partial.errorMessage = msg.errors?.join("; ") ?? `CodeBuddy error: ${msg.subtype}`;
321
327
  stream.push({ type: "error", reason: "error", error: { ...partial } });
322
- evictSession(cwd);
323
328
  }
324
329
  break;
325
330
  }
326
331
  }
327
-
328
- // Some models (Hunyuan) return empty on session reuse → evict for fresh session next turn
329
- if (msgCount === 0) {
330
- evictSession(cwd);
331
- }
332
332
  } catch (error) {
333
- evictSession(process.cwd());
334
333
  const errPartial = makeErrorPartial(model, error);
335
334
  stream.push({ type: "error", reason: "error", error: errPartial });
336
- } finally {
337
- // ponytail: do NOT close pooled sessions — they are reused across turns.
338
- // Only close when evicted (via evictSession).
339
- // session?.close() intentionally omitted here.
340
335
  }
341
336
  });
342
337
 
package/src/context.ts DELETED
@@ -1,238 +0,0 @@
1
- import type { Context, ImageContent, Message } from "@earendil-works/pi-ai/compat";
2
- import { convertToLlm } from "@earendil-works/pi-coding-agent";
3
- import type { UserMessage as CbUserMessage, ContentBlock } from "@tencent-ai/agent-sdk";
4
-
5
- // ── public API ──
6
-
7
- export function buildFullMessage(context: Context): string | CbUserMessage {
8
- return buildMessage(context, 0);
9
- }
10
-
11
- export function buildDeltaMessage(context: Context, startIndex: number): string | CbUserMessage {
12
- return buildMessage(context, startIndex);
13
- }
14
-
15
- // ── tool boundary (like pi-cursor-sdk's getCursorToolBoundaryText) ──
16
-
17
- const CODEBUDDY_TOOL_BOUNDARY = [
18
- "CodeBuddy SDK tool boundary:",
19
- "Call only CodeBuddy SDK native tools (Read, Write, Bash, Grep, Edit, Glob, etc.).",
20
- "Do not call any Pi-ecosystem tools — they are not registered in this session.",
21
- "Tool names from conversation history refer to a prior provider; they are unavailable now.",
22
- "CodeBuddy executes tools locally on the filesystem.",
23
- ].join("\n");
24
-
25
- const CODEBUDDY_TOOL_TAIL = [
26
- "Answer the latest user request above using CodeBuddy SDK tools and capabilities.",
27
- "Shell: cd to repo path for project commands; session cwd may differ from tool args.",
28
- "Exact-output requests: output exactly the requested text; no preamble or checks unless asked.",
29
- "Tools: call available CodeBuddy SDK tools; never print tool cards or names as assistant text.",
30
- ].join("\n");
31
-
32
- // ── system prompt sanitization (like pi-cursor-sdk's sanitizeSystemPromptForCursor) ──
33
-
34
- function sanitizeSystemPrompt(prompt: string): string {
35
- let s = prompt;
36
-
37
- // Strip Pi tool catalog section
38
- s = s.replace(
39
- /Available tools:\n[\s\S]*?\n\nIn addition to the tools above[\s\S]*?\n\n/g,
40
- "Pi tool catalog omitted: CodeBuddy can call only CodeBuddy SDK tools.\n\n",
41
- );
42
-
43
- // Strip Guidelines section (Pi-specific guidelines)
44
- s = s.replace(
45
- /Guidelines:\n[\s\S]*?\n\nPi documentation /g,
46
- "Guidelines:\n- Be concise in your responses.\n- Show file paths clearly when working with files.\n\nPi documentation ",
47
- );
48
-
49
- // Strip semantic code intelligence priority section
50
- s = s.replace(/\n+Semantic code intelligence priority:[\s\S]*$/g, "");
51
-
52
- return s.trim();
53
- }
54
-
55
- // ── message formatting ──
56
-
57
- function formatMessage(msg: Message): string | undefined {
58
- switch (msg.role) {
59
- case "user": {
60
- const text = extractTextContent(msg.content);
61
- const hasImages = extractImages(msg).length > 0;
62
- if (text) {
63
- return `User: ${text}${hasImages ? " [image omitted]" : ""}`;
64
- }
65
- if (hasImages) return "User: [image omitted]";
66
- return undefined;
67
- }
68
- case "assistant": {
69
- const blocks = Array.isArray(msg.content) ? msg.content : [];
70
- const parts: string[] = [];
71
- for (const block of blocks) {
72
- if (typeof block === "object" && block !== null && "type" in block) {
73
- if (block.type === "text") {
74
- parts.push((block as { text: string }).text);
75
- } else if (block.type === "toolCall") {
76
- const tc = block as { name: string; arguments: Record<string, unknown> };
77
- parts.push(`[Tool used: ${tc.name}(${JSON.stringify(tc.arguments)})]`);
78
- }
79
- // Skip thinking content blocks
80
- }
81
- }
82
- return parts.length > 0 ? `Assistant: ${parts.join("\n")}` : undefined;
83
- }
84
- case "toolResult": {
85
- const text = extractTextContent(msg.content);
86
- const label = msg.isError ? "Tool error" : "Tool result";
87
- const snippet = truncateToolResult(text);
88
- return `[${label} (${msg.toolName}): ${snippet}]`;
89
- }
90
- default:
91
- return undefined;
92
- }
93
- }
94
-
95
- // ── truncation ──
96
-
97
- const TOOL_RESULT_HEAD = 1200;
98
- const TOOL_RESULT_TAIL = 600;
99
-
100
- function truncateToolResult(text: string): string {
101
- if (!text) return "(empty)";
102
- if (text.length <= TOOL_RESULT_HEAD + TOOL_RESULT_TAIL + 50) return text;
103
- const head = text.slice(0, TOOL_RESULT_HEAD);
104
- const tail = text.slice(-TOOL_RESULT_TAIL);
105
- const skipped = text.length - TOOL_RESULT_HEAD - TOOL_RESULT_TAIL;
106
- return `${head}\n...[${skipped.toLocaleString()} chars skipped]...\n${tail}`;
107
- }
108
-
109
- // ── main builder ──
110
-
111
- function buildMessage(context: Context, startIndex: number): string | CbUserMessage {
112
- const parts: string[] = [];
113
-
114
- // 1. Tool boundary (always first)
115
- parts.push(CODEBUDDY_TOOL_BOUNDARY);
116
-
117
- // 2. Sanitized system prompt (first turn only — like pi-cursor-sdk)
118
- if (startIndex === 0 && context.systemPrompt) {
119
- const sanitized = sanitizeSystemPrompt(context.systemPrompt);
120
- if (sanitized) {
121
- parts.push(`System instructions from pi:\n${sanitized}`);
122
- }
123
- }
124
-
125
- // 3. Normalize and format messages (filter out non-LLM roles via convertToLlm)
126
- const rawMessages = context.messages as Parameters<typeof convertToLlm>[0];
127
- let llmMessages = convertToLlm(rawMessages);
128
-
129
- // ponytail: filter compaction/branch summary user messages on first turn.
130
- // convertToLlm wraps them as user messages with <summary> tags; Hunyuan
131
- // parrots context-mode state from them. Smarter models follow boundary text.
132
- if (startIndex === 0) {
133
- llmMessages = llmMessages.filter((m) => {
134
- if (m.role !== "user") return true;
135
- const text = extractTextContent(m.content);
136
- // Compaction summaries are large blocks with context-mode state
137
- if (text.includes("<summary>") || text.includes("compacted into")) return false;
138
- // context-mode session state injection
139
- if (text.includes("context-mode active") || text.includes("<session_state")) return false;
140
- return true;
141
- });
142
- }
143
- const latestUserIdx = findLatestUserIndex(llmMessages);
144
- const msgParts: string[] = [];
145
- for (let i = startIndex; i < llmMessages.length; i++) {
146
- if (i === latestUserIdx) continue; // handled separately (images etc.)
147
- const formatted = formatMessage(llmMessages[i]);
148
- if (formatted) msgParts.push(formatted);
149
- }
150
-
151
- // 4. Latest user message
152
- const lastUser = latestUserIdx >= 0 ? llmMessages[latestUserIdx] : null;
153
- const lastUserText = lastUser ? extractTextContent(lastUser.content) : "Continue.";
154
- const images = lastUser ? extractImages(lastUser) : [];
155
-
156
- // 5. Tool tail guard
157
- parts.push(
158
- msgParts.length > 0 ? msgParts.join("\n") : "(new conversation)",
159
- );
160
- parts.push(CODEBUDDY_TOOL_TAIL);
161
-
162
- const prefix = parts.join("\n\n");
163
-
164
- // Plain text (no images)
165
- if (images.length === 0) {
166
- return prefix + "\n\n" + lastUserText;
167
- }
168
-
169
- // Structured message with images
170
- const blocks: ContentBlock[] = [];
171
- blocks.push({ type: "text", text: prefix });
172
- blocks.push({ type: "text", text: lastUserText });
173
- for (const block of lastUser!.content) {
174
- if (typeof block === "object" && block !== null && "type" in block && (block as { type: string }).type === "image") {
175
- const img = block as ImageContent;
176
- blocks.push({
177
- type: "image",
178
- source: {
179
- type: "base64",
180
- media_type: parseImageMediaType(img.mimeType),
181
- data: stripBase64Prefix(img.data),
182
- },
183
- });
184
- }
185
- }
186
-
187
- return {
188
- type: "user",
189
- session_id: "",
190
- message: { role: "user", content: blocks },
191
- parent_tool_use_id: null,
192
- };
193
- }
194
-
195
- // ── helpers ──
196
-
197
- function findLatestUserIndex(messages: Message[]): number {
198
- for (let i = messages.length - 1; i >= 0; i--) {
199
- if (messages[i].role === "user") return i;
200
- }
201
- return -1;
202
- }
203
-
204
- function extractImages(msg: any): ImageContent[] {
205
- if (!msg || !Array.isArray(msg.content)) return [];
206
- return msg.content.filter(
207
- (c: any) => typeof c === "object" && c !== null && c.type === "image",
208
- );
209
- }
210
-
211
- function extractTextContent(content: unknown): string {
212
- if (typeof content === "string") return content;
213
- if (!Array.isArray(content)) return "";
214
- return content
215
- .filter(
216
- (c): c is { type: "text"; text: string } =>
217
- typeof c === "object" &&
218
- c !== null &&
219
- "type" in c &&
220
- c.type === "text" &&
221
- "text" in c,
222
- )
223
- .map((c) => c.text)
224
- .join("\n");
225
- }
226
-
227
- type ImageMediaType = "image/png" | "image/jpeg" | "image/gif" | "image/webp";
228
-
229
- function parseImageMediaType(mimeType: string): ImageMediaType {
230
- const valid: ImageMediaType[] = ["image/png", "image/jpeg", "image/gif", "image/webp"];
231
- if (valid.includes(mimeType as ImageMediaType)) return mimeType as ImageMediaType;
232
- return "image/png";
233
- }
234
-
235
- function stripBase64Prefix(data: string): string {
236
- const comma = data.indexOf(",");
237
- return comma >= 0 ? data.slice(comma + 1) : data;
238
- }
@@ -1,11 +0,0 @@
1
- /**
2
- * Check what Pi actually injects into Context.messages.
3
- * Logs all messages with their role and first 100 chars of content.
4
- */
5
- import { createAssistantMessageEventStream } from '@earendil-works/pi-ai';
6
-
7
- // Register a fake provider to intercept Context
8
- // We can't easily intercept Context without running Pi, so let's just
9
- // dump what our extension sees when loaded from Pi.
10
- console.log("Run this from Pi: pi -e src/dump-context.ts --provider codebuddy --model hy3-preview-agent-ioa -p 'test'");
11
- console.log("Then check the console output for injected messages.");
@@ -1,99 +0,0 @@
1
- /**
2
- * Session Pool — CodeBuddy CLI subprocess reuse across Pi turns.
3
- *
4
- * ~3s CLI cold-start saved per turn after the first.
5
- *
6
- * Key: cwd + fingerprint of conversation's first user message.
7
- * Session change detected when the first message differs.
8
- *
9
- * Anti-double-history: on continuation, only send new messages
10
- * (the delta since last send). CodeBuddy session keeps internal context.
11
- */
12
- import type { Message as CbMessage, Session } from "@tencent-ai/agent-sdk";
13
- import type { Context } from "@earendil-works/pi-ai/compat";
14
-
15
- type CodebuddySdk = typeof import("@tencent-ai/agent-sdk");
16
-
17
- interface PoolEntry {
18
- session: Session;
19
- sentCount: number;
20
- firstFingerprint: string;
21
- }
22
-
23
- const pool = new Map<string, PoolEntry>();
24
-
25
- function fingerprintFirstMessage(context: Context): string {
26
- const first = context.messages[0];
27
- if (!first || first.role !== "user") return "";
28
- if (typeof first.content === "string") return first.content.slice(0, 80);
29
- if (Array.isArray(first.content)) {
30
- const texts = first.content
31
- .filter((c: any) => c?.type === "text")
32
- .map((c: any) => c.text)
33
- .join(" ");
34
- return texts.slice(0, 80);
35
- }
36
- return "";
37
- }
38
-
39
- export async function acquireSession(
40
- cwd: string,
41
- sdk: CodebuddySdk,
42
- context: Context,
43
- buildFull: (ctx: Context) => string | CbMessage,
44
- buildDelta: (ctx: Context, startIndex: number) => string | CbMessage,
45
- sessionOptions: Parameters<CodebuddySdk["unstable_v2_createSession"]>[0],
46
- ): Promise<{
47
- session: Session;
48
- message: string | CbMessage;
49
- isFirst: boolean;
50
- }> {
51
- const key = cwd || process.cwd();
52
- const fp = fingerprintFirstMessage(context);
53
- const existing = pool.get(key);
54
-
55
- const isNew = !existing || existing.firstFingerprint !== fp;
56
-
57
- if (isNew) {
58
- if (existing) {
59
- try { existing.session.close(); } catch { /* best-effort */ }
60
- }
61
-
62
- const session = sdk.unstable_v2_createSession(sessionOptions);
63
- try {
64
- await session.connect();
65
- } catch (e) {
66
- try { session.close(); } catch { /* best-effort, already broken */ }
67
- throw e;
68
- }
69
-
70
- const message = buildFull(context);
71
- pool.set(key, { session, sentCount: context.messages.length, firstFingerprint: fp });
72
- return { session, message, isFirst: true };
73
- }
74
-
75
- const deltaStart = existing.sentCount;
76
- const message =
77
- deltaStart < context.messages.length
78
- ? buildDelta(context, deltaStart)
79
- : "Continue.";
80
-
81
- existing.sentCount = context.messages.length;
82
- return { session: existing.session, message, isFirst: false };
83
- }
84
-
85
- export function evictSession(cwd: string): void {
86
- const key = cwd || process.cwd();
87
- const entry = pool.get(key);
88
- if (entry) {
89
- try { entry.session.close(); } catch { /* best-effort */ }
90
- pool.delete(key);
91
- }
92
- }
93
-
94
- export function closeAll(): void {
95
- for (const [key, entry] of pool) {
96
- try { entry.session.close(); } catch { /* best-effort */ }
97
- pool.delete(key);
98
- }
99
- }