@probelabs/probe 0.6.0-rc100

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.
Files changed (115) hide show
  1. package/README.md +583 -0
  2. package/bin/.gitkeep +0 -0
  3. package/bin/probe +158 -0
  4. package/bin/probe-binary +0 -0
  5. package/build/agent/ProbeAgent.d.ts +199 -0
  6. package/build/agent/ProbeAgent.js +1486 -0
  7. package/build/agent/acp/README.md +347 -0
  8. package/build/agent/acp/connection.js +237 -0
  9. package/build/agent/acp/connection.test.js +311 -0
  10. package/build/agent/acp/examples/simple-client.js +212 -0
  11. package/build/agent/acp/examples/tool-lifecycle.js +230 -0
  12. package/build/agent/acp/final-test.js +173 -0
  13. package/build/agent/acp/index.js +5 -0
  14. package/build/agent/acp/integration.test.js +385 -0
  15. package/build/agent/acp/manual-test.js +410 -0
  16. package/build/agent/acp/protocol-test.js +190 -0
  17. package/build/agent/acp/server.js +448 -0
  18. package/build/agent/acp/server.test.js +371 -0
  19. package/build/agent/acp/test-runner.js +216 -0
  20. package/build/agent/acp/test-utils/README.md +315 -0
  21. package/build/agent/acp/test-utils/acp-tester.js +484 -0
  22. package/build/agent/acp/test-utils/mock-acp-client.js +434 -0
  23. package/build/agent/acp/tools.js +368 -0
  24. package/build/agent/acp/tools.test.js +334 -0
  25. package/build/agent/acp/types.js +218 -0
  26. package/build/agent/acp/types.test.js +327 -0
  27. package/build/agent/appTracer.js +360 -0
  28. package/build/agent/fileSpanExporter.js +169 -0
  29. package/build/agent/index.js +7426 -0
  30. package/build/agent/mcp/client.js +338 -0
  31. package/build/agent/mcp/config.js +313 -0
  32. package/build/agent/mcp/index.js +64 -0
  33. package/build/agent/mcp/xmlBridge.js +371 -0
  34. package/build/agent/mockProvider.js +53 -0
  35. package/build/agent/probeTool.js +257 -0
  36. package/build/agent/schemaUtils.js +1726 -0
  37. package/build/agent/simpleTelemetry.js +267 -0
  38. package/build/agent/telemetry.js +225 -0
  39. package/build/agent/tokenCounter.js +395 -0
  40. package/build/agent/tools.js +163 -0
  41. package/build/cli.js +49 -0
  42. package/build/delegate.js +267 -0
  43. package/build/directory-resolver.js +237 -0
  44. package/build/downloader.js +750 -0
  45. package/build/extract.js +149 -0
  46. package/build/index.js +70 -0
  47. package/build/mcp/index.js +514 -0
  48. package/build/mcp/index.ts +608 -0
  49. package/build/query.js +116 -0
  50. package/build/search.js +247 -0
  51. package/build/tools/common.js +410 -0
  52. package/build/tools/index.js +40 -0
  53. package/build/tools/langchain.js +88 -0
  54. package/build/tools/system-message.js +121 -0
  55. package/build/tools/vercel.js +271 -0
  56. package/build/utils/file-lister.js +193 -0
  57. package/build/utils.js +128 -0
  58. package/cjs/agent/ProbeAgent.cjs +5829 -0
  59. package/cjs/index.cjs +6217 -0
  60. package/cjs/package.json +3 -0
  61. package/index.d.ts +401 -0
  62. package/package.json +114 -0
  63. package/scripts/postinstall.js +172 -0
  64. package/src/agent/ProbeAgent.d.ts +199 -0
  65. package/src/agent/ProbeAgent.js +1486 -0
  66. package/src/agent/acp/README.md +347 -0
  67. package/src/agent/acp/connection.js +237 -0
  68. package/src/agent/acp/connection.test.js +311 -0
  69. package/src/agent/acp/examples/simple-client.js +212 -0
  70. package/src/agent/acp/examples/tool-lifecycle.js +230 -0
  71. package/src/agent/acp/final-test.js +173 -0
  72. package/src/agent/acp/index.js +5 -0
  73. package/src/agent/acp/integration.test.js +385 -0
  74. package/src/agent/acp/manual-test.js +410 -0
  75. package/src/agent/acp/protocol-test.js +190 -0
  76. package/src/agent/acp/server.js +448 -0
  77. package/src/agent/acp/server.test.js +371 -0
  78. package/src/agent/acp/test-runner.js +216 -0
  79. package/src/agent/acp/test-utils/README.md +315 -0
  80. package/src/agent/acp/test-utils/acp-tester.js +484 -0
  81. package/src/agent/acp/test-utils/mock-acp-client.js +434 -0
  82. package/src/agent/acp/tools.js +368 -0
  83. package/src/agent/acp/tools.test.js +334 -0
  84. package/src/agent/acp/types.js +218 -0
  85. package/src/agent/acp/types.test.js +327 -0
  86. package/src/agent/appTracer.js +360 -0
  87. package/src/agent/fileSpanExporter.js +169 -0
  88. package/src/agent/index.js +813 -0
  89. package/src/agent/mcp/client.js +338 -0
  90. package/src/agent/mcp/config.js +313 -0
  91. package/src/agent/mcp/index.js +64 -0
  92. package/src/agent/mcp/xmlBridge.js +371 -0
  93. package/src/agent/mockProvider.js +53 -0
  94. package/src/agent/probeTool.js +257 -0
  95. package/src/agent/schemaUtils.js +1726 -0
  96. package/src/agent/simpleTelemetry.js +267 -0
  97. package/src/agent/telemetry.js +225 -0
  98. package/src/agent/tokenCounter.js +395 -0
  99. package/src/agent/tools.js +163 -0
  100. package/src/cli.js +49 -0
  101. package/src/delegate.js +267 -0
  102. package/src/directory-resolver.js +237 -0
  103. package/src/downloader.js +750 -0
  104. package/src/extract.js +149 -0
  105. package/src/index.js +70 -0
  106. package/src/mcp/index.ts +608 -0
  107. package/src/query.js +116 -0
  108. package/src/search.js +247 -0
  109. package/src/tools/common.js +410 -0
  110. package/src/tools/index.js +40 -0
  111. package/src/tools/langchain.js +88 -0
  112. package/src/tools/system-message.js +121 -0
  113. package/src/tools/vercel.js +271 -0
  114. package/src/utils/file-lister.js +193 -0
  115. package/src/utils.js +128 -0
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Delegate functionality for the probe package - used automatically by AI agents
3
+ * @module delegate
4
+ */
5
+
6
+ import { spawn } from 'child_process';
7
+ import { randomUUID } from 'crypto';
8
+ import { getBinaryPath, buildCliArgs } from './utils.js';
9
+ import { createMessagePreview } from './tools/common.js';
10
+
11
+ /**
12
+ * Delegate a big distinct task to a probe subagent (used automatically by AI agents)
13
+ *
14
+ * This function is designed for automatic use within the agentic loop. AI agents
15
+ * should automatically identify complex multi-part requests and break them down
16
+ * into focused, parallel tasks using this delegation mechanism.
17
+ *
18
+ * Spawns a new probe agent with a clean environment that automatically:
19
+ * - Uses the default 'code-researcher' prompt (not inherited)
20
+ * - Disables schema validation for simpler responses
21
+ * - Disables mermaid validation for faster processing
22
+ * - Limits iterations to remaining parent iterations
23
+ *
24
+ * @param {Object} options - Delegate options
25
+ * @param {string} options.task - A complete, self-contained task for the subagent. Should be specific and focused on one area of expertise.
26
+ * @param {number} [options.timeout=300] - Timeout in seconds (default: 5 minutes)
27
+ * @param {boolean} [options.debug=false] - Enable debug logging
28
+ * @param {number} [options.currentIteration=0] - Current tool iteration count from parent agent
29
+ * @param {number} [options.maxIterations=30] - Maximum tool iterations allowed
30
+ * @returns {Promise<string>} The response from the delegate agent
31
+ */
32
+ export async function delegate({ task, timeout = 300, debug = false, currentIteration = 0, maxIterations = 30, tracer = null }) {
33
+ if (!task || typeof task !== 'string') {
34
+ throw new Error('Task parameter is required and must be a string');
35
+ }
36
+
37
+ const sessionId = randomUUID();
38
+ const startTime = Date.now();
39
+
40
+ // Calculate remaining iterations for subagent
41
+ const remainingIterations = Math.max(1, maxIterations - currentIteration);
42
+
43
+ if (debug) {
44
+ console.error(`[DELEGATE] Starting delegation session ${sessionId}`);
45
+ console.error(`[DELEGATE] Task: ${task}`);
46
+ console.error(`[DELEGATE] Current iteration: ${currentIteration}/${maxIterations}`);
47
+ console.error(`[DELEGATE] Remaining iterations for subagent: ${remainingIterations}`);
48
+ console.error(`[DELEGATE] Timeout configured: ${timeout} seconds`);
49
+ console.error(`[DELEGATE] Using clean agent environment with code-researcher prompt`);
50
+ }
51
+
52
+ try {
53
+ // Get the probe binary path
54
+ const binaryPath = await getBinaryPath();
55
+
56
+ // Create the agent command with automatic subagent configuration
57
+ const args = [
58
+ 'agent',
59
+ '--task', task,
60
+ '--session-id', sessionId,
61
+ '--prompt-type', 'code-researcher', // Automatically use default code researcher prompt
62
+ '--no-schema-validation', // Automatically disable schema validation
63
+ '--no-mermaid-validation', // Automatically disable mermaid validation
64
+ '--max-iterations', remainingIterations.toString() // Automatically limit to remaining iterations
65
+ ];
66
+
67
+ if (debug) {
68
+ args.push('--debug');
69
+ console.error(`[DELEGATE] Using binary at: ${binaryPath}`);
70
+ console.error(`[DELEGATE] Command args: ${args.join(' ')}`);
71
+ }
72
+
73
+ // Spawn the delegate process
74
+ return new Promise((resolve, reject) => {
75
+ // Create delegation span for telemetry if tracer is available
76
+ const delegationSpan = tracer ? tracer.createDelegationSpan(sessionId, task) : null;
77
+
78
+ const process = spawn(binaryPath, args, {
79
+ stdio: ['pipe', 'pipe', 'pipe'],
80
+ timeout: timeout * 1000
81
+ });
82
+
83
+ let stdout = '';
84
+ let stderr = '';
85
+ let isResolved = false;
86
+
87
+ // Collect stdout
88
+ process.stdout.on('data', (data) => {
89
+ const chunk = data.toString();
90
+ stdout += chunk;
91
+
92
+ if (debug) {
93
+ const preview = createMessagePreview(chunk);
94
+ console.error(`[DELEGATE] stdout chunk received (${chunk.length} chars): ${preview}`);
95
+ }
96
+ });
97
+
98
+ // Collect stderr
99
+ process.stderr.on('data', (data) => {
100
+ const chunk = data.toString();
101
+ stderr += chunk;
102
+
103
+ if (debug) {
104
+ const preview = createMessagePreview(chunk);
105
+ console.error(`[DELEGATE] stderr chunk received (${chunk.length} chars): ${preview}`);
106
+ }
107
+ });
108
+
109
+ // Handle process completion
110
+ process.on('close', (code) => {
111
+ if (isResolved) return;
112
+ isResolved = true;
113
+
114
+ const duration = Date.now() - startTime;
115
+
116
+ if (debug) {
117
+ console.error(`[DELEGATE] Process completed with code ${code} in ${duration}ms`);
118
+ console.error(`[DELEGATE] Duration: ${(duration / 1000).toFixed(2)}s`);
119
+ console.error(`[DELEGATE] Total stdout: ${stdout.length} chars`);
120
+ console.error(`[DELEGATE] Total stderr: ${stderr.length} chars`);
121
+ }
122
+
123
+ if (code === 0) {
124
+ // Successful delegation - return the response
125
+ const response = stdout.trim();
126
+
127
+ if (!response) {
128
+ if (debug) {
129
+ console.error(`[DELEGATE] Task completed but returned empty response for session ${sessionId}`);
130
+ }
131
+ reject(new Error('Delegate agent returned empty response'));
132
+ return;
133
+ }
134
+
135
+ if (debug) {
136
+ console.error(`[DELEGATE] Task completed successfully for session ${sessionId}`);
137
+ console.error(`[DELEGATE] Response length: ${response.length} chars`);
138
+ }
139
+
140
+ // Record successful completion in telemetry
141
+ if (tracer) {
142
+ tracer.recordDelegationEvent('completed', {
143
+ 'delegation.session_id': sessionId,
144
+ 'delegation.duration_ms': duration,
145
+ 'delegation.response_length': response.length,
146
+ 'delegation.success': true
147
+ });
148
+
149
+ if (delegationSpan) {
150
+ delegationSpan.setAttributes({
151
+ 'delegation.result.success': true,
152
+ 'delegation.result.response_length': response.length,
153
+ 'delegation.result.duration_ms': duration
154
+ });
155
+ delegationSpan.setStatus({ code: 1 }); // OK
156
+ delegationSpan.end();
157
+ }
158
+ }
159
+
160
+ resolve(response);
161
+ } else {
162
+ // Failed delegation
163
+ const errorMessage = stderr.trim() || `Delegate process failed with exit code ${code}`;
164
+ if (debug) {
165
+ console.error(`[DELEGATE] Task failed for session ${sessionId} with code ${code}`);
166
+ console.error(`[DELEGATE] Error message: ${errorMessage}`);
167
+ }
168
+
169
+ // Record failure in telemetry
170
+ if (tracer) {
171
+ tracer.recordDelegationEvent('failed', {
172
+ 'delegation.session_id': sessionId,
173
+ 'delegation.duration_ms': duration,
174
+ 'delegation.exit_code': code,
175
+ 'delegation.error_message': errorMessage,
176
+ 'delegation.success': false
177
+ });
178
+
179
+ if (delegationSpan) {
180
+ delegationSpan.setAttributes({
181
+ 'delegation.result.success': false,
182
+ 'delegation.result.exit_code': code,
183
+ 'delegation.result.error': errorMessage,
184
+ 'delegation.result.duration_ms': duration
185
+ });
186
+ delegationSpan.setStatus({ code: 2, message: errorMessage }); // ERROR
187
+ delegationSpan.end();
188
+ }
189
+ }
190
+
191
+ reject(new Error(`Delegation failed: ${errorMessage}`));
192
+ }
193
+ });
194
+
195
+ // Handle process errors
196
+ process.on('error', (error) => {
197
+ if (isResolved) return;
198
+ isResolved = true;
199
+
200
+ const duration = Date.now() - startTime;
201
+
202
+ if (debug) {
203
+ console.error(`[DELEGATE] Process spawn error after ${duration}ms:`, error);
204
+ console.error(`[DELEGATE] Session ${sessionId} failed during process creation`);
205
+ console.error(`[DELEGATE] Error type: ${error.code || 'unknown'}`);
206
+ }
207
+
208
+ reject(new Error(`Failed to start delegate process: ${error.message}`));
209
+ });
210
+
211
+ // Handle timeout
212
+ setTimeout(() => {
213
+ if (isResolved) return;
214
+ isResolved = true;
215
+
216
+ const duration = Date.now() - startTime;
217
+
218
+ if (debug) {
219
+ console.error(`[DELEGATE] Process timeout after ${(duration / 1000).toFixed(2)}s (limit: ${timeout}s)`);
220
+ console.error(`[DELEGATE] Terminating session ${sessionId} due to timeout`);
221
+ console.error(`[DELEGATE] Partial stdout: ${stdout.substring(0, 500)}${stdout.length > 500 ? '...' : ''}`);
222
+ console.error(`[DELEGATE] Partial stderr: ${stderr.substring(0, 500)}${stderr.length > 500 ? '...' : ''}`);
223
+ }
224
+
225
+ // Kill the process
226
+ process.kill('SIGTERM');
227
+
228
+ // Give it a moment to terminate gracefully
229
+ setTimeout(() => {
230
+ if (!process.killed) {
231
+ if (debug) {
232
+ console.error(`[DELEGATE] Force killing process ${sessionId} after graceful timeout`);
233
+ }
234
+ process.kill('SIGKILL');
235
+ }
236
+ }, 5000);
237
+
238
+ reject(new Error(`Delegation timed out after ${timeout} seconds`));
239
+ }, timeout * 1000);
240
+ });
241
+
242
+ } catch (error) {
243
+ const duration = Date.now() - startTime;
244
+
245
+ if (debug) {
246
+ console.error(`[DELEGATE] Error in delegate function after ${duration}ms:`, error);
247
+ console.error(`[DELEGATE] Session ${sessionId} failed during setup`);
248
+ console.error(`[DELEGATE] Error stack: ${error.stack}`);
249
+ }
250
+ throw new Error(`Delegation setup failed: ${error.message}`);
251
+ }
252
+ }
253
+
254
+
255
+ /**
256
+ * Check if delegate functionality is available
257
+ *
258
+ * @returns {Promise<boolean>} True if delegate is available
259
+ */
260
+ export async function isDelegateAvailable() {
261
+ try {
262
+ const binaryPath = await getBinaryPath();
263
+ return !!binaryPath;
264
+ } catch (error) {
265
+ return false;
266
+ }
267
+ }
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Directory resolver for probe binary storage
3
+ * Handles reliable directory resolution across different environments (CI, npx, Docker, etc.)
4
+ * @module directory-resolver
5
+ */
6
+
7
+ import path from 'path';
8
+ import os from 'os';
9
+ import fs from 'fs-extra';
10
+ import { fileURLToPath } from 'url';
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+
15
+ /**
16
+ * Get a writable directory for storing the probe binary
17
+ * Tries multiple strategies in order of preference
18
+ * @returns {Promise<string>} Path to writable bin directory
19
+ */
20
+ export async function getPackageBinDir() {
21
+ const debug = process.env.DEBUG === '1' || process.env.VERBOSE === '1';
22
+
23
+ if (debug) {
24
+ console.log('DEBUG: Starting probe binary directory resolution');
25
+ }
26
+
27
+ // Strategy 1: Check for explicit binary path override
28
+ if (process.env.PROBE_BINARY_PATH) {
29
+ if (debug) {
30
+ console.log(`DEBUG: Checking PROBE_BINARY_PATH: ${process.env.PROBE_BINARY_PATH}`);
31
+ }
32
+
33
+ const binaryPath = process.env.PROBE_BINARY_PATH;
34
+ if (await fs.pathExists(binaryPath)) {
35
+ const binDir = path.dirname(binaryPath);
36
+ if (await canWriteToDirectory(binDir)) {
37
+ if (debug) {
38
+ console.log(`DEBUG: Using PROBE_BINARY_PATH directory: ${binDir}`);
39
+ }
40
+ return binDir;
41
+ }
42
+ } else {
43
+ console.warn(`Warning: PROBE_BINARY_PATH ${binaryPath} does not exist`);
44
+ }
45
+ }
46
+
47
+ // Strategy 2: Check for cache directory override
48
+ if (process.env.PROBE_CACHE_DIR) {
49
+ if (debug) {
50
+ console.log(`DEBUG: Checking PROBE_CACHE_DIR: ${process.env.PROBE_CACHE_DIR}`);
51
+ }
52
+
53
+ const cacheDir = path.join(process.env.PROBE_CACHE_DIR, 'bin');
54
+ if (await ensureDirectory(cacheDir)) {
55
+ if (debug) {
56
+ console.log(`DEBUG: Using PROBE_CACHE_DIR: ${cacheDir}`);
57
+ }
58
+ return cacheDir;
59
+ }
60
+ }
61
+
62
+ // Strategy 3: Try to find package root and use its bin directory
63
+ const packageRoot = await findPackageRoot();
64
+ if (packageRoot) {
65
+ if (debug) {
66
+ console.log(`DEBUG: Found package root: ${packageRoot}`);
67
+ }
68
+
69
+ const packageBinDir = path.join(packageRoot, 'bin');
70
+ if (await ensureDirectory(packageBinDir) && await canWriteToDirectory(packageBinDir)) {
71
+ if (debug) {
72
+ console.log(`DEBUG: Using package bin directory: ${packageBinDir}`);
73
+ }
74
+ return packageBinDir;
75
+ } else if (debug) {
76
+ console.log(`DEBUG: Package bin directory ${packageBinDir} not writable, trying fallbacks`);
77
+ }
78
+ }
79
+
80
+ // Strategy 4: Use user home directory cache (like Puppeteer)
81
+ const homeCache = path.join(os.homedir(), '.probe', 'bin');
82
+ if (debug) {
83
+ console.log(`DEBUG: Trying home cache directory: ${homeCache}`);
84
+ }
85
+
86
+ if (await ensureDirectory(homeCache)) {
87
+ if (debug) {
88
+ console.log(`DEBUG: Using home cache directory: ${homeCache}`);
89
+ }
90
+ return homeCache;
91
+ }
92
+
93
+ // Strategy 5: Use temp directory as last resort
94
+ const tempCache = path.join(os.tmpdir(), 'probe-cache', 'bin');
95
+ if (debug) {
96
+ console.log(`DEBUG: Trying temp cache directory: ${tempCache}`);
97
+ }
98
+
99
+ if (await ensureDirectory(tempCache)) {
100
+ if (debug) {
101
+ console.log(`DEBUG: Using temp cache directory: ${tempCache}`);
102
+ }
103
+ return tempCache;
104
+ }
105
+
106
+ // If all strategies fail, throw a helpful error
107
+ const errorMessage = [
108
+ 'Could not find a writable directory for probe binary.',
109
+ 'Tried the following locations:',
110
+ packageRoot ? `- Package bin directory: ${path.join(packageRoot, 'bin')}` : '- Package root not found',
111
+ `- Home cache directory: ${homeCache}`,
112
+ `- Temp cache directory: ${tempCache}`,
113
+ '',
114
+ 'You can override the location using environment variables:',
115
+ '- PROBE_BINARY_PATH=/path/to/probe (direct path to binary)',
116
+ '- PROBE_CACHE_DIR=/path/to/cache (cache directory, binary will be in /bin subdirectory)'
117
+ ].join('\n');
118
+
119
+ throw new Error(errorMessage);
120
+ }
121
+
122
+ /**
123
+ * Find the package root directory by looking for package.json with the correct name
124
+ * @returns {Promise<string|null>} Path to package root or null if not found
125
+ */
126
+ async function findPackageRoot() {
127
+ const debug = process.env.DEBUG === '1' || process.env.VERBOSE === '1';
128
+
129
+ // Start from current module directory
130
+ let currentDir = __dirname;
131
+ const rootDir = path.parse(currentDir).root;
132
+
133
+ if (debug) {
134
+ console.log(`DEBUG: Starting package root search from: ${currentDir}`);
135
+ }
136
+
137
+ // Walk up until we find package.json with our package name
138
+ while (currentDir !== rootDir) {
139
+ const packageJsonPath = path.join(currentDir, 'package.json');
140
+
141
+ try {
142
+ if (await fs.pathExists(packageJsonPath)) {
143
+ const packageJson = await fs.readJson(packageJsonPath);
144
+
145
+ if (debug) {
146
+ console.log(`DEBUG: Found package.json at ${packageJsonPath}, name: ${packageJson.name}`);
147
+ }
148
+
149
+ // Check if this is our package
150
+ if (packageJson.name === '@probelabs/probe') {
151
+ if (debug) {
152
+ console.log(`DEBUG: Found probe package root: ${currentDir}`);
153
+ }
154
+ return currentDir;
155
+ }
156
+ }
157
+ } catch (err) {
158
+ if (debug) {
159
+ console.log(`DEBUG: Error reading package.json at ${packageJsonPath}: ${err.message}`);
160
+ }
161
+ // Continue searching
162
+ }
163
+
164
+ currentDir = path.dirname(currentDir);
165
+ }
166
+
167
+ if (debug) {
168
+ console.log('DEBUG: Package root not found, reached filesystem root');
169
+ }
170
+
171
+ return null;
172
+ }
173
+
174
+ /**
175
+ * Ensure a directory exists and is writable
176
+ * @param {string} dirPath - Path to directory
177
+ * @returns {Promise<boolean>} True if directory exists and is writable
178
+ */
179
+ async function ensureDirectory(dirPath) {
180
+ const debug = process.env.DEBUG === '1' || process.env.VERBOSE === '1';
181
+
182
+ try {
183
+ await fs.ensureDir(dirPath);
184
+
185
+ // Test write permissions by creating a temporary file
186
+ const testFile = path.join(dirPath, '.probe-write-test');
187
+ await fs.writeFile(testFile, 'test');
188
+ await fs.remove(testFile);
189
+
190
+ if (debug) {
191
+ console.log(`DEBUG: Directory ${dirPath} is writable`);
192
+ }
193
+
194
+ return true;
195
+ } catch (error) {
196
+ if (debug) {
197
+ console.log(`DEBUG: Directory ${dirPath} not writable: ${error.message}`);
198
+ }
199
+ return false;
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Check if a directory is writable
205
+ * @param {string} dirPath - Path to directory
206
+ * @returns {Promise<boolean>} True if directory is writable
207
+ */
208
+ async function canWriteToDirectory(dirPath) {
209
+ const debug = process.env.DEBUG === '1' || process.env.VERBOSE === '1';
210
+
211
+ try {
212
+ // Check if directory exists
213
+ const exists = await fs.pathExists(dirPath);
214
+ if (!exists) {
215
+ if (debug) {
216
+ console.log(`DEBUG: Directory ${dirPath} does not exist`);
217
+ }
218
+ return false;
219
+ }
220
+
221
+ // Test write permissions
222
+ const testFile = path.join(dirPath, '.probe-write-test');
223
+ await fs.writeFile(testFile, 'test');
224
+ await fs.remove(testFile);
225
+
226
+ if (debug) {
227
+ console.log(`DEBUG: Directory ${dirPath} is writable`);
228
+ }
229
+
230
+ return true;
231
+ } catch (error) {
232
+ if (debug) {
233
+ console.log(`DEBUG: Directory ${dirPath} not writable: ${error.message}`);
234
+ }
235
+ return false;
236
+ }
237
+ }