lumina-ai-cli 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,92 @@
1
+ # lumina-ai-cli — Personal AI Assistant CLI
2
+
3
+ Chat with AI models from OpenAI, Anthropic, Google (Gemini), Groq, Together AI, and custom endpoints — all from your terminal.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g lumina-ai-cli
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ lumina
15
+ ```
16
+
17
+ On first run, you'll be guided through:
18
+ 1. Choosing an API provider
19
+ 2. Entering your API key (stored locally, never shared)
20
+ 3. Selecting a default model
21
+ 4. Picking a persona
22
+ 5. Setting your token budget
23
+
24
+ ## Commands
25
+
26
+ | Command | Description |
27
+ |---------|-------------|
28
+ | `lumina` | Start the interactive TUI |
29
+ | `lumina --config` | Re-run setup |
30
+ | `lumina --model <name>` | Override model for session |
31
+ | `lumina --persona <name>` | Override persona for session |
32
+ | `lumina --budget <n>` | Override token budget |
33
+ | `lumina --help` | Show help |
34
+ | `lumina --version` | Show version |
35
+
36
+ ### In-Chat Commands
37
+
38
+ | Command | Description |
39
+ |---------|-------------|
40
+ | `:q` or `:quit` | Exit |
41
+ | `:m <model>` | Switch model |
42
+ | `:p <persona>` | Switch persona |
43
+ | `:b <budget>` | Switch token budget |
44
+ | `:clear` | Clear chat history |
45
+ | `:save <filename>` | Save conversation to file |
46
+
47
+ ## Provider Setup
48
+
49
+ ### OpenAI
50
+ Get your API key from [platform.openai.com/api-keys](https://platform.openai.com/api-keys)
51
+
52
+ ### Anthropic
53
+ Get your API key from [console.anthropic.com](https://console.anthropic.com)
54
+
55
+ ### Google (Gemini)
56
+ Get your API key from [aistudio.google.com](https://aistudio.google.com)
57
+
58
+ ### Groq
59
+ Get your API key from [console.groq.com](https://console.groq.com)
60
+
61
+ ### Together AI
62
+ Get your API key from [api.together.xyz](https://api.together.xyz)
63
+
64
+ ### Custom
65
+ Any OpenAI-compatible endpoint. Provide your base URL and API key.
66
+
67
+ ## Personas
68
+
69
+ - **Planner** — Technical architecture and project planning
70
+ - **Coder** — Senior software engineer for clean code
71
+ - **Reviewer** — Brutal code review with severity ratings
72
+ - **Fixer** — Debugger with minimal fixes
73
+ - **General** — Default helpful assistant
74
+
75
+ ## Token Efficiency
76
+
77
+ Lumina is built for extreme token efficiency:
78
+ - Personas enforce concise responses (default 256 tokens)
79
+ - Every 10 turns, conversation is summarized into a compact memory block
80
+ - Real-time token count and cost display
81
+ - Early truncation when budget exceeded
82
+
83
+ ## Troubleshooting
84
+
85
+ Invalid API key: Run `lumina --config` to update your key.
86
+ Rate limited: Wait or switch models with `:m <model>`.
87
+ Connection failed: Check your internet connection and API endpoint.
88
+ Model not found: Run `lumina --config` to see available models.
89
+
90
+ ## License
91
+
92
+ MIT
package/bin/lumina.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../src/index.js';
3
+ main(process.argv.slice(2));
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "lumina-ai-cli",
3
+ "version": "1.0.0",
4
+ "description": "Personal AI Assistant CLI — chat with AI models from OpenAI, Anthropic, Google, Groq, Together AI and custom endpoints",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "lumina": "bin/lumina.js",
8
+ "lumina-ai-cli": "bin/lumina.js"
9
+ },
10
+ "engines": {
11
+ "node": ">=18.0.0"
12
+ },
13
+ "type": "module",
14
+ "keywords": [
15
+ "ai",
16
+ "cli",
17
+ "chat",
18
+ "openai",
19
+ "anthropic",
20
+ "google",
21
+ "gemini",
22
+ "groq",
23
+ "together",
24
+ "assistant",
25
+ "terminal"
26
+ ],
27
+ "license": "MIT",
28
+ "dependencies": {
29
+ "@anthropic-ai/sdk": "^0.27.0",
30
+ "@google/generative-ai": "^0.12.0",
31
+ "chalk": "^5.3.0",
32
+ "fs-extra": "^11.2.0",
33
+ "gpt-tokenizer": "^2.1.0",
34
+ "groq-sdk": "^0.5.0",
35
+ "highlight.js": "^11.9.0",
36
+ "ink": "^5.0.0",
37
+ "ink-spinner": "^5.0.0",
38
+ "ink-text-input": "^6.0.0",
39
+ "inquirer": "^9.2.0",
40
+ "openai": "^4.47.0",
41
+ "react": "^18.2.0"
42
+ }
43
+ }
@@ -0,0 +1,63 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+ import { Provider } from './providers.js';
3
+
4
+ const ANTHROPIC_MODELS = [
5
+ 'claude-3-5-sonnet-20241022',
6
+ 'claude-3-5-haiku-20241022',
7
+ 'claude-3-opus-20240229',
8
+ 'claude-3-haiku-20240307',
9
+ 'claude-3-sonnet-20240229',
10
+ ];
11
+
12
+ export class AnthropicProvider extends Provider {
13
+ constructor(config) {
14
+ super(config);
15
+ this.client = new Anthropic({
16
+ apiKey: config.apiKey,
17
+ });
18
+ }
19
+
20
+ async validateKey() {
21
+ try {
22
+ await this.client.messages.create({
23
+ model: 'claude-3-haiku-20240307',
24
+ max_tokens: 1,
25
+ messages: [{ role: 'user', content: 'hi' }]
26
+ });
27
+ return true;
28
+ } catch (err) {
29
+ if (err.status === 401) throw new Error(err.message);
30
+ return true;
31
+ }
32
+ }
33
+
34
+ async listModels() {
35
+ return ANTHROPIC_MODELS;
36
+ }
37
+
38
+ async chat(messages, options = {}) {
39
+ this.abortController = new AbortController();
40
+ const systemMsg = messages.find(m => m.role === 'system');
41
+ const chatMessages = messages.filter(m => m.role !== 'system').map(m => ({
42
+ role: m.role === 'assistant' ? 'assistant' : 'user',
43
+ content: m.content
44
+ }));
45
+
46
+ const stream = await this.client.messages.create({
47
+ model: options.model || this.config.defaultModel,
48
+ max_tokens: options.maxTokens || (this.config.tokenBudget === 'unlimited' ? 4096 : this.config.tokenBudget),
49
+ system: systemMsg?.content || undefined,
50
+ messages: chatMessages,
51
+ stream: true,
52
+ });
53
+
54
+ let full = '';
55
+ for await (const event of stream) {
56
+ if (event.type === 'content_block_delta' && event.delta?.text) {
57
+ if (options.onToken) options.onToken(event.delta.text);
58
+ full += event.delta.text;
59
+ }
60
+ }
61
+ return full;
62
+ }
63
+ }
@@ -0,0 +1,100 @@
1
+ import { Provider } from './providers.js';
2
+
3
+ export class CustomProvider extends Provider {
4
+ constructor(config) {
5
+ super(config);
6
+ this.baseUrl = config.baseUrl?.replace(/\/+$/, '');
7
+ }
8
+
9
+ async _fetch(path, options = {}) {
10
+ const url = this.baseUrl ? `${this.baseUrl}${path}` : path;
11
+ const response = await fetch(url, {
12
+ headers: {
13
+ 'Authorization': `Bearer ${this.config.apiKey}`,
14
+ 'Content-Type': 'application/json',
15
+ },
16
+ ...options
17
+ });
18
+ if (!response.ok) {
19
+ const err = await response.text();
20
+ throw new Error(err);
21
+ }
22
+ return response.json();
23
+ }
24
+
25
+ async validateKey() {
26
+ try {
27
+ if (this.baseUrl) {
28
+ await this._fetch('/models');
29
+ }
30
+ return true;
31
+ } catch (err) {
32
+ throw new Error(err.message);
33
+ }
34
+ }
35
+
36
+ async listModels() {
37
+ if (this.baseUrl) {
38
+ try {
39
+ const data = await this._fetch('/models');
40
+ if (data.data) return data.data.map(m => m.id || m.name).sort();
41
+ if (Array.isArray(data)) return data.map(m => m.id || m.name).sort();
42
+ return Object.values(data).filter(v => typeof v === 'string').sort();
43
+ } catch {}
44
+ }
45
+ return this.config.availableModels || ['custom-model'];
46
+ }
47
+
48
+ async chat(messages, options = {}) {
49
+ this.abortController = new AbortController();
50
+ const url = this.baseUrl ? `${this.baseUrl}/chat/completions` : 'https://api.openai.com/v1/chat/completions';
51
+ const response = await fetch(url, {
52
+ method: 'POST',
53
+ headers: {
54
+ 'Authorization': `Bearer ${this.config.apiKey}`,
55
+ 'Content-Type': 'application/json',
56
+ },
57
+ body: JSON.stringify({
58
+ model: options.model || this.config.defaultModel,
59
+ messages,
60
+ stream: true,
61
+ max_tokens: options.maxTokens || (this.config.tokenBudget === 'unlimited' ? 4096 : this.config.tokenBudget),
62
+ }),
63
+ signal: this.abortController.signal
64
+ });
65
+
66
+ if (!response.ok) {
67
+ const err = await response.text();
68
+ throw new Error(err);
69
+ }
70
+
71
+ const reader = response.body.getReader();
72
+ const decoder = new TextDecoder();
73
+ let full = '';
74
+ let buffer = '';
75
+
76
+ while (true) {
77
+ const { done, value } = await reader.read();
78
+ if (done) break;
79
+ buffer += decoder.decode(value, { stream: true });
80
+ const lines = buffer.split('\n');
81
+ buffer = lines.pop() || '';
82
+
83
+ for (const line of lines) {
84
+ const trimmed = line.trim();
85
+ if (!trimmed || !trimmed.startsWith('data: ')) continue;
86
+ const data = trimmed.slice(6);
87
+ if (data === '[DONE]') continue;
88
+ try {
89
+ const parsed = JSON.parse(data);
90
+ const delta = parsed.choices?.[0]?.delta?.content || '';
91
+ if (delta && options.onToken) {
92
+ options.onToken(delta);
93
+ }
94
+ full += delta;
95
+ } catch {}
96
+ }
97
+ }
98
+ return full;
99
+ }
100
+ }
@@ -0,0 +1,63 @@
1
+ import { GoogleGenerativeAI } from '@google/generative-ai';
2
+ import { Provider } from './providers.js';
3
+
4
+ export class GoogleProvider extends Provider {
5
+ constructor(config) {
6
+ super(config);
7
+ this.client = new GoogleGenerativeAI(config.apiKey);
8
+ }
9
+
10
+ async validateKey() {
11
+ try {
12
+ const model = this.client.getGenerativeModel({ model: 'gemini-1.5-flash' });
13
+ await model.generateContent('test');
14
+ return true;
15
+ } catch (err) {
16
+ throw new Error(err.message);
17
+ }
18
+ }
19
+
20
+ async listModels() {
21
+ try {
22
+ const response = await fetch('https://generativelanguage.googleapis.com/v1/models?key=' + this.config.apiKey);
23
+ const data = await response.json();
24
+ if (data.models) {
25
+ return data.models
26
+ .filter(m => m.name.startsWith('models/gemini'))
27
+ .map(m => m.name.replace('models/', ''))
28
+ .sort();
29
+ }
30
+ } catch {}
31
+ return ['gemini-1.5-pro', 'gemini-1.5-flash', 'gemini-2.0-flash'];
32
+ }
33
+
34
+ async chat(messages, options = {}) {
35
+ this.abortController = new AbortController();
36
+ const model = this.client.getGenerativeModel({
37
+ model: options.model || this.config.defaultModel,
38
+ });
39
+
40
+ const chatMessages = messages.filter(m => m.role !== 'system');
41
+ let history = chatMessages.slice(0, -1).map(m => ({
42
+ role: m.role === 'assistant' ? 'model' : 'user',
43
+ parts: [{ text: m.content }]
44
+ }));
45
+
46
+ const lastMsg = chatMessages[chatMessages.length - 1];
47
+ const systemMsg = messages.find(m => m.role === 'system');
48
+
49
+ const chat = model.startChat({
50
+ history,
51
+ systemInstruction: systemMsg?.content || undefined,
52
+ });
53
+
54
+ const result = await chat.sendMessage(lastMsg.content);
55
+ const response = result.response;
56
+ const text = response.text();
57
+
58
+ if (options.onToken) {
59
+ options.onToken(text);
60
+ }
61
+ return text;
62
+ }
63
+ }
@@ -0,0 +1,45 @@
1
+ import Groq from 'groq-sdk';
2
+ import { Provider } from './providers.js';
3
+
4
+ export class GroqProvider extends Provider {
5
+ constructor(config) {
6
+ super(config);
7
+ this.client = new Groq({
8
+ apiKey: config.apiKey,
9
+ });
10
+ }
11
+
12
+ async validateKey() {
13
+ try {
14
+ await this.client.models.list();
15
+ return true;
16
+ } catch (err) {
17
+ throw new Error(err.message);
18
+ }
19
+ }
20
+
21
+ async listModels() {
22
+ const response = await this.client.models.list();
23
+ return response.data.map(m => m.id).sort();
24
+ }
25
+
26
+ async chat(messages, options = {}) {
27
+ this.abortController = new AbortController();
28
+ const stream = await this.client.chat.completions.create({
29
+ model: options.model || this.config.defaultModel,
30
+ messages,
31
+ stream: true,
32
+ max_tokens: options.maxTokens || (this.config.tokenBudget === 'unlimited' ? 4096 : this.config.tokenBudget),
33
+ });
34
+
35
+ let full = '';
36
+ for await (const chunk of stream) {
37
+ const delta = chunk.choices?.[0]?.delta?.content || '';
38
+ if (delta && options.onToken) {
39
+ options.onToken(delta);
40
+ }
41
+ full += delta;
42
+ }
43
+ return full;
44
+ }
45
+ }
@@ -0,0 +1,50 @@
1
+ import OpenAI from 'openai';
2
+ import { Provider } from './providers.js';
3
+
4
+ export class OpenAIProvider extends Provider {
5
+ constructor(config) {
6
+ super(config);
7
+ this.client = new OpenAI({
8
+ apiKey: config.apiKey,
9
+ baseURL: config.baseUrl || undefined,
10
+ });
11
+ }
12
+
13
+ async validateKey() {
14
+ try {
15
+ await this.client.models.list();
16
+ return true;
17
+ } catch (err) {
18
+ throw new Error(err.message);
19
+ }
20
+ }
21
+
22
+ async listModels() {
23
+ const response = await this.client.models.list();
24
+ return response.data
25
+ .filter(m => m.id.startsWith('gpt') || m.id.startsWith('o1') || m.id.startsWith('o3'))
26
+ .map(m => m.id)
27
+ .sort();
28
+ }
29
+
30
+ async chat(messages, options = {}) {
31
+ this.abortController = new AbortController();
32
+ const stream = await this.client.chat.completions.create({
33
+ model: options.model || this.config.defaultModel,
34
+ messages,
35
+ stream: true,
36
+ max_tokens: options.maxTokens || this.config.tokenBudget === 'unlimited' ? 4096 : this.config.tokenBudget,
37
+ signal: this.abortController.signal
38
+ });
39
+
40
+ let full = '';
41
+ for await (const chunk of stream) {
42
+ const delta = chunk.choices?.[0]?.delta?.content || '';
43
+ if (delta && options.onToken) {
44
+ options.onToken(delta);
45
+ }
46
+ full += delta;
47
+ }
48
+ return full;
49
+ }
50
+ }
@@ -0,0 +1,35 @@
1
+ import { encode } from 'gpt-tokenizer';
2
+
3
+ export class Provider {
4
+ constructor(config) {
5
+ this.config = config;
6
+ this.abortController = null;
7
+ }
8
+
9
+ async validateKey() {
10
+ throw new Error('validateKey() must be implemented by subclass');
11
+ }
12
+
13
+ async listModels() {
14
+ throw new Error('listModels() must be implemented by subclass');
15
+ }
16
+
17
+ async chat(messages, options) {
18
+ throw new Error('chat() must be implemented by subclass');
19
+ }
20
+
21
+ abort() {
22
+ if (this.abortController) {
23
+ this.abortController.abort();
24
+ }
25
+ }
26
+
27
+ countTokens(text) {
28
+ if (!text) return 0;
29
+ try {
30
+ return encode(text).length;
31
+ } catch {
32
+ return Math.ceil(text.length / 4);
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,89 @@
1
+ import { Provider } from './providers.js';
2
+
3
+ export class TogetherProvider extends Provider {
4
+ constructor(config) {
5
+ super(config);
6
+ this.baseUrl = 'https://api.together.xyz/v1';
7
+ }
8
+
9
+ async _fetch(path, options = {}) {
10
+ const response = await fetch(`${this.baseUrl}${path}`, {
11
+ headers: {
12
+ 'Authorization': `Bearer ${this.config.apiKey}`,
13
+ 'Content-Type': 'application/json',
14
+ },
15
+ ...options
16
+ });
17
+ if (!response.ok) {
18
+ const err = await response.text();
19
+ throw new Error(err);
20
+ }
21
+ return response.json();
22
+ }
23
+
24
+ async validateKey() {
25
+ try {
26
+ await this._fetch('/models');
27
+ return true;
28
+ } catch (err) {
29
+ throw new Error(err.message);
30
+ }
31
+ }
32
+
33
+ async listModels() {
34
+ const data = await this._fetch('/models');
35
+ return data.map(m => m.id || m.name).sort();
36
+ }
37
+
38
+ async chat(messages, options = {}) {
39
+ this.abortController = new AbortController();
40
+ const response = await fetch(`${this.baseUrl}/chat/completions`, {
41
+ method: 'POST',
42
+ headers: {
43
+ 'Authorization': `Bearer ${this.config.apiKey}`,
44
+ 'Content-Type': 'application/json',
45
+ },
46
+ body: JSON.stringify({
47
+ model: options.model || this.config.defaultModel,
48
+ messages,
49
+ stream: true,
50
+ max_tokens: options.maxTokens || (this.config.tokenBudget === 'unlimited' ? 4096 : this.config.tokenBudget),
51
+ }),
52
+ signal: this.abortController.signal
53
+ });
54
+
55
+ if (!response.ok) {
56
+ const err = await response.text();
57
+ throw new Error(err);
58
+ }
59
+
60
+ const reader = response.body.getReader();
61
+ const decoder = new TextDecoder();
62
+ let full = '';
63
+ let buffer = '';
64
+
65
+ while (true) {
66
+ const { done, value } = await reader.read();
67
+ if (done) break;
68
+ buffer += decoder.decode(value, { stream: true });
69
+ const lines = buffer.split('\n');
70
+ buffer = lines.pop() || '';
71
+
72
+ for (const line of lines) {
73
+ const trimmed = line.trim();
74
+ if (!trimmed || !trimmed.startsWith('data: ')) continue;
75
+ const data = trimmed.slice(6);
76
+ if (data === '[DONE]') continue;
77
+ try {
78
+ const parsed = JSON.parse(data);
79
+ const delta = parsed.choices?.[0]?.delta?.content || '';
80
+ if (delta && options.onToken) {
81
+ options.onToken(delta);
82
+ }
83
+ full += delta;
84
+ } catch {}
85
+ }
86
+ }
87
+ return full;
88
+ }
89
+ }
package/src/config.js ADDED
@@ -0,0 +1,50 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+
5
+ const CONFIG_DIR = path.join(os.homedir(), '.lumina');
6
+ const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
7
+
8
+ const DEFAULTS = {
9
+ provider: null,
10
+ apiKey: null,
11
+ baseUrl: null,
12
+ defaultModel: null,
13
+ availableModels: [],
14
+ persona: 'general',
15
+ tokenBudget: 256,
16
+ firstRun: true,
17
+ version: '1.0.0'
18
+ };
19
+
20
+ export function getConfigPath() {
21
+ return CONFIG_PATH;
22
+ }
23
+
24
+ export function getConfigDir() {
25
+ return CONFIG_DIR;
26
+ }
27
+
28
+ export function loadConfig() {
29
+ try {
30
+ if (fs.existsSync(CONFIG_PATH)) {
31
+ const raw = fs.readFileSync(CONFIG_PATH, 'utf-8');
32
+ return { ...DEFAULTS, ...JSON.parse(raw) };
33
+ }
34
+ } catch {}
35
+ return { ...DEFAULTS };
36
+ }
37
+
38
+ export function saveConfig(config) {
39
+ if (!fs.existsSync(CONFIG_DIR)) {
40
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
41
+ }
42
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
43
+ try {
44
+ fs.chmodSync(CONFIG_PATH, 0o600);
45
+ } catch {}
46
+ }
47
+
48
+ export function configExists() {
49
+ return fs.existsSync(CONFIG_PATH);
50
+ }