lumina-code-agent 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 +87 -0
- package/dist/agent.d.ts +37 -0
- package/dist/agent.js +311 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +77 -0
- package/dist/models/openrouter.d.ts +40 -0
- package/dist/models/openrouter.js +93 -0
- package/dist/tools/index.d.ts +22 -0
- package/dist/tools/index.js +293 -0
- package/dist/tui/index.d.ts +12 -0
- package/dist/tui/index.js +115 -0
- package/dist/utils/config.d.ts +10 -0
- package/dist/utils/config.js +44 -0
- package/package.json +19 -0
package/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Lumina Code - AI Coding Agent
|
|
2
|
+
|
|
3
|
+
The most powerful AI coding agent. Runs locally on your machine. Full filesystem access. Multi-model intelligence. Beautiful TUI.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
### macOS / Linux
|
|
8
|
+
```bash
|
|
9
|
+
curl -fsSL https://luminaai.co.in/install.sh | bash
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
### Windows (PowerShell)
|
|
13
|
+
```powershell
|
|
14
|
+
irm https://luminaai.co.in/install.ps1 | iex
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### npm (any platform)
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g lumina-agent
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### bun (any platform)
|
|
23
|
+
```bash
|
|
24
|
+
bun install -g lumina-agent
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# 1. Set your OpenRouter API key
|
|
31
|
+
lumina config set openrouter-key YOUR_KEY
|
|
32
|
+
|
|
33
|
+
# 2. Start building
|
|
34
|
+
lumina code "Build me a React app with Tailwind"
|
|
35
|
+
|
|
36
|
+
# 3. Or start interactive mode
|
|
37
|
+
lumina code
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Commands
|
|
41
|
+
|
|
42
|
+
| Command | Description |
|
|
43
|
+
|---------|-------------|
|
|
44
|
+
| `lumina code [prompt]` | Start Lumina Code agent |
|
|
45
|
+
| `lumina code -y [prompt]` | Auto-approve all actions |
|
|
46
|
+
| `lumina code -m <model> [prompt]` | Use specific model |
|
|
47
|
+
| `lumina config` | Show configuration |
|
|
48
|
+
| `lumina config set <key> <value>` | Set config value |
|
|
49
|
+
|
|
50
|
+
## Models
|
|
51
|
+
|
|
52
|
+
| Model | Use Case |
|
|
53
|
+
|-------|----------|
|
|
54
|
+
| `openrouter/owl-alpha` | Planning & reasoning (default) |
|
|
55
|
+
| `moonshotai/kimi-k2.6` | Code generation |
|
|
56
|
+
| `openai/gpt-oss-20b:free` | Fast tasks |
|
|
57
|
+
|
|
58
|
+
## Agent Tools
|
|
59
|
+
|
|
60
|
+
- **bash** - Run any shell command
|
|
61
|
+
- **read_file** - Read file contents
|
|
62
|
+
- **write_file** - Create or overwrite files
|
|
63
|
+
- **edit_file** - Make precise edits
|
|
64
|
+
- **list_dir** - List directory contents
|
|
65
|
+
- **search_files** - Find files by pattern
|
|
66
|
+
- **grep** - Search file contents
|
|
67
|
+
- **git** - Git operations
|
|
68
|
+
- **npm** - Package management
|
|
69
|
+
- **deploy** - Deploy to Vercel
|
|
70
|
+
- **ask** - Ask user for input
|
|
71
|
+
|
|
72
|
+
## Configuration
|
|
73
|
+
|
|
74
|
+
Config stored at `~/.lumina/config.json`:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"openrouterKey": "sk-or-...",
|
|
79
|
+
"defaultModel": "openrouter/owl-alpha",
|
|
80
|
+
"codingModel": "moonshotai/kimi-k2.6",
|
|
81
|
+
"fastModel": "openai/gpt-oss-20b:free"
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Get API Key
|
|
86
|
+
|
|
87
|
+
Get your OpenRouter API key at: https://openrouter.ai/keys
|
package/dist/agent.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { LuminaConfig } from '../utils/config';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
export type AgentEvent = {
|
|
4
|
+
type: 'thinking';
|
|
5
|
+
} | {
|
|
6
|
+
type: 'text';
|
|
7
|
+
content: string;
|
|
8
|
+
} | {
|
|
9
|
+
type: 'tool_start';
|
|
10
|
+
tool: string;
|
|
11
|
+
args: string;
|
|
12
|
+
} | {
|
|
13
|
+
type: 'tool_end';
|
|
14
|
+
tool: string;
|
|
15
|
+
output: string;
|
|
16
|
+
} | {
|
|
17
|
+
type: 'error';
|
|
18
|
+
message: string;
|
|
19
|
+
} | {
|
|
20
|
+
type: 'done';
|
|
21
|
+
summary: string;
|
|
22
|
+
} | {
|
|
23
|
+
type: 'ask';
|
|
24
|
+
question: string;
|
|
25
|
+
resolve: (answer: string) => void;
|
|
26
|
+
};
|
|
27
|
+
export declare class Agent extends EventEmitter {
|
|
28
|
+
private messages;
|
|
29
|
+
private config;
|
|
30
|
+
private model;
|
|
31
|
+
private cwd;
|
|
32
|
+
private autoApprove;
|
|
33
|
+
private totalTokens;
|
|
34
|
+
constructor(config: LuminaConfig, model: string, cwd: string, autoApprove: boolean);
|
|
35
|
+
run(prompt: string): Promise<string>;
|
|
36
|
+
private executeTool;
|
|
37
|
+
}
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { callModel } from '../models/openrouter.js';
|
|
3
|
+
import * as tools from '../tools/index.js';
|
|
4
|
+
import { EventEmitter } from 'events';
|
|
5
|
+
const SYSTEM_PROMPT = `You are LUMINA CODE — an elite AI coding agent running locally on the user's machine. You have full access to the filesystem, terminal, git, npm, and deployment tools.
|
|
6
|
+
|
|
7
|
+
## YOUR CAPABILITIES
|
|
8
|
+
- Read, write, edit any file
|
|
9
|
+
- Run any shell command
|
|
10
|
+
- Install packages, run builds
|
|
11
|
+
- Git operations (commit, push, branch)
|
|
12
|
+
- Deploy to Vercel
|
|
13
|
+
- Search files and code
|
|
14
|
+
- Spawn sub-agents for parallel work
|
|
15
|
+
|
|
16
|
+
## HOW YOU WORK
|
|
17
|
+
1. **PLAN**: Analyze the task. Create a step-by-step plan. Think about file structure, dependencies, edge cases.
|
|
18
|
+
2. **ACT**: Execute tools one at a time. Read files before editing. Check if packages exist before installing.
|
|
19
|
+
3. **VALIDATE**: After each step, verify it worked. Run builds. Check for errors.
|
|
20
|
+
4. **ITERATE**: If something fails, debug and fix. Don't give up after one attempt.
|
|
21
|
+
5. **COMPLETE**: When done, summarize what was built and how to use it.
|
|
22
|
+
|
|
23
|
+
## RULES
|
|
24
|
+
- Always read a file before editing it
|
|
25
|
+
- Run \`npm install\` before importing packages
|
|
26
|
+
- Test builds after making changes
|
|
27
|
+
- Use \`git status\` before committing
|
|
28
|
+
- Ask before destructive operations (rm -rf, git push --force, deploy)
|
|
29
|
+
- Create beautiful, production-quality code
|
|
30
|
+
- Use modern best practices (TypeScript, ES modules, etc.)
|
|
31
|
+
- Write clean, well-commented code
|
|
32
|
+
- Handle errors gracefully
|
|
33
|
+
|
|
34
|
+
## OUTPUT FORMAT
|
|
35
|
+
When you need to use a tool, output ONLY the tool call in this format:
|
|
36
|
+
\`\`\`
|
|
37
|
+
TOOL: <tool_name>
|
|
38
|
+
PARAMS: <json_params>
|
|
39
|
+
\`\`\`
|
|
40
|
+
|
|
41
|
+
Example:
|
|
42
|
+
\`\`\`
|
|
43
|
+
TOOL: write_file
|
|
44
|
+
PARAMS: {"path": "src/App.tsx", "content": "..."}
|
|
45
|
+
\`\`\`
|
|
46
|
+
|
|
47
|
+
After the tool executes, you'll see the result. Then continue with the next step.
|
|
48
|
+
|
|
49
|
+
## QUALITY STANDARDS
|
|
50
|
+
- Every file you create should be production-ready
|
|
51
|
+
- Use proper TypeScript types
|
|
52
|
+
- Handle edge cases
|
|
53
|
+
- Write accessible, responsive UI
|
|
54
|
+
- Follow the project's existing code style
|
|
55
|
+
- Use the project's existing dependencies when possible
|
|
56
|
+
|
|
57
|
+
Now let's build something amazing.`;
|
|
58
|
+
const TOOL_DEFINITIONS = [
|
|
59
|
+
{
|
|
60
|
+
type: 'function',
|
|
61
|
+
function: {
|
|
62
|
+
name: 'bash',
|
|
63
|
+
description: 'Run a shell command. Use for any terminal operation.',
|
|
64
|
+
parameters: {
|
|
65
|
+
type: 'object',
|
|
66
|
+
properties: {
|
|
67
|
+
command: { type: 'string', description: 'The command to run' },
|
|
68
|
+
},
|
|
69
|
+
required: ['command'],
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
type: 'function',
|
|
75
|
+
function: {
|
|
76
|
+
name: 'read_file',
|
|
77
|
+
description: 'Read the contents of a file.',
|
|
78
|
+
parameters: {
|
|
79
|
+
type: 'object',
|
|
80
|
+
properties: {
|
|
81
|
+
path: { type: 'string', description: 'File path to read' },
|
|
82
|
+
},
|
|
83
|
+
required: ['path'],
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
type: 'function',
|
|
89
|
+
function: {
|
|
90
|
+
name: 'write_file',
|
|
91
|
+
description: 'Write content to a file. Creates the file if it doesn\'t exist, overwrites if it does.',
|
|
92
|
+
parameters: {
|
|
93
|
+
type: 'object',
|
|
94
|
+
properties: {
|
|
95
|
+
path: { type: 'string', description: 'File path to write' },
|
|
96
|
+
content: { type: 'string', description: 'File content' },
|
|
97
|
+
},
|
|
98
|
+
required: ['path', 'content'],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
type: 'function',
|
|
104
|
+
function: {
|
|
105
|
+
name: 'edit_file',
|
|
106
|
+
description: 'Edit a file by replacing exact text.',
|
|
107
|
+
parameters: {
|
|
108
|
+
type: 'object',
|
|
109
|
+
properties: {
|
|
110
|
+
path: { type: 'string', description: 'File path' },
|
|
111
|
+
old_string: { type: 'string', description: 'Exact text to find' },
|
|
112
|
+
new_string: { type: 'string', description: 'Replacement text' },
|
|
113
|
+
},
|
|
114
|
+
required: ['path', 'old_string', 'new_string'],
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
type: 'function',
|
|
120
|
+
function: {
|
|
121
|
+
name: 'list_dir',
|
|
122
|
+
description: 'List files and directories.',
|
|
123
|
+
parameters: {
|
|
124
|
+
type: 'object',
|
|
125
|
+
properties: {
|
|
126
|
+
path: { type: 'string', description: 'Directory path' },
|
|
127
|
+
},
|
|
128
|
+
required: ['path'],
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
type: 'function',
|
|
134
|
+
function: {
|
|
135
|
+
name: 'search_files',
|
|
136
|
+
description: 'Search for files by name pattern.',
|
|
137
|
+
parameters: {
|
|
138
|
+
type: 'object',
|
|
139
|
+
properties: {
|
|
140
|
+
pattern: { type: 'string', description: 'Glob pattern (e.g. "**/*.ts")' },
|
|
141
|
+
},
|
|
142
|
+
required: ['pattern'],
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
type: 'function',
|
|
148
|
+
function: {
|
|
149
|
+
name: 'grep',
|
|
150
|
+
description: 'Search for text within files.',
|
|
151
|
+
parameters: {
|
|
152
|
+
type: 'object',
|
|
153
|
+
properties: {
|
|
154
|
+
pattern: { type: 'string', description: 'Search pattern (regex)' },
|
|
155
|
+
path: { type: 'string', description: 'File or directory to search' },
|
|
156
|
+
},
|
|
157
|
+
required: ['pattern', 'path'],
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
type: 'function',
|
|
163
|
+
function: {
|
|
164
|
+
name: 'git',
|
|
165
|
+
description: 'Run git commands.',
|
|
166
|
+
parameters: {
|
|
167
|
+
type: 'object',
|
|
168
|
+
properties: {
|
|
169
|
+
args: { type: 'string', description: 'Git command arguments (e.g. "status", "add .")' },
|
|
170
|
+
},
|
|
171
|
+
required: ['args'],
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
type: 'function',
|
|
177
|
+
function: {
|
|
178
|
+
name: 'npm',
|
|
179
|
+
description: 'Run npm commands.',
|
|
180
|
+
parameters: {
|
|
181
|
+
type: 'object',
|
|
182
|
+
properties: {
|
|
183
|
+
args: { type: 'string', description: 'Npm command arguments (e.g. "install", "run build")' },
|
|
184
|
+
},
|
|
185
|
+
required: ['args'],
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
type: 'function',
|
|
191
|
+
function: {
|
|
192
|
+
name: 'deploy',
|
|
193
|
+
description: 'Deploy to Vercel.',
|
|
194
|
+
parameters: {
|
|
195
|
+
type: 'object',
|
|
196
|
+
properties: {
|
|
197
|
+
target: { type: 'string', description: 'Deployment target (default: "vercel")' },
|
|
198
|
+
},
|
|
199
|
+
required: [],
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
type: 'function',
|
|
205
|
+
function: {
|
|
206
|
+
name: 'ask',
|
|
207
|
+
description: 'Ask the user for permission or input.',
|
|
208
|
+
parameters: {
|
|
209
|
+
type: 'object',
|
|
210
|
+
properties: {
|
|
211
|
+
question: { type: 'string', description: 'Question to ask' },
|
|
212
|
+
},
|
|
213
|
+
required: ['question'],
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
];
|
|
218
|
+
export class Agent extends EventEmitter {
|
|
219
|
+
messages = [];
|
|
220
|
+
config;
|
|
221
|
+
model;
|
|
222
|
+
cwd;
|
|
223
|
+
autoApprove;
|
|
224
|
+
totalTokens = 0;
|
|
225
|
+
constructor(config, model, cwd, autoApprove) {
|
|
226
|
+
super();
|
|
227
|
+
this.config = config;
|
|
228
|
+
this.model = model;
|
|
229
|
+
this.cwd = cwd;
|
|
230
|
+
this.autoApprove = autoApprove;
|
|
231
|
+
this.messages = [{ role: 'system', content: SYSTEM_PROMPT }];
|
|
232
|
+
}
|
|
233
|
+
async run(prompt) {
|
|
234
|
+
this.messages.push({ role: 'user', content: prompt });
|
|
235
|
+
this.emit('event', { type: 'thinking' });
|
|
236
|
+
let iterations = 0;
|
|
237
|
+
const maxIterations = 50;
|
|
238
|
+
while (iterations < maxIterations) {
|
|
239
|
+
iterations++;
|
|
240
|
+
const { content, toolCalls, tokens } = await callModel(this.config.openrouterKey, this.model, this.messages, TOOL_DEFINITIONS, (chunk) => this.emit('event', { type: 'text', content: chunk }));
|
|
241
|
+
this.totalTokens += tokens;
|
|
242
|
+
this.messages.push({ role: 'assistant', content });
|
|
243
|
+
if (toolCalls.length === 0) {
|
|
244
|
+
// No more tools to call, we're done
|
|
245
|
+
this.emit('event', { type: 'done', summary: content });
|
|
246
|
+
return content;
|
|
247
|
+
}
|
|
248
|
+
// Execute each tool call
|
|
249
|
+
for (const tc of toolCalls) {
|
|
250
|
+
const args = JSON.parse(tc.function.arguments || '{}');
|
|
251
|
+
this.emit('event', { type: 'tool_start', tool: tc.function.name, args: JSON.stringify(args) });
|
|
252
|
+
try {
|
|
253
|
+
const output = await this.executeTool(tc.function.name, args);
|
|
254
|
+
this.emit('event', { type: 'tool_end', tool: tc.function.name, output });
|
|
255
|
+
this.messages.push({
|
|
256
|
+
role: 'tool',
|
|
257
|
+
content: output,
|
|
258
|
+
tool_call_id: tc.id,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
catch (e) {
|
|
262
|
+
const errMsg = e.message || String(e);
|
|
263
|
+
this.emit('event', { type: 'error', message: errMsg });
|
|
264
|
+
this.messages.push({
|
|
265
|
+
role: 'tool',
|
|
266
|
+
content: `Error: ${errMsg}`,
|
|
267
|
+
tool_call_id: tc.id,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const summary = `Reached max iterations (${maxIterations}). Total tokens: ${this.totalTokens}`;
|
|
273
|
+
this.emit('event', { type: 'done', summary });
|
|
274
|
+
return summary;
|
|
275
|
+
}
|
|
276
|
+
async executeTool(name, args) {
|
|
277
|
+
switch (name) {
|
|
278
|
+
case 'bash':
|
|
279
|
+
return tools.runCommand(args.command, this.cwd).stdout;
|
|
280
|
+
case 'read_file':
|
|
281
|
+
return tools.readFile(args.path);
|
|
282
|
+
case 'write_file':
|
|
283
|
+
tools.writeFile(args.path, args.content);
|
|
284
|
+
return `Wrote ${args.content?.toString().length || 0} chars to ${args.path}`;
|
|
285
|
+
case 'edit_file':
|
|
286
|
+
tools.editFile(args.path, args.old_string, args.new_string);
|
|
287
|
+
return `Edited ${args.path}`;
|
|
288
|
+
case 'list_dir':
|
|
289
|
+
const entries = tools.listDir(args.path);
|
|
290
|
+
return entries.map(e => `${e.isDir ? '📁' : '📄'} ${e.name}${e.isDir ? '' : ` (${e.size}B)`}`).join('\n');
|
|
291
|
+
case 'search_files':
|
|
292
|
+
return tools.searchFiles(args.pattern, this.cwd).join('\n') || 'No files found';
|
|
293
|
+
case 'grep':
|
|
294
|
+
return tools.grep(args.pattern, args.path).join('\n') || 'No matches';
|
|
295
|
+
case 'git':
|
|
296
|
+
return tools.git(args.args, this.cwd);
|
|
297
|
+
case 'npm':
|
|
298
|
+
return tools.npm(args.args, this.cwd);
|
|
299
|
+
case 'deploy':
|
|
300
|
+
return tools.deployVercel(this.cwd);
|
|
301
|
+
case 'ask':
|
|
302
|
+
if (this.autoApprove)
|
|
303
|
+
return 'yes';
|
|
304
|
+
return new Promise((resolve) => {
|
|
305
|
+
this.emit('event', { type: 'ask', question: args.question, resolve });
|
|
306
|
+
});
|
|
307
|
+
default:
|
|
308
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
const CONFIG_DIR = join(homedir(), '.lumina');
|
|
7
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
8
|
+
async function loadConfig() {
|
|
9
|
+
try {
|
|
10
|
+
if (!existsSync(CONFIG_FILE))
|
|
11
|
+
return null;
|
|
12
|
+
return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'));
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function ensureConfig() {
|
|
19
|
+
if (!existsSync(CONFIG_DIR))
|
|
20
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
21
|
+
const existing = await loadConfig();
|
|
22
|
+
if (existing)
|
|
23
|
+
return existing;
|
|
24
|
+
const defaults = {
|
|
25
|
+
openrouterKey: '',
|
|
26
|
+
defaultModel: 'openrouter/owl-alpha',
|
|
27
|
+
codingModel: 'moonshotai/kimi-k2.6',
|
|
28
|
+
fastModel: 'openai/gpt-oss-20b:free',
|
|
29
|
+
};
|
|
30
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(defaults, null, 2));
|
|
31
|
+
return defaults;
|
|
32
|
+
}
|
|
33
|
+
const program = new Command();
|
|
34
|
+
program.name('lumina').description('Lumina Code - AI coding agent').version('1.0.0');
|
|
35
|
+
program.command('code')
|
|
36
|
+
.description('Start Lumina Code agent')
|
|
37
|
+
.argument('[prompt]', 'What you want to build')
|
|
38
|
+
.option('-m, --model <model>', 'Model to use')
|
|
39
|
+
.option('-y, --yes', 'Auto-approve all actions')
|
|
40
|
+
.option('--cwd <dir>', 'Working directory', process.cwd())
|
|
41
|
+
.action(async (prompt, opts) => {
|
|
42
|
+
const config = await ensureConfig();
|
|
43
|
+
if (!config.openrouterKey) {
|
|
44
|
+
console.error('\n ERROR: OpenRouter API key not set.\n');
|
|
45
|
+
console.error(' Set it with: lumina config set openrouter-key YOUR_KEY');
|
|
46
|
+
console.error(' Get a key at: https://openrouter.ai/keys\n');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
// Dynamic require to avoid ESM issues
|
|
50
|
+
const { TUIApp } = require('./tui/index');
|
|
51
|
+
const { render } = require('ink');
|
|
52
|
+
const React = require('react');
|
|
53
|
+
render(React.createElement(TUIApp, {
|
|
54
|
+
prompt,
|
|
55
|
+
config,
|
|
56
|
+
model: opts.model || config.defaultModel || 'openrouter/owl-alpha',
|
|
57
|
+
autoApprove: opts.yes || false,
|
|
58
|
+
cwd: opts.cwd,
|
|
59
|
+
}));
|
|
60
|
+
});
|
|
61
|
+
program.command('config').description('Show configuration').action(async () => {
|
|
62
|
+
const config = await ensureConfig();
|
|
63
|
+
console.log('\n Lumina Code Configuration\n');
|
|
64
|
+
console.log(' Config:', CONFIG_FILE);
|
|
65
|
+
console.log(' API Key:', config.openrouterKey ? 'Set (' + config.openrouterKey.slice(0, 8) + '...)' : 'NOT SET');
|
|
66
|
+
console.log(' Default:', config.defaultModel || 'openrouter/owl-alpha');
|
|
67
|
+
console.log(' Coding:', config.codingModel || 'moonshotai/kimi-k2.6');
|
|
68
|
+
console.log(' Fast:', config.fastModel || 'openai/gpt-oss-20b:free\n');
|
|
69
|
+
});
|
|
70
|
+
program.command('config set <key> <value>').description('Set config value').action(async (key, value) => {
|
|
71
|
+
const config = await ensureConfig();
|
|
72
|
+
const map = { 'openrouter-key': 'openrouterKey', 'default-model': 'defaultModel', 'coding-model': 'codingModel', 'fast-model': 'fastModel' };
|
|
73
|
+
config[map[key] || key] = value;
|
|
74
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
75
|
+
console.log(' Set', key, '=', value);
|
|
76
|
+
});
|
|
77
|
+
program.parse();
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface Message {
|
|
2
|
+
role: 'system' | 'user' | 'assistant' | 'tool';
|
|
3
|
+
content: string;
|
|
4
|
+
tool_call_id?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ToolCall {
|
|
7
|
+
id: string;
|
|
8
|
+
type: 'function';
|
|
9
|
+
function: {
|
|
10
|
+
name: string;
|
|
11
|
+
arguments: string;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export interface ModelResponse {
|
|
15
|
+
choices: Array<{
|
|
16
|
+
delta?: {
|
|
17
|
+
content?: string;
|
|
18
|
+
tool_calls?: Array<{
|
|
19
|
+
id?: string;
|
|
20
|
+
type?: 'function';
|
|
21
|
+
function?: {
|
|
22
|
+
name?: string;
|
|
23
|
+
arguments?: string;
|
|
24
|
+
};
|
|
25
|
+
}>;
|
|
26
|
+
};
|
|
27
|
+
message?: {
|
|
28
|
+
content?: string;
|
|
29
|
+
tool_calls?: ToolCall[];
|
|
30
|
+
};
|
|
31
|
+
}>;
|
|
32
|
+
usage?: {
|
|
33
|
+
total_tokens: number;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export declare function callModel(apiKey: string, model: string, messages: Message[], tools?: object[], onChunk?: (text: string) => void): Promise<{
|
|
37
|
+
content: string;
|
|
38
|
+
toolCalls: ToolCall[];
|
|
39
|
+
tokens: number;
|
|
40
|
+
}>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
|
2
|
+
export async function callModel(apiKey, model, messages, tools, onChunk) {
|
|
3
|
+
const headers = {
|
|
4
|
+
'Content-Type': 'application/json',
|
|
5
|
+
Authorization: `Bearer ${apiKey}`,
|
|
6
|
+
'HTTP-Referer': 'https://luminaai.co.in',
|
|
7
|
+
'X-Title': 'Lumina Code',
|
|
8
|
+
};
|
|
9
|
+
// Try primary key, fall back to env
|
|
10
|
+
const keys = [apiKey, ...(process.env.OPENROUTER_API_KEY ? [process.env.OPENROUTER_API_KEY] : [])].filter(Boolean);
|
|
11
|
+
for (const key of keys) {
|
|
12
|
+
try {
|
|
13
|
+
const res = await fetch(OPENROUTER_URL, {
|
|
14
|
+
method: 'POST',
|
|
15
|
+
headers: { ...headers, Authorization: `Bearer ${key}` },
|
|
16
|
+
body: JSON.stringify({
|
|
17
|
+
model,
|
|
18
|
+
messages: messages.map(m => {
|
|
19
|
+
if (m.role === 'tool') {
|
|
20
|
+
return { role: 'tool', content: m.content, tool_call_id: m.tool_call_id };
|
|
21
|
+
}
|
|
22
|
+
return { role: m.role, content: m.content };
|
|
23
|
+
}),
|
|
24
|
+
tools: tools || undefined,
|
|
25
|
+
stream: true,
|
|
26
|
+
max_tokens: 32000,
|
|
27
|
+
temperature: 0.3,
|
|
28
|
+
}),
|
|
29
|
+
});
|
|
30
|
+
if (!res.ok) {
|
|
31
|
+
const err = await res.text().catch(() => '');
|
|
32
|
+
console.error(`Model ${model} error ${res.status}: ${err.slice(0, 200)}`);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const reader = res.body?.getReader();
|
|
36
|
+
if (!reader)
|
|
37
|
+
continue;
|
|
38
|
+
const decoder = new TextDecoder();
|
|
39
|
+
let buf = '';
|
|
40
|
+
let content = '';
|
|
41
|
+
let toolCalls = [];
|
|
42
|
+
let tokens = 0;
|
|
43
|
+
while (true) {
|
|
44
|
+
const { done, value } = await reader.read();
|
|
45
|
+
if (done)
|
|
46
|
+
break;
|
|
47
|
+
buf += decoder.decode(value, { stream: true });
|
|
48
|
+
let nl;
|
|
49
|
+
while ((nl = buf.indexOf('\n')) !== -1) {
|
|
50
|
+
let line = buf.slice(0, nl);
|
|
51
|
+
buf = buf.slice(nl + 1);
|
|
52
|
+
if (line.endsWith('\r'))
|
|
53
|
+
line = line.slice(0, -1);
|
|
54
|
+
if (!line.startsWith('data: '))
|
|
55
|
+
continue;
|
|
56
|
+
const json = line.slice(6).trim();
|
|
57
|
+
if (json === '[DONE]')
|
|
58
|
+
continue;
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(json);
|
|
61
|
+
const delta = parsed.choices?.[0]?.delta;
|
|
62
|
+
if (delta?.content) {
|
|
63
|
+
content += delta.content;
|
|
64
|
+
onChunk?.(delta.content);
|
|
65
|
+
}
|
|
66
|
+
if (delta?.tool_calls) {
|
|
67
|
+
for (const tc of delta.tool_calls) {
|
|
68
|
+
const idx = toolCalls.length;
|
|
69
|
+
if (!toolCalls[idx]) {
|
|
70
|
+
toolCalls[idx] = { id: tc.id || '', type: 'function', function: { name: tc.function?.name || '', arguments: '' } };
|
|
71
|
+
}
|
|
72
|
+
if (tc.function?.arguments) {
|
|
73
|
+
toolCalls[idx].function.arguments += tc.function.arguments;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (parsed.usage?.total_tokens)
|
|
78
|
+
tokens = parsed.usage.total_tokens;
|
|
79
|
+
}
|
|
80
|
+
catch { /* skip malformed */ }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (content || toolCalls.length > 0) {
|
|
84
|
+
return { content, toolCalls, tokens };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
console.error(`Model ${model} failed:`, e);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
throw new Error('All models failed');
|
|
93
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare function getShell(): {
|
|
2
|
+
cmd: string;
|
|
3
|
+
args: string[];
|
|
4
|
+
};
|
|
5
|
+
export declare function runCommand(command: string, cwd: string): {
|
|
6
|
+
stdout: string;
|
|
7
|
+
stderr: string;
|
|
8
|
+
code: number;
|
|
9
|
+
};
|
|
10
|
+
export declare function readFile(path: string): string;
|
|
11
|
+
export declare function writeFile(path: string, content: string): void;
|
|
12
|
+
export declare function editFile(path: string, oldString: string, newString: string): void;
|
|
13
|
+
export declare function listDir(path: string): {
|
|
14
|
+
name: string;
|
|
15
|
+
isDir: boolean;
|
|
16
|
+
size: number;
|
|
17
|
+
}[];
|
|
18
|
+
export declare function searchFiles(pattern: string, cwd: string): string[];
|
|
19
|
+
export declare function grep(pattern: string, path: string): string[];
|
|
20
|
+
export declare function git(args: string, cwd: string): string;
|
|
21
|
+
export declare function npm(args: string, cwd: string): string;
|
|
22
|
+
export declare function deployVercel(cwd: string): string;
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* LUMINA CODE — The Ultimate AI Coding Agent
|
|
4
|
+
*
|
|
5
|
+
* Better than Claude Code. Better than Codex. Better than Cursor.
|
|
6
|
+
*
|
|
7
|
+
* Architecture:
|
|
8
|
+
* - Single model: openrouter/owl-alpha (best reasoning, 1M+ context)
|
|
9
|
+
* - Effort levels: quick, normal, beast
|
|
10
|
+
* - Full filesystem access
|
|
11
|
+
* - Multi-file operations
|
|
12
|
+
* - Self-healing code
|
|
13
|
+
* - Automatic deployment
|
|
14
|
+
*/
|
|
15
|
+
import { execSync, spawn } from 'child_process';
|
|
16
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync, readdirSync, unlinkSync, renameSync, copyFileSync } from 'fs';
|
|
17
|
+
import { join, dirname, resolve } from 'path';
|
|
18
|
+
// ── Shell Detection ─────────────────────────────────────────────────
|
|
19
|
+
export function getShell() {
|
|
20
|
+
if (process.platform === 'win32')
|
|
21
|
+
return process.env.COMSPEC || 'cmd.exe';
|
|
22
|
+
return process.env.SHELL || '/bin/bash';
|
|
23
|
+
}
|
|
24
|
+
export function detectOS() {
|
|
25
|
+
const os = process.platform === 'win32' ? 'windows' : process.platform === 'darwin' ? 'mac' : 'linux';
|
|
26
|
+
return { os, arch: process.arch };
|
|
27
|
+
}
|
|
28
|
+
// ── Command Execution ───────────────────────────────────────────────
|
|
29
|
+
export function runCommand(command, cwd, timeout = 120_000) {
|
|
30
|
+
try {
|
|
31
|
+
const result = execSync(command, {
|
|
32
|
+
cwd, encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024, timeout,
|
|
33
|
+
stdio: ['pipe', 'pipe', 'pipe'], shell: getShell(),
|
|
34
|
+
env: { ...process.env, PYTHONUNBUFFERED: '1', FORCE_COLOR: '0' },
|
|
35
|
+
});
|
|
36
|
+
return { stdout: result, stderr: '', code: 0 };
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
return { stdout: e.stdout?.toString() || '', stderr: e.stderr?.toString() || e.message || '', code: e.status || 1 };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function runCommandStreaming(command, cwd, onOutput) {
|
|
43
|
+
return new Promise((resolve) => {
|
|
44
|
+
const shell = getShell();
|
|
45
|
+
const child = spawn(command, { cwd, shell, stdio: ['pipe', 'pipe', 'pipe'], env: process.env });
|
|
46
|
+
child.stdout?.on('data', (d) => onOutput(d.toString()));
|
|
47
|
+
child.stderr?.on('data', (d) => onOutput(d.toString()));
|
|
48
|
+
child.on('close', (code) => resolve(code || 0));
|
|
49
|
+
child.on('error', () => resolve(1));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
// ── File Operations ─────────────────────────────────────────────────
|
|
53
|
+
export function readFile(path) {
|
|
54
|
+
if (!existsSync(path))
|
|
55
|
+
throw new Error('File not found: ' + path);
|
|
56
|
+
return readFileSync(path, 'utf-8');
|
|
57
|
+
}
|
|
58
|
+
export function writeFile(path, content) {
|
|
59
|
+
const dir = dirname(resolve(path));
|
|
60
|
+
if (!existsSync(dir))
|
|
61
|
+
mkdirSync(dir, { recursive: true });
|
|
62
|
+
writeFileSync(path, content, 'utf-8');
|
|
63
|
+
}
|
|
64
|
+
export function editFile(path, replacements) {
|
|
65
|
+
let content = readFile(path);
|
|
66
|
+
for (const { search, replace } of replacements) {
|
|
67
|
+
if (!content.includes(search))
|
|
68
|
+
throw new Error('String not found in ' + path + ': "' + search.slice(0, 80) + '"');
|
|
69
|
+
content = content.replace(search, replace);
|
|
70
|
+
}
|
|
71
|
+
writeFile(path, content);
|
|
72
|
+
}
|
|
73
|
+
export function deleteFile(path) {
|
|
74
|
+
if (existsSync(path))
|
|
75
|
+
unlinkSync(path);
|
|
76
|
+
}
|
|
77
|
+
export function moveFile(from, to) {
|
|
78
|
+
const dir = dirname(resolve(to));
|
|
79
|
+
if (!existsSync(dir))
|
|
80
|
+
mkdirSync(dir, { recursive: true });
|
|
81
|
+
renameSync(from, to);
|
|
82
|
+
}
|
|
83
|
+
export function copyFile(from, to) {
|
|
84
|
+
const dir = dirname(resolve(to));
|
|
85
|
+
if (!existsSync(dir))
|
|
86
|
+
mkdirSync(dir, { recursive: true });
|
|
87
|
+
copyFileSync(from, to);
|
|
88
|
+
}
|
|
89
|
+
export function listDir(path) {
|
|
90
|
+
if (!existsSync(path))
|
|
91
|
+
throw new Error('Directory not found: ' + path);
|
|
92
|
+
return readdirSync(path, { withFileTypes: true }).map(e => {
|
|
93
|
+
const p = join(path, e.name);
|
|
94
|
+
const stat = statSync(p);
|
|
95
|
+
return { name: e.name, isDir: e.isDirectory(), size: stat.size, modified: stat.mtimeMs };
|
|
96
|
+
});
|
|
97
|
+
}
|
|
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
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export function grep(pattern, path, context = 2) {
|
|
107
|
+
try {
|
|
108
|
+
const content = readFile(path);
|
|
109
|
+
const lines = content.split('\n');
|
|
110
|
+
const regex = new RegExp(pattern, 'gi');
|
|
111
|
+
const results = [];
|
|
112
|
+
lines.forEach((line, i) => {
|
|
113
|
+
if (regex.test(line)) {
|
|
114
|
+
const start = Math.max(0, i - context);
|
|
115
|
+
const end = Math.min(lines.length, i + context + 1);
|
|
116
|
+
results.push(lines.slice(start, end).map((l, j) => `${start + j + 1}: ${l}`).join('\n'));
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
return results;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export function getFileTree(path, depth = 3, prefix = '') {
|
|
126
|
+
if (depth === 0)
|
|
127
|
+
return '';
|
|
128
|
+
const entries = listDir(path).filter(e => !e.name.startsWith('.') && e.name !== 'node_modules' && e.name !== 'dist');
|
|
129
|
+
return entries.map((e, i) => {
|
|
130
|
+
const isLast = i === entries.length - 1;
|
|
131
|
+
const connector = isLast ? '└── ' : '├── ';
|
|
132
|
+
const childPrefix = isLast ? ' ' : '│ ';
|
|
133
|
+
let line = prefix + connector + e.name + (e.isDir ? '/' : ` (${formatSize(e.size)})`);
|
|
134
|
+
if (e.isDir)
|
|
135
|
+
line += '\n' + getFileTree(join(path, e.name), depth - 1, prefix + childPrefix);
|
|
136
|
+
return line;
|
|
137
|
+
}).join('\n');
|
|
138
|
+
}
|
|
139
|
+
function formatSize(bytes) {
|
|
140
|
+
if (bytes < 1024)
|
|
141
|
+
return bytes + 'B';
|
|
142
|
+
if (bytes < 1024 * 1024)
|
|
143
|
+
return (bytes / 1024).toFixed(1) + 'KB';
|
|
144
|
+
return (bytes / (1024 * 1024)).toFixed(1) + 'MB';
|
|
145
|
+
}
|
|
146
|
+
// ── Git Operations ──────────────────────────────────────────────────
|
|
147
|
+
export function git(args, cwd) {
|
|
148
|
+
const { stdout, stderr, code } = runCommand('git ' + args, cwd);
|
|
149
|
+
if (code !== 0)
|
|
150
|
+
throw new Error(stderr || ('git ' + args + ' failed'));
|
|
151
|
+
return stdout.trim();
|
|
152
|
+
}
|
|
153
|
+
export function gitStatus(cwd) { return git('status --short', cwd); }
|
|
154
|
+
export function gitDiff(cwd) { return git('diff', cwd); }
|
|
155
|
+
export function gitLog(cwd, n = 10) { return git('log --oneline -' + n, cwd); }
|
|
156
|
+
export function gitCommit(message, cwd) { return git('add -A && git commit -m "' + message.replace(/"/g, '\\"') + '"', cwd); }
|
|
157
|
+
export function gitPush(cwd) { return git('push', cwd); }
|
|
158
|
+
export function gitBranch(cwd) { return git('branch --show-current', cwd); }
|
|
159
|
+
export function gitCreateBranch(name, cwd) { return git('checkout -b ' + name, cwd); }
|
|
160
|
+
// ── Package Manager ─────────────────────────────────────────────────
|
|
161
|
+
export function detectPackageManager(cwd) {
|
|
162
|
+
if (existsSync(join(cwd, 'bun.lockb')) || existsSync(join(cwd, 'bun.lock')))
|
|
163
|
+
return 'bun';
|
|
164
|
+
if (existsSync(join(cwd, 'pnpm-lock.yaml')))
|
|
165
|
+
return 'pnpm';
|
|
166
|
+
if (existsSync(join(cwd, 'yarn.lock')))
|
|
167
|
+
return 'yarn';
|
|
168
|
+
return 'npm';
|
|
169
|
+
}
|
|
170
|
+
export function pkgInstall(packages, cwd, dev = false) {
|
|
171
|
+
const pm = detectPackageManager(cwd);
|
|
172
|
+
const cmd = pm === 'bun' ? 'bun add' : pm === 'yarn' ? 'yarn add' : pm === 'pnpm' ? 'pnpm add' : 'npm install';
|
|
173
|
+
const flag = dev ? (pm === 'yarn' ? ' -D' : ' --save-dev') : '';
|
|
174
|
+
return runCommand(`${cmd}${flag} ${packages.join(' ')}`, cwd).stdout;
|
|
175
|
+
}
|
|
176
|
+
export function pkgRun(script, cwd) {
|
|
177
|
+
const pm = detectPackageManager(cwd);
|
|
178
|
+
const cmd = pm === 'bun' ? 'bun run' : pm === 'yarn' ? 'yarn' : pm === 'pnpm' ? 'pnpm' : 'npm run';
|
|
179
|
+
return runCommand(`${cmd} ${script}`, cwd).stdout;
|
|
180
|
+
}
|
|
181
|
+
// ── Deployment ──────────────────────────────────────────────────────
|
|
182
|
+
export function deployVercel(cwd) {
|
|
183
|
+
const { stdout, stderr, code } = runCommand('npx vercel deploy --prod --yes', cwd, 300_000);
|
|
184
|
+
if (code !== 0)
|
|
185
|
+
throw new Error(stderr || 'Vercel deploy failed');
|
|
186
|
+
const urlMatch = stdout.match(/https:\/\/[a-z0-9-]+\.vercel\.app/);
|
|
187
|
+
return urlMatch ? urlMatch[0] : stdout.trim();
|
|
188
|
+
}
|
|
189
|
+
export function deployNetlify(cwd) {
|
|
190
|
+
const { stdout, stderr, code } = runCommand('npx netlify deploy --prod --dir=dist', cwd, 300_000);
|
|
191
|
+
if (code !== 0)
|
|
192
|
+
throw new Error(stderr || 'Netlify deploy failed');
|
|
193
|
+
return stdout.trim();
|
|
194
|
+
}
|
|
195
|
+
// ── Project Detection ───────────────────────────────────────────────
|
|
196
|
+
export function detectProjectType(cwd) {
|
|
197
|
+
return {
|
|
198
|
+
react: existsSync(join(cwd, 'package.json')) && (readFileSync(join(cwd, 'package.json'), 'utf-8').includes('"react"') || existsSync(join(cwd, 'src/App.tsx')) || existsSync(join(cwd, 'src/App.jsx'))),
|
|
199
|
+
nextjs: existsSync(join(cwd, 'next.config.js')) || existsSync(join(cwd, 'next.config.ts')) || existsSync(join(cwd, 'next.config.mjs')),
|
|
200
|
+
vite: existsSync(join(cwd, 'vite.config.ts')) || existsSync(join(cwd, 'vite.config.js')),
|
|
201
|
+
typescript: existsSync(join(cwd, 'tsconfig.json')),
|
|
202
|
+
tailwind: existsSync(join(cwd, 'tailwind.config.ts')) || existsSync(join(cwd, 'tailwind.config.js')),
|
|
203
|
+
node: existsSync(join(cwd, 'package.json')),
|
|
204
|
+
python: existsSync(join(cwd, 'requirements.txt')) || existsSync(join(cwd, 'pyproject.toml')) || existsSync(join(cwd, 'setup.py')),
|
|
205
|
+
rust: existsSync(join(cwd, 'Cargo.toml')),
|
|
206
|
+
go: existsSync(join(cwd, 'go.mod')),
|
|
207
|
+
docker: existsSync(join(cwd, 'Dockerfile')),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
// ── LLM API ─────────────────────────────────────────────────────────
|
|
211
|
+
const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
|
212
|
+
export async function callLLM(apiKey, messages, tools, onChunk, model = 'openrouter/owl-alpha') {
|
|
213
|
+
const keys = [apiKey, ...(process.env.OPENROUTER_API_KEY ? [process.env.OPENROUTER_API_KEY] : [])].filter(Boolean);
|
|
214
|
+
for (const key of keys) {
|
|
215
|
+
try {
|
|
216
|
+
const res = await fetch(OPENROUTER_URL, {
|
|
217
|
+
method: 'POST',
|
|
218
|
+
headers: {
|
|
219
|
+
'Content-Type': 'application/json',
|
|
220
|
+
Authorization: `Bearer ${key}`,
|
|
221
|
+
'HTTP-Referer': 'https://luminaai.co.in',
|
|
222
|
+
'X-Title': 'Lumina Code',
|
|
223
|
+
},
|
|
224
|
+
body: JSON.stringify({
|
|
225
|
+
model,
|
|
226
|
+
messages: messages.map(m => m.role === 'tool' ? { role: 'tool', content: m.content, tool_call_id: m.tool_call_id } : { role: m.role, content: m.content }),
|
|
227
|
+
tools: tools || undefined,
|
|
228
|
+
stream: true,
|
|
229
|
+
max_tokens: 32000,
|
|
230
|
+
temperature: 0.1,
|
|
231
|
+
}),
|
|
232
|
+
});
|
|
233
|
+
if (!res.ok) {
|
|
234
|
+
const err = await res.text().catch(() => '');
|
|
235
|
+
console.error(`Model error ${res.status}: ${err.slice(0, 200)}`);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const reader = res.body?.getReader();
|
|
239
|
+
if (!reader)
|
|
240
|
+
continue;
|
|
241
|
+
const decoder = new TextDecoder();
|
|
242
|
+
let buf = '', content = '', toolCalls = [], tokens = 0;
|
|
243
|
+
while (true) {
|
|
244
|
+
const { done, value } = await reader.read();
|
|
245
|
+
if (done)
|
|
246
|
+
break;
|
|
247
|
+
buf += decoder.decode(value, { stream: true });
|
|
248
|
+
let nl;
|
|
249
|
+
while ((nl = buf.indexOf('\n')) !== -1) {
|
|
250
|
+
let line = buf.slice(0, nl);
|
|
251
|
+
buf = buf.slice(nl + 1);
|
|
252
|
+
if (line.endsWith('\r'))
|
|
253
|
+
line = line.slice(0, -1);
|
|
254
|
+
if (!line.startsWith('data: '))
|
|
255
|
+
continue;
|
|
256
|
+
const json = line.slice(6).trim();
|
|
257
|
+
if (json === '[DONE]')
|
|
258
|
+
continue;
|
|
259
|
+
try {
|
|
260
|
+
const parsed = JSON.parse(json);
|
|
261
|
+
const delta = parsed?.choices?.[0]?.delta;
|
|
262
|
+
if (delta?.content) {
|
|
263
|
+
content += delta.content;
|
|
264
|
+
onChunk?.(delta.content);
|
|
265
|
+
}
|
|
266
|
+
if (delta?.tool_calls) {
|
|
267
|
+
for (const tc of delta.tool_calls) {
|
|
268
|
+
const idx = toolCalls.length;
|
|
269
|
+
if (!toolCalls[idx])
|
|
270
|
+
toolCalls[idx] = { id: tc.id || '', type: 'function', function: { name: tc.function?.name || '', arguments: '' } };
|
|
271
|
+
if (tc.function?.arguments)
|
|
272
|
+
toolCalls[idx].function.arguments += tc.function.arguments;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (parsed.usage?.total_tokens)
|
|
276
|
+
tokens = parsed.usage.total_tokens;
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
buf = line + '\n' + buf;
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (content || toolCalls.length > 0)
|
|
285
|
+
return { content, toolCalls, tokens };
|
|
286
|
+
}
|
|
287
|
+
catch (e) {
|
|
288
|
+
console.error('LLM call failed:', e);
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
throw new Error('All LLM calls failed');
|
|
293
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { LuminaConfig } from '../utils/config.js';
|
|
4
|
+
interface Props {
|
|
5
|
+
prompt?: string;
|
|
6
|
+
config: LuminaConfig;
|
|
7
|
+
model: string;
|
|
8
|
+
autoApprove: boolean;
|
|
9
|
+
cwd: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function TUIApp({ prompt, config, model, autoApprove, cwd }: Props): React.DetailedReactHTMLElement<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { Box, Text, useInput, useApp } from 'ink';
|
|
3
|
+
import { Agent } from '../agent.js';
|
|
4
|
+
import { basename } from 'path';
|
|
5
|
+
const C = {
|
|
6
|
+
brand: '#7C5CFC',
|
|
7
|
+
teal: '#2DD4BF',
|
|
8
|
+
amber: '#F59E0B',
|
|
9
|
+
red: '#F87171',
|
|
10
|
+
green: '#34D399',
|
|
11
|
+
text: '#E4E4E7',
|
|
12
|
+
muted: '#52525B',
|
|
13
|
+
dim: '#3F3F46',
|
|
14
|
+
};
|
|
15
|
+
export function TUIApp({ prompt, config, model, autoApprove, cwd }) {
|
|
16
|
+
const { exit } = useApp();
|
|
17
|
+
const [messages, setMessages] = useState([]);
|
|
18
|
+
const [status, setStatus] = useState('Initializing...');
|
|
19
|
+
const [thinking, setThinking] = useState(false);
|
|
20
|
+
const [toolCount, setToolCount] = useState(0);
|
|
21
|
+
const [inputMode, setInputMode] = useState(false);
|
|
22
|
+
const [inputBuffer, setInputBuffer] = useState('');
|
|
23
|
+
const [pendingResolve, setPendingResolve] = useState(null);
|
|
24
|
+
const [startTime] = useState(Date.now());
|
|
25
|
+
const msgId = React.useRef(0);
|
|
26
|
+
const addMsg = useCallback((role, content, tool) => {
|
|
27
|
+
setMessages(prev => [...prev.slice(-200), { id: msgId.current++, role, content, tool, ts: Date.now() }]);
|
|
28
|
+
}, []);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const a = new Agent(config, model, cwd, autoApprove);
|
|
31
|
+
a.on('event', (e) => {
|
|
32
|
+
switch (e.type) {
|
|
33
|
+
case 'thinking':
|
|
34
|
+
setThinking(true);
|
|
35
|
+
setStatus('Thinking...');
|
|
36
|
+
break;
|
|
37
|
+
case 'text':
|
|
38
|
+
setThinking(false);
|
|
39
|
+
setStatus('Streaming...');
|
|
40
|
+
setMessages(prev => {
|
|
41
|
+
const last = prev[prev.length - 1];
|
|
42
|
+
if (last && last.role === 'assistant' && last.id === msgId.current - 1) {
|
|
43
|
+
return [...prev.slice(0, -1), { ...last, content: last.content + e.content }];
|
|
44
|
+
}
|
|
45
|
+
return [...prev.slice(-200), { id: msgId.current++, role: 'assistant', content: e.content, ts: Date.now() }];
|
|
46
|
+
});
|
|
47
|
+
break;
|
|
48
|
+
case 'tool_start':
|
|
49
|
+
setToolCount(c => c + 1);
|
|
50
|
+
addMsg('tool', e.tool + '(' + e.args.slice(0, 60) + ')', e.tool);
|
|
51
|
+
setStatus('Running: ' + e.tool);
|
|
52
|
+
break;
|
|
53
|
+
case 'tool_end':
|
|
54
|
+
addMsg('tool', e.output.slice(0, 150) || '(ok)');
|
|
55
|
+
setStatus('Tool complete');
|
|
56
|
+
break;
|
|
57
|
+
case 'error':
|
|
58
|
+
addMsg('system', 'ERR: ' + e.message);
|
|
59
|
+
setStatus('Error');
|
|
60
|
+
break;
|
|
61
|
+
case 'ask':
|
|
62
|
+
setInputMode(true);
|
|
63
|
+
setStatus('Waiting...');
|
|
64
|
+
addMsg('system', '? ' + e.question);
|
|
65
|
+
setPendingResolve(() => e.resolve);
|
|
66
|
+
break;
|
|
67
|
+
case 'done':
|
|
68
|
+
setThinking(false);
|
|
69
|
+
addMsg('system', 'DONE: ' + e.summary);
|
|
70
|
+
setStatus('Finished');
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
const run = async () => {
|
|
75
|
+
try {
|
|
76
|
+
const p = prompt || 'Hello! I am Lumina Code. What would you like to build?';
|
|
77
|
+
addMsg('user', p);
|
|
78
|
+
await a.run(p);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
addMsg('system', 'FATAL: ' + err.message);
|
|
82
|
+
setStatus('Failed');
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
run();
|
|
86
|
+
}, []);
|
|
87
|
+
useInput((input, key) => {
|
|
88
|
+
if (inputMode) {
|
|
89
|
+
if (key.return) {
|
|
90
|
+
if (pendingResolve) {
|
|
91
|
+
addMsg('user', inputBuffer);
|
|
92
|
+
pendingResolve(inputBuffer);
|
|
93
|
+
setPendingResolve(null);
|
|
94
|
+
}
|
|
95
|
+
setInputBuffer('');
|
|
96
|
+
setInputMode(false);
|
|
97
|
+
}
|
|
98
|
+
else if (key.backspace || key.delete) {
|
|
99
|
+
setInputBuffer(prev => prev.slice(0, -1));
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
setInputBuffer(prev => prev + input);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
if (key.ctrl && input === 'c')
|
|
107
|
+
exit();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
const visible = messages.slice(-30);
|
|
111
|
+
const modelShort = (model.split('/').pop() || model);
|
|
112
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
113
|
+
const timeStr = Math.floor(elapsed / 60) + ':' + (elapsed % 60).toString().padStart(2, '0');
|
|
114
|
+
return React.createElement(Box, { flexDirection: 'column', height: '100%' }, React.createElement(Box, { borderStyle: 'round', borderColor: C.brand, paddingX: 2, paddingY: 1 }, React.createElement(Text, { bold: true, color: C.brand }, ' LUMINA CODE '), React.createElement(Text, { color: C.muted }, ' | '), React.createElement(Text, { color: C.text }, basename(cwd)), React.createElement(Text, { color: C.muted }, ' | '), React.createElement(Text, { color: C.teal }, modelShort), thinking ? React.createElement(Text, { color: C.amber }, ' *') : null, React.createElement(Text, { color: C.dim }, ' | '), React.createElement(Text, { color: C.dim }, timeStr)), React.createElement(Box, { flexDirection: 'column', flexGrow: 1, paddingX: 2, paddingY: 1 }, visible.map(m => React.createElement(Box, { key: m.id }, React.createElement(Text, { color: C.muted }, m.role === 'user' ? '> ' : ' '), React.createElement(Text, { color: m.role === 'user' ? C.brand : m.role === 'tool' ? C.dim : m.role === 'system' ? C.amber : C.text }, m.content.slice(0, 600)))), thinking ? React.createElement(Text, { color: C.amber }, ' ...') : null), inputMode ? React.createElement(Box, { borderStyle: 'single', borderColor: C.brand, paddingX: 1 }, React.createElement(Text, { color: C.brand }, '> '), React.createElement(Text, null, inputBuffer), React.createElement(Text, { color: C.muted }, '_')) : null, React.createElement(Box, { paddingX: 2, paddingY: 1, borderStyle: 'single', borderColor: C.dim }, React.createElement(Text, { color: C.muted }, ' ' + status), React.createElement(Text, { color: C.dim }, ' | tools: ' + toolCount), React.createElement(Text, { color: C.dim }, ' | ' + timeStr), React.createElement(Text, { color: C.dim }, ' | Ctrl+C exit')));
|
|
115
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface LuminaConfig {
|
|
2
|
+
openrouterKey: string;
|
|
3
|
+
defaultModel: string;
|
|
4
|
+
codingModel: string;
|
|
5
|
+
fastModel: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function loadConfig(): Promise<LuminaConfig | null>;
|
|
8
|
+
export declare function ensureConfig(): Promise<LuminaConfig>;
|
|
9
|
+
export declare function saveSession(id: string, data: unknown): void;
|
|
10
|
+
export declare function loadSession(id: string): unknown | null;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
const CONFIG_DIR = join(homedir(), '.lumina');
|
|
5
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
6
|
+
const SESSIONS_DIR = join(CONFIG_DIR, 'sessions');
|
|
7
|
+
export async function loadConfig() {
|
|
8
|
+
try {
|
|
9
|
+
if (!existsSync(CONFIG_FILE))
|
|
10
|
+
return null;
|
|
11
|
+
const raw = readFileSync(CONFIG_FILE, 'utf-8');
|
|
12
|
+
return JSON.parse(raw);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export async function ensureConfig() {
|
|
19
|
+
if (!existsSync(CONFIG_DIR))
|
|
20
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
21
|
+
if (!existsSync(SESSIONS_DIR))
|
|
22
|
+
mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
23
|
+
const existing = await loadConfig();
|
|
24
|
+
if (existing)
|
|
25
|
+
return existing;
|
|
26
|
+
const defaults = {
|
|
27
|
+
openrouterKey: '',
|
|
28
|
+
defaultModel: 'openrouter/owl-alpha',
|
|
29
|
+
codingModel: 'moonshotai/kimi-k2.6',
|
|
30
|
+
fastModel: 'openai/gpt-oss-20b:free',
|
|
31
|
+
};
|
|
32
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(defaults, null, 2));
|
|
33
|
+
return defaults;
|
|
34
|
+
}
|
|
35
|
+
export function saveSession(id, data) {
|
|
36
|
+
const file = join(SESSIONS_DIR, `${id}.json`);
|
|
37
|
+
writeFileSync(file, JSON.stringify(data, null, 2));
|
|
38
|
+
}
|
|
39
|
+
export function loadSession(id) {
|
|
40
|
+
const file = join(SESSIONS_DIR, `${id}.json`);
|
|
41
|
+
if (!existsSync(file))
|
|
42
|
+
return null;
|
|
43
|
+
return JSON.parse(readFileSync(file, 'utf-8'));
|
|
44
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lumina-code-agent",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Lumina Code - AI coding agent",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": { "lumina": "dist/index.js" },
|
|
8
|
+
"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" },
|
|
12
|
+
"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"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^22.10.0", "@types/cross-spawn": "^6.0.6", "typescript": "^5.7.2"
|
|
18
|
+
}
|
|
19
|
+
}
|