@gotza02/sequential-thinking 2026.2.30 → 2026.2.32

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.
@@ -2,15 +2,114 @@ import { z } from "zod";
2
2
  import * as fs from 'fs/promises';
3
3
  import * as path from 'path';
4
4
  import { execAsync, validatePath } from "../utils.js";
5
+ /**
6
+ * Default file extensions to search
7
+ * Covers common programming languages, config files, and text formats
8
+ */
9
+ const DEFAULT_SEARCH_EXTENSIONS = [
10
+ // Programming languages
11
+ 'ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs',
12
+ 'py', 'pyw', 'pyi',
13
+ 'java', 'kt', 'kts', 'scala',
14
+ 'c', 'cpp', 'cc', 'cxx', 'h', 'hpp', 'hxx',
15
+ 'cs', 'vb',
16
+ 'go', 'rs',
17
+ 'php', 'phtml',
18
+ 'rb', 'gemspec',
19
+ 'swift', 'm', 'mm',
20
+ 'dart',
21
+ 'lua', 'pl', 'pm',
22
+ 'r', 'R',
23
+ 'sh', 'bash', 'zsh', 'fish', 'ps1',
24
+ // Markup & Data
25
+ 'html', 'htm', 'xhtml', 'xml', 'svg',
26
+ 'css', 'scss', 'sass', 'less',
27
+ 'json', 'yaml', 'yml', 'toml', 'ini', 'conf', 'cfg',
28
+ 'md', 'markdown', 'rst', 'txt',
29
+ 'vue', 'svelte', 'jsx',
30
+ // Config & Build
31
+ 'gradle', 'pom', 'xml', // Java build
32
+ 'cabal', // Haskell
33
+ 'csproj', 'sln', // C#
34
+ 'pro', // Lua
35
+ 'gemfile', 'rake', // Ruby
36
+ 'dockerfile', 'makefile', 'cmake',
37
+ 'env', 'env.example',
38
+ ];
39
+ /**
40
+ * Default directories to skip during search
41
+ */
42
+ const DEFAULT_SKIP_DIRS = [
43
+ 'node_modules',
44
+ '.git',
45
+ '.svn',
46
+ '.hg',
47
+ 'dist',
48
+ 'build',
49
+ 'out',
50
+ 'coverage',
51
+ '.nyc_output',
52
+ '.gemini',
53
+ '.next',
54
+ '.nuxt',
55
+ '.cache',
56
+ '.turbo',
57
+ '.parcel',
58
+ '.vscode',
59
+ '.idea',
60
+ 'vendor',
61
+ 'target',
62
+ 'bin',
63
+ 'obj',
64
+ '__pycache__',
65
+ 'venv',
66
+ '.venv',
67
+ 'virtualenv',
68
+ 'node_modules_cache',
69
+ '.tsbuildinfo',
70
+ 'tmp',
71
+ 'temp',
72
+ '.tmp'
73
+ ];
74
+ /**
75
+ * Binary file extensions to always skip
76
+ */
77
+ const BINARY_EXTENSIONS = [
78
+ 'png', 'jpg', 'jpeg', 'gif', 'bmp', 'ico', 'webp', 'svg',
79
+ 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
80
+ 'zip', 'tar', 'gz', 'bz2', 'rar', '7z',
81
+ 'exe', 'dll', 'so', 'dylib', 'lib', 'a',
82
+ 'mp3', 'mp4', 'wav', 'ogg', 'flac',
83
+ 'ttf', 'otf', 'woff', 'woff2', 'eot',
84
+ 'class', 'jar', 'war', 'ear',
85
+ 'pyc', 'pyo',
86
+ 'node'
87
+ ];
5
88
  export function registerFileSystemTools(server) {
6
- // 3. shell_execute
89
+ // =========================================================================
90
+ // SHELL EXECUTE
91
+ // =========================================================================
7
92
  server.tool("shell_execute", "Execute a shell command. SECURITY WARNING: Use this ONLY for safe, non-destructive commands. Avoid 'rm -rf /', format, or destructive operations.", {
8
93
  command: z.string().describe("The bash command to execute")
9
94
  }, async ({ command }) => {
10
- const dangerousPatterns = [new RegExp('rm\\s+-rf\\s+\\/'), /mkfs/, /dd\s+if=/];
95
+ // Dangerous command patterns to block
96
+ const dangerousPatterns = [
97
+ new RegExp('rm\\s+-rf?\\s+[\\/~]'), // rm -rf /, rm -rf ~
98
+ new RegExp('rm\\s+-rf?\\s+\\.'), // rm -rf .
99
+ /mkfs/, // Format filesystem
100
+ /dd\s+if=/, // dd direct disk write
101
+ /chmod\s+000\s+\//, // chmod 000 /
102
+ /chown\s+-R\s+root:/, // chown -R root:
103
+ new RegExp('.*>.*\\\\/dev\\\\/.*\\\\s+\\\\w'), // Redirect to device with write
104
+ /fdisk/, // Partition manipulation
105
+ /format/, // Windows format command
106
+ ];
11
107
  if (dangerousPatterns.some(p => p.test(command))) {
12
108
  return {
13
- content: [{ type: "text", text: "Error: Dangerous command pattern detected. Execution blocked for safety." }],
109
+ content: [{
110
+ type: "text",
111
+ text: "Error: Dangerous command pattern detected. Execution blocked for safety."
112
+ }],
14
113
  isError: true
15
114
  };
16
115
  }
@@ -25,12 +124,17 @@ export function registerFileSystemTools(server) {
25
124
  }
26
125
  catch (error) {
27
126
  return {
28
- content: [{ type: "text", text: `Shell Error: ${error instanceof Error ? error.message : String(error)}` }],
127
+ content: [{
128
+ type: "text",
129
+ text: `Shell Error: ${error instanceof Error ? error.message : String(error)}`
130
+ }],
29
131
  isError: true
30
132
  };
31
133
  }
32
134
  });
33
- // 4. read_file
135
+ // =========================================================================
136
+ // READ FILE
137
+ // =========================================================================
34
138
  server.tool("read_file", "Read the contents of a file.", {
35
139
  path: z.string().describe("Path to the file")
36
140
  }, async ({ path }) => {
@@ -43,64 +147,150 @@ export function registerFileSystemTools(server) {
43
147
  }
44
148
  catch (error) {
45
149
  return {
46
- content: [{ type: "text", text: `Read Error: ${error instanceof Error ? error.message : String(error)}` }],
150
+ content: [{
151
+ type: "text",
152
+ text: `Read Error: ${error instanceof Error ? error.message : String(error)}`
153
+ }],
47
154
  isError: true
48
155
  };
49
156
  }
50
157
  });
51
- // 5. write_file
52
- server.tool("write_file", "Write content to a file (overwrites existing).", {
158
+ // =========================================================================
159
+ // WRITE FILE
160
+ // =========================================================================
161
+ server.tool("write_file", "Write content to a file (overwrites existing). Use with caution.", {
53
162
  path: z.string().describe("Path to the file"),
54
163
  content: z.string().describe("Content to write")
55
164
  }, async ({ path, content }) => {
56
165
  try {
57
166
  const safePath = validatePath(path);
167
+ // Additional safety: don't overwrite system directories
168
+ const systemDirs = ['/etc', '/usr', '/bin', '/sbin', '/boot', '/lib', '/lib64'];
169
+ const isSystemPath = systemDirs.some(sysDir => safePath.startsWith(sysDir));
170
+ if (isSystemPath) {
171
+ return {
172
+ content: [{
173
+ type: "text",
174
+ text: `Error: Cannot write to system directory: ${safePath}`
175
+ }],
176
+ isError: true
177
+ };
178
+ }
58
179
  await fs.writeFile(safePath, content, 'utf-8');
59
180
  return {
60
- content: [{ type: "text", text: `Successfully wrote to ${safePath}` }]
181
+ content: [{
182
+ type: "text",
183
+ text: `Successfully wrote to ${safePath} (${content.length} bytes)`
184
+ }]
61
185
  };
62
186
  }
63
187
  catch (error) {
64
188
  return {
65
- content: [{ type: "text", text: `Write Error: ${error instanceof Error ? error.message : String(error)}` }],
189
+ content: [{
190
+ type: "text",
191
+ text: `Write Error: ${error instanceof Error ? error.message : String(error)}`
192
+ }],
66
193
  isError: true
67
194
  };
68
195
  }
69
196
  });
70
- // 10. search_code
71
- server.tool("search_code", "Search for a text pattern in project files with advanced options (regex, case sensitivity).", {
197
+ // =========================================================================
198
+ // SEARCH CODE
199
+ // =========================================================================
200
+ server.tool("search_code", "Search for a text pattern in project files with advanced options (regex, case sensitivity, file filtering).", {
72
201
  pattern: z.string().describe("The text or regex pattern to search for"),
73
202
  path: z.string().optional().default('.').describe("Root directory to search"),
74
203
  useRegex: z.boolean().optional().default(false).describe("Treat pattern as a regular expression"),
75
- caseSensitive: z.boolean().optional().default(false).describe("Match case sensitive (default: false)")
76
- }, async ({ pattern, path: searchPath, useRegex, caseSensitive }) => {
204
+ caseSensitive: z.boolean().optional().default(false).describe("Match case sensitive"),
205
+ extensions: z.string().optional().describe("Comma-separated file extensions to search (e.g., 'ts,js,py'). Default: common code files"),
206
+ excludeExtensions: z.string().optional().describe("Comma-separated extensions to exclude (e.g., 'md,txt')"),
207
+ skipDirs: z.string().optional().describe("Comma-separated directories to skip (e.g., 'dist,test')"),
208
+ allFiles: z.boolean().optional().default(false).describe("Search all text files (ignores extension filter)"),
209
+ contextLines: z.number().optional().describe("Number of context lines to show around matches"),
210
+ maxResults: z.number().optional().default(1000).describe("Maximum number of matches to return")
211
+ }, async ({ pattern, path: searchPath, useRegex, caseSensitive, extensions, excludeExtensions, skipDirs, allFiles, contextLines, maxResults }) => {
77
212
  try {
78
213
  const resolvedPath = validatePath(searchPath || '.');
79
214
  const stats = await fs.stat(resolvedPath);
80
215
  const results = [];
216
+ let matchCount = 0;
217
+ // Parse extensions
218
+ const includedExts = extensions
219
+ ? extensions.toLowerCase().split(',').map(e => e.trim().replace(/^\./, ''))
220
+ : DEFAULT_SEARCH_EXTENSIONS;
221
+ const excludedExts = excludeExtensions
222
+ ? excludeExtensions.toLowerCase().split(',').map(e => e.trim().replace(/^\./, ''))
223
+ : BINARY_EXTENSIONS;
224
+ const skipDirList = skipDirs
225
+ ? skipDirs.split(',').map(d => d.trim())
226
+ : DEFAULT_SKIP_DIRS;
227
+ const shouldSearchFile = (fileName) => {
228
+ const ext = fileName.includes('.') ? fileName.split('.').pop()?.toLowerCase() : '';
229
+ const baseName = fileName.split('/').pop() || '';
230
+ // Always skip binary extensions
231
+ if (ext && excludedExts.includes(ext)) {
232
+ return false;
233
+ }
234
+ // Skip common binary/temp files by name
235
+ if (baseName.startsWith('.') || baseName.endsWith('~')) {
236
+ return false;
237
+ }
238
+ if (allFiles) {
239
+ // For all files, still skip known binary types
240
+ return !BINARY_EXTENSIONS.includes(ext || '');
241
+ }
242
+ // Use extension filter
243
+ return ext ? includedExts.includes(ext) : false;
244
+ };
245
+ const shouldSkipDir = (dirName) => {
246
+ return skipDirList.includes(dirName);
247
+ };
81
248
  const searchFile = async (filePath) => {
249
+ if (matchCount >= maxResults)
250
+ return;
82
251
  try {
83
252
  const content = await fs.readFile(filePath, 'utf-8');
253
+ // Skip binary-like files (high proportion of non-printable chars)
254
+ const nonPrintableRatio = (content.match(/[\x00-\x08\x0E-\x1F]/g) || []).length / content.length;
255
+ if (nonPrintableRatio > 0.3) {
256
+ return; // Likely binary file
257
+ }
84
258
  const lines = content.split('\n');
85
259
  let regex;
86
260
  if (useRegex) {
87
261
  regex = new RegExp(pattern, caseSensitive ? 'g' : 'gi');
88
262
  }
89
263
  else {
90
- // Escape regex special chars if not using regex
91
- const escaped = pattern.replace(/[.*+?^${}()|[\\]/g, '\\$&');
264
+ const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
92
265
  regex = new RegExp(escaped, caseSensitive ? 'g' : 'gi');
93
266
  }
94
267
  lines.forEach((line, index) => {
268
+ if (matchCount >= maxResults)
269
+ return;
270
+ regex.lastIndex = 0;
95
271
  if (regex.test(line)) {
96
- // Reset lastIndex if global (not strictly needed for test() but good practice)
97
- regex.lastIndex = 0;
98
- results.push(`${filePath}:${index + 1}: ${line.trim()}`);
272
+ matchCount++;
273
+ const lineNum = index + 1;
274
+ const trimmedLine = line.trim();
275
+ if (contextLines && contextLines > 0) {
276
+ // Include context lines
277
+ const start = Math.max(0, index - contextLines);
278
+ const end = Math.min(lines.length - 1, index + contextLines);
279
+ let context = '';
280
+ for (let i = start; i <= end; i++) {
281
+ const prefix = i === index ? '>' : ' ';
282
+ context += `${prefix} ${i + 1}: ${lines[i]}\n`;
283
+ }
284
+ results.push(`${filePath}:\n${context}`);
285
+ }
286
+ else {
287
+ results.push(`${filePath}:${lineNum}: ${trimmedLine}`);
288
+ }
99
289
  }
100
290
  });
101
291
  }
102
292
  catch (err) {
103
- // Ignore read errors (binary files etc)
293
+ // Ignore read errors (binary files, permissions, etc.)
104
294
  }
105
295
  };
106
296
  if (stats.isFile()) {
@@ -108,73 +298,202 @@ export function registerFileSystemTools(server) {
108
298
  }
109
299
  else {
110
300
  const searchDir = async (dir) => {
111
- const entries = await fs.readdir(dir, { withFileTypes: true });
112
- for (const entry of entries) {
113
- const fullPath = path.join(dir, entry.name);
114
- if (entry.isDirectory()) {
115
- if (['node_modules', '.git', 'dist', 'coverage', '.gemini'].includes(entry.name))
116
- continue;
117
- await searchDir(fullPath);
118
- }
119
- else if (/\.(ts|js|json|md|txt|html|css|py|java|c|cpp|h|rs|go|sh|yaml|yml)$/.test(entry.name)) {
120
- await searchFile(fullPath);
301
+ try {
302
+ const entries = await fs.readdir(dir, { withFileTypes: true });
303
+ for (const entry of entries) {
304
+ if (matchCount >= maxResults)
305
+ break;
306
+ const fullPath = path.join(dir, entry.name);
307
+ if (entry.isDirectory()) {
308
+ if (shouldSkipDir(entry.name))
309
+ continue;
310
+ await searchDir(fullPath);
311
+ }
312
+ else if (entry.isFile()) {
313
+ if (shouldSearchFile(entry.name)) {
314
+ await searchFile(fullPath);
315
+ }
316
+ }
121
317
  }
122
318
  }
319
+ catch (err) {
320
+ // Skip directories we can't read
321
+ }
123
322
  };
124
323
  await searchDir(resolvedPath);
125
324
  }
325
+ const responseText = results.length > 0
326
+ ? `Found ${Math.min(matchCount, maxResults)} match${matchCount > 1 ? 'es' : ''} for "${pattern}":\n\n${results.slice(0, maxResults).join('\n\n')}`
327
+ : `No matches found for "${pattern}"`;
126
328
  return {
127
329
  content: [{
128
330
  type: "text",
129
- text: results.length > 0 ? `Found matches for "${pattern}":\n${results.join('\n')}` : `No matches found for "${pattern}"`
331
+ text: responseText
130
332
  }]
131
333
  };
132
334
  }
133
335
  catch (error) {
134
336
  return {
135
- content: [{ type: "text", text: `Search Error: ${error instanceof Error ? error.message : String(error)}` }],
337
+ content: [{
338
+ type: "text",
339
+ text: `Search Error: ${error instanceof Error ? error.message : String(error)}`
340
+ }],
136
341
  isError: true
137
342
  };
138
343
  }
139
344
  });
140
- // 13. edit_file
345
+ // =========================================================================
346
+ // EDIT FILE
347
+ // =========================================================================
141
348
  server.tool("edit_file", "Replace a specific string in a file with a new string. Use this for surgical edits to avoid overwriting the whole file.", {
142
349
  path: z.string().describe("Path to the file"),
143
350
  oldText: z.string().describe("The exact text segment to replace"),
144
351
  newText: z.string().describe("The new text to insert"),
145
- allowMultiple: z.boolean().optional().default(false).describe("Allow replacing multiple occurrences (default: false)")
352
+ allowMultiple: z.boolean().optional().default(false).describe("Allow replacing multiple occurrences")
146
353
  }, async ({ path, oldText, newText, allowMultiple }) => {
147
354
  try {
148
355
  const safePath = validatePath(path);
356
+ // Additional safety: don't edit system files
357
+ const systemDirs = ['/etc', '/usr', '/bin', '/sbin', '/boot', '/lib', '/lib64'];
358
+ const isSystemPath = systemDirs.some(sysDir => safePath.startsWith(sysDir));
359
+ if (isSystemPath) {
360
+ return {
361
+ content: [{
362
+ type: "text",
363
+ text: `Error: Cannot edit system directory file: ${safePath}`
364
+ }],
365
+ isError: true
366
+ };
367
+ }
149
368
  const content = await fs.readFile(safePath, 'utf-8');
150
369
  // Check occurrences
151
- // Escape special regex characters in oldText to treat it as literal string
152
- const escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\\]/g, '\\$&');
370
+ const escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
153
371
  const regex = new RegExp(escapeRegExp(oldText), 'g');
154
372
  const matchCount = (content.match(regex) || []).length;
155
373
  if (matchCount === 0) {
156
374
  return {
157
- content: [{ type: "text", text: "Error: 'oldText' not found in the file. Please ensure exact matching (including whitespace/indentation)." }],
375
+ content: [{
376
+ type: "text",
377
+ text: "Error: 'oldText' not found in the file. Please ensure exact matching (including whitespace/indentation)."
378
+ }],
158
379
  isError: true
159
380
  };
160
381
  }
161
382
  if (matchCount > 1 && !allowMultiple) {
162
383
  return {
163
- content: [{ type: "text", text: `Error: Found ${matchCount} occurrences of 'oldText'. Set 'allowMultiple' to true if you intend to replace all, or provide more unique context in 'oldText'.` }],
384
+ content: [{
385
+ type: "text",
386
+ text: `Error: Found ${matchCount} occurrences of 'oldText'. Set 'allowMultiple' to true if you intend to replace all, or provide more unique context in 'oldText'.\n\nTip: Include more surrounding context to make the match unique.`
387
+ }],
164
388
  isError: true
165
389
  };
166
390
  }
167
391
  const newContent = content.replace(allowMultiple ? regex : oldText, () => newText);
168
392
  await fs.writeFile(safePath, newContent, 'utf-8');
169
393
  return {
170
- content: [{ type: "text", text: `Successfully replaced ${allowMultiple ? matchCount : 1} occurrence(s) in ${safePath}` }]
394
+ content: [{
395
+ type: "text",
396
+ text: `Successfully replaced ${allowMultiple ? matchCount : 1} occurrence(s) in ${safePath}\n\nSize changed: ${content.length} → ${newContent.length} bytes`
397
+ }]
398
+ };
399
+ }
400
+ catch (error) {
401
+ return {
402
+ content: [{
403
+ type: "text",
404
+ text: `Edit Error: ${error instanceof Error ? error.message : String(error)}`
405
+ }],
406
+ isError: true
407
+ };
408
+ }
409
+ });
410
+ // =========================================================================
411
+ // LIST DIRECTORY
412
+ // =========================================================================
413
+ server.tool("list_directory", "List files and directories in a given path.", {
414
+ path: z.string().optional().default('.').describe("Path to the directory"),
415
+ showHidden: z.boolean().optional().default(false).describe("Show hidden files/directories"),
416
+ includeStats: z.boolean().optional().default(false).describe("Include file statistics (size, mtime)")
417
+ }, async ({ path: dirPath, showHidden, includeStats }) => {
418
+ try {
419
+ const safePath = validatePath(dirPath || '.');
420
+ const stats = await fs.stat(safePath);
421
+ if (!stats.isDirectory()) {
422
+ return {
423
+ content: [{
424
+ type: "text",
425
+ text: `Error: ${safePath} is not a directory`
426
+ }],
427
+ isError: true
428
+ };
429
+ }
430
+ const entries = await fs.readdir(safePath, { withFileTypes: true });
431
+ let result = '';
432
+ for (const entry of entries) {
433
+ // Skip hidden files unless requested
434
+ if (!showHidden && entry.name.startsWith('.')) {
435
+ continue;
436
+ }
437
+ const fullPath = path.join(safePath, entry.name);
438
+ const icon = entry.isDirectory() ? '📁 ' : entry.isSymbolicLink() ? '🔗 ' : '📄 ';
439
+ let line = `${icon}${entry.name}`;
440
+ if (includeStats) {
441
+ try {
442
+ const entryStats = await fs.stat(fullPath);
443
+ const size = entryStats.size;
444
+ const mtime = entryStats.mtime.toISOString();
445
+ line += ` (${size} bytes, ${mtime})`;
446
+ }
447
+ catch {
448
+ // Skip stats for files we can't read
449
+ }
450
+ }
451
+ result += line + '\n';
452
+ }
453
+ return {
454
+ content: [{
455
+ type: "text",
456
+ text: result || `(empty directory)`
457
+ }]
171
458
  };
172
459
  }
173
460
  catch (error) {
174
461
  return {
175
- content: [{ type: "text", text: `Edit Error: ${error instanceof Error ? error.message : String(error)}` }],
462
+ content: [{
463
+ type: "text",
464
+ text: `List Error: ${error instanceof Error ? error.message : String(error)}`
465
+ }],
176
466
  isError: true
177
467
  };
178
468
  }
179
469
  });
470
+ // =========================================================================
471
+ // FILE EXISTS
472
+ // =========================================================================
473
+ server.tool("file_exists", "Check if a file or directory exists.", {
474
+ path: z.string().describe("Path to check")
475
+ }, async ({ path }) => {
476
+ try {
477
+ const safePath = validatePath(path);
478
+ await fs.access(safePath);
479
+ const stats = await fs.stat(safePath);
480
+ const type = stats.isDirectory() ? 'directory' :
481
+ stats.isFile() ? 'file' :
482
+ stats.isSymbolicLink() ? 'symlink' : 'unknown';
483
+ return {
484
+ content: [{
485
+ type: "text",
486
+ text: `Exists: ${safePath} (${type})`
487
+ }]
488
+ };
489
+ }
490
+ catch {
491
+ return {
492
+ content: [{
493
+ type: "text",
494
+ text: `Does not exist: ${path}`
495
+ }]
496
+ };
497
+ }
498
+ });
180
499
  }
@@ -66,7 +66,13 @@ If stuck in a loop:
66
66
  1. Do NOT continue linear thoughts
67
67
  2. Create a NEW BRANCH ('branchFromThought') from before the error
68
68
  3. State "Stuck detected, branching to explore Approach B"
69
- 4. Change your assumptions completely`, {
69
+ 4. Change your assumptions completely
70
+
71
+ THE RULE OF 3:
72
+ If you have tried to fix the same problem 3 times and failed:
73
+ 1. STOP immediately.
74
+ 2. Do not attempt a 4th fix in the same chain.
75
+ 3. Branch back to the analysis phase before the first fix attempt.`, {
70
76
  thought: z.string().describe("Your current thinking step"),
71
77
  thoughtType: z.enum([
72
78
  'analysis', 'planning', 'execution', 'observation',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotza02/sequential-thinking",
3
- "version": "2026.2.30",
3
+ "version": "2026.2.32",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },