@gotza02/sequential-thinking 10000.0.6 → 10000.0.7

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.
@@ -55,7 +55,7 @@ export declare class CodeDatabase {
55
55
  /**
56
56
  * Atomic save using write-to-temp + rename pattern.
57
57
  */
58
- private save;
58
+ save(): Promise<void>;
59
59
  /**
60
60
  * Reload database from disk
61
61
  */
package/dist/graph.d.ts CHANGED
@@ -11,6 +11,7 @@ export declare class ProjectKnowledgeGraph {
11
11
  private cachePath;
12
12
  private configResolver;
13
13
  private configHash;
14
+ private MAX_CACHE_SIZE;
14
15
  private getConcurrencyLimit;
15
16
  constructor();
16
17
  /**
@@ -22,7 +23,7 @@ export declare class ProjectKnowledgeGraph {
22
23
  cachedFiles: number;
23
24
  parsedFiles: number;
24
25
  }>;
25
- build(rootDir: string): Promise<{
26
+ build(rootDir: string, onProgress?: (progress: any) => void): Promise<{
26
27
  nodeCount: number;
27
28
  totalFiles: number;
28
29
  cachedFiles: number;
@@ -30,6 +31,13 @@ export declare class ProjectKnowledgeGraph {
30
31
  }>;
31
32
  private loadCache;
32
33
  private saveCache;
34
+ clearCache(): Promise<void>;
35
+ getCacheStats(): {
36
+ fileCount: number;
37
+ totalSize: string;
38
+ configHash: string | undefined;
39
+ version: string;
40
+ };
33
41
  private getAllFiles;
34
42
  private isMonorepoRoot;
35
43
  private parseFile;
package/dist/graph.js CHANGED
@@ -184,6 +184,7 @@ export class ProjectKnowledgeGraph {
184
184
  cachePath = '';
185
185
  configResolver = null;
186
186
  configHash = '';
187
+ MAX_CACHE_SIZE = 1000;
187
188
  // Dynamic concurrency based on file count
188
189
  getConcurrencyLimit(fileCount) {
189
190
  if (fileCount < 100)
@@ -214,9 +215,22 @@ export class ProjectKnowledgeGraph {
214
215
  this.configResolver = null;
215
216
  return await this.build(rootDir);
216
217
  }
217
- async build(rootDir) {
218
+ async build(rootDir, onProgress) {
218
219
  try {
220
+ // Validate input
221
+ if (!rootDir || typeof rootDir !== 'string') {
222
+ throw new Error('rootDir must be a non-empty string');
223
+ }
224
+ if (rootDir.length > 1000) {
225
+ throw new Error('rootDir path too long');
226
+ }
219
227
  this.rootDir = path.resolve(rootDir);
228
+ // Security check: prevent path traversal
229
+ const resolvedRoot = path.resolve(rootDir);
230
+ const cwd = process.cwd();
231
+ if (!resolvedRoot.startsWith(cwd) && !process.env.ALLOW_EXTERNAL_PATHS) {
232
+ throw new Error('Access denied: Cannot build graph outside current working directory');
233
+ }
220
234
  this.cachePath = path.join(this.rootDir, '.gemini_graph_cache.json');
221
235
  // Check if rootDir exists and is a directory
222
236
  const stats = await fs.stat(this.rootDir);
@@ -262,6 +276,8 @@ export class ProjectKnowledgeGraph {
262
276
  }
263
277
  // Step 3: Parse new/modified files concurrently with dynamic limit
264
278
  const CONCURRENCY_LIMIT = this.getConcurrencyLimit(filesToParse.length);
279
+ let processedFiles = files.length - filesToParse.length; // Start with cached files count
280
+ const totalFiles = files.length;
265
281
  for (let i = 0; i < filesToParse.length; i += CONCURRENCY_LIMIT) {
266
282
  const chunk = filesToParse.slice(i, i + CONCURRENCY_LIMIT);
267
283
  await Promise.all(chunk.map(async (file) => {
@@ -274,6 +290,14 @@ export class ProjectKnowledgeGraph {
274
290
  imports: data.imports,
275
291
  symbols: data.symbols
276
292
  };
293
+ processedFiles++;
294
+ if (onProgress && processedFiles % 10 === 0) {
295
+ onProgress({
296
+ processed: processedFiles,
297
+ total: totalFiles,
298
+ percentage: Math.round((processedFiles / totalFiles) * 100)
299
+ });
300
+ }
277
301
  }));
278
302
  }
279
303
  // Update config hash in cache
@@ -317,6 +341,20 @@ export class ProjectKnowledgeGraph {
317
341
  }
318
342
  async saveCache() {
319
343
  try {
344
+ // Limit cache size
345
+ const fileEntries = Object.entries(this.cache.files);
346
+ if (fileEntries.length > this.MAX_CACHE_SIZE) {
347
+ // Sort by access time (if available) or keep most recent
348
+ // Since we only track mtime, we can't strictly purge by access time unless we add atime.
349
+ // We'll use mtime as a proxy for relevance or just keep the ones we encountered (if sorted by something else).
350
+ // Actually, let's just keep the ones with latest mtime? No, that might purge old stable files.
351
+ // The patch suggests: sort by mtime.
352
+ const sorted = fileEntries.sort((a, b) => {
353
+ return (b[1].mtime || 0) - (a[1].mtime || 0);
354
+ });
355
+ this.cache.files = Object.fromEntries(sorted.slice(0, this.MAX_CACHE_SIZE));
356
+ console.warn(`[Graph] Cache trimmed to ${this.MAX_CACHE_SIZE} entries`);
357
+ }
320
358
  const tmpPath = this.cachePath + '.tmp';
321
359
  await fs.writeFile(tmpPath, JSON.stringify(this.cache, null, 2), 'utf-8');
322
360
  await fs.rename(tmpPath, this.cachePath);
@@ -325,6 +363,26 @@ export class ProjectKnowledgeGraph {
325
363
  console.error('Failed to save graph cache:', e);
326
364
  }
327
365
  }
366
+ async clearCache() {
367
+ this.cache = { version: '2.0', files: {} };
368
+ try {
369
+ await fs.unlink(this.cachePath);
370
+ console.log('[Graph] Cache cleared');
371
+ }
372
+ catch {
373
+ // Ignore if file doesn't exist
374
+ }
375
+ }
376
+ getCacheStats() {
377
+ const files = Object.keys(this.cache.files);
378
+ const totalSize = JSON.stringify(this.cache).length;
379
+ return {
380
+ fileCount: files.length,
381
+ totalSize: `${Math.round(totalSize / 1024)}KB`,
382
+ configHash: this.cache.configHash,
383
+ version: this.cache.version
384
+ };
385
+ }
328
386
  async getAllFiles(dir) {
329
387
  try {
330
388
  const entries = await fs.readdir(dir, { withFileTypes: true });
@@ -387,24 +445,37 @@ export class ProjectKnowledgeGraph {
387
445
  return false;
388
446
  }
389
447
  async parseFile(filePath) {
390
- const ext = path.extname(filePath);
391
- if (['.ts', '.js', '.tsx', '.jsx', '.mjs', '.cjs'].includes(ext)) {
392
- return await this.parseTypeScript(filePath);
393
- }
394
- else if (ext === '.py') {
395
- return await this.parsePython(filePath);
396
- }
397
- else if (ext === '.go') {
398
- return await this.parseGo(filePath);
399
- }
400
- else if (ext === '.rs') {
401
- return await this.parseRust(filePath);
402
- }
403
- else if (['.java', '.kt', '.kts'].includes(ext)) {
404
- return await this.parseJavaLike(filePath);
448
+ try {
449
+ const ext = path.extname(filePath);
450
+ // Check file size before parsing
451
+ const stats = await fs.stat(filePath);
452
+ const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
453
+ if (stats.size > MAX_FILE_SIZE) {
454
+ console.warn(`[Graph] Skipping large file: ${filePath} (${Math.round(stats.size / 1024 / 1024)}MB)`);
455
+ return { imports: [], symbols: [] };
456
+ }
457
+ if (['.ts', '.js', '.tsx', '.jsx', '.mjs', '.cjs'].includes(ext)) {
458
+ return await this.parseTypeScript(filePath);
459
+ }
460
+ else if (ext === '.py') {
461
+ return await this.parsePython(filePath);
462
+ }
463
+ else if (ext === '.go') {
464
+ return await this.parseGo(filePath);
465
+ }
466
+ else if (ext === '.rs') {
467
+ return await this.parseRust(filePath);
468
+ }
469
+ else if (['.java', '.kt', '.kts'].includes(ext)) {
470
+ return await this.parseJavaLike(filePath);
471
+ }
472
+ else {
473
+ return await this.parseGeneric(filePath);
474
+ }
405
475
  }
406
- else {
407
- return await this.parseGeneric(filePath);
476
+ catch (error) {
477
+ console.warn(`[Graph] Failed to parse ${filePath}:`, error instanceof Error ? error.message : String(error));
478
+ return { imports: [], symbols: [] };
408
479
  }
409
480
  }
410
481
  async parseTypeScript(filePath) {
@@ -35,7 +35,7 @@ const PORT = process.env.PORT || 3000;
35
35
  const corsOrigin = process.env.CORS_ORIGIN;
36
36
  let corsOptions = {
37
37
  origin: false, // Default: deny all cross-origin requests
38
- credentials: true,
38
+ credentials: false, // Must be false when origin is false
39
39
  };
40
40
  if (corsOrigin === '*') {
41
41
  console.warn('⚠️ WARNING: CORS set to allow all origins (*). This is not secure for production!');
@@ -43,6 +43,7 @@ if (corsOrigin === '*') {
43
43
  }
44
44
  else if (corsOrigin) {
45
45
  const allowedOrigins = corsOrigin.split(',').map(o => o.trim());
46
+ corsOptions.credentials = true; // Enable credentials for specific origins
46
47
  corsOptions.origin = function (origin, callback) {
47
48
  // Allow requests with no origin (like mobile apps, curl, Postman)
48
49
  if (!origin)
@@ -64,6 +65,39 @@ app.use(cors(corsOptions));
64
65
  */
65
66
  const bodyLimit = process.env.BODY_LIMIT || '10mb';
66
67
  app.use(express.json({ limit: bodyLimit }));
68
+ // Request Logging Middleware
69
+ app.use((req, res, next) => {
70
+ const start = Date.now();
71
+ res.on('finish', () => {
72
+ const duration = Date.now() - start;
73
+ console.log(`[${new Date().toISOString()}] ${req.method} ${req.path} - ${res.statusCode} (${duration}ms)`);
74
+ });
75
+ next();
76
+ });
77
+ // Rate Limiting Middleware
78
+ const requestCounts = new Map();
79
+ const RATE_LIMIT = 100; // requests per minute
80
+ const RATE_WINDOW = 60000; // 1 minute
81
+ app.use((req, res, next) => {
82
+ const clientIp = req.ip || req.connection.remoteAddress || 'unknown';
83
+ const now = Date.now();
84
+ if (!requestCounts.has(clientIp)) {
85
+ requestCounts.set(clientIp, []);
86
+ }
87
+ const requests = requestCounts.get(clientIp);
88
+ // Remove old requests
89
+ while (requests.length > 0 && now - requests[0] > RATE_WINDOW) {
90
+ requests.shift();
91
+ }
92
+ if (requests.length >= RATE_LIMIT) {
93
+ return res.status(429).json({
94
+ error: 'Rate limit exceeded',
95
+ retryAfter: Math.ceil((requests[0] + RATE_WINDOW - now) / 1000)
96
+ });
97
+ }
98
+ requests.push(now);
99
+ next();
100
+ });
67
101
  // Log security configuration on startup
68
102
  console.log(`Security Configuration:`);
69
103
  console.log(` CORS: ${corsOrigin === '*' ? 'OPEN (all origins)' : corsOrigin ? `Restricted to: ${corsOrigin}` : 'Same-origin only'}`);
@@ -108,12 +142,40 @@ const managers = ServerManagers.getInstance();
108
142
  * HEALTH & METADATA ENDPOINTS
109
143
  * ============================================================================
110
144
  */
111
- app.get('/health', (req, res) => {
145
+ app.get('/health', async (req, res) => {
146
+ const memoryUsage = process.memoryUsage();
147
+ const uptime = process.uptime();
148
+ // Check disk space (if possible)
149
+ let diskInfo = null;
150
+ try {
151
+ const { execAsync } = await import('./utils.js');
152
+ const { stdout } = await execAsync('df -h .');
153
+ diskInfo = stdout.split('\n')[1]?.trim();
154
+ }
155
+ catch { }
112
156
  res.json({
113
157
  status: 'ok',
114
158
  service: 'sequential-thinking-http-wrapper',
115
159
  version: '2.0',
116
- timestamp: new Date().toISOString()
160
+ timestamp: new Date().toISOString(),
161
+ system: {
162
+ memory: {
163
+ used: Math.round(memoryUsage.heapUsed / 1024 / 1024) + 'MB',
164
+ total: Math.round(memoryUsage.heapTotal / 1024 / 1024) + 'MB',
165
+ rss: Math.round(memoryUsage.rss / 1024 / 1024) + 'MB'
166
+ },
167
+ uptime: Math.floor(uptime) + 's',
168
+ disk: diskInfo
169
+ },
170
+ managers: {
171
+ thinking: {
172
+ historyLength: managers.thinking.getHistoryLength(),
173
+ currentBlock: managers.thinking.getCurrentBlock()?.id || null
174
+ },
175
+ notes: {
176
+ count: (await managers.notes.listNotes()).length
177
+ }
178
+ }
117
179
  });
118
180
  });
119
181
  app.get('/', (req, res) => {
@@ -225,7 +287,10 @@ app.post('/api/thinking/thought', async (req, res) => {
225
287
  */
226
288
  app.get('/api/thinking/history', async (req, res) => {
227
289
  try {
228
- const history = await managers.thinking.searchHistory('');
290
+ const history = await managers.thinking.searchHistory('') || [];
291
+ if (!Array.isArray(history)) {
292
+ return res.status(500).json({ error: 'Invalid history data' });
293
+ }
229
294
  const transformedHistory = history.map((t, index) => ({
230
295
  id: `thought-${index}-${t.thoughtNumber}`,
231
296
  text: t.thought,
@@ -478,8 +543,13 @@ app.get('/api/notes', async (req, res) => {
478
543
  try {
479
544
  const tag = req.query.tag;
480
545
  const includeExpired = req.query.includeExpired === 'true';
481
- const notes = await managers.notes.listNotes(tag, includeExpired);
482
- const transformedNotes = notes.map(note => ({
546
+ const page = parseInt(req.query.page) || 1;
547
+ const limit = Math.min(parseInt(req.query.limit) || 20, 100); // Max 100
548
+ const allNotes = await managers.notes.listNotes(tag, includeExpired);
549
+ const total = allNotes.length;
550
+ const offset = (page - 1) * limit;
551
+ const paginatedNotes = allNotes.slice(offset, offset + limit);
552
+ const transformedNotes = paginatedNotes.map(note => ({
483
553
  id: note.id,
484
554
  _id: note.id,
485
555
  title: note.title,
@@ -491,7 +561,15 @@ app.get('/api/notes', async (req, res) => {
491
561
  createdAt: note.createdAt,
492
562
  updatedAt: note.updatedAt
493
563
  }));
494
- res.json(transformedNotes);
564
+ res.json({
565
+ data: transformedNotes,
566
+ pagination: {
567
+ page,
568
+ limit,
569
+ total,
570
+ totalPages: Math.ceil(total / limit)
571
+ }
572
+ });
495
573
  }
496
574
  catch (error) {
497
575
  res.status(500).json({
@@ -981,7 +1059,8 @@ app.get('/api/admin/stats', async (req, res) => {
981
1059
  }
982
1060
  });
983
1061
  // Start server
984
- app.listen(PORT, () => {
1062
+ let isShuttingDown = false;
1063
+ const server = app.listen(PORT, () => {
985
1064
  console.log(`Sequential Thinking HTTP Server running on port ${PORT}`);
986
1065
  console.log(`Health check: http://localhost:${PORT}/health`);
987
1066
  console.log(`API docs: http://localhost:${PORT}/`);
@@ -990,3 +1069,32 @@ app.listen(PORT, () => {
990
1069
  console.log('This server uses in-memory storage. Do NOT run in cluster mode');
991
1070
  console.log('without external storage (Redis/PostgreSQL) for data persistence.');
992
1071
  });
1072
+ async function gracefulShutdown(signal) {
1073
+ if (isShuttingDown)
1074
+ return;
1075
+ isShuttingDown = true;
1076
+ console.log(`\n[${signal}] Starting graceful shutdown...`);
1077
+ // Stop accepting new connections
1078
+ server.close(async () => {
1079
+ console.log('[Shutdown] HTTP server closed');
1080
+ // Save all data
1081
+ try {
1082
+ await managers.thinking.gracefulShutdown?.();
1083
+ await managers.notes.save?.(); // Assuming save exists or will fail safely
1084
+ await managers.codeDb.save?.();
1085
+ console.log('[Shutdown] All data saved');
1086
+ }
1087
+ catch (error) {
1088
+ console.error('[Shutdown] Error saving data:', error);
1089
+ }
1090
+ console.log('[Shutdown] Complete');
1091
+ process.exit(0);
1092
+ });
1093
+ // Force shutdown after 10 seconds
1094
+ setTimeout(() => {
1095
+ console.error('[Shutdown] Forced exit after timeout');
1096
+ process.exit(1);
1097
+ }, 10000);
1098
+ }
1099
+ process.on('SIGINT', () => gracefulShutdown('SIGINT'));
1100
+ process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
package/dist/lib.d.ts CHANGED
@@ -40,7 +40,8 @@ export declare class SequentialThinkingServer {
40
40
  private confidenceScore;
41
41
  private contextManager;
42
42
  private ruleManager;
43
- constructor(storagePath?: string, delayMs?: number);
43
+ private maxHistorySize;
44
+ constructor(storagePath?: string, delayMs?: number, maxHistorySize?: number);
44
45
  private loadHistory;
45
46
  private attemptRecovery;
46
47
  private rebuildBlocks;
@@ -72,6 +73,7 @@ export declare class SequentialThinkingServer {
72
73
  thoughtCount: number;
73
74
  }[];
74
75
  getHistoryLength(): number;
76
+ gracefulShutdown(): Promise<void>;
75
77
  private learnFromFailures;
76
78
  }
77
79
  export {};
package/dist/lib.js CHANGED
@@ -19,7 +19,9 @@ export class SequentialThinkingServer {
19
19
  confidenceScore = 100; // Meta-Cognition Score (0-100)
20
20
  contextManager = new ContextManager();
21
21
  ruleManager = new RuleManager();
22
- constructor(storagePath = 'thoughts_history.json', delayMs = 0) {
22
+ maxHistorySize;
23
+ constructor(storagePath = 'thoughts_history.json', delayMs = 0, maxHistorySize = 1000) {
24
+ this.maxHistorySize = maxHistorySize;
23
25
  this.disableThoughtLogging = (process.env.DISABLE_THOUGHT_LOGGING || "").toLowerCase() === "true";
24
26
  this.storagePath = path.resolve(storagePath);
25
27
  this.solutionsDbPath = path.resolve(path.dirname(this.storagePath), 'solutions_db.json');
@@ -190,7 +192,13 @@ export class SequentialThinkingServer {
190
192
  try {
191
193
  // Auto-Pruning: Keep max 100 thoughts, summarize older blocks
192
194
  if (this.thoughtHistory.length > 100) {
193
- this.autoPrune();
195
+ try {
196
+ this.autoPrune();
197
+ }
198
+ catch (pruneError) {
199
+ console.error('[AutoPrune] Failed:', pruneError);
200
+ // Continue saving without pruning
201
+ }
194
202
  }
195
203
  const storage = {
196
204
  version: '2.0',
@@ -199,10 +207,21 @@ export class SequentialThinkingServer {
199
207
  };
200
208
  const tmpPath = `${this.storagePath}.tmp`;
201
209
  await fs.writeFile(tmpPath, JSON.stringify(storage, null, 2), 'utf-8');
210
+ // Verify file was written correctly
211
+ const stats = await fs.stat(tmpPath);
212
+ if (stats.size === 0) {
213
+ throw new Error('Temporary file is empty after write');
214
+ }
202
215
  await fs.rename(tmpPath, this.storagePath);
203
216
  }
204
217
  catch (error) {
205
218
  console.error(`Error saving history to ${this.storagePath}:`, error);
219
+ // Cleanup temp file if exists
220
+ try {
221
+ await fs.unlink(`${this.storagePath}.tmp`);
222
+ }
223
+ catch { }
224
+ throw error; // Re-throw to let caller know
206
225
  }
207
226
  });
208
227
  }
@@ -331,6 +350,12 @@ export class SequentialThinkingServer {
331
350
  if (input.thoughtNumber > input.totalThoughts) {
332
351
  input.totalThoughts = input.thoughtNumber;
333
352
  }
353
+ // Enforce max history size
354
+ if (this.thoughtHistory.length >= this.maxHistorySize) {
355
+ const removeCount = this.thoughtHistory.length - this.maxHistorySize + 1;
356
+ this.thoughtHistory.splice(0, removeCount);
357
+ console.warn(`[Memory] Removed ${removeCount} old thoughts to maintain limit of ${this.maxHistorySize}`);
358
+ }
334
359
  this.thoughtHistory.push(input);
335
360
  if (input.branchFromThought && input.branchId) {
336
361
  const branchKey = `${input.branchFromThought}-${input.branchId}`;
@@ -671,8 +696,9 @@ ${typeof wrappedThought === 'string' && wrappedThought.startsWith('│') ? wrapp
671
696
  this.consecutiveStallCount = 0;
672
697
  }
673
698
  // --- 📊 Update Confidence Score ---
674
- if (warnings.length > 0)
675
- this.confidenceScore -= (5 * warnings.length);
699
+ if (warnings.length > 0) {
700
+ this.confidenceScore = Math.max(0, this.confidenceScore - (5 * warnings.length));
701
+ }
676
702
  if (input.thoughtType === 'reflexion') {
677
703
  this.confidenceScore = Math.min(100, this.confidenceScore + 10);
678
704
  }
@@ -819,6 +845,16 @@ ${typeof wrappedThought === 'string' && wrappedThought.startsWith('│') ? wrapp
819
845
  getHistoryLength() {
820
846
  return this.thoughtHistory.length;
821
847
  }
848
+ async gracefulShutdown() {
849
+ console.log('[Shutdown] Saving thought history...');
850
+ try {
851
+ await this.saveHistory();
852
+ console.log('[Shutdown] History saved successfully');
853
+ }
854
+ catch (error) {
855
+ console.error('[Shutdown] Failed to save history:', error);
856
+ }
857
+ }
822
858
  async learnFromFailures(blockId) {
823
859
  const blockThoughts = this.thoughtHistory.filter(t => t.blockId === blockId);
824
860
  for (let i = 0; i < blockThoughts.length - 1; i++) {
package/dist/notes.d.ts CHANGED
@@ -33,7 +33,7 @@ export declare class NotesManager {
33
33
  * Atomic save using write-to-temp + rename pattern.
34
34
  * This ensures either the old file or new file exists, never a partial write.
35
35
  */
36
- private save;
36
+ save(): Promise<void>;
37
37
  /**
38
38
  * Reload notes from disk (useful for external changes)
39
39
  */
@@ -93,6 +93,17 @@ export function registerCodingTools(server, graph) {
93
93
  }, async ({ path: filePath, oldText, newText, reasoning, runTest }) => {
94
94
  try {
95
95
  const absolutePath = validatePath(filePath);
96
+ const stats = await fs.stat(absolutePath);
97
+ const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
98
+ if (stats.size > MAX_FILE_SIZE) {
99
+ return {
100
+ content: [{
101
+ type: "text",
102
+ text: `Error: File size (${Math.round(stats.size / 1024 / 1024)}MB) exceeds maximum allowed (${MAX_FILE_SIZE / 1024 / 1024}MB)`
103
+ }],
104
+ isError: true
105
+ };
106
+ }
96
107
  const content = await fs.readFile(absolutePath, 'utf-8');
97
108
  if (!content.includes(oldText)) {
98
109
  return { content: [{ type: "text", text: "Error: Target text not found. Ensure exact match including whitespace." }], isError: true };
@@ -159,10 +170,16 @@ export function registerCodingTools(server, graph) {
159
170
  }
160
171
  }
161
172
  // -------------------------------------
173
+ // Cleanup backup file after successful edit (keep only last 10)
174
+ await cleanupBackups(path.dirname(absolutePath), 10);
175
+ try {
176
+ await fs.unlink(backupPath);
177
+ }
178
+ catch { }
162
179
  return {
163
180
  content: [{
164
181
  type: "text",
165
- text: `Successfully applied edit to ${filePath}.\nReasoning verified: ${reasoning}\n(Backup created at ${backupPath})`
182
+ text: `Successfully applied edit to ${filePath}.\nReasoning verified: ${reasoning}`
166
183
  }]
167
184
  };
168
185
  }
@@ -171,3 +188,28 @@ export function registerCodingTools(server, graph) {
171
188
  }
172
189
  });
173
190
  }
191
+ async function cleanupBackups(dir, maxBackups = 10) {
192
+ try {
193
+ const files = await fs.readdir(dir);
194
+ const backupFiles = await Promise.all(files
195
+ .filter(f => f.endsWith('.bak'))
196
+ .map(async (f) => ({
197
+ name: f,
198
+ path: path.join(dir, f),
199
+ stat: await fs.stat(path.join(dir, f))
200
+ })));
201
+ // Sort by mtime (newest first)
202
+ const sortedBackups = backupFiles.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs);
203
+ // Remove old backups
204
+ for (let i = maxBackups; i < sortedBackups.length; i++) {
205
+ try {
206
+ await fs.unlink(sortedBackups[i].path);
207
+ console.log(`[Backup] Cleaned up old backup: ${sortedBackups[i].name}`);
208
+ }
209
+ catch { }
210
+ }
211
+ }
212
+ catch {
213
+ // Ignore cleanup errors
214
+ }
215
+ }
@@ -1,3 +1,3 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- export declare function scrapeWebpage(url: string): Promise<string>;
2
+ export declare function scrapeWebpage(url: string, timeoutMs?: number): Promise<string>;
3
3
  export declare function registerWebTools(server: McpServer): void;
package/dist/tools/web.js CHANGED
@@ -3,65 +3,100 @@ import { fetchWithRetry, validatePublicUrl } from "../utils.js";
3
3
  import { JSDOM } from 'jsdom';
4
4
  import { Readability } from '@mozilla/readability';
5
5
  import TurndownService from 'turndown';
6
- export async function scrapeWebpage(url) {
6
+ class SearchRateLimiter {
7
+ requests = new Map(); // provider -> timestamps[]
8
+ limits = {
9
+ brave: { requests: 100, windowMs: 60000 }, // 100/min
10
+ exa: { requests: 50, windowMs: 60000 }, // 50/min
11
+ google: { requests: 100, windowMs: 60000 }, // 100/min
12
+ duckduckgo: { requests: 30, windowMs: 60000 } // 30/min (be nice to DDG)
13
+ };
14
+ canProceed(provider) {
15
+ const limit = this.limits[provider];
16
+ if (!limit)
17
+ return true;
18
+ const now = Date.now();
19
+ const timestamps = this.requests.get(provider) || [];
20
+ // Remove old requests outside window
21
+ const validTimestamps = timestamps.filter((t) => now - t < limit.windowMs);
22
+ this.requests.set(provider, validTimestamps);
23
+ return validTimestamps.length < limit.requests;
24
+ }
25
+ recordRequest(provider) {
26
+ const timestamps = this.requests.get(provider) || [];
27
+ timestamps.push(Date.now());
28
+ this.requests.set(provider, timestamps);
29
+ }
30
+ }
31
+ const rateLimiter = new SearchRateLimiter();
32
+ export async function scrapeWebpage(url, timeoutMs = 30000) {
7
33
  await validatePublicUrl(url);
8
- const response = await fetchWithRetry(url);
9
- const html = await response.text();
10
- const doc = new JSDOM(html, { url });
11
- const reader = new Readability(doc.window.document);
12
- const article = reader.parse();
13
- if (!article)
14
- throw new Error("Could not parse article content");
15
- const turndownService = new TurndownService({
16
- headingStyle: 'atx',
17
- codeBlockStyle: 'fenced'
18
- });
19
- // Custom Rule for GitHub Flavored Markdown Tables
20
- turndownService.addRule('tables', {
21
- filter: ['table'],
22
- replacement: function (content, node) {
23
- const rows = [];
24
- const table = node;
25
- const trs = Array.from(table.querySelectorAll('tr'));
26
- trs.forEach((tr, index) => {
27
- const cols = [];
28
- const tds = Array.from(tr.querySelectorAll('th, td'));
29
- tds.forEach(td => {
30
- // Clean content: remove newlines and pipe characters
31
- cols.push(td.textContent?.replace(/[\n\r]/g, ' ').replace(/\|/g, '\\|').trim() || "");
32
- });
33
- if (cols.length > 0) {
34
- rows.push(`| ${cols.join(' | ')} |`);
35
- // Add separator after header
36
- if (index === 0 || tr.querySelector('th')) {
37
- rows.push(`| ${cols.map(() => '---').join(' | ')} |`);
34
+ const controller = new AbortController();
35
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
36
+ try {
37
+ const response = await fetchWithRetry(url, {
38
+ signal: controller.signal
39
+ });
40
+ const html = await response.text();
41
+ const doc = new JSDOM(html, { url });
42
+ const reader = new Readability(doc.window.document);
43
+ const article = reader.parse();
44
+ if (!article)
45
+ throw new Error("Could not parse article content");
46
+ const turndownService = new TurndownService({
47
+ headingStyle: 'atx',
48
+ codeBlockStyle: 'fenced'
49
+ });
50
+ // Custom Rule for GitHub Flavored Markdown Tables
51
+ turndownService.addRule('tables', {
52
+ filter: ['table'],
53
+ replacement: function (content, node) {
54
+ const rows = [];
55
+ const table = node;
56
+ const trs = Array.from(table.querySelectorAll('tr'));
57
+ trs.forEach((tr, index) => {
58
+ const cols = [];
59
+ const tds = Array.from(tr.querySelectorAll('th, td'));
60
+ tds.forEach(td => {
61
+ // Clean content: remove newlines and pipe characters
62
+ cols.push(td.textContent?.replace(/[\n\r]/g, ' ').replace(/\|/g, '\\|').trim() || "");
63
+ });
64
+ if (cols.length > 0) {
65
+ rows.push(`| ${cols.join(' | ')} |`);
66
+ // Add separator after header
67
+ if (index === 0 || tr.querySelector('th')) {
68
+ rows.push(`| ${cols.map(() => '---').join(' | ')} |`);
69
+ }
38
70
  }
39
- }
40
- });
41
- // Filter out duplicate separator lines if any
42
- const uniqueRows = rows.filter((row, i) => {
43
- if (row.includes('---') && rows[i - 1]?.includes('---'))
44
- return false;
45
- return true;
46
- });
47
- return '\n\n' + uniqueRows.join('\n') + '\n\n';
71
+ });
72
+ // Filter out duplicate separator lines if any
73
+ const uniqueRows = rows.filter((row, i) => {
74
+ if (row.includes('---') && rows[i - 1]?.includes('---'))
75
+ return false;
76
+ return true;
77
+ });
78
+ return '\n\n' + uniqueRows.join('\n') + '\n\n';
79
+ }
80
+ });
81
+ let markdown = turndownService.turndown(article.content || "");
82
+ if (markdown.length > 20000) {
83
+ markdown = markdown.substring(0, 20000) + "\n...(truncated)";
48
84
  }
49
- });
50
- let markdown = turndownService.turndown(article.content || "");
51
- if (markdown.length > 20000) {
52
- markdown = markdown.substring(0, 20000) + "\n...(truncated)";
85
+ return `Title: ${article.title}\n\n${markdown}`;
86
+ }
87
+ finally {
88
+ clearTimeout(timeoutId);
53
89
  }
54
- return `Title: ${article.title}\n\n${markdown}`;
55
90
  }
56
91
  export function registerWebTools(server) {
57
92
  // 1. web_search
58
93
  server.tool("web_search", "Search the web using Brave or Exa APIs (requires API keys in environment variables: BRAVE_API_KEY or EXA_API_KEY).", {
59
94
  query: z.string().min(1).describe("The search query"),
60
- provider: z.enum(['brave', 'exa', 'google']).optional().describe("Preferred search provider")
95
+ provider: z.enum(['brave', 'exa', 'google', 'duckduckgo']).optional().describe("Preferred search provider")
61
96
  }, async ({ query, provider }) => {
62
97
  const errors = [];
63
98
  // 1. Identify available providers
64
- const availableProviders = [];
99
+ const availableProviders = ['duckduckgo']; // Always available (HTML scraping)
65
100
  if (process.env.BRAVE_API_KEY)
66
101
  availableProviders.push('brave');
67
102
  if (process.env.EXA_API_KEY)
@@ -91,7 +126,45 @@ export function registerWebTools(server) {
91
126
  // Default priority if no preference: Brave > Exa > Google (Already respected by push order if mapped correctly, but let's be explicit if needed. Here they are added in that order.)
92
127
  // 3. Try providers sequentially
93
128
  for (const currentProvider of attemptOrder) {
129
+ if (!rateLimiter.canProceed(currentProvider)) {
130
+ errors.push(`[${currentProvider}] Rate limit exceeded`);
131
+ continue;
132
+ }
133
+ rateLimiter.recordRequest(currentProvider);
94
134
  try {
135
+ if (currentProvider === 'duckduckgo') {
136
+ try {
137
+ // DuckDuckGo HTML interface (no API key required)
138
+ const response = await fetchWithRetry(`https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`, {
139
+ headers: {
140
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
141
+ }
142
+ });
143
+ if (!response.ok)
144
+ throw new Error(`DuckDuckGo Status ${response.status}`);
145
+ const html = await response.text();
146
+ // Simple regex extraction (in production, use proper HTML parser)
147
+ const results = [];
148
+ const resultRegex = /<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/g;
149
+ let match;
150
+ while ((match = resultRegex.exec(html)) !== null && results.length < 5) {
151
+ results.push({
152
+ title: match[2].replace(/<[^>]*>/g, ''), // Strip HTML tags
153
+ url: match[1],
154
+ snippet: ''
155
+ });
156
+ }
157
+ return {
158
+ content: [{
159
+ type: "text",
160
+ text: JSON.stringify(results, null, 2)
161
+ }]
162
+ };
163
+ }
164
+ catch (error) {
165
+ throw error; // Let outer loop handle it
166
+ }
167
+ }
95
168
  if (currentProvider === 'brave') {
96
169
  const response = await fetchWithRetry(`https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=5`, {
97
170
  headers: {
package/dist/utils.d.ts CHANGED
@@ -51,4 +51,13 @@ export declare const DEFAULT_HEADERS: {
51
51
  'Accept-Language': string;
52
52
  };
53
53
  export declare function fetchWithRetry(url: string, options?: any, retries?: number, backoff?: number): Promise<Response>;
54
+ export declare function withRetry(fn: () => Promise<any>, maxRetries?: number, baseDelay?: number): Promise<any>;
55
+ export declare class FileLock {
56
+ locks: Map<any, any>;
57
+ acquire(filePath: string, timeout?: number): Promise<{
58
+ release: () => void;
59
+ }>;
60
+ }
61
+ export declare const fileLock: FileLock;
62
+ export declare function sanitizeInput(input: any, maxLength?: number): string;
54
63
  export {};
package/dist/utils.js CHANGED
@@ -9,7 +9,7 @@ import chalk from 'chalk';
9
9
  * Shell metacharacters that could enable command injection
10
10
  * Blocking these prevents command chaining like: cmd; rm -rf /
11
11
  */
12
- const SHELL_METACHARACTERS = /[;|&`$()<>]/u;
12
+ const SHELL_METACHARACTERS = /[;|&`$()<>\{\}\[\]\*\?~]|\$\{[^}]*\}|\\x[0-9a-fA-F]{2}|\$[a-zA-Z_][a-zA-Z0-9_]*/gu;
13
13
  /**
14
14
  * Whitelist of safe commands that can be executed
15
15
  * Commands must be explicitly allowed for security
@@ -118,6 +118,13 @@ function validateCommand(command) {
118
118
  new RegExp('chown\\s+-R\\s+root:'), // Recursive ownership change
119
119
  new RegExp('fdisk'), // Partition tools
120
120
  new RegExp('>\\s*/dev/\\w+\\s*\\+w'), // Write to device
121
+ // New patterns:
122
+ new RegExp('curl\\s+.*\\s*\\|\\s*sh'), // curl | sh
123
+ new RegExp('wget\\s+.*\\s*\\|\\s*sh'), // wget | sh
124
+ new RegExp('base64\\s+-d\\s*\\|'), // encoded commands
125
+ new RegExp('eval\\s*\\('), // eval()
126
+ new RegExp('\\$\\(\\(.*\\)\\)'), // $() command substitution
127
+ new RegExp('`.*`'), // backtick substitution
121
128
  ];
122
129
  for (const pattern of dangerousPatterns) {
123
130
  if (pattern.test(command)) {
@@ -418,3 +425,59 @@ export async function fetchWithRetry(url, options = {}, retries = 3, backoff = 1
418
425
  throw error;
419
426
  }
420
427
  }
428
+ export async function withRetry(fn, maxRetries = 3, baseDelay = 100) {
429
+ let lastError;
430
+ for (let i = 0; i < maxRetries; i++) {
431
+ try {
432
+ return await fn();
433
+ }
434
+ catch (error) {
435
+ lastError = error;
436
+ // Don't retry on certain errors
437
+ if (error.code === 'ENOENT' || error.code === 'EACCES' || error.code === 'EPERM') {
438
+ throw error;
439
+ }
440
+ if (i < maxRetries - 1) {
441
+ const delay = baseDelay * Math.pow(2, i);
442
+ console.warn(`[Retry] Attempt ${i + 1} failed, retrying in ${delay}ms...`);
443
+ await new Promise(resolve => setTimeout(resolve, delay));
444
+ }
445
+ }
446
+ }
447
+ throw lastError;
448
+ }
449
+ export class FileLock {
450
+ locks = new Map();
451
+ async acquire(filePath, timeout = 5000) {
452
+ const startTime = Date.now();
453
+ while (this.locks.has(filePath)) {
454
+ if (Date.now() - startTime > timeout) {
455
+ throw new Error(`Timeout acquiring lock for ${filePath}`);
456
+ }
457
+ await new Promise(resolve => setTimeout(resolve, 10));
458
+ }
459
+ this.locks.set(filePath, true);
460
+ return {
461
+ release: () => {
462
+ this.locks.delete(filePath);
463
+ }
464
+ };
465
+ }
466
+ }
467
+ export const fileLock = new FileLock();
468
+ export function sanitizeInput(input, maxLength = 10000) {
469
+ if (typeof input !== 'string') {
470
+ return '';
471
+ }
472
+ // Trim whitespace
473
+ let sanitized = input.trim();
474
+ // Limit length
475
+ if (sanitized.length > maxLength) {
476
+ sanitized = sanitized.substring(0, maxLength);
477
+ }
478
+ // Remove null bytes
479
+ sanitized = sanitized.replace(/\x00/g, '');
480
+ // Normalize unicode (prevent homograph attacks)
481
+ sanitized = sanitized.normalize('NFC');
482
+ return sanitized;
483
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotza02/sequential-thinking",
3
- "version": "10000.0.6",
3
+ "version": "10000.0.7",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },