codebot-ai 1.0.2 → 1.1.1

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.
@@ -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.1",
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",
@@ -33,11 +33,11 @@
33
33
  "license": "MIT",
34
34
  "repository": {
35
35
  "type": "git",
36
- "url": "git+https://github.com/AscendralSoftware/codebot-ai.git"
36
+ "url": "git+https://github.com/zanderone1980/codebot-ai.git"
37
37
  },
38
- "homepage": "https://github.com/AscendralSoftware/codebot-ai#readme",
38
+ "homepage": "https://github.com/zanderone1980/codebot-ai#readme",
39
39
  "bugs": {
40
- "url": "https://github.com/AscendralSoftware/codebot-ai/issues"
40
+ "url": "https://github.com/zanderone1980/codebot-ai/issues"
41
41
  },
42
42
  "engines": {
43
43
  "node": ">=18.0.0"