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 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
+ }