@krmxd/onegpt 0.1.9-beta → 0.2.1-beta

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krmxd/onegpt",
3
- "version": "0.1.9-beta",
3
+ "version": "0.2.1-beta",
4
4
  "description": "OGPT - AI coding assistant for the terminal with a built-in local engine",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/agent.js CHANGED
@@ -85,6 +85,9 @@ const TOOL_GUIDANCE = (
85
85
  "boilerplate, never code dumped only in chat.\n" +
86
86
  "- write_file creates a file AND all missing parent folders automatically, so a separate " +
87
87
  "make_dir call is usually unnecessary - just call write_file with the full path.\n" +
88
+ "- You also have move_path, copy_path and delete_path to reorganize files, and " +
89
+ "web_search (Google) + web_fetch for live documentation - prefer checking real docs " +
90
+ "over guessing APIs.\n" +
88
91
  "- Complete EVERY part of the request before replying. If asked for a folder and files " +
89
92
  "inside it, make all the tool calls in one turn: one write_file per file.\n" +
90
93
  "- Never stop halfway: after each tool result, continue with the next step until the " +
@@ -128,7 +131,15 @@ const EXECUTE_NUDGE =
128
131
 
129
132
  function wantsAction(userInput) {
130
133
  // True when the user asked for something to be built (not explained).
131
- return TASK_RE.test(userInput) && !QUESTION_RE.test(userInput.trim());
134
+ const t = String(userInput || "").trim();
135
+ const tm = TASK_RE.exec(t);
136
+ if (!tm) return false;
137
+ // A question word BEFORE the build verb means they're ASKING, not
138
+ // requesting work: "how do I fix the login bug?" must be answered,
139
+ // not turned into a flurry of file edits.
140
+ const qm = /\b(how|what|why|when|where|which|who|whom|explain|describe)\b/i.exec(t);
141
+ if (qm && qm.index < tm.index) return false;
142
+ return true;
132
143
  }
133
144
 
134
145
  function redirectMsg(task) {
@@ -214,11 +225,16 @@ The user asked you to build a web project. Deliver a COMPLETE working site:
214
225
  1. PLAN first - list every file you will create (2-4 lines max).
215
226
  2. Create ALL of these in a project folder: index.html, styles.css, script.js.
216
227
  3. Quality bar (mandatory):
217
- - index.html: semantic HTML5 with viewport meta, linking styles.css and script.js
218
- - styles.css: modern design - custom properties, flexbox/grid layout, responsive media queries, hover/focus states (aim for 80+ lines)
219
- - script.js: real interactivity wired to actual element ids from your HTML (aim for 40+ lines)
228
+ - index.html: semantic HTML5 (header/nav/main/footer), viewport meta, aria-labels
229
+ on interactive elements, linking styles.css and script.js with defer
230
+ - styles.css: modern design - custom properties, flexbox/grid layout, responsive
231
+ media queries, hover/focus states, prefers-color-scheme support (aim for 80+ lines)
232
+ - script.js: real interactivity wired to actual element ids from your HTML,
233
+ event listeners + DOM updates (aim for 40+ lines)
220
234
  4. Zero placeholders ("TODO", "content here", "..."). Every button and link does something real.
221
- 5. VERIFY each file exists (read_file/list_files), then summarize how to open it.
235
+ 5. Unsure about an API or library? Use web_search to check real docs BEFORE writing
236
+ code that guesses. CDN libs via <script src> are allowed when they help.
237
+ 6. VERIFY each file exists (read_file/list_files), then summarize how to open it.
222
238
  Do NOT say done until every file from your plan exists on disk.`;
223
239
 
224
240
  const PROJECT_CODE_BRIEF = `
@@ -623,8 +639,11 @@ class Agent {
623
639
  // Self-correction: instructions/code instead of done work gets one
624
640
  // firm push to act - on ANY round, not just the first (tiny models
625
641
  // relapse into how-to mode mid-task). An EMPTY reply after partial
626
- // work means it stalled - push it too.
627
- const replyLazy = repliedWithoutActing(resp.content);
642
+ // work means it stalled - push it too. BUT a reply is never "lazy"
643
+ // once real work happened this turn: closing summaries often quote
644
+ // commands in fences ("run it with ```npm start```") - nudging then
645
+ // made the model edit files over and over instead of answering.
646
+ const replyLazy = repliedWithoutActing(resp.content) && doneBits.length === 0;
628
647
  const wentSilent = !resp.content.trim() && (rounds > 1 || doneBits.length > 0);
629
648
  if (nudges < 3 && !this._cancelled
630
649
  && wantsAction(userInput) && (replyLazy || wentSilent || stalledHalfway(resp.content, userInput, doneBits))) {
@@ -776,8 +795,11 @@ class Agent {
776
795
  // Self-correction: instructions/code instead of done work gets one
777
796
  // firm push to act - on ANY round, not just the first (tiny models
778
797
  // relapse into how-to mode mid-task). An EMPTY reply after partial
779
- // work means it stalled - push it too.
780
- const replyLazy = repliedWithoutActing(full);
798
+ // work means it stalled - push it too. BUT a reply is never "lazy"
799
+ // once real work happened this turn: closing summaries often quote
800
+ // commands in fences ("run it with ```npm start```") - nudging then
801
+ // made the model edit files over and over instead of answering.
802
+ const replyLazy = repliedWithoutActing(full) && doneBits.length === 0;
781
803
  const wentSilent = !full.trim() && (rounds > 1 || doneBits.length > 0);
782
804
  if (nudges < 3 && !this._cancelled
783
805
  && wantsAction(userInput) && (replyLazy || wentSilent || stalledHalfway(full, userInput, doneBits))) {
@@ -884,4 +906,4 @@ class Agent {
884
906
  }
885
907
  }
886
908
 
887
- module.exports = { Agent, extractJsonTools, repairToolJson };
909
+ module.exports = { Agent, extractJsonTools, repairToolJson, wantsAction, repliedWithoutActing, stalledHalfway };
package/src/cli.js CHANGED
@@ -11,6 +11,7 @@ const { Agent } = require("./agent");
11
11
  const Platform = require("./platform");
12
12
  const catalog = require("./catalog");
13
13
  const web = require("./web");
14
+ const { renderMarkdown, SmoothPrinter } = require("./render");
14
15
 
15
16
  // Single source of truth: package.json version + device-build tag.
16
17
  const PKG = require("../package.json");
@@ -200,29 +201,36 @@ class CLI {
200
201
  const stream = this.cfg.get("ui.stream", true);
201
202
  const collected = [];
202
203
  this._chatActive = true;
204
+ let printer = null;
203
205
  try {
204
206
  if (stream) {
205
207
  this._raw("\n");
206
208
  this._thinkingStart();
207
209
  let thinkingShown = process.stdout.isTTY === true;
210
+ printer = new SmoothPrinter();
211
+ printer.begin();
208
212
  for await (const chunk of this.agent.runStream(userInput)) {
209
213
  if (chunk) {
210
214
  if (thinkingShown) { this._thinkingClear(); thinkingShown = false; }
211
215
  collected.push(chunk);
212
- this._raw(chunk);
216
+ printer.feed(chunk);
213
217
  }
214
218
  }
215
- // Only clear the indicator - never touch the response text.
219
+ // Repaints the whole message in place: boxed code, styled markdown.
216
220
  if (thinkingShown) { this._thinkingClear(); thinkingShown = false; }
221
+ printer.finish();
217
222
  this._raw("\n");
218
223
  web.recordRun(this.agent.lastRun);
219
224
  this._timingFooter();
220
225
  } else {
221
226
  const response = await this.agent.run(userInput);
222
227
  web.recordRun(this.agent.lastRun);
223
- this.print(response);
228
+ this.print(process.stdout.isTTY && process.env.TERM !== "dumb"
229
+ ? renderMarkdown(response, Math.max(40, Math.min(120, (process.stdout.columns || 100) - 2)))
230
+ : response);
224
231
  }
225
232
  } catch (e) {
233
+ if (printer) printer.abort();
226
234
  this._raw("\n");
227
235
  this.error(`Error: ${e.message}`);
228
236
  } finally {
package/src/render.js ADDED
@@ -0,0 +1,196 @@
1
+ "use strict";
2
+
3
+ // Terminal chat renderer, opencode-style:
4
+ // - live-updating output (SmoothPrinter repaints in place instead of
5
+ // spilling raw tokens, which is what made streaming feel choppy)
6
+ // - fenced code blocks drawn as rounded boxes with a language label
7
+ // - light inline styling: bold/italic, `inline code`, headings, links
8
+ // Everything degrades to plain passthrough when stdout is not a TTY.
9
+
10
+ const ESC = "\x1b";
11
+ const C = {
12
+ reset: `${ESC}[0m`, bold: `${ESC}[1m`, dim: `${ESC}[2m`,
13
+ red: `${ESC}[31m`, green: `${ESC}[32m`, yellow: `${ESC}[33m`,
14
+ blue: `${ESC}[34m`, magenta: `${ESC}[35m`, cyan: `${ESC}[36m`,
15
+ };
16
+
17
+ function stripAnsi(s) { return s.replace(/\x1b\[[0-9;]*m/g, ""); }
18
+
19
+ function truncateVisible(s, max) {
20
+ if (visibleWidth(s) <= max) return s;
21
+ let out = "", w = 0;
22
+ for (let i = 0; i < s.length; i++) {
23
+ if (s[i] === ESC && s[i + 1] === "[") {
24
+ const m = /\x1b\[[0-9;]*m/.exec(s.slice(i));
25
+ if (m) { out += m[0]; i += m[0].length - 1; continue; }
26
+ }
27
+ w += s.codePointAt(i) > 0xffff ? 2 : 1;
28
+ if (w > max - 1) return out + C.dim + "…" + C.reset;
29
+ out += s[i];
30
+ }
31
+ return out;
32
+ }
33
+
34
+ function visibleWidth(s) { return stripAnsi(s).length; }
35
+
36
+ const KEYWORDS = new Set(("const let var function return if else for while class import " +
37
+ "from export default async await new try catch throw switch case break continue " +
38
+ "def lambda elif None True False self public private static void int float str " +
39
+ "bool this super extends implements package interface type enum struct impl fn " +
40
+ "match use pub mut nil null true false and or not in is").split(" "));
41
+
42
+ // Minimal, conservative token tinting for box bodies. Strings first
43
+ // (placeholder-protected), then comments, then keywords.
44
+ function tintCode(line) {
45
+ const strings = [];
46
+ line = line.replace(/("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g, (m) => {
47
+ strings.push(m);
48
+ return `\x00${strings.length - 1}\x00`;
49
+ });
50
+ if (/^\s*(\/\/|#|\/\*|\*|--)/.test(line)) {
51
+ line = C.dim + line + C.reset;
52
+ } else {
53
+ line = line.replace(/\b[A-Za-z_][A-Za-z0-9_]*\b/g, (w) =>
54
+ KEYWORDS.has(w) ? C.magenta + w + C.reset : w);
55
+ line = line.replace(/\b(\d+(?:\.\d+)?)\b/g, (n) => C.blue + n + C.reset);
56
+ }
57
+ line = line.replace(/\x00(\d+)\x00/g, (_, i) => C.yellow + strings[+i] + C.reset);
58
+ return line;
59
+ }
60
+
61
+ function padVisible(s, width) {
62
+ const pad = width - visibleWidth(s);
63
+ return s + " ".repeat(Math.max(0, pad));
64
+ }
65
+
66
+ function renderCodeBox(lang, lines, width) {
67
+ const inner = Math.max(20, width - 4);
68
+ const label = (lang || "").trim();
69
+ const top = label
70
+ ? `╭─ ${C.cyan}${label}${C.reset} ${"─".repeat(Math.max(1, inner - label.length - 4))}`
71
+ : `╭─${"─".repeat(inner - 1)}`;
72
+ const body = lines.map((l) =>
73
+ `${C.dim}│${C.reset} ` + padVisible(truncateVisible(tintCode(l), inner), inner) + ` ${C.dim}│${C.reset}`);
74
+ return [top + C.dim + "╮" + C.reset, ...body,
75
+ C.dim + "╰" + "─".repeat(inner + 2) + "╯" + C.reset];
76
+ }
77
+
78
+ function renderInline(s) {
79
+ return s
80
+ .replace(/\*\*([^*]+)\*\*/g, `${C.bold}$1${C.reset}`)
81
+ .replace(/(^|\s)\*([^*\s][^*]*)\*/g, `$1${C.dim}$2${C.reset}`)
82
+ .replace(/`([^`]+)`/g, `${C.cyan}$1${C.reset}`)
83
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, `$1 ${C.dim}($2)${C.reset}`);
84
+ }
85
+
86
+ /**
87
+ * Render a full assistant message for the terminal.
88
+ * Fenced blocks become rounded boxes; prose gets light markdown styling.
89
+ * Lines are hard-truncated to the given width so repaint math stays exact.
90
+ */
91
+ function renderMarkdown(text, width = 100) {
92
+ width = Math.max(40, Math.min(120, width));
93
+ const src = String(text ?? "").split("\n");
94
+ const out = [];
95
+ let inFence = false, lang = "", buf = [];
96
+ const flushFence = () => {
97
+ out.push(...renderCodeBox(lang, buf, width));
98
+ buf = []; lang = "";
99
+ };
100
+ for (const line of src) {
101
+ const m = /^\s*```(.*)$/.exec(line);
102
+ if (m) {
103
+ if (inFence) { flushFence(); inFence = false; }
104
+ else { inFence = true; lang = m[1]; }
105
+ continue;
106
+ }
107
+ if (inFence) { buf.push(line); continue; }
108
+ const h = /^(#{1,6})\s+(.*)$/.exec(line);
109
+ if (h) {
110
+ out.push(C.bold + C.cyan + h[2] + C.reset);
111
+ continue;
112
+ }
113
+ if (/^\s*(-{3,}|={3,}|\*{3,})\s*$/.test(line)) {
114
+ out.push(C.dim + "─".repeat(width) + C.reset);
115
+ continue;
116
+ }
117
+ if (/^\s*&gt;\s?/.test(line)) { out.push(C.dim + line + C.reset); continue; }
118
+ out.push(truncateVisible(renderInline(line), width));
119
+ }
120
+ if (inFence) flushFence();
121
+ return out.join("\n");
122
+ }
123
+
124
+ const MAX_PREVIEW_CHARS = 60000;
125
+
126
+ /**
127
+ * In-place repainting stream target. TTY: buffers chunks and repaints the
128
+ * rendered message every ~50ms (opencode feel). Non-TTY: raw passthrough.
129
+ */
130
+ class SmoothPrinter {
131
+ constructor(out = process.stdout) {
132
+ this.out = out;
133
+ this.tty = !!(out.isTTY && process.env.TERM !== "dumb"
134
+ && !process.env.OGPT_PLAIN);
135
+ this.width = Math.max(40, Math.min(120, (out.columns || 100) - 2));
136
+ }
137
+
138
+ begin() {
139
+ this.buf = "";
140
+ this.drawn = 0;
141
+ this.lastPaint = 0;
142
+ this.timer = null;
143
+ if (!this.tty) return;
144
+ // Hide the cursor while we repaint to avoid flicker.
145
+ this.out.write(`${ESC}[?25l`);
146
+ }
147
+
148
+ feed(chunk) {
149
+ if (!this.tty) { this.out.write(chunk); return; }
150
+ this.buf += chunk;
151
+ if (this.buf.length > MAX_PREVIEW_CHARS * 1.5) {
152
+ this.buf = this.buf.slice(-MAX_PREVIEW_CHARS);
153
+ }
154
+ if (!this.timer) {
155
+ this.timer = setTimeout(() => { this.timer = null; this._paint(); }, 50);
156
+ }
157
+ }
158
+
159
+ _erase() {
160
+ if (this.drawn > 0) this.out.write(`${ESC}[${this.drawn}A${ESC}[J`);
161
+ this.drawn = 0;
162
+ }
163
+
164
+ _paint() {
165
+ if (!this.tty || this.closed) return;
166
+ const now = Date.now();
167
+ if (now - this.lastPaint < 45) return;
168
+ this._erase();
169
+ let preview = this.buf;
170
+ if (preview.length > MAX_PREVIEW_CHARS) preview = preview.slice(-MAX_PREVIEW_CHARS);
171
+ const rendered = renderMarkdown(preview, this.width);
172
+ this.out.write(rendered);
173
+ this.drawn = rendered.split("\n").length - 1;
174
+ this.lastPaint = now;
175
+ }
176
+
177
+ finish() {
178
+ if (this.closed) return;
179
+ this.closed = true;
180
+ if (this.timer) { clearTimeout(this.timer); this.timer = null; }
181
+ if (!this.tty) return;
182
+ this._erase();
183
+ this.out.write(renderMarkdown(this.buf, this.width) + "\n");
184
+ this.out.write(`${ESC}[?25h`);
185
+ }
186
+
187
+ abort() {
188
+ if (this.closed) return;
189
+ this.closed = true;
190
+ if (this.timer) { clearTimeout(this.timer); this.timer = null; }
191
+ if (!this.tty) return;
192
+ this.out.write("\n" + `${ESC}[?25h`);
193
+ }
194
+ }
195
+
196
+ module.exports = { renderMarkdown, SmoothPrinter, tintCode, visibleWidth };
package/src/tools.js CHANGED
@@ -59,6 +59,18 @@ const TOOL_ALIASES = {
59
59
  create_directory: "make_dir", newdir: "make_dir", new_dir: "make_dir",
60
60
  newfolder: "make_dir", new_folder: "make_dir", createfolder: "make_dir",
61
61
  create_folder: "make_dir", folder: "make_dir", directory: "make_dir",
62
+ writefolder: "make_dir", write_folder: "make_dir", writefolderfile: "make_dir",
63
+ // move / copy / delete
64
+ move: "move_path", mv: "move_path", rename: "move_path", rename_file: "move_path",
65
+ renamefile: "move_path", movefile: "move_path", move_file: "move_path",
66
+ movefolder: "move_path", move_folder: "move_path", movepath: "move_path",
67
+ copy: "copy_path", cp: "copy_path", copyfile: "copy_path", copy_file: "copy_path",
68
+ copyfolder: "copy_path", copy_folder: "copy_path", copypath: "copy_path",
69
+ duplicate: "copy_path", dup: "copy_path",
70
+ delete: "delete_path", del: "delete_path", rm: "delete_path", remove: "delete_path",
71
+ removefile: "delete_path", remove_file: "delete_path", deletefile: "delete_path",
72
+ delete_file: "delete_path", deletefolder: "delete_path", delete_folder: "delete_path",
73
+ rmdir: "delete_path", unlink: "delete_path", trash: "delete_path",
62
74
  // file writing
63
75
  writefile: "write_file", filewrite: "write_file", createfile: "write_file",
64
76
  create_file: "write_file", newfile: "write_file", new_file: "write_file",
@@ -555,6 +567,20 @@ const TOOLS = [
555
567
  "Create a folder/directory (nested parents are created automatically)", {
556
568
  type: "object", properties: { path: { type: "string" } }, required: ["path"],
557
569
  }),
570
+ new ToolDef("move_path",
571
+ "Move or rename a file/folder (destination folder is created if needed)", {
572
+ type: "object",
573
+ properties: { source: { type: "string" }, destination: { type: "string" } },
574
+ required: ["source", "destination"],
575
+ }, true),
576
+ new ToolDef("copy_path", "Copy a file or whole folder", {
577
+ type: "object",
578
+ properties: { source: { type: "string" }, destination: { type: "string" } },
579
+ required: ["source", "destination"],
580
+ }),
581
+ new ToolDef("delete_path", "Delete a file or folder (recursive)", {
582
+ type: "object", properties: { path: { type: "string" } }, required: ["path"],
583
+ }, true),
558
584
  new ToolDef("edit_file", "Replace exact text in file", {
559
585
  type: "object",
560
586
  properties: {
@@ -747,8 +773,11 @@ const IMPLEMENTATIONS = {
747
773
  "/* Styles */\n\n" +
748
774
  ":root {\n" +
749
775
  " --bg: #0f172a;\n" +
776
+ " --surface: #1e293b;\n" +
750
777
  " --fg: #e2e8f0;\n" +
778
+ " --muted: #94a3b8;\n" +
751
779
  " --accent: #38bdf8;\n" +
780
+ " --radius: 12px;\n" +
752
781
  "}\n\n" +
753
782
  "*,\n*::before,\n*::after {\n" +
754
783
  " box-sizing: border-box;\n" +
@@ -758,9 +787,35 @@ const IMPLEMENTATIONS = {
758
787
  " font-family: system-ui, sans-serif;\n" +
759
788
  " background: var(--bg);\n" +
760
789
  " color: var(--fg);\n" +
761
- " line-height: 1.6;\n}\n";
790
+ " line-height: 1.6;\n}\n\n" +
791
+ ".container {\n" +
792
+ " max-width: 960px;\n" +
793
+ " margin: 0 auto;\n" +
794
+ " padding: 1.5rem;\n}\n\n" +
795
+ ".card {\n" +
796
+ " background: var(--surface);\n" +
797
+ " border-radius: var(--radius);\n" +
798
+ " padding: 1.25rem;\n}\n\n" +
799
+ "button,\n.btn {\n" +
800
+ " background: var(--accent);\n" +
801
+ " color: #06283d;\n" +
802
+ " border: none;\n" +
803
+ " border-radius: var(--radius);\n" +
804
+ " padding: 0.6rem 1.2rem;\n" +
805
+ " font-weight: 600;\n" +
806
+ " cursor: pointer;\n}\n\n" +
807
+ "button:hover,\n.btn:hover { filter: brightness(1.1); }\n\n" +
808
+ "@media (prefers-color-scheme: light) {\n" +
809
+ " :root {\n --bg: #f8fafc;\n --surface: #ffffff;\n --fg: #0f172a;\n --muted: #64748b;\n }\n}\n\n" +
810
+ "@media (max-width: 600px) {\n" +
811
+ " .container { padding: 1rem; }\n}\n";
762
812
  } else if (ext === ".js") {
763
- content = "// App scripts\n\"use strict\";\n";
813
+ content =
814
+ "// App scripts\n\"use strict\";\n\n" +
815
+ "document.addEventListener(\"DOMContentLoaded\", () => {\n" +
816
+ " const $ = (sel) => document.querySelector(sel);\n\n" +
817
+ " // Wire your UI here - every element id in index.html can be grabbed with $(\"#id\").\n" +
818
+ "});\n";
764
819
  }
765
820
  }
766
821
  fs.mkdirSync(path.dirname(p), { recursive: true });
@@ -787,6 +842,55 @@ const IMPLEMENTATIONS = {
787
842
  return new ToolResult(`Created directory: ${p}`);
788
843
  },
789
844
 
845
+ move_path(args) {
846
+ const src = path.resolve(anchorPath(String(args.source || args.path || "")).replace(/^~(?=$|\/)/, os.homedir()));
847
+ let dst = String(args.destination || args.dest || args.to || "").trim();
848
+ if (!src || !dst) return new ToolResult("", "move_path needs 'source' and 'destination'", false);
849
+ dst = path.resolve(dst.replace(/^~(?=$|\/)/, os.homedir()));
850
+ if (!fs.existsSync(src)) return new ToolResult("", `Not found: ${src}`, false);
851
+ if (fs.existsSync(dst) && fs.statSync(dst).isDirectory()) {
852
+ dst = path.join(dst, path.basename(src));
853
+ }
854
+ if (dst === src) return new ToolResult(`Unchanged: ${src}`);
855
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
856
+ try {
857
+ fs.renameSync(src, dst);
858
+ } catch { // cross-device: copy + remove
859
+ fs.cpSync(src, dst, { recursive: true });
860
+ fs.rmSync(src, { recursive: true, force: true });
861
+ }
862
+ const kind = fs.statSync(dst).isDirectory() ? "directory" : "file";
863
+ return new ToolResult(`Moved ${kind}: ${src} -> ${dst}`);
864
+ },
865
+
866
+ copy_path(args) {
867
+ const src = path.resolve(String(args.source || args.path || "").replace(/^~(?=$|\/)/, os.homedir()));
868
+ let dst = String(args.destination || args.dest || args.to || "").trim();
869
+ if (!src || !dst) return new ToolResult("", "copy_path needs 'source' and 'destination'", false);
870
+ dst = path.resolve(dst.replace(/^~(?=$|\/)/, os.homedir()));
871
+ if (!fs.existsSync(src)) return new ToolResult("", `Not found: ${src}`, false);
872
+ if (fs.existsSync(dst) && fs.statSync(dst).isDirectory()) {
873
+ dst = path.join(dst, path.basename(src));
874
+ }
875
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
876
+ fs.cpSync(src, dst, { recursive: true });
877
+ const kind = fs.statSync(dst).isDirectory() ? "directory" : "file";
878
+ return new ToolResult(`Copied ${kind}: ${src} -> ${dst}`);
879
+ },
880
+
881
+ delete_path(args) {
882
+ const raw = String(args.path || "").trim();
883
+ if (!raw) return new ToolResult("", "Empty path", false);
884
+ const p = path.resolve(raw.replace(/^~(?=$|\/)/, os.homedir()));
885
+ if (p === path.parse(p).root || p === os.homedir() || p === process.cwd()) {
886
+ return new ToolResult("", `Refusing to delete '${p}' - it is a protected root.`, false);
887
+ }
888
+ if (!fs.existsSync(p)) return new ToolResult(`Already gone: ${p}`);
889
+ const kind = fs.statSync(p).isDirectory() ? "directory" : "file";
890
+ fs.rmSync(p, { recursive: true, force: true });
891
+ return new ToolResult(`Deleted ${kind}: ${p}`);
892
+ },
893
+
790
894
  edit_file(args) {
791
895
  const p = path.resolve(anchorPath(String(args.path)).replace(/^~(?=$|\/)/, os.homedir()));
792
896
  if (!fs.existsSync(p)) return new ToolResult("", `Not found: ${p}`, false);