@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krmxd/onegpt",
3
- "version": "0.1.9-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 {
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);