@krmxd/onegpt 0.2.2-beta → 0.2.4-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.2-beta",
3
+ "version": "0.2.4-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
@@ -10,6 +10,41 @@ const path = require("path");
10
10
  const { getConfig } = require("./config");
11
11
  const { getSharedProvider } = require("./ollama");
12
12
  const { ToolRegistry } = require("./tools");
13
+ const { spawnSync } = require("child_process");
14
+
15
+ // --- auto syntax check for AI-written code -------------------------------
16
+ const CHECKABLE_EXT = new Set([".py", ".js", ".mjs", ".cjs"]);
17
+
18
+ function _pick(binaries, args, timeoutMs) {
19
+ for (const bin of binaries) {
20
+ try {
21
+ const r = spawnSync(bin, args, { timeout: timeoutMs });
22
+ if (r.status === 0) return null;
23
+ if (r.error && r.error.code === "ENOENT") continue;
24
+ const err = (r.stderr ? r.stderr.toString() : "")
25
+ || (r.stdout ? r.stdout.toString() : "") || `exit ${r.status}`;
26
+ return err.split("\n").filter((l) => l.trim()).slice(0, 4).join("\n").slice(0, 500);
27
+ } catch { /* try next */ }
28
+ }
29
+ return null; // no interpreter available - skip silently
30
+ }
31
+
32
+ function syntaxCheckError(filePath) {
33
+ const ext = path.extname(filePath).toLowerCase();
34
+ if (!CHECKABLE_EXT.has(ext)) return null;
35
+ if (!fs.existsSync(filePath)) return null;
36
+ if (ext === ".py") {
37
+ return _pick(["python3", "python"], ["-m", "py_compile", filePath], 15000);
38
+ }
39
+ return _pick([process.execPath || "node", "node"], ["--check", filePath], 15000);
40
+ }
41
+
42
+ function autoCheckMsg(p, err) {
43
+ // Loop-break redirect style: tiny models need the exact shape again.
44
+ return "[auto-check] The file you just wrote FAILED its syntax check:\n" +
45
+ `${p}\n${err}\n` +
46
+ "Rewrite the ENTIRE file correctly NOW with write_file (bare JSON tool call), fixing every reported error. Do not explain.";
47
+ }
13
48
 
14
49
  const TOOL_RE = /\x00TOOL:([^:]*):([^:]*):(.*?)\x00/g;
15
50
  const USAGE_RE = /\x00USAGE:(.*?)\x00/;
@@ -74,6 +109,9 @@ const TOOL_GUIDANCE = (
74
109
  "'/home/user/...' or '/Users/...' - they do not exist on this machine.\n" +
75
110
  "- Tool arguments are REAL values: 'path': 'my_folder/app.py'. Never send the " +
76
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" +
77
115
  "- To act, output plain JSON tool calls (never shell commands in code fences):\n" +
78
116
  '{"name": "write_file", "arguments": {"path": "widgets/gadget.py", ' +
79
117
  '"content": "print(\'hi\')"}}\n' +
@@ -160,6 +198,21 @@ function repliedWithoutActing(reply) {
160
198
  return SLOTH_RE.test(reply) || reply.includes("```");
161
199
  }
162
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
+
163
216
  // "Created the folder." style announcements. Harmless on their own, but a
164
217
  // red flag when the request named files that were never actually created
165
218
  // (e.g. 'a folder AND an index.html' - folder done, file skipped).
@@ -570,7 +623,9 @@ class Agent {
570
623
  let full = "";
571
624
  let rounds = 0;
572
625
  let nudges = 0;
626
+ this._autoChecks = 0;
573
627
  let lastSig = null;
628
+ let collectedText = "";
574
629
  let loopBreaks = 0;
575
630
  const doneBits = [];
576
631
 
@@ -633,6 +688,15 @@ class Agent {
633
688
  doneBits.push(result.output.split("\n")[0].slice(0, 90));
634
689
  const ev = codeEvent(tc, pre);
635
690
  if (ev) full += (full.trim() ? "\n\n" : "") + codeMarkdown(ev);
691
+ // Auto syntax check: broken AI-written code gets one bounce-back.
692
+ if (ev && (tc.name === "write_file" || tc.name === "edit_file")
693
+ && this._autoChecks < 2) {
694
+ const err = syntaxCheckError(ev.path);
695
+ if (err) {
696
+ this._autoChecks++;
697
+ this.history.push({ role: "user", content: autoCheckMsg(ev.path, err), ts: Date.now() / 1000 });
698
+ }
699
+ }
636
700
  }
637
701
  }
638
702
  } else {
@@ -682,11 +746,13 @@ class Agent {
682
746
  this.history.push({ role: "user", content: userInput, ts: Date.now() / 1000 });
683
747
  let rounds = 0;
684
748
  let nudges = 0;
749
+ this._autoChecks = 0;
685
750
  const tStart = Date.now();
686
751
  let ttft = 0;
687
752
  let nTokens = 0;
688
753
  this.lastRun = {};
689
754
  let lastSig = null;
755
+ let collectedText = "";
690
756
  let loopBreaks = 0;
691
757
  const doneBits = [];
692
758
  let sawText = false;
@@ -735,6 +801,20 @@ class Agent {
735
801
  }
736
802
 
737
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;
738
818
  this.usage.prompt += usage.prompt;
739
819
  this.usage.completion += usage.completion;
740
820
  this.usage.total += usage.total;
@@ -790,6 +870,16 @@ class Agent {
790
870
  }
791
871
  const ev = codeEvent(tc, pre);
792
872
  if (ev) yield "\n" + codeMarkdown(ev) + "\n";
873
+ // Auto syntax check: broken AI-written code gets one bounce-back.
874
+ if (ev && (tc.name === "write_file" || tc.name === "edit_file")
875
+ && this._autoChecks < 2) {
876
+ const err = syntaxCheckError(ev.path);
877
+ if (err) {
878
+ this._autoChecks++;
879
+ yield `\n[auto-check] ${path.basename(ev.path)} has a syntax error - fixing...\n`;
880
+ this.history.push({ role: "user", content: autoCheckMsg(ev.path, err), ts: Date.now() / 1000 });
881
+ }
882
+ }
793
883
  }
794
884
  }
795
885
  } else {
@@ -909,4 +999,6 @@ class Agent {
909
999
  }
910
1000
  }
911
1001
 
912
- module.exports = { Agent, extractJsonTools, repairToolJson, wantsAction, repliedWithoutActing, stalledHalfway };
1002
+ module.exports = { Agent, extractJsonTools, repairToolJson, wantsAction,
1003
+ repliedWithoutActing, stalledHalfway, syntaxCheckError, autoCheckMsg,
1004
+ degenerateRepeat };
package/src/render.js CHANGED
@@ -257,30 +257,29 @@ 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
  }
275
276
 
276
277
  _degrade(skipFlush) {
277
278
  this.degraded = true;
278
- if (!skipFlush && this.buf) {
279
- // Hand back the un-styled tail so nothing is lost visually.
280
- this.out.write("\n" + this.buf.slice(-4000) + "\n");
281
- } else {
282
- this.out.write("\n");
283
- }
279
+ // Do NOT reprint the buffer here - part of it is already on screen
280
+ // and repainting would duplicate it. Just mark the seam and let the
281
+ // remaining chunks stream raw.
282
+ this.out.write("\n");
284
283
  this.buf = "";
285
284
  }
286
285
 
@@ -288,7 +287,9 @@ class SmoothPrinter {
288
287
  if (this.closed) return;
289
288
  this.closed = true;
290
289
  if (this.timer) { clearTimeout(this.timer); this.timer = null; }
291
- if (!this.tty || this.degraded) return;
290
+ if (!this.tty) return;
291
+ // Always un-hide the cursor, degraded or not.
292
+ if (this.degraded) { this.out.write(`${ESC}[?25h`); return; }
292
293
  this._erase();
293
294
  const width = this.widthOf();
294
295
  this.out.write(renderMarkdown(this.buf, width) + "\n");