@krmxd/onegpt 0.1.9-beta → 0.2.0-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 +1 -1
- package/src/agent.js +12 -4
- package/src/cli.js +11 -3
- package/src/render.js +196 -0
- package/src/tools.js +106 -2
package/package.json
CHANGED
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 " +
|
|
@@ -214,11 +217,16 @@ The user asked you to build a web project. Deliver a COMPLETE working site:
|
|
|
214
217
|
1. PLAN first - list every file you will create (2-4 lines max).
|
|
215
218
|
2. Create ALL of these in a project folder: index.html, styles.css, script.js.
|
|
216
219
|
3. Quality bar (mandatory):
|
|
217
|
-
- index.html: semantic HTML5
|
|
218
|
-
|
|
219
|
-
-
|
|
220
|
+
- index.html: semantic HTML5 (header/nav/main/footer), viewport meta, aria-labels
|
|
221
|
+
on interactive elements, linking styles.css and script.js with defer
|
|
222
|
+
- styles.css: modern design - custom properties, flexbox/grid layout, responsive
|
|
223
|
+
media queries, hover/focus states, prefers-color-scheme support (aim for 80+ lines)
|
|
224
|
+
- script.js: real interactivity wired to actual element ids from your HTML,
|
|
225
|
+
event listeners + DOM updates (aim for 40+ lines)
|
|
220
226
|
4. Zero placeholders ("TODO", "content here", "..."). Every button and link does something real.
|
|
221
|
-
5.
|
|
227
|
+
5. Unsure about an API or library? Use web_search to check real docs BEFORE writing
|
|
228
|
+
code that guesses. CDN libs via <script src> are allowed when they help.
|
|
229
|
+
6. VERIFY each file exists (read_file/list_files), then summarize how to open it.
|
|
222
230
|
Do NOT say done until every file from your plan exists on disk.`;
|
|
223
231
|
|
|
224
232
|
const PROJECT_CODE_BRIEF = `
|
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
|
-
|
|
216
|
+
printer.feed(chunk);
|
|
213
217
|
}
|
|
214
218
|
}
|
|
215
|
-
//
|
|
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(
|
|
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*>\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 =
|
|
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);
|