omni-agent-cli 2.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 +161 -0
- package/index.js +364 -0
- package/package.json +30 -0
- package/src/agent.js +183 -0
- package/src/config.js +338 -0
- package/src/model-fetcher.js +83 -0
- package/src/providers.js +232 -0
- package/src/stats.js +65 -0
- package/src/tools.js +366 -0
- package/src/ui.js +320 -0
package/README.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# ◈ OmniAgent — Universal AI CLI Agent
|
|
2
|
+
|
|
3
|
+
A powerful, beautiful CLI agent that works with **any AI provider** and has full filesystem + shell capabilities.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
██████╗ ███╗ ███╗███╗ ██╗██╗ ███████╗ ██████╗ ███████╗███╗ ██╗████████╗
|
|
7
|
+
██╔═══██╗████╗ ████║████╗ ██║██║ ██╔════╝██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝
|
|
8
|
+
██║ ██║██╔████╔██║██╔██╗ ██║██║ ███████╗██║ ███╗█████╗ ██╔██╗ ██║ ██║
|
|
9
|
+
██║ ██║██║╚██╔╝██║██║╚██╗██║██║ ╚════██║██║ ██║██╔══╝ ██║╚██╗██║ ██║
|
|
10
|
+
╚██████╔╝██║ ╚═╝ ██║██║ ╚████║██║ ███████║╚██████╔╝███████╗██║ ╚████║ ██║
|
|
11
|
+
╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚══════╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 🚀 Quick Start
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Install dependencies
|
|
20
|
+
npm install
|
|
21
|
+
|
|
22
|
+
# First run — guided setup wizard
|
|
23
|
+
node index.js
|
|
24
|
+
|
|
25
|
+
# Or set up separately
|
|
26
|
+
node index.js --setup
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 🔌 Supported Providers
|
|
32
|
+
|
|
33
|
+
| Provider | Format | Notes |
|
|
34
|
+
|----------|--------|-------|
|
|
35
|
+
| **Anthropic** (Claude) | Native | claude-opus, sonnet, haiku |
|
|
36
|
+
| **OpenAI** | OpenAI | gpt-4o, o1, o3-mini |
|
|
37
|
+
| **Groq** | OpenAI | Ultra fast inference |
|
|
38
|
+
| **xAI** (Grok) | OpenAI | grok-2, grok-beta |
|
|
39
|
+
| **DeepSeek** | OpenAI | deepseek-chat, reasoner |
|
|
40
|
+
| **Mistral AI** | OpenAI | mistral-large |
|
|
41
|
+
| **Together AI** | OpenAI | Llama, Qwen, etc. |
|
|
42
|
+
| **OpenRouter** | OpenAI | All models via one key |
|
|
43
|
+
| **io.net** | OpenAI | Decentralized inference |
|
|
44
|
+
| **Ollama** | OpenAI | Local models |
|
|
45
|
+
| **Custom** | OpenAI | Any OpenAI-compatible API (kiai.ai, etc.) |
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 💻 Usage
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Start in current directory
|
|
53
|
+
node index.js
|
|
54
|
+
|
|
55
|
+
# Start in specific directory
|
|
56
|
+
node index.js /path/to/project
|
|
57
|
+
|
|
58
|
+
# Global install (optional)
|
|
59
|
+
npm link
|
|
60
|
+
omni-agent /path/to/project
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## ⌨️ Commands
|
|
66
|
+
|
|
67
|
+
| Command | Description |
|
|
68
|
+
|---------|-------------|
|
|
69
|
+
| `/help` | Show all commands |
|
|
70
|
+
| `/clear` | Clear screen + conversation history |
|
|
71
|
+
| `/stats` | Show current session statistics |
|
|
72
|
+
| `/cd <dir>` | Change working directory |
|
|
73
|
+
| `/model <name>` | Switch model on the fly |
|
|
74
|
+
| `/setup` | Reconfigure provider/API key/model |
|
|
75
|
+
| `/reset` | Start new conversation (clear history) |
|
|
76
|
+
| `/history` | Show conversation history |
|
|
77
|
+
| `/exit` | Exit and show session statistics |
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 🛠️ Agent Tools
|
|
82
|
+
|
|
83
|
+
The agent has access to **13 filesystem and shell tools**:
|
|
84
|
+
|
|
85
|
+
| Tool | Description |
|
|
86
|
+
|------|-------------|
|
|
87
|
+
| `list_directory` | List files with metadata |
|
|
88
|
+
| `read_file` | Read file content |
|
|
89
|
+
| `write_file` | Create/overwrite files |
|
|
90
|
+
| `append_to_file` | Append to files |
|
|
91
|
+
| `patch_file` | Surgically edit text in files |
|
|
92
|
+
| `delete_path` | Delete files/directories |
|
|
93
|
+
| `copy_path` | Copy files |
|
|
94
|
+
| `move_path` | Move/rename files |
|
|
95
|
+
| `create_directory` | Create directories recursively |
|
|
96
|
+
| `get_file_info` | Get file metadata |
|
|
97
|
+
| `find_files` | Find files by glob pattern |
|
|
98
|
+
| `search_in_files` | Grep search in files |
|
|
99
|
+
| `execute_command` | Run any shell command |
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## ⚡ Token Efficiency
|
|
104
|
+
|
|
105
|
+
OmniAgent uses **parallel tool calling** — when the AI needs multiple independent pieces of information, it fetches them ALL in a single API call instead of making sequential requests.
|
|
106
|
+
|
|
107
|
+
Example: "List the src/ folder and read package.json"
|
|
108
|
+
- ❌ Naive: 3 API calls (list → read → answer)
|
|
109
|
+
- ✅ OmniAgent: 2 API calls (list+read simultaneously → answer)
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 📊 Session Statistics
|
|
114
|
+
|
|
115
|
+
On exit (or `/stats`), OmniAgent shows:
|
|
116
|
+
- Session duration
|
|
117
|
+
- Messages sent/received
|
|
118
|
+
- API requests made
|
|
119
|
+
- Input/output token counts
|
|
120
|
+
- Estimated cost
|
|
121
|
+
- Tool usage breakdown with visual bars
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## ⚙️ Config File
|
|
126
|
+
|
|
127
|
+
Config is saved to `~/.omni-agent.json`:
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"providerKey": "groq",
|
|
132
|
+
"baseURL": "https://api.groq.com/openai/v1",
|
|
133
|
+
"apiKey": "your-key-here",
|
|
134
|
+
"model": "llama-3.3-70b-versatile",
|
|
135
|
+
"format": "openai",
|
|
136
|
+
"maxTokens": 8192,
|
|
137
|
+
"temperature": 0.3
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 💡 Example Prompts
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
⟩ Refactor all the .js files in src/ to use async/await instead of callbacks
|
|
147
|
+
|
|
148
|
+
⟩ Find all TODO comments in this project and create a TODO.md file
|
|
149
|
+
|
|
150
|
+
⟩ Run the tests and fix any failures
|
|
151
|
+
|
|
152
|
+
⟩ Create a REST API with Express for a simple todo app
|
|
153
|
+
|
|
154
|
+
⟩ Analyze this codebase and suggest improvements
|
|
155
|
+
|
|
156
|
+
⟩ Set up a Python virtual environment and install requirements.txt
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
*Built with Node.js · chalk · gradient-string · figlet · ora · inquirer · boxen*
|
package/index.js
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createInterface } from 'readline';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { existsSync } from 'fs';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
displayBanner, displayHelp, displayMessage,
|
|
10
|
+
displayToolCall, displayToolResult, displayError,
|
|
11
|
+
displayStats, createSpinner, displayThinking,
|
|
12
|
+
displayFilePreview, displayConfirmPrompt, C,
|
|
13
|
+
} from './src/ui.js';
|
|
14
|
+
import { loadConfig, saveConfig, setupWizard, PROVIDERS } from './src/config.js';
|
|
15
|
+
import { Agent } from './src/agent.js';
|
|
16
|
+
import { SessionStats } from './src/stats.js';
|
|
17
|
+
import { fetchProviderModels, fetchAllProviderModels, formatModelList, formatAllModels } from './src/model-fetcher.js';
|
|
18
|
+
|
|
19
|
+
// ─── Main ──────────────────────────────────────────────────────────────────────
|
|
20
|
+
async function main() {
|
|
21
|
+
await displayBanner();
|
|
22
|
+
|
|
23
|
+
// Config
|
|
24
|
+
let config = loadConfig();
|
|
25
|
+
const needsSetup = !config || process.argv.includes('--setup') || process.argv.includes('-s');
|
|
26
|
+
if (needsSetup) {
|
|
27
|
+
config = await setupWizard(config);
|
|
28
|
+
saveConfig(config);
|
|
29
|
+
if (process.argv.includes('--setup') || process.argv.includes('-s')) {
|
|
30
|
+
console.log(chalk.green('\n ✅ Setup complete. Run: node index.js\n'));
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Working directory
|
|
36
|
+
const argDir = process.argv.find((a) => !a.startsWith('-') && a !== process.argv[0] && a !== process.argv[1]);
|
|
37
|
+
let workdir = argDir ? path.resolve(argDir) : process.cwd();
|
|
38
|
+
if (argDir && !existsSync(workdir)) { displayError(`Dir not found: ${workdir}`); workdir = process.cwd(); }
|
|
39
|
+
|
|
40
|
+
// Agent + Stats
|
|
41
|
+
const stats = new SessionStats();
|
|
42
|
+
const agent = new Agent(config, stats);
|
|
43
|
+
agent.setWorkdir(workdir);
|
|
44
|
+
|
|
45
|
+
// Confirmation mode (can be toggled with /confirm off)
|
|
46
|
+
let confirmEnabled = true;
|
|
47
|
+
// Store last fetched model list for selection by number
|
|
48
|
+
let lastModelList = [];
|
|
49
|
+
|
|
50
|
+
// Info bar
|
|
51
|
+
console.log(
|
|
52
|
+
` 📁 ${C.primary('Dir:')} ${C.dim(workdir)}\n` +
|
|
53
|
+
` 🤖 ${C.secondary('Provider:')} ${chalk.magenta(config.providerKey)} ${C.dim('›')} ${C.accent(config.model)}\n` +
|
|
54
|
+
` 💡 ${C.dim('Type')} ${chalk.white('/help')} ${C.dim('for commands, /exit to quit')}\n` +
|
|
55
|
+
C.dim(' ' + '─'.repeat(56) + '\n')
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// ── Readline ──────────────────────────────────────────────────────────────────
|
|
59
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
60
|
+
let isProcessing = false;
|
|
61
|
+
|
|
62
|
+
// ── Readline helper to read a single key/line ─────────────────────────────────
|
|
63
|
+
const readLine = (prompt) => new Promise((resolve) => {
|
|
64
|
+
rl.question(prompt, resolve);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const readKey = () => new Promise((resolve) => {
|
|
68
|
+
process.stdin.setRawMode?.(true);
|
|
69
|
+
process.stdin.resume();
|
|
70
|
+
process.stdin.once('data', (buf) => {
|
|
71
|
+
process.stdin.setRawMode?.(false);
|
|
72
|
+
resolve(buf.toString());
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ── Exit ──────────────────────────────────────────────────────────────────────
|
|
77
|
+
const handleExit = async () => {
|
|
78
|
+
console.log('\n');
|
|
79
|
+
await displayStats(stats.getStats());
|
|
80
|
+
rl.close();
|
|
81
|
+
process.exit(0);
|
|
82
|
+
};
|
|
83
|
+
rl.on('close', async () => { if (!isProcessing) await displayStats(stats.getStats()); process.exit(0); });
|
|
84
|
+
process.on('SIGINT', handleExit);
|
|
85
|
+
process.on('SIGTERM', handleExit);
|
|
86
|
+
|
|
87
|
+
// Safety net — prevents any unhandled rejection from killing the process
|
|
88
|
+
process.on('unhandledRejection', (err) => {
|
|
89
|
+
try { displayError('Unhandled error: ' + (err?.message || err)); } catch {}
|
|
90
|
+
isProcessing = false;
|
|
91
|
+
showPrompt();
|
|
92
|
+
});
|
|
93
|
+
process.on('uncaughtException', (err) => {
|
|
94
|
+
try { displayError('Uncaught error: ' + (err?.message || err)); } catch {}
|
|
95
|
+
isProcessing = false;
|
|
96
|
+
showPrompt();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ── Prompt ────────────────────────────────────────────────────────────────────
|
|
100
|
+
const showPrompt = () => {
|
|
101
|
+
const dirName = path.basename(workdir);
|
|
102
|
+
const confirmTag = confirmEnabled ? C.dim('') : C.warning(' [no-confirm]');
|
|
103
|
+
rl.question(`\n ${C.dim('[')}${C.primary(dirName)}${C.dim(']')}${confirmTag} ${C.secondary('⟩')} `, handleInput);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// ── Confirmation handler ──────────────────────────────────────────────────────
|
|
107
|
+
const handleConfirmation = async (toolName, input) => {
|
|
108
|
+
displayFilePreview(toolName, input);
|
|
109
|
+
await displayConfirmPrompt(toolName);
|
|
110
|
+
|
|
111
|
+
process.stdout.write('\n ' + C.dim('Waiting for your choice [1/2/3/4]... '));
|
|
112
|
+
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
// Try raw mode first (single keypress)
|
|
115
|
+
if (process.stdin.setRawMode) {
|
|
116
|
+
process.stdin.setRawMode(true);
|
|
117
|
+
process.stdin.resume();
|
|
118
|
+
const onKey = (buf) => {
|
|
119
|
+
const key = buf.toString();
|
|
120
|
+
process.stdin.setRawMode(false);
|
|
121
|
+
process.stdin.removeListener('data', onKey);
|
|
122
|
+
if (key === '1' || key === '\r' || key === '\n') {
|
|
123
|
+
console.log(C.success('1') + C.dim(' — Yes, once'));
|
|
124
|
+
resolve('yes');
|
|
125
|
+
} else if (key === '2') {
|
|
126
|
+
console.log(C.success('2') + C.dim(' — Yes, allow always'));
|
|
127
|
+
resolve('always');
|
|
128
|
+
} else if (key === '3' || key === '\x1b') {
|
|
129
|
+
console.log(C.warning('3') + C.dim(' — Skipped'));
|
|
130
|
+
resolve('skip');
|
|
131
|
+
} else if (key === '4') {
|
|
132
|
+
console.log(C.error('4') + C.dim(' — Suggest changes'));
|
|
133
|
+
resolve('modify');
|
|
134
|
+
} else {
|
|
135
|
+
console.log(C.success('1') + C.dim(' — Yes (default)'));
|
|
136
|
+
resolve('yes');
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
process.stdin.on('data', onKey);
|
|
140
|
+
} else {
|
|
141
|
+
// Fallback: readline for Termux/environments without raw mode
|
|
142
|
+
rl.question('', (ans) => {
|
|
143
|
+
const n = ans.trim();
|
|
144
|
+
if (n === '2') resolve('always');
|
|
145
|
+
else if (n === '3') resolve('skip');
|
|
146
|
+
else if (n === '4') resolve('modify');
|
|
147
|
+
else resolve('yes');
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// ── Input handler ─────────────────────────────────────────────────────────────
|
|
154
|
+
const handleInput = async (input) => {
|
|
155
|
+
const trimmed = input.trim();
|
|
156
|
+
if (!trimmed) { showPrompt(); return; }
|
|
157
|
+
|
|
158
|
+
// Bare number input after /models → select model by number
|
|
159
|
+
if (/^\d+$/.test(trimmed) && lastModelList.length > 0) {
|
|
160
|
+
const n = parseInt(trimmed, 10);
|
|
161
|
+
if (n >= 1 && n <= lastModelList.length) {
|
|
162
|
+
config.model = lastModelList[n - 1]; // full name, not truncated
|
|
163
|
+
agent.updateConfig(config); saveConfig(config);
|
|
164
|
+
console.log(C.success('\n Model: ' + C.accent(config.model) + '\n'));
|
|
165
|
+
} else {
|
|
166
|
+
console.log(C.warning('\n Number out of range (1-' + lastModelList.length + ')\n'));
|
|
167
|
+
}
|
|
168
|
+
showPrompt(); return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── Commands ───────────────────────────────────────────────────────────────
|
|
172
|
+
if (trimmed === '/exit' || trimmed === '/quit' || trimmed === '/q') { await handleExit(); return; }
|
|
173
|
+
if (trimmed === '/help' || trimmed === '/h') { displayHelp(); showPrompt(); return; }
|
|
174
|
+
if (trimmed === '/clear' || trimmed === '/cls') {
|
|
175
|
+
await displayBanner(); agent.clearHistory();
|
|
176
|
+
console.log(C.dim(' 🔄 Cleared.\n')); showPrompt(); return;
|
|
177
|
+
}
|
|
178
|
+
if (trimmed === '/stats') { await displayStats(stats.getStats()); showPrompt(); return; }
|
|
179
|
+
if (trimmed === '/reset' || trimmed === '/new') {
|
|
180
|
+
agent.clearHistory();
|
|
181
|
+
console.log(C.success('\n ✅ New conversation.\n')); showPrompt(); return;
|
|
182
|
+
}
|
|
183
|
+
if (trimmed.startsWith('/cd ')) {
|
|
184
|
+
const newDir = trimmed.slice(4).trim();
|
|
185
|
+
const resolved = path.isAbsolute(newDir) ? newDir : path.resolve(workdir, newDir);
|
|
186
|
+
if (existsSync(resolved)) {
|
|
187
|
+
workdir = resolved; agent.setWorkdir(workdir); process.chdir(workdir);
|
|
188
|
+
console.log(C.success(`\n ✅ ${C.primary(workdir)}\n`));
|
|
189
|
+
} else { displayError(`Not found: ${resolved}`); }
|
|
190
|
+
showPrompt(); return;
|
|
191
|
+
}
|
|
192
|
+
if (trimmed === '/setup') {
|
|
193
|
+
config = await setupWizard(config); saveConfig(config); agent.updateConfig(config);
|
|
194
|
+
showPrompt(); return;
|
|
195
|
+
}
|
|
196
|
+
if (trimmed.startsWith('/model ')) {
|
|
197
|
+
const modelArg = trimmed.slice(7).trim();
|
|
198
|
+
// Support /model 3 (pick by number from last /models list)
|
|
199
|
+
const asNum = parseInt(modelArg, 10);
|
|
200
|
+
if (!isNaN(asNum) && asNum >= 1 && lastModelList.length >= asNum) {
|
|
201
|
+
config.model = lastModelList[asNum - 1];
|
|
202
|
+
} else {
|
|
203
|
+
config.model = modelArg;
|
|
204
|
+
}
|
|
205
|
+
agent.updateConfig(config); saveConfig(config);
|
|
206
|
+
console.log(C.success(`\n ✅ Model: ${C.accent(config.model)}\n`)); showPrompt(); return;
|
|
207
|
+
}
|
|
208
|
+
if (trimmed.startsWith('/confirm')) {
|
|
209
|
+
const val = trimmed.split(' ')[1];
|
|
210
|
+
if (val === 'off') { confirmEnabled = false; agent.allowAllTools = true; console.log(C.warning('\n ⚠ Confirmations OFF — agent will run freely\n')); }
|
|
211
|
+
else { confirmEnabled = true; agent.allowAllTools = false; console.log(C.success('\n ✅ Confirmations ON\n')); }
|
|
212
|
+
showPrompt(); return;
|
|
213
|
+
}
|
|
214
|
+
if (trimmed === '/models' || trimmed.startsWith('/models ')) {
|
|
215
|
+
const arg = trimmed.slice(7).trim();
|
|
216
|
+
|
|
217
|
+
if (arg === 'all') {
|
|
218
|
+
process.stdout.write(C.dim(' Fetching all providers...\n'));
|
|
219
|
+
const apiConfigs = { [config.providerKey]: { apiKey: config.apiKey, baseURL: config.baseURL } };
|
|
220
|
+
let allResults;
|
|
221
|
+
try { allResults = await fetchAllProviderModels(apiConfigs); }
|
|
222
|
+
catch(e) { displayError(e.message); showPrompt(); return; }
|
|
223
|
+
console.log('\n' + C.primary(' -- ALL PROVIDERS & MODELS --') + '\n');
|
|
224
|
+
console.log(formatAllModels(allResults));
|
|
225
|
+
lastModelList = Object.values(allResults).flatMap(r => r.models);
|
|
226
|
+
console.log('\n' + C.dim(' /model <number> to switch') + '\n');
|
|
227
|
+
showPrompt(); return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const targetKey = arg || config.providerKey;
|
|
231
|
+
const provider = PROVIDERS[targetKey];
|
|
232
|
+
if (!provider) {
|
|
233
|
+
displayError('Unknown: ' + targetKey + ' | Available: ' + Object.keys(PROVIDERS).join(', '));
|
|
234
|
+
showPrompt(); return;
|
|
235
|
+
}
|
|
236
|
+
process.stdout.write(C.dim(' Fetching ' + provider.label + '...\n'));
|
|
237
|
+
let modResult;
|
|
238
|
+
try {
|
|
239
|
+
const isSame = (targetKey === config.providerKey);
|
|
240
|
+
modResult = await fetchProviderModels(
|
|
241
|
+
targetKey,
|
|
242
|
+
isSame ? config.apiKey : null,
|
|
243
|
+
isSame ? config.baseURL : provider.baseURL
|
|
244
|
+
);
|
|
245
|
+
} catch(e) { displayError(e.message); showPrompt(); return; }
|
|
246
|
+
console.log('\n' + formatModelList(targetKey, modResult) + '\n');
|
|
247
|
+
lastModelList = modResult.models;
|
|
248
|
+
if (modResult.models.length > 0) console.log(C.dim(' /model <number> to switch\n'));
|
|
249
|
+
showPrompt(); return;
|
|
250
|
+
}
|
|
251
|
+
if (trimmed === '/providers') {
|
|
252
|
+
console.log('\n');
|
|
253
|
+
for (const [key, p] of Object.entries(PROVIDERS)) {
|
|
254
|
+
const active = key === config.providerKey;
|
|
255
|
+
console.log(` ${active ? C.accent(key.padEnd(14)) : C.dim(key.padEnd(14))} ${p.label}${active ? C.success(' ◄') : ''}`);
|
|
256
|
+
}
|
|
257
|
+
console.log('\n' + C.dim(' /setup to switch\n')); showPrompt(); return;
|
|
258
|
+
}
|
|
259
|
+
if (trimmed === '/history') {
|
|
260
|
+
for (const m of agent.history) {
|
|
261
|
+
const preview = typeof m.content === 'string' ? m.content.slice(0, 80).replace(/\n/g,' ') : '[structured]';
|
|
262
|
+
console.log(` ${C.accent(m.role.padEnd(12))} ${C.dim(preview)}`);
|
|
263
|
+
}
|
|
264
|
+
console.log(''); showPrompt(); return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── Send to agent ──────────────────────────────────────────────────────────
|
|
268
|
+
isProcessing = true;
|
|
269
|
+
stats.addMessage('user');
|
|
270
|
+
|
|
271
|
+
process.stdout.write(C.dim(' ⟳ Thinking...') );
|
|
272
|
+
let _dotTimer = setInterval(() => process.stdout.write(C.dim('.')), 1500);
|
|
273
|
+
let _stopped = false;
|
|
274
|
+
const spinner = { stop: () => { if(_stopped) return; _stopped=true; clearInterval(_dotTimer); process.stdout.write('\n'); }, text: '', start: () => {} };
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
const finalContent = await agent.chat(trimmed, workdir, async (event) => {
|
|
278
|
+
|
|
279
|
+
// ── Thinking bullets ─────────────────────────────────────────────────
|
|
280
|
+
if (event.type === 'thinking') {
|
|
281
|
+
spinner.stop();
|
|
282
|
+
displayThinking(event.bullets);
|
|
283
|
+
spinner.start();
|
|
284
|
+
spinner.text = C.dim('Planning…');
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ── Confirmation required ────────────────────────────────────────────
|
|
289
|
+
if (event.type === 'confirm_needed') {
|
|
290
|
+
spinner.stop();
|
|
291
|
+
if (confirmEnabled) {
|
|
292
|
+
const decision = await handleConfirmation(event.tool, event.input);
|
|
293
|
+
event.resolve(decision);
|
|
294
|
+
} else {
|
|
295
|
+
// Confirmations off — auto approve, but still show preview
|
|
296
|
+
displayFilePreview(event.tool, event.input);
|
|
297
|
+
console.log(C.dim(' ✓ Auto-approved (confirm off)\n'));
|
|
298
|
+
event.resolve('yes');
|
|
299
|
+
}
|
|
300
|
+
spinner.start();
|
|
301
|
+
spinner.text = C.dim('Executing…');
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ── Feedback request (option 4) ──────────────────────────────────────
|
|
306
|
+
if (event.type === 'request_feedback') {
|
|
307
|
+
spinner.stop();
|
|
308
|
+
const fb = await readLine('\n ' + C.accent('What should be changed? ') + C.dim('→ '));
|
|
309
|
+
event.resolve(fb);
|
|
310
|
+
spinner.start();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ── Tool start (non-confirm tools) ───────────────────────────────────
|
|
315
|
+
if (event.type === 'tool_start') {
|
|
316
|
+
spinner.stop();
|
|
317
|
+
displayToolCall(event.tool, event.input);
|
|
318
|
+
spinner.start();
|
|
319
|
+
spinner.text = C.dim(`Running ${event.tool}…`);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ── Tool done ────────────────────────────────────────────────────────
|
|
324
|
+
if (event.type === 'tool_done') {
|
|
325
|
+
spinner.stop();
|
|
326
|
+
displayToolResult(event.tool, event.result, event.durationMs);
|
|
327
|
+
spinner.start();
|
|
328
|
+
spinner.text = C.dim('Thinking…');
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ── Token counter ────────────────────────────────────────────────────
|
|
333
|
+
if (event.type === 'tokens') {
|
|
334
|
+
stats.addTokens(event.inputTokens || 0, event.outputTokens || 0);
|
|
335
|
+
const total = (event.inputTokens || 0) + (event.outputTokens || 0);
|
|
336
|
+
spinner.text = C.dim(`Thinking… `) + C.dim(`[${total.toLocaleString()} tok]`);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
spinner.stop();
|
|
341
|
+
stats.addMessage('assistant');
|
|
342
|
+
displayMessage('assistant', finalContent);
|
|
343
|
+
|
|
344
|
+
} catch (err) {
|
|
345
|
+
spinner.stop();
|
|
346
|
+
stats.addError();
|
|
347
|
+
displayError(err.message || 'Unexpected error');
|
|
348
|
+
if (err.message?.includes('401') || err.message?.includes('Unauthorized'))
|
|
349
|
+
console.log(C.dim(`\n 💡 Check API key: /setup\n`));
|
|
350
|
+
else if (err.message?.includes('model'))
|
|
351
|
+
console.log(C.dim(`\n 💡 Try: /model <name>\n`));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
isProcessing = false;
|
|
355
|
+
showPrompt();
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
showPrompt();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
main().catch((err) => {
|
|
362
|
+
console.error(chalk.red('\n Fatal:'), err.message);
|
|
363
|
+
process.exit(1);
|
|
364
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "omni-agent-cli",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Universal AI CLI Agent — any provider, any model, full filesystem power",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"omni-agent": "./index.js",
|
|
8
|
+
"oa": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node index.js",
|
|
12
|
+
"setup": "node index.js --setup"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"axios": "^1.7.2",
|
|
16
|
+
"boxen": "^7.1.1",
|
|
17
|
+
"chalk": "^5.3.0",
|
|
18
|
+
"cli-spinners": "^2.9.2",
|
|
19
|
+
"glob": "^11.0.0",
|
|
20
|
+
"gradient-string": "^2.0.2",
|
|
21
|
+
"inquirer": "^9.3.7",
|
|
22
|
+
"mime-types": "^2.1.35",
|
|
23
|
+
"ora": "^8.1.1",
|
|
24
|
+
"strip-ansi": "^7.1.0"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18.0.0"
|
|
28
|
+
},
|
|
29
|
+
"preferGlobal": true
|
|
30
|
+
}
|