@krmxd/onegpt 0.1.8-fix-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/bin/ogpt.js CHANGED
@@ -133,6 +133,23 @@ Options:
133
133
 
134
134
  if (noStream) cfg.set("ui.stream", false);
135
135
 
136
+ // Local-engine bootstrap: install/start the engine and make sure a model
137
+ // exists BEFORE chatting, so users never see raw ECONNREFUSED errors.
138
+ if ((cfg.get("active_provider", "ollama") || "ollama") === "ollama"
139
+ && args[0] !== "--help") {
140
+ const { ensureLocalEngine } = require("../src/ollama-setup");
141
+ const res = await ensureLocalEngine({
142
+ host: cfg.get("providers.ollama.host"),
143
+ model: cfg.activeModel ? cfg.activeModel() : cfg.get("active_model"),
144
+ });
145
+ if (res.ok && res.model && res.model !== cfg.get("active_model")) {
146
+ cfg.set("active_model", res.model);
147
+ }
148
+ if (!res.ok && res.reason) {
149
+ console.log("\n" + res.reason + "\n");
150
+ }
151
+ }
152
+
136
153
  const cli = new CLI({ config: cfg });
137
154
 
138
155
  if (autoApprove) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krmxd/onegpt",
3
- "version": "0.1.8-fix-beta",
3
+ "version": "0.2.0-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 " +
@@ -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 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)
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. VERIFY each file exists (read_file/list_files), then summarize how to open it.
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
- 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 {
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+
3
+ // Local-engine bootstrap (parity with ogpt/utils/ollama_setup.py).
4
+ // Makes `ogpt` work right after install: installs the engine if missing,
5
+ // starts the server daemon, picks a model that fits the RAM, and pulls it -
6
+ // all installer output hidden behind short status lines.
7
+
8
+ const { spawn, spawnSync } = require("child_process");
9
+ const os = require("os");
10
+
11
+ function isInstalled() {
12
+ try {
13
+ return spawnSync("ollama", ["--version"],
14
+ { stdio: "ignore", timeout: 15000 }).status === 0;
15
+ } catch { return false; }
16
+ }
17
+
18
+ function _getJson(host, path, timeoutMs) {
19
+ return new Promise((resolve) => {
20
+ const uri = new URL(path, host);
21
+ const mod = uri.protocol === "https:" ? require("https") : require("http");
22
+ const req = mod.request(uri, { timeout: timeoutMs }, (res) => {
23
+ let data = "";
24
+ res.on("data", (c) => { data += c; });
25
+ res.on("end", () => {
26
+ try { resolve({ ok: res.statusCode === 200, json: JSON.parse(data) }); }
27
+ catch { resolve({ ok: false }); }
28
+ });
29
+ });
30
+ req.on("timeout", () => { req.destroy(); resolve({ ok: false }); });
31
+ req.on("error", () => resolve({ ok: false }));
32
+ req.end();
33
+ });
34
+ }
35
+
36
+ async function serverUp(host, timeoutMs = 2500) {
37
+ const r = await _getJson(host, "/api/tags", timeoutMs);
38
+ return r.ok;
39
+ }
40
+
41
+ async function startServer(host, waitSec = 20) {
42
+ try {
43
+ const child = spawn("ollama", ["serve"],
44
+ { stdio: "ignore", detached: true });
45
+ child.unref();
46
+ } catch { return false; }
47
+ const deadline = Date.now() + waitSec * 1000;
48
+ while (Date.now() < deadline) {
49
+ await sleep(500);
50
+ if (await serverUp(host)) return true;
51
+ }
52
+ return false;
53
+ }
54
+
55
+ function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
56
+
57
+ async function listModels(host) {
58
+ const r = await _getJson(host, "/api/tags", 4000);
59
+ if (!r.ok || !r.json || !Array.isArray(r.json.models)) return [];
60
+ return r.json.models.map((m) => m.name || "").filter(Boolean);
61
+ }
62
+
63
+ async function pullModel(host, model, timeoutSec = 1800) {
64
+ console.log("Installing packages...");
65
+ try {
66
+ spawnSync("ollama", ["pull", model],
67
+ { stdio: "ignore", timeout: timeoutSec * 1000 });
68
+ } catch {}
69
+ const installed = await listModels(host);
70
+ const base = model.split(":")[0];
71
+ return installed.includes(model)
72
+ || installed.some((m) => m.startsWith(base + ":"));
73
+ }
74
+
75
+ function ramGB() {
76
+ return Math.round(os.totalmem() / (1024 * 1024 * 1024));
77
+ }
78
+
79
+ async function pickModel(host, configuredModel) {
80
+ const installed = await listModels(host);
81
+ if (installed.includes(configuredModel)) return configuredModel;
82
+ let fit = null;
83
+ try {
84
+ const { recommendForRam } = require("./catalog");
85
+ fit = recommendForRam(ramGB());
86
+ } catch {}
87
+ if (fit && installed.includes(fit)) return fit;
88
+ if (fit && !installed.length) return fit; // fresh machine: pull best fit
89
+ if (installed.length) return installed[0]; // something already there
90
+ return fit || configuredModel;
91
+ }
92
+
93
+ /**
94
+ * Ensure the local engine is installed, running and has a usable model.
95
+ * Returns {ok, model, reason} - never throws.
96
+ */
97
+ async function ensureLocalEngine({ host, model } = {}) {
98
+ host = host || "http://127.0.0.1:11434";
99
+ model = model || "qwen2.5-coder:1.5b";
100
+ try {
101
+ if (!isInstalled()) {
102
+ console.log("Installing packages...");
103
+ const { installOllama } = require("./bootstrap");
104
+ if (!installOllama()) {
105
+ return { ok: false, reason:
106
+ "Local engine could not be installed automatically. "
107
+ + "Grab it from https://ollama.com/download, or pick another "
108
+ + "provider with /provider." };
109
+ }
110
+ }
111
+ let up = await serverUp(host);
112
+ if (!up) {
113
+ console.log("Starting local engine...");
114
+ up = await startServer(host);
115
+ if (!up) {
116
+ return { ok: false, reason:
117
+ `Local engine did not respond on ${host} within 20s. `
118
+ + "Try 'ollama serve' manually or check 'journalctl -u ollama'." };
119
+ }
120
+ }
121
+ const chosen = await pickModel(host, model);
122
+ const installed = await listModels(host);
123
+ const base = chosen.split(":")[0];
124
+ const have = installed.includes(chosen)
125
+ || installed.some((m) => m.startsWith(base + ":"));
126
+ if (!have) {
127
+ const okPull = await pullModel(host, chosen);
128
+ if (!okPull) {
129
+ return { ok: false, reason:
130
+ `Model ${chosen} could not be downloaded (offline?). `
131
+ + "Check your connection or /model to pick an installed one." };
132
+ }
133
+ }
134
+ return { ok: true, model: chosen };
135
+ } catch (e) {
136
+ return { ok: false, reason: e.message };
137
+ }
138
+ }
139
+
140
+ module.exports = { ensureLocalEngine, serverUp, startServer, listModels,
141
+ pullModel, pickModel, isInstalled };
package/src/ollama.js CHANGED
@@ -348,7 +348,16 @@ class OllamaProvider {
348
348
  });
349
349
  }
350
350
  );
351
- req.on("error", reject);
351
+ req.on("error", (e) => {
352
+ if (e && (e.code === "ECONNREFUSED" || /ECONNREFUSED/.test(e.message || ""))) {
353
+ reject(new Error(
354
+ `Local engine is not running at ${this.host}. `
355
+ + "Start it with 'ollama serve' (OGPT normally does this "
356
+ + "automatically at launch), or switch providers with /provider."));
357
+ } else {
358
+ reject(e);
359
+ }
360
+ });
352
361
  req.on("timeout", () => { req.destroy(); reject(new Error("Timeout")); });
353
362
  if (body) req.write(JSON.stringify(body));
354
363
  req.end();
@@ -394,7 +403,16 @@ class OllamaProvider {
394
403
  req.destroy(new Error(`No data from the local engine for ${Math.round(this.idleTimeout / 1000)}s`));
395
404
  }, this.idleTimeout);
396
405
  };
397
- req.on("error", (e) => { clearIdle(); reject(e); });
406
+ req.on("error", (e) => {
407
+ clearIdle();
408
+ if (e && e.code === "ECONNREFUSED") {
409
+ reject(new Error(
410
+ `Local engine is not running at ${this.host}. `
411
+ + "Start it with 'ollama serve' or switch providers with /provider."));
412
+ } else {
413
+ reject(e);
414
+ }
415
+ });
398
416
  req.on("close", clearIdle);
399
417
  req.write(JSON.stringify(body));
400
418
  req.end();
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);