novaprime 1.0.0 → 1.2.4
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 +88 -54
- package/package.json +2 -3
- package/src/agent.js +46 -40
- package/src/config.js +3 -1
- package/src/prompt.js +27 -0
- package/src/render.js +61 -0
- package/src/tools.js +2 -12
- package/src/ui.js +91 -21
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 {
|
|
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('\
|
|
13
|
-
ui.info('Paste the novaprime-key your
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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.
|
|
37
|
-
|
|
38
|
-
${c.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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.
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
89
|
+
console.log('');
|
|
90
|
+
process.stdout.write(ui.inputTop() + '\n');
|
|
91
|
+
const raw = await ask(ui.inputPrompt());
|
|
92
|
+
process.stdout.write(ui.inputBottom() + '\n\n');
|
|
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
|
|
97
|
+
if (input === '/exit' || input === '/quit') { console.log(c.muted(' bye')); break; }
|
|
65
98
|
if (input === '/help') { printHelp(); continue; }
|
|
66
|
-
if (input === '/
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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')
|
|
89
|
-
if (cmd === 'login')
|
|
90
|
-
if (cmd === 'logout') { config.clear();
|
|
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.
|
|
3
|
+
"version": "1.2.4",
|
|
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,
|
|
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
|
-
`
|
|
12
|
-
`
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
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;
|
|
113
|
+
continue;
|
|
108
114
|
}
|
|
109
|
-
return;
|
|
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
|
|
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
|
|
6
|
-
const { c, tool
|
|
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,102 @@ 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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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;
|
|
51
|
+
}
|
|
52
|
+
|
|
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
|
+
}));
|
|
24
81
|
}
|
|
25
82
|
|
|
26
|
-
|
|
27
|
-
function
|
|
83
|
+
// ---- Claude-style chat input box (clean: no status inside) ----
|
|
84
|
+
function boxWidth() { return Math.min((process.stdout.columns || 80) - 2, 60); }
|
|
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 - 1)) + '╯');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function aiLabel() { process.stdout.write('\n' + c.violet('● ') + c.violet.bold('Nova Prime') + '\n'); }
|
|
96
|
+
|
|
28
97
|
function info(msg) { console.log(c.muted(msg)); }
|
|
29
|
-
function
|
|
30
|
-
function
|
|
31
|
-
function
|
|
32
|
-
function
|
|
98
|
+
function hint(msg) { console.log(c.dim(msg)); }
|
|
99
|
+
function warn(msg) { console.log(c.amber(' ! ') + c.amber(msg)); }
|
|
100
|
+
function error(msg) { console.log(c.red(' ✕ ') + c.red(msg)); }
|
|
101
|
+
function ok(msg) { console.log(c.green(' ✓ ') + c.body(msg)); }
|
|
102
|
+
function tool(name, detail) { console.log(c.dim(' · ' + name + (detail ? ' ' + detail : ''))); }
|
|
33
103
|
|
|
34
|
-
module.exports = { chalk, boxen, c, banner, aiLabel,
|
|
104
|
+
module.exports = { chalk, boxen, c, banner, maskKey, aiLabel, inputTop, inputPrompt, inputBottom, info, hint, warn, error, ok, tool };
|