novaprime 1.0.0 → 1.2.5

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 CHANGED
@@ -1,95 +1,129 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
- const prompts = require('prompts');
4
3
  const config = require('../src/config');
5
4
  const ui = require('../src/ui');
6
5
  const { c } = ui;
7
- const { runTurn } = require('../src/agent');
8
-
6
+ const { ask, close, get } = require('../src/prompt');
7
+ const { runTurn, fetchMe } = require('../src/agent');
9
8
  const pkg = require('../package.json');
10
9
 
10
+ // Validate the key against the server before saving — a wrong key never logs in.
11
11
  async function doLogin() {
12
- console.log(c.brand.bold('\nNovaPrime login'));
13
- ui.info('Paste the novaprime-key your administrator gave you.');
14
- const ans = await prompts([
15
- { type: 'password', name: 'key', message: 'novaprime-key' },
16
- { type: 'text', name: 'server', message: 'Server URL', initial: config.getServer() },
17
- ]);
18
- if (!ans.key) { ui.error('No key entered. Login cancelled.'); process.exit(1); }
19
- const cfg = config.load();
20
- cfg.key = ans.key.trim();
21
- if (ans.server && ans.server.trim()) cfg.server = ans.server.trim();
22
- config.save(cfg);
23
- ui.ok('Saved to ' + config.FILE);
12
+ console.log(c.brand.bold('\n NovaPrime login'));
13
+ ui.info(' Paste the novaprime-key from your dashboard, then press Enter.');
14
+ while (true) {
15
+ const raw = await ask(' ' + c.indigoBold('key ') + c.dim(''));
16
+ if (raw === null || !raw.trim()) { ui.error('Login cancelled.'); return null; }
17
+ const key = raw.trim();
18
+ process.stdout.write(c.muted(' verifying key...'));
19
+ const me = await fetchMe(config.getServer(), key);
20
+ process.stdout.write('\r' + ' '.repeat(22) + '\r');
21
+ if (!me || !me.ok) { ui.error('That key is not valid. Try again, or press Enter on an empty line to cancel.'); continue; }
22
+ const cfg = config.load(); cfg.key = key; config.save(cfg);
23
+ ui.ok('Logged in as ' + (me.name || 'user') + (me.plan ? ' · ' + me.plan.name : ''));
24
+ return cfg;
25
+ }
24
26
  }
25
27
 
26
28
  async function ensureKey() {
27
29
  const cfg = config.load();
28
- if (cfg.key) return cfg;
29
- ui.warn('No novaprime-key found. Let\'s set it up.');
30
- await doLogin();
31
- return config.load();
30
+ if (cfg.key) {
31
+ const me = await fetchMe(config.getServer(), cfg.key);
32
+ if (me && me.ok) return { cfg, me };
33
+ ui.warn('Your saved key is not valid anymore. Please log in again.');
34
+ }
35
+ const logged = await doLogin();
36
+ if (!logged) { close(); process.exit(1); }
37
+ const me = await fetchMe(config.getServer(), logged.key);
38
+ return { cfg: logged, me };
39
+ }
40
+
41
+ function meToBanner(cfg, me) {
42
+ return {
43
+ folder: process.cwd(), key: cfg.key,
44
+ name: me && me.name, plan: me && me.plan ? me.plan.name : null,
45
+ model: me && me.plan ? ('auto-routing · up to ' + me.plan.max_model) : 'auto-routing · GLM',
46
+ windowUsed: me ? me.windowUsed : 0, windowLimit: me && me.plan ? me.plan.window_limit : null,
47
+ weeklyUsed: me ? me.weeklyUsed : 0, weeklyLimit: me && me.plan ? me.plan.weekly_limit : null,
48
+ daysLeft: me ? me.daysLeft : null, expired: me ? me.expired : false,
49
+ };
32
50
  }
33
51
 
34
52
  function printHelp() {
35
53
  console.log(`
36
- ${c.brand.bold('novaprime')} ${c.muted('— AI coding assistant in your terminal')}
37
-
38
- ${c.bold('Usage:')}
39
- novaprime start an interactive session
40
- novaprime "task..." run a single task and exit
41
- novaprime login save or change your novaprime-key
42
- novaprime logout remove your saved key
43
- novaprime --version show version
44
- novaprime --help show this help
54
+ ${c.white.bold('Commands')}
55
+ ${c.indigo('/help')} show this help
56
+ ${c.indigo('/key')} show your key (masked)
57
+ ${c.indigo('/usage')} show your plan limits and usage
58
+ ${c.indigo('/clear')} clear the screen
59
+ ${c.indigo('/exit')} quit ${c.dim('(Ctrl+C also quits)')}
45
60
 
46
- ${c.bold('In a session:')}
47
- /help show help /clear start a new conversation
48
- /exit quit (Ctrl+C also quits)
61
+ ${c.muted('Type a task and press Enter. NovaPrime asks before writing files or running commands.')}
49
62
  `);
50
63
  }
51
64
 
65
+ function showUsage(me) {
66
+ if (!me) { ui.warn('Could not load usage right now.'); return; }
67
+ if (!me.plan) { ui.warn('No active plan on your account.'); return; }
68
+ console.log('');
69
+ console.log(' ' + c.muted('plan ') + c.violet(me.plan.name) +
70
+ (me.daysLeft != null ? c.dim(' · ') + (me.expired ? c.red('expired') : c.muted(me.daysLeft + ' days left')) : ''));
71
+ console.log(' ' + c.muted('5-hour ') + c.white(`${me.windowUsed}/${me.plan.window_limit || '∞'}`));
72
+ console.log(' ' + c.muted('weekly ') + c.white(`${me.weeklyUsed}/${me.plan.weekly_limit || '∞'}`));
73
+ console.log('');
74
+ }
75
+
52
76
  async function repl() {
53
- const cfg = await ensureKey();
54
- ui.banner();
55
- let messages = [];
56
- // graceful Ctrl+C
57
- process.on('SIGINT', () => { console.log(c.muted('\nbye 👋')); process.exit(0); });
77
+ const { cfg } = await ensureKey();
78
+ let me = await fetchMe(config.getServer(), cfg.key);
79
+ console.clear(); // hide login/clutter — start clean with the header at the top
80
+ ui.banner(meToBanner(cfg, me));
81
+ get().on('SIGINT', () => { console.log(c.muted('\n bye')); close(); process.exit(0); });
82
+
83
+ // push the first input box toward the bottom of the screen (like Claude)
84
+ const rows = process.stdout.rows || 24;
85
+ process.stdout.write('\n'.repeat(Math.max(0, rows - 23)));
58
86
 
87
+ let messages = [];
59
88
  while (true) {
60
- const ans = await prompts({ type: 'text', name: 'msg', message: ui.youLabel() });
61
- const input = (ans.msg || '').trim();
62
- if (ans.msg === undefined) { console.log(c.muted('bye 👋')); break; } // Ctrl+C / EOF
89
+ console.log('');
90
+ ui.inputBoxOpen();
91
+ const raw = await ask(ui.inputPrompt());
92
+ ui.inputBoxClose();
93
+
94
+ if (raw === null) { console.log(c.muted(' bye')); break; }
95
+ const input = raw.trim();
63
96
  if (!input) continue;
64
- if (input === '/exit' || input === '/quit') { console.log(c.muted('bye 👋')); break; }
97
+ if (input === '/exit' || input === '/quit') { console.log(c.muted(' bye')); break; }
65
98
  if (input === '/help') { printHelp(); continue; }
66
- if (input === '/clear') { messages = []; ui.ok('Started a new conversation.'); continue; }
99
+ if (input === '/key') { console.log('\n ' + c.muted('key ') + c.green(ui.maskKey(cfg.key)) + '\n'); continue; }
100
+ if (input === '/usage') { me = await fetchMe(config.getServer(), cfg.key); showUsage(me); continue; }
101
+ if (input === '/clear') { console.clear(); me = await fetchMe(config.getServer(), cfg.key); ui.banner(meToBanner(cfg, me)); continue; }
67
102
 
68
103
  messages.push({ role: 'user', content: input });
69
- try {
70
- await runTurn(cfg.server || config.getServer(), cfg.key, messages);
71
- } catch (err) {
72
- ui.error(err.message);
73
- }
104
+ try { await runTurn(config.getServer(), cfg.key, messages); }
105
+ catch (err) { ui.error(err.message); }
106
+ me = await fetchMe(config.getServer(), cfg.key); // refresh usage for the status bar
74
107
  }
108
+ close();
75
109
  }
76
110
 
77
111
  async function oneShot(task) {
78
- const cfg = await ensureKey();
79
- const messages = [{ role: 'user', content: task }];
80
- await runTurn(cfg.server || config.getServer(), cfg.key, messages);
112
+ const { cfg } = await ensureKey();
113
+ try { await runTurn(config.getServer(), cfg.key, [{ role: 'user', content: task }]); }
114
+ catch (err) { ui.error(err.message); }
115
+ close();
81
116
  }
82
117
 
83
118
  async function main() {
84
119
  const args = process.argv.slice(2);
85
120
  const cmd = args[0];
86
-
87
121
  if (cmd === '--version' || cmd === '-v') return console.log(pkg.version);
88
- if (cmd === '--help' || cmd === '-h' || cmd === 'help') return printHelp();
89
- if (cmd === 'login') return doLogin();
90
- if (cmd === 'logout') { config.clear(); return ui.ok('Logged out (key removed).'); }
122
+ if (cmd === '--help' || cmd === '-h' || cmd === 'help') { printHelp(); return; }
123
+ if (cmd === 'login') { await doLogin(); close(); return; }
124
+ if (cmd === 'logout') { config.clear(); ui.ok('Logged out (key removed).'); return; }
91
125
  if (cmd && !cmd.startsWith('-')) return oneShot(args.join(' '));
92
126
  return repl();
93
127
  }
94
128
 
95
- main().catch((err) => { ui.error(err.message); process.exit(1); });
129
+ main().catch((err) => { ui.error(err.message); close(); process.exit(1); });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "novaprime",
3
- "version": "1.0.0",
3
+ "version": "1.2.5",
4
4
  "description": "NovaPrime — an AI coding assistant in your terminal, powered by GLM.",
5
5
  "bin": {
6
6
  "novaprime": "bin/novaprime.js"
@@ -18,7 +18,6 @@
18
18
  "dependencies": {
19
19
  "boxen": "^5.1.2",
20
20
  "chalk": "^4.1.2",
21
- "ora": "^5.4.1",
22
- "prompts": "^2.4.2"
21
+ "ora": "^5.4.1"
23
22
  }
24
23
  }
package/src/agent.js CHANGED
@@ -1,70 +1,80 @@
1
1
  'use strict';
2
2
  const os = require('os');
3
+ const ora = require('ora');
3
4
  const tools = require('./tools');
4
5
  const { c, aiLabel, error } = require('./ui');
6
+ const { Renderer } = require('./render');
5
7
 
6
8
  const SYSTEM_PROMPT =
7
- `You are NovaPrime, an AI coding assistant running inside the user's terminal. ` +
9
+ `You are NovaPrime, a friendly AI coding assistant running inside the user's terminal. ` +
8
10
  `You help with coding, files, databases (e.g. MySQL/XAMPP) and shell tasks. ` +
9
11
  `You can read, write and edit files and run shell commands using the provided tools, ` +
10
12
  `all relative to the user's current working directory. ` +
11
- `Always prefer making concrete changes with tools over only describing them. ` +
12
- `Be concise and clear. Current OS: ${os.platform()}. Working directory: ${process.cwd()}.`;
13
+ `LANGUAGE: the user may write in English, Bangla (Bengali script), or romanized Banglish ` +
14
+ `(Bengali written with English letters, e.g. "tumi kemon acho", "kemon aso", "ki korso"). ` +
15
+ `Always understand them and reply in the SAME language and style the user used — if they write Banglish, reply in friendly Banglish. ` +
16
+ `Prefer making concrete changes with tools over only describing them. ` +
17
+ `Use clear markdown: short paragraphs, bullet lists, and fenced code blocks with a language tag. ` +
18
+ `Be concise and warm. Current OS: ${os.platform()}. Working directory: ${process.cwd()}.`;
19
+
20
+ // Fetch read-only account info for the header (name, plan, usage). Never throws.
21
+ async function fetchMe(server, key) {
22
+ try {
23
+ const r = await fetch(server.replace(/\/$/, '') + '/v1/me', { headers: { 'x-novaprime-key': key } });
24
+ if (!r.ok) return null;
25
+ return await r.json();
26
+ } catch (_) { return null; }
27
+ }
13
28
 
14
- // Parse the SSE stream from the server, print text live, and reconstruct content blocks.
15
29
  async function streamMessage(server, key, messages) {
16
- const res = await fetch(server.replace(/\/$/, '') + '/v1/messages', {
17
- method: 'POST',
18
- headers: { 'content-type': 'application/json', 'x-novaprime-key': key },
19
- body: JSON.stringify({
20
- max_tokens: 4096,
21
- system: SYSTEM_PROMPT,
22
- tools: tools.definitions,
23
- messages,
24
- stream: true,
25
- }),
26
- });
30
+ const spinner = ora({ text: c.muted('thinking'), spinner: 'dots', color: 'magenta' }).start();
31
+ let spinning = true;
32
+ const stopSpin = () => { if (spinning) { spinner.stop(); spinning = false; } };
33
+
34
+ let res;
35
+ try {
36
+ res = await fetch(server.replace(/\/$/, '') + '/v1/messages', {
37
+ method: 'POST',
38
+ headers: { 'content-type': 'application/json', 'x-novaprime-key': key },
39
+ body: JSON.stringify({ max_tokens: 4096, system: SYSTEM_PROMPT, tools: tools.definitions, messages, stream: true }),
40
+ });
41
+ } catch (err) { stopSpin(); return { error: 'Could not reach NovaPrime: ' + err.message }; }
27
42
 
28
43
  if (!res.ok) {
44
+ stopSpin();
29
45
  let msg = `Server error (HTTP ${res.status}).`;
30
- try {
31
- const j = await res.json();
32
- if (j && j.error && j.error.message) msg = j.error.message;
33
- } catch (_) {}
46
+ try { const j = await res.json(); if (j && j.error && j.error.message) msg = j.error.message; } catch (_) {}
34
47
  return { error: msg };
35
48
  }
36
49
 
37
50
  const blocks = [];
38
- let stopReason = null;
39
- let printedAi = false;
40
- let buffer = '';
51
+ let stopReason = null, labelShown = false, buffer = '';
52
+ const renderer = new Renderer();
41
53
  const decoder = new TextDecoder();
42
54
  const reader = res.body.getReader();
43
55
 
44
56
  while (true) {
45
57
  const { done, value } = await reader.read();
46
58
  if (done) break;
59
+ stopSpin();
47
60
  buffer += decoder.decode(value, { stream: true });
48
61
  let idx;
49
62
  while ((idx = buffer.indexOf('\n\n')) !== -1) {
50
- const evt = buffer.slice(0, idx);
51
- buffer = buffer.slice(idx + 2);
63
+ const evt = buffer.slice(0, idx); buffer = buffer.slice(idx + 2);
52
64
  const dataLine = evt.split('\n').find((l) => l.startsWith('data:'));
53
65
  if (!dataLine) continue;
54
- let json;
55
- try { json = JSON.parse(dataLine.slice(5).trim()); } catch (_) { continue; }
66
+ let json; try { json = JSON.parse(dataLine.slice(5).trim()); } catch (_) { continue; }
56
67
 
57
68
  if (json.type === 'content_block_start') {
58
69
  blocks[json.index] = json.content_block.type === 'tool_use'
59
70
  ? { type: 'tool_use', id: json.content_block.id, name: json.content_block.name, _json: '' }
60
71
  : { type: 'text', text: '' };
61
72
  } else if (json.type === 'content_block_delta') {
62
- const b = blocks[json.index];
63
- if (!b) continue;
73
+ const b = blocks[json.index]; if (!b) continue;
64
74
  if (json.delta.type === 'text_delta') {
65
- if (!printedAi) { aiLabel(); printedAi = true; }
66
- process.stdout.write(json.delta.text);
75
+ if (!labelShown) { aiLabel(); labelShown = true; }
67
76
  b.text += json.delta.text;
77
+ renderer.feed(json.delta.text);
68
78
  } else if (json.delta.type === 'input_json_delta') {
69
79
  b._json += json.delta.partial_json;
70
80
  }
@@ -73,27 +83,23 @@ async function streamMessage(server, key, messages) {
73
83
  }
74
84
  }
75
85
  }
76
- if (printedAi) process.stdout.write('\n');
86
+ stopSpin();
87
+ renderer.end();
77
88
 
78
- // finalize tool_use inputs
79
89
  const content = blocks.filter(Boolean).map((b) => {
80
90
  if (b.type === 'tool_use') {
81
- let input = {};
82
- try { input = b._json ? JSON.parse(b._json) : {}; } catch (_) {}
91
+ let input = {}; try { input = b._json ? JSON.parse(b._json) : {}; } catch (_) {}
83
92
  return { type: 'tool_use', id: b.id, name: b.name, input };
84
93
  }
85
94
  return { type: 'text', text: b.text };
86
95
  });
87
-
88
96
  return { content, stopReason };
89
97
  }
90
98
 
91
- // One full turn: keep calling the model until it stops needing tools.
92
99
  async function runTurn(server, key, messages) {
93
100
  while (true) {
94
101
  const result = await streamMessage(server, key, messages);
95
102
  if (result.error) { error(result.error); return; }
96
-
97
103
  messages.push({ role: 'assistant', content: result.content });
98
104
 
99
105
  const toolUses = result.content.filter((b) => b.type === 'tool_use');
@@ -104,10 +110,10 @@ async function runTurn(server, key, messages) {
104
110
  toolResults.push({ type: 'tool_result', tool_use_id: tu.id, content: out });
105
111
  }
106
112
  messages.push({ role: 'user', content: toolResults });
107
- continue; // let the model react to the tool results
113
+ continue;
108
114
  }
109
- return; // done
115
+ return;
110
116
  }
111
117
  }
112
118
 
113
- module.exports = { runTurn, SYSTEM_PROMPT };
119
+ module.exports = { runTurn, fetchMe, SYSTEM_PROMPT };
package/src/config.js CHANGED
@@ -24,8 +24,10 @@ function clear() {
24
24
  try { fs.unlinkSync(FILE); } catch (_) {}
25
25
  }
26
26
 
27
+ // Server URL is permanent (baked into the package). Override only via env for dev.
28
+ // If it ever changes, we ship a new version on npm — users never enter it.
27
29
  function getServer() {
28
- return load().server || DEFAULT_SERVER;
30
+ return process.env.NOVAPRIME_SERVER || DEFAULT_SERVER;
29
31
  }
30
32
 
31
33
  module.exports = { load, save, clear, getServer, FILE, DIR, DEFAULT_SERVER };
package/src/prompt.js ADDED
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+ const readline = require('readline');
3
+
4
+ let rl = null;
5
+ function get() {
6
+ if (!rl) rl = readline.createInterface({ input: process.stdin, output: process.stdout, historySize: 200 });
7
+ return rl;
8
+ }
9
+
10
+ // ask a free-text question, returns the typed line (or null on Ctrl+C/EOF)
11
+ function ask(promptStr) {
12
+ return new Promise((resolve) => {
13
+ const r = get();
14
+ const onClose = () => resolve(null);
15
+ r.once('close', onClose);
16
+ r.question(promptStr, (answer) => { r.removeListener('close', onClose); resolve(answer); });
17
+ });
18
+ }
19
+
20
+ // yes/no confirmation (default No)
21
+ function confirm(message) {
22
+ return ask(message + ' (y/N) ').then((a) => /^y(es)?$/i.test((a || '').trim()));
23
+ }
24
+
25
+ function close() { if (rl) { rl.close(); rl = null; } }
26
+
27
+ module.exports = { ask, confirm, close, get };
package/src/render.js ADDED
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+ const chalk = require('chalk');
3
+ const { c } = require('./ui');
4
+
5
+ const CODE = chalk.hex('#7dd3fc'); // soft cyan for code
6
+
7
+ // safely style inline `code` and **bold** without nesting ANSI corruption
8
+ function styleInline(text) {
9
+ return text.split(/(`[^`]+`)/).map((seg) => {
10
+ if (/^`[^`]+`$/.test(seg)) return CODE(seg.slice(1, -1));
11
+ return seg.split(/(\*\*[^*]+\*\*)/).map((s) =>
12
+ /^\*\*[^*]+\*\*$/.test(s) ? c.white.bold(s.slice(2, -2)) : c.body(s)).join('');
13
+ }).join('');
14
+ }
15
+
16
+ function styleLine(line) {
17
+ let m = line.match(/^(#{1,6})\s+(.*)/);
18
+ if (m) return c.white.bold(m[2]);
19
+ m = line.match(/^(\s*)[-*]\s+(.*)/);
20
+ if (m) return m[1] + c.violet('•') + ' ' + styleInline(m[2]);
21
+ m = line.match(/^(\s*)(\d+)\.\s+(.*)/);
22
+ if (m) return m[1] + c.indigo(m[2] + '.') + ' ' + styleInline(m[3]);
23
+ if (!line.trim()) return '';
24
+ return styleInline(line);
25
+ }
26
+
27
+ // Streaming, line-buffered markdown + code-block renderer
28
+ class Renderer {
29
+ constructor() { this.buf = ''; this.inCode = false; }
30
+ feed(text) {
31
+ this.buf += text;
32
+ let nl;
33
+ while ((nl = this.buf.indexOf('\n')) >= 0) {
34
+ const line = this.buf.slice(0, nl);
35
+ this.buf = this.buf.slice(nl + 1);
36
+ this._line(line);
37
+ }
38
+ }
39
+ end() {
40
+ if (this.buf.length) { this._line(this.buf); this.buf = ''; }
41
+ if (this.inCode) { console.log(c.dim(' └' + '─'.repeat(44))); this.inCode = false; }
42
+ }
43
+ _line(line) {
44
+ const fence = line.trim().match(/^```(\w*)/);
45
+ if (fence) {
46
+ if (!this.inCode) {
47
+ const lang = fence[1] || 'code';
48
+ this.inCode = true;
49
+ console.log(c.dim(' ┌─ ') + c.violet(lang) + ' ' + c.dim('─'.repeat(Math.max(2, 40 - lang.length))));
50
+ } else {
51
+ console.log(c.dim(' └' + '─'.repeat(44)));
52
+ this.inCode = false;
53
+ }
54
+ return;
55
+ }
56
+ if (this.inCode) { console.log(c.dim(' │ ') + CODE(line)); return; }
57
+ console.log(' ' + styleLine(line));
58
+ }
59
+ }
60
+
61
+ module.exports = { Renderer };
package/src/tools.js CHANGED
@@ -2,8 +2,8 @@
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
4
  const { spawnSync } = require('child_process');
5
- const prompts = require('prompts');
6
- const { c, tool, warn } = require('./ui');
5
+ const { confirm } = require('./prompt');
6
+ const { c, tool } = require('./ui');
7
7
 
8
8
  const MAX_OUTPUT = 20000; // cap tool output sent back to the model
9
9
 
@@ -67,16 +67,6 @@ function clip(s) {
67
67
  return s;
68
68
  }
69
69
 
70
- async function confirm(message) {
71
- const res = await prompts({
72
- type: 'confirm',
73
- name: 'ok',
74
- message,
75
- initial: false,
76
- });
77
- return res.ok === true;
78
- }
79
-
80
70
  // ---- Executors ----
81
71
  async function execute(name, input) {
82
72
  try {
package/src/ui.js CHANGED
@@ -3,32 +3,108 @@ const chalk = require('chalk');
3
3
  const boxen = require('boxen');
4
4
 
5
5
  const c = {
6
+ indigo: chalk.hex('#818cf8'),
7
+ indigoBold: chalk.hex('#818cf8').bold,
8
+ violet: chalk.hex('#a78bfa'),
6
9
  brand: chalk.hex('#6d8bff'),
7
- accent: chalk.hex('#46d39a'),
8
- warn: chalk.hex('#ffb454'),
9
- danger: chalk.hex('#ff6b6b'),
10
- muted: chalk.gray,
10
+ body: chalk.hex('#cbd5e1'),
11
+ white: chalk.hex('#f1f5f9'),
12
+ muted: chalk.hex('#64748b'),
13
+ dim: chalk.hex('#475569'),
14
+ green: chalk.hex('#34d399'),
15
+ amber: chalk.hex('#fbbf24'),
16
+ red: chalk.hex('#f87171'),
11
17
  bold: chalk.bold,
12
18
  };
13
19
 
14
- function banner() {
15
- const title = c.brand.bold('NovaPrime') + c.muted(' · AI coding assistant');
16
- console.log(
17
- boxen(title + '\n' + c.muted('Type your task. /help for commands, /exit to quit.'), {
18
- padding: { top: 0, bottom: 0, left: 1, right: 1 },
19
- margin: { top: 1, bottom: 1, left: 0, right: 0 },
20
- borderStyle: 'round',
21
- borderColor: '#6d8bff',
22
- })
23
- );
20
+ const LOGO_LINES = [
21
+ '███╗ ██╗ ██████╗ ██╗ ██╗ █████╗ ',
22
+ '████╗ ██║██╔═══██╗██║ ██║██╔══██╗',
23
+ '██╔██╗ ██║██║ ██║██║ ██║███████║',
24
+ '██║╚██╗██║██║ ██║╚██╗ ██╔╝██╔══██║',
25
+ '██║ ╚████║╚██████╔╝ ╚████╔╝ ██║ ██║',
26
+ '╚═╝ ╚═══╝ ╚═════╝ ╚═══╝ ╚═╝ ╚═╝',
27
+ ];
28
+ const GRAD = ['#6366f1', '#6f63f3', '#7c5ff5', '#8b5cf6', '#9a6ff8', '#a78bfa'];
29
+
30
+ // visible length, ignoring ANSI color escapes (ESC ... m)
31
+ function vlen(s) {
32
+ const ESC = String.fromCharCode(27);
33
+ let n = 0, i = 0;
34
+ while (i < s.length) {
35
+ if (s[i] === ESC) { while (i < s.length && s[i] !== 'm') i++; i++; }
36
+ else { n++; i++; }
37
+ }
38
+ return n;
39
+ }
40
+ function padTo(s, w) { return s + ' '.repeat(Math.max(1, w - vlen(s))); }
41
+
42
+ function maskKey(key) {
43
+ if (!key) return '—';
44
+ return key.slice(0, 9) + '•'.repeat(16);
45
+ }
46
+
47
+ function twoCol(l1, v1, l2, v2) {
48
+ const left = c.muted(l1.padEnd(7)) + v1;
49
+ if (l2 === undefined) return left;
50
+ return padTo(left, 34) + c.muted(l2.padEnd(7)) + v2;
24
51
  }
25
52
 
26
- function aiLabel() { process.stdout.write(c.accent('\n● novaprime ')); }
27
- function youLabel() { return c.brand(''); }
53
+ function banner({ folder, key, name, plan, model, windowUsed, windowLimit, weeklyUsed, weeklyLimit, daysLeft, expired }) {
54
+ const logo = LOGO_LINES.map((l, i) => chalk.hex(GRAD[i])(l)).join('\n');
55
+ const wordmark = c.white.bold('N O V A P R I M E') + c.muted(' · AI coding agent');
56
+
57
+ const userVal = plan ? (c.white(name || 'user') + c.dim(' · ') + c.violet(plan)) : c.white(name || 'guest');
58
+ const usage5h = plan ? c.white(`${windowUsed}/${windowLimit || '∞'}`) : c.muted('—');
59
+ const usageWk = plan ? c.white(`${weeklyUsed}/${weeklyLimit || '∞'}`) : c.muted('—');
60
+ const daysVal = (daysLeft !== null && daysLeft !== undefined)
61
+ ? (expired ? c.red('expired') : c.body(daysLeft + ' days'))
62
+ : c.muted('—');
63
+
64
+ const lines = [
65
+ logo,
66
+ '',
67
+ wordmark,
68
+ '',
69
+ twoCol('folder', c.white(folder), 'model', c.body(model || 'auto-routing')),
70
+ twoCol('key', c.green(maskKey(key)), 'user', userVal),
71
+ twoCol('5h', usage5h, 'week', usageWk),
72
+ twoCol('left', daysVal, 'cmds', c.muted('/help /key /usage /clear /exit')),
73
+ ];
74
+
75
+ console.log(boxen(lines.join('\n'), {
76
+ padding: { top: 1, bottom: 1, left: 3, right: 3 },
77
+ margin: { top: 1, bottom: 0, left: 0, right: 0 },
78
+ borderStyle: 'round',
79
+ borderColor: '#6d8bff',
80
+ }));
81
+ }
82
+
83
+ // ---- Claude-style chat input box (full width, complete box) ----
84
+ function boxWidth() { return Math.max(24, (process.stdout.columns || 80) - 1); }
85
+ function inputTop() {
86
+ const w = boxWidth();
87
+ return c.dim('╭─ ') + c.indigo('message') + ' ' + c.dim('─'.repeat(Math.max(2, w - 12)) + '╮');
88
+ }
89
+ function inputPrompt() { return c.dim('│ ') + c.indigoBold('› '); }
90
+ function inputBottom() {
91
+ const w = boxWidth();
92
+ return c.dim('╰' + '─'.repeat(Math.max(2, w - 2)) + '╯');
93
+ }
94
+ // draw the FULL box (top, empty input line, bottom), then move the cursor up onto the input line
95
+ function inputBoxOpen() {
96
+ const ESC = String.fromCharCode(27);
97
+ process.stdout.write(inputTop() + '\n\n' + inputBottom() + '\n' + ESC + '[2A');
98
+ }
99
+ function inputBoxClose() { process.stdout.write('\n'); }
100
+
101
+ function aiLabel() { process.stdout.write('\n' + c.violet('● ') + c.violet.bold('Nova Prime') + '\n'); }
102
+
28
103
  function info(msg) { console.log(c.muted(msg)); }
29
- function warn(msg) { console.log(c.warn('⚠ ' + msg)); }
30
- function error(msg) { console.log(c.danger(' ' + msg)); }
31
- function ok(msg) { console.log(c.accent(' ' + msg)); }
32
- function tool(name, detail) { console.log(c.muted(' ' + name + (detail ? ' ' + detail : ''))); }
104
+ function hint(msg) { console.log(c.dim(msg)); }
105
+ function warn(msg) { console.log(c.amber(' ! ') + c.amber(msg)); }
106
+ function error(msg) { console.log(c.red(' ') + c.red(msg)); }
107
+ function ok(msg) { console.log(c.green(' ') + c.body(msg)); }
108
+ function tool(name, detail) { console.log(c.dim(' · ' + name + (detail ? ' ' + detail : ''))); }
33
109
 
34
- module.exports = { chalk, boxen, c, banner, aiLabel, youLabel, info, warn, error, ok, tool };
110
+ module.exports = { chalk, boxen, c, banner, maskKey, aiLabel, inputTop, inputBottom, inputPrompt, inputBoxOpen, inputBoxClose, info, hint, warn, error, ok, tool };