codebot-ai 1.0.0 → 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.
@@ -52,6 +52,9 @@ class OpenAIProvider {
52
52
  const decoder = new TextDecoder();
53
53
  let buffer = '';
54
54
  const toolCalls = new Map();
55
+ // Track <think>...</think> blocks (used by qwen3, deepseek, etc.)
56
+ let insideThink = false;
57
+ let contentBuffer = '';
55
58
  try {
56
59
  while (true) {
57
60
  const { done, value } = await reader.read();
@@ -65,6 +68,11 @@ class OpenAIProvider {
65
68
  if (!trimmed || !trimmed.startsWith('data: '))
66
69
  continue;
67
70
  if (trimmed === 'data: [DONE]') {
71
+ // Flush any remaining content buffer
72
+ if (contentBuffer && !insideThink) {
73
+ yield { type: 'text', text: contentBuffer };
74
+ contentBuffer = '';
75
+ }
68
76
  for (const [, tc] of toolCalls) {
69
77
  yield {
70
78
  type: 'tool_call_end',
@@ -84,7 +92,48 @@ class OpenAIProvider {
84
92
  if (!delta)
85
93
  continue;
86
94
  if (delta.content) {
87
- yield { type: 'text', text: delta.content };
95
+ contentBuffer += delta.content;
96
+ // Process buffer for <think>...</think> tags
97
+ let changed = true;
98
+ while (changed) {
99
+ changed = false;
100
+ if (insideThink) {
101
+ const end = contentBuffer.indexOf('</think>');
102
+ if (end !== -1) {
103
+ // End of think block — discard thinking content, continue
104
+ contentBuffer = contentBuffer.slice(end + 8);
105
+ insideThink = false;
106
+ changed = true;
107
+ }
108
+ // else: still inside think, wait for more data
109
+ }
110
+ else {
111
+ const start = contentBuffer.indexOf('<think>');
112
+ if (start !== -1) {
113
+ // Found <think> — output everything before it, enter think mode
114
+ const before = contentBuffer.slice(0, start);
115
+ if (before)
116
+ yield { type: 'text', text: before };
117
+ contentBuffer = contentBuffer.slice(start + 7);
118
+ insideThink = true;
119
+ changed = true;
120
+ }
121
+ else {
122
+ // No think tag — check if buffer ends with a partial "<think" prefix
123
+ let holdBack = 0;
124
+ for (let len = Math.min(6, contentBuffer.length); len >= 1; len--) {
125
+ if ('<think>'.startsWith(contentBuffer.slice(-len))) {
126
+ holdBack = len;
127
+ break;
128
+ }
129
+ }
130
+ const safe = contentBuffer.slice(0, contentBuffer.length - holdBack);
131
+ contentBuffer = contentBuffer.slice(contentBuffer.length - holdBack);
132
+ if (safe)
133
+ yield { type: 'text', text: safe };
134
+ }
135
+ }
136
+ }
88
137
  }
89
138
  if (delta.tool_calls) {
90
139
  for (const tc of delta.tool_calls) {
@@ -126,7 +175,12 @@ class OpenAIProvider {
126
175
  finally {
127
176
  reader.releaseLock();
128
177
  }
129
- // 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
130
184
  for (const [, tc] of toolCalls) {
131
185
  yield {
132
186
  type: 'tool_call_end',
package/dist/setup.js CHANGED
@@ -194,16 +194,16 @@ async function runSetup() {
194
194
  const choice = await ask(rl, fmt(`\nSelect [1-${options.length}]: `, 'cyan'));
195
195
  const selected = options[parseInt(choice, 10) - 1] || options[0];
196
196
  // Step 4: Show available models for chosen provider
197
- const providerModels = Object.entries(registry_1.MODEL_REGISTRY)
198
- .filter(([, info]) => {
199
- if (selected.provider === 'openai' && !info.provider)
200
- return true; // local models
201
- return info.provider === selected.provider;
202
- })
203
- .map(([name]) => name);
197
+ // For local servers, use the actual installed models instead of the hardcoded registry
198
+ const matchedServer = localServers.find(s => s.url === selected.baseUrl);
199
+ const providerModels = matchedServer && matchedServer.models.length > 0
200
+ ? matchedServer.models
201
+ : Object.entries(registry_1.MODEL_REGISTRY)
202
+ .filter(([, info]) => info.provider === selected.provider)
203
+ .map(([name]) => name);
204
204
  if (providerModels.length > 1) {
205
- console.log(fmt(`\nAvailable models for ${selected.label}:`, 'bold'));
206
- providerModels.slice(0, 10).forEach((m, i) => {
205
+ console.log(fmt(`\nAvailable models${matchedServer ? ` on ${matchedServer.name}` : ''}:`, 'bold'));
206
+ providerModels.slice(0, 15).forEach((m, i) => {
207
207
  const marker = m === selected.model ? fmt(' (default)', 'green') : '';
208
208
  console.log(` ${fmt(`${i + 1}`, 'cyan')} ${m}${marker}`);
209
209
  });
@@ -214,7 +214,7 @@ async function runSetup() {
214
214
  selected.model = providerModels[modelIdx];
215
215
  }
216
216
  else if (modelChoice.length > 2) {
217
- // Treat as model name
217
+ // Treat as model name typed directly
218
218
  selected.model = modelChoice;
219
219
  }
220
220
  }
@@ -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.0",
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",