@probelabs/probe 0.6.0-rc159 → 0.6.0-rc161
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/README.md +80 -1
- package/build/agent/FallbackManager.d.ts +176 -0
- package/build/agent/FallbackManager.js +545 -0
- package/build/agent/ProbeAgent.d.ts +7 -1
- package/build/agent/ProbeAgent.js +199 -7
- package/build/agent/RetryManager.d.ts +157 -0
- package/build/agent/RetryManager.js +334 -0
- package/build/agent/acp/tools.js +6 -2
- package/build/agent/index.js +1379 -168
- package/build/agent/probeTool.js +20 -2
- package/build/agent/tools.js +16 -0
- package/build/index.js +13 -0
- package/build/tools/common.js +5 -3
- package/build/tools/edit.js +409 -0
- package/build/tools/index.js +11 -0
- package/cjs/agent/ProbeAgent.cjs +1822 -497
- package/cjs/index.cjs +1784 -454
- package/package.json +2 -2
- package/src/agent/FallbackManager.d.ts +176 -0
- package/src/agent/FallbackManager.js +545 -0
- package/src/agent/ProbeAgent.d.ts +7 -1
- package/src/agent/ProbeAgent.js +199 -7
- package/src/agent/RetryManager.d.ts +157 -0
- package/src/agent/RetryManager.js +334 -0
- package/src/agent/acp/tools.js +6 -2
- package/src/agent/probeTool.js +20 -2
- package/src/agent/tools.js +16 -0
- package/src/index.js +13 -0
- package/src/tools/common.js +5 -3
- package/src/tools/edit.js +409 -0
- package/src/tools/index.js +11 -0
- package/bin/binaries/probe-v0.6.0-rc159-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc159-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc159-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc159-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc159-x86_64-unknown-linux-musl.tar.gz +0 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edit and Create tools for file modification
|
|
3
|
+
* @module tools/edit
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { tool } from 'ai';
|
|
7
|
+
import { promises as fs } from 'fs';
|
|
8
|
+
import { dirname, resolve, isAbsolute, sep } from 'path';
|
|
9
|
+
import { existsSync } from 'fs';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Validates that a path is within allowed directories
|
|
13
|
+
* @param {string} filePath - Path to validate
|
|
14
|
+
* @param {string[]} allowedFolders - List of allowed folders
|
|
15
|
+
* @returns {boolean} True if path is allowed
|
|
16
|
+
*/
|
|
17
|
+
function isPathAllowed(filePath, allowedFolders) {
|
|
18
|
+
if (!allowedFolders || allowedFolders.length === 0) {
|
|
19
|
+
// If no restrictions, allow current directory and below
|
|
20
|
+
const resolvedPath = resolve(filePath);
|
|
21
|
+
const cwd = resolve(process.cwd());
|
|
22
|
+
// Ensure proper path separator to prevent path traversal
|
|
23
|
+
return resolvedPath === cwd || resolvedPath.startsWith(cwd + sep);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const resolvedPath = resolve(filePath);
|
|
27
|
+
return allowedFolders.some(folder => {
|
|
28
|
+
const allowedPath = resolve(folder);
|
|
29
|
+
// Ensure proper path separator to prevent path traversal
|
|
30
|
+
return resolvedPath === allowedPath || resolvedPath.startsWith(allowedPath + sep);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Common configuration for file tools
|
|
36
|
+
* @param {Object} options - Configuration options
|
|
37
|
+
* @returns {Object} Parsed configuration
|
|
38
|
+
*/
|
|
39
|
+
function parseFileToolOptions(options = {}) {
|
|
40
|
+
return {
|
|
41
|
+
debug: options.debug || false,
|
|
42
|
+
allowedFolders: options.allowedFolders || [],
|
|
43
|
+
defaultPath: options.defaultPath
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Edit tool generator - Claude Code style string replacement
|
|
49
|
+
*
|
|
50
|
+
* @param {Object} [options] - Configuration options
|
|
51
|
+
* @param {boolean} [options.debug=false] - Enable debug logging
|
|
52
|
+
* @param {string[]} [options.allowedFolders] - Allowed directories for file operations
|
|
53
|
+
* @param {string} [options.defaultPath] - Default working directory
|
|
54
|
+
* @returns {Object} Configured edit tool
|
|
55
|
+
*/
|
|
56
|
+
export const editTool = (options = {}) => {
|
|
57
|
+
const { debug, allowedFolders, defaultPath } = parseFileToolOptions(options);
|
|
58
|
+
|
|
59
|
+
return tool({
|
|
60
|
+
name: 'edit',
|
|
61
|
+
description: `Edit files using exact string replacement (Claude Code style).
|
|
62
|
+
|
|
63
|
+
This tool performs exact string replacements in files. It requires the old_string to match exactly what's in the file, including all whitespace and indentation.
|
|
64
|
+
|
|
65
|
+
Parameters:
|
|
66
|
+
- file_path: Path to the file to edit (absolute or relative)
|
|
67
|
+
- old_string: Exact text to find and replace (must be unique in the file unless replace_all is true)
|
|
68
|
+
- new_string: Text to replace with
|
|
69
|
+
- replace_all: (optional) Replace all occurrences instead of requiring uniqueness
|
|
70
|
+
|
|
71
|
+
Important:
|
|
72
|
+
- The old_string must match EXACTLY including whitespace
|
|
73
|
+
- If old_string appears multiple times and replace_all is false, the edit will fail
|
|
74
|
+
- Use larger context around the string to ensure uniqueness when needed`,
|
|
75
|
+
|
|
76
|
+
inputSchema: {
|
|
77
|
+
type: 'object',
|
|
78
|
+
properties: {
|
|
79
|
+
file_path: {
|
|
80
|
+
type: 'string',
|
|
81
|
+
description: 'Path to the file to edit'
|
|
82
|
+
},
|
|
83
|
+
old_string: {
|
|
84
|
+
type: 'string',
|
|
85
|
+
description: 'Exact text to find and replace'
|
|
86
|
+
},
|
|
87
|
+
new_string: {
|
|
88
|
+
type: 'string',
|
|
89
|
+
description: 'Text to replace with'
|
|
90
|
+
},
|
|
91
|
+
replace_all: {
|
|
92
|
+
type: 'boolean',
|
|
93
|
+
description: 'Replace all occurrences (default: false)',
|
|
94
|
+
default: false
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
required: ['file_path', 'old_string', 'new_string']
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
execute: async ({ file_path, old_string, new_string, replace_all = false }) => {
|
|
101
|
+
try {
|
|
102
|
+
// Validate input parameters
|
|
103
|
+
if (!file_path || typeof file_path !== 'string' || file_path.trim() === '') {
|
|
104
|
+
return `Error editing file: Invalid file_path - must be a non-empty string`;
|
|
105
|
+
}
|
|
106
|
+
if (old_string === undefined || old_string === null || typeof old_string !== 'string') {
|
|
107
|
+
return `Error editing file: Invalid old_string - must be a string`;
|
|
108
|
+
}
|
|
109
|
+
if (new_string === undefined || new_string === null || typeof new_string !== 'string') {
|
|
110
|
+
return `Error editing file: Invalid new_string - must be a string`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Resolve the file path
|
|
114
|
+
const resolvedPath = isAbsolute(file_path) ? file_path : resolve(defaultPath || process.cwd(), file_path);
|
|
115
|
+
|
|
116
|
+
if (debug) {
|
|
117
|
+
console.error(`[Edit] Attempting to edit file: ${resolvedPath}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Check if path is allowed
|
|
121
|
+
if (!isPathAllowed(resolvedPath, allowedFolders)) {
|
|
122
|
+
return `Error editing file: Permission denied - ${file_path} is outside allowed directories`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Check if file exists
|
|
126
|
+
if (!existsSync(resolvedPath)) {
|
|
127
|
+
return `Error editing file: File not found - ${file_path}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Read the file
|
|
131
|
+
const content = await fs.readFile(resolvedPath, 'utf-8');
|
|
132
|
+
|
|
133
|
+
// Check if old_string exists in the file
|
|
134
|
+
if (!content.includes(old_string)) {
|
|
135
|
+
return `Error editing file: String not found - the specified old_string was not found in ${file_path}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Count occurrences
|
|
139
|
+
const occurrences = content.split(old_string).length - 1;
|
|
140
|
+
|
|
141
|
+
// Check uniqueness if not replacing all
|
|
142
|
+
if (!replace_all && occurrences > 1) {
|
|
143
|
+
return `Error editing file: Multiple occurrences found - the old_string appears ${occurrences} times. Use replace_all: true to replace all occurrences, or provide more context to make the string unique.`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Perform the replacement
|
|
147
|
+
let newContent;
|
|
148
|
+
if (replace_all) {
|
|
149
|
+
newContent = content.replaceAll(old_string, new_string);
|
|
150
|
+
} else {
|
|
151
|
+
newContent = content.replace(old_string, new_string);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Check if replacement was made
|
|
155
|
+
if (newContent === content) {
|
|
156
|
+
return `Error editing file: No changes made - old_string and new_string might be the same`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Write the file back
|
|
160
|
+
await fs.writeFile(resolvedPath, newContent, 'utf-8');
|
|
161
|
+
|
|
162
|
+
const replacedCount = replace_all ? occurrences : 1;
|
|
163
|
+
|
|
164
|
+
if (debug) {
|
|
165
|
+
console.error(`[Edit] Successfully edited ${resolvedPath}, replaced ${replacedCount} occurrence(s)`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Return success message as a string (matching other tools pattern)
|
|
169
|
+
return `Successfully edited ${file_path} (${replacedCount} replacement${replacedCount !== 1 ? 's' : ''})`;
|
|
170
|
+
|
|
171
|
+
} catch (error) {
|
|
172
|
+
console.error('[Edit] Error:', error);
|
|
173
|
+
return `Error editing file: ${error.message}`;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Create tool generator - Create new files
|
|
181
|
+
*
|
|
182
|
+
* @param {Object} [options] - Configuration options
|
|
183
|
+
* @param {boolean} [options.debug=false] - Enable debug logging
|
|
184
|
+
* @param {string[]} [options.allowedFolders] - Allowed directories for file operations
|
|
185
|
+
* @param {string} [options.defaultPath] - Default working directory
|
|
186
|
+
* @returns {Object} Configured create tool
|
|
187
|
+
*/
|
|
188
|
+
export const createTool = (options = {}) => {
|
|
189
|
+
const { debug, allowedFolders, defaultPath } = parseFileToolOptions(options);
|
|
190
|
+
|
|
191
|
+
return tool({
|
|
192
|
+
name: 'create',
|
|
193
|
+
description: `Create new files with specified content.
|
|
194
|
+
|
|
195
|
+
This tool creates new files in the filesystem. It will create parent directories if they don't exist.
|
|
196
|
+
|
|
197
|
+
Parameters:
|
|
198
|
+
- file_path: Path where the file should be created (absolute or relative)
|
|
199
|
+
- content: Content to write to the file
|
|
200
|
+
- overwrite: (optional) Whether to overwrite if file exists (default: false)
|
|
201
|
+
|
|
202
|
+
Important:
|
|
203
|
+
- By default, will fail if the file already exists
|
|
204
|
+
- Set overwrite: true to replace existing files
|
|
205
|
+
- Parent directories will be created automatically if needed`,
|
|
206
|
+
|
|
207
|
+
inputSchema: {
|
|
208
|
+
type: 'object',
|
|
209
|
+
properties: {
|
|
210
|
+
file_path: {
|
|
211
|
+
type: 'string',
|
|
212
|
+
description: 'Path where the file should be created'
|
|
213
|
+
},
|
|
214
|
+
content: {
|
|
215
|
+
type: 'string',
|
|
216
|
+
description: 'Content to write to the file'
|
|
217
|
+
},
|
|
218
|
+
overwrite: {
|
|
219
|
+
type: 'boolean',
|
|
220
|
+
description: 'Overwrite if file exists (default: false)',
|
|
221
|
+
default: false
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
required: ['file_path', 'content']
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
execute: async ({ file_path, content, overwrite = false }) => {
|
|
228
|
+
try {
|
|
229
|
+
// Validate input parameters
|
|
230
|
+
if (!file_path || typeof file_path !== 'string' || file_path.trim() === '') {
|
|
231
|
+
return `Error creating file: Invalid file_path - must be a non-empty string`;
|
|
232
|
+
}
|
|
233
|
+
if (content === undefined || content === null || typeof content !== 'string') {
|
|
234
|
+
return `Error creating file: Invalid content - must be a string`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Resolve the file path
|
|
238
|
+
const resolvedPath = isAbsolute(file_path) ? file_path : resolve(defaultPath || process.cwd(), file_path);
|
|
239
|
+
|
|
240
|
+
if (debug) {
|
|
241
|
+
console.error(`[Create] Attempting to create file: ${resolvedPath}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Check if path is allowed
|
|
245
|
+
if (!isPathAllowed(resolvedPath, allowedFolders)) {
|
|
246
|
+
return `Error creating file: Permission denied - ${file_path} is outside allowed directories`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Check if file exists
|
|
250
|
+
if (existsSync(resolvedPath) && !overwrite) {
|
|
251
|
+
return `Error creating file: File already exists - ${file_path}. Use overwrite: true to replace it.`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Ensure parent directory exists
|
|
255
|
+
const dir = dirname(resolvedPath);
|
|
256
|
+
await fs.mkdir(dir, { recursive: true });
|
|
257
|
+
|
|
258
|
+
// Write the file
|
|
259
|
+
await fs.writeFile(resolvedPath, content, 'utf-8');
|
|
260
|
+
|
|
261
|
+
const action = existsSync(resolvedPath) && overwrite ? 'overwrote' : 'created';
|
|
262
|
+
const bytes = Buffer.byteLength(content, 'utf-8');
|
|
263
|
+
|
|
264
|
+
if (debug) {
|
|
265
|
+
console.error(`[Create] Successfully ${action} ${resolvedPath}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Return success message as a string (matching other tools pattern)
|
|
269
|
+
return `Successfully ${action} ${file_path} (${bytes} bytes)`;
|
|
270
|
+
|
|
271
|
+
} catch (error) {
|
|
272
|
+
console.error('[Create] Error:', error);
|
|
273
|
+
return `Error creating file: ${error.message}`;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// Export schemas for tool definitions
|
|
280
|
+
export const editSchema = {
|
|
281
|
+
type: 'object',
|
|
282
|
+
properties: {
|
|
283
|
+
file_path: {
|
|
284
|
+
type: 'string',
|
|
285
|
+
description: 'Path to the file to edit'
|
|
286
|
+
},
|
|
287
|
+
old_string: {
|
|
288
|
+
type: 'string',
|
|
289
|
+
description: 'Exact text to find and replace'
|
|
290
|
+
},
|
|
291
|
+
new_string: {
|
|
292
|
+
type: 'string',
|
|
293
|
+
description: 'Text to replace with'
|
|
294
|
+
},
|
|
295
|
+
replace_all: {
|
|
296
|
+
type: 'boolean',
|
|
297
|
+
description: 'Replace all occurrences (default: false)'
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
required: ['file_path', 'old_string', 'new_string']
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
export const createSchema = {
|
|
304
|
+
type: 'object',
|
|
305
|
+
properties: {
|
|
306
|
+
file_path: {
|
|
307
|
+
type: 'string',
|
|
308
|
+
description: 'Path where the file should be created'
|
|
309
|
+
},
|
|
310
|
+
content: {
|
|
311
|
+
type: 'string',
|
|
312
|
+
description: 'Content to write to the file'
|
|
313
|
+
},
|
|
314
|
+
overwrite: {
|
|
315
|
+
type: 'boolean',
|
|
316
|
+
description: 'Overwrite if file exists (default: false)'
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
required: ['file_path', 'content']
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// Tool descriptions for XML definitions
|
|
323
|
+
export const editDescription = 'Edit files using exact string replacement. Requires exact match including whitespace.';
|
|
324
|
+
export const createDescription = 'Create new files with specified content. Will create parent directories if needed.';
|
|
325
|
+
|
|
326
|
+
// XML tool definitions
|
|
327
|
+
export const editToolDefinition = `
|
|
328
|
+
## edit
|
|
329
|
+
Description: ${editDescription}
|
|
330
|
+
|
|
331
|
+
When to use:
|
|
332
|
+
- For precise, surgical edits to existing files
|
|
333
|
+
- When you need to change specific lines or blocks of code
|
|
334
|
+
- For renaming functions, variables, or updating configuration values
|
|
335
|
+
- When the exact text to replace is known and unique (or use replace_all for multiple occurrences)
|
|
336
|
+
|
|
337
|
+
When NOT to use:
|
|
338
|
+
- For creating new files (use 'create' tool instead)
|
|
339
|
+
- When you cannot determine the exact text to replace
|
|
340
|
+
- When changes span multiple locations that would be better handled together
|
|
341
|
+
|
|
342
|
+
Parameters:
|
|
343
|
+
- file_path: (required) Path to the file to edit
|
|
344
|
+
- old_string: (required) Exact text to find and replace (must match including whitespace, newlines, and indentation)
|
|
345
|
+
- new_string: (required) Text to replace with
|
|
346
|
+
- replace_all: (optional, default: false) Replace all occurrences if the string appears multiple times
|
|
347
|
+
|
|
348
|
+
Important notes:
|
|
349
|
+
- The old_string MUST match EXACTLY, including all whitespace, indentation, and line breaks
|
|
350
|
+
- If old_string appears multiple times and replace_all is false, the tool will fail
|
|
351
|
+
- Always verify the exact formatting of the text you want to replace
|
|
352
|
+
|
|
353
|
+
Examples:
|
|
354
|
+
<edit>
|
|
355
|
+
<file_path>src/main.js</file_path>
|
|
356
|
+
<old_string>function oldName() {
|
|
357
|
+
return 42;
|
|
358
|
+
}</old_string>
|
|
359
|
+
<new_string>function newName() {
|
|
360
|
+
return 42;
|
|
361
|
+
}</new_string>
|
|
362
|
+
</edit>
|
|
363
|
+
|
|
364
|
+
<edit>
|
|
365
|
+
<file_path>config.json</file_path>
|
|
366
|
+
<old_string>"debug": false</old_string>
|
|
367
|
+
<new_string>"debug": true</new_string>
|
|
368
|
+
<replace_all>true</replace_all>
|
|
369
|
+
</edit>`;
|
|
370
|
+
|
|
371
|
+
export const createToolDefinition = `
|
|
372
|
+
## create
|
|
373
|
+
Description: ${createDescription}
|
|
374
|
+
|
|
375
|
+
When to use:
|
|
376
|
+
- For creating brand new files from scratch
|
|
377
|
+
- When you need to add configuration files, documentation, or new modules
|
|
378
|
+
- For generating boilerplate code or templates
|
|
379
|
+
- When you have the complete content ready to write
|
|
380
|
+
|
|
381
|
+
When NOT to use:
|
|
382
|
+
- For editing existing files (use 'edit' tool instead)
|
|
383
|
+
- When a file already exists unless you explicitly want to overwrite it
|
|
384
|
+
|
|
385
|
+
Parameters:
|
|
386
|
+
- file_path: (required) Path where the file should be created
|
|
387
|
+
- content: (required) Complete content to write to the file
|
|
388
|
+
- overwrite: (optional, default: false) Whether to overwrite if file already exists
|
|
389
|
+
|
|
390
|
+
Important notes:
|
|
391
|
+
- Parent directories will be created automatically if they don't exist
|
|
392
|
+
- The tool will fail if the file already exists and overwrite is false
|
|
393
|
+
- Be careful with the overwrite option as it completely replaces existing files
|
|
394
|
+
|
|
395
|
+
Examples:
|
|
396
|
+
<create>
|
|
397
|
+
<file_path>src/newFile.js</file_path>
|
|
398
|
+
<content>export function hello() {
|
|
399
|
+
return "Hello, world!";
|
|
400
|
+
}</content>
|
|
401
|
+
</create>
|
|
402
|
+
|
|
403
|
+
<create>
|
|
404
|
+
<file_path>README.md</file_path>
|
|
405
|
+
<content># My Project
|
|
406
|
+
|
|
407
|
+
This is a new project.</content>
|
|
408
|
+
<overwrite>true</overwrite>
|
|
409
|
+
</create>`;
|
package/src/tools/index.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// Export Vercel AI SDK tool generators
|
|
7
7
|
export { searchTool, queryTool, extractTool, delegateTool } from './vercel.js';
|
|
8
8
|
export { bashTool } from './bash.js';
|
|
9
|
+
export { editTool, createTool } from './edit.js';
|
|
9
10
|
|
|
10
11
|
// Export LangChain tools
|
|
11
12
|
export { createSearchTool, createQueryTool, createExtractTool } from './langchain.js';
|
|
@@ -25,6 +26,16 @@ export {
|
|
|
25
26
|
attemptCompletionToolDefinition
|
|
26
27
|
} from './common.js';
|
|
27
28
|
|
|
29
|
+
// Export edit and create schemas
|
|
30
|
+
export {
|
|
31
|
+
editSchema,
|
|
32
|
+
createSchema,
|
|
33
|
+
editDescription,
|
|
34
|
+
createDescription,
|
|
35
|
+
editToolDefinition,
|
|
36
|
+
createToolDefinition
|
|
37
|
+
} from './edit.js';
|
|
38
|
+
|
|
28
39
|
// Export system message
|
|
29
40
|
export { DEFAULT_SYSTEM_MESSAGE } from './system-message.js';
|
|
30
41
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|