novaprime 1.7.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "novaprime",
3
- "version": "1.7.1",
3
+ "version": "1.8.0",
4
4
  "description": "NovaPrime — an AI coding assistant in your terminal, powered by GLM.",
5
5
  "bin": {
6
6
  "novaprime": "bin/novaprime.js"
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,56 @@ function stopBackground() {
62
62
  for (const s of bgServers) { try { s.child.kill(); } catch (_) {} }
63
63
  }
64
64
 
65
+ // Visit a URL. Uses a headless browser (puppeteer) if installed — for real JS
66
+ // rendering + console errors — otherwise falls back to a plain HTTP fetch.
67
+ async function browsePage(url, waitMs) {
68
+ let pup = null;
69
+ try { pup = require('puppeteer'); } catch (_) { try { pup = require('puppeteer-core'); } catch (_) {} }
70
+ if (pup) {
71
+ let browser;
72
+ try {
73
+ browser = await pup.launch({ headless: 'new', args: ['--no-sandbox', '--disable-setuid-sandbox'] });
74
+ const page = await browser.newPage();
75
+ const errors = [];
76
+ page.on('console', (m) => { if (m.type() === 'error') errors.push('console.error: ' + m.text()); });
77
+ page.on('pageerror', (e) => errors.push('pageerror: ' + e.message));
78
+ const resp = await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
79
+ if (waitMs) await new Promise((r) => setTimeout(r, Math.min(Math.max(0, waitMs), 8000)));
80
+ const title = await page.title();
81
+ const text = await page.evaluate(() => (document.body ? document.body.innerText : ''));
82
+ try { await browser.close(); } catch (_) {}
83
+ const status = resp ? resp.status() : '?';
84
+ return {
85
+ summary: 'rendered (HTTP ' + status + ')' + (errors.length ? ' · ' + errors.length + ' JS error(s)' : ' · no JS errors'),
86
+ text: 'URL: ' + url + '\nHTTP ' + status + ' · title: ' + title + '\n' +
87
+ (errors.length ? 'JAVASCRIPT ERRORS:\n' + errors.join('\n') + '\n\n' : '(no console errors)\n\n') +
88
+ 'RENDERED PAGE TEXT:\n' + clip(text),
89
+ };
90
+ } catch (e) { try { if (browser) await browser.close(); } catch (_) {} /* fall back to fetch */ }
91
+ }
92
+ try {
93
+ const ctrl = new AbortController();
94
+ const t = setTimeout(() => ctrl.abort(), 20000);
95
+ const r = await fetch(url, { signal: ctrl.signal, headers: { 'user-agent': 'NovaPrime-CLI' } });
96
+ clearTimeout(t);
97
+ const ct = (r.headers.get('content-type') || '').split(';')[0];
98
+ const body = await r.text();
99
+ const tm = body.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
100
+ const title = tm ? tm[1].trim() : '';
101
+ const isHtml = /html/i.test(ct);
102
+ const text = isHtml
103
+ ? body.replace(/<script[\s\S]*?<\/script>/gi, ' ').replace(/<style[\s\S]*?<\/style>/gi, ' ').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim()
104
+ : body;
105
+ return {
106
+ summary: 'fetched (HTTP ' + r.status + ', ' + ct + ')',
107
+ text: 'URL: ' + url + '\nHTTP ' + r.status + ' · ' + ct + (title ? ' · title: ' + title : '') + '\n\n' +
108
+ (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),
109
+ };
110
+ } catch (e) {
111
+ return { summary: 'failed: ' + e.message, text: 'ERROR browsing ' + url + ': ' + e.message };
112
+ }
113
+ }
114
+
65
115
  // Permission gate. Returns true if allowed. In auto-accept mode it never asks.
66
116
  // Answering "a" (always) flips auto-accept ON for the rest of the session.
67
117
  async function allow(label) {
@@ -129,6 +179,27 @@ const definitions = [
129
179
  required: ['command'],
130
180
  },
131
181
  },
182
+ {
183
+ name: 'browse',
184
+ 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.',
185
+ input_schema: {
186
+ type: 'object',
187
+ properties: {
188
+ url: { type: 'string', description: 'Full URL, e.g. http://localhost:5173 or https://example.com' },
189
+ wait_ms: { type: 'number', description: 'Optional extra wait for the page to render (max 8000)' },
190
+ },
191
+ required: ['url'],
192
+ },
193
+ },
194
+ {
195
+ name: 'open_url',
196
+ 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).',
197
+ input_schema: {
198
+ type: 'object',
199
+ properties: { url: { type: 'string' } },
200
+ required: ['url'],
201
+ },
202
+ },
132
203
  ];
133
204
 
134
205
  function clip(s) {
@@ -189,6 +260,30 @@ async function execute(name, input) {
189
260
  const out = (r.stdout || '') + (r.stderr || '');
190
261
  return clip(`exit_code=${r.status}\n${out}`.trim());
191
262
  }
263
+ case 'browse': {
264
+ const url = String(input.url || '').trim();
265
+ if (!/^https?:\/\//i.test(url)) return 'ERROR: url must start with http:// or https://';
266
+ const isLocal = /^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(:|\/|$)/i.test(url);
267
+ console.log('');
268
+ console.log(c.indigo(' ╭─ permission · browse ') + c.dim('────────────────────'));
269
+ console.log(c.indigo(' │ ') + c.bold(url) + (isLocal ? c.dim(' (localhost)') : ''));
270
+ if (!isLocal) {
271
+ if (!(await allow(c.indigo(' ╰─ open this URL?')))) { console.log(c.dim(' · skipped')); return 'DENIED: user did not allow browsing this URL.'; }
272
+ } else { console.log(c.indigo(' ╰─ ') + c.dim('visiting…')); }
273
+ const res = await browsePage(url, input.wait_ms);
274
+ console.log(c.green(' ✓ ') + c.dim(res.summary));
275
+ return res.text;
276
+ }
277
+ case 'open_url': {
278
+ const url = String(input.url || '').trim();
279
+ if (!/^https?:\/\//i.test(url)) return 'ERROR: url must start with http:// or https://';
280
+ console.log(c.green(' ↗ ') + c.body('opening in your browser: ') + c.white(url));
281
+ try {
282
+ if (process.platform === 'win32') spawn('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore', windowsHide: true }).unref();
283
+ else spawn(process.platform === 'darwin' ? 'open' : 'xdg-open', [url], { detached: true, stdio: 'ignore' }).unref();
284
+ } catch (e) { return 'ERROR opening browser: ' + e.message; }
285
+ return 'OK: opened ' + url + ' in the default browser for the user to see.';
286
+ }
192
287
  default:
193
288
  return 'ERROR: unknown tool ' + name;
194
289
  }