novaprime 1.3.5 → 1.4.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "novaprime",
3
- "version": "1.3.5",
3
+ "version": "1.4.1",
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
@@ -2,7 +2,7 @@
2
2
  const os = require('os');
3
3
  const ora = require('ora');
4
4
  const tools = require('./tools');
5
- const { c, aiLabel, error, warn } = require('./ui');
5
+ const { c, aiLabel, routeReport, error, warn } = require('./ui');
6
6
  const { Renderer } = require('./render');
7
7
 
8
8
  const SYSTEM_PROMPT =
@@ -50,6 +50,15 @@ async function streamMessage(server, key, messages) {
50
50
  return { error: msg };
51
51
  }
52
52
 
53
+ // live model-switch report: show what the free Flash brain decided (only on a fresh prompt)
54
+ try {
55
+ const hdr = res.headers.get('x-novaprime-route');
56
+ if (hdr) {
57
+ const report = JSON.parse(decodeURIComponent(hdr));
58
+ if (report && report.fresh) { stopSpin(); routeReport(report); ensureSpin(c.muted('thinking')); }
59
+ }
60
+ } catch (_) {}
61
+
53
62
  const blocks = [];
54
63
  let stopReason = null, labelShown = false, buffer = '', usedModel = null;
55
64
  const renderer = new Renderer();
package/src/prompt.js CHANGED
@@ -1,7 +1,8 @@
1
1
  'use strict';
2
2
  const readline = require('readline');
3
3
 
4
- // Use Node's readline it correctly handles long lines, wrapping, editing and paste.
4
+ // Use Node's readline for single-line prompts (login key, y/N) it handles
5
+ // editing and history. The multi-line chat box uses a paste-aware reader below.
5
6
  let rl = null;
6
7
  function getRl() {
7
8
  if (!rl) {
@@ -22,19 +23,131 @@ function confirm(message) {
22
23
  return ask(message + ' (y/N) ').then((a) => /^y(es)?$/i.test((a || '').trim()));
23
24
  }
24
25
 
25
- // Chat input: top border above (safe), readline handles the input line,
26
- // and the bottom border is drawn AFTER submit so the message shows as a complete box.
26
+ function close() { if (rl) { rl.close(); rl = null; } }
27
+
28
+ // ---- bracketed-paste-aware chat input ----------------------------------
29
+ // Terminals that support bracketed paste wrap pasted text in these markers.
30
+ // We capture everything between them (newlines included) WITHOUT submitting,
31
+ // so pasting a big multi-line prompt no longer auto-sends on the first line.
32
+ const PASTE_START = '\x1b[200~';
33
+ const PASTE_END = '\x1b[201~';
34
+
35
+ function normalize(t) { return t.replace(/\r\n?/g, '\n'); }
36
+
37
+ // length of the longest suffix of s that is a prefix of marker (handles a
38
+ // marker that got split across two stdin chunks)
39
+ function partialTail(s, marker) {
40
+ const max = Math.min(s.length, marker.length - 1);
41
+ for (let n = max; n > 0; n--) {
42
+ if (marker.startsWith(s.slice(s.length - n))) return n;
43
+ }
44
+ return 0;
45
+ }
46
+
47
+ // Handle a run of plain (non-paste) bytes: typing, Enter, backspace, Ctrl+C/D,
48
+ // and swallow escape sequences (arrow keys, etc.) so they don't corrupt input.
49
+ function handlePlain(state, s, sink) {
50
+ for (let i = 0; i < s.length && !state.finished; i++) {
51
+ const ch = s[i];
52
+ if (ch === '\x03') { sink.cancel(); return; } // Ctrl+C
53
+ if (ch === '\x04') { if (!state.buf) { sink.cancel(); return; } continue; } // Ctrl+D on empty
54
+ if (ch === '\r' || ch === '\n') { sink.submit(state.buf); return; } // Enter -> send
55
+ if (ch === '\x7f' || ch === '\b') { // backspace
56
+ if (state.buf.length) {
57
+ const last = state.buf[state.buf.length - 1];
58
+ state.buf = state.buf.slice(0, -1);
59
+ if (last !== '\n') sink.out('\b \b');
60
+ }
61
+ continue;
62
+ }
63
+ if (ch === '\x1b') { // skip CSI/SS3 escape (arrows, fn keys)
64
+ if (s[i + 1] === '[' || s[i + 1] === 'O') {
65
+ i += 2;
66
+ while (i < s.length && !/[A-Za-z~]/.test(s[i])) i++;
67
+ }
68
+ continue;
69
+ }
70
+ if (ch === '\t') { state.buf += ' '; sink.out(' '); continue; } // tab -> 2 spaces
71
+ if (ch >= ' ') { state.buf += ch; sink.out(ch); } // printable
72
+ }
73
+ }
74
+
75
+ // Pure state machine — testable. state = { buf, pasting, pending, finished }.
76
+ // sink = { out(str), submit(value), cancel() }.
77
+ function processChunk(state, chunk, sink) {
78
+ let s = state.pending + chunk;
79
+ state.pending = '';
80
+ while (s.length && !state.finished) {
81
+ if (state.pasting) {
82
+ const end = s.indexOf(PASTE_END);
83
+ if (end === -1) {
84
+ const hold = partialTail(s, PASTE_END);
85
+ const seg = normalize(s.slice(0, s.length - hold));
86
+ if (seg) { state.buf += seg; sink.out(seg); }
87
+ state.pending = s.slice(s.length - hold);
88
+ return;
89
+ }
90
+ const seg = normalize(s.slice(0, end));
91
+ if (seg) { state.buf += seg; sink.out(seg); }
92
+ state.pasting = false;
93
+ s = s.slice(end + PASTE_END.length);
94
+ continue;
95
+ }
96
+ const start = s.indexOf(PASTE_START);
97
+ if (start === -1) {
98
+ const hold = partialTail(s, PASTE_START);
99
+ const plain = s.slice(0, s.length - hold);
100
+ handlePlain(state, plain, sink);
101
+ if (!state.finished) state.pending = s.slice(s.length - hold);
102
+ return;
103
+ }
104
+ handlePlain(state, s.slice(0, start), sink);
105
+ if (state.finished) return;
106
+ state.pasting = true;
107
+ s = s.slice(start + PASTE_START.length);
108
+ }
109
+ }
110
+
111
+ function ttyRaw() { return process.stdin.isTTY && typeof process.stdin.setRawMode === 'function'; }
112
+
113
+ // Chat input box. Top border above; readline-free raw reader captures multi-line
114
+ // pastes; bottom border drawn after submit so the message shows as a complete box.
27
115
  function boxInput(top, bottom, prompt) {
116
+ // Fallback for piped / non-TTY input (e.g. `echo ... | novaprime`): plain readline.
117
+ if (!ttyRaw()) {
118
+ return new Promise((resolve) => {
119
+ process.stdout.write(top + '\n');
120
+ const r = getRl();
121
+ r.question(prompt, (answer) => { process.stdout.write(bottom + '\n'); resolve(answer); });
122
+ });
123
+ }
124
+ if (rl) { rl.close(); rl = null; } // release stdin from readline
125
+
28
126
  return new Promise((resolve) => {
127
+ const stdin = process.stdin;
128
+ const state = { buf: '', pasting: false, pending: '', finished: false };
129
+
130
+ const onData = (chunk) => processChunk(state, chunk, sink);
131
+ const cleanup = () => {
132
+ try { process.stdout.write('\x1b[?2004l'); } catch (_) {}
133
+ try { stdin.setRawMode(false); } catch (_) {}
134
+ stdin.pause();
135
+ stdin.removeListener('data', onData);
136
+ };
137
+ const sink = {
138
+ out: (str) => process.stdout.write(str),
139
+ submit: (val) => { state.finished = true; cleanup(); process.stdout.write('\n' + bottom + '\n'); resolve(val); },
140
+ cancel: () => { state.finished = true; cleanup(); process.stdout.write('\n' + bottom + '\n'); resolve(null); },
141
+ };
142
+
29
143
  process.stdout.write(top + '\n');
30
- const r = getRl();
31
- r.question(prompt, (answer) => {
32
- process.stdout.write(bottom + '\n');
33
- resolve(answer);
34
- });
144
+ process.stdout.write(prompt);
145
+ stdin.setRawMode(true);
146
+ stdin.resume();
147
+ stdin.setEncoding('utf8');
148
+ process.stdout.write('\x1b[?2004h'); // enable bracketed paste
149
+ stdin.on('data', onData);
35
150
  });
36
151
  }
37
152
 
38
- function close() { if (rl) { rl.close(); rl = null; } }
39
-
40
- module.exports = { ask, confirm, boxInput, close };
153
+ module.exports = { ask, confirm, boxInput, close, processChunk };
package/src/ui.js CHANGED
@@ -100,6 +100,25 @@ function inputBoxClose() { process.stdout.write('\n'); }
100
100
 
101
101
  function aiLabel(model) { process.stdout.write('\n' + c.violet('● ') + c.violet.bold('Nova Prime') + (model ? c.dim(' · ' + model) : '') + '\n'); }
102
102
 
103
+ // Live "model switch" report — shows what the free Flash brain decided for this request.
104
+ function routeReport(r) {
105
+ if (!r) return;
106
+ const sep = c.dim(' → ');
107
+ const tag = c.violet('⚡ ') + c.muted('Flash analyzed');
108
+ let mid, model;
109
+ if (r.decision === 'build') {
110
+ mid = c.amber('new / heavy build');
111
+ model = c.white.bold('switched to ' + (r.chosenLabel || r.chosenModel));
112
+ } else if (r.escalated) {
113
+ mid = c.amber(r.reason || 'escalated');
114
+ model = c.white.bold('using ' + (r.chosenLabel || r.chosenModel));
115
+ } else {
116
+ mid = c.green('quick task');
117
+ model = c.body(r.chosenLabel || r.chosenModel) + (r.free ? c.dim(' · free') : '');
118
+ }
119
+ console.log('\n' + tag + sep + mid + sep + model);
120
+ }
121
+
103
122
  function info(msg) { console.log(c.muted(msg)); }
104
123
  function hint(msg) { console.log(c.dim(msg)); }
105
124
  function warn(msg) { console.log(c.amber(' ! ') + c.amber(msg)); }
@@ -107,4 +126,4 @@ function error(msg) { console.log(c.red(' ✕ ') + c.red(msg)); }
107
126
  function ok(msg) { console.log(c.green(' ✓ ') + c.body(msg)); }
108
127
  function tool(name, detail) { console.log(c.dim(' · ' + name + (detail ? ' ' + detail : ''))); }
109
128
 
110
- module.exports = { chalk, boxen, c, banner, maskKey, aiLabel, inputTop, inputBottom, inputPrompt, inputBoxOpen, inputBoxClose, info, hint, warn, error, ok, tool };
129
+ module.exports = { chalk, boxen, c, banner, maskKey, aiLabel, routeReport, inputTop, inputBottom, inputPrompt, inputBoxOpen, inputBoxClose, info, hint, warn, error, ok, tool };