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 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
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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
+ }