novaprime 1.4.2 → 1.5.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/bin/novaprime.js +7 -1
- package/package.json +1 -1
- package/src/agent.js +3 -0
- package/src/mode.js +12 -0
- package/src/prompt.js +13 -2
- package/src/tools.js +27 -4
package/bin/novaprime.js
CHANGED
|
@@ -58,7 +58,11 @@ function printHelp() {
|
|
|
58
58
|
${c.indigo('/clear')} clear the screen
|
|
59
59
|
${c.indigo('/exit')} quit ${c.dim('(Ctrl+C also quits)')}
|
|
60
60
|
|
|
61
|
-
${c.
|
|
61
|
+
${c.white.bold('Permissions')}
|
|
62
|
+
${c.indigo('Shift+Tab')} toggle ${c.green('auto-accept')} ${c.dim('(no more permission prompts this session)')}
|
|
63
|
+
${c.indigo('a')} at any prompt = "always" ${c.dim('(approve this and everything after)')}
|
|
64
|
+
|
|
65
|
+
${c.muted('Type a task and press Enter. By default NovaPrime asks before writing files or running commands.')}
|
|
62
66
|
`);
|
|
63
67
|
}
|
|
64
68
|
|
|
@@ -80,6 +84,8 @@ async function repl() {
|
|
|
80
84
|
ui.banner(meToBanner(cfg, me));
|
|
81
85
|
process.on('SIGINT', () => { console.log(c.muted('\n bye')); process.exit(0); });
|
|
82
86
|
|
|
87
|
+
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
|
+
|
|
83
89
|
// push the first input box toward the lower part of the screen, leaving a bottom margin
|
|
84
90
|
const rows = process.stdout.rows || 24;
|
|
85
91
|
process.stdout.write('\n'.repeat(Math.max(0, rows - 26)));
|
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -17,6 +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
|
+
`NEVER start dev servers or long-running/blocking processes (e.g. "npm run dev", "npm start", "vite", "next dev", ` +
|
|
21
|
+
`"nodemon", watchers) — they block the session forever. After creating the files, finish, then tell the user the ` +
|
|
22
|
+
`commands to run it themselves (e.g. cd into the folder and run "npm run dev", then open the localhost URL). ` +
|
|
20
23
|
`Be concise and warm. Current OS: ${os.platform()}. Working directory: ${process.cwd()}.`;
|
|
21
24
|
|
|
22
25
|
// Fetch read-only account info for the header (name, plan, usage). Never throws.
|
package/src/mode.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Session-wide permission mode. When auto-accept is ON, NovaPrime runs commands
|
|
4
|
+
// and writes/edits files without asking each time. Toggle with Shift+Tab, or by
|
|
5
|
+
// answering "a" (always) at any permission prompt.
|
|
6
|
+
let auto = false;
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
isAuto: () => auto,
|
|
10
|
+
setAuto: (v) => { auto = !!v; },
|
|
11
|
+
toggle: () => { auto = !auto; return auto; },
|
|
12
|
+
};
|
package/src/prompt.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
const readline = require('readline');
|
|
3
|
+
const { c } = require('./ui');
|
|
4
|
+
const mode = require('./mode');
|
|
3
5
|
|
|
4
6
|
// Use Node's readline for single-line prompts (login key, y/N) — it handles
|
|
5
7
|
// editing and history. The multi-line chat box uses a paste-aware reader below.
|
|
@@ -60,8 +62,9 @@ function handlePlain(state, s, sink) {
|
|
|
60
62
|
}
|
|
61
63
|
continue;
|
|
62
64
|
}
|
|
63
|
-
if (ch === '\x1b') {
|
|
64
|
-
if (s
|
|
65
|
+
if (ch === '\x1b') {
|
|
66
|
+
if (s.slice(i, i + 3) === '\x1b[Z') { if (sink.toggleMode) sink.toggleMode(); i += 2; continue; } // Shift+Tab
|
|
67
|
+
if (s[i + 1] === '[' || s[i + 1] === 'O') { // skip other CSI/SS3 escapes (arrows, fn keys)
|
|
65
68
|
i += 2;
|
|
66
69
|
while (i < s.length && !/[A-Za-z~]/.test(s[i])) i++;
|
|
67
70
|
}
|
|
@@ -138,6 +141,14 @@ function boxInput(top, bottom, prompt) {
|
|
|
138
141
|
out: (str) => process.stdout.write(str),
|
|
139
142
|
submit: (val) => { state.finished = true; cleanup(); process.stdout.write('\n' + bottom + '\n'); resolve(val); },
|
|
140
143
|
cancel: () => { state.finished = true; cleanup(); process.stdout.write('\n' + bottom + '\n'); resolve(null); },
|
|
144
|
+
toggleMode: () => {
|
|
145
|
+
const on = mode.toggle();
|
|
146
|
+
process.stdout.write('\r\x1b[2K'); // clear current input line
|
|
147
|
+
process.stdout.write((on
|
|
148
|
+
? c.green(' ⚡ auto-accept: ON') + c.dim(' — running everything without asking (Shift+Tab to undo)')
|
|
149
|
+
: c.dim(' ⚡ auto-accept: OFF — will ask before changes')) + '\n');
|
|
150
|
+
process.stdout.write(prompt + state.buf); // redraw input line with whatever was typed
|
|
151
|
+
},
|
|
141
152
|
};
|
|
142
153
|
|
|
143
154
|
process.stdout.write(top + '\n');
|
package/src/tools.js
CHANGED
|
@@ -2,11 +2,26 @@
|
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const { spawnSync } = require('child_process');
|
|
5
|
-
const {
|
|
5
|
+
const { ask } = require('./prompt');
|
|
6
6
|
const { c, tool } = require('./ui');
|
|
7
|
+
const mode = require('./mode');
|
|
7
8
|
|
|
8
9
|
const MAX_OUTPUT = 20000; // cap tool output sent back to the model
|
|
9
10
|
|
|
11
|
+
// Permission gate. Returns true if allowed. In auto-accept mode it never asks.
|
|
12
|
+
// Answering "a" (always) flips auto-accept ON for the rest of the session.
|
|
13
|
+
async function allow(label) {
|
|
14
|
+
if (mode.isAuto()) { console.log(c.dim(' · auto-accepted')); return true; }
|
|
15
|
+
const raw = (await ask(label + c.dim(' (y/N, a=always) '))) || '';
|
|
16
|
+
const a = raw.trim().toLowerCase();
|
|
17
|
+
if (a === 'a' || a === 'always') {
|
|
18
|
+
mode.setAuto(true);
|
|
19
|
+
console.log(c.green(' ✓ auto-accept ON for this session') + c.dim(' (Shift+Tab to turn off)'));
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
return a === 'y' || a === 'yes';
|
|
23
|
+
}
|
|
24
|
+
|
|
10
25
|
// ---- Tool definitions (Anthropic tool-use format) ----
|
|
11
26
|
const definitions = [
|
|
12
27
|
{
|
|
@@ -87,7 +102,7 @@ async function execute(name, input) {
|
|
|
87
102
|
console.log('');
|
|
88
103
|
console.log(c.amber(' ╭─ permission ') + c.dim('─────────────────────────────'));
|
|
89
104
|
console.log(c.amber(' │ ') + c.bold('Write file') + ' ' + c.white(input.path) + c.muted(` (${(input.content || '').length} chars)`));
|
|
90
|
-
if (!(await
|
|
105
|
+
if (!(await allow(c.amber(' ╰─ allow?')))) { console.log(c.dim(' · skipped')); return 'DENIED: user did not allow writing the file.'; }
|
|
91
106
|
fs.mkdirSync(path.dirname(path.resolve(input.path)), { recursive: true });
|
|
92
107
|
fs.writeFileSync(input.path, input.content);
|
|
93
108
|
console.log(c.green(' ✓ wrote ') + c.white(input.path));
|
|
@@ -103,16 +118,24 @@ async function execute(name, input) {
|
|
|
103
118
|
console.log(c.amber(' │ ') + c.bold('Edit file') + ' ' + c.white(input.path));
|
|
104
119
|
console.log(c.red(' │ - ' + input.old_string.split('\n').join('\n │ - ')));
|
|
105
120
|
console.log(c.green(' │ + ' + input.new_string.split('\n').join('\n │ + ')));
|
|
106
|
-
if (!(await
|
|
121
|
+
if (!(await allow(c.amber(' ╰─ allow?')))) { console.log(c.dim(' · skipped')); return 'DENIED: user did not allow the edit.'; }
|
|
107
122
|
fs.writeFileSync(input.path, before.replace(input.old_string, input.new_string));
|
|
108
123
|
console.log(c.green(' ✓ edited ') + c.white(input.path));
|
|
109
124
|
return 'OK: edited ' + input.path;
|
|
110
125
|
}
|
|
111
126
|
case 'run_command': {
|
|
127
|
+
const cmd = String(input.command || '');
|
|
128
|
+
// Refuse blocking dev servers / watchers — they never exit and freeze the session.
|
|
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
|
+
}
|
|
112
135
|
console.log('');
|
|
113
136
|
console.log(c.red(' ╭─ permission · run command ') + c.dim('───────────────────'));
|
|
114
137
|
console.log(c.red(' │ ') + c.bold(input.command));
|
|
115
|
-
if (!(await
|
|
138
|
+
if (!(await allow(c.red(' ╰─ run this command?')))) { console.log(c.dim(' · skipped')); return 'DENIED: user did not allow running the command.'; }
|
|
116
139
|
const r = spawnSync(input.command, { shell: true, encoding: 'utf8', timeout: 1000 * 120 });
|
|
117
140
|
const out = (r.stdout || '') + (r.stderr || '');
|
|
118
141
|
return clip(`exit_code=${r.status}\n${out}`.trim());
|