@krmxd/onegpt 0.2.3-beta → 0.2.5-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.2.3-beta",
3
+ "version": "0.2.5-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
@@ -109,6 +109,9 @@ const TOOL_GUIDANCE = (
109
109
  "'/home/user/...' or '/Users/...' - they do not exist on this machine.\n" +
110
110
  "- Tool arguments are REAL values: 'path': 'my_folder/app.py'. Never send the " +
111
111
  "parameter schema ({'type': 'string'}) as a value, and never leave arguments empty.\n" +
112
+ "- Call tools ONLY as exact JSON: {\"name\": \"write_file\", \"arguments\": {...}}. " +
113
+ "NEVER invent formats like 'name_file:path' or bare '<html>' dumps - those are NOT " +
114
+ "tool calls, they will be ignored, and you must redo them properly.\n" +
112
115
  "- To act, output plain JSON tool calls (never shell commands in code fences):\n" +
113
116
  '{"name": "write_file", "arguments": {"path": "widgets/gadget.py", ' +
114
117
  '"content": "print(\'hi\')"}}\n' +
@@ -195,6 +198,21 @@ function repliedWithoutActing(reply) {
195
198
  return SLOTH_RE.test(reply) || reply.includes("```");
196
199
  }
197
200
 
201
+ // Tiny models degenerate into printing the same chunk over and over
202
+ // ("name_file:web <html>..." x20). Detect any meaningful line repeated
203
+ // too many times so the agent can cut the run instead of spamming chat.
204
+ function degenerateRepeat(text, minLen = 12, maxHits = 4) {
205
+ const counts = new Map();
206
+ for (const rawLine of String(text || "").split("\n")) {
207
+ const line = rawLine.trim();
208
+ if (line.length < minLen) continue;
209
+ const n = (counts.get(line) || 0) + 1;
210
+ counts.set(line, n);
211
+ if (n >= maxHits) return line;
212
+ }
213
+ return null;
214
+ }
215
+
198
216
  // "Created the folder." style announcements. Harmless on their own, but a
199
217
  // red flag when the request named files that were never actually created
200
218
  // (e.g. 'a folder AND an index.html' - folder done, file skipped).
@@ -607,6 +625,7 @@ class Agent {
607
625
  let nudges = 0;
608
626
  this._autoChecks = 0;
609
627
  let lastSig = null;
628
+ let collectedText = "";
610
629
  let loopBreaks = 0;
611
630
  const doneBits = [];
612
631
 
@@ -733,6 +752,7 @@ class Agent {
733
752
  let nTokens = 0;
734
753
  this.lastRun = {};
735
754
  let lastSig = null;
755
+ let collectedText = "";
736
756
  let loopBreaks = 0;
737
757
  const doneBits = [];
738
758
  let sawText = false;
@@ -781,6 +801,20 @@ class Agent {
781
801
  }
782
802
 
783
803
  const full = contentParts.join("");
804
+ // Degeneration breaker: same meaningful line 4+ times in ONE round
805
+ // (or across the whole reply) -> stop instead of spamming chat.
806
+ const rep = degenerateRepeat(full)
807
+ || (full.trim().length < 40 && degenerateRepeat(collectedText, 8, 6));
808
+ if (rep) {
809
+ this.history.push({ role: "assistant", content: full, ts: Date.now() / 1000 });
810
+ yield "\n· repeated output detected - cutting it off\n";
811
+ if (wantsAction(userInput)) {
812
+ this.history.push({ role: "user", content: redirectMsg(userInput), ts: Date.now() / 1000 });
813
+ if (loopBreaks < 2) { loopBreaks++; continue; }
814
+ }
815
+ break;
816
+ }
817
+ collectedText = (collectedText ? collectedText + "\n" : "") + full;
784
818
  this.usage.prompt += usage.prompt;
785
819
  this.usage.completion += usage.completion;
786
820
  this.usage.total += usage.total;
@@ -966,4 +1000,5 @@ class Agent {
966
1000
  }
967
1001
 
968
1002
  module.exports = { Agent, extractJsonTools, repairToolJson, wantsAction,
969
- repliedWithoutActing, stalledHalfway, syntaxCheckError, autoCheckMsg };
1003
+ repliedWithoutActing, stalledHalfway, syntaxCheckError, autoCheckMsg,
1004
+ degenerateRepeat };
package/src/render.js CHANGED
@@ -257,18 +257,19 @@ class SmoothPrinter {
257
257
  let preview = this.buf;
258
258
  if (preview.length > MAX_PREVIEW_CHARS) preview = preview.slice(-MAX_PREVIEW_CHARS);
259
259
  const rendered = renderMarkdown(preview, width);
260
- const lines = rendered.split("\n");
261
- const rows = Math.max(0, lines.length - 1);
262
- const maxRows = (this.out.rows || 2000) - 1;
263
- if (rows > maxRows) {
264
- // Taller than the screen: scrolling would already have eaten the top
265
- // rows - repainting can't reach them. Degrade instead of duplicating.
266
- this._degrade(true);
267
- return;
268
- }
260
+ let lines = rendered.split("\n");
261
+ const target = Math.max(0, lines.length - 1);
262
+ const maxRows = Math.max(3, (this.out.rows || 2000) - 2);
263
+ // Viewport with INCREMENTAL REVEAL: grow by one row per tick so the
264
+ // painted block never pushes past the screen edge (which would scroll
265
+ // ghosts in and desync the next erase). Once at maxRows it repaints a
266
+ // stable window; finish() prints the complete styled message.
267
+ let take = Math.min(target, maxRows, this.drawn + 1);
268
+ if (target < take) take = target;
269
+ lines = lines.slice(-(take + 1));
269
270
  this._erase();
270
- this.out.write(rendered);
271
- this.drawn = rows;
271
+ this.out.write(lines.join("\n"));
272
+ this.drawn = Math.max(0, lines.length - 1);
272
273
  this.paintWidth = cols || width;
273
274
  this.lastPaint = now;
274
275
  }
package/src/tools.js CHANGED
@@ -891,6 +891,55 @@ const IMPLEMENTATIONS = {
891
891
  return new ToolResult(`Deleted ${kind}: ${p}`);
892
892
  },
893
893
 
894
+ create_project(args) {
895
+ const raw = String(args.path || args.name || "").trim();
896
+ if (!raw) return new ToolResult("", "Empty path", false);
897
+ const p = path.resolve(raw.replace(/^~(?=$|\/)/, os.homedir()));
898
+ const template = String(args.template || "web").toLowerCase();
899
+ if (fs.existsSync(p) && fs.readdirSync(p).length) {
900
+ return new ToolResult("", `Folder not empty: ${p} - pick a new project name.`, false);
901
+ }
902
+ fs.mkdirSync(p, { recursive: true });
903
+ const wf = (rel, content) => {
904
+ const f = path.join(p, rel);
905
+ fs.mkdirSync(path.dirname(f), { recursive: true });
906
+ fs.writeFileSync(f, content, "utf-8");
907
+ return rel;
908
+ };
909
+ const made = [];
910
+ const title = path.basename(p).replace(/[-_]/g, " ");
911
+ if (template === "flask") {
912
+ made.push(wf("app.py",
913
+ "from flask import Flask, render_template\n\n" +
914
+ "app = Flask(__name__)\n\n\n" +
915
+ "@app.route(\"/\")\n" +
916
+ "def index():\n" +
917
+ ` return render_template(\"index.html\", title=\"${title}\")\n\n\n` +
918
+ "if __name__ == \"__main__\":\n" +
919
+ " app.run(debug=True)\n"));
920
+ made.push(wf("requirements.txt", "flask\n"));
921
+ made.push(wf("templates/index.html",
922
+ "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n" +
923
+ " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" +
924
+ ` <title>${title}</title>\n</head>\n<body>\n <h1>${title}</h1>\n` +
925
+ " {% block content %}{% endblock %}\n</body>\n</html>\n"));
926
+ made.push(wf("static/style.css", "/* Styles */\nbody { font-family: system-ui; }\n"));
927
+ } else {
928
+ // web / website / static starter
929
+ made.push(wf("index.html",
930
+ "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n" +
931
+ " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" +
932
+ ` <title>${title}</title>\n <link rel=\"stylesheet\" href=\"styles.css\">\n` +
933
+ "</head>\n<body>\n <script src=\"script.js\" defer></script>\n</body>\n</html>\n"));
934
+ this.write_file({ path: path.join(p, "styles.css"), content: "" });
935
+ this.write_file({ path: path.join(p, "script.js"), content: "" });
936
+ made.push("styles.css (starter)", "script.js (starter)");
937
+ }
938
+ return new ToolResult(
939
+ `Created ${template} project '${path.basename(p)}' with: ` +
940
+ made.join(", ") + ". Extend these files with write_file/edit_file next.");
941
+ },
942
+
894
943
  edit_file(args) {
895
944
  const p = path.resolve(anchorPath(String(args.path)).replace(/^~(?=$|\/)/, os.homedir()));
896
945
  if (!fs.existsSync(p)) return new ToolResult("", `Not found: ${p}`, false);