@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 +17 -0
- package/package.json +1 -1
- package/src/agent.js +12 -4
- package/src/cli.js +11 -3
- package/src/ollama-setup.js +141 -0
- package/src/ollama.js +20 -2
- package/src/render.js +196 -0
- package/src/tools.js +106 -2
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
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 {
|
|
@@ -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",
|
|
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) => {
|
|
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*>\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);
|