@hienlh/ppm 0.8.14 → 0.8.15

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.15] - 2026-03-24
4
+
5
+ ### Fixed
6
+ - **Usage polling reliability**: Replace `setInterval` with recursive `setTimeout` to prevent overlap and timer death from unhandled async rejections; wrap `pollOnce` in try/catch
7
+
8
+ ### Added
9
+ - **Lowest-usage account strategy**: New `lowest-usage` routing strategy picks account with lowest 5-hour utilization, skips accounts at 100% weekly/5hr, falls back gracefully when all exhausted
10
+
3
11
  ## [0.8.14] - 2026-03-24
4
12
 
5
13
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.8.14",
3
+ "version": "0.8.15",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "author": "hienlh",
6
6
  "license": "MIT",
@@ -18,7 +18,7 @@
18
18
  "start": "bun run src/index.ts start",
19
19
  "typecheck": "bunx tsc --noEmit",
20
20
  "prepublishOnly": "bun run build:web",
21
- "postinstall": "echo 'postinstall done'"
21
+ "postinstall": "node scripts/patch-sdk.mjs"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@tailwindcss/vite": "^4.2.1",
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Postinstall patch for @anthropic-ai/claude-agent-sdk
4
+ *
5
+ * Fixes Windows + Bun subprocess pipe issues:
6
+ * 1. Adding drain() handling to ProcessTransport.write()
7
+ * 2. Awaiting the initial prompt write in query() entry point
8
+ * 3. Replacing readline async iterator with manual line reader in readMessages()
9
+ *
10
+ * Bun on Windows has broken: stdin pipe backpressure, unawaited async writes,
11
+ * and readline.createInterface() async iterator (Symbol.asyncIterator).
12
+ *
13
+ * Tracking issues:
14
+ * - TS SDK #44: https://github.com/anthropics/claude-agent-sdk-typescript/issues/44
15
+ * - TS SDK #64: https://github.com/anthropics/claude-agent-sdk-typescript/issues/64
16
+ *
17
+ * Remove this patch when upstream fixes land.
18
+ */
19
+
20
+ import { readFileSync, writeFileSync, existsSync } from "fs";
21
+ import { join } from "path";
22
+
23
+ const sdkPath = join(
24
+ import.meta.dirname,
25
+ "..",
26
+ "node_modules",
27
+ "@anthropic-ai",
28
+ "claude-agent-sdk",
29
+ "sdk.mjs",
30
+ );
31
+
32
+ if (!existsSync(sdkPath)) {
33
+ console.log("[patch-sdk] SDK not found, skipping patch");
34
+ process.exit(0);
35
+ }
36
+
37
+ let content = readFileSync(sdkPath, "utf8");
38
+ let patches = 0;
39
+
40
+ // ── Patch 1: ProcessTransport.write() — add drain handling ──
41
+
42
+ if (content.includes("waiting for drain")) {
43
+ console.log("[patch-sdk] Patch 1 (drain): already applied");
44
+ } else {
45
+ // Surgical approach: find the backpressure line and patch it
46
+ const drainPattern =
47
+ /if\(!this\.processStdin\.write\(([A-Za-z_$][A-Za-z0-9_$]*)\)\)([A-Za-z_$][A-Za-z0-9_$]*)\("\[ProcessTransport\] Write buffer full, data queued"\)/;
48
+ const drainMatch = content.match(drainPattern);
49
+
50
+ if (!drainMatch) {
51
+ console.warn("[patch-sdk] Patch 1 (drain): pattern not found, skipping");
52
+ } else {
53
+ const oldLine = drainMatch[0];
54
+ const arg = drainMatch[1];
55
+ const logger = drainMatch[2];
56
+
57
+ // Replace backpressure line: await drain instead of just logging
58
+ const newLine =
59
+ `if(!this.processStdin.write(${arg})){` +
60
+ `${logger}("[ProcessTransport] Write buffer full, waiting for drain");` +
61
+ `await new Promise(_dr=>this.processStdin.once("drain",_dr))}`;
62
+
63
+ content = content.replace(oldLine, newLine);
64
+
65
+ // Make the method async
66
+ const writeIdx = content.indexOf(newLine);
67
+ const oldDecl = `write(${arg}){`;
68
+ const declIdx = content.lastIndexOf(oldDecl, writeIdx);
69
+ if (declIdx >= 0) {
70
+ content =
71
+ content.substring(0, declIdx) +
72
+ `async write(${arg}){` +
73
+ content.substring(declIdx + oldDecl.length);
74
+ }
75
+
76
+ patches++;
77
+ console.log("[patch-sdk] Patch 1 (drain): applied");
78
+ }
79
+ }
80
+
81
+ // ── Patch 2: Await initial prompt write in query() entry point ──
82
+ // The query() function writes the user prompt to transport.write() without
83
+ // awaiting. Since write() is now async (returns Promise on backpressure),
84
+ // the prompt data can be lost on Windows where pipe buffers are small.
85
+ //
86
+ // Pattern (minified):
87
+ // if(typeof Q==="string")TRANSPORT.write(SERIALIZE({type:"user",...})+"\n");
88
+ // else QUERY.streamInput(Q);
89
+ //
90
+ // We need to await the write and make the surrounding context async-compatible.
91
+ // Since write is fire-and-forget here (the Promise is dropped), we wrap it.
92
+
93
+ if (content.includes("__ppm_await_write__")) {
94
+ console.log("[patch-sdk] Patch 2 (await prompt): already applied");
95
+ } else {
96
+ // Match: TRANSPORT.write(SERIALIZE({type:"user",...})+`\n`);
97
+ // Anchor on stable string literals: type:"user",session_id:"",message:{role:"user"
98
+ const promptWritePattern =
99
+ /([A-Za-z_$][A-Za-z0-9_$]*)\.write\(([A-Za-z_$][A-Za-z0-9_$]*)\(\{type:"user",session_id:"",message:\{role:"user",content:\[\{type:"text",text:([A-Za-z_$][A-Za-z0-9_$]*)\}\]\},parent_tool_use_id:null\}\)\+(?:`\n`|"\\n")\)/;
100
+ const promptMatch = content.match(promptWritePattern);
101
+
102
+ if (!promptMatch) {
103
+ console.warn(
104
+ "[patch-sdk] Patch 2 (await prompt): pattern not found, skipping",
105
+ );
106
+ } else {
107
+ const oldPromptWrite = promptMatch[0];
108
+ // Wrap in async IIFE — keeps query() sync so callers don't need `await query()`
109
+ const newPromptWrite =
110
+ `/*__ppm_await_write__*/(async()=>{await ${oldPromptWrite}})()`;
111
+
112
+ content = content.replace(oldPromptWrite, newPromptWrite);
113
+ patches++;
114
+ console.log("[patch-sdk] Patch 2 (await prompt): applied");
115
+ }
116
+ }
117
+
118
+ // ── Patch 3: Replace readline async iterator in readMessages() ──
119
+ // Bun on Windows doesn't implement Symbol.asyncIterator for
120
+ // readline.createInterface(), causing "undefined is not a function"
121
+ // when the SDK does `for await (let X of readlineInterface)`.
122
+ //
123
+ // Replace with a manual line reader using raw stream 'data' events.
124
+
125
+ if (content.includes("__ppm_manual_readline__")) {
126
+ console.log("[patch-sdk] Patch 3 (readline): already applied");
127
+ } else {
128
+ // Match the readMessages method by anchoring on the stable error string
129
+ const readMsgPattern =
130
+ /async\s?\*\s?readMessages\(\)\{if\(!this\.processStdout\)throw Error\("ProcessTransport output stream not available"\);let ([A-Za-z_$][A-Za-z0-9_$]*)=([A-Za-z_$][A-Za-z0-9_$]*)\(\{input:this\.processStdout\}\);try\{for await\(let ([A-Za-z_$][A-Za-z0-9_$]*) of \1\)if\(\3\.trim\(\)\)try\{yield ([A-Za-z_$][A-Za-z0-9_$]*)\(\3\)\}catch\(([A-Za-z_$][A-Za-z0-9_$]*)\)\{throw ([A-Za-z_$][A-Za-z0-9_$]*)\(`Non-JSON stdout: \$\{\3\}`\),Error\(`CLI output was not valid JSON\. This may indicate an error during startup\. Output: \$\{\3\.slice\(0,200\)\}\$\{\3\.length>200\?"\.\.\.":""\}`\)\}await this\.waitForExit\(\)\}catch\(\3\)\{throw \3\}finally\{\1\.close\(\)\}\}/;
131
+ const readMsgMatch = content.match(readMsgPattern);
132
+
133
+ if (!readMsgMatch) {
134
+ console.warn(
135
+ "[patch-sdk] Patch 3 (readline): pattern not found, skipping",
136
+ );
137
+ } else {
138
+ const oldReadMsg = readMsgMatch[0];
139
+ const rlVar = readMsgMatch[1]; // Q (readline interface)
140
+ const createRL = readMsgMatch[2]; // DU (createInterface)
141
+ const lineVar = readMsgMatch[3]; // X (line variable)
142
+ const parseJSON = readMsgMatch[4]; // O1 (JSON parser)
143
+ const errVar = readMsgMatch[5]; // Y (error variable)
144
+ const logger = readMsgMatch[6]; // i0 (logger)
145
+
146
+ // Manual line reader: use stream 'data' events + buffer splitting
147
+ // This avoids readline's broken async iterator on Bun/Windows
148
+ const newReadMsg =
149
+ `/*__ppm_manual_readline__*/async*readMessages(){` +
150
+ `if(!this.processStdout)throw Error("ProcessTransport output stream not available");` +
151
+ // Create a manual async line iterator using stream events
152
+ `let _buf="";` +
153
+ `const _lines=[];` +
154
+ `let _done=false;` +
155
+ `let _err=null;` +
156
+ `let _resolve=null;` +
157
+ `const _notify=()=>{if(_resolve){const r=_resolve;_resolve=null;r()}};` +
158
+ `this.processStdout.setEncoding("utf8");` +
159
+ `this.processStdout.on("data",(chunk)=>{` +
160
+ `_buf+=chunk;` +
161
+ `let nl;` +
162
+ `while((nl=_buf.indexOf("\\n"))!==-1){` +
163
+ `_lines.push(_buf.slice(0,nl));` +
164
+ `_buf=_buf.slice(nl+1)` +
165
+ `}` +
166
+ `_notify()` +
167
+ `});` +
168
+ `this.processStdout.on("end",()=>{` +
169
+ `if(_buf.trim())_lines.push(_buf);` +
170
+ `_buf="";_done=true;_notify()` +
171
+ `});` +
172
+ `this.processStdout.on("error",(e)=>{_err=e;_done=true;_notify()});` +
173
+ `try{` +
174
+ `while(true){` +
175
+ `while(_lines.length>0){` +
176
+ `const ${lineVar}=_lines.shift();` +
177
+ `if(${lineVar}.trim())` +
178
+ `try{yield ${parseJSON}(${lineVar})}` +
179
+ `catch(${errVar}){` +
180
+ `throw ${logger}(\`Non-JSON stdout: \${${lineVar}}\`),` +
181
+ `Error(\`CLI output was not valid JSON. This may indicate an error during startup. Output: \${${lineVar}.slice(0,200)}\${${lineVar}.length>200?"...":""}\`)` +
182
+ `}` +
183
+ `}` +
184
+ `if(_err)throw _err;` +
185
+ `if(_done)break;` +
186
+ `await new Promise(r=>{_resolve=r})` +
187
+ `}` +
188
+ `await this.waitForExit()` +
189
+ `}catch(${lineVar}){throw ${lineVar}}}`;
190
+
191
+ content = content.replace(oldReadMsg, newReadMsg);
192
+ patches++;
193
+ console.log("[patch-sdk] Patch 3 (readline): applied");
194
+ }
195
+ }
196
+
197
+ if (patches > 0) {
198
+ writeFileSync(sdkPath, content, "utf8");
199
+ console.log(`[patch-sdk] Done — ${patches} patch(es) written`);
200
+ } else {
201
+ console.log("[patch-sdk] No patches needed");
202
+ }
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Reproduces the stdin backpressure bug that causes SDK to hang on Windows.
4
+ *
5
+ * The issue: ProcessTransport.write() calls stdin.write() but ignores the
6
+ * return value (false = buffer full). Without awaiting 'drain', the subprocess
7
+ * may never receive the data — causing a hang.
8
+ *
9
+ * This script simulates the scenario with a slow-reading subprocess.
10
+ * On macOS/Linux the OS pipe buffer is larger (64KB+), so we write enough
11
+ * to overflow it. On Windows + Bun, even small writes can trigger this.
12
+ *
13
+ * Usage: node scripts/test-drain-bug.mjs
14
+ */
15
+
16
+ import { spawn } from "node:child_process";
17
+
18
+ // Subprocess that reads stdin slowly (simulates claude CLI processing)
19
+ const slowReader = spawn("node", [
20
+ "-e",
21
+ `
22
+ // Read stdin 1 byte at a time with delays to create backpressure
23
+ process.stdin.setEncoding("utf8");
24
+ let total = 0;
25
+ process.stdin.on("data", (chunk) => {
26
+ total += chunk.length;
27
+ // Pause stdin to simulate slow processing (like claude thinking)
28
+ process.stdin.pause();
29
+ setTimeout(() => process.stdin.resume(), 50);
30
+ });
31
+ process.stdin.on("end", () => {
32
+ process.stdout.write(JSON.stringify({ received: total }));
33
+ });
34
+ `,
35
+ ]);
36
+
37
+ const CHUNK = "x".repeat(1024); // 1KB chunk
38
+ const TOTAL_WRITES = 256; // 256KB total — enough to overflow pipe buffer
39
+
40
+ // ── Test 1: WITHOUT drain (current SDK behavior) ──
41
+ console.log("=== Test 1: Write WITHOUT drain (current SDK bug) ===");
42
+ let writesFailed = 0;
43
+ let writesOk = 0;
44
+
45
+ for (let i = 0; i < TOTAL_WRITES; i++) {
46
+ const ok = slowReader.stdin.write(CHUNK);
47
+ if (!ok) writesFailed++;
48
+ else writesOk++;
49
+ }
50
+ slowReader.stdin.end();
51
+
52
+ let output = "";
53
+ slowReader.stdout.on("data", (d) => (output += d));
54
+
55
+ await new Promise((resolve) => slowReader.on("close", resolve));
56
+ const result1 = JSON.parse(output || '{"received":0}');
57
+
58
+ console.log(` Writes OK: ${writesOk}`);
59
+ console.log(` Writes FULL: ${writesFailed} (buffer was full, SDK just logs & continues)`);
60
+ console.log(` Data sent: ${TOTAL_WRITES * 1024} bytes`);
61
+ console.log(` Data received: ${result1.received} bytes`);
62
+ console.log(
63
+ ` Lost data: ${writesFailed > 0 ? "POSSIBLE — depends on OS buffer behavior" : "none (buffer was big enough)"}`,
64
+ );
65
+
66
+ // ── Test 2: WITH drain (patched behavior) ──
67
+ console.log("\n=== Test 2: Write WITH drain (patched SDK) ===");
68
+
69
+ const slowReader2 = spawn("node", [
70
+ "-e",
71
+ `
72
+ process.stdin.setEncoding("utf8");
73
+ let total = 0;
74
+ process.stdin.on("data", (chunk) => {
75
+ total += chunk.length;
76
+ process.stdin.pause();
77
+ setTimeout(() => process.stdin.resume(), 50);
78
+ });
79
+ process.stdin.on("end", () => {
80
+ process.stdout.write(JSON.stringify({ received: total }));
81
+ });
82
+ `,
83
+ ]);
84
+
85
+ let drainWaits = 0;
86
+ const start = Date.now();
87
+
88
+ for (let i = 0; i < TOTAL_WRITES; i++) {
89
+ const ok = slowReader2.stdin.write(CHUNK);
90
+ if (!ok) {
91
+ drainWaits++;
92
+ await new Promise((r) => slowReader2.stdin.once("drain", r));
93
+ }
94
+ }
95
+ slowReader2.stdin.end();
96
+
97
+ let output2 = "";
98
+ slowReader2.stdout.on("data", (d) => (output2 += d));
99
+ await new Promise((resolve) => slowReader2.on("close", resolve));
100
+ const result2 = JSON.parse(output2 || '{"received":0}');
101
+ const elapsed = Date.now() - start;
102
+
103
+ console.log(` Drain waits: ${drainWaits}`);
104
+ console.log(` Data sent: ${TOTAL_WRITES * 1024} bytes`);
105
+ console.log(` Data received: ${result2.received} bytes`);
106
+ console.log(` Match: ${result2.received === TOTAL_WRITES * 1024 ? "YES — all data delivered" : "NO — data lost!"}`);
107
+ console.log(` Time: ${elapsed}ms (slower due to drain waits, but reliable)`);
108
+
109
+ // ── Summary ──
110
+ console.log("\n=== Summary ===");
111
+ if (writesFailed > 0) {
112
+ console.log(
113
+ `Buffer overflowed ${writesFailed}x in Test 1 (no drain).`,
114
+ );
115
+ console.log(
116
+ "On Windows + Bun, this causes the SDK subprocess to hang indefinitely.",
117
+ );
118
+ console.log(
119
+ "The patch adds 'await drain' to prevent data loss → fixes the hang.",
120
+ );
121
+ } else {
122
+ console.log(
123
+ "Buffer did NOT overflow on this OS (macOS/Linux has large pipe buffers).",
124
+ );
125
+ console.log(
126
+ "On Windows + Bun, pipe buffers are smaller → overflow happens even with small prompts.",
127
+ );
128
+ console.log(
129
+ "The patch is still correct: it's a no-op when write() returns true.",
130
+ );
131
+ }
@@ -17,6 +17,7 @@ import { getSessionMapping, setSessionMapping } from "../services/db.service.ts"
17
17
  import { accountSelector } from "../services/account-selector.service.ts";
18
18
  import { accountService } from "../services/account.service.ts";
19
19
  import { resolve } from "node:path";
20
+ import { existsSync } from "node:fs";
20
21
  import { homedir } from "node:os";
21
22
 
22
23
  function getSdkSessionId(ppmId: string): string {
@@ -537,7 +538,9 @@ export class ClaudeAgentSdkProvider implements AIProvider {
537
538
  const sdkId = shouldFork ? getSdkSessionId(forkSourceId!) : getSdkSessionId(sessionId);
538
539
  // Fallback cwd: SDK needs a valid working directory even when no project is selected.
539
540
  // On Windows daemons, undefined cwd can cause the subprocess to fail silently.
540
- const effectiveCwd = meta.projectPath || homedir();
541
+ // Resolve path and validate existence — invalid cwd causes spawn to hang on Windows.
542
+ const rawCwd = meta.projectPath || homedir();
543
+ const effectiveCwd = existsSync(rawCwd) ? rawCwd : homedir();
541
544
 
542
545
  // Account-based auth injection (multi-account mode)
543
546
  // Fallback to existing env (ANTHROPIC_API_KEY) when no accounts configured.
@@ -1,7 +1,7 @@
1
1
  import { accountService, type AccountWithTokens } from "./account.service.ts";
2
- import { getConfigValue, setConfigValue } from "./db.service.ts";
2
+ import { getConfigValue, setConfigValue, getLatestSnapshotForAccount } from "./db.service.ts";
3
3
 
4
- export type AccountStrategy = "round-robin" | "fill-first";
4
+ export type AccountStrategy = "round-robin" | "fill-first" | "lowest-usage";
5
5
 
6
6
  const STRATEGY_CONFIG_KEY = "account_strategy";
7
7
  const MAX_RETRY_CONFIG_KEY = "account_max_retry";
@@ -68,7 +68,10 @@ class AccountSelectorService {
68
68
  }
69
69
 
70
70
  let pickedId: string;
71
- if (this.getStrategy() === "fill-first") {
71
+ const strategy = this.getStrategy();
72
+ if (strategy === "lowest-usage") {
73
+ pickedId = this.pickLowestUsage(active);
74
+ } else if (strategy === "fill-first") {
72
75
  const sorted = [...active].sort((a, b) => b.priority - a.priority || a.createdAt - b.createdAt);
73
76
  pickedId = sorted[0]!.id;
74
77
  } else {
@@ -85,6 +88,33 @@ class AccountSelectorService {
85
88
  return result;
86
89
  }
87
90
 
91
+ /**
92
+ * Pick account with lowest 5-hour utilization.
93
+ * Skips accounts with weekly >= 100% (fully exhausted).
94
+ * Accounts with no usage data are treated as 0% (preferred).
95
+ * Falls back to round-robin if all accounts are maxed.
96
+ */
97
+ private pickLowestUsage(active: { id: string; createdAt: number }[]): string {
98
+ const scored = active.map((acc) => {
99
+ const snap = getLatestSnapshotForAccount(acc.id);
100
+ const fiveHour = snap?.five_hour_util ?? 0;
101
+ const weekly = snap?.weekly_util ?? 0;
102
+ // weekly >= 1.0 means fully exhausted — mark as unavailable
103
+ const exhausted = weekly >= 1.0 || fiveHour >= 1.0;
104
+ return { id: acc.id, fiveHour, weekly, exhausted };
105
+ });
106
+
107
+ const available = scored.filter((s) => !s.exhausted);
108
+ if (available.length > 0) {
109
+ available.sort((a, b) => a.fiveHour - b.fiveHour || a.weekly - b.weekly);
110
+ return available[0]!.id;
111
+ }
112
+
113
+ // All exhausted — fallback: pick the one with earliest reset (lowest current util)
114
+ scored.sort((a, b) => a.fiveHour - b.fiveHour || a.weekly - b.weekly);
115
+ return scored[0]!.id;
116
+ }
117
+
88
118
  /** Called when account receives 429 — apply exponential backoff */
89
119
  onRateLimit(accountId: string): void {
90
120
  const retries = (this.retryCounts.get(accountId) ?? 0) + 1;
@@ -225,11 +225,15 @@ async function fetchLegacySingleAccount(): Promise<void> {
225
225
  }
226
226
 
227
227
  async function pollOnce(): Promise<void> {
228
- const hasAccounts = accountService.list().length > 0;
229
- if (hasAccounts) {
230
- await fetchAllAccountUsages();
231
- } else {
232
- await fetchLegacySingleAccount();
228
+ try {
229
+ const hasAccounts = accountService.list().length > 0;
230
+ if (hasAccounts) {
231
+ await fetchAllAccountUsages();
232
+ } else {
233
+ await fetchLegacySingleAccount();
234
+ }
235
+ } catch (e) {
236
+ console.error("[usage] pollOnce error:", (e as Error).message);
233
237
  }
234
238
  }
235
239
 
@@ -285,12 +289,19 @@ export function getCachedUsage(): ClaudeUsage & { activeAccountId?: string; acti
285
289
 
286
290
  export function startUsagePolling(): void {
287
291
  if (pollTimer) return;
288
- pollOnce();
289
- pollTimer = setInterval(() => pollOnce(), POLL_INTERVAL);
292
+ // Use recursive setTimeout instead of setInterval to prevent overlap
293
+ // and ensure polling continues even if a single iteration errors
294
+ const scheduleNext = () => {
295
+ pollTimer = setTimeout(async () => {
296
+ await pollOnce();
297
+ scheduleNext();
298
+ }, POLL_INTERVAL);
299
+ };
300
+ pollOnce().then(scheduleNext);
290
301
  }
291
302
 
292
303
  export function stopUsagePolling(): void {
293
- if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
304
+ if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; }
294
305
  }
295
306
 
296
307
  export function updateFromSdkEvent(_rateLimitType?: string, _utilization?: number, costUsd?: number): void {
package/test-tool.mjs CHANGED
@@ -1,9 +1,10 @@
1
+ import { tmpdir } from "node:os";
1
2
  import { ClaudeAgentSdkProvider } from "./src/providers/claude-agent-sdk.ts";
2
3
 
3
4
  // Remove CLAUDECODE to avoid nested session error
4
5
  delete process.env.CLAUDECODE;
5
6
 
6
- const projectPath = process.argv[2] || "/tmp";
7
+ const projectPath = process.argv[2] || tmpdir();
7
8
  const prompt = process.argv[3] || "Run bash: echo TOOL_TEST_OK";
8
9
 
9
10
  console.log(`Testing tools with projectPath: ${projectPath}`);