@krmxd/onegpt 0.1.0 → 0.1.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/README.md +21 -0
- package/bin/ogpt.js +8 -5
- package/package.json +3 -2
- package/src/agent.js +138 -14
- package/src/bootstrap.js +102 -0
- package/src/cli.js +6 -4
- package/src/static.js +1 -1
- package/src/tools.js +113 -6
- package/src/web.js +2 -2
package/README.md
CHANGED
|
@@ -11,6 +11,20 @@ npm i -g @krmxd/onegpt
|
|
|
11
11
|
ogpt
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
## Troubleshooting
|
|
15
|
+
|
|
16
|
+
**`ModuleNotFoundError: No module named 'ogpt'` from a path like
|
|
17
|
+
`.../usr/bin/ogpt`** - that is the *Python* build's launcher, not this
|
|
18
|
+
package. An old pip install is shadowing `ogpt`. Fix:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip uninstall ogpt && rm -f $PREFIX/bin/ogpt
|
|
22
|
+
npm i -g @krmxd/onegpt@latest --force
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
If `ogpt` is still grabbed by something else, the Node build is also
|
|
26
|
+
installed as **`ogpt-node`** (same app, unshadowable name).
|
|
27
|
+
|
|
14
28
|
## Requirements
|
|
15
29
|
|
|
16
30
|
- Node.js >= 18
|
|
@@ -20,6 +34,13 @@ ogpt
|
|
|
20
34
|
|
|
21
35
|
- 24 built-in tools: read/write/edit files, run Python & Node code,
|
|
22
36
|
install packages (pip/npm), call HTTP APIs, git, web search & fetch
|
|
37
|
+
- **Self-installing**: missing packages - including **Ollama** itself if the
|
|
38
|
+
device doesn't have it - install quietly behind a single
|
|
39
|
+
`Installing packages...` line; never raw installer logs
|
|
40
|
+
(`OGPT_NO_AUTO_INSTALL=1` disables)
|
|
41
|
+
- **Smart Project Mode**: "build me a portfolio website" → it plans, writes
|
|
42
|
+
complete HTML/CSS/JS (never stubs), verifies the files exist and only then
|
|
43
|
+
reports done - lazy one-file answers get auto-corrected
|
|
23
44
|
- Permission prompts only for genuinely risky actions (shell, installs,
|
|
24
45
|
git commit) - everything else just works
|
|
25
46
|
- Token dashboard with live tok/s charts at `http://127.0.0.1:8756` (`/dash`)
|
package/bin/ogpt.js
CHANGED
|
@@ -8,14 +8,13 @@ if (maj < 18) {
|
|
|
8
8
|
process.exit(1);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
let CLI, getConfig;
|
|
11
|
+
let CLI, getConfig, VERSION_FULL;
|
|
12
12
|
try {
|
|
13
|
-
({ CLI } = require("../src/cli"));
|
|
13
|
+
({ CLI, VERSION_FULL } = require("../src/cli"));
|
|
14
14
|
({ getConfig } = require("../src/config"));
|
|
15
15
|
} catch (e) {
|
|
16
16
|
console.error(`OGPT failed to start: ${e.message}`);
|
|
17
|
-
if (e.code === "MODULE_NOT_FOUND") {
|
|
18
|
-
console.error(`
|
|
17
|
+
if (e.code === "MODULE_NOT_FOUND") { console.error(`
|
|
19
18
|
The installation looks broken or out of date. Fix it with:
|
|
20
19
|
|
|
21
20
|
npm cache clean --force
|
|
@@ -33,6 +32,10 @@ Or run straight from the repo without installing:
|
|
|
33
32
|
process.exit(1);
|
|
34
33
|
}
|
|
35
34
|
|
|
35
|
+
// Silent dependency self-heal + Ollama install - all hidden behind a single
|
|
36
|
+
// 'Installing packages...' line; no-op when everything is already present.
|
|
37
|
+
try { require("../src/bootstrap").ensureBoot(); } catch {}
|
|
38
|
+
|
|
36
39
|
async function main() {
|
|
37
40
|
const args = process.argv.slice(2);
|
|
38
41
|
const cfg = getConfig();
|
|
@@ -57,7 +60,7 @@ async function main() {
|
|
|
57
60
|
const name = args[++i];
|
|
58
61
|
cfg.set("active_model", cfg.resolveModel(name));
|
|
59
62
|
} else if (args[i] === "--version" || args[i] === "-v") {
|
|
60
|
-
console.log(
|
|
63
|
+
console.log(`ogpt v${VERSION_FULL} (by KareemXD)`);
|
|
61
64
|
process.exit(0);
|
|
62
65
|
} else if (args[i] === "--help") {
|
|
63
66
|
console.log(`Usage: ogpt [options] [prompt]
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krmxd/onegpt",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.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": {
|
|
7
|
-
"ogpt": "bin/ogpt.js"
|
|
7
|
+
"ogpt": "bin/ogpt.js",
|
|
8
|
+
"ogpt-node": "bin/ogpt.js"
|
|
8
9
|
},
|
|
9
10
|
"scripts": {
|
|
10
11
|
"start": "node bin/ogpt.js",
|
package/src/agent.js
CHANGED
|
@@ -104,7 +104,9 @@ const TOOL_GUIDANCE = (
|
|
|
104
104
|
const TASK_RE = new RegExp(
|
|
105
105
|
"\\b(create|make|build|write|add|generate|scaffold|implement|fix|update)\\b" +
|
|
106
106
|
"[^\\n]{0,120}?\\b(file|files|folder|folders|directory|dir|script|module|" +
|
|
107
|
-
"app|component|config|readme|tests?)
|
|
107
|
+
"app|component|config|readme|tests?|website|web\\s?site|web\\s?app(?:lication)?|" +
|
|
108
|
+
"landing(?:\\s?page)?|portfolio|blog|dashboard|homepage|page|site|game|" +
|
|
109
|
+
"calculator|quiz|\\bbot\\b|\\bapi\\b|server|tool|cli|program|project)\\b", "i");
|
|
108
110
|
const QUESTION_RE = new RegExp(
|
|
109
111
|
"^\\s*(how|what|why|when|where|which|who|whom|can you (?:tell|explain)|" +
|
|
110
112
|
"explain|describe|list|is|are|does|do)\\b", "i");
|
|
@@ -185,13 +187,62 @@ function stalledHalfway(reply, userInput, doneOutputs) {
|
|
|
185
187
|
if (!t || t.length > 160 || t.includes("```")) return false;
|
|
186
188
|
if (!ANNOUNCE_RE.test(t)) return false;
|
|
187
189
|
const bits = (doneOutputs || []).join(" ").toLowerCase();
|
|
188
|
-
|
|
190
|
+
let expected = namedFiles(userInput);
|
|
191
|
+
// A web-build brief demands the standard trio - hold the model to what
|
|
192
|
+
// PROJECT MODE promised ("make me a portfolio" => all three files).
|
|
193
|
+
if (projectBrief(userInput) === PROJECT_WEB_BRIEF) {
|
|
194
|
+
const have = new Set(expected.map((n) => n.toLowerCase()));
|
|
195
|
+
for (const f of ["index.html", "styles.css", "script.js"]) {
|
|
196
|
+
if (!have.has(f)) expected = expected.concat([f]);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return expected.some((n) => {
|
|
189
200
|
const base = path.basename(n).toLowerCase();
|
|
190
201
|
if (base && bits.includes(base)) return false;
|
|
191
202
|
return !artifactExists(n);
|
|
192
203
|
});
|
|
193
204
|
}
|
|
194
205
|
|
|
206
|
+
const PROJECT_VERB_RE = /\b(?:build|create|make|write|start|scaffold|develop|design|code)\b/i;
|
|
207
|
+
const PROJECT_WEB_NOUN_RE = /\b(?:web\s?site|web\s?app(?:lication)?|landing(?:\s?page)?|portfolio|blog|dashboard|homepage|front[\s-]?end|game|calculator|to-?do\s?(?:list|app)|quiz)\b/i;
|
|
208
|
+
const PROJECT_CODE_NOUN_RE = /\b(?:project|application|\bapp\b|\bcli\b|\btool\b|script\b|\bbot\b|\bapi\b|server|program|package|library|automation)\b/i;
|
|
209
|
+
|
|
210
|
+
const PROJECT_WEB_BRIEF = `
|
|
211
|
+
|
|
212
|
+
### PROJECT MODE - WEB BUILD
|
|
213
|
+
The user asked you to build a web project. Deliver a COMPLETE working site:
|
|
214
|
+
1. PLAN first - list every file you will create (2-4 lines max).
|
|
215
|
+
2. Create ALL of these in a project folder: index.html, styles.css, script.js.
|
|
216
|
+
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
|
+
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.
|
|
222
|
+
Do NOT say done until every file from your plan exists on disk.`;
|
|
223
|
+
|
|
224
|
+
const PROJECT_CODE_BRIEF = `
|
|
225
|
+
|
|
226
|
+
### PROJECT MODE - BUILD
|
|
227
|
+
The user asked you to build something. Process:
|
|
228
|
+
1. PLAN first - list the files you will create.
|
|
229
|
+
2. Write every file completely with real working code - no TODO placeholders.
|
|
230
|
+
3. Include a README.md with run instructions.
|
|
231
|
+
4. TEST your work: run the code (run_python/run_node/terminal) and fix any errors before finishing.
|
|
232
|
+
5. Summarize what you built and how to run it.
|
|
233
|
+
Do NOT say done until the code runs without errors.`;
|
|
234
|
+
|
|
235
|
+
// Extra system-prompt guidance when the request is 'build me a ...'.
|
|
236
|
+
function projectBrief(userInput) {
|
|
237
|
+
const text = userInput || "";
|
|
238
|
+
if (!PROJECT_VERB_RE.test(text)) return "";
|
|
239
|
+
// Questions ABOUT building are not build orders ("how do I make a game?").
|
|
240
|
+
if (/^\s*(?:how|what|why|when|where|which|who|can\s+you\s+(?:tell|explain)|explain|describe)\b/i.test(text)) return "";
|
|
241
|
+
if (PROJECT_WEB_NOUN_RE.test(text)) return PROJECT_WEB_BRIEF;
|
|
242
|
+
if (PROJECT_CODE_NOUN_RE.test(text)) return PROJECT_CODE_BRIEF;
|
|
243
|
+
return "";
|
|
244
|
+
}
|
|
245
|
+
|
|
195
246
|
function expandHome(p) {
|
|
196
247
|
return String(p).replace(/^~(?=$|\/)/, os.homedir());
|
|
197
248
|
}
|
|
@@ -226,15 +277,73 @@ function callSig(calls) {
|
|
|
226
277
|
|
|
227
278
|
// Catch tool calls the model printed as bare JSON text instead of using the
|
|
228
279
|
// native tool_calls channel (very common with tiny models).
|
|
280
|
+
// Salvage a malformed tool-call blob. Tiny local models constantly emit
|
|
281
|
+
// invalid JSON - most often unescaped quotes inside string values
|
|
282
|
+
// ("content": "<div class="hero">") - which strict parsing rejects and the
|
|
283
|
+
// call silently becomes chat noise. Field-wise extraction does not care.
|
|
284
|
+
function repairToolJson(raw) {
|
|
285
|
+
const nameM = raw.match(/"name"\s*:\s*"([A-Za-z_][\w]*)"/);
|
|
286
|
+
if (!nameM) return null;
|
|
287
|
+
const argIdx = raw.indexOf('"arguments"');
|
|
288
|
+
if (argIdx === -1) return null;
|
|
289
|
+
let body = raw.slice(argIdx + '"arguments"'.length);
|
|
290
|
+
const braceAt = body.indexOf("{");
|
|
291
|
+
if (braceAt !== -1 && !body.slice(0, braceAt).includes(":")) body = body.slice(braceAt + 1);
|
|
292
|
+
|
|
293
|
+
const keyRe = /"([A-Za-z_]\w*)"\s*:\s*/g;
|
|
294
|
+
const marks = [];
|
|
295
|
+
let m;
|
|
296
|
+
while ((m = keyRe.exec(body)) !== null) marks.push({ key: m[1], vs: m.index + m[0].length });
|
|
297
|
+
const args = {};
|
|
298
|
+
for (let i = 0; i < marks.length; i++) {
|
|
299
|
+
const k = marks[i];
|
|
300
|
+
if (k.key === "arguments") continue; // the wrapper key itself
|
|
301
|
+
const next = marks.find((x, j) => j > i && x.key !== "arguments");
|
|
302
|
+
let v;
|
|
303
|
+
if (next) {
|
|
304
|
+
// Value runs until just before the next key's opening quote.
|
|
305
|
+
const nq = body.lastIndexOf('"', Math.max(0, next.vs - next.key.length - 2));
|
|
306
|
+
v = body.slice(k.vs, nq > k.vs ? nq : undefined);
|
|
307
|
+
v = v.replace(/\s*$/, "").replace(/,\s*$/, "");
|
|
308
|
+
if (v.startsWith('"')) v = v.slice(1);
|
|
309
|
+
if (v.endsWith('"')) v = v.slice(0, -1);
|
|
310
|
+
} else {
|
|
311
|
+
// Last value: everything up to the closing brace(s) of the blob.
|
|
312
|
+
v = body.slice(k.vs).replace(/\s*\}\s*\}\s*$/, "").replace(/\s*\}\s*$/, "");
|
|
313
|
+
if (v.endsWith(",")) v = v.slice(0, -1);
|
|
314
|
+
if (v.startsWith('"')) v = v.slice(1);
|
|
315
|
+
if (v.endsWith('"')) v = v.slice(0, -1);
|
|
316
|
+
}
|
|
317
|
+
if (v.startsWith("[") && v.endsWith("]")) {
|
|
318
|
+
try { args[k.key] = JSON.parse(v); continue; }
|
|
319
|
+
catch {}
|
|
320
|
+
args[k.key] = v.slice(1, -1).split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
args[k.key] = v;
|
|
324
|
+
}
|
|
325
|
+
if (!Object.keys(args).length) return null;
|
|
326
|
+
return {
|
|
327
|
+
id: `tc_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
328
|
+
name: nameM[1],
|
|
329
|
+
arguments: args,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
229
333
|
function extractJsonTools(text) {
|
|
230
334
|
const calls = [];
|
|
231
335
|
const spans = [];
|
|
232
336
|
if (!text || text.indexOf('"arguments"') === -1) return { clean: text || "", calls };
|
|
337
|
+
// Fenced blobs (```json ... ```) - strip the fence markers so the objects
|
|
338
|
+
// inside become scannable. Python's scanner does this natively.
|
|
339
|
+
const src = text.includes("```")
|
|
340
|
+
? text.replace(/```[a-zA-Z]*\n?/g, "\n")
|
|
341
|
+
: text;
|
|
233
342
|
let i = 0;
|
|
234
|
-
while ((i =
|
|
343
|
+
while ((i = src.indexOf("{", i)) !== -1 && calls.length < 8) {
|
|
235
344
|
let depth = 0, inStr = false, esc = false;
|
|
236
|
-
for (let j = i; j <
|
|
237
|
-
const c =
|
|
345
|
+
for (let j = i; j < src.length && j < i + 20000; j++) {
|
|
346
|
+
const c = src[j];
|
|
238
347
|
if (inStr) {
|
|
239
348
|
if (esc) esc = false;
|
|
240
349
|
else if (c === "\\") esc = true;
|
|
@@ -247,7 +356,7 @@ function extractJsonTools(text) {
|
|
|
247
356
|
depth--;
|
|
248
357
|
if (depth === 0) {
|
|
249
358
|
try {
|
|
250
|
-
const obj = JSON.parse(
|
|
359
|
+
const obj = JSON.parse(src.slice(i, j + 1));
|
|
251
360
|
if (obj && typeof obj.name === "string"
|
|
252
361
|
&& obj.arguments && typeof obj.arguments === "object"
|
|
253
362
|
&& !Array.isArray(obj.arguments)) {
|
|
@@ -266,13 +375,17 @@ function extractJsonTools(text) {
|
|
|
266
375
|
}
|
|
267
376
|
i++;
|
|
268
377
|
}
|
|
378
|
+
if (!calls.length && /"name"\s*:/.test(src) && text.includes('"arguments"')) {
|
|
379
|
+
const rep = repairToolJson(src);
|
|
380
|
+
if (rep) return { clean: "", calls: [rep] };
|
|
381
|
+
}
|
|
269
382
|
let clean = "";
|
|
270
383
|
let pos = 0;
|
|
271
384
|
for (const [s, e] of spans) {
|
|
272
|
-
clean +=
|
|
385
|
+
clean += src.slice(pos, s);
|
|
273
386
|
pos = e;
|
|
274
387
|
}
|
|
275
|
-
clean +=
|
|
388
|
+
clean += src.slice(pos);
|
|
276
389
|
return { clean, calls };
|
|
277
390
|
}
|
|
278
391
|
|
|
@@ -365,11 +478,13 @@ class Agent {
|
|
|
365
478
|
this.lastRun = {};
|
|
366
479
|
}
|
|
367
480
|
|
|
368
|
-
_systemPrompt() {
|
|
481
|
+
_systemPrompt(userInput) {
|
|
369
482
|
// The base prompt may contain an {ogpt_model} placeholder that is replaced
|
|
370
483
|
// with the active model's oGPT display name (e.g. oGPT-1a). The identity
|
|
371
484
|
// block is always appended so the assistant never exposes the underlying
|
|
372
|
-
// engine, even if an older prompt is set in the user's config.
|
|
485
|
+
// engine, even if an older prompt is set in the user's config. When the
|
|
486
|
+
// request looks like a build-a-project task, a PROJECT MODE brief is
|
|
487
|
+
// appended for this turn only.
|
|
373
488
|
const base =
|
|
374
489
|
this.cfg.get("agent.system_prompt", "") ||
|
|
375
490
|
"You are OGPT, an expert AI coding assistant running as {ogpt_model}.";
|
|
@@ -382,6 +497,7 @@ class Agent {
|
|
|
382
497
|
base.split("{ogpt_model}").join(name)
|
|
383
498
|
+ IDENTITY_BLOCK.split("{name}").join(name)
|
|
384
499
|
+ TOOL_GUIDANCE.split("{cwd}").join(cwd)
|
|
500
|
+
+ projectBrief(userInput)
|
|
385
501
|
);
|
|
386
502
|
}
|
|
387
503
|
|
|
@@ -444,7 +560,7 @@ class Agent {
|
|
|
444
560
|
const model = this.cfg.activeModel();
|
|
445
561
|
const resp = await this.provider.chat(this.history, model, {
|
|
446
562
|
tools: this.tools.definitions(),
|
|
447
|
-
system: this._systemPrompt(),
|
|
563
|
+
system: this._systemPrompt(userInput),
|
|
448
564
|
temperature: this.temperature,
|
|
449
565
|
maxTokens: this.maxTokens,
|
|
450
566
|
});
|
|
@@ -560,11 +676,12 @@ class Agent {
|
|
|
560
676
|
const contentParts = [];
|
|
561
677
|
const toolCalls = [];
|
|
562
678
|
let usage = { prompt: 0, completion: 0, total: 0 };
|
|
679
|
+
let jsonHold = false;
|
|
563
680
|
|
|
564
681
|
try {
|
|
565
682
|
for await (const chunk of this.provider.stream(this.history, model, {
|
|
566
683
|
tools: this.tools.definitions(),
|
|
567
|
-
system: this._systemPrompt(),
|
|
684
|
+
system: this._systemPrompt(userInput),
|
|
568
685
|
temperature: this.temperature,
|
|
569
686
|
maxTokens: this.maxTokens,
|
|
570
687
|
})) {
|
|
@@ -573,7 +690,14 @@ class Agent {
|
|
|
573
690
|
if (!ttft) ttft = Date.now() - tStart;
|
|
574
691
|
nTokens++;
|
|
575
692
|
sawText = true;
|
|
576
|
-
|
|
693
|
+
// A tool-call blob typed as plain text must not scroll into the
|
|
694
|
+
// chat as garbage - hold it back; the executor prints chips.
|
|
695
|
+
const acc = contentParts.join("");
|
|
696
|
+
if (!jsonHold && ((/^\s*\{/.test(acc) && acc.includes('"name"'))
|
|
697
|
+
|| (/^\s*```/.test(acc) && (acc.includes('"name"') || acc.includes('"arguments"'))))) {
|
|
698
|
+
jsonHold = true;
|
|
699
|
+
}
|
|
700
|
+
if (!jsonHold) yield chunk.text;
|
|
577
701
|
} else if (chunk.type === "tool") {
|
|
578
702
|
toolCalls.push(chunk);
|
|
579
703
|
} else if (chunk.type === "usage") {
|
|
@@ -760,4 +884,4 @@ class Agent {
|
|
|
760
884
|
}
|
|
761
885
|
}
|
|
762
886
|
|
|
763
|
-
module.exports = { Agent };
|
|
887
|
+
module.exports = { Agent, extractJsonTools, repairToolJson };
|
package/src/bootstrap.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Boot-time dependency self-healing (parity with the Python build).
|
|
4
|
+
//
|
|
5
|
+
// - PACKAGES: runtime npm deps (empty today -> instant no-op). List entries
|
|
6
|
+
// as [require-name, npm-name] and first launch installs them quietly.
|
|
7
|
+
// - Ollama: if the local-engine binary is missing, the official installer
|
|
8
|
+
// runs hidden. Never touched when already present.
|
|
9
|
+
//
|
|
10
|
+
// The user only ever sees one line: 'Installing packages...'.
|
|
11
|
+
// Set OGPT_NO_AUTO_INSTALL=1 to disable (CI, tests, offline devices).
|
|
12
|
+
|
|
13
|
+
const { spawnSync } = require("child_process");
|
|
14
|
+
const fs = require("fs");
|
|
15
|
+
const path = require("path");
|
|
16
|
+
|
|
17
|
+
const PACKAGES = [];
|
|
18
|
+
|
|
19
|
+
function missingPackages() {
|
|
20
|
+
return PACKAGES.filter(([req]) => {
|
|
21
|
+
try { require.resolve(req); return false; } catch { return true; }
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function haveOllama() {
|
|
26
|
+
if (spawnSync("ollama", ["--version"],
|
|
27
|
+
{ stdio: "ignore", timeout: 15000 }).status === 0) return true;
|
|
28
|
+
// same-process PATH additions (fresh install) + common locations
|
|
29
|
+
for (const dir of ["/usr/local/bin", "/usr/bin", "/opt/homebrew/bin",
|
|
30
|
+
path.join(process.env.HOME || "", ".local/bin")]) {
|
|
31
|
+
try {
|
|
32
|
+
if (dir && fs.existsSync(path.join(dir, "ollama"))) {
|
|
33
|
+
process.env.PATH = dir + path.delimiter + process.env.PATH;
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
} catch {}
|
|
37
|
+
}
|
|
38
|
+
if (process.platform === "win32") {
|
|
39
|
+
for (const dir of ["C:\\Program Files\\Ollama",
|
|
40
|
+
path.join(process.env.LOCALAPPDATA || "", "Programs", "Ollama")]) {
|
|
41
|
+
try { if (dir && fs.existsSync(path.join(dir, "ollama.exe"))) return true; }
|
|
42
|
+
catch {}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function npmInstall(pkgs, cwd, timeout = 600000) {
|
|
49
|
+
try {
|
|
50
|
+
const r = spawnSync("npm", ["install", "--no-fund", "--no-audit",
|
|
51
|
+
"--silent", ...pkgs],
|
|
52
|
+
{ cwd: cwd ? path.resolve(cwd) : undefined, stdio: "ignore", timeout });
|
|
53
|
+
return !r.error && r.status === 0;
|
|
54
|
+
} catch { return false; }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function installOllama(timeout = 1200000) {
|
|
58
|
+
if (process.platform === "win32") {
|
|
59
|
+
try {
|
|
60
|
+
spawnSync("winget", ["install", "--silent",
|
|
61
|
+
"--accept-package-agreements", "--accept-source-agreements",
|
|
62
|
+
"Ollama.Ollama"], { stdio: "ignore", timeout, shell: true });
|
|
63
|
+
} catch {}
|
|
64
|
+
return haveOllama();
|
|
65
|
+
}
|
|
66
|
+
const has = (c) => {
|
|
67
|
+
try { return spawnSync(c, ["--version"], { stdio: "ignore" }).status === 0; }
|
|
68
|
+
catch { return false; }
|
|
69
|
+
};
|
|
70
|
+
let script = null;
|
|
71
|
+
if (has("curl")) script = "curl -fsSL https://ollama.com/install.sh | sh";
|
|
72
|
+
else if (has("wget")) script = "wget -qO- https://ollama.com/install.sh | sh";
|
|
73
|
+
if (!script) return false;
|
|
74
|
+
try {
|
|
75
|
+
spawnSync("sh", ["-c", script], { stdio: "ignore", timeout });
|
|
76
|
+
} catch {}
|
|
77
|
+
return haveOllama();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function ensureBoot() {
|
|
81
|
+
if (process.env.OGPT_NO_AUTO_INSTALL) return [];
|
|
82
|
+
const need = missingPackages();
|
|
83
|
+
const wantOllama = !haveOllama();
|
|
84
|
+
if (!need.length && !wantOllama) return [];
|
|
85
|
+
console.log("Installing packages...");
|
|
86
|
+
if (need.length) npmInstall(need.map(([, pkg]) => pkg));
|
|
87
|
+
if (wantOllama) installOllama();
|
|
88
|
+
return missingPackages().map(([, pkg]) => pkg);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Back-compat shim (deps-only, no Ollama check).
|
|
92
|
+
function ensurePackages() {
|
|
93
|
+
if (process.env.OGPT_NO_AUTO_INSTALL) return [];
|
|
94
|
+
const need = missingPackages();
|
|
95
|
+
if (!need.length) return [];
|
|
96
|
+
console.log("Installing packages...");
|
|
97
|
+
npmInstall(need.map(([, pkg]) => pkg));
|
|
98
|
+
return missingPackages().map(([, pkg]) => pkg);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = { ensureBoot, ensurePackages, installOllama, haveOllama,
|
|
102
|
+
npmInstall, PACKAGES };
|
package/src/cli.js
CHANGED
|
@@ -12,7 +12,9 @@ const Platform = require("./platform");
|
|
|
12
12
|
const catalog = require("./catalog");
|
|
13
13
|
const web = require("./web");
|
|
14
14
|
|
|
15
|
-
const VERSION = "
|
|
15
|
+
const VERSION = "0.1.5-beta";
|
|
16
|
+
const VERSION_TAG = "devices-all";
|
|
17
|
+
const VERSION_FULL = `${VERSION}(${VERSION_TAG})`;
|
|
16
18
|
|
|
17
19
|
class Command {
|
|
18
20
|
constructor(name, handler, description, usage, category, ai, aliases) {
|
|
@@ -144,14 +146,14 @@ class CLI {
|
|
|
144
146
|
const lines = [""];
|
|
145
147
|
for (const l of art) lines.push(`${B}${C}${center(l)}${R}`);
|
|
146
148
|
lines.push("");
|
|
147
|
-
lines.push(`${B}${W}${center(`AI coding assistant by KareemXD · v${
|
|
149
|
+
lines.push(`${B}${W}${center(`AI coding assistant by KareemXD · v${VERSION_FULL}`)}${R}`);
|
|
148
150
|
lines.push(`${B}${C}${center(`Active model: ${model} · ${Platform.ramGB()}GB RAM · tier ${tier}`)}${R}`);
|
|
149
151
|
lines.push(`${D}${center("Type /help for commands · /quit to exit")}${R}`);
|
|
150
152
|
lines.push("", "");
|
|
151
153
|
const vpad = Math.max(0, Math.floor((rows - lines.length) / 2) - 1);
|
|
152
154
|
process.stdout.write("\n".repeat(vpad) + lines.join("\n") + "\n\n");
|
|
153
155
|
} else {
|
|
154
|
-
const out = [...art, "", center(`OGPT v${
|
|
156
|
+
const out = [...art, "", center(`OGPT v${VERSION_FULL} · model: ${model} · ${Platform.ramGB()}GB RAM · tier ${tier}`), center("Type /help for commands, /quit to exit"), ""];
|
|
155
157
|
process.stdout.write(out.join("\n") + "\n");
|
|
156
158
|
}
|
|
157
159
|
}
|
|
@@ -1154,4 +1156,4 @@ function formatSize(bytes) {
|
|
|
1154
1156
|
return bytes + " B";
|
|
1155
1157
|
}
|
|
1156
1158
|
|
|
1157
|
-
module.exports = { CLI };
|
|
1159
|
+
module.exports = { CLI, VERSION, VERSION_TAG, VERSION_FULL };
|
package/src/static.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Single-file token dashboard UI - identical to the Python build.
|
|
2
|
-
module.exports = "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>OGPT \u00b7 Token Dashboard</title>\n<script src=\"https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js\"></script>\n<style>\n:root{\n --bg:#0d0f14; --panel:#141822; --panel2:#181d2a; --line:#232a3a;\n --text:#e8ecf4; --dim:#8b93a7; --mag:#e34fd0; --cyan:#39c6e3;\n --green:#3ddc84; --red:#ff5c69; --amber:#ffc857; --radius:14px;\n --mono:\"SFMono-Regular\",Consolas,\"Liberation Mono\",Menlo,monospace;\n}\n*{box-sizing:border-box}\nbody{margin:0;background:\n radial-gradient(1200px 500px at 80% -10%, #1a1030 0%, transparent 60%),\n radial-gradient(900px 400px at -10% 110%, #06202b 0%, transparent 55%),\n var(--bg);\n color:var(--text);font:15px/1.5 -apple-system,\"Segoe UI\",Roboto,sans-serif;}\n.wrap{max-width:1180px;margin:0 auto;padding:18px 20px 40px}\n\n/* topbar */\n.topbar{display:flex;align-items:center;gap:12px;padding:14px 4px;border-bottom:1px solid var(--line)}\n.logo{font-weight:800;font-size:19px;letter-spacing:.3px;color:var(--mag)}\n.logo .dot{color:var(--cyan)}\n.chip{background:var(--panel);border:1px solid var(--line);border-radius:999px;\n padding:4px 12px;font-size:12.5px;color:var(--dim)}\n.chip b{color:var(--text)}\n.live{margin-left:auto;display:flex;align-items:center;gap:7px;font-size:12px;color:var(--green)}\n.live i{width:9px;height:9px;border-radius:50%;background:var(--green);\n box-shadow:0 0 0 0 rgba(61,220,132,.6);animation:pulse 1.6s infinite}\n@keyframes pulse{70%{box-shadow:0 0 0 8px rgba(61,220,132,0)}100%{box-shadow:0 0 0 0 rgba(61,220,132,0)}}\n\n/* cards */\n.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin:18px 0}\n.card{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);padding:14px 16px}\n.card .k{font-size:11.5px;text-transform:uppercase;letter-spacing:.08em;color:var(--dim)}\n.card .v{font-family:var(--mono);font-size:24px;font-weight:700;margin-top:4px}\n.card .s{font-size:11.5px;color:var(--dim);margin-top:2px}\n.v.mag{color:var(--mag)} .v.cyan{color:var(--cyan)} .v.green{color:var(--green)} .v.amber{color:var(--amber)}\n\n/* panels & charts */\n.grid2{display:grid;grid-template-columns:2fr 1fr;gap:12px;margin-bottom:12px}\n.panel{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);padding:14px 16px}\n.panel h3{margin:0 0 10px;font-size:13px;color:var(--dim);text-transform:uppercase;letter-spacing:.07em}\ncanvas{width:100%!important;height:240px!important}\n\n/* main columns */\n.main{display:grid;grid-template-columns:1fr 380px;gap:12px}\n@media(max-width:900px){.main,.grid2{grid-template-columns:1fr}}\n#feed{height:430px;overflow-y:auto;display:flex;flex-direction:column;gap:10px;padding-right:6px}\n.msg{border:1px solid var(--line);border-left:3px solid var(--cyan);border-radius:10px;\n background:var(--panel2);padding:10px 12px}\n.msg.user{border-left-color:var(--mag)}\n.msg .who{font-size:11.5px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--dim);margin-bottom:3px}\n.msg.user .who{color:var(--mag)} .msg.bot .who{color:var(--cyan)}\n.msg .txt{white-space:pre-wrap;word-wrap:break-word;font-size:14px}\n.meta{font-size:11px;color:var(--dim);font-family:var(--mono);margin-top:6px}\n.empty{color:var(--dim);text-align:center;padding:30px 0}\n\n/* chat composer */\n.composer{display:flex;flex-direction:column;gap:8px}\n#chat-in{width:100%;min-height:74px;resize:vertical;background:var(--panel2);color:var(--text);\n border:1px solid var(--line);border-radius:10px;padding:10px 12px;font:inherit}\n#chat-in:focus{outline:none;border-color:var(--mag)}\n.rowbtn{display:flex;gap:8px;align-items:center}\nbutton.send{background:linear-gradient(135deg,var(--mag),#8a4fe3);border:0;color:#fff;font-weight:700;\n padding:9px 20px;border-radius:999px;cursor:pointer;font-size:14px}\nbutton.send:disabled{opacity:.45;cursor:not-allowed}\n.hint{font-size:11.5px;color:var(--dim)}\n\n/* bottom tables */\n.bottom{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:12px}\n@media(max-width:900px){.bottom{grid-template-columns:1fr}}\ntable{width:100%;border-collapse:collapse;font-size:13px}\ntd,th{padding:7px 8px;border-bottom:1px solid var(--line);text-align:left}\nth{color:var(--dim);font-weight:600;font-size:11.5px;text-transform:uppercase;letter-spacing:.06em}\n.tools{display:flex;flex-wrap:wrap;gap:7px}\n.toolchip{background:var(--panel2);border:1px solid var(--line);border-radius:999px;\n padding:4px 11px;font-size:12px;color:var(--cyan);font-family:var(--mono)}\nfooter{margin-top:22px;color:var(--dim);font-size:12px;display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px}\nfooter code{font-family:var(--mono);color:var(--cyan)}\n</style>\n</head>\n<body>\n<div class=\"wrap\">\n\n <div class=\"topbar\">\n <span class=\"logo\">\u25c6 OGPT<span class=\"dot\"> \u00b7 </span>Token Dashboard</span>\n <span class=\"chip\" id=\"chip-model\"><b>\u2014</b></span>\n <span class=\"chip\" id=\"chip-provider\">provider \u2014</span>\n <span class=\"live\"><i></i> LIVE</span>\n </div>\n\n <div class=\"cards\">\n <div class=\"card\"><div class=\"k\">Total tokens</div><div class=\"v mag\" id=\"c-total\">0</div><div class=\"s\" id=\"c-total-s\">prompt + completion</div></div>\n <div class=\"card\"><div class=\"k\">Prompt tokens</div><div class=\"v\" id=\"c-prompt\">0</div></div>\n <div class=\"card\"><div class=\"k\">Completion tokens</div><div class=\"v cyan\" id=\"c-completion\">0</div></div>\n <div class=\"card\"><div class=\"k\">Avg speed</div><div class=\"v green\" id=\"c-avgtps\">\u2013</div><div class=\"s\">tokens/sec</div></div>\n <div class=\"card\"><div class=\"k\">Best speed</div><div class=\"v green\" id=\"c-besttps\">\u2013</div></div>\n <div class=\"card\"><div class=\"k\">Last TTFT</div><div class=\"v amber\" id=\"c-ttft\">\u2013</div><div class=\"s\">time to first token</div></div>\n <div class=\"card\"><div class=\"k\">Messages</div><div class=\"v\" id=\"c-msgs\">0</div><div class=\"s\" id=\"c-runs\">0 runs tracked</div></div>\n <div class=\"card\"><div class=\"k\">Uptime</div><div class=\"v\" id=\"c-uptime\">0:00</div><div class=\"s\" id=\"c-session\">no session</div></div>\n </div>\n\n <div class=\"grid2\">\n <div class=\"panel\"><h3>Tokens per run</h3><canvas id=\"ch-runs\"></canvas></div>\n <div class=\"panel\"><h3>Token split</h3><canvas id=\"ch-split\"></canvas></div>\n </div>\n\n <div class=\"main\">\n <div class=\"panel\"><h3>Conversation</h3><div id=\"feed\"><div class=\"empty\">No messages yet.</div></div></div>\n <div class=\"panel composer-panel\">\n <h3>Chat with this agent</h3>\n <div class=\"composer\">\n <textarea id=\"chat-in\" placeholder=\"Ask anything\u2026 (shares the same brain as the terminal session)\"></textarea>\n <div class=\"rowbtn\">\n <button class=\"send\" id=\"chat-send\">Send \u23ce</button>\n <span class=\"hint\" id=\"chat-hint\">Streams from the live model.</span>\n </div>\n <div class=\"hint\">Enter to send \u00b7 Shift+Enter newline</div>\n </div>\n </div>\n </div>\n\n <div class=\"bottom\">\n <div class=\"panel\"><h3>Sessions</h3>\n <table><thead><tr><th>ID</th><th>Title</th><th>Msgs</th><th>Updated</th></tr></thead>\n <tbody id=\"sess-rows\"><tr><td colspan=\"4\" style=\"color:var(--dim)\">\u2014</td></tr></tbody></table>\n </div>\n <div class=\"panel\"><h3>Registered tools (<span id=\"tool-count\">0</span>)</h3>\n <div class=\"tools\" id=\"tools-list\"><span style=\"color:var(--dim)\">\u2014</span></div>\n </div>\n </div>\n\n <footer>\n <span>OGPT v1.1.0 \u00b7 dashboard bound to <code>127.0.0.1</code> only</span>\n <span>polling every 1.5s \u00b7 <code>/api/stats</code></span>\n </footer>\n</div>\n\n<script>\nconst $=id=>document.getElementById(id);\nconst fmt=n=>Number(n||0).toLocaleString();\nlet runsChart=null, splitChart=null;\nlet lastHistoryKey=\"\", busy=false;\n\nfunction fmtDur(s){s=Math.floor(s||0);const h=Math.floor(s/3600),m=Math.floor(s%3600/60),\n x=s%60;return (h?h+\":\":\"\")+m+\":\"+String(x).padStart(2,\"0\");}\nfunction setText(id,v){$(id).textContent=v;}\n\nasync function poll(){\n try{\n const d=await (await fetch(\"/api/stats\")).json();\n setText(\"chip-model\",\"\");$(\"chip-model\").innerHTML=\"<b>\"+d.model+\"</b>\";\n $(\"chip-provider\").textContent=\"provider \"+d.provider+\" \u00b7 \"+d.cwd;\n setText(\"c-total\",fmt(d.tokens.total));\n setText(\"c-total-s\",fmt(d.tokens.prompt)+\" prompt + \"+fmt(d.tokens.completion)+\" completion\");\n setText(\"c-prompt\",fmt(d.tokens.prompt));\n setText(\"c-completion\",fmt(d.tokens.completion));\n setText(\"c-avgtps\",d.avg_tps?d.avg_tps.toFixed(1):\"\u2013\");\n setText(\"c-besttps\",d.best_tps?d.best_tps.toFixed(1):\"\u2013\");\n const lr=d.last_run||{};\n setText(\"c-ttft\",(lr.ttft!=null)?lr.ttft.toFixed(2)+\"s\":\"\u2013\");\n setText(\"c-msgs\",fmt(d.messages));\n setText(\"c-runs\",fmt(d.runs_count)+\" runs tracked\");\n setText(\"c-uptime\",fmtDur(d.uptime_s));\n setText(\"c-session\",d.session?(\"session \"+d.session):\"no session\");\n updateCharts(d);\n }catch(e){/* server gone quiet */}\n}\n\nfunction ensureCharts(){\n if(!window.Chart||runsChart)return;\n Chart.defaults.color=\"#8b93a7\";Chart.defaults.borderColor=\"#232a3a\";\n runsChart=new Chart($(\"ch-runs\"),{type:\"line\",\n data:{labels:[],datasets:[{label:\"tokens\",data:[],borderColor:\"#e34fd0\",\n backgroundColor:\"rgba(227,79,208,.15)\",fill:true,tension:.35,pointRadius:2}]},\n options:{animation:false,maintainAspectRatio:false,\n scales:{y:{beginAtZero:true},x:{ticks:{maxTicksLimit:10}}}}});\n splitChart=new Chart($(\"ch-split\"),{type:\"doughnut\",\n data:{labels:[\"prompt\",\"completion\"],\n datasets:[{data:[0,0],backgroundColor:[\"#39c6e3\",\"#e34fd0\"],borderWidth:0}]},\n options:{animation:false,maintainAspectRatio:false,cutout:\"62%\",\n plugins:{legend:{position:\"bottom\"}}}});\n}\nfunction updateCharts(d){\n ensureCharts();if(!runsChart)return;\n const rs=d.runs||[];\n runsChart.data.labels=rs.map(r=>new Date(r.ts*1000).toLocaleTimeString());\n runsChart.data.datasets[0].data=rs.map(r=>r.tokens);\n runsChart.update(\"none\");\n splitChart.data.datasets[0].data=[d.tokens.prompt,d.tokens.completion];\n splitChart.update(\"none\");\n}\n\nasync function loadHistory(force){\n try{\n const msgs=await (await fetch(\"/api/history\")).json();\n const key=msgs.length+\":\"+((msgs[msgs.length-1]||{}).ts||0);\n if(!force&&key===lastHistoryKey)return;\n lastHistoryKey=key;\n const feed=$(\"feed\");feed.textContent=\"\";\n if(!msgs.length){feed.innerHTML='<div class=\"empty\">No messages yet.</div>';return;}\n for(const m of msgs){\n const div=document.createElement(\"div\");\n div.className=\"msg \"+(m.role===\"user\"?\"user\":\"bot\");\n const who=document.createElement(\"div\");who.className=\"who\";\n who.textContent=m.role===\"user\"?\"\u276f You\":\"\u25cf oGPT\";\n const txt=document.createElement(\"div\");txt.className=\"txt\";\n txt.textContent=m.content;\n div.append(who,txt);\n if(m.ts){const meta=document.createElement(\"div\");meta.className=\"meta\";\n meta.textContent=new Date(m.ts*1000).toLocaleString();div.append(meta);}\n feed.appendChild(div);\n }\n feed.scrollTop=feed.scrollHeight;\n }catch(e){}\n}\n\nfunction addBubble(kind){\n const div=document.createElement(\"div\");\n div.className=\"msg \"+kind;\n const who=document.createElement(\"div\");who.className=\"who\";\n who.textContent=kind===\"user\"?\"\u276f You\":\"\u25cf oGPT\";\n const txt=document.createElement(\"div\");txt.className=\"txt\";\n if(kind!==\"user\"){const m=document.createElement(\"div\");m.className=\"meta\";\n m.textContent=\"streaming\u2026\";txt.dataset.meta=m;txt.append(m);}\n div.append(who,txt);$(\"feed\").appendChild(div);\n $(\"feed\").scrollTop=$(\"feed\").scrollHeight;\n return txt;\n}\nfunction sendChat(){\n if(busy)return;\n const inp=$(\"chat-in\"),msg=inp.value.trim();\n if(!msg)return;\n inp.value=\"\";busy=true;$(\"chat-send\").disabled=true;\n addBubble(\"user\");\n const bot=addBubble(\"bot\");\n let acc=\"\";\n const es=new EventSource(\"/api/chat/stream?msg=\"+encodeURIComponent(msg));\n es.onmessage=e=>{\n let d;try{d=JSON.parse(e.data);}catch(_){return;}\n if(d.t){acc+=d.t;bot.textContent=acc;\n $(\"feed\").scrollTop=$(\"feed\").scrollHeight;}\n if(d.error){acc+=\"\\n[error] \"+d.error;bot.textContent=acc;}\n if(d.done){es.close();\n const s=d.stats||{};const bits=[];\n if(s.tokens)bits.push(s.tokens+\" tok\");\n if(s.tps)bits.push(s.tps+\" tok/s\");\n bot.dataset.meta&&(bot.querySelector(\".meta\")||{}).remove;\n const meta=document.createElement(\"div\");meta.className=\"meta\";\n meta.textContent=bits.length?bits.join(\" \u00b7 \"):\"done\";\n bot.parentElement.appendChild(meta);\n busy=false;$(\"chat-send\").disabled=false;loadHistory(true);}\n };\n es.onerror=()=>{es.close();busy=false;$(\"chat-send\").disabled=false;};\n}\n$(\"chat-send\").addEventListener(\"click\",sendChat);\n$(\"chat-in\").addEventListener(\"keydown\",ev=>{\n if(ev.key===\"Enter\"&&!ev.shiftKey){ev.preventDefault();sendChat();}\n});\n\nasync function loadBottom(){\n try{\n const rows=await (await fetch(\"/api/sessions\")).json();\n const tb=$(\"sess-rows\");tb.textContent=\"\";\n if(!rows.length){tb.innerHTML='<tr><td colspan=\"4\" style=\"color:var(--dim)\">\u2014</td></tr>';return;}\n for(const s of rows){\n const tr=document.createElement(\"tr\");\n [s.id,s.title,s.msgs,new Date((s.updated||0)*1000).toLocaleString()].forEach(v=>{\n const td=document.createElement(\"td\");td.textContent=v??\"\";tr.appendChild(td);});\n tb.appendChild(tr);\n }\n }catch(e){}\n try{\n const names=await (await fetch(\"/api/tools\")).json();\n $(\"tool-count\").textContent=names.length;\n const list=$(\"tools-list\");list.textContent=\"\";\n for(const n of names){const c=document.createElement(\"span\");\n c.className=\"toolchip\";c.textContent=n;list.appendChild(c);}\n }catch(e){}\n}\n\npoll();loadHistory();loadBottom();\nsetInterval(poll,1500);\nsetInterval(()=>loadHistory(false),5000);\nsetInterval(loadBottom,20000);\nensureCharts();\n</script>\n</body>\n</html>\n";
|
|
2
|
+
module.exports = "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>OGPT \u00b7 Token Dashboard</title>\n<script src=\"https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js\"></script>\n<style>\n:root{\n --bg:#0d0f14; --panel:#141822; --panel2:#181d2a; --line:#232a3a;\n --text:#e8ecf4; --dim:#8b93a7; --mag:#e34fd0; --cyan:#39c6e3;\n --green:#3ddc84; --red:#ff5c69; --amber:#ffc857; --radius:14px;\n --mono:\"SFMono-Regular\",Consolas,\"Liberation Mono\",Menlo,monospace;\n}\n*{box-sizing:border-box}\nbody{margin:0;background:\n radial-gradient(1200px 500px at 80% -10%, #1a1030 0%, transparent 60%),\n radial-gradient(900px 400px at -10% 110%, #06202b 0%, transparent 55%),\n var(--bg);\n color:var(--text);font:15px/1.5 -apple-system,\"Segoe UI\",Roboto,sans-serif;}\n.wrap{max-width:1180px;margin:0 auto;padding:18px 20px 40px}\n\n/* topbar */\n.topbar{display:flex;align-items:center;gap:12px;padding:14px 4px;border-bottom:1px solid var(--line)}\n.logo{font-weight:800;font-size:19px;letter-spacing:.3px;color:var(--mag)}\n.logo .dot{color:var(--cyan)}\n.chip{background:var(--panel);border:1px solid var(--line);border-radius:999px;\n padding:4px 12px;font-size:12.5px;color:var(--dim)}\n.chip b{color:var(--text)}\n.live{margin-left:auto;display:flex;align-items:center;gap:7px;font-size:12px;color:var(--green)}\n.live i{width:9px;height:9px;border-radius:50%;background:var(--green);\n box-shadow:0 0 0 0 rgba(61,220,132,.6);animation:pulse 1.6s infinite}\n@keyframes pulse{70%{box-shadow:0 0 0 8px rgba(61,220,132,0)}100%{box-shadow:0 0 0 0 rgba(61,220,132,0)}}\n\n/* cards */\n.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin:18px 0}\n.card{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);padding:14px 16px}\n.card .k{font-size:11.5px;text-transform:uppercase;letter-spacing:.08em;color:var(--dim)}\n.card .v{font-family:var(--mono);font-size:24px;font-weight:700;margin-top:4px}\n.card .s{font-size:11.5px;color:var(--dim);margin-top:2px}\n.v.mag{color:var(--mag)} .v.cyan{color:var(--cyan)} .v.green{color:var(--green)} .v.amber{color:var(--amber)}\n\n/* panels & charts */\n.grid2{display:grid;grid-template-columns:2fr 1fr;gap:12px;margin-bottom:12px}\n.panel{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);padding:14px 16px}\n.panel h3{margin:0 0 10px;font-size:13px;color:var(--dim);text-transform:uppercase;letter-spacing:.07em}\ncanvas{width:100%!important;height:240px!important}\n\n/* main columns */\n.main{display:grid;grid-template-columns:1fr 380px;gap:12px}\n@media(max-width:900px){.main,.grid2{grid-template-columns:1fr}}\n#feed{height:430px;overflow-y:auto;display:flex;flex-direction:column;gap:10px;padding-right:6px}\n.msg{border:1px solid var(--line);border-left:3px solid var(--cyan);border-radius:10px;\n background:var(--panel2);padding:10px 12px}\n.msg.user{border-left-color:var(--mag)}\n.msg .who{font-size:11.5px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--dim);margin-bottom:3px}\n.msg.user .who{color:var(--mag)} .msg.bot .who{color:var(--cyan)}\n.msg .txt{white-space:pre-wrap;word-wrap:break-word;font-size:14px}\n.meta{font-size:11px;color:var(--dim);font-family:var(--mono);margin-top:6px}\n.empty{color:var(--dim);text-align:center;padding:30px 0}\n\n/* chat composer */\n.composer{display:flex;flex-direction:column;gap:8px}\n#chat-in{width:100%;min-height:74px;resize:vertical;background:var(--panel2);color:var(--text);\n border:1px solid var(--line);border-radius:10px;padding:10px 12px;font:inherit}\n#chat-in:focus{outline:none;border-color:var(--mag)}\n.rowbtn{display:flex;gap:8px;align-items:center}\nbutton.send{background:linear-gradient(135deg,var(--mag),#8a4fe3);border:0;color:#fff;font-weight:700;\n padding:9px 20px;border-radius:999px;cursor:pointer;font-size:14px}\nbutton.send:disabled{opacity:.45;cursor:not-allowed}\n.hint{font-size:11.5px;color:var(--dim)}\n\n/* bottom tables */\n.bottom{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:12px}\n@media(max-width:900px){.bottom{grid-template-columns:1fr}}\ntable{width:100%;border-collapse:collapse;font-size:13px}\ntd,th{padding:7px 8px;border-bottom:1px solid var(--line);text-align:left}\nth{color:var(--dim);font-weight:600;font-size:11.5px;text-transform:uppercase;letter-spacing:.06em}\n.tools{display:flex;flex-wrap:wrap;gap:7px}\n.toolchip{background:var(--panel2);border:1px solid var(--line);border-radius:999px;\n padding:4px 11px;font-size:12px;color:var(--cyan);font-family:var(--mono)}\nfooter{margin-top:22px;color:var(--dim);font-size:12px;display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px}\nfooter code{font-family:var(--mono);color:var(--cyan)}\n</style>\n</head>\n<body>\n<div class=\"wrap\">\n\n <div class=\"topbar\">\n <span class=\"logo\">\u25c6 OGPT<span class=\"dot\"> \u00b7 </span>Token Dashboard</span>\n <span class=\"chip\" id=\"chip-model\"><b>\u2014</b></span>\n <span class=\"chip\" id=\"chip-provider\">provider \u2014</span>\n <span class=\"live\"><i></i> LIVE</span>\n </div>\n\n <div class=\"cards\">\n <div class=\"card\"><div class=\"k\">Total tokens</div><div class=\"v mag\" id=\"c-total\">0</div><div class=\"s\" id=\"c-total-s\">prompt + completion</div></div>\n <div class=\"card\"><div class=\"k\">Prompt tokens</div><div class=\"v\" id=\"c-prompt\">0</div></div>\n <div class=\"card\"><div class=\"k\">Completion tokens</div><div class=\"v cyan\" id=\"c-completion\">0</div></div>\n <div class=\"card\"><div class=\"k\">Avg speed</div><div class=\"v green\" id=\"c-avgtps\">\u2013</div><div class=\"s\">tokens/sec</div></div>\n <div class=\"card\"><div class=\"k\">Best speed</div><div class=\"v green\" id=\"c-besttps\">\u2013</div></div>\n <div class=\"card\"><div class=\"k\">Last TTFT</div><div class=\"v amber\" id=\"c-ttft\">\u2013</div><div class=\"s\">time to first token</div></div>\n <div class=\"card\"><div class=\"k\">Messages</div><div class=\"v\" id=\"c-msgs\">0</div><div class=\"s\" id=\"c-runs\">0 runs tracked</div></div>\n <div class=\"card\"><div class=\"k\">Uptime</div><div class=\"v\" id=\"c-uptime\">0:00</div><div class=\"s\" id=\"c-session\">no session</div></div>\n </div>\n\n <div class=\"grid2\">\n <div class=\"panel\"><h3>Tokens per run</h3><canvas id=\"ch-runs\"></canvas></div>\n <div class=\"panel\"><h3>Token split</h3><canvas id=\"ch-split\"></canvas></div>\n </div>\n\n <div class=\"main\">\n <div class=\"panel\"><h3>Conversation</h3><div id=\"feed\"><div class=\"empty\">No messages yet.</div></div></div>\n <div class=\"panel composer-panel\">\n <h3>Chat with this agent</h3>\n <div class=\"composer\">\n <textarea id=\"chat-in\" placeholder=\"Ask anything\u2026 (shares the same brain as the terminal session)\"></textarea>\n <div class=\"rowbtn\">\n <button class=\"send\" id=\"chat-send\">Send \u23ce</button>\n <span class=\"hint\" id=\"chat-hint\">Streams from the live model.</span>\n </div>\n <div class=\"hint\">Enter to send \u00b7 Shift+Enter newline</div>\n </div>\n </div>\n </div>\n\n <div class=\"bottom\">\n <div class=\"panel\"><h3>Sessions</h3>\n <table><thead><tr><th>ID</th><th>Title</th><th>Msgs</th><th>Updated</th></tr></thead>\n <tbody id=\"sess-rows\"><tr><td colspan=\"4\" style=\"color:var(--dim)\">\u2014</td></tr></tbody></table>\n </div>\n <div class=\"panel\"><h3>Registered tools (<span id=\"tool-count\">0</span>)</h3>\n <div class=\"tools\" id=\"tools-list\"><span style=\"color:var(--dim)\">\u2014</span></div>\n </div>\n </div>\n\n <footer>\n <span>OGPT v0.1.5-beta(devices-all) \u00b7 dashboard bound to <code>127.0.0.1</code> only</span>\n <span>polling every 1.5s \u00b7 <code>/api/stats</code></span>\n </footer>\n</div>\n\n<script>\nconst $=id=>document.getElementById(id);\nconst fmt=n=>Number(n||0).toLocaleString();\nlet runsChart=null, splitChart=null;\nlet lastHistoryKey=\"\", busy=false;\n\nfunction fmtDur(s){s=Math.floor(s||0);const h=Math.floor(s/3600),m=Math.floor(s%3600/60),\n x=s%60;return (h?h+\":\":\"\")+m+\":\"+String(x).padStart(2,\"0\");}\nfunction setText(id,v){$(id).textContent=v;}\n\nasync function poll(){\n try{\n const d=await (await fetch(\"/api/stats\")).json();\n setText(\"chip-model\",\"\");$(\"chip-model\").innerHTML=\"<b>\"+d.model+\"</b>\";\n $(\"chip-provider\").textContent=\"provider \"+d.provider+\" \u00b7 \"+d.cwd;\n setText(\"c-total\",fmt(d.tokens.total));\n setText(\"c-total-s\",fmt(d.tokens.prompt)+\" prompt + \"+fmt(d.tokens.completion)+\" completion\");\n setText(\"c-prompt\",fmt(d.tokens.prompt));\n setText(\"c-completion\",fmt(d.tokens.completion));\n setText(\"c-avgtps\",d.avg_tps?d.avg_tps.toFixed(1):\"\u2013\");\n setText(\"c-besttps\",d.best_tps?d.best_tps.toFixed(1):\"\u2013\");\n const lr=d.last_run||{};\n setText(\"c-ttft\",(lr.ttft!=null)?lr.ttft.toFixed(2)+\"s\":\"\u2013\");\n setText(\"c-msgs\",fmt(d.messages));\n setText(\"c-runs\",fmt(d.runs_count)+\" runs tracked\");\n setText(\"c-uptime\",fmtDur(d.uptime_s));\n setText(\"c-session\",d.session?(\"session \"+d.session):\"no session\");\n updateCharts(d);\n }catch(e){/* server gone quiet */}\n}\n\nfunction ensureCharts(){\n if(!window.Chart||runsChart)return;\n Chart.defaults.color=\"#8b93a7\";Chart.defaults.borderColor=\"#232a3a\";\n runsChart=new Chart($(\"ch-runs\"),{type:\"line\",\n data:{labels:[],datasets:[{label:\"tokens\",data:[],borderColor:\"#e34fd0\",\n backgroundColor:\"rgba(227,79,208,.15)\",fill:true,tension:.35,pointRadius:2}]},\n options:{animation:false,maintainAspectRatio:false,\n scales:{y:{beginAtZero:true},x:{ticks:{maxTicksLimit:10}}}}});\n splitChart=new Chart($(\"ch-split\"),{type:\"doughnut\",\n data:{labels:[\"prompt\",\"completion\"],\n datasets:[{data:[0,0],backgroundColor:[\"#39c6e3\",\"#e34fd0\"],borderWidth:0}]},\n options:{animation:false,maintainAspectRatio:false,cutout:\"62%\",\n plugins:{legend:{position:\"bottom\"}}}});\n}\nfunction updateCharts(d){\n ensureCharts();if(!runsChart)return;\n const rs=d.runs||[];\n runsChart.data.labels=rs.map(r=>new Date(r.ts*1000).toLocaleTimeString());\n runsChart.data.datasets[0].data=rs.map(r=>r.tokens);\n runsChart.update(\"none\");\n splitChart.data.datasets[0].data=[d.tokens.prompt,d.tokens.completion];\n splitChart.update(\"none\");\n}\n\nasync function loadHistory(force){\n try{\n const msgs=await (await fetch(\"/api/history\")).json();\n const key=msgs.length+\":\"+((msgs[msgs.length-1]||{}).ts||0);\n if(!force&&key===lastHistoryKey)return;\n lastHistoryKey=key;\n const feed=$(\"feed\");feed.textContent=\"\";\n if(!msgs.length){feed.innerHTML='<div class=\"empty\">No messages yet.</div>';return;}\n for(const m of msgs){\n const div=document.createElement(\"div\");\n div.className=\"msg \"+(m.role===\"user\"?\"user\":\"bot\");\n const who=document.createElement(\"div\");who.className=\"who\";\n who.textContent=m.role===\"user\"?\"\u276f You\":\"\u25cf oGPT\";\n const txt=document.createElement(\"div\");txt.className=\"txt\";\n txt.textContent=m.content;\n div.append(who,txt);\n if(m.ts){const meta=document.createElement(\"div\");meta.className=\"meta\";\n meta.textContent=new Date(m.ts*1000).toLocaleString();div.append(meta);}\n feed.appendChild(div);\n }\n feed.scrollTop=feed.scrollHeight;\n }catch(e){}\n}\n\nfunction addBubble(kind){\n const div=document.createElement(\"div\");\n div.className=\"msg \"+kind;\n const who=document.createElement(\"div\");who.className=\"who\";\n who.textContent=kind===\"user\"?\"\u276f You\":\"\u25cf oGPT\";\n const txt=document.createElement(\"div\");txt.className=\"txt\";\n if(kind!==\"user\"){const m=document.createElement(\"div\");m.className=\"meta\";\n m.textContent=\"streaming\u2026\";txt.dataset.meta=m;txt.append(m);}\n div.append(who,txt);$(\"feed\").appendChild(div);\n $(\"feed\").scrollTop=$(\"feed\").scrollHeight;\n return txt;\n}\nfunction sendChat(){\n if(busy)return;\n const inp=$(\"chat-in\"),msg=inp.value.trim();\n if(!msg)return;\n inp.value=\"\";busy=true;$(\"chat-send\").disabled=true;\n addBubble(\"user\");\n const bot=addBubble(\"bot\");\n let acc=\"\";\n const es=new EventSource(\"/api/chat/stream?msg=\"+encodeURIComponent(msg));\n es.onmessage=e=>{\n let d;try{d=JSON.parse(e.data);}catch(_){return;}\n if(d.t){acc+=d.t;bot.textContent=acc;\n $(\"feed\").scrollTop=$(\"feed\").scrollHeight;}\n if(d.error){acc+=\"\\n[error] \"+d.error;bot.textContent=acc;}\n if(d.done){es.close();\n const s=d.stats||{};const bits=[];\n if(s.tokens)bits.push(s.tokens+\" tok\");\n if(s.tps)bits.push(s.tps+\" tok/s\");\n bot.dataset.meta&&(bot.querySelector(\".meta\")||{}).remove;\n const meta=document.createElement(\"div\");meta.className=\"meta\";\n meta.textContent=bits.length?bits.join(\" \u00b7 \"):\"done\";\n bot.parentElement.appendChild(meta);\n busy=false;$(\"chat-send\").disabled=false;loadHistory(true);}\n };\n es.onerror=()=>{es.close();busy=false;$(\"chat-send\").disabled=false;};\n}\n$(\"chat-send\").addEventListener(\"click\",sendChat);\n$(\"chat-in\").addEventListener(\"keydown\",ev=>{\n if(ev.key===\"Enter\"&&!ev.shiftKey){ev.preventDefault();sendChat();}\n});\n\nasync function loadBottom(){\n try{\n const rows=await (await fetch(\"/api/sessions\")).json();\n const tb=$(\"sess-rows\");tb.textContent=\"\";\n if(!rows.length){tb.innerHTML='<tr><td colspan=\"4\" style=\"color:var(--dim)\">\u2014</td></tr>';return;}\n for(const s of rows){\n const tr=document.createElement(\"tr\");\n [s.id,s.title,s.msgs,new Date((s.updated||0)*1000).toLocaleString()].forEach(v=>{\n const td=document.createElement(\"td\");td.textContent=v??\"\";tr.appendChild(td);});\n tb.appendChild(tr);\n }\n }catch(e){}\n try{\n const names=await (await fetch(\"/api/tools\")).json();\n $(\"tool-count\").textContent=names.length;\n const list=$(\"tools-list\");list.textContent=\"\";\n for(const n of names){const c=document.createElement(\"span\");\n c.className=\"toolchip\";c.textContent=n;list.appendChild(c);}\n }catch(e){}\n}\n\npoll();loadHistory();loadBottom();\nsetInterval(poll,1500);\nsetInterval(()=>loadHistory(false),5000);\nsetInterval(loadBottom,20000);\nensureCharts();\n</script>\n</body>\n</html>\n";
|
package/src/tools.js
CHANGED
|
@@ -424,6 +424,77 @@ function resolveBin(bin) {
|
|
|
424
424
|
return bin; // let spawnSync produce its own ENOENT
|
|
425
425
|
}
|
|
426
426
|
|
|
427
|
+
// ---- silent package auto-heal --------------------------------------------
|
|
428
|
+
// When AI-written code imports something that is not installed, we install
|
|
429
|
+
// it quietly (all installer output hidden) and retry once. The user only
|
|
430
|
+
// ever sees 'Installing packages...' via the short result line.
|
|
431
|
+
|
|
432
|
+
const PY_ALIASES = {
|
|
433
|
+
cv2: "opencv-python", PIL: "pillow", bs4: "beautifulsoup4",
|
|
434
|
+
yaml: "pyyaml", sklearn: "scikit-learn", dotenv: "python-dotenv",
|
|
435
|
+
Crypto: "pycryptodome", fitz: "PyMuPDF", docx: "python-docx",
|
|
436
|
+
pptx: "python-pptx",
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
function pyMissingModules(text) {
|
|
440
|
+
const names = [];
|
|
441
|
+
for (const m of String(text || "").matchAll(/No module named '(?:([\w.]+)'|([\w.]+)")/g)) {
|
|
442
|
+
const root = (m[1] || m[2] || "").split(".")[0];
|
|
443
|
+
if (root && !names.includes(root)) names.push(root);
|
|
444
|
+
}
|
|
445
|
+
return names;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function pipInstallQuiet(mods) {
|
|
449
|
+
const pkgs = mods.map((m) => PY_ALIASES[m] || m);
|
|
450
|
+
try {
|
|
451
|
+
const r = spawnSync(resolveBin("python3") || "python3",
|
|
452
|
+
["-m", "pip", "install", "--quiet", "--disable-pip-version-check", ...pkgs],
|
|
453
|
+
{ stdio: "ignore", timeout: 600000 });
|
|
454
|
+
return !r.error && r.status === 0;
|
|
455
|
+
} catch { return false; }
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function nodeMissingModules(text) {
|
|
459
|
+
const names = [];
|
|
460
|
+
for (const m of String(text || "").matchAll(/Cannot find module '([^']+)'/g)) {
|
|
461
|
+
const spec = m[1];
|
|
462
|
+
if (!spec || /^([./]|node:|[A-Z]:\\)/.test(spec)) continue; // relative/builtin
|
|
463
|
+
if (!names.includes(spec)) names.push(spec);
|
|
464
|
+
}
|
|
465
|
+
return names;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function npmInstallQuiet(pkgs, cwd) {
|
|
469
|
+
try {
|
|
470
|
+
const r = spawnSync(resolveBin("npm") || "npm",
|
|
471
|
+
["install", "--no-fund", "--no-audit", "--silent", ...pkgs],
|
|
472
|
+
{ cwd: path.resolve(cwd || "."), stdio: "ignore", timeout: 600000 });
|
|
473
|
+
return !r.error && r.status === 0;
|
|
474
|
+
} catch { return false; }
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function autoHealNode(result, rerun, cwd) {
|
|
478
|
+
// NOTE: runInterpreter reports success=true for nonzero exits (the process
|
|
479
|
+
// spawned fine), so key off the traceback text, not the success flag.
|
|
480
|
+
const text = (result.output || "") + "\n" + (result.error || "");
|
|
481
|
+
const failed = !result.success || /\[exit \d+\]/.test(text);
|
|
482
|
+
if (!failed) return result;
|
|
483
|
+
const mods = nodeMissingModules(text);
|
|
484
|
+
if (mods.length && npmInstallQuiet(mods, cwd)) {
|
|
485
|
+
const r2 = rerun();
|
|
486
|
+
if (r2.success) r2.output = `Installed ${mods.join(", ")}\n${r2.output}`;
|
|
487
|
+
return r2;
|
|
488
|
+
}
|
|
489
|
+
const pymods = pyMissingModules(text);
|
|
490
|
+
if (pymods.length && pipInstallQuiet(pymods)) {
|
|
491
|
+
const r2 = rerun();
|
|
492
|
+
if (r2.success) r2.output = `Installed ${pymods.join(", ")}\n${r2.output}`;
|
|
493
|
+
return r2;
|
|
494
|
+
}
|
|
495
|
+
return result;
|
|
496
|
+
}
|
|
497
|
+
|
|
427
498
|
function runInterpreter(bin, argv, cwd, timeout) {
|
|
428
499
|
const r = spawnSync(resolveBin(bin), argv, {
|
|
429
500
|
cwd: path.resolve(cwd || "."), timeout: timeout || 30000,
|
|
@@ -659,8 +730,10 @@ const IMPLEMENTATIONS = {
|
|
|
659
730
|
let content = String(args.content ?? "");
|
|
660
731
|
if (!content.trim()) {
|
|
661
732
|
// An empty .html file gets a professional HTML5 skeleton (page title
|
|
662
|
-
// from the file name) instead of a blank 0-byte file.
|
|
663
|
-
|
|
733
|
+
// from the file name) instead of a blank 0-byte file. Empty .css/.js
|
|
734
|
+
// files get minimal modern starting points.
|
|
735
|
+
const ext = path.extname(p).toLowerCase();
|
|
736
|
+
if (ext === ".html") {
|
|
664
737
|
let title = path.basename(p, ".html").replace(/[-_]/g, " ").trim();
|
|
665
738
|
title = title ? title[0].toUpperCase() + title.slice(1) : "Document";
|
|
666
739
|
content =
|
|
@@ -669,6 +742,25 @@ const IMPLEMENTATIONS = {
|
|
|
669
742
|
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" +
|
|
670
743
|
` <title>${title}</title>\n` +
|
|
671
744
|
"</head>\n<body>\n\n</body>\n</html>\n";
|
|
745
|
+
} else if (ext === ".css") {
|
|
746
|
+
content =
|
|
747
|
+
"/* Styles */\n\n" +
|
|
748
|
+
":root {\n" +
|
|
749
|
+
" --bg: #0f172a;\n" +
|
|
750
|
+
" --fg: #e2e8f0;\n" +
|
|
751
|
+
" --accent: #38bdf8;\n" +
|
|
752
|
+
"}\n\n" +
|
|
753
|
+
"*,\n*::before,\n*::after {\n" +
|
|
754
|
+
" box-sizing: border-box;\n" +
|
|
755
|
+
" margin: 0;\n" +
|
|
756
|
+
" padding: 0;\n}\n\n" +
|
|
757
|
+
"body {\n" +
|
|
758
|
+
" font-family: system-ui, sans-serif;\n" +
|
|
759
|
+
" background: var(--bg);\n" +
|
|
760
|
+
" color: var(--fg);\n" +
|
|
761
|
+
" line-height: 1.6;\n}\n";
|
|
762
|
+
} else if (ext === ".js") {
|
|
763
|
+
content = "// App scripts\n\"use strict\";\n";
|
|
672
764
|
}
|
|
673
765
|
}
|
|
674
766
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
@@ -817,7 +909,17 @@ const IMPLEMENTATIONS = {
|
|
|
817
909
|
return new ToolResult("", `File not found: ${file} (use a path relative to your workspace)`, false);
|
|
818
910
|
}
|
|
819
911
|
const argv = file ? [file] : ["-c", code];
|
|
820
|
-
|
|
912
|
+
let r = runInterpreter("python3", argv, args.cwd || ".", parseInt(args.timeout || 60, 10) * 1000);
|
|
913
|
+
// Auto-heal: missing python packages get installed silently (the user
|
|
914
|
+
// only sees 'Installing packages...' via the tool flow) and we retry once.
|
|
915
|
+
if (!r.success) {
|
|
916
|
+
const mods = pyMissingModules((r.error || "") + (r.output || ""));
|
|
917
|
+
if (mods.length && pipInstallQuiet(mods)) {
|
|
918
|
+
r = runInterpreter("python3", argv, args.cwd || ".", parseInt(args.timeout || 60, 10) * 1000);
|
|
919
|
+
if (r.success) r.output = `Installed ${mods.join(", ")}\n${r.output}`;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
return r;
|
|
821
923
|
},
|
|
822
924
|
|
|
823
925
|
run_node(args) {
|
|
@@ -837,15 +939,20 @@ const IMPLEMENTATIONS = {
|
|
|
837
939
|
const tmp = path.join(os.tmpdir(), `ogpt-run-${Date.now()}.mjs`);
|
|
838
940
|
fs.writeFileSync(tmp, code);
|
|
839
941
|
try {
|
|
840
|
-
|
|
942
|
+
let r = runInterpreter("node", [tmp], args.cwd || ".",
|
|
841
943
|
parseInt(args.timeout || 60, 10) * 1000);
|
|
944
|
+
r = autoHealNode(r, () => runInterpreter("node", [tmp], args.cwd || ".",
|
|
945
|
+
parseInt(args.timeout || 60, 10) * 1000), args.cwd || ".");
|
|
946
|
+
return r;
|
|
842
947
|
} finally {
|
|
843
948
|
try { fs.unlinkSync(tmp); } catch {}
|
|
844
949
|
}
|
|
845
950
|
} else {
|
|
846
951
|
argv = ["-e", code];
|
|
847
952
|
}
|
|
848
|
-
|
|
953
|
+
let r = runInterpreter("node", argv, args.cwd || ".", parseInt(args.timeout || 60, 10) * 1000);
|
|
954
|
+
return autoHealNode(r, () => runInterpreter("node", argv, args.cwd || ".",
|
|
955
|
+
parseInt(args.timeout || 60, 10) * 1000), args.cwd || ".");
|
|
849
956
|
},
|
|
850
957
|
|
|
851
958
|
install_packages(args) {
|
|
@@ -1164,4 +1271,4 @@ class ToolRegistry {
|
|
|
1164
1271
|
}
|
|
1165
1272
|
}
|
|
1166
1273
|
|
|
1167
|
-
module.exports = { ToolRegistry, ToolDef, ToolResult, TOOLS };
|
|
1274
|
+
module.exports = { ToolRegistry, ToolDef, ToolResult, TOOLS, IMPLEMENTATIONS };
|
package/src/web.js
CHANGED
|
@@ -111,7 +111,7 @@ function createDashboard(cli) {
|
|
|
111
111
|
return json(res, 200, {
|
|
112
112
|
ok: true,
|
|
113
113
|
app: "OGPT",
|
|
114
|
-
version: "
|
|
114
|
+
version: "0.1.5-beta(devices-all)",
|
|
115
115
|
model: name,
|
|
116
116
|
provider: "Ollama",
|
|
117
117
|
uptime_s: Math.round((Date.now() - STARTED_AT) / 100) / 10,
|
|
@@ -140,7 +140,7 @@ function createDashboard(cli) {
|
|
|
140
140
|
} catch {}
|
|
141
141
|
return json(res, 200, {
|
|
142
142
|
ok: true,
|
|
143
|
-
version: "
|
|
143
|
+
version: "0.1.5-beta(devices-all)",
|
|
144
144
|
model: name,
|
|
145
145
|
model_id: name,
|
|
146
146
|
provider: "Ollama",
|