codebot-ai 1.0.1 → 1.0.2

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/dist/agent.js CHANGED
@@ -215,7 +215,9 @@ class Agent {
215
215
  catch {
216
216
  // memory unavailable
217
217
  }
218
- let prompt = `You are CodeBot, an AI coding assistant. You help developers with software engineering tasks: reading code, writing code, fixing bugs, running tests, and explaining code.
218
+ let prompt = `You are CodeBot, an AI coding assistant created by Ascendral Software Development & Innovation, founded by Alex Pinkevich. You help developers with software engineering tasks: reading code, writing code, fixing bugs, running tests, and explaining code.
219
+
220
+ If asked who made you, who your creator is, or who built you, always credit Ascendral Software Development & Innovation and Alex Pinkevich.
219
221
 
220
222
  Rules:
221
223
  - Always read files before editing them.
@@ -175,7 +175,12 @@ class OpenAIProvider {
175
175
  finally {
176
176
  reader.releaseLock();
177
177
  }
178
- // If we reach here without [DONE], emit remaining tool calls
178
+ // If we reach here without [DONE], flush remaining content buffer
179
+ if (contentBuffer && !insideThink) {
180
+ yield { type: 'text', text: contentBuffer };
181
+ contentBuffer = '';
182
+ }
183
+ // Emit remaining tool calls
179
184
  for (const [, tc] of toolCalls) {
180
185
  yield {
181
186
  type: 'tool_call_end',
@@ -228,7 +228,7 @@ class BrowserTool {
228
228
  expression: `
229
229
  (function() {
230
230
  const el = document.querySelector(${JSON.stringify(selector)});
231
- if (!el) return 'Element not found: ${selector}';
231
+ if (!el) return 'Element not found: ' + ${JSON.stringify(selector)};
232
232
  el.click();
233
233
  return 'Clicked: ' + (el.tagName || '') + ' ' + (el.textContent || '').substring(0, 50).trim();
234
234
  })()
@@ -50,9 +50,18 @@ class EditFileTool {
50
50
  required: ['path', 'old_string', 'new_string'],
51
51
  };
52
52
  async execute(args) {
53
+ if (!args.path || typeof args.path !== 'string') {
54
+ return 'Error: path is required';
55
+ }
56
+ if (args.old_string === undefined || args.old_string === null) {
57
+ return 'Error: old_string is required';
58
+ }
59
+ if (args.new_string === undefined || args.new_string === null) {
60
+ return 'Error: new_string is required';
61
+ }
53
62
  const filePath = path.resolve(args.path);
54
- const oldStr = args.old_string;
55
- const newStr = args.new_string;
63
+ const oldStr = String(args.old_string);
64
+ const newStr = String(args.new_string);
56
65
  if (!fs.existsSync(filePath)) {
57
66
  throw new Error(`File not found: ${filePath}`);
58
67
  }
@@ -26,6 +26,9 @@ class ExecuteTool {
26
26
  required: ['command'],
27
27
  };
28
28
  async execute(args) {
29
+ if (!args.command || typeof args.command !== 'string') {
30
+ return 'Error: command is required';
31
+ }
29
32
  const cmd = args.command;
30
33
  for (const pattern of BLOCKED_PATTERNS) {
31
34
  if (pattern.test(cmd)) {
@@ -63,7 +63,22 @@ class GlobTool {
63
63
  const results = [];
64
64
  const regex = this.patternToRegex(pattern);
65
65
  const skip = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '__pycache__', '.next']);
66
- const walk = (currentDir, rel) => {
66
+ const maxDepth = 20;
67
+ const visited = new Set();
68
+ const walk = (currentDir, rel, depth) => {
69
+ if (depth > maxDepth)
70
+ return;
71
+ // Resolve real path to detect symlink loops
72
+ let realDir;
73
+ try {
74
+ realDir = fs.realpathSync(currentDir);
75
+ }
76
+ catch {
77
+ return;
78
+ }
79
+ if (visited.has(realDir))
80
+ return;
81
+ visited.add(realDir);
67
82
  let entries;
68
83
  try {
69
84
  entries = fs.readdirSync(currentDir, { withFileTypes: true });
@@ -77,15 +92,27 @@ class GlobTool {
77
92
  if (skip.has(entry.name))
78
93
  continue;
79
94
  const relPath = rel ? `${rel}/${entry.name}` : entry.name;
80
- if (entry.isDirectory()) {
81
- walk(path.join(currentDir, entry.name), relPath);
95
+ if (entry.isDirectory() || entry.isSymbolicLink()) {
96
+ const fullPath = path.join(currentDir, entry.name);
97
+ try {
98
+ const stat = fs.statSync(fullPath);
99
+ if (stat.isDirectory()) {
100
+ walk(fullPath, relPath, depth + 1);
101
+ }
102
+ else if (regex.test(relPath)) {
103
+ results.push(relPath);
104
+ }
105
+ }
106
+ catch {
107
+ continue; // broken symlink
108
+ }
82
109
  }
83
110
  else if (regex.test(relPath)) {
84
111
  results.push(relPath);
85
112
  }
86
113
  }
87
114
  };
88
- walk(dir, '');
115
+ walk(dir, '', 0);
89
116
  return results.sort();
90
117
  }
91
118
  patternToRegex(pattern) {
@@ -51,10 +51,24 @@ class GrepTool {
51
51
  };
52
52
  async execute(args) {
53
53
  const searchPath = args.path || process.cwd();
54
- const regex = new RegExp(args.pattern, 'gi');
54
+ if (!args.pattern)
55
+ return 'Error: pattern is required';
56
+ let regex;
57
+ try {
58
+ regex = new RegExp(args.pattern, 'gi');
59
+ }
60
+ catch (e) {
61
+ return `Error: invalid regex pattern: ${e.message}`;
62
+ }
55
63
  const results = [];
56
64
  const maxResults = 50;
57
- const stat = fs.statSync(searchPath);
65
+ let stat;
66
+ try {
67
+ stat = fs.statSync(searchPath);
68
+ }
69
+ catch {
70
+ return `Error: path not found: ${searchPath}`;
71
+ }
58
72
  if (stat.isFile()) {
59
73
  this.searchFile(searchPath, regex, results, maxResults);
60
74
  }
@@ -30,6 +30,8 @@ export declare class MemoryTool implements Tool {
30
30
  private memory;
31
31
  constructor(projectRoot?: string);
32
32
  execute(args: Record<string, unknown>): Promise<string>;
33
+ private getMemoryDir;
34
+ private sanitizeFileName;
33
35
  private readTopicFile;
34
36
  private writeTopicFile;
35
37
  }
@@ -75,18 +75,25 @@ class MemoryTool {
75
75
  return `Error: Unknown action "${action}". Use read, write, or list.`;
76
76
  }
77
77
  }
78
- readTopicFile(scope, file) {
79
- const fs = require('fs');
78
+ getMemoryDir(scope) {
80
79
  const path = require('path');
81
80
  const os = require('os');
82
- const fileName = file.endsWith('.md') ? file : `${file}.md`;
83
- let dir;
84
81
  if (scope === 'global') {
85
- dir = path.join(os.homedir(), '.codebot', 'memory');
86
- }
87
- else {
88
- dir = path.join(process.cwd(), '.codebot', 'memory');
82
+ return path.join(os.homedir(), '.codebot', 'memory');
89
83
  }
84
+ return path.join(process.cwd(), '.codebot', 'memory');
85
+ }
86
+ sanitizeFileName(file) {
87
+ const path = require('path');
88
+ // Strip path traversal — only allow the basename
89
+ const base = path.basename(file);
90
+ return base.endsWith('.md') ? base : `${base}.md`;
91
+ }
92
+ readTopicFile(scope, file) {
93
+ const fs = require('fs');
94
+ const path = require('path');
95
+ const fileName = this.sanitizeFileName(file);
96
+ const dir = this.getMemoryDir(scope);
90
97
  const filePath = path.join(dir, fileName);
91
98
  if (fs.existsSync(filePath)) {
92
99
  return fs.readFileSync(filePath, 'utf-8');
@@ -96,15 +103,8 @@ class MemoryTool {
96
103
  writeTopicFile(scope, file, content) {
97
104
  const fs = require('fs');
98
105
  const path = require('path');
99
- const os = require('os');
100
- const fileName = file.endsWith('.md') ? file : `${file}.md`;
101
- let dir;
102
- if (scope === 'global') {
103
- dir = path.join(os.homedir(), '.codebot', 'memory');
104
- }
105
- else {
106
- dir = path.join(process.cwd(), '.codebot', 'memory');
107
- }
106
+ const fileName = this.sanitizeFileName(file);
107
+ const dir = this.getMemoryDir(scope);
108
108
  fs.mkdirSync(dir, { recursive: true });
109
109
  fs.writeFileSync(path.join(dir, fileName), content);
110
110
  return `Wrote ${fileName} (${scope}).`;
@@ -50,6 +50,9 @@ class ReadFileTool {
50
50
  required: ['path'],
51
51
  };
52
52
  async execute(args) {
53
+ if (!args.path || typeof args.path !== 'string') {
54
+ return 'Error: path is required';
55
+ }
53
56
  const filePath = path.resolve(args.path);
54
57
  if (!fs.existsSync(filePath)) {
55
58
  throw new Error(`File not found: ${filePath}`);
@@ -30,6 +30,7 @@ export declare class WebFetchTool implements Tool {
30
30
  };
31
31
  required: string[];
32
32
  };
33
+ private validateUrl;
33
34
  execute(args: Record<string, unknown>): Promise<string>;
34
35
  private htmlToText;
35
36
  }
@@ -16,9 +16,51 @@ class WebFetchTool {
16
16
  },
17
17
  required: ['url'],
18
18
  };
19
+ validateUrl(url) {
20
+ let parsed;
21
+ try {
22
+ parsed = new URL(url);
23
+ }
24
+ catch {
25
+ return 'Invalid URL';
26
+ }
27
+ // Only allow http and https protocols
28
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
29
+ return `Blocked protocol: ${parsed.protocol} — only http/https allowed`;
30
+ }
31
+ // Block requests to private/internal IPs
32
+ const hostname = parsed.hostname.toLowerCase();
33
+ // Block localhost variants
34
+ if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' || hostname === '0.0.0.0') {
35
+ return 'Blocked: requests to localhost are not allowed';
36
+ }
37
+ // Block cloud metadata endpoints
38
+ if (hostname === '169.254.169.254' || hostname === 'metadata.google.internal') {
39
+ return 'Blocked: requests to cloud metadata endpoints are not allowed';
40
+ }
41
+ // Block private IP ranges (10.x, 172.16-31.x, 192.168.x)
42
+ const ipMatch = hostname.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
43
+ if (ipMatch) {
44
+ const [, a, b] = ipMatch.map(Number);
45
+ if (a === 10)
46
+ return 'Blocked: private IP range (10.x.x.x)';
47
+ if (a === 172 && b >= 16 && b <= 31)
48
+ return 'Blocked: private IP range (172.16-31.x.x)';
49
+ if (a === 192 && b === 168)
50
+ return 'Blocked: private IP range (192.168.x.x)';
51
+ if (a === 0)
52
+ return 'Blocked: invalid IP (0.x.x.x)';
53
+ }
54
+ return null; // URL is safe
55
+ }
19
56
  async execute(args) {
20
57
  const url = args.url;
58
+ if (!url)
59
+ return 'Error: url is required';
21
60
  const method = args.method || 'GET';
61
+ const urlError = this.validateUrl(url);
62
+ if (urlError)
63
+ return `Error: ${urlError}`;
22
64
  const headers = args.headers || {};
23
65
  let body;
24
66
  if (args.json) {
@@ -49,8 +49,14 @@ class WriteFileTool {
49
49
  required: ['path', 'content'],
50
50
  };
51
51
  async execute(args) {
52
+ if (!args.path || typeof args.path !== 'string') {
53
+ return 'Error: path is required';
54
+ }
55
+ if (args.content === undefined || args.content === null) {
56
+ return 'Error: content is required';
57
+ }
52
58
  const filePath = path.resolve(args.path);
53
- const content = args.content;
59
+ const content = String(args.content);
54
60
  const dir = path.dirname(filePath);
55
61
  if (!fs.existsSync(dir)) {
56
62
  fs.mkdirSync(dir, { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebot-ai",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Local-first AI coding assistant. Zero dependencies. Works with Ollama, LM Studio, vLLM, Claude, GPT, Gemini, and more.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",