novaprime 1.5.1 → 1.6.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/bin/novaprime.js +3 -1
- package/package.json +1 -1
- package/src/agent.js +3 -3
- package/src/tools.js +60 -11
package/bin/novaprime.js
CHANGED
|
@@ -5,6 +5,7 @@ const ui = require('../src/ui');
|
|
|
5
5
|
const { c } = ui;
|
|
6
6
|
const { ask, close, boxInput } = require('../src/prompt');
|
|
7
7
|
const { runTurn, fetchMe } = require('../src/agent');
|
|
8
|
+
const tools = require('../src/tools');
|
|
8
9
|
const pkg = require('../package.json');
|
|
9
10
|
|
|
10
11
|
// Validate the key against the server before saving — a wrong key never logs in.
|
|
@@ -82,7 +83,8 @@ async function repl() {
|
|
|
82
83
|
let me = await fetchMe(config.getServer(), cfg.key);
|
|
83
84
|
console.clear(); // hide login/clutter — start clean with the header at the top
|
|
84
85
|
ui.banner(meToBanner(cfg, me));
|
|
85
|
-
process.on('
|
|
86
|
+
process.on('exit', () => { try { tools.stopBackground(); } catch (_) {} }); // stop bg dev servers on quit
|
|
87
|
+
process.on('SIGINT', () => { try { tools.stopBackground(); } catch (_) {} console.log(c.muted('\n bye')); process.exit(0); });
|
|
86
88
|
|
|
87
89
|
ui.hint(' Tip: press ' + c.indigo('Shift+Tab') + c.dim(' for auto-accept (no permission prompts), or answer ') + c.indigo('a') + c.dim(' at any prompt.'));
|
|
88
90
|
|
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -17,9 +17,9 @@ const SYSTEM_PROMPT =
|
|
|
17
17
|
`Use clear markdown: short paragraphs, bullet lists, and fenced code blocks with a language tag. ` +
|
|
18
18
|
`When asked to create or change files, call the tool RIGHT AWAY with at most a one-line intro — ` +
|
|
19
19
|
`do NOT write long explanations or feature lists before creating the file. Keep any summary to 1-2 short lines after. ` +
|
|
20
|
-
`
|
|
21
|
-
`
|
|
22
|
-
`
|
|
20
|
+
`You MAY start the dev server when the project is ready — run it with run_command (e.g. "cd <project> && npm run dev"). ` +
|
|
21
|
+
`Dev/watch servers are automatically run in the BACKGROUND and the tool returns the localhost URL, so they do NOT block. ` +
|
|
22
|
+
`After it starts, give the user the localhost URL to open in their browser, and do NOT start it a second time. ` +
|
|
23
23
|
`Be concise and warm. Current OS: ${os.platform()}. Working directory: ${process.cwd()}.`;
|
|
24
24
|
|
|
25
25
|
// Fetch read-only account info for the header (name, plan, usage). Never throws.
|
package/src/tools.js
CHANGED
|
@@ -1,13 +1,67 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
3
4
|
const path = require('path');
|
|
4
|
-
const { spawnSync } = require('child_process');
|
|
5
|
+
const { spawnSync, spawn } = require('child_process');
|
|
5
6
|
const { ask } = require('./prompt');
|
|
6
7
|
const { c, tool } = require('./ui');
|
|
7
8
|
const mode = require('./mode');
|
|
8
9
|
|
|
9
10
|
const MAX_OUTPUT = 20000; // cap tool output sent back to the model
|
|
10
11
|
|
|
12
|
+
// dev/watch servers: long-running, so we run them DETACHED in the background and
|
|
13
|
+
// hand back the localhost URL instead of blocking the session.
|
|
14
|
+
const DEV_SERVER_RE = /\b(npm|pnpm|yarn|bun)\s+(run\s+)?(dev|start|serve|watch)\b|\bvite\b(?!\s+build)|\bnext\s+dev\b|\bnodemon\b|webpack-dev-server|webpack\s+serve|\bnpx\s+serve\b|\bhttp-server\b|\blive-server\b|php\s+-S|python\s+-m\s+http\.server|flask\s+run|rails\s+s(erver)?\b|\bng\s+serve\b/i;
|
|
15
|
+
const URL_RE = /https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d+)?\/?\S*/i;
|
|
16
|
+
const bgServers = []; // { pid, cmd, child } — kept so they can be stopped on exit
|
|
17
|
+
|
|
18
|
+
// Start a long-running command (dev server) in the background. We DON'T await it —
|
|
19
|
+
// we stream its output to a log, grab the localhost URL, and return right away so the
|
|
20
|
+
// session never blocks. It keeps running as a child of the CLI for the whole session.
|
|
21
|
+
async function runBackground(cmd) {
|
|
22
|
+
const logPath = path.join(os.tmpdir(), 'novaprime-dev-' + Date.now() + '.log');
|
|
23
|
+
let log = '';
|
|
24
|
+
let child;
|
|
25
|
+
try {
|
|
26
|
+
child = spawn(cmd, { shell: true, windowsHide: true });
|
|
27
|
+
} catch (err) {
|
|
28
|
+
return 'ERROR starting dev server: ' + err.message;
|
|
29
|
+
}
|
|
30
|
+
bgServers.push({ pid: child.pid, cmd, child });
|
|
31
|
+
const onData = (d) => { const s = d.toString(); log += s; if (log.length > MAX_OUTPUT) log = log.slice(-MAX_OUTPUT); try { fs.appendFileSync(logPath, s); } catch (_) {} };
|
|
32
|
+
if (child.stdout) child.stdout.on('data', onData);
|
|
33
|
+
if (child.stderr) child.stderr.on('data', onData);
|
|
34
|
+
let earlyExit = null;
|
|
35
|
+
child.on('error', (e) => { log += '\n[spawn error] ' + e.message; });
|
|
36
|
+
child.on('exit', (code) => { earlyExit = code; });
|
|
37
|
+
|
|
38
|
+
const url = await new Promise((resolve) => {
|
|
39
|
+
const start = Date.now();
|
|
40
|
+
const t = setInterval(() => {
|
|
41
|
+
const m = log.match(URL_RE);
|
|
42
|
+
if (m) { clearInterval(t); resolve(m[0]); }
|
|
43
|
+
else if (earlyExit !== null) { clearInterval(t); resolve(null); } // it crashed on startup
|
|
44
|
+
else if (Date.now() - start >= 10000) { clearInterval(t); resolve(null); }
|
|
45
|
+
}, 250);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (earlyExit !== null && !url) {
|
|
49
|
+
console.log(c.amber(' ! dev server exited (code ' + earlyExit + ')'));
|
|
50
|
+
return 'The dev command "' + cmd + '" exited immediately (code ' + earlyExit + ') instead of staying up. It likely failed. Read the log and fix the problem, then start it again.\n\nOutput:\n' + clip(log);
|
|
51
|
+
}
|
|
52
|
+
console.log(c.green(' ✓ dev server running in background') + c.dim(' (pid ' + child.pid + ')'));
|
|
53
|
+
if (url) console.log(c.green(' → ') + c.white(url) + c.dim(' — open this in your browser'));
|
|
54
|
+
else console.log(c.dim(' · still starting… log: ' + logPath));
|
|
55
|
+
return 'OK: started "' + cmd + '" in the BACKGROUND (pid ' + child.pid + '), so the session is NOT blocked and the server keeps running. ' +
|
|
56
|
+
(url ? 'It is serving at ' + url + ' — tell the user to open that URL in their browser. ' : 'It is still booting; tell the user to watch for the localhost URL. ') +
|
|
57
|
+
'Do NOT start it again.\n\nStartup log:\n' + clip(log);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Stop any background dev servers (called when the CLI exits).
|
|
61
|
+
function stopBackground() {
|
|
62
|
+
for (const s of bgServers) { try { s.child.kill(); } catch (_) {} }
|
|
63
|
+
}
|
|
64
|
+
|
|
11
65
|
// Permission gate. Returns true if allowed. In auto-accept mode it never asks.
|
|
12
66
|
// Answering "a" (always) flips auto-accept ON for the rest of the session.
|
|
13
67
|
async function allow(label) {
|
|
@@ -125,18 +179,13 @@ async function execute(name, input) {
|
|
|
125
179
|
}
|
|
126
180
|
case 'run_command': {
|
|
127
181
|
const cmd = String(input.command || '');
|
|
128
|
-
|
|
129
|
-
if (/\b(npm|pnpm|yarn|bun)\s+(run\s+)?(dev|start|serve|watch)\b|\bvite\b(?!\s+build)|\bnext\s+dev\b|\bnodemon\b|webpack-dev-server|webpack\s+serve|\bnpx\s+serve\b|\bhttp-server\b|\blive-server\b|php\s+-S|python\s+-m\s+http\.server|flask\s+run|rails\s+s(erver)?\b|\bng\s+serve\b/i.test(cmd)) {
|
|
130
|
-
console.log('');
|
|
131
|
-
console.log(c.amber(' ! ') + c.amber('skipped dev server: ') + c.white(cmd));
|
|
132
|
-
console.log(c.dim(' (long-running — start it yourself in another terminal when ready)'));
|
|
133
|
-
return 'SKIPPED: "' + cmd + '" is a long-running dev/watch server and was NOT run (it would block the session forever). Do NOT start dev servers. Finish the work, then tell the user how to run it themselves in a separate terminal, e.g. `cd <project>` then `npm run dev`, and to open the localhost URL it prints.';
|
|
134
|
-
}
|
|
182
|
+
const isDevServer = DEV_SERVER_RE.test(cmd);
|
|
135
183
|
console.log('');
|
|
136
184
|
console.log(c.red(' ╭─ permission · run command ') + c.dim('───────────────────'));
|
|
137
|
-
console.log(c.red(' │ ') + c.bold(
|
|
185
|
+
console.log(c.red(' │ ') + c.bold(cmd) + (isDevServer ? c.dim(' (runs in background)') : ''));
|
|
138
186
|
if (!(await allow(c.red(' ╰─ run this command?')))) { console.log(c.dim(' · skipped')); return 'DENIED: user did not allow running the command.'; }
|
|
139
|
-
|
|
187
|
+
if (isDevServer) return await runBackground(cmd); // long-running: detach so it doesn't block
|
|
188
|
+
const r = spawnSync(cmd, { shell: true, encoding: 'utf8', timeout: 1000 * 120 });
|
|
140
189
|
const out = (r.stdout || '') + (r.stderr || '');
|
|
141
190
|
return clip(`exit_code=${r.status}\n${out}`.trim());
|
|
142
191
|
}
|
|
@@ -148,4 +197,4 @@ async function execute(name, input) {
|
|
|
148
197
|
}
|
|
149
198
|
}
|
|
150
199
|
|
|
151
|
-
module.exports = { definitions, execute };
|
|
200
|
+
module.exports = { definitions, execute, runBackground, stopBackground };
|