novaprime 1.7.1 → 1.8.1
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 +11 -3
- package/src/agent.js +4 -0
- package/src/tools.js +121 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "novaprime",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.1",
|
|
4
4
|
"description": "NovaPrime — an AI coding assistant in your terminal, powered by GLM.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"novaprime": "bin/novaprime.js"
|
|
@@ -13,11 +13,19 @@
|
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=18"
|
|
15
15
|
},
|
|
16
|
-
"keywords": [
|
|
16
|
+
"keywords": [
|
|
17
|
+
"ai",
|
|
18
|
+
"cli",
|
|
19
|
+
"coding",
|
|
20
|
+
"assistant",
|
|
21
|
+
"glm",
|
|
22
|
+
"novaprime"
|
|
23
|
+
],
|
|
17
24
|
"license": "MIT",
|
|
18
25
|
"dependencies": {
|
|
19
26
|
"boxen": "^5.1.2",
|
|
20
27
|
"chalk": "^4.1.2",
|
|
21
|
-
"ora": "^5.4.1"
|
|
28
|
+
"ora": "^5.4.1",
|
|
29
|
+
"puppeteer-core": "^25.1.0"
|
|
22
30
|
}
|
|
23
31
|
}
|
package/src/agent.js
CHANGED
|
@@ -26,6 +26,10 @@ const SYSTEM_PROMPT =
|
|
|
26
26
|
`You MAY start the dev server when the project is ready — run it with run_command (e.g. "cd <project> && npm run dev"). ` +
|
|
27
27
|
`Dev/watch servers are automatically run in the BACKGROUND and the tool returns the localhost URL, so they do NOT block. ` +
|
|
28
28
|
`After it starts, give the user the localhost URL to open in their browser, and do NOT start it a second time. ` +
|
|
29
|
+
`BROWSING: you can browse the web with the \`browse\` tool — use it to open a link the user shares and read it, ` +
|
|
30
|
+
`and ESPECIALLY to VERIFY your own work: after starting a site/app, browse its localhost URL to check it actually ` +
|
|
31
|
+
`renders and to see any JavaScript/console errors, then fix the issues you find and browse again to confirm. ` +
|
|
32
|
+
`Use \`open_url\` to open a page in the user's own browser when they want to see it themselves. ` +
|
|
29
33
|
`Be concise and warm. Current OS: ${os.platform()}. Working directory: ${process.cwd()}.`;
|
|
30
34
|
|
|
31
35
|
// Fetch read-only account info for the header (name, plan, usage). Never throws.
|
package/src/tools.js
CHANGED
|
@@ -62,6 +62,82 @@ function stopBackground() {
|
|
|
62
62
|
for (const s of bgServers) { try { s.child.kill(); } catch (_) {} }
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// Find an installed Chrome/Edge/Chromium on this machine (so puppeteer-core needs no download).
|
|
66
|
+
function findBrowser() {
|
|
67
|
+
const cands = [];
|
|
68
|
+
if (process.platform === 'win32') {
|
|
69
|
+
const pf = process.env['PROGRAMFILES'] || 'C:\\Program Files';
|
|
70
|
+
const pf86 = process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)';
|
|
71
|
+
const la = process.env['LOCALAPPDATA'] || '';
|
|
72
|
+
cands.push(
|
|
73
|
+
pf86 + '\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
74
|
+
pf + '\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
75
|
+
pf + '\\Google\\Chrome\\Application\\chrome.exe',
|
|
76
|
+
pf86 + '\\Google\\Chrome\\Application\\chrome.exe',
|
|
77
|
+
la + '\\Google\\Chrome\\Application\\chrome.exe',
|
|
78
|
+
);
|
|
79
|
+
} else if (process.platform === 'darwin') {
|
|
80
|
+
cands.push('/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge', '/Applications/Chromium.app/Contents/MacOS/Chromium');
|
|
81
|
+
} else {
|
|
82
|
+
cands.push('/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/chromium', '/usr/bin/chromium-browser', '/usr/bin/microsoft-edge', '/snap/bin/chromium');
|
|
83
|
+
}
|
|
84
|
+
for (const p of cands) { try { if (p && fs.existsSync(p)) return p; } catch (_) {} }
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Visit a URL. Uses a headless browser (system Chrome/Edge via puppeteer-core) for
|
|
89
|
+
// real JS rendering + console errors — otherwise falls back to a plain HTTP fetch.
|
|
90
|
+
async function browsePage(url, waitMs) {
|
|
91
|
+
let pup = null, isCore = false;
|
|
92
|
+
try { pup = require('puppeteer'); } catch (_) { try { pup = require('puppeteer-core'); isCore = true; } catch (_) {} }
|
|
93
|
+
const exe = isCore ? findBrowser() : null;
|
|
94
|
+
if (pup && (!isCore || exe)) {
|
|
95
|
+
let browser;
|
|
96
|
+
try {
|
|
97
|
+
const opts = { headless: 'new', args: ['--no-sandbox', '--disable-setuid-sandbox'] };
|
|
98
|
+
if (exe) opts.executablePath = exe;
|
|
99
|
+
browser = await pup.launch(opts);
|
|
100
|
+
const page = await browser.newPage();
|
|
101
|
+
const errors = [];
|
|
102
|
+
page.on('console', (m) => { if (m.type() === 'error') errors.push('console.error: ' + m.text()); });
|
|
103
|
+
page.on('pageerror', (e) => errors.push('pageerror: ' + e.message));
|
|
104
|
+
const resp = await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
|
|
105
|
+
if (waitMs) await new Promise((r) => setTimeout(r, Math.min(Math.max(0, waitMs), 8000)));
|
|
106
|
+
const title = await page.title();
|
|
107
|
+
const text = await page.evaluate(() => (document.body ? document.body.innerText : ''));
|
|
108
|
+
try { await browser.close(); } catch (_) {}
|
|
109
|
+
const status = resp ? resp.status() : '?';
|
|
110
|
+
return {
|
|
111
|
+
summary: 'rendered (HTTP ' + status + ')' + (errors.length ? ' · ' + errors.length + ' JS error(s)' : ' · no JS errors'),
|
|
112
|
+
text: 'URL: ' + url + '\nHTTP ' + status + ' · title: ' + title + '\n' +
|
|
113
|
+
(errors.length ? 'JAVASCRIPT ERRORS:\n' + errors.join('\n') + '\n\n' : '(no console errors)\n\n') +
|
|
114
|
+
'RENDERED PAGE TEXT:\n' + clip(text),
|
|
115
|
+
};
|
|
116
|
+
} catch (e) { try { if (browser) await browser.close(); } catch (_) {} /* fall back to fetch */ }
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const ctrl = new AbortController();
|
|
120
|
+
const t = setTimeout(() => ctrl.abort(), 20000);
|
|
121
|
+
const r = await fetch(url, { signal: ctrl.signal, headers: { 'user-agent': 'NovaPrime-CLI' } });
|
|
122
|
+
clearTimeout(t);
|
|
123
|
+
const ct = (r.headers.get('content-type') || '').split(';')[0];
|
|
124
|
+
const body = await r.text();
|
|
125
|
+
const tm = body.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
126
|
+
const title = tm ? tm[1].trim() : '';
|
|
127
|
+
const isHtml = /html/i.test(ct);
|
|
128
|
+
const text = isHtml
|
|
129
|
+
? body.replace(/<script[\s\S]*?<\/script>/gi, ' ').replace(/<style[\s\S]*?<\/style>/gi, ' ').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim()
|
|
130
|
+
: body;
|
|
131
|
+
return {
|
|
132
|
+
summary: 'fetched (HTTP ' + r.status + ', ' + ct + ')',
|
|
133
|
+
text: 'URL: ' + url + '\nHTTP ' + r.status + ' · ' + ct + (title ? ' · title: ' + title : '') + '\n\n' +
|
|
134
|
+
(isHtml ? 'NOTE: plain HTTP fetch (JavaScript NOT rendered — a React/Vite app shows little here). For full rendered output + console errors, install puppeteer in the CLI.\n\nVISIBLE TEXT:\n' : 'BODY:\n') + clip(text),
|
|
135
|
+
};
|
|
136
|
+
} catch (e) {
|
|
137
|
+
return { summary: 'failed: ' + e.message, text: 'ERROR browsing ' + url + ': ' + e.message };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
65
141
|
// Permission gate. Returns true if allowed. In auto-accept mode it never asks.
|
|
66
142
|
// Answering "a" (always) flips auto-accept ON for the rest of the session.
|
|
67
143
|
async function allow(label) {
|
|
@@ -129,6 +205,27 @@ const definitions = [
|
|
|
129
205
|
required: ['command'],
|
|
130
206
|
},
|
|
131
207
|
},
|
|
208
|
+
{
|
|
209
|
+
name: 'browse',
|
|
210
|
+
description: 'Visit a web URL (including http://localhost dev servers) and read it. Returns the HTTP status, page title, the RENDERED visible text, and any JavaScript console/page errors. Use this to open a link the user shares, and especially to VERIFY a website/app you built actually works — check it renders and find runtime errors — then fix the issues you find.',
|
|
211
|
+
input_schema: {
|
|
212
|
+
type: 'object',
|
|
213
|
+
properties: {
|
|
214
|
+
url: { type: 'string', description: 'Full URL, e.g. http://localhost:5173 or https://example.com' },
|
|
215
|
+
wait_ms: { type: 'number', description: 'Optional extra wait for the page to render (max 8000)' },
|
|
216
|
+
},
|
|
217
|
+
required: ['url'],
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: 'open_url',
|
|
222
|
+
description: 'Open a URL in the USER\'s real default browser so they can see it themselves (e.g. show them the dev server or a finished page).',
|
|
223
|
+
input_schema: {
|
|
224
|
+
type: 'object',
|
|
225
|
+
properties: { url: { type: 'string' } },
|
|
226
|
+
required: ['url'],
|
|
227
|
+
},
|
|
228
|
+
},
|
|
132
229
|
];
|
|
133
230
|
|
|
134
231
|
function clip(s) {
|
|
@@ -189,6 +286,30 @@ async function execute(name, input) {
|
|
|
189
286
|
const out = (r.stdout || '') + (r.stderr || '');
|
|
190
287
|
return clip(`exit_code=${r.status}\n${out}`.trim());
|
|
191
288
|
}
|
|
289
|
+
case 'browse': {
|
|
290
|
+
const url = String(input.url || '').trim();
|
|
291
|
+
if (!/^https?:\/\//i.test(url)) return 'ERROR: url must start with http:// or https://';
|
|
292
|
+
const isLocal = /^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(:|\/|$)/i.test(url);
|
|
293
|
+
console.log('');
|
|
294
|
+
console.log(c.indigo(' ╭─ permission · browse ') + c.dim('────────────────────'));
|
|
295
|
+
console.log(c.indigo(' │ ') + c.bold(url) + (isLocal ? c.dim(' (localhost)') : ''));
|
|
296
|
+
if (!isLocal) {
|
|
297
|
+
if (!(await allow(c.indigo(' ╰─ open this URL?')))) { console.log(c.dim(' · skipped')); return 'DENIED: user did not allow browsing this URL.'; }
|
|
298
|
+
} else { console.log(c.indigo(' ╰─ ') + c.dim('visiting…')); }
|
|
299
|
+
const res = await browsePage(url, input.wait_ms);
|
|
300
|
+
console.log(c.green(' ✓ ') + c.dim(res.summary));
|
|
301
|
+
return res.text;
|
|
302
|
+
}
|
|
303
|
+
case 'open_url': {
|
|
304
|
+
const url = String(input.url || '').trim();
|
|
305
|
+
if (!/^https?:\/\//i.test(url)) return 'ERROR: url must start with http:// or https://';
|
|
306
|
+
console.log(c.green(' ↗ ') + c.body('opening in your browser: ') + c.white(url));
|
|
307
|
+
try {
|
|
308
|
+
if (process.platform === 'win32') spawn('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore', windowsHide: true }).unref();
|
|
309
|
+
else spawn(process.platform === 'darwin' ? 'open' : 'xdg-open', [url], { detached: true, stdio: 'ignore' }).unref();
|
|
310
|
+
} catch (e) { return 'ERROR opening browser: ' + e.message; }
|
|
311
|
+
return 'OK: opened ' + url + ' in the default browser for the user to see.';
|
|
312
|
+
}
|
|
192
313
|
default:
|
|
193
314
|
return 'ERROR: unknown tool ' + name;
|
|
194
315
|
}
|