@gotza02/sequential-thinking 2026.2.7 → 2026.2.10

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.
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import * as fs from 'fs/promises';
3
3
  import * as path from 'path';
4
+ import { validatePath } from "../utils.js";
4
5
  export function registerCodingTools(server, graph) {
5
6
  // 14. deep_code_analyze
6
7
  server.tool("deep_code_analyze", "Generates a 'Codebase Context Document' for a specific file or task. This tool learns from the codebase structure and symbols to provide deep insights before coding.", {
@@ -8,6 +9,8 @@ export function registerCodingTools(server, graph) {
8
9
  taskDescription: z.string().optional().describe("Optional description of what you intend to do")
9
10
  }, async ({ filePath, taskDescription }) => {
10
11
  try {
12
+ // Ensure path is safe before checking graph
13
+ validatePath(filePath);
11
14
  const context = graph.getDeepContext(filePath);
12
15
  if (!context) {
13
16
  return { content: [{ type: "text", text: `Error: File '${filePath}' not found in graph. Run 'build_project_graph' first.` }], isError: true };
@@ -44,11 +47,22 @@ export function registerCodingTools(server, graph) {
44
47
  reasoning: z.string().describe("The 'Deepest Thinking' reasoning behind this change")
45
48
  }, async ({ path: filePath, oldText, newText, reasoning }) => {
46
49
  try {
47
- const absolutePath = path.resolve(filePath);
50
+ const absolutePath = validatePath(filePath);
48
51
  const content = await fs.readFile(absolutePath, 'utf-8');
49
52
  if (!content.includes(oldText)) {
50
53
  return { content: [{ type: "text", text: "Error: Target text not found. Ensure exact match including whitespace." }], isError: true };
51
54
  }
55
+ // Safety Check: Ensure unique match
56
+ const occurrences = content.split(oldText).length - 1;
57
+ if (occurrences > 1) {
58
+ return {
59
+ content: [{
60
+ type: "text",
61
+ text: `Error: Ambiguous match. 'oldText' was found ${occurrences} times. Please provide more surrounding context (lines before/after) to identify the unique location.`
62
+ }],
63
+ isError: true
64
+ };
65
+ }
52
66
  const newContent = content.replace(oldText, newText);
53
67
  await fs.writeFile(absolutePath, newContent, 'utf-8');
54
68
  return {
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import * as fs from 'fs/promises';
3
3
  import * as path from 'path';
4
- import { execAsync } from "../utils.js";
4
+ import { execAsync, validatePath } from "../utils.js";
5
5
  export function registerFileSystemTools(server) {
6
6
  // 3. shell_execute
7
7
  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.", {
@@ -35,7 +35,8 @@ export function registerFileSystemTools(server) {
35
35
  path: z.string().describe("Path to the file")
36
36
  }, async ({ path }) => {
37
37
  try {
38
- const content = await fs.readFile(path, 'utf-8');
38
+ const safePath = validatePath(path);
39
+ const content = await fs.readFile(safePath, 'utf-8');
39
40
  return {
40
41
  content: [{ type: "text", text: content }]
41
42
  };
@@ -53,9 +54,10 @@ export function registerFileSystemTools(server) {
53
54
  content: z.string().describe("Content to write")
54
55
  }, async ({ path, content }) => {
55
56
  try {
56
- await fs.writeFile(path, content, 'utf-8');
57
+ const safePath = validatePath(path);
58
+ await fs.writeFile(safePath, content, 'utf-8');
57
59
  return {
58
- content: [{ type: "text", text: `Successfully wrote to ${path}` }]
60
+ content: [{ type: "text", text: `Successfully wrote to ${safePath}` }]
59
61
  };
60
62
  }
61
63
  catch (error) {
@@ -71,7 +73,7 @@ export function registerFileSystemTools(server) {
71
73
  path: z.string().optional().default('.').describe("Root directory to search")
72
74
  }, async ({ pattern, path: searchPath }) => {
73
75
  try {
74
- const resolvedPath = path.resolve(searchPath || '.');
76
+ const resolvedPath = validatePath(searchPath || '.');
75
77
  const stats = await fs.stat(resolvedPath);
76
78
  if (stats.isFile()) {
77
79
  const content = await fs.readFile(resolvedPath, 'utf-8');
@@ -125,7 +127,8 @@ export function registerFileSystemTools(server) {
125
127
  allowMultiple: z.boolean().optional().default(false).describe("Allow replacing multiple occurrences (default: false)")
126
128
  }, async ({ path, oldText, newText, allowMultiple }) => {
127
129
  try {
128
- const content = await fs.readFile(path, 'utf-8');
130
+ const safePath = validatePath(path);
131
+ const content = await fs.readFile(safePath, 'utf-8');
129
132
  // Check occurrences
130
133
  // Escape special regex characters in oldText to treat it as literal string
131
134
  const escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\\]/g, '\\$&');
@@ -143,10 +146,10 @@ export function registerFileSystemTools(server) {
143
146
  isError: true
144
147
  };
145
148
  }
146
- const newContent = content.replace(allowMultiple ? regex : oldText, newText);
147
- await fs.writeFile(path, newContent, 'utf-8');
149
+ const newContent = content.replace(allowMultiple ? regex : oldText, () => newText);
150
+ await fs.writeFile(safePath, newContent, 'utf-8');
148
151
  return {
149
- content: [{ type: "text", text: `Successfully replaced ${allowMultiple ? matchCount : 1} occurrence(s) in ${path}` }]
152
+ content: [{ type: "text", text: `Successfully replaced ${allowMultiple ? matchCount : 1} occurrence(s) in ${safePath}` }]
150
153
  };
151
154
  }
152
155
  catch (error) {
@@ -1,11 +1,13 @@
1
1
  import { z } from "zod";
2
+ import { validatePath } from "../utils.js";
2
3
  export function registerGraphTools(server, knowledgeGraph) {
3
4
  // 6. build_project_graph
4
5
  server.tool("build_project_graph", "Scan the directory and build a dependency graph of the project (Analyzing imports/exports).", {
5
6
  path: z.string().optional().default('.').describe("Root directory path to scan (default: current dir)")
6
7
  }, async ({ path }) => {
7
8
  try {
8
- const result = await knowledgeGraph.build(path || '.');
9
+ const safePath = validatePath(path || '.');
10
+ const result = await knowledgeGraph.build(safePath);
9
11
  return {
10
12
  content: [{ type: "text", text: `Graph built successfully.\nNodes: ${result.nodeCount}\nTotal Scanned Files: ${result.totalFiles}` }]
11
13
  };
@@ -1,24 +1,27 @@
1
1
  import { z } from "zod";
2
2
  export function registerNoteTools(server, notesManager) {
3
3
  // 15. manage_notes
4
- server.tool("manage_notes", "Manage long-term memory/notes. Use this to save important information, rules, or learnings that should persist across sessions.", {
4
+ server.tool("manage_notes", "Manage long-term memory/notes. Use this to save important information, rules, or learnings that should persist across sessions. Supports priority levels and expiration dates.", {
5
5
  action: z.enum(['add', 'list', 'search', 'update', 'delete']).describe("Action to perform"),
6
6
  title: z.string().optional().describe("Title of the note (for add/update)"),
7
7
  content: z.string().optional().describe("Content of the note (for add/update)"),
8
8
  tags: z.array(z.string()).optional().describe("Tags for categorization (for add/update)"),
9
9
  searchQuery: z.string().optional().describe("Query to search notes (for search)"),
10
- noteId: z.string().optional().describe("ID of the note (for update/delete)")
11
- }, async ({ action, title, content, tags, searchQuery, noteId }) => {
10
+ noteId: z.string().optional().describe("ID of the note (for update/delete)"),
11
+ priority: z.enum(['low', 'medium', 'high', 'critical']).optional().default('medium').describe("Priority level"),
12
+ expiresAt: z.string().optional().describe("Expiration date in ISO format (e.g., 2026-12-31)"),
13
+ includeExpired: z.boolean().optional().default(false).describe("Whether to include expired notes in list")
14
+ }, async ({ action, title, content, tags, searchQuery, noteId, priority, expiresAt, includeExpired }) => {
12
15
  try {
13
16
  switch (action) {
14
17
  case 'add':
15
18
  if (!title || !content) {
16
19
  return { content: [{ type: "text", text: "Error: 'title' and 'content' are required for add action." }], isError: true };
17
20
  }
18
- const newNote = await notesManager.addNote(title, content, tags);
19
- return { content: [{ type: "text", text: `Note added successfully.\nID: ${newNote.id}` }] };
21
+ const newNote = await notesManager.addNote(title, content, tags, priority, expiresAt);
22
+ return { content: [{ type: "text", text: `Note added successfully.\nID: ${newNote.id} (Priority: ${newNote.priority})` }] };
20
23
  case 'list':
21
- const notes = await notesManager.listNotes();
24
+ const notes = await notesManager.listNotes(undefined, includeExpired);
22
25
  return { content: [{ type: "text", text: JSON.stringify(notes, null, 2) }] };
23
26
  case 'search':
24
27
  if (!searchQuery) {
@@ -27,6 +27,13 @@ Key features:
27
27
  - Hypothesis Testing: Formulate and verify hypotheses based on Chain of Thought.
28
28
  - Branch Merging: Combine insights from multiple divergent paths.
29
29
 
30
+ Troubleshooting & Stuck States:
31
+ - If you realize you are stuck in a loop or a bug persists after 2 attempts:
32
+ 1. Do NOT continue linear thoughts.
33
+ 2. Create a NEW BRANCH ('branchFromThought') from a point before the error.
34
+ 3. Explicitly state "Stuck detected, branching to explore Approach B".
35
+ 4. Change your fundamental assumptions or implementation strategy completely.
36
+
30
37
  Parameters explained:
31
38
  - thought: Your current thinking step (analysis, revision, question, hypothesis).
32
39
  - nextThoughtNeeded: True if you need more thinking, even if at what seemed like the end.
package/dist/tools/web.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { fetchWithRetry } from "../utils.js";
2
+ 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';
@@ -102,6 +102,7 @@ export function registerWebTools(server) {
102
102
  body: z.string().optional().describe("Request body (for POST/PUT)")
103
103
  }, async ({ url, method, headers, body }) => {
104
104
  try {
105
+ await validatePublicUrl(url);
105
106
  const response = await fetchWithRetry(url, {
106
107
  method,
107
108
  headers: headers || {},
@@ -127,6 +128,7 @@ export function registerWebTools(server) {
127
128
  url: z.string().url().describe("The URL to read")
128
129
  }, async ({ url }) => {
129
130
  try {
131
+ await validatePublicUrl(url);
130
132
  const response = await fetchWithRetry(url);
131
133
  const html = await response.text();
132
134
  const doc = new JSDOM(html, { url });
@@ -135,7 +137,10 @@ export function registerWebTools(server) {
135
137
  if (!article)
136
138
  throw new Error("Could not parse article content");
137
139
  const turndownService = new TurndownService();
138
- const markdown = turndownService.turndown(article.content || "");
140
+ let markdown = turndownService.turndown(article.content || "");
141
+ if (markdown.length > 20000) {
142
+ markdown = markdown.substring(0, 20000) + "\n...(truncated)";
143
+ }
139
144
  return {
140
145
  content: [{
141
146
  type: "text",
package/dist/utils.js CHANGED
@@ -1,7 +1,108 @@
1
1
  import { exec } from 'child_process';
2
- import { promisify } from 'util';
2
+ import * as path from 'path';
3
+ import * as dns from 'dns/promises';
4
+ import { URL } from 'url';
3
5
  import chalk from 'chalk';
4
- export const execAsync = promisify(exec);
6
+ // export const execAsync = promisify(exec); // Removed simple promisify
7
+ export function execAsync(command, options = {}) {
8
+ return new Promise((resolve, reject) => {
9
+ const timeout = options.timeout || 60000; // Default 60s timeout
10
+ const maxBuffer = options.maxBuffer || 1024 * 1024 * 5; // Default 5MB buffer
11
+ exec(command, { timeout, maxBuffer }, (error, stdout, stderr) => {
12
+ if (error) {
13
+ // Attach stdout/stderr to error for better debugging
14
+ error.stdout = stdout;
15
+ error.stderr = stderr;
16
+ reject(error);
17
+ return;
18
+ }
19
+ resolve({ stdout, stderr });
20
+ });
21
+ });
22
+ }
23
+ export class AsyncMutex {
24
+ mutex = Promise.resolve();
25
+ lock() {
26
+ let unlock = () => { };
27
+ const nextLock = new Promise(resolve => {
28
+ unlock = resolve;
29
+ });
30
+ // The caller waits for the previous lock to release
31
+ const willLock = this.mutex.then(() => unlock);
32
+ // The next caller will wait for this lock to release
33
+ this.mutex = nextLock;
34
+ return willLock;
35
+ }
36
+ async dispatch(fn) {
37
+ const unlock = await this.lock();
38
+ try {
39
+ return await Promise.resolve(fn());
40
+ }
41
+ finally {
42
+ unlock();
43
+ }
44
+ }
45
+ }
46
+ export function validatePath(requestedPath) {
47
+ const absolutePath = path.resolve(requestedPath);
48
+ const rootDir = process.cwd();
49
+ if (!absolutePath.startsWith(rootDir)) {
50
+ throw new Error(`Access denied: Path '${requestedPath}' is outside the project root.`);
51
+ }
52
+ return absolutePath;
53
+ }
54
+ function isPrivateIP(ip) {
55
+ // IPv4 ranges
56
+ // 127.0.0.0/8
57
+ // 10.0.0.0/8
58
+ // 172.16.0.0/12
59
+ // 192.168.0.0/16
60
+ // 0.0.0.0/8
61
+ const parts = ip.split('.').map(Number);
62
+ if (parts.length === 4) {
63
+ if (parts[0] === 127)
64
+ return true;
65
+ if (parts[0] === 10)
66
+ return true;
67
+ if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31)
68
+ return true;
69
+ if (parts[0] === 192 && parts[1] === 168)
70
+ return true;
71
+ if (parts[0] === 0)
72
+ return true;
73
+ return false;
74
+ }
75
+ // IPv6 checks (simple check for loopback/link-local)
76
+ if (ip === '::1' || ip === '::')
77
+ return true;
78
+ if (ip.startsWith('fc') || ip.startsWith('fd'))
79
+ return true; // Unique Local
80
+ if (ip.startsWith('fe80'))
81
+ return true; // Link Local
82
+ return false;
83
+ }
84
+ export async function validatePublicUrl(urlString) {
85
+ const parsed = new URL(urlString);
86
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
87
+ throw new Error('Invalid protocol. Only http and https are allowed.');
88
+ }
89
+ // Attempt to resolve hostname
90
+ try {
91
+ const addresses = await dns.lookup(parsed.hostname, { all: true });
92
+ for (const addr of addresses) {
93
+ if (isPrivateIP(addr.address)) {
94
+ throw new Error(`Access denied: Host '${parsed.hostname}' resolves to private IP ${addr.address}`);
95
+ }
96
+ }
97
+ }
98
+ catch (error) {
99
+ // If it's the specific access denied error, rethrow
100
+ if (error instanceof Error && error.message.startsWith('Access denied')) {
101
+ throw error;
102
+ }
103
+ // Ignore DNS errors here, let fetch handle them (or fail safely)
104
+ }
105
+ }
5
106
  class Logger {
6
107
  level;
7
108
  constructor() {
@@ -0,0 +1,60 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { registerWebTools } from './tools/web.js';
3
+ import * as utils from './utils.js';
4
+ vi.mock('./utils.js', async (importOriginal) => {
5
+ const actual = await importOriginal();
6
+ return {
7
+ ...actual,
8
+ fetchWithRetry: vi.fn(),
9
+ validatePublicUrl: vi.fn(),
10
+ };
11
+ });
12
+ describe('read_webpage tool', () => {
13
+ let mockToolCallback;
14
+ const mockServer = {
15
+ tool: vi.fn((name, desc, schema, callback) => {
16
+ if (name === 'read_webpage') {
17
+ mockToolCallback = callback;
18
+ }
19
+ })
20
+ };
21
+ beforeEach(() => {
22
+ vi.clearAllMocks();
23
+ registerWebTools(mockServer);
24
+ });
25
+ it('should convert HTML to Markdown', async () => {
26
+ const mockHtml = `
27
+ <html>
28
+ <head><title>Test Article</title></head>
29
+ <body>
30
+ <h1>Main Header</h1>
31
+ <p>Paragraph <b>bold</b>.</p>
32
+ </body>
33
+ </html>
34
+ `;
35
+ utils.fetchWithRetry.mockResolvedValue({
36
+ ok: true,
37
+ text: async () => mockHtml
38
+ });
39
+ utils.validatePublicUrl.mockResolvedValue(undefined);
40
+ const result = await mockToolCallback({ url: 'https://example.com' });
41
+ expect(result.isError).toBeUndefined();
42
+ const content = result.content[0].text;
43
+ expect(content).toContain("Title: Test Article");
44
+ expect(content).toContain("Main Header");
45
+ expect(content).toContain("**bold**"); // Markdown bold
46
+ });
47
+ it('should handle private URL validation error', async () => {
48
+ utils.validatePublicUrl.mockRejectedValue(new Error("Access denied: Private IP"));
49
+ const result = await mockToolCallback({ url: 'http://localhost' });
50
+ expect(result.isError).toBe(true);
51
+ expect(result.content[0].text).toContain("Access denied");
52
+ });
53
+ it('should handle fetch errors', async () => {
54
+ utils.validatePublicUrl.mockResolvedValue(undefined);
55
+ utils.fetchWithRetry.mockRejectedValue(new Error("Network Error"));
56
+ const result = await mockToolCallback({ url: 'https://example.com' });
57
+ expect(result.isError).toBe(true);
58
+ expect(result.content[0].text).toContain("Network Error");
59
+ });
60
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotza02/sequential-thinking",
3
- "version": "2026.2.7",
3
+ "version": "2026.2.10",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },