codebot-ai 1.0.2 → 1.1.0

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/README.md CHANGED
@@ -115,6 +115,8 @@ codebot --resume <session-id> # Resume specific session
115
115
  /models List all supported models
116
116
  /sessions List saved sessions
117
117
  /auto Toggle autonomous mode
118
+ /undo Undo last file edit (/undo [path])
119
+ /usage Show token usage for this session
118
120
  /clear Clear conversation
119
121
  /compact Force context compaction
120
122
  /config Show configuration
@@ -123,13 +125,14 @@ codebot --resume <session-id> # Resume specific session
123
125
 
124
126
  ## Tools
125
127
 
126
- CodeBot has 10 built-in tools:
128
+ CodeBot has 11 built-in tools:
127
129
 
128
130
  | Tool | Description | Permission |
129
131
  |------|-------------|-----------|
130
132
  | `read_file` | Read files with line numbers | auto |
131
- | `write_file` | Create or overwrite files | prompt |
132
- | `edit_file` | Find-and-replace edits | prompt |
133
+ | `write_file` | Create or overwrite files (with undo snapshots) | prompt |
134
+ | `edit_file` | Find-and-replace edits with diff preview + undo | prompt |
135
+ | `batch_edit` | Multi-file atomic find-and-replace | prompt |
133
136
  | `execute` | Run shell commands | always-ask |
134
137
  | `glob` | Find files by pattern | auto |
135
138
  | `grep` | Search file contents with regex | auto |
@@ -168,6 +171,40 @@ CodeBot has persistent memory that survives across sessions:
168
171
  - Memory is automatically injected into the system prompt
169
172
  - The agent can read/write its own memory using the `memory` tool
170
173
 
174
+ ### Plugins
175
+
176
+ Extend CodeBot with custom tools. Drop `.js` files in `.codebot/plugins/` (project) or `~/.codebot/plugins/` (global):
177
+
178
+ ```javascript
179
+ // .codebot/plugins/my-tool.js
180
+ module.exports = {
181
+ name: 'my_tool',
182
+ description: 'Does something useful',
183
+ permission: 'prompt',
184
+ parameters: { type: 'object', properties: { input: { type: 'string' } }, required: ['input'] },
185
+ execute: async (args) => { return `Result: ${args.input}`; }
186
+ };
187
+ ```
188
+
189
+ ### MCP Servers
190
+
191
+ Connect external tool servers via [Model Context Protocol](https://modelcontextprotocol.io). Create `.codebot/mcp.json`:
192
+
193
+ ```json
194
+ {
195
+ "servers": [
196
+ {
197
+ "name": "my-server",
198
+ "command": "npx",
199
+ "args": ["-y", "@my/mcp-server"],
200
+ "env": {}
201
+ }
202
+ ]
203
+ }
204
+ ```
205
+
206
+ MCP tools appear automatically with the `mcp_<server>_<tool>` prefix.
207
+
171
208
  ## Supported Models
172
209
 
173
210
  ### Local (Ollama / LM Studio / vLLM)
@@ -204,8 +241,11 @@ src/
204
241
  registry.ts Model registry, provider detection
205
242
  browser/
206
243
  cdp.ts Chrome DevTools Protocol client (zero-dep WebSocket)
244
+ plugins.ts Plugin loader (.codebot/plugins/)
245
+ mcp.ts MCP (Model Context Protocol) client
207
246
  tools/
208
247
  read.ts, write.ts, edit.ts, execute.ts
248
+ batch-edit.ts Multi-file atomic editing
209
249
  glob.ts, grep.ts, think.ts
210
250
  memory.ts, web-fetch.ts, browser.ts
211
251
  ```
package/dist/agent.js CHANGED
@@ -41,6 +41,7 @@ const manager_1 = require("./context/manager");
41
41
  const repo_map_1 = require("./context/repo-map");
42
42
  const memory_1 = require("./memory");
43
43
  const registry_1 = require("./providers/registry");
44
+ const plugins_1 = require("./plugins");
44
45
  class Agent {
45
46
  provider;
46
47
  tools;
@@ -60,6 +61,14 @@ class Agent {
60
61
  this.autoApprove = opts.autoApprove || false;
61
62
  this.askPermission = opts.askPermission || defaultAskPermission;
62
63
  this.onMessage = opts.onMessage;
64
+ // Load plugins
65
+ try {
66
+ const plugins = (0, plugins_1.loadPlugins)(process.cwd());
67
+ for (const plugin of plugins) {
68
+ this.tools.register(plugin);
69
+ }
70
+ }
71
+ catch { /* plugins unavailable */ }
63
72
  const supportsTools = (0, registry_1.getModelInfo)(opts.model).supportsToolCalling;
64
73
  this.messages.push({
65
74
  role: 'system',
package/dist/cli.js CHANGED
@@ -42,7 +42,10 @@ const registry_1 = require("./providers/registry");
42
42
  const history_1 = require("./history");
43
43
  const setup_1 = require("./setup");
44
44
  const banner_1 = require("./banner");
45
- const VERSION = '1.0.0';
45
+ const tools_1 = require("./tools");
46
+ const VERSION = '1.1.0';
47
+ // Session-wide token tracking
48
+ let sessionTokens = { input: 0, output: 0, total: 0 };
46
49
  const C = {
47
50
  reset: '\x1b[0m',
48
51
  bold: '\x1b[1m',
@@ -229,6 +232,12 @@ function renderEvent(event) {
229
232
  break;
230
233
  case 'usage':
231
234
  if (event.usage) {
235
+ if (event.usage.inputTokens)
236
+ sessionTokens.input += event.usage.inputTokens;
237
+ if (event.usage.outputTokens)
238
+ sessionTokens.output += event.usage.outputTokens;
239
+ if (event.usage.totalTokens)
240
+ sessionTokens.total += event.usage.totalTokens;
232
241
  const parts = [];
233
242
  if (event.usage.inputTokens)
234
243
  parts.push(`in: ${event.usage.inputTokens}`);
@@ -278,6 +287,8 @@ function handleSlashCommand(input, agent, config) {
278
287
  /clear Clear conversation history
279
288
  /compact Force context compaction
280
289
  /auto Toggle autonomous mode
290
+ /undo Undo last file edit (/undo [path])
291
+ /usage Show token usage for this session
281
292
  /config Show current config
282
293
  /quit Exit`);
283
294
  break;
@@ -328,6 +339,19 @@ function handleSlashCommand(input, agent, config) {
328
339
  }
329
340
  break;
330
341
  }
342
+ case '/undo': {
343
+ const undoPath = rest.length > 0 ? rest.join(' ') : undefined;
344
+ const undoResult = tools_1.EditFileTool.undo(undoPath);
345
+ console.log(c(undoResult, undoResult.includes('Restored') ? 'green' : 'yellow'));
346
+ break;
347
+ }
348
+ case '/usage': {
349
+ console.log(c('\nToken Usage (this session):', 'bold'));
350
+ console.log(` Input: ${sessionTokens.input.toLocaleString()} tokens`);
351
+ console.log(` Output: ${sessionTokens.output.toLocaleString()} tokens`);
352
+ console.log(` Total: ${(sessionTokens.input + sessionTokens.output).toLocaleString()} tokens`);
353
+ break;
354
+ }
331
355
  case '/config':
332
356
  console.log(JSON.stringify({ ...config, apiKey: config.apiKey ? '***' : undefined }, null, 2));
333
357
  break;
package/dist/index.d.ts CHANGED
@@ -7,6 +7,8 @@ export { buildRepoMap } from './context/repo-map';
7
7
  export { SessionManager } from './history';
8
8
  export { MemoryManager } from './memory';
9
9
  export { parseToolCalls } from './parser';
10
+ export { loadPlugins } from './plugins';
11
+ export { loadMCPTools } from './mcp';
10
12
  export { MODEL_REGISTRY, PROVIDER_DEFAULTS, getModelInfo, detectProvider } from './providers/registry';
11
13
  export type { ModelInfo } from './providers/registry';
12
14
  export * from './types';
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.detectProvider = exports.getModelInfo = exports.PROVIDER_DEFAULTS = exports.MODEL_REGISTRY = exports.parseToolCalls = exports.MemoryManager = exports.SessionManager = exports.buildRepoMap = exports.ContextManager = exports.ToolRegistry = exports.AnthropicProvider = exports.OpenAIProvider = exports.Agent = void 0;
17
+ exports.detectProvider = exports.getModelInfo = exports.PROVIDER_DEFAULTS = exports.MODEL_REGISTRY = exports.loadMCPTools = exports.loadPlugins = exports.parseToolCalls = exports.MemoryManager = exports.SessionManager = exports.buildRepoMap = exports.ContextManager = exports.ToolRegistry = exports.AnthropicProvider = exports.OpenAIProvider = exports.Agent = void 0;
18
18
  var agent_1 = require("./agent");
19
19
  Object.defineProperty(exports, "Agent", { enumerable: true, get: function () { return agent_1.Agent; } });
20
20
  var openai_1 = require("./providers/openai");
@@ -33,6 +33,10 @@ var memory_1 = require("./memory");
33
33
  Object.defineProperty(exports, "MemoryManager", { enumerable: true, get: function () { return memory_1.MemoryManager; } });
34
34
  var parser_1 = require("./parser");
35
35
  Object.defineProperty(exports, "parseToolCalls", { enumerable: true, get: function () { return parser_1.parseToolCalls; } });
36
+ var plugins_1 = require("./plugins");
37
+ Object.defineProperty(exports, "loadPlugins", { enumerable: true, get: function () { return plugins_1.loadPlugins; } });
38
+ var mcp_1 = require("./mcp");
39
+ Object.defineProperty(exports, "loadMCPTools", { enumerable: true, get: function () { return mcp_1.loadMCPTools; } });
36
40
  var registry_1 = require("./providers/registry");
37
41
  Object.defineProperty(exports, "MODEL_REGISTRY", { enumerable: true, get: function () { return registry_1.MODEL_REGISTRY; } });
38
42
  Object.defineProperty(exports, "PROVIDER_DEFAULTS", { enumerable: true, get: function () { return registry_1.PROVIDER_DEFAULTS; } });
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { Tool } from './types';
2
+ /** Load MCP config and connect to all servers, returning Tool wrappers */
3
+ export declare function loadMCPTools(projectRoot?: string): Promise<{
4
+ tools: Tool[];
5
+ cleanup: () => void;
6
+ }>;
7
+ //# sourceMappingURL=mcp.d.ts.map
package/dist/mcp.js ADDED
@@ -0,0 +1,174 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.loadMCPTools = loadMCPTools;
37
+ const child_process_1 = require("child_process");
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const os = __importStar(require("os"));
41
+ class MCPConnection {
42
+ process;
43
+ buffer = '';
44
+ requestId = 0;
45
+ pending = new Map();
46
+ name;
47
+ constructor(config) {
48
+ this.name = config.name;
49
+ this.process = (0, child_process_1.spawn)(config.command, config.args || [], {
50
+ stdio: ['pipe', 'pipe', 'pipe'],
51
+ env: { ...process.env, ...config.env },
52
+ });
53
+ this.process.stdout?.on('data', (chunk) => {
54
+ this.buffer += chunk.toString();
55
+ this.processBuffer();
56
+ });
57
+ this.process.on('error', () => { });
58
+ }
59
+ processBuffer() {
60
+ const lines = this.buffer.split('\n');
61
+ this.buffer = lines.pop() || '';
62
+ for (const line of lines) {
63
+ if (!line.trim())
64
+ continue;
65
+ try {
66
+ const msg = JSON.parse(line);
67
+ if (msg.id !== undefined && this.pending.has(msg.id)) {
68
+ const { resolve, reject } = this.pending.get(msg.id);
69
+ this.pending.delete(msg.id);
70
+ if (msg.error) {
71
+ reject(new Error(msg.error.message || 'MCP error'));
72
+ }
73
+ else {
74
+ resolve(msg.result);
75
+ }
76
+ }
77
+ }
78
+ catch { /* skip malformed */ }
79
+ }
80
+ }
81
+ async request(method, params) {
82
+ const id = ++this.requestId;
83
+ const msg = JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n';
84
+ return new Promise((resolve, reject) => {
85
+ this.pending.set(id, { resolve, reject });
86
+ this.process.stdin?.write(msg);
87
+ // Timeout after 30s
88
+ setTimeout(() => {
89
+ if (this.pending.has(id)) {
90
+ this.pending.delete(id);
91
+ reject(new Error(`MCP request timeout: ${method}`));
92
+ }
93
+ }, 30000);
94
+ });
95
+ }
96
+ async initialize() {
97
+ await this.request('initialize', {
98
+ protocolVersion: '2024-11-05',
99
+ capabilities: {},
100
+ clientInfo: { name: 'codebot-ai', version: '1.1.0' },
101
+ });
102
+ await this.request('notifications/initialized');
103
+ }
104
+ async listTools() {
105
+ const result = await this.request('tools/list');
106
+ return result?.tools || [];
107
+ }
108
+ async callTool(name, args) {
109
+ const result = await this.request('tools/call', { name, arguments: args });
110
+ if (result?.content) {
111
+ return result.content
112
+ .filter(c => c.type === 'text')
113
+ .map(c => c.text || '')
114
+ .join('\n');
115
+ }
116
+ return JSON.stringify(result);
117
+ }
118
+ close() {
119
+ this.process.kill();
120
+ }
121
+ }
122
+ /** Create Tool wrappers from an MCP server's tools */
123
+ function mcpToolToTool(connection, def) {
124
+ return {
125
+ name: `mcp_${connection.name}_${def.name}`,
126
+ description: `[MCP:${connection.name}] ${def.description}`,
127
+ permission: 'prompt',
128
+ parameters: def.inputSchema || { type: 'object', properties: {} },
129
+ execute: async (args) => {
130
+ return connection.callTool(def.name, args);
131
+ },
132
+ };
133
+ }
134
+ /** Load MCP config and connect to all servers, returning Tool wrappers */
135
+ async function loadMCPTools(projectRoot) {
136
+ const tools = [];
137
+ const connections = [];
138
+ const configPaths = [
139
+ path.join(os.homedir(), '.codebot', 'mcp.json'),
140
+ ];
141
+ if (projectRoot) {
142
+ configPaths.push(path.join(projectRoot, '.codebot', 'mcp.json'));
143
+ }
144
+ for (const configPath of configPaths) {
145
+ if (!fs.existsSync(configPath))
146
+ continue;
147
+ let config;
148
+ try {
149
+ config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
150
+ }
151
+ catch {
152
+ continue;
153
+ }
154
+ for (const serverConfig of config.servers || []) {
155
+ try {
156
+ const conn = new MCPConnection(serverConfig);
157
+ await conn.initialize();
158
+ const serverTools = await conn.listTools();
159
+ for (const toolDef of serverTools) {
160
+ tools.push(mcpToolToTool(conn, toolDef));
161
+ }
162
+ connections.push(conn);
163
+ }
164
+ catch (err) {
165
+ console.error(`MCP server "${serverConfig.name}" failed: ${err instanceof Error ? err.message : String(err)}`);
166
+ }
167
+ }
168
+ }
169
+ return {
170
+ tools,
171
+ cleanup: () => connections.forEach(c => c.close()),
172
+ };
173
+ }
174
+ //# sourceMappingURL=mcp.js.map
@@ -0,0 +1,17 @@
1
+ import { Tool } from './types';
2
+ /**
3
+ * Plugin system for CodeBot.
4
+ *
5
+ * Plugins are .js files in `.codebot/plugins/` (project) or `~/.codebot/plugins/` (global).
6
+ * Each plugin exports a default function or object that implements the Tool interface:
7
+ *
8
+ * module.exports = {
9
+ * name: 'my_tool',
10
+ * description: 'Does something useful',
11
+ * permission: 'prompt',
12
+ * parameters: { type: 'object', properties: { ... }, required: [...] },
13
+ * execute: async (args) => { return 'result'; }
14
+ * };
15
+ */
16
+ export declare function loadPlugins(projectRoot?: string): Tool[];
17
+ //# sourceMappingURL=plugins.d.ts.map
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.loadPlugins = loadPlugins;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ /**
40
+ * Plugin system for CodeBot.
41
+ *
42
+ * Plugins are .js files in `.codebot/plugins/` (project) or `~/.codebot/plugins/` (global).
43
+ * Each plugin exports a default function or object that implements the Tool interface:
44
+ *
45
+ * module.exports = {
46
+ * name: 'my_tool',
47
+ * description: 'Does something useful',
48
+ * permission: 'prompt',
49
+ * parameters: { type: 'object', properties: { ... }, required: [...] },
50
+ * execute: async (args) => { return 'result'; }
51
+ * };
52
+ */
53
+ function loadPlugins(projectRoot) {
54
+ const plugins = [];
55
+ const os = require('os');
56
+ const dirs = [
57
+ path.join(os.homedir(), '.codebot', 'plugins'),
58
+ ];
59
+ if (projectRoot) {
60
+ dirs.push(path.join(projectRoot, '.codebot', 'plugins'));
61
+ }
62
+ for (const dir of dirs) {
63
+ if (!fs.existsSync(dir))
64
+ continue;
65
+ let entries;
66
+ try {
67
+ entries = fs.readdirSync(dir, { withFileTypes: true });
68
+ }
69
+ catch {
70
+ continue;
71
+ }
72
+ for (const entry of entries) {
73
+ if (!entry.isFile() || !entry.name.endsWith('.js'))
74
+ continue;
75
+ try {
76
+ const pluginPath = path.join(dir, entry.name);
77
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
78
+ const mod = require(pluginPath);
79
+ const plugin = mod.default || mod;
80
+ if (isValidTool(plugin)) {
81
+ plugins.push(plugin);
82
+ }
83
+ }
84
+ catch (err) {
85
+ console.error(`Plugin load error (${entry.name}): ${err instanceof Error ? err.message : String(err)}`);
86
+ }
87
+ }
88
+ }
89
+ return plugins;
90
+ }
91
+ function isValidTool(obj) {
92
+ if (!obj || typeof obj !== 'object')
93
+ return false;
94
+ const t = obj;
95
+ return (typeof t.name === 'string' &&
96
+ typeof t.description === 'string' &&
97
+ typeof t.execute === 'function' &&
98
+ typeof t.parameters === 'object' &&
99
+ typeof t.permission === 'string');
100
+ }
101
+ //# sourceMappingURL=plugins.js.map
@@ -0,0 +1,36 @@
1
+ import { Tool } from '../types';
2
+ export declare class BatchEditTool implements Tool {
3
+ name: string;
4
+ description: string;
5
+ permission: Tool['permission'];
6
+ parameters: {
7
+ type: string;
8
+ properties: {
9
+ edits: {
10
+ type: string;
11
+ description: string;
12
+ items: {
13
+ type: string;
14
+ properties: {
15
+ path: {
16
+ type: string;
17
+ description: string;
18
+ };
19
+ old_string: {
20
+ type: string;
21
+ description: string;
22
+ };
23
+ new_string: {
24
+ type: string;
25
+ description: string;
26
+ };
27
+ };
28
+ required: string[];
29
+ };
30
+ };
31
+ };
32
+ required: string[];
33
+ };
34
+ execute(args: Record<string, unknown>): Promise<string>;
35
+ }
36
+ //# sourceMappingURL=batch-edit.d.ts.map
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.BatchEditTool = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ class BatchEditTool {
40
+ name = 'batch_edit';
41
+ description = 'Apply multiple find-and-replace edits across one or more files atomically. All edits are validated before any are applied. Useful for renaming, refactoring, and coordinated multi-file changes.';
42
+ permission = 'prompt';
43
+ parameters = {
44
+ type: 'object',
45
+ properties: {
46
+ edits: {
47
+ type: 'array',
48
+ description: 'Array of edit operations: [{path, old_string, new_string}, ...]',
49
+ items: {
50
+ type: 'object',
51
+ properties: {
52
+ path: { type: 'string', description: 'File path' },
53
+ old_string: { type: 'string', description: 'Exact string to find' },
54
+ new_string: { type: 'string', description: 'Replacement string' },
55
+ },
56
+ required: ['path', 'old_string', 'new_string'],
57
+ },
58
+ },
59
+ },
60
+ required: ['edits'],
61
+ };
62
+ async execute(args) {
63
+ const edits = args.edits;
64
+ if (!edits || !Array.isArray(edits) || edits.length === 0) {
65
+ return 'Error: edits array is required and must not be empty';
66
+ }
67
+ // Phase 1: Validate all edits before applying any
68
+ const errors = [];
69
+ const validated = [];
70
+ // Group edits by file so we can chain them
71
+ const byFile = new Map();
72
+ for (const edit of edits) {
73
+ if (!edit.path || !edit.old_string === undefined) {
74
+ errors.push(`Invalid edit: missing path or old_string`);
75
+ continue;
76
+ }
77
+ const filePath = path.resolve(edit.path);
78
+ if (!byFile.has(filePath))
79
+ byFile.set(filePath, []);
80
+ byFile.get(filePath).push(edit);
81
+ }
82
+ for (const [filePath, fileEdits] of byFile) {
83
+ if (!fs.existsSync(filePath)) {
84
+ errors.push(`File not found: ${filePath}`);
85
+ continue;
86
+ }
87
+ let content = fs.readFileSync(filePath, 'utf-8');
88
+ const originalContent = content;
89
+ for (const edit of fileEdits) {
90
+ const oldStr = String(edit.old_string);
91
+ const newStr = String(edit.new_string);
92
+ const count = content.split(oldStr).length - 1;
93
+ if (count === 0) {
94
+ errors.push(`String not found in ${filePath}: "${oldStr.substring(0, 60)}${oldStr.length > 60 ? '...' : ''}"`);
95
+ continue;
96
+ }
97
+ if (count > 1) {
98
+ errors.push(`String found ${count} times in ${filePath} (must be unique): "${oldStr.substring(0, 60)}${oldStr.length > 60 ? '...' : ''}"`);
99
+ continue;
100
+ }
101
+ content = content.replace(oldStr, newStr);
102
+ }
103
+ if (content !== originalContent) {
104
+ validated.push({ filePath, content: originalContent, updated: content, edit: fileEdits[0] });
105
+ }
106
+ }
107
+ if (errors.length > 0) {
108
+ return `Validation failed (no changes made):\n${errors.map(e => ` - ${e}`).join('\n')}`;
109
+ }
110
+ // Phase 2: Apply all edits atomically
111
+ const results = [];
112
+ for (const { filePath, updated } of validated) {
113
+ fs.writeFileSync(filePath, updated, 'utf-8');
114
+ results.push(filePath);
115
+ }
116
+ const fileCount = validated.length;
117
+ const editCount = edits.length;
118
+ return `Applied ${editCount} edit${editCount > 1 ? 's' : ''} across ${fileCount} file${fileCount > 1 ? 's' : ''}:\n${results.map(f => ` ✓ ${f}`).join('\n')}`;
119
+ }
120
+ }
121
+ exports.BatchEditTool = BatchEditTool;
122
+ //# sourceMappingURL=batch-edit.js.map
@@ -22,5 +22,11 @@ export declare class EditFileTool implements Tool {
22
22
  required: string[];
23
23
  };
24
24
  execute(args: Record<string, unknown>): Promise<string>;
25
+ private generateDiff;
26
+ /** Save a snapshot for undo */
27
+ private saveSnapshot;
28
+ private loadManifest;
29
+ /** Undo the last edit to a file. Returns result message. */
30
+ static undo(filePath?: string): string;
25
31
  }
26
32
  //# sourceMappingURL=edit.d.ts.map
@@ -36,9 +36,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.EditFileTool = void 0;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
+ const os = __importStar(require("os"));
40
+ // Undo snapshot directory
41
+ const UNDO_DIR = path.join(os.homedir(), '.codebot', 'undo');
42
+ const MAX_UNDO = 50;
39
43
  class EditFileTool {
40
44
  name = 'edit_file';
41
- description = 'Edit a file by replacing an exact string match with new content. The old_string must appear exactly once in the file.';
45
+ description = 'Edit a file by replacing an exact string match with new content. The old_string must appear exactly once in the file. Shows a diff preview and creates an undo snapshot.';
42
46
  permission = 'prompt';
43
47
  parameters = {
44
48
  type: 'object',
@@ -73,9 +77,120 @@ class EditFileTool {
73
77
  if (count > 1) {
74
78
  throw new Error(`String found ${count} times in ${filePath}. Provide more surrounding context to make it unique.`);
75
79
  }
80
+ // Save undo snapshot
81
+ this.saveSnapshot(filePath, content);
76
82
  const updated = content.replace(oldStr, newStr);
77
83
  fs.writeFileSync(filePath, updated, 'utf-8');
78
- return `Edited ${filePath} (1 replacement)`;
84
+ // Generate diff preview
85
+ const diff = this.generateDiff(oldStr, newStr, content, filePath);
86
+ return diff;
87
+ }
88
+ generateDiff(oldStr, newStr, content, filePath) {
89
+ const lines = content.split('\n');
90
+ const matchIdx = content.indexOf(oldStr);
91
+ const linesBefore = content.substring(0, matchIdx).split('\n');
92
+ const startLine = linesBefore.length;
93
+ const oldLines = oldStr.split('\n');
94
+ const newLines = newStr.split('\n');
95
+ let diff = `Edited ${filePath}\n`;
96
+ // Show context (2 lines before)
97
+ const contextStart = Math.max(0, startLine - 3);
98
+ for (let i = contextStart; i < startLine - 1; i++) {
99
+ diff += ` ${i + 1} │ ${lines[i]}\n`;
100
+ }
101
+ // Show removed lines
102
+ for (const line of oldLines) {
103
+ diff += ` - │ ${line}\n`;
104
+ }
105
+ // Show added lines
106
+ for (const line of newLines) {
107
+ diff += ` + │ ${line}\n`;
108
+ }
109
+ // Show context (2 lines after)
110
+ const endLine = startLine - 1 + oldLines.length;
111
+ for (let i = endLine; i < Math.min(lines.length, endLine + 2); i++) {
112
+ diff += ` ${i + 1} │ ${lines[i]}\n`;
113
+ }
114
+ return diff.trimEnd();
115
+ }
116
+ /** Save a snapshot for undo */
117
+ saveSnapshot(filePath, content) {
118
+ try {
119
+ fs.mkdirSync(UNDO_DIR, { recursive: true });
120
+ const manifest = this.loadManifest();
121
+ const entry = {
122
+ file: filePath,
123
+ timestamp: Date.now(),
124
+ snapshotFile: `${Date.now()}-${path.basename(filePath)}`,
125
+ };
126
+ // Write snapshot content
127
+ fs.writeFileSync(path.join(UNDO_DIR, entry.snapshotFile), content);
128
+ manifest.push(entry);
129
+ // Prune old snapshots
130
+ while (manifest.length > MAX_UNDO) {
131
+ const old = manifest.shift();
132
+ try {
133
+ fs.unlinkSync(path.join(UNDO_DIR, old.snapshotFile));
134
+ }
135
+ catch { /* ok */ }
136
+ }
137
+ fs.writeFileSync(path.join(UNDO_DIR, 'manifest.json'), JSON.stringify(manifest, null, 2));
138
+ }
139
+ catch {
140
+ // Best-effort, don't fail the edit
141
+ }
142
+ }
143
+ loadManifest() {
144
+ try {
145
+ const raw = fs.readFileSync(path.join(UNDO_DIR, 'manifest.json'), 'utf-8');
146
+ return JSON.parse(raw);
147
+ }
148
+ catch {
149
+ return [];
150
+ }
151
+ }
152
+ /** Undo the last edit to a file. Returns result message. */
153
+ static undo(filePath) {
154
+ try {
155
+ const manifestPath = path.join(UNDO_DIR, 'manifest.json');
156
+ if (!fs.existsSync(manifestPath))
157
+ return 'No undo history available.';
158
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
159
+ if (manifest.length === 0)
160
+ return 'No undo history available.';
161
+ // Find the entry to undo
162
+ let entry;
163
+ if (filePath) {
164
+ const resolved = path.resolve(filePath);
165
+ for (let i = manifest.length - 1; i >= 0; i--) {
166
+ if (manifest[i].file === resolved) {
167
+ entry = manifest.splice(i, 1)[0];
168
+ break;
169
+ }
170
+ }
171
+ if (!entry)
172
+ return `No undo history for ${filePath}`;
173
+ }
174
+ else {
175
+ entry = manifest.pop();
176
+ }
177
+ // Restore the snapshot
178
+ const snapshotPath = path.join(UNDO_DIR, entry.snapshotFile);
179
+ if (!fs.existsSync(snapshotPath))
180
+ return 'Snapshot file missing.';
181
+ const content = fs.readFileSync(snapshotPath, 'utf-8');
182
+ fs.writeFileSync(entry.file, content, 'utf-8');
183
+ // Cleanup
184
+ try {
185
+ fs.unlinkSync(snapshotPath);
186
+ }
187
+ catch { /* ok */ }
188
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
189
+ return `Restored ${entry.file} to state before last edit.`;
190
+ }
191
+ catch (err) {
192
+ return `Undo failed: ${err instanceof Error ? err.message : String(err)}`;
193
+ }
79
194
  }
80
195
  }
81
196
  exports.EditFileTool = EditFileTool;
@@ -3,14 +3,42 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ExecuteTool = void 0;
4
4
  const child_process_1 = require("child_process");
5
5
  const BLOCKED_PATTERNS = [
6
+ // Destructive filesystem operations
6
7
  /rm\s+-rf\s+\//,
7
8
  /rm\s+-rf\s+~/,
8
9
  /rm\s+-rf\s+\*/,
10
+ /rm\s+-rf\s+\.\s/,
11
+ /rm\s+(-[a-z]*f[a-z]*\s+)?--no-preserve-root/,
12
+ // Disk/partition destruction
9
13
  /mkfs\./,
10
14
  /dd\s+if=.*of=\/dev\//,
15
+ /wipefs/,
16
+ /fdisk\s+\/dev\//,
17
+ /parted\s+\/dev\//,
18
+ // Fork bomb
11
19
  /:\(\)\s*\{[^}]*:\|:.*\}/,
20
+ // Permission escalation
12
21
  /chmod\s+-R\s+777\s+\//,
22
+ /chmod\s+777\s+\//,
23
+ /chown\s+-R\s+.*\s+\//,
24
+ // Windows destructive
13
25
  /format\s+c:/i,
26
+ /del\s+\/[sfq]\s+c:\\/i,
27
+ /rd\s+\/s\s+\/q\s+c:\\/i,
28
+ // Curl to shell pipes (common attack vector)
29
+ /curl\s+.*\|\s*(ba)?sh/,
30
+ /wget\s+.*\|\s*(ba)?sh/,
31
+ // History/log destruction
32
+ />\s*\/dev\/sda/,
33
+ /history\s+-c.*&&.*rm/,
34
+ // Shutdown/reboot
35
+ /shutdown\s+(-h\s+)?now/,
36
+ /reboot\b/,
37
+ /init\s+[06]/,
38
+ // Kernel module manipulation
39
+ /insmod\b/,
40
+ /rmmod\b/,
41
+ /modprobe\s+-r/,
14
42
  ];
15
43
  class ExecuteTool {
16
44
  name = 'execute';
@@ -1,4 +1,5 @@
1
1
  import { Tool, ToolSchema } from '../types';
2
+ export { EditFileTool } from './edit';
2
3
  export declare class ToolRegistry {
3
4
  private tools;
4
5
  constructor(projectRoot?: string);
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ToolRegistry = void 0;
3
+ exports.ToolRegistry = exports.EditFileTool = void 0;
4
4
  const read_1 = require("./read");
5
5
  const write_1 = require("./write");
6
6
  const edit_1 = require("./edit");
@@ -11,12 +11,16 @@ const think_1 = require("./think");
11
11
  const memory_1 = require("./memory");
12
12
  const web_fetch_1 = require("./web-fetch");
13
13
  const browser_1 = require("./browser");
14
+ const batch_edit_1 = require("./batch-edit");
15
+ var edit_2 = require("./edit");
16
+ Object.defineProperty(exports, "EditFileTool", { enumerable: true, get: function () { return edit_2.EditFileTool; } });
14
17
  class ToolRegistry {
15
18
  tools = new Map();
16
19
  constructor(projectRoot) {
17
20
  this.register(new read_1.ReadFileTool());
18
21
  this.register(new write_1.WriteFileTool());
19
22
  this.register(new edit_1.EditFileTool());
23
+ this.register(new batch_edit_1.BatchEditTool());
20
24
  this.register(new execute_1.ExecuteTool());
21
25
  this.register(new glob_1.GlobTool());
22
26
  this.register(new grep_1.GrepTool());
@@ -18,5 +18,6 @@ export declare class WriteFileTool implements Tool {
18
18
  required: string[];
19
19
  };
20
20
  execute(args: Record<string, unknown>): Promise<string>;
21
+ private saveSnapshot;
21
22
  }
22
23
  //# sourceMappingURL=write.d.ts.map
@@ -36,9 +36,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.WriteFileTool = void 0;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
+ const os = __importStar(require("os"));
40
+ const UNDO_DIR = path.join(os.homedir(), '.codebot', 'undo');
39
41
  class WriteFileTool {
40
42
  name = 'write_file';
41
- description = 'Create a new file or overwrite an existing file with the given content.';
43
+ description = 'Create a new file or overwrite an existing file with the given content. Automatically saves an undo snapshot for existing files.';
42
44
  permission = 'prompt';
43
45
  parameters = {
44
46
  type: 'object',
@@ -62,10 +64,45 @@ class WriteFileTool {
62
64
  fs.mkdirSync(dir, { recursive: true });
63
65
  }
64
66
  const existed = fs.existsSync(filePath);
67
+ // Save undo snapshot before overwriting
68
+ if (existed) {
69
+ try {
70
+ const oldContent = fs.readFileSync(filePath, 'utf-8');
71
+ this.saveSnapshot(filePath, oldContent);
72
+ }
73
+ catch { /* best effort */ }
74
+ }
65
75
  fs.writeFileSync(filePath, content, 'utf-8');
66
76
  const lines = content.split('\n').length;
67
77
  return `${existed ? 'Overwrote' : 'Created'} ${filePath} (${lines} lines, ${content.length} bytes)`;
68
78
  }
79
+ saveSnapshot(filePath, content) {
80
+ try {
81
+ fs.mkdirSync(UNDO_DIR, { recursive: true });
82
+ const manifestPath = path.join(UNDO_DIR, 'manifest.json');
83
+ let manifest = [];
84
+ try {
85
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
86
+ }
87
+ catch { /* empty */ }
88
+ const entry = {
89
+ file: filePath,
90
+ timestamp: Date.now(),
91
+ snapshotFile: `${Date.now()}-${path.basename(filePath)}`,
92
+ };
93
+ fs.writeFileSync(path.join(UNDO_DIR, entry.snapshotFile), content);
94
+ manifest.push(entry);
95
+ while (manifest.length > 50) {
96
+ const old = manifest.shift();
97
+ try {
98
+ fs.unlinkSync(path.join(UNDO_DIR, old.snapshotFile));
99
+ }
100
+ catch { /* ok */ }
101
+ }
102
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
103
+ }
104
+ catch { /* best effort */ }
105
+ }
69
106
  }
70
107
  exports.WriteFileTool = WriteFileTool;
71
108
  //# sourceMappingURL=write.js.map
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "codebot-ai",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
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",
7
7
  "bin": {
8
- "codebot": "./bin/codebot"
8
+ "codebot": "bin/codebot"
9
9
  },
10
10
  "scripts": {
11
11
  "build": "tsc",