novaprime 1.0.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/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # NovaPrime
2
+
3
+ An AI coding assistant that lives in your terminal โ€” read, write and edit files and run commands, powered by GLM through your NovaPrime server.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g novaprime
9
+ ```
10
+
11
+ ## First run
12
+
13
+ ```bash
14
+ novaprime login
15
+ ```
16
+
17
+ Paste the **novaprime-key** your administrator gave you. It is saved locally to `~/.novaprime/config.json` and never shared with anyone but your NovaPrime server.
18
+
19
+ ## Use
20
+
21
+ ```bash
22
+ novaprime # start an interactive session
23
+ novaprime "fix the bug in app.js" # run a single task
24
+ ```
25
+
26
+ In a session:
27
+
28
+ | Command | What it does |
29
+ |---------|--------------|
30
+ | `/help` | show help |
31
+ | `/clear` | start a new conversation |
32
+ | `/exit` | quit (Ctrl+C also works) |
33
+
34
+ NovaPrime works in your **current folder**. Before it writes a file, edits a file, or runs a shell command, it shows you exactly what it will do and asks for permission.
35
+
36
+ ## Other commands
37
+
38
+ ```bash
39
+ novaprime logout # remove your saved key
40
+ novaprime --version
41
+ novaprime --help
42
+ ```
43
+
44
+ ## Privacy & safety
45
+
46
+ - Your key only talks to your NovaPrime server; the underlying AI provider key never touches your machine.
47
+ - File writes, edits and shell commands always require your confirmation.
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ const prompts = require('prompts');
4
+ const config = require('../src/config');
5
+ const ui = require('../src/ui');
6
+ const { c } = ui;
7
+ const { runTurn } = require('../src/agent');
8
+
9
+ const pkg = require('../package.json');
10
+
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);
24
+ }
25
+
26
+ async function ensureKey() {
27
+ 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();
32
+ }
33
+
34
+ function printHelp() {
35
+ 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
45
+
46
+ ${c.bold('In a session:')}
47
+ /help show help /clear start a new conversation
48
+ /exit quit (Ctrl+C also quits)
49
+ `);
50
+ }
51
+
52
+ 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); });
58
+
59
+ 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
63
+ if (!input) continue;
64
+ if (input === '/exit' || input === '/quit') { console.log(c.muted('bye ๐Ÿ‘‹')); break; }
65
+ if (input === '/help') { printHelp(); continue; }
66
+ if (input === '/clear') { messages = []; ui.ok('Started a new conversation.'); continue; }
67
+
68
+ 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
+ }
74
+ }
75
+ }
76
+
77
+ 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);
81
+ }
82
+
83
+ async function main() {
84
+ const args = process.argv.slice(2);
85
+ const cmd = args[0];
86
+
87
+ 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).'); }
91
+ if (cmd && !cmd.startsWith('-')) return oneShot(args.join(' '));
92
+ return repl();
93
+ }
94
+
95
+ main().catch((err) => { ui.error(err.message); process.exit(1); });
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "novaprime",
3
+ "version": "1.0.0",
4
+ "description": "NovaPrime โ€” an AI coding assistant in your terminal, powered by GLM.",
5
+ "bin": {
6
+ "novaprime": "bin/novaprime.js"
7
+ },
8
+ "files": [
9
+ "bin/",
10
+ "src/",
11
+ "README.md"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "keywords": ["ai", "cli", "coding", "assistant", "glm", "novaprime"],
17
+ "license": "MIT",
18
+ "dependencies": {
19
+ "boxen": "^5.1.2",
20
+ "chalk": "^4.1.2",
21
+ "ora": "^5.4.1",
22
+ "prompts": "^2.4.2"
23
+ }
24
+ }
package/src/agent.js ADDED
@@ -0,0 +1,113 @@
1
+ 'use strict';
2
+ const os = require('os');
3
+ const tools = require('./tools');
4
+ const { c, aiLabel, error } = require('./ui');
5
+
6
+ const SYSTEM_PROMPT =
7
+ `You are NovaPrime, an AI coding assistant running inside the user's terminal. ` +
8
+ `You help with coding, files, databases (e.g. MySQL/XAMPP) and shell tasks. ` +
9
+ `You can read, write and edit files and run shell commands using the provided tools, ` +
10
+ `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
+
14
+ // Parse the SSE stream from the server, print text live, and reconstruct content blocks.
15
+ 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
+ });
27
+
28
+ if (!res.ok) {
29
+ 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 (_) {}
34
+ return { error: msg };
35
+ }
36
+
37
+ const blocks = [];
38
+ let stopReason = null;
39
+ let printedAi = false;
40
+ let buffer = '';
41
+ const decoder = new TextDecoder();
42
+ const reader = res.body.getReader();
43
+
44
+ while (true) {
45
+ const { done, value } = await reader.read();
46
+ if (done) break;
47
+ buffer += decoder.decode(value, { stream: true });
48
+ let idx;
49
+ while ((idx = buffer.indexOf('\n\n')) !== -1) {
50
+ const evt = buffer.slice(0, idx);
51
+ buffer = buffer.slice(idx + 2);
52
+ const dataLine = evt.split('\n').find((l) => l.startsWith('data:'));
53
+ if (!dataLine) continue;
54
+ let json;
55
+ try { json = JSON.parse(dataLine.slice(5).trim()); } catch (_) { continue; }
56
+
57
+ if (json.type === 'content_block_start') {
58
+ blocks[json.index] = json.content_block.type === 'tool_use'
59
+ ? { type: 'tool_use', id: json.content_block.id, name: json.content_block.name, _json: '' }
60
+ : { type: 'text', text: '' };
61
+ } else if (json.type === 'content_block_delta') {
62
+ const b = blocks[json.index];
63
+ if (!b) continue;
64
+ if (json.delta.type === 'text_delta') {
65
+ if (!printedAi) { aiLabel(); printedAi = true; }
66
+ process.stdout.write(json.delta.text);
67
+ b.text += json.delta.text;
68
+ } else if (json.delta.type === 'input_json_delta') {
69
+ b._json += json.delta.partial_json;
70
+ }
71
+ } else if (json.type === 'message_delta') {
72
+ if (json.delta && json.delta.stop_reason) stopReason = json.delta.stop_reason;
73
+ }
74
+ }
75
+ }
76
+ if (printedAi) process.stdout.write('\n');
77
+
78
+ // finalize tool_use inputs
79
+ const content = blocks.filter(Boolean).map((b) => {
80
+ if (b.type === 'tool_use') {
81
+ let input = {};
82
+ try { input = b._json ? JSON.parse(b._json) : {}; } catch (_) {}
83
+ return { type: 'tool_use', id: b.id, name: b.name, input };
84
+ }
85
+ return { type: 'text', text: b.text };
86
+ });
87
+
88
+ return { content, stopReason };
89
+ }
90
+
91
+ // One full turn: keep calling the model until it stops needing tools.
92
+ async function runTurn(server, key, messages) {
93
+ while (true) {
94
+ const result = await streamMessage(server, key, messages);
95
+ if (result.error) { error(result.error); return; }
96
+
97
+ messages.push({ role: 'assistant', content: result.content });
98
+
99
+ const toolUses = result.content.filter((b) => b.type === 'tool_use');
100
+ if (result.stopReason === 'tool_use' && toolUses.length) {
101
+ const toolResults = [];
102
+ for (const tu of toolUses) {
103
+ const out = await tools.execute(tu.name, tu.input);
104
+ toolResults.push({ type: 'tool_result', tool_use_id: tu.id, content: out });
105
+ }
106
+ messages.push({ role: 'user', content: toolResults });
107
+ continue; // let the model react to the tool results
108
+ }
109
+ return; // done
110
+ }
111
+ }
112
+
113
+ module.exports = { runTurn, SYSTEM_PROMPT };
package/src/config.js ADDED
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+
6
+ const DEFAULT_SERVER = 'https://novaprime.fragzonebd.com';
7
+ const DIR = path.join(os.homedir(), '.novaprime');
8
+ const FILE = path.join(DIR, 'config.json');
9
+
10
+ function load() {
11
+ try {
12
+ return JSON.parse(fs.readFileSync(FILE, 'utf8'));
13
+ } catch (_) {
14
+ return {};
15
+ }
16
+ }
17
+
18
+ function save(cfg) {
19
+ fs.mkdirSync(DIR, { recursive: true });
20
+ fs.writeFileSync(FILE, JSON.stringify(cfg, null, 2), { mode: 0o600 });
21
+ }
22
+
23
+ function clear() {
24
+ try { fs.unlinkSync(FILE); } catch (_) {}
25
+ }
26
+
27
+ function getServer() {
28
+ return load().server || DEFAULT_SERVER;
29
+ }
30
+
31
+ module.exports = { load, save, clear, getServer, FILE, DIR, DEFAULT_SERVER };
package/src/tools.js ADDED
@@ -0,0 +1,131 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const { spawnSync } = require('child_process');
5
+ const prompts = require('prompts');
6
+ const { c, tool, warn } = require('./ui');
7
+
8
+ const MAX_OUTPUT = 20000; // cap tool output sent back to the model
9
+
10
+ // ---- Tool definitions (Anthropic tool-use format) ----
11
+ const definitions = [
12
+ {
13
+ name: 'read_file',
14
+ description: 'Read the contents of a text file in the current project.',
15
+ input_schema: {
16
+ type: 'object',
17
+ properties: { path: { type: 'string', description: 'Relative or absolute file path' } },
18
+ required: ['path'],
19
+ },
20
+ },
21
+ {
22
+ name: 'list_files',
23
+ description: 'List files and folders in a directory (default: current directory).',
24
+ input_schema: {
25
+ type: 'object',
26
+ properties: { path: { type: 'string', description: 'Directory path (default ".")' } },
27
+ },
28
+ },
29
+ {
30
+ name: 'write_file',
31
+ description: 'Create a new file or overwrite an existing file with the given content.',
32
+ input_schema: {
33
+ type: 'object',
34
+ properties: {
35
+ path: { type: 'string' },
36
+ content: { type: 'string' },
37
+ },
38
+ required: ['path', 'content'],
39
+ },
40
+ },
41
+ {
42
+ name: 'edit_file',
43
+ description: 'Replace an exact piece of text in a file with new text. old_string must match exactly once.',
44
+ input_schema: {
45
+ type: 'object',
46
+ properties: {
47
+ path: { type: 'string' },
48
+ old_string: { type: 'string' },
49
+ new_string: { type: 'string' },
50
+ },
51
+ required: ['path', 'old_string', 'new_string'],
52
+ },
53
+ },
54
+ {
55
+ name: 'run_command',
56
+ description: 'Run a shell command in the current directory (e.g. for git, npm, mysql). Always asks the user for permission first.',
57
+ input_schema: {
58
+ type: 'object',
59
+ properties: { command: { type: 'string' } },
60
+ required: ['command'],
61
+ },
62
+ },
63
+ ];
64
+
65
+ function clip(s) {
66
+ if (s.length > MAX_OUTPUT) return s.slice(0, MAX_OUTPUT) + `\n...[truncated ${s.length - MAX_OUTPUT} chars]`;
67
+ return s;
68
+ }
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
+ // ---- Executors ----
81
+ async function execute(name, input) {
82
+ try {
83
+ switch (name) {
84
+ case 'read_file': {
85
+ tool('read', input.path);
86
+ const data = fs.readFileSync(input.path, 'utf8');
87
+ return clip(data);
88
+ }
89
+ case 'list_files': {
90
+ const dir = input.path || '.';
91
+ tool('list', dir);
92
+ const items = fs.readdirSync(dir, { withFileTypes: true })
93
+ .map((d) => (d.isDirectory() ? d.name + '/' : d.name));
94
+ return items.join('\n') || '(empty)';
95
+ }
96
+ case 'write_file': {
97
+ console.log(c.warn('\n โœŽ write file: ') + c.bold(input.path) +
98
+ c.muted(` (${(input.content || '').length} chars)`));
99
+ if (!(await confirm('Allow writing this file?'))) return 'DENIED: user did not allow writing the file.';
100
+ fs.mkdirSync(path.dirname(path.resolve(input.path)), { recursive: true });
101
+ fs.writeFileSync(input.path, input.content);
102
+ return 'OK: wrote ' + input.path;
103
+ }
104
+ case 'edit_file': {
105
+ const before = fs.readFileSync(input.path, 'utf8');
106
+ const count = before.split(input.old_string).length - 1;
107
+ if (count === 0) return 'ERROR: old_string not found in ' + input.path;
108
+ if (count > 1) return `ERROR: old_string matches ${count} times; make it more specific.`;
109
+ console.log(c.warn('\n โœŽ edit file: ') + c.bold(input.path));
110
+ console.log(c.danger(' - ' + input.old_string.split('\n').join('\n - ')));
111
+ console.log(c.accent(' + ' + input.new_string.split('\n').join('\n + ')));
112
+ if (!(await confirm('Allow this edit?'))) return 'DENIED: user did not allow the edit.';
113
+ fs.writeFileSync(input.path, before.replace(input.old_string, input.new_string));
114
+ return 'OK: edited ' + input.path;
115
+ }
116
+ case 'run_command': {
117
+ console.log(c.warn('\n $ ') + c.bold(input.command));
118
+ if (!(await confirm('Run this command?'))) return 'DENIED: user did not allow running the command.';
119
+ const r = spawnSync(input.command, { shell: true, encoding: 'utf8', timeout: 1000 * 120 });
120
+ const out = (r.stdout || '') + (r.stderr || '');
121
+ return clip(`exit_code=${r.status}\n${out}`.trim());
122
+ }
123
+ default:
124
+ return 'ERROR: unknown tool ' + name;
125
+ }
126
+ } catch (err) {
127
+ return 'ERROR: ' + err.message;
128
+ }
129
+ }
130
+
131
+ module.exports = { definitions, execute };
package/src/ui.js ADDED
@@ -0,0 +1,34 @@
1
+ 'use strict';
2
+ const chalk = require('chalk');
3
+ const boxen = require('boxen');
4
+
5
+ const c = {
6
+ brand: chalk.hex('#6d8bff'),
7
+ accent: chalk.hex('#46d39a'),
8
+ warn: chalk.hex('#ffb454'),
9
+ danger: chalk.hex('#ff6b6b'),
10
+ muted: chalk.gray,
11
+ bold: chalk.bold,
12
+ };
13
+
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
+ );
24
+ }
25
+
26
+ function aiLabel() { process.stdout.write(c.accent('\nโ— novaprime ')); }
27
+ function youLabel() { return c.brand('โฏ '); }
28
+ 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 : ''))); }
33
+
34
+ module.exports = { chalk, boxen, c, banner, aiLabel, youLabel, info, warn, error, ok, tool };