lumina-code-agent 1.2.0 → 1.4.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.
Files changed (2) hide show
  1. package/dist/index.js +302 -54
  2. package/package.json +3 -7
package/dist/index.js CHANGED
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  // @ts-nocheck
3
- import { Command } from 'commander';
4
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
3
+ import React, { useState, useCallback, useRef } from 'react';
4
+ import { Box, Text, useInput, useApp, Spacer } from 'ink';
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'fs';
5
6
  import { homedir } from 'os';
6
- import { join } from 'path';
7
+ import { join, dirname, resolve, relative } from 'path';
7
8
  const CONFIG_DIR = join(homedir(), '.lumina');
8
9
  const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
9
- async function loadConfig() {
10
+ function loadConfig() {
10
11
  try {
11
12
  if (!existsSync(CONFIG_FILE))
12
13
  return null;
@@ -16,57 +17,304 @@ async function loadConfig() {
16
17
  return null;
17
18
  }
18
19
  }
19
- async function ensureConfig() {
20
+ function saveConfig(config) {
20
21
  if (!existsSync(CONFIG_DIR))
21
22
  mkdirSync(CONFIG_DIR, { recursive: true });
22
- const existing = await loadConfig();
23
- if (existing)
24
- return existing;
25
- const defaults = { openrouterKey: '', defaultEffort: 'normal' };
26
- writeFileSync(CONFIG_FILE, JSON.stringify(defaults, null, 2));
27
- return defaults;
23
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
24
+ }
25
+ // ── Simple Text Input Component ─────────────────────────────────────
26
+ function PromptInput({ value, onChange, onSubmit, placeholder }) {
27
+ useInput((input, key) => {
28
+ if (key.return && onSubmit) {
29
+ onSubmit(value);
30
+ }
31
+ else if (key.backspace || key.delete) {
32
+ onChange(value.slice(0, -1));
33
+ }
34
+ else if (!key.ctrl && !key.meta && input) {
35
+ onChange(value + input);
36
+ }
37
+ });
38
+ return React.createElement(Box, null, React.createElement(Text, { color: '#7C5CFC' }, ' > '), React.createElement(Text, { color: '#FAFAFA' }, value || placeholder || 'Type something...'), React.createElement(Text, { color: '#52525B' }, '▌'));
39
+ }
40
+ // ── Onboarding Screen ───────────────────────────────────────────────
41
+ function Onboarding({ onComplete }) {
42
+ const [step, setStep] = useState(0);
43
+ const [apiKey, setApiKey] = useState('');
44
+ const [name, setName] = useState('');
45
+ const handleKey = useCallback((input, key) => {
46
+ if (key.return) {
47
+ if (step === 0) {
48
+ setStep(1);
49
+ }
50
+ else if (step === 1 && apiKey.trim().length > 10) {
51
+ setStep(2);
52
+ }
53
+ else if (step === 2) {
54
+ saveConfig({ openrouterKey: apiKey.trim(), userName: name.trim() || 'User' });
55
+ onComplete();
56
+ }
57
+ }
58
+ else if (key.backspace || key.delete) {
59
+ if (step === 1)
60
+ setApiKey(v => v.slice(0, -1));
61
+ if (step === 2)
62
+ setName(v => v.slice(0, -1));
63
+ }
64
+ else if (!key.ctrl && !key.meta && input) {
65
+ if (step === 1)
66
+ setApiKey(v => v + input);
67
+ if (step === 2)
68
+ setName(v => v + input);
69
+ }
70
+ }, [step, apiKey, name, onComplete]);
71
+ useInput(handleKey);
72
+ return React.createElement(Box, { flexDirection: 'column', padding: 2 }, React.createElement(Box, { flexDirection: 'column', marginBottom: 2 }, React.createElement(Text, { bold: true, color: '#7C5CFC' }, ' ⚡ LUMINA CODE'), React.createElement(Text, { color: '#52525B' }, ' AI Coding Agent')), step === 0 && React.createElement(Box, { flexDirection: 'column' }, React.createElement(Text, { color: '#A1A1AA' }, ' Welcome! Let\'s get you set up.'), React.createElement(Text, { color: '#52525B' }, ' Press Enter to continue...')), step === 1 && React.createElement(Box, { flexDirection: 'column' }, React.createElement(Text, { color: '#A1A1AA' }, ' Enter your OpenRouter API key:'), React.createElement(Text, { color: '#52525B' }, ' (Get one at https://openrouter.ai/keys)'), React.createElement(Box, { marginTop: 1 }, React.createElement(Text, { color: '#7C5CFC' }, ' > '), React.createElement(Text, { color: '#FAFAFA' }, apiKey || 'sk-or-...'), React.createElement(Text, { color: '#52525B' }, '▌')), React.createElement(Text, { color: '#3F3F46', marginTop: 1 }, ' Press Enter to continue')), step === 2 && React.createElement(Box, { flexDirection: 'column' }, React.createElement(Text, { color: '#A1A1AA' }, ' What should I call you? (optional)'), React.createElement(Box, { marginTop: 1 }, React.createElement(Text, { color: '#7C5CFC' }, ' > '), React.createElement(Text, { color: '#FAFAFA' }, name || 'Your name'), React.createElement(Text, { color: '#52525B' }, '▌')), React.createElement(Text, { color: '#3F3F46', marginTop: 1 }, ' Press Enter to start coding!')));
28
73
  }
29
- const program = new Command();
30
- program.name('lumina').description('Lumina Code AI coding agent').version('1.1.0');
31
- // Main command: lumina code
32
- program.command('code')
33
- .description('Start Lumina Code agent')
34
- .argument('[prompt]', 'What you want to build')
35
- .option('-e, --effort <level>', 'Effort level: quick, normal, beast', 'normal')
36
- .option('-y, --yes', 'Auto-approve all actions')
37
- .option('--cwd <dir>', 'Working directory', process.cwd())
38
- .action(async (prompt, opts) => {
39
- const config = await ensureConfig();
40
- if (!config.openrouterKey) {
41
- console.error('\n ERROR: OpenRouter API key not set.\n');
42
- console.error(' Set it with: lumina config set openrouter-key YOUR_KEY');
43
- console.error(' Get a key at: https://openrouter.ai/keys\n');
44
- process.exit(1);
74
+ // ── Chat Screen ─────────────────────────────────────────────────────
75
+ function ChatScreen({ config }) {
76
+ const { exit } = useApp();
77
+ const [messages, setMessages] = useState([]);
78
+ const [input, setInput] = useState('');
79
+ const [status, setStatus] = useState('Ready');
80
+ const [thinking, setThinking] = useState(false);
81
+ const scrollRef = useRef(0);
82
+ const visibleMessages = messages.slice(-50);
83
+ const handleSubmit = useCallback(async (text) => {
84
+ const trimmed = text.trim();
85
+ if (!trimmed || thinking)
86
+ return;
87
+ setInput('');
88
+ setThinking(true);
89
+ setStatus('Thinking...');
90
+ scrollRef.current++;
91
+ const userMsg = { role: 'user', content: trimmed };
92
+ const newMessages = [...messages, userMsg];
93
+ setMessages(newMessages);
94
+ try {
95
+ const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions';
96
+ const tools = [
97
+ { type: 'function', function: { name: 'run_command', description: 'Run any shell command', parameters: { type: 'object', properties: { command: { type: 'string' }, cwd: { type: 'string' }, timeout: { type: 'number' } }, required: ['command'] } } },
98
+ { type: 'function', function: { name: 'read_file', description: 'Read file contents. ALWAYS read before editing.', parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } } },
99
+ { type: 'function', function: { name: 'write_file', description: 'Create or overwrite a file. Creates directories automatically.', parameters: { type: 'object', properties: { path: { type: 'string' }, content: { type: 'string' } }, required: ['path', 'content'] } } },
100
+ { type: 'function', function: { name: 'edit_file', description: 'Make precise edits to a file. search/replace.', parameters: { type: 'object', properties: { path: { type: 'string' }, search: { type: 'string' }, replace: { type: 'string' } }, required: ['path', 'search', 'replace'] } } },
101
+ { type: 'function', function: { name: 'list_dir', description: 'List directory contents', parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } } },
102
+ { type: 'function', function: { name: 'search_files', description: 'Find files by glob pattern', parameters: { type: 'object', properties: { pattern: { type: 'string' }, cwd: { type: 'string' } }, required: ['pattern'] } } },
103
+ { type: 'function', function: { name: 'grep', description: 'Search file contents', parameters: { type: 'object', properties: { pattern: { type: 'string' }, path: { type: 'string' } }, required: ['pattern', 'path'] } } },
104
+ { type: 'function', function: { name: 'git', description: 'Run git commands', parameters: { type: 'object', properties: { args: { type: 'string' }, cwd: { type: 'string' } }, required: ['args'] } } },
105
+ { type: 'function', function: { name: 'npm', description: 'Run npm/yarn/pnpm/bun commands', parameters: { type: 'object', properties: { args: { type: 'string' }, cwd: { type: 'string' } }, required: ['args'] } } },
106
+ { type: 'function', function: { name: 'deploy', description: 'Deploy to Vercel', parameters: { type: 'object', properties: { target: { type: 'string' }, cwd: { type: 'string' } } } } },
107
+ ];
108
+ let iterations = 0;
109
+ const maxIterations = 30;
110
+ let currentMessages = [...newMessages];
111
+ while (iterations < maxIterations) {
112
+ iterations++;
113
+ setStatus(`Working (step ${iterations})...`);
114
+ const res = await fetch(OPENROUTER_URL, {
115
+ method: 'POST',
116
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${config.openrouterKey}`, 'HTTP-Referer': 'https://luminaai.co.in', 'X-Title': 'Lumina Code' },
117
+ body: JSON.stringify({
118
+ model: 'openrouter/owl-alpha',
119
+ messages: [{ role: 'system', content: `You are LUMINA CODE — an elite AI coding agent.
120
+
121
+ MODEL: openrouter/owl-alpha (1M+ context, best reasoning)
122
+
123
+ WORKFLOW:
124
+ 1. PLAN: Analyze the task. Create a brief plan.
125
+ 2. ACT: Execute tools step by step. Read before writing.
126
+ 3. VERIFY: Run builds, check for errors.
127
+ 4. FIX: If something fails, debug and fix immediately.
128
+ 5. DEPLOY: If requested, deploy automatically.
129
+
130
+ QUALITY STANDARDS:
131
+ - Production-grade code, always
132
+ - TypeScript with proper types (never use 'any')
133
+ - Error handling everywhere
134
+ - Responsive design (320px to 2560px)
135
+ - Accessible (semantic HTML, ARIA, keyboard navigation)
136
+ - Clean architecture, modern patterns
137
+ - Beautiful UI (consistent spacing, typography, color)
138
+
139
+ FORBIDDEN:
140
+ - Lorem ipsum or placeholder content
141
+ - TODO/FIXME comments in production code
142
+ - Emoji in code or UI
143
+ - var keyword (always let/const)
144
+ - any type in TypeScript
145
+ - Skipping error handling
146
+ - Hardcoded secrets
147
+
148
+ When using a tool, output ONLY:
149
+ TOOL: <name>
150
+ PARAMS: <json>
151
+
152
+ Working directory: ${process.cwd()}` }, ...currentMessages],
153
+ tools,
154
+ stream: false,
155
+ max_tokens: 32000,
156
+ temperature: 0.1,
157
+ }),
158
+ });
159
+ if (!res.ok) {
160
+ const err = await res.text().catch(() => '');
161
+ throw new Error(`API error ${res.status}: ${err.slice(0, 200)}`);
162
+ }
163
+ const data = await res.json();
164
+ const choice = data.choices?.[0];
165
+ if (!choice)
166
+ throw new Error('No response from model');
167
+ const content = choice.message?.content || '';
168
+ const toolCalls = choice.message?.tool_calls || [];
169
+ currentMessages.push({ role: 'assistant', content });
170
+ setMessages([...currentMessages]);
171
+ scrollRef.current++;
172
+ if (toolCalls.length === 0) {
173
+ setStatus('Done');
174
+ break;
175
+ }
176
+ // Execute tools
177
+ for (const tc of toolCalls) {
178
+ const args = JSON.parse(tc.function.arguments || '{}');
179
+ const toolName = tc.function.name;
180
+ setStatus(`Running: ${toolName}...`);
181
+ scrollRef.current++;
182
+ let output = '';
183
+ try {
184
+ const { execSync } = await import('child_process');
185
+ switch (toolName) {
186
+ case 'run_command': {
187
+ const cwd = args.cwd || process.cwd();
188
+ output = execSync(args.command, { cwd, encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024, timeout: args.timeout || 120000, shell: true }) || '(no output)';
189
+ break;
190
+ }
191
+ case 'read_file': {
192
+ output = readFileSync(args.path, 'utf-8');
193
+ break;
194
+ }
195
+ case 'write_file': {
196
+ const dir = dirname(resolve(args.path));
197
+ if (!existsSync(dir))
198
+ mkdirSync(dir, { recursive: true });
199
+ writeFileSync(args.path, args.content, 'utf-8');
200
+ output = `Wrote ${args.content.length} chars to ${args.path}`;
201
+ break;
202
+ }
203
+ case 'edit_file': {
204
+ let fileContent = readFileSync(args.path, 'utf-8');
205
+ if (!fileContent.includes(args.search))
206
+ throw new Error(`String not found: "${args.search.slice(0, 50)}"`);
207
+ fileContent = fileContent.replace(args.search, args.replace);
208
+ writeFileSync(args.path, fileContent, 'utf-8');
209
+ output = `Edited ${args.path}`;
210
+ break;
211
+ }
212
+ case 'list_dir': {
213
+ const entries = readdirSync(args.path, { withFileTypes: true });
214
+ output = entries.map(e => `${e.isDirectory() ? '📁' : '📄'} ${e.name}`).join('\n');
215
+ break;
216
+ }
217
+ case 'search_files': {
218
+ const results = [];
219
+ const search = (dir, depth) => {
220
+ if (depth > 5)
221
+ return;
222
+ try {
223
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
224
+ if (e.name.startsWith('.') || e.name === 'node_modules' || e.name === 'dist')
225
+ continue;
226
+ const fp = join(dir, e.name);
227
+ if (e.isDirectory())
228
+ search(fp, depth + 1);
229
+ else if (new RegExp(args.pattern.replace(/\*/g, '.*'), 'i').test(e.name))
230
+ results.push(relative(args.cwd || process.cwd(), fp));
231
+ }
232
+ }
233
+ catch { }
234
+ };
235
+ search(args.cwd || process.cwd(), 0);
236
+ output = results.join('\n') || 'No files found';
237
+ break;
238
+ }
239
+ case 'grep': {
240
+ const content = readFileSync(args.path, 'utf-8');
241
+ const lines = content.split('\n');
242
+ const regex = new RegExp(args.pattern, 'gi');
243
+ output = lines.map((l, i) => regex.test(l) ? `${i + 1}: ${l}` : null).filter(Boolean).join('\n') || 'No matches';
244
+ break;
245
+ }
246
+ case 'git': {
247
+ output = execSync(`git ${args.args}`, { cwd: args.cwd || process.cwd(), encoding: 'utf-8', maxBuffer: 1024 * 1024 }) || '(ok)';
248
+ break;
249
+ }
250
+ case 'npm': {
251
+ const pm = existsSync(join(args.cwd || process.cwd(), 'bun.lockb')) ? 'bun' : existsSync(join(args.cwd || process.cwd(), 'yarn.lock')) ? 'yarn' : 'npm';
252
+ output = execSync(`${pm} ${args.args}`, { cwd: args.cwd || process.cwd(), encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024, timeout: 120000 }) || '(ok)';
253
+ break;
254
+ }
255
+ case 'deploy': {
256
+ output = execSync('npx vercel deploy --prod --yes', { cwd: args.cwd || process.cwd(), encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024, timeout: 300000 }) || '(deployed)';
257
+ break;
258
+ }
259
+ default:
260
+ output = `Unknown tool: ${toolName}`;
261
+ }
262
+ }
263
+ catch (e) {
264
+ output = `Error: ${e.message}`;
265
+ }
266
+ currentMessages.push({ role: 'tool', content: output.slice(0, 2000), tool_call_id: tc.id });
267
+ setMessages([...currentMessages]);
268
+ scrollRef.current++;
269
+ }
270
+ }
271
+ if (iterations >= maxIterations)
272
+ setStatus('Reached max iterations');
273
+ }
274
+ catch (e) {
275
+ setMessages(prev => [...prev, { role: 'assistant', content: `⚠ Error: ${e.message}` }]);
276
+ setStatus('Error');
277
+ }
278
+ setThinking(false);
279
+ }, [input, thinking, messages, config]);
280
+ useInput((input, key) => {
281
+ if (key.ctrl && input === 'c')
282
+ exit();
283
+ if (key.ctrl && input === 'd')
284
+ exit();
285
+ if (key.return) {
286
+ handleSubmit(input);
287
+ }
288
+ else if (key.backspace || key.delete) {
289
+ setInput(v => v.slice(0, -1));
290
+ }
291
+ else if (!key.ctrl && !key.meta) {
292
+ setInput(v => v + input);
293
+ }
294
+ });
295
+ return React.createElement(Box, { flexDirection: 'column', height: '100%' },
296
+ // Header
297
+ React.createElement(Box, { borderStyle: 'round', borderColor: '#7C5CFC', paddingX: 2, paddingY: 1 }, React.createElement(Text, { bold: true, color: '#7C5CFC' }, ' ⚡ LUMINA CODE'), React.createElement(Spacer, null), React.createElement(Text, { color: '#52525B' }, thinking ? ' ⏳ ' + status : ' ● ' + status)),
298
+ // Messages
299
+ React.createElement(Box, { flexDirection: 'column', flexGrow: 1, paddingX: 2, paddingY: 1, overflowY: 'hidden' }, visibleMessages.map((m, i) => {
300
+ if (m.role === 'user') {
301
+ return React.createElement(Box, { key: i, marginTop: 1 }, React.createElement(Text, { color: '#7C5CFC', bold: true }, '> '), React.createElement(Text, { color: '#FAFAFA' }, m.content));
302
+ }
303
+ if (m.role === 'tool') {
304
+ return React.createElement(Box, { key: i, marginTop: 0, paddingLeft: 2 }, React.createElement(Text, { color: '#52525B' }, ' ' + m.content.slice(0, 200)));
305
+ }
306
+ return React.createElement(Box, { key: i, marginTop: 1, paddingLeft: 2 }, React.createElement(Text, { color: '#A1A1AA' }, m.content.slice(0, 500)));
307
+ }), thinking && React.createElement(Text, { color: '#F59E0B' }, ' ⏳ thinking...')),
308
+ // Input
309
+ React.createElement(Box, { borderStyle: 'single', borderColor: '#3F3F46', paddingX: 1 }, React.createElement(Text, { color: '#7C5CFC' }, ' > '), React.createElement(Text, { color: '#FAFAFA' }, input || 'What do you want to build?'), React.createElement(Text, { color: '#52525B' }, '▌')),
310
+ // Footer
311
+ React.createElement(Box, { paddingX: 2 }, React.createElement(Text, { color: '#3F3F46' }, ' Ctrl+C to exit | OWL-Alpha ')));
312
+ }
313
+ // ── Main App ────────────────────────────────────────────────────────
314
+ export default function App() {
315
+ const config = loadConfig();
316
+ if (!config?.openrouterKey) {
317
+ return React.createElement(Onboarding, { onComplete: () => { } });
45
318
  }
46
- const effort = ['quick', 'normal', 'beast'].includes(opts.effort) ? opts.effort : 'normal';
47
- const { TUIApp } = await import('./tui/index.js');
48
- const { render } = await import('ink');
49
- const React = await import('react');
50
- render(React.createElement(TUIApp, { prompt, config, effort, autoApprove: opts.yes || false, cwd: opts.cwd }));
51
- });
52
- // Config commands: lumina config, lumina config set <key> <value>
53
- const configCmd = program.command('config').description('Manage configuration');
54
- configCmd.action(async () => {
55
- const config = await ensureConfig();
56
- console.log('\n Lumina Code Configuration\n');
57
- console.log(' Config:', CONFIG_FILE);
58
- console.log(' API Key:', config.openrouterKey ? 'Set (' + config.openrouterKey.slice(0, 8) + '...)' : 'NOT SET');
59
- console.log(' Default Effort:', config.defaultEffort || 'normal');
60
- console.log('\n Commands:');
61
- console.log(' lumina config set openrouter-key <key>');
62
- console.log(' lumina config set default-effort <quick|normal|beast>\n');
63
- });
64
- configCmd.command('set <key> <value>')
65
- .description('Set a config value')
66
- .action(async (key, value) => {
67
- const config = await ensureConfig();
68
- config[key] = value;
69
- writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
70
- console.log(' Set', key, '=', value);
71
- });
72
- program.parse();
319
+ return React.createElement(ChatScreen, { config });
320
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lumina-code-agent",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Lumina Code - AI coding agent",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -10,16 +10,12 @@
10
10
  "engines": { "node": ">=18.0.0" },
11
11
  "scripts": { "build": "tsc --noEmit false", "prepublishOnly": "npm run build" },
12
12
  "dependencies": {
13
- "commander": "^12.1.0",
14
13
  "ink": "^5.1.0",
15
14
  "react": "^18.3.1",
16
- "chalk": "^5.3.0",
17
- "cross-spawn": "^7.0.3",
18
- "node-fetch": "^3.3.2"
15
+ "chalk": "^5.3.0"
19
16
  },
20
17
  "devDependencies": {
21
- "@types/node": "^22.10.0",
22
- "@types/cross-spawn": "^6.0.6",
18
+ "@types/react": "^18.3.12",
23
19
  "typescript": "^5.7.2"
24
20
  }
25
21
  }