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 +43 -3
- package/dist/agent.js +9 -0
- package/dist/cli.js +25 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -1
- package/dist/mcp.d.ts +7 -0
- package/dist/mcp.js +174 -0
- package/dist/plugins.d.ts +17 -0
- package/dist/plugins.js +101 -0
- package/dist/tools/batch-edit.d.ts +36 -0
- package/dist/tools/batch-edit.js +122 -0
- package/dist/tools/edit.d.ts +6 -0
- package/dist/tools/edit.js +117 -2
- package/dist/tools/execute.js +28 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +5 -1
- package/dist/tools/write.d.ts +1 -0
- package/dist/tools/write.js +38 -1
- package/package.json +2 -2
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
|
|
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
|
|
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
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
|
package/dist/plugins.js
ADDED
|
@@ -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
|
package/dist/tools/edit.d.ts
CHANGED
|
@@ -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
|
package/dist/tools/edit.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/tools/execute.js
CHANGED
|
@@ -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';
|
package/dist/tools/index.d.ts
CHANGED
package/dist/tools/index.js
CHANGED
|
@@ -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());
|
package/dist/tools/write.d.ts
CHANGED
package/dist/tools/write.js
CHANGED
|
@@ -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
|
|
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": "
|
|
8
|
+
"codebot": "bin/codebot"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "tsc",
|