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