smart-aipi 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +183 -0
- package/bin/cli.js +426 -0
- package/lib/docs.js +311 -0
- package/lib/login.js +186 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Smart AIPI CLI
|
|
2
|
+
|
|
3
|
+
CLI for [Smart AIPI](https://smartaipi.com) — OpenAI-compatible API at 75% off.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g smart-aipi
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or with Homebrew:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
brew tap smart-aipi/tap
|
|
15
|
+
brew install smart-aipi
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Create an account
|
|
22
|
+
smart-aipi signup
|
|
23
|
+
|
|
24
|
+
# Create an API key
|
|
25
|
+
smart-aipi keys create
|
|
26
|
+
|
|
27
|
+
# Set environment variables
|
|
28
|
+
export OPENAI_BASE_URL=https://api.smartaipi.com/v1
|
|
29
|
+
export OPENAI_API_KEY=sk-your-key
|
|
30
|
+
|
|
31
|
+
# Done — your AI tools now use Smart AIPI
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Commands
|
|
35
|
+
|
|
36
|
+
### Account
|
|
37
|
+
|
|
38
|
+
| Command | Description |
|
|
39
|
+
|---------|-------------|
|
|
40
|
+
| `smart-aipi signup` | Create a new account |
|
|
41
|
+
| `smart-aipi login` | Log in |
|
|
42
|
+
| `smart-aipi logout` | Log out |
|
|
43
|
+
| `smart-aipi status` | Show login status |
|
|
44
|
+
| `smart-aipi me` | Show account info |
|
|
45
|
+
| `smart-aipi usage` | Show credits and usage |
|
|
46
|
+
|
|
47
|
+
### API Keys
|
|
48
|
+
|
|
49
|
+
| Command | Description |
|
|
50
|
+
|---------|-------------|
|
|
51
|
+
| `smart-aipi keys list` | List your API keys |
|
|
52
|
+
| `smart-aipi keys create` | Create a new API key |
|
|
53
|
+
| `smart-aipi keys revoke <id>` | Revoke an API key |
|
|
54
|
+
|
|
55
|
+
### Tokens (for MCP)
|
|
56
|
+
|
|
57
|
+
| Command | Description |
|
|
58
|
+
|---------|-------------|
|
|
59
|
+
| `smart-aipi tokens list` | List account tokens |
|
|
60
|
+
| `smart-aipi tokens create` | Create a token for MCP server |
|
|
61
|
+
|
|
62
|
+
### Documentation
|
|
63
|
+
|
|
64
|
+
| Command | Description |
|
|
65
|
+
|---------|-------------|
|
|
66
|
+
| `smart-aipi docs` | List all setup guides |
|
|
67
|
+
| `smart-aipi docs <topic>` | Show setup guide for a specific tool |
|
|
68
|
+
| `smart-aipi models` | List available models |
|
|
69
|
+
| `smart-aipi models --live` | Fetch live model list from API |
|
|
70
|
+
| `smart-aipi config` | Show base URL and env vars |
|
|
71
|
+
|
|
72
|
+
#### Available docs topics
|
|
73
|
+
|
|
74
|
+
- **Getting Started:** quickstart, models
|
|
75
|
+
- **IDE & Agents:** cursor, cline, windsurf, continue, aider, opencode, codex-cli
|
|
76
|
+
- **SDKs:** python, nodejs, langchain
|
|
77
|
+
- **MCP Server:** mcp
|
|
78
|
+
|
|
79
|
+
## Connecting to AI Tools
|
|
80
|
+
|
|
81
|
+
### Cursor
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
Settings > Models > Override OpenAI Base URL
|
|
85
|
+
Base URL: https://api.smartaipi.com/v1
|
|
86
|
+
API Key: sk-your-key
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### OpenCode
|
|
90
|
+
|
|
91
|
+
Edit `~/.config/opencode/config.json`:
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"provider": {
|
|
96
|
+
"openai": {
|
|
97
|
+
"options": {
|
|
98
|
+
"baseURL": "https://api.smartaipi.com/v1",
|
|
99
|
+
"apiKey": "sk-your-key"
|
|
100
|
+
},
|
|
101
|
+
"models": {
|
|
102
|
+
"gpt-5.3-codex": {}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
"model": "openai/gpt-5.3-codex"
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Codex CLI
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
export OPENAI_BASE_URL=https://api.smartaipi.com/v1
|
|
114
|
+
export OPENAI_API_KEY=sk-your-key
|
|
115
|
+
codex "explain this code"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Any OpenAI-compatible tool
|
|
119
|
+
|
|
120
|
+
Just set `OPENAI_BASE_URL` and `OPENAI_API_KEY`. Run `smart-aipi docs <tool>` for specific guides.
|
|
121
|
+
|
|
122
|
+
## Available Models
|
|
123
|
+
|
|
124
|
+
### Codex (Code-optimized)
|
|
125
|
+
- `gpt-5.3-codex` — latest, recommended
|
|
126
|
+
- `gpt-5.2-codex`, `gpt-5.1-codex`, `gpt-5-codex`
|
|
127
|
+
- `gpt-5.2-codex-mini`, `gpt-5.1-codex-mini`, `gpt-5-codex-mini`
|
|
128
|
+
- `gpt-5.1-codex-max` — maximum context window
|
|
129
|
+
|
|
130
|
+
### GPT-5
|
|
131
|
+
- `gpt-5.2`, `gpt-5.1`, `gpt-5`
|
|
132
|
+
|
|
133
|
+
### Image Generation
|
|
134
|
+
- `gpt-image-latest` — always points to latest
|
|
135
|
+
- `gpt-image-1.5`, `gpt-image-1-mini`
|
|
136
|
+
|
|
137
|
+
### Free Tier
|
|
138
|
+
- `gpt-4.1`, `gpt-4.1-mini`, `gpt-4.1-nano`
|
|
139
|
+
- `gpt-5-mini`, `gpt-5-nano`
|
|
140
|
+
- `gpt-4o`, `gpt-4o-mini`
|
|
141
|
+
|
|
142
|
+
## MCP Server
|
|
143
|
+
|
|
144
|
+
For AI agents that support MCP, install the companion server:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
npm install -g @smart-aipi/mcp
|
|
148
|
+
smart-aipi tokens create -n "MCP Token"
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
See `smart-aipi docs mcp` for full setup instructions.
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT
|
|
156
|
+
|
|
157
|
+
## Development
|
|
158
|
+
|
|
159
|
+
The published CLI runtime files are in:
|
|
160
|
+
- `bin/cli.js`
|
|
161
|
+
- `lib/*.js`
|
|
162
|
+
|
|
163
|
+
TypeScript source files live in:
|
|
164
|
+
- `src/bin/cli.ts`
|
|
165
|
+
- `src/lib/*.ts`
|
|
166
|
+
|
|
167
|
+
Validation commands:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
npm test
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Install TypeScript first if needed:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
npm install --save-dev typescript @types/node
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Then rebuild JS outputs from TS:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
npm run build
|
|
183
|
+
```
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import Conf from 'conf';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import inquirer from 'inquirer';
|
|
7
|
+
import { createRequire } from 'node:module';
|
|
8
|
+
import { DOCS, TOPIC_LIST, DOC_CATEGORIES } from '../lib/docs.js';
|
|
9
|
+
import { registerLoginCommand, saveAuthState } from '../lib/login.js';
|
|
10
|
+
const API_BASE = process.env.SMART_AIPI_URL || 'https://api.smartaipi.com';
|
|
11
|
+
const WEB_BASE = process.env.SMART_AIPI_WEB_URL || 'https://smartaipi.com';
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const { version: CLI_VERSION } = require('../package.json');
|
|
14
|
+
const config = new Conf({ projectName: 'smart-aipi' });
|
|
15
|
+
const program = new Command();
|
|
16
|
+
program
|
|
17
|
+
.name('smart-aipi')
|
|
18
|
+
.description('CLI for Smart AIPI - OpenAI-compatible API at 75% off')
|
|
19
|
+
.version(CLI_VERSION);
|
|
20
|
+
registerLoginCommand(program, {
|
|
21
|
+
config,
|
|
22
|
+
apiBase: API_BASE,
|
|
23
|
+
webBase: WEB_BASE
|
|
24
|
+
});
|
|
25
|
+
program
|
|
26
|
+
.command('signup')
|
|
27
|
+
.description('Create a new Smart AIPI account')
|
|
28
|
+
.action(async () => {
|
|
29
|
+
const answers = await inquirer.prompt([
|
|
30
|
+
{ type: 'input', name: 'name', message: 'Name:' },
|
|
31
|
+
{ type: 'input', name: 'email', message: 'Email:' },
|
|
32
|
+
{ type: 'password', name: 'password', message: 'Password (min 8 chars):', mask: '*' }
|
|
33
|
+
]);
|
|
34
|
+
if (answers.password.length < 8) {
|
|
35
|
+
console.log(chalk.red('Password must be at least 8 characters'));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const spinner = ora('Creating account...').start();
|
|
39
|
+
try {
|
|
40
|
+
const res = await fetch(`${API_BASE}/api/auth/signup`, {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: { 'Content-Type': 'application/json' },
|
|
43
|
+
body: JSON.stringify(answers)
|
|
44
|
+
});
|
|
45
|
+
const data = await res.json();
|
|
46
|
+
if (!res.ok) {
|
|
47
|
+
spinner.fail(chalk.red(data.error || 'Signup failed'));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
saveAuthState(config, { token: data.token, email: data.email, userId: data.user_id });
|
|
51
|
+
spinner.succeed(chalk.green('Account created successfully!'));
|
|
52
|
+
console.log(chalk.dim(` Email: ${data.email}`));
|
|
53
|
+
console.log(chalk.dim(` Token saved to config`));
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
spinner.fail(chalk.red(`Error: ${err.message}`));
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
program
|
|
60
|
+
.command('logout')
|
|
61
|
+
.description('Log out of your Smart AIPI account')
|
|
62
|
+
.action(() => {
|
|
63
|
+
config.delete('token');
|
|
64
|
+
config.delete('email');
|
|
65
|
+
config.delete('userId');
|
|
66
|
+
console.log(chalk.green('Logged out successfully'));
|
|
67
|
+
});
|
|
68
|
+
const keys = program
|
|
69
|
+
.command('keys')
|
|
70
|
+
.description('Manage API keys');
|
|
71
|
+
keys
|
|
72
|
+
.command('list')
|
|
73
|
+
.description('List your API keys')
|
|
74
|
+
.action(async () => {
|
|
75
|
+
const token = config.get('token');
|
|
76
|
+
if (!token) {
|
|
77
|
+
console.log(chalk.red('Not logged in. Run: smart-aipi login'));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const spinner = ora('Fetching keys...').start();
|
|
81
|
+
try {
|
|
82
|
+
const res = await fetch(`${API_BASE}/api/keys`, {
|
|
83
|
+
headers: { 'Authorization': `Bearer ${token}` }
|
|
84
|
+
});
|
|
85
|
+
const data = await res.json();
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
spinner.fail(chalk.red(data.error || 'Failed to fetch keys'));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
spinner.stop();
|
|
91
|
+
if (!data.keys || data.keys.length === 0) {
|
|
92
|
+
console.log(chalk.dim('No API keys. Create one with: smart-aipi keys create'));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
console.log(chalk.bold('\nYour API Keys:\n'));
|
|
96
|
+
for (const key of data.keys) {
|
|
97
|
+
const enabled = key.enabled ?? key.Enabled;
|
|
98
|
+
const keyPrefix = key.key_prefix ?? key.KeyPrefix;
|
|
99
|
+
const keyName = key.name ?? key.Name;
|
|
100
|
+
const createdAt = key.created_at ?? key.CreatedAt;
|
|
101
|
+
const status = enabled ? chalk.green('active') : chalk.red('revoked');
|
|
102
|
+
console.log(` ${chalk.cyan(keyPrefix)}... ${keyName} [${status}]`);
|
|
103
|
+
console.log(chalk.dim(` Created: ${new Date(createdAt).toLocaleDateString()}`));
|
|
104
|
+
}
|
|
105
|
+
console.log();
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
spinner.fail(chalk.red(`Error: ${err.message}`));
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
keys
|
|
112
|
+
.command('create')
|
|
113
|
+
.description('Create a new API key')
|
|
114
|
+
.option('-n, --name <name>', 'Name for the key', 'CLI Key')
|
|
115
|
+
.action(async (options) => {
|
|
116
|
+
const token = config.get('token');
|
|
117
|
+
if (!token) {
|
|
118
|
+
console.log(chalk.red('Not logged in. Run: smart-aipi login'));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const spinner = ora('Creating key...').start();
|
|
122
|
+
try {
|
|
123
|
+
const res = await fetch(`${API_BASE}/api/keys/create`, {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
headers: {
|
|
126
|
+
'Authorization': `Bearer ${token}`,
|
|
127
|
+
'Content-Type': 'application/json'
|
|
128
|
+
},
|
|
129
|
+
body: JSON.stringify({ name: options.name })
|
|
130
|
+
});
|
|
131
|
+
const data = await res.json();
|
|
132
|
+
if (!res.ok) {
|
|
133
|
+
spinner.fail(chalk.red(data.error || 'Failed to create key'));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
spinner.succeed(chalk.green('API key created!'));
|
|
137
|
+
console.log();
|
|
138
|
+
console.log(chalk.yellow(' Save this key - you won\'t see it again!'));
|
|
139
|
+
console.log();
|
|
140
|
+
console.log(` ${chalk.bold.cyan(data.key)}`);
|
|
141
|
+
console.log();
|
|
142
|
+
console.log(chalk.dim(' Use with: OPENAI_API_KEY=' + data.key));
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
spinner.fail(chalk.red(`Error: ${err.message}`));
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
keys
|
|
149
|
+
.command('revoke <key_id>')
|
|
150
|
+
.description('Revoke an API key')
|
|
151
|
+
.action(async (keyId) => {
|
|
152
|
+
const token = config.get('token');
|
|
153
|
+
if (!token) {
|
|
154
|
+
console.log(chalk.red('Not logged in. Run: smart-aipi login'));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const { confirm } = await inquirer.prompt([{
|
|
158
|
+
type: 'confirm',
|
|
159
|
+
name: 'confirm',
|
|
160
|
+
message: `Revoke key ${keyId}?`,
|
|
161
|
+
default: false
|
|
162
|
+
}]);
|
|
163
|
+
if (!confirm)
|
|
164
|
+
return;
|
|
165
|
+
const spinner = ora('Revoking key...').start();
|
|
166
|
+
try {
|
|
167
|
+
const res = await fetch(`${API_BASE}/api/keys/revoke`, {
|
|
168
|
+
method: 'POST',
|
|
169
|
+
headers: {
|
|
170
|
+
'Authorization': `Bearer ${token}`,
|
|
171
|
+
'Content-Type': 'application/json'
|
|
172
|
+
},
|
|
173
|
+
body: JSON.stringify({ key_id: keyId })
|
|
174
|
+
});
|
|
175
|
+
const data = await res.json();
|
|
176
|
+
if (!res.ok) {
|
|
177
|
+
spinner.fail(chalk.red(data.error || 'Failed to revoke key'));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
spinner.succeed(chalk.green('Key revoked successfully'));
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
spinner.fail(chalk.red(`Error: ${err.message}`));
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
const tokens = program
|
|
187
|
+
.command('tokens')
|
|
188
|
+
.description('Manage account tokens (for CLI/MCP)');
|
|
189
|
+
tokens
|
|
190
|
+
.command('list')
|
|
191
|
+
.description('List your account tokens')
|
|
192
|
+
.action(async () => {
|
|
193
|
+
const token = config.get('token');
|
|
194
|
+
if (!token) {
|
|
195
|
+
console.log(chalk.red('Not logged in. Run: smart-aipi login'));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const spinner = ora('Fetching tokens...').start();
|
|
199
|
+
try {
|
|
200
|
+
const res = await fetch(`${API_BASE}/api/tokens`, {
|
|
201
|
+
headers: { 'Authorization': `Bearer ${token}` }
|
|
202
|
+
});
|
|
203
|
+
const data = await res.json();
|
|
204
|
+
if (!res.ok) {
|
|
205
|
+
spinner.fail(chalk.red(data.error || 'Failed to fetch tokens'));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
spinner.stop();
|
|
209
|
+
if (!data.tokens || data.tokens.length === 0) {
|
|
210
|
+
console.log(chalk.dim('No account tokens.'));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
console.log(chalk.bold('\nYour Account Tokens:\n'));
|
|
214
|
+
for (const t of data.tokens) {
|
|
215
|
+
const status = t.revoked ? chalk.red('revoked') : chalk.green('active');
|
|
216
|
+
console.log(` ${chalk.cyan(t.token_prefix)}... ${t.name} [${status}]`);
|
|
217
|
+
console.log(chalk.dim(` ID: ${t.id} Created: ${new Date(t.created_at).toLocaleDateString()}`));
|
|
218
|
+
}
|
|
219
|
+
console.log();
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
spinner.fail(chalk.red(`Error: ${err.message}`));
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
tokens
|
|
226
|
+
.command('create')
|
|
227
|
+
.description('Create a new account token')
|
|
228
|
+
.option('-n, --name <name>', 'Name for the token', 'CLI Token')
|
|
229
|
+
.action(async (options) => {
|
|
230
|
+
const token = config.get('token');
|
|
231
|
+
if (!token) {
|
|
232
|
+
console.log(chalk.red('Not logged in. Run: smart-aipi login'));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const spinner = ora('Creating token...').start();
|
|
236
|
+
try {
|
|
237
|
+
const res = await fetch(`${API_BASE}/api/tokens/create`, {
|
|
238
|
+
method: 'POST',
|
|
239
|
+
headers: {
|
|
240
|
+
'Authorization': `Bearer ${token}`,
|
|
241
|
+
'Content-Type': 'application/json'
|
|
242
|
+
},
|
|
243
|
+
body: JSON.stringify({ name: options.name })
|
|
244
|
+
});
|
|
245
|
+
const data = await res.json();
|
|
246
|
+
if (!res.ok) {
|
|
247
|
+
spinner.fail(chalk.red(data.error || 'Failed to create token'));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
spinner.succeed(chalk.green('Account token created!'));
|
|
251
|
+
console.log();
|
|
252
|
+
console.log(chalk.yellow(' Save this token - you won\'t see it again!'));
|
|
253
|
+
console.log();
|
|
254
|
+
console.log(` ${chalk.bold.cyan(data.token)}`);
|
|
255
|
+
console.log();
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
spinner.fail(chalk.red(`Error: ${err.message}`));
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
program
|
|
262
|
+
.command('usage')
|
|
263
|
+
.description('Show your usage summary and credits')
|
|
264
|
+
.action(async () => {
|
|
265
|
+
const token = config.get('token');
|
|
266
|
+
if (!token) {
|
|
267
|
+
console.log(chalk.red('Not logged in. Run: smart-aipi login'));
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const spinner = ora('Fetching usage...').start();
|
|
271
|
+
try {
|
|
272
|
+
const res = await fetch(`${API_BASE}/api/usage`, {
|
|
273
|
+
headers: { 'Authorization': `Bearer ${token}` }
|
|
274
|
+
});
|
|
275
|
+
const data = await res.json();
|
|
276
|
+
if (!res.ok) {
|
|
277
|
+
spinner.fail(chalk.red(data.error || 'Failed to fetch usage'));
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
spinner.stop();
|
|
281
|
+
const credits = (data.credits / 100).toFixed(2);
|
|
282
|
+
console.log(chalk.bold('\n Credits Balance: ') + chalk.green(`$${credits}`));
|
|
283
|
+
console.log();
|
|
284
|
+
console.log(chalk.bold(' Usage Summary'));
|
|
285
|
+
console.log(chalk.dim(' ─────────────────────────────────────'));
|
|
286
|
+
const fmt = (label, s) => {
|
|
287
|
+
const tokens = s.tokens >= 1000 ? (s.tokens / 1000).toFixed(1) + 'K' : String(s.tokens);
|
|
288
|
+
const cost = '$' + (s.cost / 100).toFixed(2);
|
|
289
|
+
console.log(` ${label.padEnd(14)} ${tokens.padStart(10)} tokens ${cost.padStart(8)}`);
|
|
290
|
+
};
|
|
291
|
+
fmt('Today', data.summary.today);
|
|
292
|
+
fmt('This Week', data.summary.week);
|
|
293
|
+
fmt('This Month', data.summary.month);
|
|
294
|
+
fmt('All Time', data.summary.all_time);
|
|
295
|
+
if (data.history && data.history.length > 0) {
|
|
296
|
+
console.log();
|
|
297
|
+
console.log(chalk.bold(' Recent Requests'));
|
|
298
|
+
console.log(chalk.dim(' ─────────────────────────────────────'));
|
|
299
|
+
for (const r of data.history.slice(0, 10)) {
|
|
300
|
+
const model = r.model.padEnd(20);
|
|
301
|
+
const tokens = String(r.total_tokens).padStart(8);
|
|
302
|
+
const cost = '$' + (r.cost / 100).toFixed(4);
|
|
303
|
+
console.log(chalk.dim(` ${model} ${tokens} tok ${cost}`));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
console.log();
|
|
307
|
+
}
|
|
308
|
+
catch (err) {
|
|
309
|
+
spinner.fail(chalk.red(`Error: ${err.message}`));
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
program
|
|
313
|
+
.command('me')
|
|
314
|
+
.description('Show your account info')
|
|
315
|
+
.action(async () => {
|
|
316
|
+
const token = config.get('token');
|
|
317
|
+
if (!token) {
|
|
318
|
+
console.log(chalk.red('Not logged in. Run: smart-aipi login'));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const spinner = ora('Fetching account info...').start();
|
|
322
|
+
try {
|
|
323
|
+
const res = await fetch(`${API_BASE}/api/me`, {
|
|
324
|
+
headers: { 'Authorization': `Bearer ${token}` }
|
|
325
|
+
});
|
|
326
|
+
const data = await res.json();
|
|
327
|
+
if (!res.ok) {
|
|
328
|
+
spinner.fail(chalk.red(data.error || 'Failed to fetch account info'));
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
spinner.stop();
|
|
332
|
+
console.log(chalk.bold('\n Account Info\n'));
|
|
333
|
+
console.log(` ${chalk.dim('Name:')} ${data.name || chalk.dim('not set')}`);
|
|
334
|
+
console.log(` ${chalk.dim('Email:')} ${data.email}`);
|
|
335
|
+
console.log(` ${chalk.dim('Plan:')} ${data.plan}`);
|
|
336
|
+
console.log(` ${chalk.dim('Credits:')} ${chalk.green('$' + (data.credits / 100).toFixed(2))}`);
|
|
337
|
+
console.log(` ${chalk.dim('Verified:')} ${data.verified ? chalk.green('yes') : chalk.red('no')}`);
|
|
338
|
+
console.log();
|
|
339
|
+
}
|
|
340
|
+
catch (err) {
|
|
341
|
+
spinner.fail(chalk.red(`Error: ${err.message}`));
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
program
|
|
345
|
+
.command('status')
|
|
346
|
+
.description('Show current login status')
|
|
347
|
+
.action(() => {
|
|
348
|
+
const email = config.get('email');
|
|
349
|
+
const token = config.get('token');
|
|
350
|
+
if (!email || !token) {
|
|
351
|
+
console.log(chalk.dim('Not logged in'));
|
|
352
|
+
console.log(chalk.dim('Run: smart-aipi login'));
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
console.log(chalk.green('Logged in'));
|
|
356
|
+
console.log(chalk.dim(` Email: ${email}`));
|
|
357
|
+
});
|
|
358
|
+
program
|
|
359
|
+
.command('config')
|
|
360
|
+
.description('Show configuration for OpenAI-compatible tools')
|
|
361
|
+
.action(() => {
|
|
362
|
+
console.log(chalk.bold('\nConfiguration for OpenAI-compatible tools:\n'));
|
|
363
|
+
console.log(chalk.cyan('Base URL:'));
|
|
364
|
+
console.log(` ${API_BASE}/v1\n`);
|
|
365
|
+
console.log(chalk.cyan('Environment variables:'));
|
|
366
|
+
console.log(` OPENAI_BASE_URL=${API_BASE}/v1`);
|
|
367
|
+
console.log(` OPENAI_API_KEY=<your-api-key>\n`);
|
|
368
|
+
console.log(chalk.dim('Get an API key with: smart-aipi keys create'));
|
|
369
|
+
});
|
|
370
|
+
program
|
|
371
|
+
.command('docs [topic]')
|
|
372
|
+
.description('Show setup guides (topics: quickstart, cursor, cline, opencode, mcp, models, ...)')
|
|
373
|
+
.action((topic) => {
|
|
374
|
+
if (!topic) {
|
|
375
|
+
console.log(chalk.bold('\n Smart AIPI Documentation\n'));
|
|
376
|
+
console.log(chalk.cyan(' Available topics:\n'));
|
|
377
|
+
for (const [cat, topics] of Object.entries(DOC_CATEGORIES)) {
|
|
378
|
+
console.log(` ${chalk.bold(cat)}`);
|
|
379
|
+
for (const t of topics) {
|
|
380
|
+
console.log(` ${chalk.green(t.padEnd(14))} ${DOCS[t].title}`);
|
|
381
|
+
}
|
|
382
|
+
console.log();
|
|
383
|
+
}
|
|
384
|
+
console.log(chalk.dim(' Run: smart-aipi docs <topic>\n'));
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const doc = DOCS[topic];
|
|
388
|
+
if (!doc) {
|
|
389
|
+
console.log(chalk.red(`\n Unknown topic: ${topic}`));
|
|
390
|
+
console.log(chalk.dim(` Available: ${TOPIC_LIST.join(', ')}\n`));
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
console.log(doc.content);
|
|
394
|
+
});
|
|
395
|
+
program
|
|
396
|
+
.command('models')
|
|
397
|
+
.description('List available models')
|
|
398
|
+
.option('--live', 'Fetch live model list from the API')
|
|
399
|
+
.action(async (options) => {
|
|
400
|
+
if (options.live) {
|
|
401
|
+
const spinner = ora('Fetching models...').start();
|
|
402
|
+
try {
|
|
403
|
+
const res = await fetch(`${API_BASE}/v1/models`);
|
|
404
|
+
const data = await res.json();
|
|
405
|
+
if (!res.ok) {
|
|
406
|
+
spinner.fail(chalk.red('Failed to fetch models'));
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
spinner.stop();
|
|
410
|
+
console.log(chalk.bold('\n Available Models (live)\n'));
|
|
411
|
+
const models = data.data || [];
|
|
412
|
+
models.sort((a, b) => a.id.localeCompare(b.id));
|
|
413
|
+
for (const m of models) {
|
|
414
|
+
console.log(` ${chalk.green(m.id.padEnd(24))} ${chalk.dim(m.owned_by)}`);
|
|
415
|
+
}
|
|
416
|
+
console.log();
|
|
417
|
+
}
|
|
418
|
+
catch (err) {
|
|
419
|
+
spinner.fail(chalk.red(`Error: ${err.message}`));
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
console.log(DOCS['models'].content);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
program.parse();
|
package/lib/docs.js
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
export const DOCS = {
|
|
3
|
+
'quickstart': {
|
|
4
|
+
title: 'Quick Start',
|
|
5
|
+
content: `
|
|
6
|
+
${chalk.bold('Smart AIPI Quick Start')}
|
|
7
|
+
|
|
8
|
+
Smart AIPI is an OpenAI-compatible API. Set two env vars and go.
|
|
9
|
+
|
|
10
|
+
${chalk.cyan('1. Install & sign up')}
|
|
11
|
+
${chalk.dim('$ npm install -g smart-aipi')}
|
|
12
|
+
${chalk.dim('$ smart-aipi signup')}
|
|
13
|
+
${chalk.dim('$ smart-aipi keys create')}
|
|
14
|
+
|
|
15
|
+
${chalk.cyan('2. Set environment variables')}
|
|
16
|
+
${chalk.dim('export OPENAI_BASE_URL=https://api.smartaipi.com/v1')}
|
|
17
|
+
${chalk.dim('export OPENAI_API_KEY=sk-your-api-key')}
|
|
18
|
+
|
|
19
|
+
${chalk.cyan('3. Use with any OpenAI-compatible tool')}
|
|
20
|
+
Your existing code, SDKs, and AI tools work automatically.
|
|
21
|
+
|
|
22
|
+
${chalk.cyan('Available topics:')}
|
|
23
|
+
cursor, cline, windsurf, continue, aider,
|
|
24
|
+
opencode, codex-cli, langchain, python, nodejs, mcp, models
|
|
25
|
+
|
|
26
|
+
${chalk.dim('Run: smart-aipi docs <topic>')}
|
|
27
|
+
`
|
|
28
|
+
},
|
|
29
|
+
'cursor': {
|
|
30
|
+
title: 'Cursor IDE',
|
|
31
|
+
content: `
|
|
32
|
+
${chalk.bold('Using Smart AIPI with Cursor')}
|
|
33
|
+
|
|
34
|
+
${chalk.cyan('Setup')}
|
|
35
|
+
1. Open Cursor Settings (Cmd+, or Ctrl+,)
|
|
36
|
+
2. Go to "Models" section
|
|
37
|
+
3. Under "OpenAI API Key", enter your Smart AIPI key
|
|
38
|
+
4. Click "Override OpenAI Base URL"
|
|
39
|
+
5. Enter: ${chalk.green('https://api.smartaipi.com/v1')}
|
|
40
|
+
6. Select a model (e.g. gpt-5.3-codex) and start coding
|
|
41
|
+
|
|
42
|
+
${chalk.cyan('Recommended models')}
|
|
43
|
+
- ${chalk.green('gpt-5.3-codex')} — best for code generation
|
|
44
|
+
- ${chalk.green('gpt-5.2-codex-mini')} — fast, cheaper
|
|
45
|
+
- ${chalk.green('gpt-5.1-codex-max')} — maximum context window
|
|
46
|
+
`
|
|
47
|
+
},
|
|
48
|
+
'cline': {
|
|
49
|
+
title: 'Cline',
|
|
50
|
+
content: `
|
|
51
|
+
${chalk.bold('Using Smart AIPI with Cline')}
|
|
52
|
+
|
|
53
|
+
${chalk.cyan('Option 1: Cline settings')}
|
|
54
|
+
In Cline settings, configure:
|
|
55
|
+
- API Provider: ${chalk.green('OpenAI Compatible')}
|
|
56
|
+
- Base URL: ${chalk.green('https://api.smartaipi.com/v1')}
|
|
57
|
+
- API Key: ${chalk.green('sk-your-smart-aipi-key')}
|
|
58
|
+
- Model: ${chalk.green('gpt-5.3-codex')}
|
|
59
|
+
|
|
60
|
+
${chalk.cyan('Option 2: Environment variables')}
|
|
61
|
+
${chalk.dim('export OPENAI_BASE_URL=https://api.smartaipi.com/v1')}
|
|
62
|
+
${chalk.dim('export OPENAI_API_KEY=sk-your-smart-aipi-key')}
|
|
63
|
+
`
|
|
64
|
+
},
|
|
65
|
+
'windsurf': {
|
|
66
|
+
title: 'Windsurf',
|
|
67
|
+
content: `
|
|
68
|
+
${chalk.bold('Using Smart AIPI with Windsurf')}
|
|
69
|
+
|
|
70
|
+
${chalk.cyan('Setup')}
|
|
71
|
+
1. Open Windsurf Settings
|
|
72
|
+
2. Go to AI Provider settings
|
|
73
|
+
3. Select "OpenAI Compatible" as provider
|
|
74
|
+
4. Set Base URL: ${chalk.green('https://api.smartaipi.com/v1')}
|
|
75
|
+
5. Set API Key: ${chalk.green('sk-your-smart-aipi-key')}
|
|
76
|
+
6. Set Model: ${chalk.green('gpt-5.3-codex')}
|
|
77
|
+
`
|
|
78
|
+
},
|
|
79
|
+
'continue': {
|
|
80
|
+
title: 'Continue',
|
|
81
|
+
content: `
|
|
82
|
+
${chalk.bold('Using Smart AIPI with Continue')}
|
|
83
|
+
|
|
84
|
+
Edit your Continue config (~/.continue/config.json):
|
|
85
|
+
|
|
86
|
+
${chalk.dim(`{
|
|
87
|
+
"models": [{
|
|
88
|
+
"title": "Smart AIPI",
|
|
89
|
+
"provider": "openai",
|
|
90
|
+
"model": "gpt-5.3-codex",
|
|
91
|
+
"apiBase": "https://api.smartaipi.com/v1",
|
|
92
|
+
"apiKey": "sk-your-smart-aipi-key"
|
|
93
|
+
}]
|
|
94
|
+
}`)}
|
|
95
|
+
`
|
|
96
|
+
},
|
|
97
|
+
'aider': {
|
|
98
|
+
title: 'Aider',
|
|
99
|
+
content: `
|
|
100
|
+
${chalk.bold('Using Smart AIPI with Aider')}
|
|
101
|
+
|
|
102
|
+
${chalk.cyan('Setup')}
|
|
103
|
+
${chalk.dim('export OPENAI_API_BASE=https://api.smartaipi.com/v1')}
|
|
104
|
+
${chalk.dim('export OPENAI_API_KEY=sk-your-smart-aipi-key')}
|
|
105
|
+
|
|
106
|
+
Then run:
|
|
107
|
+
${chalk.dim('aider --model openai/gpt-5.3-codex')}
|
|
108
|
+
`
|
|
109
|
+
},
|
|
110
|
+
'opencode': {
|
|
111
|
+
title: 'OpenCode',
|
|
112
|
+
content: `
|
|
113
|
+
${chalk.bold('Using Smart AIPI with OpenCode')}
|
|
114
|
+
|
|
115
|
+
Edit ~/.config/opencode/config.json:
|
|
116
|
+
|
|
117
|
+
${chalk.dim(`{
|
|
118
|
+
"provider": {
|
|
119
|
+
"openai": {
|
|
120
|
+
"options": {
|
|
121
|
+
"baseURL": "https://api.smartaipi.com/v1",
|
|
122
|
+
"apiKey": "sk-your-smart-aipi-key"
|
|
123
|
+
},
|
|
124
|
+
"models": {
|
|
125
|
+
"gpt-5.3-codex": {}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
"model": "openai/gpt-5.3-codex"
|
|
130
|
+
}`)}
|
|
131
|
+
`
|
|
132
|
+
},
|
|
133
|
+
'codex-cli': {
|
|
134
|
+
title: 'Codex CLI',
|
|
135
|
+
content: `
|
|
136
|
+
${chalk.bold('Using Smart AIPI with Codex CLI')}
|
|
137
|
+
|
|
138
|
+
${chalk.cyan('Setup')}
|
|
139
|
+
${chalk.dim('export OPENAI_BASE_URL=https://api.smartaipi.com/v1')}
|
|
140
|
+
${chalk.dim('export OPENAI_API_KEY=sk-your-smart-aipi-key')}
|
|
141
|
+
|
|
142
|
+
Then use Codex CLI as normal:
|
|
143
|
+
${chalk.dim('codex "explain this code"')}
|
|
144
|
+
|
|
145
|
+
${chalk.cyan('Or edit ~/.codex/config.toml')}
|
|
146
|
+
${chalk.dim(`[api]
|
|
147
|
+
base_url = "https://api.smartaipi.com/v1"
|
|
148
|
+
api_key = "sk-your-smart-aipi-key"
|
|
149
|
+
model = "gpt-5.3-codex"`)}
|
|
150
|
+
`
|
|
151
|
+
},
|
|
152
|
+
'langchain': {
|
|
153
|
+
title: 'LangChain',
|
|
154
|
+
content: `
|
|
155
|
+
${chalk.bold('Using Smart AIPI with LangChain')}
|
|
156
|
+
|
|
157
|
+
${chalk.cyan('Python')}
|
|
158
|
+
${chalk.dim(`from langchain_openai import ChatOpenAI
|
|
159
|
+
|
|
160
|
+
llm = ChatOpenAI(
|
|
161
|
+
model="gpt-5.3-codex",
|
|
162
|
+
openai_api_base="https://api.smartaipi.com/v1",
|
|
163
|
+
openai_api_key="sk-your-smart-aipi-key"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
response = llm.invoke("Hello!")`)}
|
|
167
|
+
|
|
168
|
+
${chalk.cyan('JavaScript')}
|
|
169
|
+
${chalk.dim(`import { ChatOpenAI } from "@langchain/openai";
|
|
170
|
+
|
|
171
|
+
const llm = new ChatOpenAI({
|
|
172
|
+
model: "gpt-5.3-codex",
|
|
173
|
+
configuration: {
|
|
174
|
+
baseURL: "https://api.smartaipi.com/v1",
|
|
175
|
+
apiKey: "sk-your-smart-aipi-key"
|
|
176
|
+
}
|
|
177
|
+
});`)}
|
|
178
|
+
`
|
|
179
|
+
},
|
|
180
|
+
'python': {
|
|
181
|
+
title: 'Python (OpenAI SDK)',
|
|
182
|
+
content: `
|
|
183
|
+
${chalk.bold('Using Smart AIPI with Python')}
|
|
184
|
+
|
|
185
|
+
${chalk.dim(`from openai import OpenAI
|
|
186
|
+
|
|
187
|
+
client = OpenAI(
|
|
188
|
+
base_url="https://api.smartaipi.com/v1",
|
|
189
|
+
api_key="sk-your-smart-aipi-key"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Chat completions
|
|
193
|
+
response = client.chat.completions.create(
|
|
194
|
+
model="gpt-5.3-codex",
|
|
195
|
+
messages=[{"role": "user", "content": "Hello!"}]
|
|
196
|
+
)
|
|
197
|
+
print(response.choices[0].message.content)
|
|
198
|
+
|
|
199
|
+
# Image generation
|
|
200
|
+
image = client.images.generate(
|
|
201
|
+
model="gpt-image-latest",
|
|
202
|
+
prompt="A sunset over mountains"
|
|
203
|
+
)
|
|
204
|
+
print(image.data[0].url)`)}
|
|
205
|
+
`
|
|
206
|
+
},
|
|
207
|
+
'nodejs': {
|
|
208
|
+
title: 'Node.js',
|
|
209
|
+
content: `
|
|
210
|
+
${chalk.bold('Using Smart AIPI with Node.js')}
|
|
211
|
+
|
|
212
|
+
${chalk.dim(`import OpenAI from 'openai';
|
|
213
|
+
|
|
214
|
+
const openai = new OpenAI({
|
|
215
|
+
baseURL: 'https://api.smartaipi.com/v1',
|
|
216
|
+
apiKey: 'sk-your-smart-aipi-key'
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Chat completions
|
|
220
|
+
const response = await openai.chat.completions.create({
|
|
221
|
+
model: 'gpt-5.3-codex',
|
|
222
|
+
messages: [{ role: 'user', content: 'Hello!' }]
|
|
223
|
+
});
|
|
224
|
+
console.log(response.choices[0].message.content);
|
|
225
|
+
|
|
226
|
+
// Image generation
|
|
227
|
+
const image = await openai.images.generate({
|
|
228
|
+
model: 'gpt-image-latest',
|
|
229
|
+
prompt: 'A sunset over mountains'
|
|
230
|
+
});`)}
|
|
231
|
+
`
|
|
232
|
+
},
|
|
233
|
+
'mcp': {
|
|
234
|
+
title: 'MCP Server Setup',
|
|
235
|
+
content: `
|
|
236
|
+
${chalk.bold('Smart AIPI MCP Server')}
|
|
237
|
+
|
|
238
|
+
The MCP server lets AI agents manage your keys, check usage,
|
|
239
|
+
and access setup docs directly.
|
|
240
|
+
|
|
241
|
+
${chalk.cyan('1. Install')}
|
|
242
|
+
${chalk.dim('npm install -g @smart-aipi/mcp')}
|
|
243
|
+
|
|
244
|
+
${chalk.cyan('2. Get an account token')}
|
|
245
|
+
${chalk.dim('smart-aipi login')}
|
|
246
|
+
${chalk.dim('smart-aipi tokens create -n "MCP Token"')}
|
|
247
|
+
|
|
248
|
+
${chalk.cyan('3. Add to your AI tool')}
|
|
249
|
+
|
|
250
|
+
${chalk.bold('Cursor / Cline / OpenCode')} — add to MCP settings:
|
|
251
|
+
${chalk.dim(`{
|
|
252
|
+
"mcpServers": {
|
|
253
|
+
"smart-aipi": {
|
|
254
|
+
"command": "smart-aipi-mcp",
|
|
255
|
+
"env": { "SMART_AIPI_TOKEN": "your-token" }
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}`)}
|
|
259
|
+
|
|
260
|
+
${chalk.cyan('Available MCP tools')}
|
|
261
|
+
- smart_aipi_docs — get setup guides
|
|
262
|
+
- smart_aipi_config — get base URL and env vars
|
|
263
|
+
- smart_aipi_models — list available models
|
|
264
|
+
- smart_aipi_list_keys — list your API keys
|
|
265
|
+
- smart_aipi_create_key — create a new API key
|
|
266
|
+
- smart_aipi_revoke_key — revoke an API key
|
|
267
|
+
- smart_aipi_usage — check credits and usage
|
|
268
|
+
- smart_aipi_me — view account info
|
|
269
|
+
`
|
|
270
|
+
},
|
|
271
|
+
'models': {
|
|
272
|
+
title: 'Available Models',
|
|
273
|
+
content: `
|
|
274
|
+
${chalk.bold('Available Models')}
|
|
275
|
+
|
|
276
|
+
${chalk.cyan('Codex Series (Code-optimized)')}
|
|
277
|
+
- ${chalk.green('gpt-5.3-codex')} — latest, recommended for coding
|
|
278
|
+
- ${chalk.green('gpt-5.2-codex')} — stable code generation
|
|
279
|
+
- ${chalk.green('gpt-5.1-codex')} — code generation
|
|
280
|
+
- ${chalk.green('gpt-5-codex')} — base codex model
|
|
281
|
+
- ${chalk.green('gpt-5.2-codex-mini')} — fast & efficient
|
|
282
|
+
- ${chalk.green('gpt-5.1-codex-mini')} — fast & efficient
|
|
283
|
+
- ${chalk.green('gpt-5-codex-mini')} — fast & efficient
|
|
284
|
+
- ${chalk.green('gpt-5.1-codex-max')} — maximum context window
|
|
285
|
+
|
|
286
|
+
${chalk.cyan('GPT-5 Series')}
|
|
287
|
+
- ${chalk.green('gpt-5.2')} — latest general-purpose
|
|
288
|
+
- ${chalk.green('gpt-5.1')} — general-purpose
|
|
289
|
+
- ${chalk.green('gpt-5')} — base GPT-5
|
|
290
|
+
|
|
291
|
+
${chalk.cyan('Image Generation')}
|
|
292
|
+
- ${chalk.green('gpt-image-latest')} — always points to the latest model
|
|
293
|
+
- ${chalk.green('gpt-image-1.5')} — frontier image generation
|
|
294
|
+
- ${chalk.green('gpt-image-1-mini')} — fast image generation
|
|
295
|
+
|
|
296
|
+
${chalk.cyan('Free Tier Models')}
|
|
297
|
+
- gpt-4.1, gpt-4.1-mini, gpt-4.1-nano
|
|
298
|
+
- gpt-5-mini, gpt-5-nano
|
|
299
|
+
- gpt-4o, gpt-4o-mini
|
|
300
|
+
|
|
301
|
+
${chalk.dim('Live list: smart-aipi models --live')}
|
|
302
|
+
`
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
export const TOPIC_LIST = Object.keys(DOCS);
|
|
306
|
+
export const DOC_CATEGORIES = {
|
|
307
|
+
'Getting Started': ['quickstart', 'models'],
|
|
308
|
+
'IDE & Agent Setup': ['cursor', 'cline', 'windsurf', 'continue', 'aider', 'opencode', 'codex-cli'],
|
|
309
|
+
'SDK & Framework': ['python', 'nodejs', 'langchain'],
|
|
310
|
+
'MCP Server': ['mcp']
|
|
311
|
+
};
|
package/lib/login.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { createServer } from 'node:http';
|
|
5
|
+
import { randomBytes } from 'node:crypto';
|
|
6
|
+
import open from 'open';
|
|
7
|
+
const AUTH_TIMEOUT_MS = 5 * 60 * 1000;
|
|
8
|
+
const CALLBACK_HOST = '127.0.0.1';
|
|
9
|
+
const AUTH_FAILURE_PAGES = {
|
|
10
|
+
invalidState: '<html><body style="font-family:sans-serif;text-align:center;padding:60px"><h2>Authorization Failed</h2><p>Invalid state parameter. Please try again.</p></body></html>',
|
|
11
|
+
missingToken: '<html><body style="font-family:sans-serif;text-align:center;padding:60px"><h2>Authorization Failed</h2><p>No token received. Please try again.</p></body></html>'
|
|
12
|
+
};
|
|
13
|
+
const AUTH_SUCCESS_PAGE = `<html><body style="font-family:system-ui,sans-serif;text-align:center;padding:60px;background:#f9fafb">
|
|
14
|
+
<div style="max-width:400px;margin:0 auto;background:white;border-radius:16px;border:1px solid #e5e7eb;padding:40px">
|
|
15
|
+
<div style="width:48px;height:48px;background:#dbeafe;border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 16px">
|
|
16
|
+
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="#2563eb" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
|
|
17
|
+
</div>
|
|
18
|
+
<h2 style="color:#111827;margin:0 0 8px;font-size:20px">CLI Authorized!</h2>
|
|
19
|
+
<p style="color:#6b7280;margin:0;font-size:14px">You can close this tab and return to your terminal.</p>
|
|
20
|
+
</div></body></html>`;
|
|
21
|
+
function isHeadlessEnvironment() {
|
|
22
|
+
return !!(process.env.SSH_TTY ||
|
|
23
|
+
process.env.SSH_CONNECTION ||
|
|
24
|
+
(process.platform === 'linux' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY));
|
|
25
|
+
}
|
|
26
|
+
function formatError(error) {
|
|
27
|
+
return error instanceof Error ? error.message : String(error);
|
|
28
|
+
}
|
|
29
|
+
export function saveAuthState(config, { token, email, userId }) {
|
|
30
|
+
if (token)
|
|
31
|
+
config.set('token', token);
|
|
32
|
+
if (email)
|
|
33
|
+
config.set('email', email);
|
|
34
|
+
if (userId)
|
|
35
|
+
config.set('userId', userId);
|
|
36
|
+
}
|
|
37
|
+
function printLoginSuccess(email) {
|
|
38
|
+
console.log(chalk.green('\nLogged in successfully!'));
|
|
39
|
+
if (email)
|
|
40
|
+
console.log(chalk.dim(` Email: ${email}`));
|
|
41
|
+
console.log(chalk.dim(' Token saved to config'));
|
|
42
|
+
}
|
|
43
|
+
async function hydrateUserFromToken(config, apiBase, token) {
|
|
44
|
+
try {
|
|
45
|
+
const meRes = await fetch(`${apiBase}/api/me`, {
|
|
46
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
47
|
+
});
|
|
48
|
+
if (!meRes.ok)
|
|
49
|
+
return;
|
|
50
|
+
const me = (await meRes.json());
|
|
51
|
+
saveAuthState(config, { email: me.email, userId: me.id });
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function closeServerSafely(server) {
|
|
57
|
+
if (server.listening) {
|
|
58
|
+
server.close();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function writeHtmlResponse(res, statusCode, body) {
|
|
62
|
+
res.writeHead(statusCode, { 'Content-Type': 'text/html' });
|
|
63
|
+
res.end(body);
|
|
64
|
+
}
|
|
65
|
+
async function loginWithBrowser({ config, apiBase, webBase }) {
|
|
66
|
+
const state = randomBytes(16).toString('hex');
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
let settled = false;
|
|
69
|
+
const waiting = ora({ text: 'Waiting for authorization...', spinner: 'dots' });
|
|
70
|
+
const finish = (ok) => {
|
|
71
|
+
if (settled)
|
|
72
|
+
return;
|
|
73
|
+
settled = true;
|
|
74
|
+
resolve(ok);
|
|
75
|
+
};
|
|
76
|
+
const server = createServer(async (req, res) => {
|
|
77
|
+
const url = new URL(req.url ?? '/', `http://${CALLBACK_HOST}`);
|
|
78
|
+
if (url.pathname !== '/callback') {
|
|
79
|
+
res.writeHead(404);
|
|
80
|
+
res.end('Not found');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const token = url.searchParams.get('token');
|
|
84
|
+
const returnedState = url.searchParams.get('state');
|
|
85
|
+
if (returnedState !== state) {
|
|
86
|
+
waiting.stop();
|
|
87
|
+
writeHtmlResponse(res, 400, AUTH_FAILURE_PAGES.invalidState);
|
|
88
|
+
closeServerSafely(server);
|
|
89
|
+
console.log(chalk.red('\nAuthorization failed: state mismatch'));
|
|
90
|
+
finish(false);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (!token) {
|
|
94
|
+
waiting.stop();
|
|
95
|
+
writeHtmlResponse(res, 400, AUTH_FAILURE_PAGES.missingToken);
|
|
96
|
+
closeServerSafely(server);
|
|
97
|
+
console.log(chalk.red('\nAuthorization failed: no token received'));
|
|
98
|
+
finish(false);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
waiting.stop();
|
|
102
|
+
saveAuthState(config, { token });
|
|
103
|
+
await hydrateUserFromToken(config, apiBase, token);
|
|
104
|
+
writeHtmlResponse(res, 200, AUTH_SUCCESS_PAGE);
|
|
105
|
+
closeServerSafely(server);
|
|
106
|
+
printLoginSuccess(config.get('email'));
|
|
107
|
+
finish(true);
|
|
108
|
+
});
|
|
109
|
+
server.on('error', (err) => {
|
|
110
|
+
waiting.stop();
|
|
111
|
+
console.log(chalk.red(`\nFailed to start callback server: ${formatError(err)}`));
|
|
112
|
+
finish(false);
|
|
113
|
+
});
|
|
114
|
+
server.listen(0, CALLBACK_HOST, () => {
|
|
115
|
+
const address = server.address();
|
|
116
|
+
if (!address || typeof address === 'string') {
|
|
117
|
+
waiting.stop();
|
|
118
|
+
closeServerSafely(server);
|
|
119
|
+
console.log(chalk.red('\nFailed to determine callback server port.'));
|
|
120
|
+
finish(false);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const port = address.port;
|
|
124
|
+
const authURL = `${webBase}/auth/cli?port=${port}&state=${state}`;
|
|
125
|
+
console.log(chalk.dim('Opening browser for authentication...'));
|
|
126
|
+
console.log(chalk.dim(` If it doesn't open, visit: ${authURL}\n`));
|
|
127
|
+
open(authURL).catch(() => {
|
|
128
|
+
console.log(chalk.yellow(' Could not open browser automatically.'));
|
|
129
|
+
console.log(chalk.yellow(` Open this URL manually: ${authURL}\n`));
|
|
130
|
+
});
|
|
131
|
+
waiting.start();
|
|
132
|
+
const timeout = setTimeout(() => {
|
|
133
|
+
waiting.stop();
|
|
134
|
+
closeServerSafely(server);
|
|
135
|
+
console.log(chalk.red('\nAuthorization timed out (5 minutes).'));
|
|
136
|
+
console.log(chalk.dim(' Try again or use: smart-aipi login --no-browser'));
|
|
137
|
+
finish(false);
|
|
138
|
+
}, AUTH_TIMEOUT_MS);
|
|
139
|
+
server.on('close', () => clearTimeout(timeout));
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
async function loginWithPrompt({ config, apiBase }) {
|
|
144
|
+
const answers = (await inquirer.prompt([
|
|
145
|
+
{ type: 'input', name: 'email', message: 'Email:' },
|
|
146
|
+
{ type: 'password', name: 'password', message: 'Password:', mask: '*' }
|
|
147
|
+
]));
|
|
148
|
+
const spinner = ora('Logging in...').start();
|
|
149
|
+
try {
|
|
150
|
+
const res = await fetch(`${apiBase}/api/auth/login`, {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
headers: { 'Content-Type': 'application/json' },
|
|
153
|
+
body: JSON.stringify(answers)
|
|
154
|
+
});
|
|
155
|
+
const data = (await res.json());
|
|
156
|
+
if (!res.ok || !data.token || !data.email) {
|
|
157
|
+
spinner.fail(chalk.red(data.error || 'Login failed'));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
saveAuthState(config, { token: data.token, email: data.email, userId: data.user_id });
|
|
161
|
+
spinner.succeed(chalk.green('Logged in successfully!'));
|
|
162
|
+
console.log(chalk.dim(` Email: ${data.email}`));
|
|
163
|
+
console.log(chalk.dim(' Token saved to config'));
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
spinner.fail(chalk.red(`Error: ${formatError(err)}`));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
export function registerLoginCommand(program, deps) {
|
|
170
|
+
program
|
|
171
|
+
.command('login')
|
|
172
|
+
.description('Log in to your Smart AIPI account')
|
|
173
|
+
.option('--no-browser', 'Use email/password prompt instead of browser')
|
|
174
|
+
.action(async (options) => {
|
|
175
|
+
const isHeadless = isHeadlessEnvironment();
|
|
176
|
+
if (options.browser === false || isHeadless) {
|
|
177
|
+
if (isHeadless && options.browser !== false) {
|
|
178
|
+
console.log(chalk.dim('Headless environment detected, using email/password login.\n'));
|
|
179
|
+
}
|
|
180
|
+
await loginWithPrompt({ config: deps.config, apiBase: deps.apiBase });
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
await loginWithBrowser(deps);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "smart-aipi",
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "CLI for Smart AIPI - OpenAI-compatible API gateway",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"bin/",
|
|
8
|
+
"lib/"
|
|
9
|
+
],
|
|
10
|
+
"bin": {
|
|
11
|
+
"smart-aipi": "bin/cli.js",
|
|
12
|
+
"saipi": "bin/cli.js"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc -p tsconfig.json",
|
|
16
|
+
"test:runtime": "node --check bin/cli.js && node --check lib/login.js && node --check lib/docs.js",
|
|
17
|
+
"test": "npm run build && npm run test:runtime"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"openai",
|
|
21
|
+
"api",
|
|
22
|
+
"cli",
|
|
23
|
+
"ai",
|
|
24
|
+
"gpt",
|
|
25
|
+
"chatgpt"
|
|
26
|
+
],
|
|
27
|
+
"author": "",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"chalk": "^5.3.0",
|
|
31
|
+
"commander": "^11.0.0",
|
|
32
|
+
"conf": "^12.0.0",
|
|
33
|
+
"inquirer": "^9.2.0",
|
|
34
|
+
"open": "^10.2.0",
|
|
35
|
+
"ora": "^7.0.0"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18.0.0"
|
|
39
|
+
},
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/magic/smart-aipi.git"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^25.2.2",
|
|
46
|
+
"typescript": "^5.9.3"
|
|
47
|
+
}
|
|
48
|
+
}
|