@wonderwhy-er/desktop-commander 0.2.2 → 0.2.3
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 +2 -1
- package/dist/handlers/filesystem-handlers.js +2 -4
- package/dist/server.js +40 -25
- package/dist/tools/edit.js +3 -3
- package/dist/tools/filesystem.d.ts +10 -0
- package/dist/tools/filesystem.js +290 -35
- package/dist/tools/improved-process-tools.d.ts +24 -0
- package/dist/tools/improved-process-tools.js +312 -0
- package/dist/tools/schemas.js +0 -1
- package/dist/utils/capture.d.ts +2 -0
- package/dist/utils/capture.js +17 -8
- package/dist/utils/process-detection.d.ts +23 -0
- package/dist/utils/process-detection.js +150 -0
- package/dist/utils/system-info.d.ts +30 -0
- package/dist/utils/system-info.js +146 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -52,6 +52,7 @@ Execute long-running terminal commands on your computer and manage processes thr
|
|
|
52
52
|
- Move files/directories
|
|
53
53
|
- Search files
|
|
54
54
|
- Get file metadata
|
|
55
|
+
- **Negative offset file reading**: Read from end of files using negative offset values (like Unix tail)
|
|
55
56
|
- Code editing capabilities:
|
|
56
57
|
- Surgical text replacements for small changes
|
|
57
58
|
- Full file rewrites for major changes
|
|
@@ -187,7 +188,7 @@ The server provides a comprehensive set of tools organized into several categori
|
|
|
187
188
|
| | `list_sessions` | List all active terminal sessions |
|
|
188
189
|
| | `list_processes` | List all running processes with detailed information |
|
|
189
190
|
| | `kill_process` | Terminate a running process by PID |
|
|
190
|
-
| **Filesystem** | `read_file` | Read contents from local filesystem or URLs with line-based pagination (supports offset and length parameters) |
|
|
191
|
+
| **Filesystem** | `read_file` | Read contents from local filesystem or URLs with line-based pagination (supports positive/negative offset and length parameters) |
|
|
191
192
|
| | `read_multiple_files` | Read multiple files simultaneously |
|
|
192
193
|
| | `write_file` | Write file contents with options for rewrite or append mode (uses configurable line limits) |
|
|
193
194
|
| | `create_directory` | Create a new directory or ensure it exists |
|
|
@@ -125,11 +125,9 @@ export async function handleWriteFile(args) {
|
|
|
125
125
|
const lineCount = lines.length;
|
|
126
126
|
let errorMessage = "";
|
|
127
127
|
if (lineCount > MAX_LINES) {
|
|
128
|
-
errorMessage =
|
|
128
|
+
errorMessage = `✅ File written successfully! (${lineCount} lines)
|
|
129
129
|
|
|
130
|
-
|
|
131
|
-
1. First chunk: write_file(path, firstChunk, {mode: 'rewrite'})
|
|
132
|
-
2. Additional chunks: write_file(path, nextChunk, {mode: 'append'})`;
|
|
130
|
+
💡 Performance tip: For optimal speed, consider chunking files into ≤30 line pieces in future operations.`;
|
|
133
131
|
}
|
|
134
132
|
// Pass the mode parameter to writeFile
|
|
135
133
|
await writeFile(parsed.path, parsed.content, parsed.mode);
|
package/dist/server.js
CHANGED
|
@@ -8,7 +8,7 @@ import { ExecuteCommandArgsSchema, ReadOutputArgsSchema, ForceTerminateArgsSchem
|
|
|
8
8
|
import { getConfig, setConfigValue } from './tools/config.js';
|
|
9
9
|
import { trackToolCall } from './utils/trackTools.js';
|
|
10
10
|
import { VERSION } from './version.js';
|
|
11
|
-
import { capture } from "./utils/capture.js";
|
|
11
|
+
import { capture, capture_call_tool } from "./utils/capture.js";
|
|
12
12
|
console.error("Loading server.ts");
|
|
13
13
|
export const server = new Server({
|
|
14
14
|
name: "desktop-commander",
|
|
@@ -87,7 +87,22 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
87
87
|
|
|
88
88
|
Supports partial file reading with:
|
|
89
89
|
- 'offset' (start line, default: 0)
|
|
90
|
+
* Positive: Start from line N (0-based indexing)
|
|
91
|
+
* Negative: Read last N lines from end (tail behavior)
|
|
90
92
|
- 'length' (max lines to read, default: configurable via 'fileReadLineLimit' setting, initially 1000)
|
|
93
|
+
* Used with positive offsets for range reading
|
|
94
|
+
* Ignored when offset is negative (reads all requested tail lines)
|
|
95
|
+
|
|
96
|
+
Examples:
|
|
97
|
+
- offset: 0, length: 10 → First 10 lines
|
|
98
|
+
- offset: 100, length: 5 → Lines 100-104
|
|
99
|
+
- offset: -20 → Last 20 lines
|
|
100
|
+
- offset: -5, length: 10 → Last 5 lines (length ignored)
|
|
101
|
+
|
|
102
|
+
Performance optimizations:
|
|
103
|
+
- Large files with negative offsets use reverse reading for efficiency
|
|
104
|
+
- Large files with deep positive offsets use byte estimation
|
|
105
|
+
- Small files use fast readline streaming
|
|
91
106
|
|
|
92
107
|
When reading from the file system, only works within allowed directories.
|
|
93
108
|
Can fetch content from URLs when isUrl parameter is set to true
|
|
@@ -119,30 +134,30 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
119
134
|
{
|
|
120
135
|
name: "write_file",
|
|
121
136
|
description: `
|
|
122
|
-
Write or append to file contents
|
|
123
|
-
|
|
137
|
+
Write or append to file contents.
|
|
138
|
+
|
|
139
|
+
🎯 CHUNKING IS STANDARD PRACTICE: Always write files in chunks of 25-30 lines maximum.
|
|
140
|
+
This is the normal, recommended way to write files - not an emergency measure.
|
|
141
|
+
|
|
142
|
+
STANDARD PROCESS FOR ANY FILE:
|
|
143
|
+
1. FIRST → write_file(filePath, firstChunk, {mode: 'rewrite'}) [≤30 lines]
|
|
144
|
+
2. THEN → write_file(filePath, secondChunk, {mode: 'append'}) [≤30 lines]
|
|
145
|
+
3. CONTINUE → write_file(filePath, nextChunk, {mode: 'append'}) [≤30 lines]
|
|
146
|
+
|
|
147
|
+
⚠️ ALWAYS CHUNK PROACTIVELY - don't wait for performance warnings!
|
|
124
148
|
|
|
125
|
-
|
|
126
|
-
1.
|
|
127
|
-
2. When writing
|
|
128
|
-
3. When
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
HANDLING TRUNCATION ("Continue" prompts):
|
|
139
|
-
If user asked to "Continue" after unfinished file write:
|
|
140
|
-
1. First, read the file to find out what content was successfully written
|
|
141
|
-
2. Identify exactly where the content was truncated
|
|
142
|
-
3. Continue writing ONLY the remaining content using {mode: 'append'}
|
|
143
|
-
4. Split the remaining content into smaller chunks (15-20 lines per chunk)
|
|
144
|
-
|
|
145
|
-
Files over the line limit (configurable via 'fileWriteLineLimit' setting) WILL BE REJECTED if not broken into chunks as described above.
|
|
149
|
+
WHEN TO CHUNK (always be proactive):
|
|
150
|
+
1. Any file expected to be longer than 25-30 lines
|
|
151
|
+
2. When writing multiple files in sequence
|
|
152
|
+
3. When creating documentation, code files, or configuration files
|
|
153
|
+
|
|
154
|
+
HANDLING CONTINUATION ("Continue" prompts):
|
|
155
|
+
If user asks to "Continue" after an incomplete operation:
|
|
156
|
+
1. Read the file to see what was successfully written
|
|
157
|
+
2. Continue writing ONLY the remaining content using {mode: 'append'}
|
|
158
|
+
3. Keep chunks to 25-30 lines each
|
|
159
|
+
|
|
160
|
+
Files over 50 lines will generate performance notes but are still written successfully.
|
|
146
161
|
Only works within allowed directories.
|
|
147
162
|
|
|
148
163
|
${PATH_GUIDANCE}
|
|
@@ -347,7 +362,7 @@ import * as handlers from './handlers/index.js';
|
|
|
347
362
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
348
363
|
try {
|
|
349
364
|
const { name, arguments: args } = request.params;
|
|
350
|
-
|
|
365
|
+
capture_call_tool('server_call_tool', {
|
|
351
366
|
name
|
|
352
367
|
});
|
|
353
368
|
// Track tool call
|
package/dist/tools/edit.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { writeFile, readFileInternal } from './filesystem.js';
|
|
2
2
|
import { recursiveFuzzyIndexOf, getSimilarityRatio } from './fuzzySearch.js';
|
|
3
3
|
import { capture } from '../utils/capture.js';
|
|
4
4
|
import { EditBlockArgsSchema } from "./schemas.js";
|
|
@@ -86,8 +86,8 @@ export async function performSearchReplace(filePath, block, expectedReplacements
|
|
|
86
86
|
}],
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
|
-
// Read file as plain string
|
|
90
|
-
const
|
|
89
|
+
// Read file as plain string without status messages
|
|
90
|
+
const content = await readFileInternal(filePath, 0, Number.MAX_SAFE_INTEGER);
|
|
91
91
|
// Make sure content is a string
|
|
92
92
|
if (typeof content !== 'string') {
|
|
93
93
|
capture('server_edit_block_content_not_string', { fileExtension: fileExtension, expectedReplacements });
|
|
@@ -36,6 +36,16 @@ export declare function readFileFromDisk(filePath: string, offset?: number, leng
|
|
|
36
36
|
* @returns File content or file result with metadata
|
|
37
37
|
*/
|
|
38
38
|
export declare function readFile(filePath: string, isUrl?: boolean, offset?: number, length?: number): Promise<FileResult>;
|
|
39
|
+
/**
|
|
40
|
+
* Read file content without status messages for internal operations
|
|
41
|
+
* This function preserves exact file content including original line endings,
|
|
42
|
+
* which is essential for edit operations that need to maintain file formatting.
|
|
43
|
+
* @param filePath Path to the file
|
|
44
|
+
* @param offset Starting line number to read from (default: 0)
|
|
45
|
+
* @param length Maximum number of lines to read (default: from config or 1000)
|
|
46
|
+
* @returns File content without status headers, with preserved line endings
|
|
47
|
+
*/
|
|
48
|
+
export declare function readFileInternal(filePath: string, offset?: number, length?: number): Promise<string>;
|
|
39
49
|
export declare function writeFile(filePath: string, content: string, mode?: 'rewrite' | 'append'): Promise<void>;
|
|
40
50
|
export interface MultiFileResult {
|
|
41
51
|
path: string;
|
package/dist/tools/filesystem.js
CHANGED
|
@@ -2,6 +2,8 @@ import fs from "fs/promises";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import fetch from 'cross-fetch';
|
|
5
|
+
import { createReadStream } from 'fs';
|
|
6
|
+
import { createInterface } from 'readline';
|
|
5
7
|
import { capture } from '../utils/capture.js';
|
|
6
8
|
import { withTimeout } from '../utils/withTimeout.js';
|
|
7
9
|
import { configManager } from '../config-manager.js';
|
|
@@ -205,6 +207,214 @@ export async function readFileFromUrl(url) {
|
|
|
205
207
|
throw new Error(errorMessage);
|
|
206
208
|
}
|
|
207
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Read file content using smart positioning for optimal performance
|
|
212
|
+
* @param filePath Path to the file (already validated)
|
|
213
|
+
* @param offset Starting line number (negative for tail behavior)
|
|
214
|
+
* @param length Maximum number of lines to read
|
|
215
|
+
* @param mimeType MIME type of the file
|
|
216
|
+
* @param includeStatusMessage Whether to include status headers (default: true)
|
|
217
|
+
* @returns File result with content
|
|
218
|
+
*/
|
|
219
|
+
async function readFileWithSmartPositioning(filePath, offset, length, mimeType, includeStatusMessage = true) {
|
|
220
|
+
const stats = await fs.stat(filePath);
|
|
221
|
+
const fileSize = stats.size;
|
|
222
|
+
const LARGE_FILE_THRESHOLD = 10 * 1024 * 1024; // 10MB threshold
|
|
223
|
+
const SMALL_READ_THRESHOLD = 100; // For very small reads, use efficient methods
|
|
224
|
+
// For negative offsets (tail behavior), use reverse reading
|
|
225
|
+
if (offset < 0) {
|
|
226
|
+
const requestedLines = Math.abs(offset);
|
|
227
|
+
if (fileSize > LARGE_FILE_THRESHOLD && requestedLines <= SMALL_READ_THRESHOLD) {
|
|
228
|
+
// Use efficient reverse reading for large files with small tail requests
|
|
229
|
+
return await readLastNLinesReverse(filePath, requestedLines, mimeType, includeStatusMessage);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
// Use readline circular buffer for other cases
|
|
233
|
+
return await readFromEndWithReadline(filePath, requestedLines, mimeType, includeStatusMessage);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// For positive offsets
|
|
237
|
+
else {
|
|
238
|
+
// For small files or reading from start, use simple readline
|
|
239
|
+
if (fileSize < LARGE_FILE_THRESHOLD || offset === 0) {
|
|
240
|
+
return await readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage);
|
|
241
|
+
}
|
|
242
|
+
// For large files with middle/end reads, try to estimate position
|
|
243
|
+
else {
|
|
244
|
+
// If seeking deep into file, try byte estimation
|
|
245
|
+
if (offset > 1000) {
|
|
246
|
+
return await readFromEstimatedPosition(filePath, offset, length, mimeType, includeStatusMessage);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
return await readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Read last N lines efficiently by reading file backwards in chunks
|
|
256
|
+
*/
|
|
257
|
+
async function readLastNLinesReverse(filePath, n, mimeType, includeStatusMessage = true) {
|
|
258
|
+
const fd = await fs.open(filePath, 'r');
|
|
259
|
+
try {
|
|
260
|
+
const stats = await fd.stat();
|
|
261
|
+
const fileSize = stats.size;
|
|
262
|
+
const chunkSize = 8192; // 8KB chunks
|
|
263
|
+
let position = fileSize;
|
|
264
|
+
let lines = [];
|
|
265
|
+
let partialLine = '';
|
|
266
|
+
while (position > 0 && lines.length < n) {
|
|
267
|
+
const readSize = Math.min(chunkSize, position);
|
|
268
|
+
position -= readSize;
|
|
269
|
+
const buffer = Buffer.alloc(readSize);
|
|
270
|
+
await fd.read(buffer, 0, readSize, position);
|
|
271
|
+
const chunk = buffer.toString('utf-8');
|
|
272
|
+
const text = chunk + partialLine;
|
|
273
|
+
const chunkLines = text.split('\n');
|
|
274
|
+
partialLine = chunkLines.shift() || '';
|
|
275
|
+
lines = chunkLines.concat(lines);
|
|
276
|
+
}
|
|
277
|
+
// Add the remaining partial line if we reached the beginning
|
|
278
|
+
if (position === 0 && partialLine) {
|
|
279
|
+
lines.unshift(partialLine);
|
|
280
|
+
}
|
|
281
|
+
const result = lines.slice(-n); // Get exactly n lines
|
|
282
|
+
const content = includeStatusMessage
|
|
283
|
+
? `[Reading last ${result.length} lines]\n\n${result.join('\n')}`
|
|
284
|
+
: result.join('\n');
|
|
285
|
+
return { content, mimeType, isImage: false };
|
|
286
|
+
}
|
|
287
|
+
finally {
|
|
288
|
+
await fd.close();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Read from end using readline with circular buffer
|
|
293
|
+
*/
|
|
294
|
+
async function readFromEndWithReadline(filePath, requestedLines, mimeType, includeStatusMessage = true) {
|
|
295
|
+
const rl = createInterface({
|
|
296
|
+
input: createReadStream(filePath),
|
|
297
|
+
crlfDelay: Infinity
|
|
298
|
+
});
|
|
299
|
+
const buffer = new Array(requestedLines);
|
|
300
|
+
let bufferIndex = 0;
|
|
301
|
+
let totalLines = 0;
|
|
302
|
+
for await (const line of rl) {
|
|
303
|
+
buffer[bufferIndex] = line;
|
|
304
|
+
bufferIndex = (bufferIndex + 1) % requestedLines;
|
|
305
|
+
totalLines++;
|
|
306
|
+
}
|
|
307
|
+
rl.close();
|
|
308
|
+
// Extract lines in correct order
|
|
309
|
+
let result;
|
|
310
|
+
if (totalLines >= requestedLines) {
|
|
311
|
+
result = [
|
|
312
|
+
...buffer.slice(bufferIndex),
|
|
313
|
+
...buffer.slice(0, bufferIndex)
|
|
314
|
+
].filter(line => line !== undefined);
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
result = buffer.slice(0, totalLines);
|
|
318
|
+
}
|
|
319
|
+
const content = includeStatusMessage
|
|
320
|
+
? `[Reading last ${result.length} lines]\n\n${result.join('\n')}`
|
|
321
|
+
: result.join('\n');
|
|
322
|
+
return { content, mimeType, isImage: false };
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Read from start/middle using readline
|
|
326
|
+
*/
|
|
327
|
+
async function readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage = true) {
|
|
328
|
+
const rl = createInterface({
|
|
329
|
+
input: createReadStream(filePath),
|
|
330
|
+
crlfDelay: Infinity
|
|
331
|
+
});
|
|
332
|
+
const result = [];
|
|
333
|
+
let lineNumber = 0;
|
|
334
|
+
for await (const line of rl) {
|
|
335
|
+
if (lineNumber >= offset && result.length < length) {
|
|
336
|
+
result.push(line);
|
|
337
|
+
}
|
|
338
|
+
if (result.length >= length)
|
|
339
|
+
break; // Early exit optimization
|
|
340
|
+
lineNumber++;
|
|
341
|
+
}
|
|
342
|
+
rl.close();
|
|
343
|
+
if (includeStatusMessage) {
|
|
344
|
+
const statusMessage = offset === 0
|
|
345
|
+
? `[Reading ${result.length} lines from start]`
|
|
346
|
+
: `[Reading ${result.length} lines from line ${offset}]`;
|
|
347
|
+
const content = `${statusMessage}\n\n${result.join('\n')}`;
|
|
348
|
+
return { content, mimeType, isImage: false };
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
const content = result.join('\n');
|
|
352
|
+
return { content, mimeType, isImage: false };
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Read from estimated byte position for very large files
|
|
357
|
+
*/
|
|
358
|
+
async function readFromEstimatedPosition(filePath, offset, length, mimeType, includeStatusMessage = true) {
|
|
359
|
+
// First, do a quick scan to estimate lines per byte
|
|
360
|
+
const rl = createInterface({
|
|
361
|
+
input: createReadStream(filePath),
|
|
362
|
+
crlfDelay: Infinity
|
|
363
|
+
});
|
|
364
|
+
let sampleLines = 0;
|
|
365
|
+
let bytesRead = 0;
|
|
366
|
+
const SAMPLE_SIZE = 10000; // Sample first 10KB
|
|
367
|
+
for await (const line of rl) {
|
|
368
|
+
bytesRead += Buffer.byteLength(line, 'utf-8') + 1; // +1 for newline
|
|
369
|
+
sampleLines++;
|
|
370
|
+
if (bytesRead >= SAMPLE_SIZE)
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
rl.close();
|
|
374
|
+
if (sampleLines === 0) {
|
|
375
|
+
// Fallback to simple read
|
|
376
|
+
return await readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage);
|
|
377
|
+
}
|
|
378
|
+
// Estimate average line length and seek position
|
|
379
|
+
const avgLineLength = bytesRead / sampleLines;
|
|
380
|
+
const estimatedBytePosition = Math.floor(offset * avgLineLength);
|
|
381
|
+
// Create a new stream starting from estimated position
|
|
382
|
+
const fd = await fs.open(filePath, 'r');
|
|
383
|
+
try {
|
|
384
|
+
const stats = await fd.stat();
|
|
385
|
+
const startPosition = Math.min(estimatedBytePosition, stats.size);
|
|
386
|
+
const stream = createReadStream(filePath, { start: startPosition });
|
|
387
|
+
const rl2 = createInterface({
|
|
388
|
+
input: stream,
|
|
389
|
+
crlfDelay: Infinity
|
|
390
|
+
});
|
|
391
|
+
const result = [];
|
|
392
|
+
let lineCount = 0;
|
|
393
|
+
let firstLineSkipped = false;
|
|
394
|
+
for await (const line of rl2) {
|
|
395
|
+
// Skip first potentially partial line if we didn't start at beginning
|
|
396
|
+
if (!firstLineSkipped && startPosition > 0) {
|
|
397
|
+
firstLineSkipped = true;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (result.length < length) {
|
|
401
|
+
result.push(line);
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
lineCount++;
|
|
407
|
+
}
|
|
408
|
+
rl2.close();
|
|
409
|
+
const content = includeStatusMessage
|
|
410
|
+
? `[Reading ${result.length} lines from estimated position (target line ${offset})]\n\n${result.join('\n')}`
|
|
411
|
+
: result.join('\n');
|
|
412
|
+
return { content, mimeType, isImage: false };
|
|
413
|
+
}
|
|
414
|
+
finally {
|
|
415
|
+
await fd.close();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
208
418
|
/**
|
|
209
419
|
* Read file content from the local filesystem
|
|
210
420
|
* @param filePath Path to the file
|
|
@@ -258,42 +468,9 @@ export async function readFileFromDisk(filePath, offset = 0, length) {
|
|
|
258
468
|
return { content, mimeType, isImage };
|
|
259
469
|
}
|
|
260
470
|
else {
|
|
261
|
-
// For all other files,
|
|
471
|
+
// For all other files, use smart positioning approach
|
|
262
472
|
try {
|
|
263
|
-
|
|
264
|
-
const buffer = await fs.readFile(validPath);
|
|
265
|
-
const fullContent = buffer.toString('utf-8');
|
|
266
|
-
// Split into lines for line-based access
|
|
267
|
-
const lines = fullContent.split('\n');
|
|
268
|
-
const totalLines = lines.length;
|
|
269
|
-
// Apply line-based offset and length - handle beyond-file-size scenario
|
|
270
|
-
let startLine = Math.min(offset, totalLines);
|
|
271
|
-
let endLine = Math.min(startLine + length, totalLines);
|
|
272
|
-
// If startLine equals totalLines (reading beyond end), adjust to show some content
|
|
273
|
-
// Only do this if we're not trying to read the whole file
|
|
274
|
-
if (startLine === totalLines && offset > 0 && length < Number.MAX_SAFE_INTEGER) {
|
|
275
|
-
// Show last few lines instead of nothing
|
|
276
|
-
const lastLinesCount = Math.min(10, totalLines); // Show last 10 lines or fewer if file is smaller
|
|
277
|
-
startLine = Math.max(0, totalLines - lastLinesCount);
|
|
278
|
-
endLine = totalLines;
|
|
279
|
-
}
|
|
280
|
-
const selectedLines = lines.slice(startLine, endLine);
|
|
281
|
-
const truncatedContent = selectedLines.join('\n');
|
|
282
|
-
// Add an informational message if truncated or adjusted
|
|
283
|
-
let content = truncatedContent;
|
|
284
|
-
// Only add informational message for normal reads (not when reading entire file)
|
|
285
|
-
const isEntireFileRead = offset === 0 && length >= Number.MAX_SAFE_INTEGER;
|
|
286
|
-
if (!isEntireFileRead) {
|
|
287
|
-
if (offset >= totalLines && totalLines > 0) {
|
|
288
|
-
// Reading beyond end of file case
|
|
289
|
-
content = `[NOTICE: Offset ${offset} exceeds file length (${totalLines} lines). Showing last ${endLine - startLine} lines instead.]\n\n${truncatedContent}`;
|
|
290
|
-
}
|
|
291
|
-
else if (offset > 0 || endLine < totalLines) {
|
|
292
|
-
// Normal partial read case
|
|
293
|
-
content = `[Reading ${endLine - startLine} lines from line ${startLine} of ${totalLines} total lines]\n\n${truncatedContent}`;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
return { content, mimeType, isImage };
|
|
473
|
+
return await readFileWithSmartPositioning(validPath, offset, length, mimeType, true);
|
|
297
474
|
}
|
|
298
475
|
catch (error) {
|
|
299
476
|
// If UTF-8 reading fails, treat as binary and return base64 but still as text
|
|
@@ -324,6 +501,84 @@ export async function readFile(filePath, isUrl, offset, length) {
|
|
|
324
501
|
? readFileFromUrl(filePath)
|
|
325
502
|
: readFileFromDisk(filePath, offset, length);
|
|
326
503
|
}
|
|
504
|
+
/**
|
|
505
|
+
* Read file content without status messages for internal operations
|
|
506
|
+
* This function preserves exact file content including original line endings,
|
|
507
|
+
* which is essential for edit operations that need to maintain file formatting.
|
|
508
|
+
* @param filePath Path to the file
|
|
509
|
+
* @param offset Starting line number to read from (default: 0)
|
|
510
|
+
* @param length Maximum number of lines to read (default: from config or 1000)
|
|
511
|
+
* @returns File content without status headers, with preserved line endings
|
|
512
|
+
*/
|
|
513
|
+
export async function readFileInternal(filePath, offset = 0, length) {
|
|
514
|
+
// Get default length from config if not provided
|
|
515
|
+
if (length === undefined) {
|
|
516
|
+
const config = await configManager.getConfig();
|
|
517
|
+
length = config.fileReadLineLimit ?? 1000;
|
|
518
|
+
}
|
|
519
|
+
const validPath = await validatePath(filePath);
|
|
520
|
+
// Get file extension and MIME type
|
|
521
|
+
const fileExtension = path.extname(validPath).toLowerCase();
|
|
522
|
+
const { getMimeType, isImageFile } = await import('./mime-types.js');
|
|
523
|
+
const mimeType = getMimeType(validPath);
|
|
524
|
+
const isImage = isImageFile(mimeType);
|
|
525
|
+
if (isImage) {
|
|
526
|
+
throw new Error('Cannot read image files as text for internal operations');
|
|
527
|
+
}
|
|
528
|
+
// IMPORTANT: For internal operations (especially edit operations), we must
|
|
529
|
+
// preserve exact file content including original line endings.
|
|
530
|
+
// We cannot use readline-based reading as it strips line endings.
|
|
531
|
+
// Read entire file content preserving line endings
|
|
532
|
+
const content = await fs.readFile(validPath, 'utf8');
|
|
533
|
+
// If we need to apply offset/length, do it while preserving line endings
|
|
534
|
+
if (offset === 0 && length >= Number.MAX_SAFE_INTEGER) {
|
|
535
|
+
// Most common case for edit operations: read entire file
|
|
536
|
+
return content;
|
|
537
|
+
}
|
|
538
|
+
// Handle offset/length by splitting on line boundaries while preserving line endings
|
|
539
|
+
const lines = splitLinesPreservingEndings(content);
|
|
540
|
+
// Apply offset and length
|
|
541
|
+
const selectedLines = lines.slice(offset, offset + length);
|
|
542
|
+
// Join back together (this preserves the original line endings)
|
|
543
|
+
return selectedLines.join('');
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Split text into lines while preserving original line endings with each line
|
|
547
|
+
* @param content The text content to split
|
|
548
|
+
* @returns Array of lines, each including its original line ending
|
|
549
|
+
*/
|
|
550
|
+
function splitLinesPreservingEndings(content) {
|
|
551
|
+
if (!content)
|
|
552
|
+
return [''];
|
|
553
|
+
const lines = [];
|
|
554
|
+
let currentLine = '';
|
|
555
|
+
for (let i = 0; i < content.length; i++) {
|
|
556
|
+
const char = content[i];
|
|
557
|
+
currentLine += char;
|
|
558
|
+
// Check for line ending patterns
|
|
559
|
+
if (char === '\n') {
|
|
560
|
+
// LF or end of CRLF
|
|
561
|
+
lines.push(currentLine);
|
|
562
|
+
currentLine = '';
|
|
563
|
+
}
|
|
564
|
+
else if (char === '\r') {
|
|
565
|
+
// Could be CR or start of CRLF
|
|
566
|
+
if (i + 1 < content.length && content[i + 1] === '\n') {
|
|
567
|
+
// It's CRLF, include the \n as well
|
|
568
|
+
currentLine += content[i + 1];
|
|
569
|
+
i++; // Skip the \n in next iteration
|
|
570
|
+
}
|
|
571
|
+
// Either way, we have a complete line
|
|
572
|
+
lines.push(currentLine);
|
|
573
|
+
currentLine = '';
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
// Handle any remaining content (file not ending with line ending)
|
|
577
|
+
if (currentLine) {
|
|
578
|
+
lines.push(currentLine);
|
|
579
|
+
}
|
|
580
|
+
return lines;
|
|
581
|
+
}
|
|
327
582
|
export async function writeFile(filePath, content, mode = 'rewrite') {
|
|
328
583
|
const validPath = await validatePath(filePath);
|
|
329
584
|
// Get file extension for telemetry
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ServerResult } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Start a new process (renamed from execute_command)
|
|
4
|
+
* Includes early detection of process waiting for input
|
|
5
|
+
*/
|
|
6
|
+
export declare function startProcess(args: unknown): Promise<ServerResult>;
|
|
7
|
+
/**
|
|
8
|
+
* Read output from a running process (renamed from read_output)
|
|
9
|
+
* Includes early detection of process waiting for input
|
|
10
|
+
*/
|
|
11
|
+
export declare function readProcessOutput(args: unknown): Promise<ServerResult>;
|
|
12
|
+
/**
|
|
13
|
+
* Interact with a running process (renamed from send_input)
|
|
14
|
+
* Automatically detects when process is ready and returns output
|
|
15
|
+
*/
|
|
16
|
+
export declare function interactWithProcess(args: unknown): Promise<ServerResult>;
|
|
17
|
+
/**
|
|
18
|
+
* Force terminate a process
|
|
19
|
+
*/
|
|
20
|
+
export declare function forceTerminate(args: unknown): Promise<ServerResult>;
|
|
21
|
+
/**
|
|
22
|
+
* List active sessions
|
|
23
|
+
*/
|
|
24
|
+
export declare function listSessions(): Promise<ServerResult>;
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { terminalManager } from '../terminal-manager.js';
|
|
2
|
+
import { commandManager } from '../command-manager.js';
|
|
3
|
+
import { StartProcessArgsSchema, ReadProcessOutputArgsSchema, InteractWithProcessArgsSchema, ForceTerminateArgsSchema } from './schemas.js';
|
|
4
|
+
import { capture } from "../utils/capture.js";
|
|
5
|
+
import { analyzeProcessState, cleanProcessOutput, formatProcessStateMessage } from '../utils/process-detection.js';
|
|
6
|
+
import { getSystemInfo } from '../utils/system-info.js';
|
|
7
|
+
/**
|
|
8
|
+
* Start a new process (renamed from execute_command)
|
|
9
|
+
* Includes early detection of process waiting for input
|
|
10
|
+
*/
|
|
11
|
+
export async function startProcess(args) {
|
|
12
|
+
const parsed = StartProcessArgsSchema.safeParse(args);
|
|
13
|
+
if (!parsed.success) {
|
|
14
|
+
capture('server_start_process_failed');
|
|
15
|
+
return {
|
|
16
|
+
content: [{ type: "text", text: `Error: Invalid arguments for start_process: ${parsed.error}` }],
|
|
17
|
+
isError: true,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const commands = commandManager.extractCommands(parsed.data.command).join(', ');
|
|
22
|
+
capture('server_start_process', {
|
|
23
|
+
command: commandManager.getBaseCommand(parsed.data.command),
|
|
24
|
+
commands: commands
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
capture('server_start_process', {
|
|
29
|
+
command: commandManager.getBaseCommand(parsed.data.command)
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
const isAllowed = await commandManager.validateCommand(parsed.data.command);
|
|
33
|
+
if (!isAllowed) {
|
|
34
|
+
return {
|
|
35
|
+
content: [{ type: "text", text: `Error: Command not allowed: ${parsed.data.command}` }],
|
|
36
|
+
isError: true,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const result = await terminalManager.executeCommand(parsed.data.command, parsed.data.timeout_ms, parsed.data.shell);
|
|
40
|
+
if (result.pid === -1) {
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: "text", text: result.output }],
|
|
43
|
+
isError: true,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// Analyze the process state to detect if it's waiting for input
|
|
47
|
+
const processState = analyzeProcessState(result.output, result.pid);
|
|
48
|
+
// Get system info for shell information
|
|
49
|
+
const systemInfo = getSystemInfo();
|
|
50
|
+
const shellUsed = parsed.data.shell || systemInfo.defaultShell;
|
|
51
|
+
let statusMessage = '';
|
|
52
|
+
if (processState.isWaitingForInput) {
|
|
53
|
+
statusMessage = `\n🔄 ${formatProcessStateMessage(processState, result.pid)}`;
|
|
54
|
+
}
|
|
55
|
+
else if (processState.isFinished) {
|
|
56
|
+
statusMessage = `\n✅ ${formatProcessStateMessage(processState, result.pid)}`;
|
|
57
|
+
}
|
|
58
|
+
else if (result.isBlocked) {
|
|
59
|
+
statusMessage = '\n⏳ Process is running. Use read_process_output to get more output.';
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
content: [{
|
|
63
|
+
type: "text",
|
|
64
|
+
text: `Process started with PID ${result.pid} (shell: ${shellUsed})\nInitial output:\n${result.output}${statusMessage}`
|
|
65
|
+
}],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Read output from a running process (renamed from read_output)
|
|
70
|
+
* Includes early detection of process waiting for input
|
|
71
|
+
*/
|
|
72
|
+
export async function readProcessOutput(args) {
|
|
73
|
+
const parsed = ReadProcessOutputArgsSchema.safeParse(args);
|
|
74
|
+
if (!parsed.success) {
|
|
75
|
+
return {
|
|
76
|
+
content: [{ type: "text", text: `Error: Invalid arguments for read_process_output: ${parsed.error}` }],
|
|
77
|
+
isError: true,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const { pid, timeout_ms = 5000 } = parsed.data;
|
|
81
|
+
const session = terminalManager.getSession(pid);
|
|
82
|
+
if (!session) {
|
|
83
|
+
return {
|
|
84
|
+
content: [{ type: "text", text: `No active session found for PID ${pid}` }],
|
|
85
|
+
isError: true,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
let output = "";
|
|
89
|
+
let timeoutReached = false;
|
|
90
|
+
let earlyExit = false;
|
|
91
|
+
let processState;
|
|
92
|
+
try {
|
|
93
|
+
const outputPromise = new Promise((resolve) => {
|
|
94
|
+
const initialOutput = terminalManager.getNewOutput(pid);
|
|
95
|
+
if (initialOutput && initialOutput.length > 0) {
|
|
96
|
+
resolve(initialOutput);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
let resolved = false;
|
|
100
|
+
let interval = null;
|
|
101
|
+
let timeout = null;
|
|
102
|
+
const cleanup = () => {
|
|
103
|
+
if (interval)
|
|
104
|
+
clearInterval(interval);
|
|
105
|
+
if (timeout)
|
|
106
|
+
clearTimeout(timeout);
|
|
107
|
+
};
|
|
108
|
+
const resolveOnce = (value, isTimeout = false) => {
|
|
109
|
+
if (resolved)
|
|
110
|
+
return;
|
|
111
|
+
resolved = true;
|
|
112
|
+
cleanup();
|
|
113
|
+
timeoutReached = isTimeout;
|
|
114
|
+
resolve(value);
|
|
115
|
+
};
|
|
116
|
+
interval = setInterval(() => {
|
|
117
|
+
const newOutput = terminalManager.getNewOutput(pid);
|
|
118
|
+
if (newOutput && newOutput.length > 0) {
|
|
119
|
+
const currentOutput = output + newOutput;
|
|
120
|
+
const state = analyzeProcessState(currentOutput, pid);
|
|
121
|
+
// Early exit if process is clearly waiting for input
|
|
122
|
+
if (state.isWaitingForInput) {
|
|
123
|
+
earlyExit = true;
|
|
124
|
+
processState = state;
|
|
125
|
+
resolveOnce(newOutput);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
output = currentOutput;
|
|
129
|
+
// Continue collecting if still running
|
|
130
|
+
if (!state.isFinished) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
// Process finished
|
|
134
|
+
processState = state;
|
|
135
|
+
resolveOnce(newOutput);
|
|
136
|
+
}
|
|
137
|
+
}, 200); // Check every 200ms
|
|
138
|
+
timeout = setTimeout(() => {
|
|
139
|
+
const finalOutput = terminalManager.getNewOutput(pid) || "";
|
|
140
|
+
resolveOnce(finalOutput, true);
|
|
141
|
+
}, timeout_ms);
|
|
142
|
+
});
|
|
143
|
+
const newOutput = await outputPromise;
|
|
144
|
+
output += newOutput;
|
|
145
|
+
// Analyze final state if not already done
|
|
146
|
+
if (!processState) {
|
|
147
|
+
processState = analyzeProcessState(output, pid);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
return {
|
|
152
|
+
content: [{ type: "text", text: `Error reading output: ${error}` }],
|
|
153
|
+
isError: true,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
// Format response based on what we detected
|
|
157
|
+
let statusMessage = '';
|
|
158
|
+
if (earlyExit && processState?.isWaitingForInput) {
|
|
159
|
+
statusMessage = `\n🔄 ${formatProcessStateMessage(processState, pid)}`;
|
|
160
|
+
}
|
|
161
|
+
else if (processState?.isFinished) {
|
|
162
|
+
statusMessage = `\n✅ ${formatProcessStateMessage(processState, pid)}`;
|
|
163
|
+
}
|
|
164
|
+
else if (timeoutReached) {
|
|
165
|
+
statusMessage = '\n⏱️ Timeout reached - process may still be running';
|
|
166
|
+
}
|
|
167
|
+
const responseText = output || 'No new output available';
|
|
168
|
+
return {
|
|
169
|
+
content: [{
|
|
170
|
+
type: "text",
|
|
171
|
+
text: `${responseText}${statusMessage}`
|
|
172
|
+
}],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Interact with a running process (renamed from send_input)
|
|
177
|
+
* Automatically detects when process is ready and returns output
|
|
178
|
+
*/
|
|
179
|
+
export async function interactWithProcess(args) {
|
|
180
|
+
const parsed = InteractWithProcessArgsSchema.safeParse(args);
|
|
181
|
+
if (!parsed.success) {
|
|
182
|
+
capture('server_interact_with_process_failed', {
|
|
183
|
+
error: 'Invalid arguments'
|
|
184
|
+
});
|
|
185
|
+
return {
|
|
186
|
+
content: [{ type: "text", text: `Error: Invalid arguments for interact_with_process: ${parsed.error}` }],
|
|
187
|
+
isError: true,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const { pid, input, timeout_ms = 8000, wait_for_prompt = true } = parsed.data;
|
|
191
|
+
try {
|
|
192
|
+
capture('server_interact_with_process', {
|
|
193
|
+
pid: pid,
|
|
194
|
+
inputLength: input.length
|
|
195
|
+
});
|
|
196
|
+
const success = terminalManager.sendInputToProcess(pid, input);
|
|
197
|
+
if (!success) {
|
|
198
|
+
return {
|
|
199
|
+
content: [{ type: "text", text: `Error: Failed to send input to process ${pid}. The process may have exited or doesn't accept input.` }],
|
|
200
|
+
isError: true,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
// If not waiting for response, return immediately
|
|
204
|
+
if (!wait_for_prompt) {
|
|
205
|
+
return {
|
|
206
|
+
content: [{
|
|
207
|
+
type: "text",
|
|
208
|
+
text: `✅ Input sent to process ${pid}. Use read_process_output to get the response.`
|
|
209
|
+
}],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
// Smart waiting with process state detection
|
|
213
|
+
let output = "";
|
|
214
|
+
let attempts = 0;
|
|
215
|
+
const maxAttempts = Math.ceil(timeout_ms / 200);
|
|
216
|
+
let processState;
|
|
217
|
+
while (attempts < maxAttempts) {
|
|
218
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
219
|
+
const newOutput = terminalManager.getNewOutput(pid);
|
|
220
|
+
if (newOutput && newOutput.length > 0) {
|
|
221
|
+
output += newOutput;
|
|
222
|
+
// Analyze current state
|
|
223
|
+
processState = analyzeProcessState(output, pid);
|
|
224
|
+
// Exit early if we detect the process is waiting for input
|
|
225
|
+
if (processState.isWaitingForInput) {
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
// Also exit if process finished
|
|
229
|
+
if (processState.isFinished) {
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
attempts++;
|
|
234
|
+
}
|
|
235
|
+
// Clean and format output
|
|
236
|
+
const cleanOutput = cleanProcessOutput(output, input);
|
|
237
|
+
const timeoutReached = attempts >= maxAttempts;
|
|
238
|
+
// Determine final state
|
|
239
|
+
if (!processState) {
|
|
240
|
+
processState = analyzeProcessState(output, pid);
|
|
241
|
+
}
|
|
242
|
+
let statusMessage = '';
|
|
243
|
+
if (processState.isWaitingForInput) {
|
|
244
|
+
statusMessage = `\n🔄 ${formatProcessStateMessage(processState, pid)}`;
|
|
245
|
+
}
|
|
246
|
+
else if (processState.isFinished) {
|
|
247
|
+
statusMessage = `\n✅ ${formatProcessStateMessage(processState, pid)}`;
|
|
248
|
+
}
|
|
249
|
+
else if (timeoutReached) {
|
|
250
|
+
statusMessage = '\n⏱️ Response may be incomplete (timeout reached)';
|
|
251
|
+
}
|
|
252
|
+
if (cleanOutput.trim().length === 0 && !timeoutReached) {
|
|
253
|
+
return {
|
|
254
|
+
content: [{
|
|
255
|
+
type: "text",
|
|
256
|
+
text: `✅ Input executed in process ${pid}.\n(No output produced)${statusMessage}`
|
|
257
|
+
}],
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
content: [{
|
|
262
|
+
type: "text",
|
|
263
|
+
text: `✅ Input executed in process ${pid}:\n\n${cleanOutput}${statusMessage}`
|
|
264
|
+
}],
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
269
|
+
capture('server_interact_with_process_error', {
|
|
270
|
+
error: errorMessage
|
|
271
|
+
});
|
|
272
|
+
return {
|
|
273
|
+
content: [{ type: "text", text: `Error interacting with process: ${errorMessage}` }],
|
|
274
|
+
isError: true,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Force terminate a process
|
|
280
|
+
*/
|
|
281
|
+
export async function forceTerminate(args) {
|
|
282
|
+
const parsed = ForceTerminateArgsSchema.safeParse(args);
|
|
283
|
+
if (!parsed.success) {
|
|
284
|
+
return {
|
|
285
|
+
content: [{ type: "text", text: `Error: Invalid arguments for force_terminate: ${parsed.error}` }],
|
|
286
|
+
isError: true,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
const success = terminalManager.forceTerminate(parsed.data.pid);
|
|
290
|
+
return {
|
|
291
|
+
content: [{
|
|
292
|
+
type: "text",
|
|
293
|
+
text: success
|
|
294
|
+
? `Successfully initiated termination of session ${parsed.data.pid}`
|
|
295
|
+
: `No active session found for PID ${parsed.data.pid}`
|
|
296
|
+
}],
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* List active sessions
|
|
301
|
+
*/
|
|
302
|
+
export async function listSessions() {
|
|
303
|
+
const sessions = terminalManager.listActiveSessions();
|
|
304
|
+
return {
|
|
305
|
+
content: [{
|
|
306
|
+
type: "text",
|
|
307
|
+
text: sessions.length === 0
|
|
308
|
+
? 'No active sessions'
|
|
309
|
+
: sessions.map(s => `PID: ${s.pid}, Blocked: ${s.isBlocked}, Runtime: ${Math.round(s.runtime / 1000)}s`).join('\n')
|
|
310
|
+
}],
|
|
311
|
+
};
|
|
312
|
+
}
|
package/dist/tools/schemas.js
CHANGED
package/dist/utils/capture.d.ts
CHANGED
|
@@ -12,4 +12,6 @@ export declare function sanitizeError(error: any): {
|
|
|
12
12
|
* @param event Event name
|
|
13
13
|
* @param properties Optional event properties
|
|
14
14
|
*/
|
|
15
|
+
export declare const captureBase: (captureURL: string, event: string, properties?: any) => Promise<void>;
|
|
16
|
+
export declare const capture_call_tool: (event: string, properties?: any) => Promise<void>;
|
|
15
17
|
export declare const capture: (event: string, properties?: any) => Promise<void>;
|
package/dist/utils/capture.js
CHANGED
|
@@ -10,11 +10,6 @@ try {
|
|
|
10
10
|
catch {
|
|
11
11
|
// Continue without version info if not available
|
|
12
12
|
}
|
|
13
|
-
// Configuration
|
|
14
|
-
const GA_MEASUREMENT_ID = 'G-NGGDNL0K4L'; // Replace with your GA4 Measurement ID
|
|
15
|
-
const GA_API_SECRET = '5M0mC--2S_6t94m8WrI60A'; // Replace with your GA4 API Secret
|
|
16
|
-
const GA_BASE_URL = `https://www.google-analytics.com/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`;
|
|
17
|
-
const GA_DEBUG_BASE_URL = `https://www.google-analytics.com/debug/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`;
|
|
18
13
|
// Will be initialized when needed
|
|
19
14
|
let uniqueUserId = 'unknown';
|
|
20
15
|
// Function to get or create a persistent UUID
|
|
@@ -70,12 +65,12 @@ export function sanitizeError(error) {
|
|
|
70
65
|
* @param event Event name
|
|
71
66
|
* @param properties Optional event properties
|
|
72
67
|
*/
|
|
73
|
-
export const
|
|
68
|
+
export const captureBase = async (captureURL, event, properties) => {
|
|
74
69
|
try {
|
|
75
70
|
// Check if telemetry is enabled in config (defaults to true if not set)
|
|
76
71
|
const telemetryEnabled = await configManager.getValue('telemetryEnabled');
|
|
77
72
|
// If telemetry is explicitly disabled or GA credentials are missing, don't send
|
|
78
|
-
if (telemetryEnabled === false || !
|
|
73
|
+
if (telemetryEnabled === false || !captureURL) {
|
|
79
74
|
return;
|
|
80
75
|
}
|
|
81
76
|
// Get or create the client ID if not already initialized
|
|
@@ -145,7 +140,7 @@ export const capture = async (event, properties) => {
|
|
|
145
140
|
'Content-Length': Buffer.byteLength(postData)
|
|
146
141
|
}
|
|
147
142
|
};
|
|
148
|
-
const req = https.request(
|
|
143
|
+
const req = https.request(captureURL, options, (res) => {
|
|
149
144
|
// Response handling (optional)
|
|
150
145
|
let data = '';
|
|
151
146
|
res.on('data', (chunk) => {
|
|
@@ -173,3 +168,17 @@ export const capture = async (event, properties) => {
|
|
|
173
168
|
// Silently fail - we don't want analytics issues to break functionality
|
|
174
169
|
}
|
|
175
170
|
};
|
|
171
|
+
export const capture_call_tool = async (event, properties) => {
|
|
172
|
+
const GA_MEASUREMENT_ID = 'G-35YKFM782B'; // Replace with your GA4 Measurement ID
|
|
173
|
+
const GA_API_SECRET = 'qM5VNk6aQy6NN5s-tCppZw'; // Replace with your GA4 API Secret
|
|
174
|
+
const GA_BASE_URL = `https://www.google-analytics.com/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`;
|
|
175
|
+
const GA_DEBUG_BASE_URL = `https://www.google-analytics.com/debug/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`;
|
|
176
|
+
return await captureBase(GA_BASE_URL, event, properties);
|
|
177
|
+
};
|
|
178
|
+
export const capture = async (event, properties) => {
|
|
179
|
+
const GA_MEASUREMENT_ID = 'G-NGGDNL0K4L'; // Replace with your GA4 Measurement ID
|
|
180
|
+
const GA_API_SECRET = '5M0mC--2S_6t94m8WrI60A'; // Replace with your GA4 API Secret
|
|
181
|
+
const GA_BASE_URL = `https://www.google-analytics.com/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`;
|
|
182
|
+
const GA_DEBUG_BASE_URL = `https://www.google-analytics.com/debug/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`;
|
|
183
|
+
return await captureBase(GA_BASE_URL, event, properties);
|
|
184
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REPL and Process State Detection Utilities
|
|
3
|
+
* Detects when processes are waiting for input vs finished vs running
|
|
4
|
+
*/
|
|
5
|
+
export interface ProcessState {
|
|
6
|
+
isWaitingForInput: boolean;
|
|
7
|
+
isFinished: boolean;
|
|
8
|
+
isRunning: boolean;
|
|
9
|
+
detectedPrompt?: string;
|
|
10
|
+
lastOutput: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Analyze process output to determine current state
|
|
14
|
+
*/
|
|
15
|
+
export declare function analyzeProcessState(output: string, pid?: number): ProcessState;
|
|
16
|
+
/**
|
|
17
|
+
* Clean output by removing prompts and input echoes
|
|
18
|
+
*/
|
|
19
|
+
export declare function cleanProcessOutput(output: string, inputSent?: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Format process state for user display
|
|
22
|
+
*/
|
|
23
|
+
export declare function formatProcessStateMessage(state: ProcessState, pid: number): string;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REPL and Process State Detection Utilities
|
|
3
|
+
* Detects when processes are waiting for input vs finished vs running
|
|
4
|
+
*/
|
|
5
|
+
// Common REPL prompt patterns
|
|
6
|
+
const REPL_PROMPTS = {
|
|
7
|
+
python: ['>>> ', '... '],
|
|
8
|
+
node: ['> ', '... '],
|
|
9
|
+
r: ['> ', '+ '],
|
|
10
|
+
julia: ['julia> ', ' '], // julia continuation is spaces
|
|
11
|
+
shell: ['$ ', '# ', '% ', 'bash-', 'zsh-'],
|
|
12
|
+
mysql: ['mysql> ', ' -> '],
|
|
13
|
+
postgres: ['=# ', '-# '],
|
|
14
|
+
redis: ['redis> '],
|
|
15
|
+
mongo: ['> ', '... ']
|
|
16
|
+
};
|
|
17
|
+
// Error patterns that indicate completion (even with errors)
|
|
18
|
+
const ERROR_COMPLETION_PATTERNS = [
|
|
19
|
+
/Error:/i,
|
|
20
|
+
/Exception:/i,
|
|
21
|
+
/Traceback/i,
|
|
22
|
+
/SyntaxError/i,
|
|
23
|
+
/NameError/i,
|
|
24
|
+
/TypeError/i,
|
|
25
|
+
/ValueError/i,
|
|
26
|
+
/ReferenceError/i,
|
|
27
|
+
/Uncaught/i,
|
|
28
|
+
/at Object\./i, // Node.js stack traces
|
|
29
|
+
/^\s*\^/m // Syntax error indicators
|
|
30
|
+
];
|
|
31
|
+
// Process completion indicators
|
|
32
|
+
const COMPLETION_INDICATORS = [
|
|
33
|
+
/Process finished/i,
|
|
34
|
+
/Command completed/i,
|
|
35
|
+
/\[Process completed\]/i,
|
|
36
|
+
/Program terminated/i,
|
|
37
|
+
/Exit code:/i
|
|
38
|
+
];
|
|
39
|
+
/**
|
|
40
|
+
* Analyze process output to determine current state
|
|
41
|
+
*/
|
|
42
|
+
export function analyzeProcessState(output, pid) {
|
|
43
|
+
if (!output || output.trim().length === 0) {
|
|
44
|
+
return {
|
|
45
|
+
isWaitingForInput: false,
|
|
46
|
+
isFinished: false,
|
|
47
|
+
isRunning: true,
|
|
48
|
+
lastOutput: output
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const lines = output.split('\n');
|
|
52
|
+
const lastLine = lines[lines.length - 1] || '';
|
|
53
|
+
const lastFewLines = lines.slice(-3).join('\n');
|
|
54
|
+
// Check for REPL prompts (waiting for input)
|
|
55
|
+
const allPrompts = Object.values(REPL_PROMPTS).flat();
|
|
56
|
+
const detectedPrompt = allPrompts.find(prompt => lastLine.endsWith(prompt) || lastLine.includes(prompt));
|
|
57
|
+
if (detectedPrompt) {
|
|
58
|
+
return {
|
|
59
|
+
isWaitingForInput: true,
|
|
60
|
+
isFinished: false,
|
|
61
|
+
isRunning: true,
|
|
62
|
+
detectedPrompt,
|
|
63
|
+
lastOutput: output
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
// Check for completion indicators
|
|
67
|
+
const hasCompletionIndicator = COMPLETION_INDICATORS.some(pattern => pattern.test(output));
|
|
68
|
+
if (hasCompletionIndicator) {
|
|
69
|
+
return {
|
|
70
|
+
isWaitingForInput: false,
|
|
71
|
+
isFinished: true,
|
|
72
|
+
isRunning: false,
|
|
73
|
+
lastOutput: output
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// Check for error completion (errors usually end with prompts, but let's be thorough)
|
|
77
|
+
const hasErrorCompletion = ERROR_COMPLETION_PATTERNS.some(pattern => pattern.test(lastFewLines));
|
|
78
|
+
if (hasErrorCompletion) {
|
|
79
|
+
// Errors can indicate completion, but check if followed by prompt
|
|
80
|
+
if (detectedPrompt) {
|
|
81
|
+
return {
|
|
82
|
+
isWaitingForInput: true,
|
|
83
|
+
isFinished: false,
|
|
84
|
+
isRunning: true,
|
|
85
|
+
detectedPrompt,
|
|
86
|
+
lastOutput: output
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
return {
|
|
91
|
+
isWaitingForInput: false,
|
|
92
|
+
isFinished: true,
|
|
93
|
+
isRunning: false,
|
|
94
|
+
lastOutput: output
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Default: process is running, not clearly waiting or finished
|
|
99
|
+
return {
|
|
100
|
+
isWaitingForInput: false,
|
|
101
|
+
isFinished: false,
|
|
102
|
+
isRunning: true,
|
|
103
|
+
lastOutput: output
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Clean output by removing prompts and input echoes
|
|
108
|
+
*/
|
|
109
|
+
export function cleanProcessOutput(output, inputSent) {
|
|
110
|
+
let cleaned = output;
|
|
111
|
+
// Remove input echo if provided
|
|
112
|
+
if (inputSent) {
|
|
113
|
+
const inputLines = inputSent.split('\n');
|
|
114
|
+
inputLines.forEach(line => {
|
|
115
|
+
if (line.trim()) {
|
|
116
|
+
cleaned = cleaned.replace(new RegExp(`^${escapeRegExp(line.trim())}\\s*\n?`, 'm'), '');
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
// Remove common prompt patterns from output
|
|
121
|
+
cleaned = cleaned.replace(/^>>>\s*/gm, ''); // Python >>>
|
|
122
|
+
cleaned = cleaned.replace(/^>\s*/gm, ''); // Node.js/Shell >
|
|
123
|
+
cleaned = cleaned.replace(/^\.{3}\s*/gm, ''); // Python ...
|
|
124
|
+
cleaned = cleaned.replace(/^\+\s*/gm, ''); // R +
|
|
125
|
+
// Remove trailing prompts
|
|
126
|
+
cleaned = cleaned.replace(/\n>>>\s*$/, '');
|
|
127
|
+
cleaned = cleaned.replace(/\n>\s*$/, '');
|
|
128
|
+
cleaned = cleaned.replace(/\n\+\s*$/, '');
|
|
129
|
+
return cleaned.trim();
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Escape special regex characters
|
|
133
|
+
*/
|
|
134
|
+
function escapeRegExp(string) {
|
|
135
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Format process state for user display
|
|
139
|
+
*/
|
|
140
|
+
export function formatProcessStateMessage(state, pid) {
|
|
141
|
+
if (state.isWaitingForInput) {
|
|
142
|
+
return `Process ${pid} is waiting for input${state.detectedPrompt ? ` (detected: "${state.detectedPrompt.trim()}")` : ''}`;
|
|
143
|
+
}
|
|
144
|
+
else if (state.isFinished) {
|
|
145
|
+
return `Process ${pid} has finished execution`;
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
return `Process ${pid} is running`;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface SystemInfo {
|
|
2
|
+
platform: string;
|
|
3
|
+
platformName: string;
|
|
4
|
+
defaultShell: string;
|
|
5
|
+
pathSeparator: string;
|
|
6
|
+
isWindows: boolean;
|
|
7
|
+
isMacOS: boolean;
|
|
8
|
+
isLinux: boolean;
|
|
9
|
+
examplePaths: {
|
|
10
|
+
home: string;
|
|
11
|
+
temp: string;
|
|
12
|
+
absolute: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Get comprehensive system information for tool prompts
|
|
17
|
+
*/
|
|
18
|
+
export declare function getSystemInfo(): SystemInfo;
|
|
19
|
+
/**
|
|
20
|
+
* Generate OS-specific guidance for tool prompts
|
|
21
|
+
*/
|
|
22
|
+
export declare function getOSSpecificGuidance(systemInfo: SystemInfo): string;
|
|
23
|
+
/**
|
|
24
|
+
* Get common development tool guidance based on OS
|
|
25
|
+
*/
|
|
26
|
+
export declare function getDevelopmentToolGuidance(systemInfo: SystemInfo): string;
|
|
27
|
+
/**
|
|
28
|
+
* Get path guidance (simplified since paths are normalized)
|
|
29
|
+
*/
|
|
30
|
+
export declare function getPathGuidance(systemInfo: SystemInfo): string;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
/**
|
|
3
|
+
* Get comprehensive system information for tool prompts
|
|
4
|
+
*/
|
|
5
|
+
export function getSystemInfo() {
|
|
6
|
+
const platform = os.platform();
|
|
7
|
+
const isWindows = platform === 'win32';
|
|
8
|
+
const isMacOS = platform === 'darwin';
|
|
9
|
+
const isLinux = platform === 'linux';
|
|
10
|
+
let platformName;
|
|
11
|
+
let defaultShell;
|
|
12
|
+
let pathSeparator;
|
|
13
|
+
let examplePaths;
|
|
14
|
+
if (isWindows) {
|
|
15
|
+
platformName = 'Windows';
|
|
16
|
+
defaultShell = 'powershell.exe';
|
|
17
|
+
pathSeparator = '\\';
|
|
18
|
+
examplePaths = {
|
|
19
|
+
home: 'C:\\Users\\username',
|
|
20
|
+
temp: 'C:\\Temp',
|
|
21
|
+
absolute: 'C:\\path\\to\\file.txt'
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
else if (isMacOS) {
|
|
25
|
+
platformName = 'macOS';
|
|
26
|
+
defaultShell = 'zsh';
|
|
27
|
+
pathSeparator = '/';
|
|
28
|
+
examplePaths = {
|
|
29
|
+
home: '/Users/username',
|
|
30
|
+
temp: '/tmp',
|
|
31
|
+
absolute: '/path/to/file.txt'
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
else if (isLinux) {
|
|
35
|
+
platformName = 'Linux';
|
|
36
|
+
defaultShell = 'bash';
|
|
37
|
+
pathSeparator = '/';
|
|
38
|
+
examplePaths = {
|
|
39
|
+
home: '/home/username',
|
|
40
|
+
temp: '/tmp',
|
|
41
|
+
absolute: '/path/to/file.txt'
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
// Fallback for other Unix-like systems
|
|
46
|
+
platformName = 'Unix';
|
|
47
|
+
defaultShell = 'bash';
|
|
48
|
+
pathSeparator = '/';
|
|
49
|
+
examplePaths = {
|
|
50
|
+
home: '/home/username',
|
|
51
|
+
temp: '/tmp',
|
|
52
|
+
absolute: '/path/to/file.txt'
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
platform,
|
|
57
|
+
platformName,
|
|
58
|
+
defaultShell,
|
|
59
|
+
pathSeparator,
|
|
60
|
+
isWindows,
|
|
61
|
+
isMacOS,
|
|
62
|
+
isLinux,
|
|
63
|
+
examplePaths
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Generate OS-specific guidance for tool prompts
|
|
68
|
+
*/
|
|
69
|
+
export function getOSSpecificGuidance(systemInfo) {
|
|
70
|
+
const { platformName, defaultShell, isWindows } = systemInfo;
|
|
71
|
+
let guidance = `Running on ${platformName}. Default shell: ${defaultShell}.`;
|
|
72
|
+
if (isWindows) {
|
|
73
|
+
guidance += `
|
|
74
|
+
|
|
75
|
+
WINDOWS-SPECIFIC TROUBLESHOOTING:
|
|
76
|
+
- If Node.js/Python commands fail with "not recognized" errors:
|
|
77
|
+
* Try different shells: specify shell parameter as "cmd" or "powershell.exe"
|
|
78
|
+
* PowerShell may have execution policy restrictions for some tools
|
|
79
|
+
* CMD typically has better compatibility with development tools
|
|
80
|
+
* Use set_config_value to change defaultShell if needed
|
|
81
|
+
- Windows services and processes use different commands (Get-Process vs ps)
|
|
82
|
+
- Package managers: choco, winget, scoop instead of apt/brew
|
|
83
|
+
- Environment variables: $env:VAR instead of $VAR
|
|
84
|
+
- File permissions work differently than Unix systems`;
|
|
85
|
+
}
|
|
86
|
+
else if (systemInfo.isMacOS) {
|
|
87
|
+
guidance += `
|
|
88
|
+
|
|
89
|
+
MACOS-SPECIFIC NOTES:
|
|
90
|
+
- Package manager: brew (Homebrew) is commonly used
|
|
91
|
+
- Python 3 might be 'python3' command, not 'python'
|
|
92
|
+
- Some GNU tools have different names (e.g., gsed instead of sed)
|
|
93
|
+
- System Integrity Protection (SIP) may block certain operations
|
|
94
|
+
- Use 'open' command to open files/applications from terminal`;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
guidance += `
|
|
98
|
+
|
|
99
|
+
LINUX-SPECIFIC NOTES:
|
|
100
|
+
- Package managers vary by distro: apt, yum, dnf, pacman, zypper
|
|
101
|
+
- Python 3 might be 'python3' command, not 'python'
|
|
102
|
+
- Standard Unix shell tools available (grep, awk, sed, etc.)
|
|
103
|
+
- File permissions and ownership important for many operations
|
|
104
|
+
- Systemd services common on modern distributions`;
|
|
105
|
+
}
|
|
106
|
+
return guidance;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Get common development tool guidance based on OS
|
|
110
|
+
*/
|
|
111
|
+
export function getDevelopmentToolGuidance(systemInfo) {
|
|
112
|
+
const { isWindows, isMacOS, isLinux, platformName } = systemInfo;
|
|
113
|
+
if (isWindows) {
|
|
114
|
+
return `
|
|
115
|
+
COMMON WINDOWS DEVELOPMENT TOOLS:
|
|
116
|
+
- Node.js: Usually installed globally, accessible from any shell
|
|
117
|
+
- Python: May be 'python' or 'py' command, check both
|
|
118
|
+
- Git: Git Bash provides Unix-like environment
|
|
119
|
+
- WSL: Windows Subsystem for Linux available for Unix tools
|
|
120
|
+
- Visual Studio tools: cl, msbuild for C++ compilation`;
|
|
121
|
+
}
|
|
122
|
+
else if (isMacOS) {
|
|
123
|
+
return `
|
|
124
|
+
COMMON MACOS DEVELOPMENT TOOLS:
|
|
125
|
+
- Xcode Command Line Tools: Required for many development tools
|
|
126
|
+
- Homebrew: Primary package manager for development tools
|
|
127
|
+
- Python: Usually python3, check if python points to Python 2
|
|
128
|
+
- Node.js: Available via brew or direct installer
|
|
129
|
+
- Ruby: System Ruby available, rbenv/rvm for version management`;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
return `
|
|
133
|
+
COMMON LINUX DEVELOPMENT TOOLS:
|
|
134
|
+
- Package managers: Install tools via distribution package manager
|
|
135
|
+
- Python: Usually python3, python may point to Python 2
|
|
136
|
+
- Node.js: Available via package manager or NodeSource repository
|
|
137
|
+
- Build tools: gcc, make typically available or easily installed
|
|
138
|
+
- Container tools: docker, podman common for development`;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Get path guidance (simplified since paths are normalized)
|
|
143
|
+
*/
|
|
144
|
+
export function getPathGuidance(systemInfo) {
|
|
145
|
+
return `Always use absolute paths for reliability. Paths are automatically normalized regardless of slash direction.`;
|
|
146
|
+
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.2.
|
|
1
|
+
export declare const VERSION = "0.2.3";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.2.
|
|
1
|
+
export const VERSION = '0.2.3';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wonderwhy-er/desktop-commander",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "MCP server for terminal operations and file editing",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Eduards Ruzga",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"setup:debug": "npm install && npm run build && node setup-claude-server.js --debug",
|
|
33
33
|
"prepare": "npm run build",
|
|
34
34
|
"test": "node test/run-all-tests.js",
|
|
35
|
+
"test:debug": "node --inspect test/run-all-tests.js",
|
|
35
36
|
"link:local": "npm run build && npm link",
|
|
36
37
|
"unlink:local": "npm unlink",
|
|
37
38
|
"inspector": "npx @modelcontextprotocol/inspector dist/index.js",
|