novaprime 1.4.1 → 1.5.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 +7 -1
- package/package.json +1 -1
- package/src/agent.js +16 -8
- package/src/mode.js +12 -0
- package/src/prompt.js +13 -2
- package/src/tools.js +19 -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
|
@@ -34,14 +34,22 @@ async function streamMessage(server, key, messages) {
|
|
|
34
34
|
const stopSpin = () => { if (spinning) { spinner.stop(); spinning = false; } };
|
|
35
35
|
const ensureSpin = (text) => { if (!spinning) { spinner.start(); spinning = true; } spinner.text = text; };
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
37
|
+
// Retry transient connection drops (server restart window, brief network blip)
|
|
38
|
+
// so a 1–2s hiccup doesn't abort a long agentic build mid-turn.
|
|
39
|
+
const payload = JSON.stringify({ max_tokens: 16000, system: SYSTEM_PROMPT, tools: tools.definitions, messages, stream: true });
|
|
40
|
+
const url = server.replace(/\/$/, '') + '/v1/messages';
|
|
41
|
+
let res, lastErr = null;
|
|
42
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
43
|
+
try {
|
|
44
|
+
res = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json', 'x-novaprime-key': key }, body: payload });
|
|
45
|
+
lastErr = null;
|
|
46
|
+
break;
|
|
47
|
+
} catch (err) {
|
|
48
|
+
lastErr = err;
|
|
49
|
+
if (attempt < 3) { ensureSpin(c.muted('connection dropped — reconnecting… (' + (attempt + 1) + '/3)')); await new Promise((r) => setTimeout(r, 1000 * (attempt + 1))); }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (lastErr) { stopSpin(); return { error: 'Could not reach NovaPrime: ' + lastErr.message + ' (retried 3×)' }; }
|
|
45
53
|
|
|
46
54
|
if (!res.ok) {
|
|
47
55
|
stopSpin();
|
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,7 +118,7 @@ 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;
|
|
@@ -112,7 +127,7 @@ async function execute(name, input) {
|
|
|
112
127
|
console.log('');
|
|
113
128
|
console.log(c.red(' ╭─ permission · run command ') + c.dim('───────────────────'));
|
|
114
129
|
console.log(c.red(' │ ') + c.bold(input.command));
|
|
115
|
-
if (!(await
|
|
130
|
+
if (!(await allow(c.red(' ╰─ run this command?')))) { console.log(c.dim(' · skipped')); return 'DENIED: user did not allow running the command.'; }
|
|
116
131
|
const r = spawnSync(input.command, { shell: true, encoding: 'utf8', timeout: 1000 * 120 });
|
|
117
132
|
const out = (r.stdout || '') + (r.stderr || '');
|
|
118
133
|
return clip(`exit_code=${r.status}\n${out}`.trim());
|