@hienlh/ppm 0.5.11 → 0.5.13
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/CHANGELOG.md +11 -0
- package/package.json +1 -1
- package/src/providers/claude-agent-sdk.ts +173 -113
- package/test-sdk.mjs +106 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.13] - 2026-03-18
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Windows: simulated token streaming** — CLI `stream-json` only emits complete `assistant` messages (no per-token deltas). Now synthesizes `stream_event` / `content_block_delta` events in ~30-char chunks so FE gets smooth typing effect instead of all text appearing at once
|
|
7
|
+
|
|
8
|
+
## [0.5.12] - 2026-03-18
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **Windows: direct CLI fallback for chat** — on Windows, bypass SDK `query()` (broken due to Bun subprocess pipe buffering) and spawn `claude -p --verbose --output-format stream-json` directly. Same event format, same features — streaming, tools, session resume all work
|
|
12
|
+
- Removed SDK timeout/diagnostic code (no longer needed with direct CLI fallback)
|
|
13
|
+
|
|
3
14
|
## [0.5.11] - 2026-03-18
|
|
4
15
|
|
|
5
16
|
### Fixed
|
package/package.json
CHANGED
|
@@ -88,6 +88,130 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
88
88
|
} catch { return {}; }
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Direct CLI fallback for Windows — spawns `claude -p` with stream-json output.
|
|
93
|
+
* Workaround for Bun + Windows SDK subprocess pipe buffering issue.
|
|
94
|
+
* Returns an async generator yielding the same event types as SDK query().
|
|
95
|
+
*/
|
|
96
|
+
private async *queryDirectCli(opts: {
|
|
97
|
+
prompt: string;
|
|
98
|
+
cwd: string;
|
|
99
|
+
sessionId: string;
|
|
100
|
+
sdkId: string;
|
|
101
|
+
isFirstMessage: boolean;
|
|
102
|
+
shouldFork: boolean;
|
|
103
|
+
env: Record<string, string | undefined>;
|
|
104
|
+
providerConfig: Partial<import("../types/config.ts").AIProviderConfig>;
|
|
105
|
+
}): AsyncGenerator<any> {
|
|
106
|
+
const args = ["-p", opts.prompt, "--verbose", "--output-format", "stream-json"];
|
|
107
|
+
|
|
108
|
+
// Session management
|
|
109
|
+
if (!opts.isFirstMessage || opts.shouldFork) {
|
|
110
|
+
args.push("--resume", opts.sdkId);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Config-driven options
|
|
114
|
+
if (opts.providerConfig.model) args.push("--model", opts.providerConfig.model);
|
|
115
|
+
const maxTurns = opts.providerConfig.max_turns ?? 100;
|
|
116
|
+
args.push("--max-turns", String(maxTurns));
|
|
117
|
+
if (opts.providerConfig.effort) args.push("--effort", opts.providerConfig.effort);
|
|
118
|
+
|
|
119
|
+
// Permission mode
|
|
120
|
+
args.push("--permission-mode", "bypassPermissions", "--dangerously-skip-permissions");
|
|
121
|
+
|
|
122
|
+
console.log(`[sdk-cli] spawning: claude ${args.slice(0, 6).join(" ")}... cwd=${opts.cwd}`);
|
|
123
|
+
|
|
124
|
+
const proc = Bun.spawn({
|
|
125
|
+
cmd: ["claude", ...args],
|
|
126
|
+
cwd: opts.cwd,
|
|
127
|
+
stdout: "pipe",
|
|
128
|
+
stderr: "pipe",
|
|
129
|
+
env: opts.env as Record<string, string>,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Store proc for abort support
|
|
133
|
+
const abortHandle = { close: () => { try { proc.kill(); } catch {} } };
|
|
134
|
+
this.activeQueries.set(opts.sessionId, abortHandle as any);
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const reader = proc.stdout.getReader();
|
|
138
|
+
const decoder = new TextDecoder();
|
|
139
|
+
let buffer = "";
|
|
140
|
+
|
|
141
|
+
while (true) {
|
|
142
|
+
const { done, value } = await reader.read();
|
|
143
|
+
if (done) break;
|
|
144
|
+
|
|
145
|
+
buffer += decoder.decode(value, { stream: true });
|
|
146
|
+
const lines = buffer.split("\n");
|
|
147
|
+
buffer = lines.pop() ?? ""; // Keep incomplete last line in buffer
|
|
148
|
+
|
|
149
|
+
for (const line of lines) {
|
|
150
|
+
const trimmed = line.trim();
|
|
151
|
+
if (!trimmed) continue;
|
|
152
|
+
try {
|
|
153
|
+
const event = JSON.parse(trimmed);
|
|
154
|
+
// CLI stream-json doesn't emit per-token stream_event deltas — it sends
|
|
155
|
+
// complete assistant messages. Synthesize stream_event deltas so the FE
|
|
156
|
+
// gets a smooth streaming experience (same as SDK with includePartialMessages).
|
|
157
|
+
if (event.type === "assistant" && event.message?.content) {
|
|
158
|
+
for (const block of event.message.content) {
|
|
159
|
+
if (block.type === "text" && block.text) {
|
|
160
|
+
// Emit text in ~30-char chunks as synthetic stream_event deltas
|
|
161
|
+
const text = block.text as string;
|
|
162
|
+
const CHUNK = 30;
|
|
163
|
+
for (let i = 0; i < text.length; i += CHUNK) {
|
|
164
|
+
yield {
|
|
165
|
+
type: "stream_event",
|
|
166
|
+
event: {
|
|
167
|
+
type: "content_block_delta",
|
|
168
|
+
delta: { type: "text_delta", text: text.slice(i, i + CHUNK) },
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
} else if (block.type === "thinking" && block.thinking) {
|
|
173
|
+
yield {
|
|
174
|
+
type: "stream_event",
|
|
175
|
+
event: {
|
|
176
|
+
type: "content_block_delta",
|
|
177
|
+
delta: { type: "thinking_delta", thinking: block.thinking },
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Always yield the original event too (for init, result, rate_limit, etc.)
|
|
184
|
+
yield event;
|
|
185
|
+
} catch {
|
|
186
|
+
// Skip non-JSON lines (e.g. progress indicators)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Process remaining buffer
|
|
192
|
+
if (buffer.trim()) {
|
|
193
|
+
try { yield JSON.parse(buffer.trim()); } catch {}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Wait for process to exit
|
|
197
|
+
const exitCode = await proc.exited;
|
|
198
|
+
console.log(`[sdk-cli] process exited: code=${exitCode}`);
|
|
199
|
+
|
|
200
|
+
// Read stderr if process failed
|
|
201
|
+
if (exitCode !== 0) {
|
|
202
|
+
try {
|
|
203
|
+
const errReader = proc.stderr.getReader();
|
|
204
|
+
const { value: errBytes } = await errReader.read();
|
|
205
|
+
const stderr = errBytes ? new TextDecoder().decode(errBytes).trim() : "";
|
|
206
|
+
if (stderr) console.error(`[sdk-cli] stderr: ${stderr.slice(0, 500)}`);
|
|
207
|
+
} catch {}
|
|
208
|
+
}
|
|
209
|
+
} finally {
|
|
210
|
+
this.activeQueries.delete(opts.sessionId);
|
|
211
|
+
try { proc.kill(); } catch {}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
91
215
|
/** Read current provider config from yaml (fresh each call) */
|
|
92
216
|
private getProviderConfig(): Partial<import("../types/config.ts").AIProviderConfig> {
|
|
93
217
|
const ai = configService.get("ai");
|
|
@@ -289,9 +413,6 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
289
413
|
let resultSubtype: string | undefined;
|
|
290
414
|
let resultNumTurns: number | undefined;
|
|
291
415
|
let resultContextWindowPct: number | undefined;
|
|
292
|
-
let firstEventReceived = false;
|
|
293
|
-
let sdkTimeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
294
|
-
|
|
295
416
|
try {
|
|
296
417
|
const providerConfig = this.getProviderConfig();
|
|
297
418
|
// Resolve SDK's actual session ID for resume (may differ from PPM's UUID)
|
|
@@ -300,113 +421,67 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
300
421
|
// Fallback cwd: SDK needs a valid working directory even when no project is selected.
|
|
301
422
|
// On Windows daemons, undefined cwd can cause the subprocess to fail silently.
|
|
302
423
|
const effectiveCwd = meta.projectPath || homedir();
|
|
424
|
+
const queryEnv = { ...process.env, ...this.getProjectEnvOverrides(meta.projectPath) };
|
|
303
425
|
console.log(`[sdk] query: session=${sessionId} sdkId=${sdkId} isFirst=${isFirstMessage} fork=${shouldFork} cwd=${effectiveCwd} platform=${process.platform}`);
|
|
304
426
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
sessionId: isFirstMessage && !shouldFork ? sessionId : undefined,
|
|
309
|
-
resume: (isFirstMessage && !shouldFork) ? undefined : sdkId,
|
|
310
|
-
...(shouldFork && { forkSession: true }),
|
|
311
|
-
cwd: effectiveCwd,
|
|
312
|
-
// Use full Claude Code system prompt (coding guidelines, security, response style)
|
|
313
|
-
systemPrompt: { type: "preset", preset: "claude_code" },
|
|
314
|
-
// Load skills/settings from both user (~/.claude) and project directory
|
|
315
|
-
settingSources: ["user", "project"],
|
|
316
|
-
// Neutralize project .env keys that would override user's global auth.
|
|
317
|
-
// Only clear if the project's .env contains them (prevents .env poisoning).
|
|
318
|
-
// Keep global env vars intact for API key auth users.
|
|
319
|
-
env: {
|
|
320
|
-
...process.env,
|
|
321
|
-
...this.getProjectEnvOverrides(meta.projectPath),
|
|
322
|
-
},
|
|
323
|
-
// Override project-local Claude settings that may restrict tool permissions
|
|
324
|
-
settings: { permissions: { allow: [], deny: [] } },
|
|
325
|
-
allowedTools: [
|
|
326
|
-
"Read", "Write", "Edit", "Bash", "Glob", "Grep",
|
|
327
|
-
"WebSearch", "WebFetch", "AskUserQuestion",
|
|
328
|
-
"Agent", "Skill", "TodoWrite", "ToolSearch",
|
|
329
|
-
],
|
|
330
|
-
permissionMode: "bypassPermissions",
|
|
331
|
-
allowDangerouslySkipPermissions: true,
|
|
332
|
-
// Config-driven values from ppm.yaml
|
|
333
|
-
...(providerConfig.model && { model: providerConfig.model }),
|
|
334
|
-
...(providerConfig.effort && { effort: providerConfig.effort }),
|
|
335
|
-
maxTurns: providerConfig.max_turns ?? 100,
|
|
336
|
-
...(providerConfig.max_budget_usd && { maxBudgetUsd: providerConfig.max_budget_usd }),
|
|
337
|
-
...(providerConfig.thinking_budget_tokens != null && {
|
|
338
|
-
thinkingBudgetTokens: providerConfig.thinking_budget_tokens,
|
|
339
|
-
}),
|
|
340
|
-
canUseTool,
|
|
341
|
-
includePartialMessages: true,
|
|
342
|
-
} as any,
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
// Track active query for abort support
|
|
346
|
-
this.activeQueries.set(sessionId, q);
|
|
347
|
-
console.log(`[sdk] query object created, starting iteration...`);
|
|
427
|
+
// On Windows, use direct CLI fallback (SDK query() hangs due to Bun subprocess pipe buffering)
|
|
428
|
+
const useDirectCli = process.platform === "win32";
|
|
429
|
+
let eventSource: AsyncIterable<any>;
|
|
348
430
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
stdout: "pipe", stderr: "pipe",
|
|
354
|
-
});
|
|
355
|
-
const claudePath = which.stdout.toString().trim().split("\n")[0];
|
|
356
|
-
console.log(`[sdk] claude CLI: ${claudePath || "(not found in PATH)"}`);
|
|
357
|
-
} catch { console.log("[sdk] claude CLI: check failed"); }
|
|
358
|
-
|
|
359
|
-
// Quick CLI version check — verify the binary actually runs from this process
|
|
360
|
-
try {
|
|
361
|
-
const verProc = Bun.spawnSync({
|
|
362
|
-
cmd: ["claude", "--version"],
|
|
363
|
-
stdout: "pipe", stderr: "pipe",
|
|
431
|
+
if (useDirectCli) {
|
|
432
|
+
console.log(`[sdk] Windows detected — using direct CLI fallback (bypasses SDK pipe issue)`);
|
|
433
|
+
eventSource = this.queryDirectCli({
|
|
434
|
+
prompt: message,
|
|
364
435
|
cwd: effectiveCwd,
|
|
436
|
+
sessionId,
|
|
437
|
+
sdkId,
|
|
438
|
+
isFirstMessage,
|
|
439
|
+
shouldFork,
|
|
440
|
+
env: queryEnv,
|
|
441
|
+
providerConfig,
|
|
365
442
|
});
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
443
|
+
} else {
|
|
444
|
+
const q = query({
|
|
445
|
+
prompt: message,
|
|
446
|
+
options: {
|
|
447
|
+
sessionId: isFirstMessage && !shouldFork ? sessionId : undefined,
|
|
448
|
+
resume: (isFirstMessage && !shouldFork) ? undefined : sdkId,
|
|
449
|
+
...(shouldFork && { forkSession: true }),
|
|
450
|
+
cwd: effectiveCwd,
|
|
451
|
+
systemPrompt: { type: "preset", preset: "claude_code" },
|
|
452
|
+
settingSources: ["user", "project"],
|
|
453
|
+
env: queryEnv,
|
|
454
|
+
settings: { permissions: { allow: [], deny: [] } },
|
|
455
|
+
allowedTools: [
|
|
456
|
+
"Read", "Write", "Edit", "Bash", "Glob", "Grep",
|
|
457
|
+
"WebSearch", "WebFetch", "AskUserQuestion",
|
|
458
|
+
"Agent", "Skill", "TodoWrite", "ToolSearch",
|
|
459
|
+
],
|
|
460
|
+
permissionMode: "bypassPermissions",
|
|
461
|
+
allowDangerouslySkipPermissions: true,
|
|
462
|
+
...(providerConfig.model && { model: providerConfig.model }),
|
|
463
|
+
...(providerConfig.effort && { effort: providerConfig.effort }),
|
|
464
|
+
maxTurns: providerConfig.max_turns ?? 100,
|
|
465
|
+
...(providerConfig.max_budget_usd && { maxBudgetUsd: providerConfig.max_budget_usd }),
|
|
466
|
+
...(providerConfig.thinking_budget_tokens != null && {
|
|
467
|
+
thinkingBudgetTokens: providerConfig.thinking_budget_tokens,
|
|
468
|
+
}),
|
|
469
|
+
canUseTool,
|
|
470
|
+
includePartialMessages: true,
|
|
471
|
+
} as any,
|
|
472
|
+
});
|
|
473
|
+
this.activeQueries.set(sessionId, q);
|
|
474
|
+
eventSource = q;
|
|
372
475
|
}
|
|
373
476
|
|
|
374
|
-
// Log env keys relevant to SDK auth (values redacted)
|
|
375
|
-
const authKeys = ["ANTHROPIC_API_KEY", "CLAUDE_CODE_USE_BEDROCK", "CLAUDE_CODE_USE_VERTEX", "CLAUDE_CODE_USE_FOUNDRY"];
|
|
376
|
-
const envStatus = authKeys.map(k => `${k}=${process.env[k] ? "SET" : "unset"}`).join(" ");
|
|
377
|
-
console.log(`[sdk] env auth: ${envStatus}`);
|
|
378
|
-
|
|
379
477
|
let lastPartialText = "";
|
|
380
478
|
/** Number of tool_use blocks pending results (top-level tools only, not subagent children) */
|
|
381
479
|
let pendingToolCount = 0;
|
|
382
480
|
|
|
383
|
-
// First-event timeout: if SDK hangs (common on Windows/Bun), close query and run diagnostics
|
|
384
|
-
const SDK_TIMEOUT_MS = 30_000;
|
|
385
|
-
sdkTimeoutId = setTimeout(async () => {
|
|
386
|
-
if (firstEventReceived) return;
|
|
387
|
-
console.error(`[sdk] TIMEOUT: no events after ${SDK_TIMEOUT_MS / 1000}s — closing query and running diagnostics`);
|
|
388
|
-
try { q.close(); } catch {}
|
|
389
|
-
|
|
390
|
-
// Direct CLI test to determine if the issue is SDK-specific or CLI-wide
|
|
391
|
-
try {
|
|
392
|
-
const directProc = Bun.spawnSync({
|
|
393
|
-
cmd: ["claude", "-p", "say ok", "--output-format", "stream-json", "--max-turns", "1"],
|
|
394
|
-
stdout: "pipe", stderr: "pipe",
|
|
395
|
-
cwd: effectiveCwd,
|
|
396
|
-
env: { ...process.env, ...this.getProjectEnvOverrides(meta.projectPath) },
|
|
397
|
-
});
|
|
398
|
-
console.log(`[sdk] direct CLI test: exit=${directProc.exitCode} stdout=${directProc.stdout.toString().trim().slice(0, 500)} stderr=${directProc.stderr.toString().trim().slice(0, 300)}`);
|
|
399
|
-
} catch (e) {
|
|
400
|
-
console.error(`[sdk] direct CLI test failed: ${(e as Error).message}`);
|
|
401
|
-
}
|
|
402
|
-
}, SDK_TIMEOUT_MS);
|
|
403
|
-
|
|
404
481
|
let sdkEventCount = 0;
|
|
405
|
-
for await (const msg of
|
|
482
|
+
for await (const msg of eventSource) {
|
|
406
483
|
sdkEventCount++;
|
|
407
484
|
if (sdkEventCount === 1) {
|
|
408
|
-
firstEventReceived = true;
|
|
409
|
-
clearTimeout(sdkTimeoutId);
|
|
410
485
|
console.log(`[sdk] first event received: type=${(msg as any).type} subtype=${(msg as any).subtype ?? "none"}`);
|
|
411
486
|
}
|
|
412
487
|
// Extract parent_tool_use_id from SDK message (present on subagent-scoped messages)
|
|
@@ -647,31 +722,16 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
647
722
|
yield approvalEvents.shift()!;
|
|
648
723
|
}
|
|
649
724
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
yield {
|
|
653
|
-
type: "error",
|
|
654
|
-
message: "Claude SDK did not respond (query timed out). This may be a Bun + Windows compatibility issue. Try: `ppm chat` from terminal, or run with Node.js: `npx tsx src/index.ts start -f`",
|
|
655
|
-
};
|
|
725
|
+
if (sdkEventCount === 0) {
|
|
726
|
+
yield { type: "error", message: "Claude did not respond. Check that 'claude' CLI works in your terminal." };
|
|
656
727
|
}
|
|
657
728
|
} catch (e) {
|
|
658
729
|
const msg = (e as Error).message ?? String(e);
|
|
659
|
-
const stack = (e as Error).stack ?? "";
|
|
660
730
|
console.error(`[sdk] error: ${msg}`);
|
|
661
|
-
if (stack) console.error(`[sdk] stack: ${stack}`);
|
|
662
|
-
// Don't yield error for intentional abort or timeout-triggered close
|
|
663
731
|
if (!msg.includes("abort") && !msg.includes("closed")) {
|
|
664
732
|
yield { type: "error", message: `SDK error: ${msg}` };
|
|
665
733
|
}
|
|
666
|
-
// If closed by timeout (no events received), provide user-facing error
|
|
667
|
-
if (!firstEventReceived) {
|
|
668
|
-
yield {
|
|
669
|
-
type: "error",
|
|
670
|
-
message: "Claude SDK did not respond (query timed out). This may be a Bun + Windows compatibility issue. Try: `ppm chat` from terminal, or run with Node.js: `npx tsx src/index.ts start -f`",
|
|
671
|
-
};
|
|
672
|
-
}
|
|
673
734
|
} finally {
|
|
674
|
-
clearTimeout(sdkTimeoutId);
|
|
675
735
|
this.activeQueries.delete(sessionId);
|
|
676
736
|
}
|
|
677
737
|
|
package/test-sdk.mjs
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal SDK test — run on Windows to diagnose Bun + SDK issue.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* bun test-sdk.mjs
|
|
6
|
+
* node --experimental-strip-types test-sdk.mjs
|
|
7
|
+
*/
|
|
8
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
|
|
12
|
+
// Remove CLAUDECODE to avoid nested session error
|
|
13
|
+
delete process.env.CLAUDECODE;
|
|
14
|
+
|
|
15
|
+
const cwd = homedir();
|
|
16
|
+
|
|
17
|
+
console.log("=== SDK Test ===");
|
|
18
|
+
console.log(`Platform: ${process.platform}`);
|
|
19
|
+
console.log(`Runtime: ${typeof Bun !== "undefined" ? `Bun ${Bun.version}` : `Node ${process.version}`}`);
|
|
20
|
+
console.log(`CWD: ${cwd}`);
|
|
21
|
+
console.log(`API_KEY: ${process.env.ANTHROPIC_API_KEY ? "SET" : "unset"}`);
|
|
22
|
+
|
|
23
|
+
// Test 1: claude --version
|
|
24
|
+
console.log("\n--- Test 1: claude --version ---");
|
|
25
|
+
try {
|
|
26
|
+
const ver = spawnSync("claude", ["--version"], { encoding: "utf-8", timeout: 10000 });
|
|
27
|
+
console.log(`exit=${ver.status} stdout="${ver.stdout?.trim()}" stderr="${ver.stderr?.trim()}"`);
|
|
28
|
+
} catch (e) {
|
|
29
|
+
console.log(`FAILED: ${e.message}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Test 2: claude -p (direct CLI)
|
|
33
|
+
console.log("\n--- Test 2: claude -p (direct spawn) ---");
|
|
34
|
+
try {
|
|
35
|
+
const direct = spawnSync("claude", ["-p", "say ok", "--output-format", "text", "--max-turns", "1"], {
|
|
36
|
+
encoding: "utf-8",
|
|
37
|
+
timeout: 30000,
|
|
38
|
+
cwd,
|
|
39
|
+
env: process.env,
|
|
40
|
+
});
|
|
41
|
+
console.log(`exit=${direct.status}`);
|
|
42
|
+
console.log(`stdout="${direct.stdout?.trim().slice(0, 200)}"`);
|
|
43
|
+
if (direct.stderr?.trim()) console.log(`stderr="${direct.stderr.trim().slice(0, 200)}"`);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
console.log(`FAILED: ${e.message}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Test 3: SDK query()
|
|
49
|
+
console.log("\n--- Test 3: SDK query() ---");
|
|
50
|
+
const startTime = Date.now();
|
|
51
|
+
const TIMEOUT = 15000;
|
|
52
|
+
let gotEvent = false;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const q = query({
|
|
56
|
+
prompt: "say ok",
|
|
57
|
+
options: {
|
|
58
|
+
cwd,
|
|
59
|
+
maxTurns: 1,
|
|
60
|
+
permissionMode: "bypassPermissions",
|
|
61
|
+
allowDangerouslySkipPermissions: true,
|
|
62
|
+
systemPrompt: { type: "custom", custom: "Reply only with: ok" },
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Race first event against timeout
|
|
67
|
+
const iterator = q[Symbol.asyncIterator]();
|
|
68
|
+
const result = await Promise.race([
|
|
69
|
+
iterator.next(),
|
|
70
|
+
new Promise((resolve) => setTimeout(() => resolve("TIMEOUT"), TIMEOUT)),
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
if (result === "TIMEOUT") {
|
|
74
|
+
console.log(`TIMEOUT: no events after ${TIMEOUT / 1000}s`);
|
|
75
|
+
console.log(">>> This confirms the Bun + Windows SDK issue <<<");
|
|
76
|
+
try { q.close(); } catch {}
|
|
77
|
+
} else {
|
|
78
|
+
gotEvent = true;
|
|
79
|
+
const elapsed = Date.now() - startTime;
|
|
80
|
+
const msg = result.value;
|
|
81
|
+
console.log(`First event in ${elapsed}ms: type=${msg?.type} subtype=${msg?.subtype ?? "none"}`);
|
|
82
|
+
|
|
83
|
+
// Read remaining events
|
|
84
|
+
let count = 1;
|
|
85
|
+
for await (const ev of { [Symbol.asyncIterator]: () => iterator }) {
|
|
86
|
+
count++;
|
|
87
|
+
if (ev.type === "assistant") {
|
|
88
|
+
const text = ev.message?.content?.find((b) => b.type === "text")?.text ?? "";
|
|
89
|
+
console.log(`Event #${count}: assistant text="${text.slice(0, 100)}"`);
|
|
90
|
+
} else if (ev.type === "result") {
|
|
91
|
+
console.log(`Event #${count}: result subtype=${ev.subtype}`);
|
|
92
|
+
break;
|
|
93
|
+
} else {
|
|
94
|
+
console.log(`Event #${count}: ${ev.type}`);
|
|
95
|
+
}
|
|
96
|
+
if (count > 20) { console.log("(stopping after 20 events)"); break; }
|
|
97
|
+
}
|
|
98
|
+
console.log(`\nSUCCESS: SDK works! Total events: ${count}`);
|
|
99
|
+
}
|
|
100
|
+
} catch (e) {
|
|
101
|
+
console.log(`ERROR: ${e.message}`);
|
|
102
|
+
if (e.stack) console.log(e.stack);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.log(`\nTotal time: ${((Date.now() - startTime) / 1000).toFixed(1)}s`);
|
|
106
|
+
process.exit(gotEvent ? 0 : 1);
|