@ceo-ai/cli 1.0.2
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 +301 -0
- package/bin/ceo.js +224 -0
- package/lib/client.js +170 -0
- package/lib/config.js +58 -0
- package/package.json +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
## CLI README
|
|
2
|
+
|
|
3
|
+
### `ceo-cli/README.md`
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
`markdown
|
|
7
|
+
# @ceo-ai/cli
|
|
8
|
+
|
|
9
|
+
Command-line interface for the CEO.AI API. Send prompts to your AI agents, poll for results, and manage your configuration — all from the terminal.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install -g @ceo-ai/cli
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# 1. Configure your API key
|
|
24
|
+
ceo configure --key sk_live_your_api_key_here
|
|
25
|
+
|
|
26
|
+
# 2. Send a prompt and wait for the result
|
|
27
|
+
ceo prompt "What was our Q4 revenue?" --poll
|
|
28
|
+
|
|
29
|
+
# 3. That's it!
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
## Configuration
|
|
34
|
+
|
|
35
|
+
### Interactive Setup
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
ceo configure
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
You'll be prompted for:
|
|
44
|
+
- **API Key** — your `sk_live_...` key from the CEO.AI dashboard
|
|
45
|
+
- **Endpoint** — the API URL (defaults to `https://ingestion.api.ceo.ai`)
|
|
46
|
+
|
|
47
|
+
### Inline Setup
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
ceo configure --key sk_live_abc123 --endpoint https://ingestion.api.ceo.ai
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
### View Current Config
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
ceo config:show
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
Current Configuration:
|
|
66
|
+
|
|
67
|
+
Endpoint: https://ingestion.api.ceo.ai
|
|
68
|
+
API Key: sk_live_abc1••••••••def4
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
Your configuration is stored in `~/.ceo-ai/config.json` with restricted file permissions (`0600`).
|
|
73
|
+
|
|
74
|
+
## Commands
|
|
75
|
+
|
|
76
|
+
### `ceo prompt <text>`
|
|
77
|
+
|
|
78
|
+
Send a prompt to your AI agent.
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Basic — returns the presigned URL immediately
|
|
83
|
+
ceo prompt "What was our Q4 revenue?"
|
|
84
|
+
|
|
85
|
+
# Wait for the result
|
|
86
|
+
ceo prompt "What was our Q4 revenue?" --poll
|
|
87
|
+
|
|
88
|
+
# Save result to a file
|
|
89
|
+
ceo prompt "What was our Q4 revenue?" --poll -o result.json
|
|
90
|
+
|
|
91
|
+
# Disable RAG mode
|
|
92
|
+
ceo prompt "What is 2+2?" --no-rag --poll
|
|
93
|
+
|
|
94
|
+
# Raw JSON output (useful for piping)
|
|
95
|
+
ceo prompt "What was our Q4 revenue?" --poll --json
|
|
96
|
+
|
|
97
|
+
# Override API key for a single request
|
|
98
|
+
ceo prompt "Hello" --key sk_live_different_key --poll
|
|
99
|
+
|
|
100
|
+
# Custom polling settings
|
|
101
|
+
ceo prompt "Complex query..." --poll --poll-interval 5000 --poll-timeout 300000
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
**Options:**
|
|
106
|
+
|
|
107
|
+
| Flag | Description | Default |
|
|
108
|
+
|------|-------------|---------|
|
|
109
|
+
| `--poll` | Wait for results instead of returning the presigned URL | `false` |
|
|
110
|
+
| `--poll-interval <ms>` | How often to check for results | `2000` |
|
|
111
|
+
| `--poll-timeout <ms>` | Maximum time to wait for results | `120000` |
|
|
112
|
+
| `--no-rag` | Disable RAG mode (RAG is enabled by default) | — |
|
|
113
|
+
| `-o, --output <file>` | Write results to a file (implies `--poll`) | — |
|
|
114
|
+
| `-k, --key <apiKey>` | Override the configured API key | — |
|
|
115
|
+
| `-e, --endpoint <url>` | Override the configured endpoint | — |
|
|
116
|
+
| `--json` | Output raw JSON | `false` |
|
|
117
|
+
|
|
118
|
+
### `ceo poll <url>`
|
|
119
|
+
|
|
120
|
+
Poll an existing presigned URL for results. Useful if you submitted a prompt earlier and want to check for results later.
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# Poll a presigned URL
|
|
125
|
+
ceo poll "https://ceo-ai-api-output-results.s3.amazonaws.com/api-data/..."
|
|
126
|
+
|
|
127
|
+
# Save to file
|
|
128
|
+
ceo poll "https://..." -o result.json
|
|
129
|
+
|
|
130
|
+
# Custom interval and timeout
|
|
131
|
+
ceo poll "https://..." --interval 5000 --timeout 300000
|
|
132
|
+
|
|
133
|
+
# Raw JSON output
|
|
134
|
+
ceo poll "https://..." --json
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
**Options:**
|
|
139
|
+
|
|
140
|
+
| Flag | Description | Default |
|
|
141
|
+
|------|-------------|---------|
|
|
142
|
+
| `--interval <ms>` | Polling interval | `2000` |
|
|
143
|
+
| `--timeout <ms>` | Maximum wait time | `120000` |
|
|
144
|
+
| `-o, --output <file>` | Write results to a file | — |
|
|
145
|
+
| `--json` | Output raw JSON | `false` |
|
|
146
|
+
|
|
147
|
+
### `ceo configure`
|
|
148
|
+
|
|
149
|
+
Set your API key and endpoint.
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
# Interactive
|
|
154
|
+
ceo configure
|
|
155
|
+
|
|
156
|
+
# Inline
|
|
157
|
+
ceo configure --key sk_live_abc123
|
|
158
|
+
ceo configure --endpoint https://custom.endpoint.com
|
|
159
|
+
ceo configure --key sk_live_abc123 --endpoint https://custom.endpoint.com
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
### `ceo config:show`
|
|
164
|
+
|
|
165
|
+
Display the current configuration (API key is masked).
|
|
166
|
+
|
|
167
|
+
## Examples
|
|
168
|
+
|
|
169
|
+
### Basic Prompt and Poll
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
$ ceo prompt "Summarize our sales performance this quarter" --poll
|
|
174
|
+
|
|
175
|
+
✓ Request submitted
|
|
176
|
+
Agent: Sales Analyst
|
|
177
|
+
Model: gpt-4
|
|
178
|
+
Credits: 1500
|
|
179
|
+
⠋ Waiting for results... (attempt 12)
|
|
180
|
+
✓ Results received
|
|
181
|
+
|
|
182
|
+
--- Response ---
|
|
183
|
+
|
|
184
|
+
Based on the available data, here are the key highlights...
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
### Save Results to File
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
$ ceo prompt "Generate a detailed financial report" --poll -o report.json
|
|
193
|
+
✓ Request submitted
|
|
194
|
+
✓ Results received
|
|
195
|
+
|
|
196
|
+
✓ Results written to /Users/you/report.json
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
### Pipe JSON Output
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
# Pipe to jq for processing
|
|
205
|
+
ceo prompt "List all customers" --poll --json | jq '.response.customers'
|
|
206
|
+
|
|
207
|
+
# Pipe to another command
|
|
208
|
+
ceo prompt "Get metrics" --poll --json > metrics.json
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
### Two-Step Workflow
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
# Step 1: Submit prompt, get presigned URL
|
|
217
|
+
$ ceo prompt "Long running analysis..."
|
|
218
|
+
|
|
219
|
+
✓ Request submitted
|
|
220
|
+
Agent: Data Analyst
|
|
221
|
+
Model: claude-sonnet-4-5
|
|
222
|
+
Credits: 3000
|
|
223
|
+
|
|
224
|
+
Presigned URL:
|
|
225
|
+
https://ceo-ai-api-output-results.s3.amazonaws.com/api-data/abc/def/1234.json?X-Amz-...
|
|
226
|
+
|
|
227
|
+
Use --poll to wait for results, or fetch the URL when ready.
|
|
228
|
+
|
|
229
|
+
# Step 2: Later, poll for results
|
|
230
|
+
$ ceo poll "https://ceo-ai-api-output-results.s3.amazonaws.com/api-data/abc/def/1234.json?X-Amz-..."
|
|
231
|
+
|
|
232
|
+
✓ Results received
|
|
233
|
+
|
|
234
|
+
--- Response ---
|
|
235
|
+
|
|
236
|
+
Analysis complete...
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
### Environment Variable Override
|
|
241
|
+
|
|
242
|
+
You can also provide your API key via an environment variable:
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
CEO_API_KEY=sk_live_abc123 ceo prompt "Hello" --poll
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
## Error Handling
|
|
251
|
+
|
|
252
|
+
The CLI provides clear error messages:
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
# No API key configured
|
|
257
|
+
$ ceo prompt "Hello"
|
|
258
|
+
Error: No API key configured. Run: ceo configure
|
|
259
|
+
|
|
260
|
+
# Invalid API key
|
|
261
|
+
$ ceo prompt "Hello" --key invalid_key
|
|
262
|
+
Error: Invalid API key format
|
|
263
|
+
|
|
264
|
+
# Insufficient credits
|
|
265
|
+
$ ceo prompt "Hello" --poll
|
|
266
|
+
✗ Request failed
|
|
267
|
+
Error: You need 1500 credits but only have 200 available.
|
|
268
|
+
Details: {"requiredCredits":1500,"availableCredits":200,"deficit":1300}
|
|
269
|
+
|
|
270
|
+
# Polling timeout
|
|
271
|
+
$ ceo prompt "Hello" --poll --poll-timeout 5000
|
|
272
|
+
✗ Polling failed
|
|
273
|
+
Error: Polling timed out after 5000ms (3 attempts)
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
## Getting Your API Key
|
|
278
|
+
|
|
279
|
+
1. Log in to [CEO.AI Dashboard](https://app.ceo.ai)
|
|
280
|
+
2. Navigate to **API Keys**
|
|
281
|
+
3. Click **Create API Key** (NB: you must have a paid subscription to create api keys)
|
|
282
|
+
4. Select the agent you want to use
|
|
283
|
+
5. Copy the generated key (it will only be shown once)
|
|
284
|
+
|
|
285
|
+
## Uninstall
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
npm uninstall -g @ceo-ai/cli
|
|
290
|
+
|
|
291
|
+
# Optionally remove config
|
|
292
|
+
rm -rf ~/.ceo-ai
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
## Requirements
|
|
297
|
+
|
|
298
|
+
- Node.js 16 or later
|
|
299
|
+
- A CEO.AI account with an active API key and sufficient credits
|
|
300
|
+
|
|
301
|
+
|
package/bin/ceo.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const { CeoClient } = require('../lib/client');
|
|
6
|
+
const { ConfigManager } = require('../lib/config');
|
|
7
|
+
const ora = require('ora');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const pkg = require('../package.json');
|
|
11
|
+
|
|
12
|
+
const program = new Command();
|
|
13
|
+
const config = new ConfigManager();
|
|
14
|
+
|
|
15
|
+
program
|
|
16
|
+
.name('ceo')
|
|
17
|
+
.description('CLI for CEO.AI API')
|
|
18
|
+
.version(pkg.version);
|
|
19
|
+
|
|
20
|
+
// ---- Configure API key ----
|
|
21
|
+
|
|
22
|
+
program
|
|
23
|
+
.command('configure')
|
|
24
|
+
.description('Set your API key and endpoint')
|
|
25
|
+
.option('-k, --key <apiKey>', 'API key (sk_live_...)')
|
|
26
|
+
.option('-e, --endpoint <url>', 'API endpoint URL')
|
|
27
|
+
.action(async (opts) => {
|
|
28
|
+
if (opts.key) {
|
|
29
|
+
config.set('apiKey', opts.key);
|
|
30
|
+
console.log(chalk.green('✓ API key saved'));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (opts.endpoint) {
|
|
34
|
+
config.set('endpoint', opts.endpoint);
|
|
35
|
+
console.log(chalk.green('✓ Endpoint saved'));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Only go interactive if neither flag was provided
|
|
39
|
+
if (!opts.key && !opts.endpoint) {
|
|
40
|
+
const readline = require('readline');
|
|
41
|
+
const rl = readline.createInterface({
|
|
42
|
+
input: process.stdin,
|
|
43
|
+
output: process.stdout
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const ask = (question) => new Promise((resolve) => {
|
|
47
|
+
rl.question(question, resolve);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const apiKey = await ask('Enter your API key (sk_live_...): ');
|
|
51
|
+
if (apiKey.trim()) {
|
|
52
|
+
config.set('apiKey', apiKey.trim());
|
|
53
|
+
console.log(chalk.green('✓ API key saved'));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const endpoint = await ask(`API endpoint [${config.get('endpoint') || 'https://injestion.api.ceo.ai'}]: `);
|
|
57
|
+
if (endpoint.trim()) {
|
|
58
|
+
config.set('endpoint', endpoint.trim());
|
|
59
|
+
console.log(chalk.green('✓ Endpoint saved'));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
rl.close();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ---- Show current config ----
|
|
67
|
+
|
|
68
|
+
program
|
|
69
|
+
.command('config:show')
|
|
70
|
+
.description('Show current configuration')
|
|
71
|
+
.action(() => {
|
|
72
|
+
const apiKey = config.get('apiKey');
|
|
73
|
+
const endpoint = config.get('endpoint') || 'https://injestion.api.ceo.ai';
|
|
74
|
+
|
|
75
|
+
console.log(chalk.bold('\nCurrent Configuration:\n'));
|
|
76
|
+
console.log(` Endpoint: ${chalk.cyan(endpoint)}`);
|
|
77
|
+
|
|
78
|
+
if (apiKey) {
|
|
79
|
+
const masked = apiKey.substring(0, 12) + '••••••••' + apiKey.substring(apiKey.length - 4);
|
|
80
|
+
console.log(` API Key: ${chalk.cyan(masked)}`);
|
|
81
|
+
} else {
|
|
82
|
+
console.log(` API Key: ${chalk.yellow('Not set')}`);
|
|
83
|
+
}
|
|
84
|
+
console.log();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// ---- Send a prompt ----
|
|
88
|
+
|
|
89
|
+
program
|
|
90
|
+
.command('prompt')
|
|
91
|
+
.description('Send a prompt to your agent')
|
|
92
|
+
.argument('<text>', 'The prompt text to send')
|
|
93
|
+
.option('--no-rag', 'Disable RAG mode')
|
|
94
|
+
.option('--poll', 'Poll for results and display them')
|
|
95
|
+
.option('--poll-interval <ms>', 'Polling interval in milliseconds', '2000')
|
|
96
|
+
.option('--poll-timeout <ms>', 'Max time to wait for results', '120000')
|
|
97
|
+
.option('-o, --output <file>', 'Write result to file (implies --poll)')
|
|
98
|
+
.option('-k, --key <apiKey>', 'Override configured API key')
|
|
99
|
+
.option('-e, --endpoint <url>', 'Override configured endpoint')
|
|
100
|
+
.option('--json', 'Output raw JSON response')
|
|
101
|
+
.action(async (text, opts) => {
|
|
102
|
+
const apiKey = opts.key || config.get('apiKey');
|
|
103
|
+
const endpoint = opts.endpoint || config.get('endpoint') || 'https://injestion.api.ceo.ai';
|
|
104
|
+
|
|
105
|
+
if (!apiKey) {
|
|
106
|
+
console.error(chalk.red('Error: No API key configured. Run: ceo configure'));
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const client = new CeoClient({ apiKey, endpoint });
|
|
111
|
+
const spinner = ora('Sending prompt...').start();
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const result = await client.prompt(text, {
|
|
115
|
+
ragMode: opts.rag !== false
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
spinner.succeed('Request submitted');
|
|
119
|
+
|
|
120
|
+
if (!opts.json) {
|
|
121
|
+
console.log(chalk.dim(` Agent: ${result.agentName || result.agentId}`));
|
|
122
|
+
console.log(chalk.dim(` Model: ${result.model || 'N/A'}`));
|
|
123
|
+
console.log(chalk.dim(` Credits: ${result.estimatedCredits || 'N/A'}`));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// If --poll or --output, wait for results
|
|
127
|
+
if (opts.poll || opts.output) {
|
|
128
|
+
spinner.start('Waiting for results...');
|
|
129
|
+
|
|
130
|
+
const pollInterval = parseInt(opts.pollInterval);
|
|
131
|
+
const pollTimeout = parseInt(opts.pollTimeout);
|
|
132
|
+
|
|
133
|
+
const response = await client.pollForResult(result.presignedUrl, {
|
|
134
|
+
interval: pollInterval,
|
|
135
|
+
timeout: pollTimeout,
|
|
136
|
+
onPoll: (attempt) => {
|
|
137
|
+
spinner.text = `Waiting for results... (attempt ${attempt})`;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
spinner.succeed('Results received');
|
|
142
|
+
|
|
143
|
+
if (opts.output) {
|
|
144
|
+
const outputPath = path.resolve(opts.output);
|
|
145
|
+
const content = typeof response === 'string' ? response : JSON.stringify(response, null, 2);
|
|
146
|
+
fs.writeFileSync(outputPath, content);
|
|
147
|
+
console.log(chalk.green(`\n✓ Results written to ${outputPath}`));
|
|
148
|
+
} else if (opts.json) {
|
|
149
|
+
console.log(JSON.stringify(response, null, 2));
|
|
150
|
+
} else {
|
|
151
|
+
console.log(chalk.bold('\n--- Response ---\n'));
|
|
152
|
+
const content = typeof response === 'string' ? response : JSON.stringify(response, null, 2);
|
|
153
|
+
console.log(content);
|
|
154
|
+
console.log();
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
// Just show the presigned URL
|
|
158
|
+
if (opts.json) {
|
|
159
|
+
console.log(JSON.stringify(result, null, 2));
|
|
160
|
+
} else {
|
|
161
|
+
console.log(chalk.bold('\n Presigned URL:'));
|
|
162
|
+
console.log(chalk.cyan(` ${result.presignedUrl}`));
|
|
163
|
+
console.log(chalk.dim('\n Use --poll to wait for results, or fetch the URL when ready.\n'));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
} catch (error) {
|
|
168
|
+
spinner.fail('Request failed');
|
|
169
|
+
console.error(chalk.red(`\n Error: ${error.message}`));
|
|
170
|
+
|
|
171
|
+
if (error.details) {
|
|
172
|
+
console.error(chalk.yellow(` Details: ${JSON.stringify(error.details)}`));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// ---- Poll an existing presigned URL ----
|
|
180
|
+
|
|
181
|
+
program
|
|
182
|
+
.command('poll')
|
|
183
|
+
.description('Poll an existing presigned URL for results')
|
|
184
|
+
.argument('<url>', 'Presigned URL to poll')
|
|
185
|
+
.option('--interval <ms>', 'Polling interval in milliseconds', '2000')
|
|
186
|
+
.option('--timeout <ms>', 'Max time to wait', '120000')
|
|
187
|
+
.option('-o, --output <file>', 'Write result to file')
|
|
188
|
+
.option('--json', 'Output raw JSON')
|
|
189
|
+
.action(async (url, opts) => {
|
|
190
|
+
const client = new CeoClient({ apiKey: 'unused', endpoint: 'unused' });
|
|
191
|
+
const spinner = ora('Polling for results...').start();
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const response = await client.pollForResult(url, {
|
|
195
|
+
interval: parseInt(opts.interval),
|
|
196
|
+
timeout: parseInt(opts.timeout),
|
|
197
|
+
onPoll: (attempt) => {
|
|
198
|
+
spinner.text = `Polling for results... (attempt ${attempt})`;
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
spinner.succeed('Results received');
|
|
203
|
+
|
|
204
|
+
if (opts.output) {
|
|
205
|
+
const outputPath = path.resolve(opts.output);
|
|
206
|
+
const content = typeof response === 'string' ? response : JSON.stringify(response, null, 2);
|
|
207
|
+
fs.writeFileSync(outputPath, content);
|
|
208
|
+
console.log(chalk.green(`\n✓ Results written to ${outputPath}`));
|
|
209
|
+
} else if (opts.json) {
|
|
210
|
+
console.log(JSON.stringify(response, null, 2));
|
|
211
|
+
} else {
|
|
212
|
+
console.log(chalk.bold('\n--- Response ---\n'));
|
|
213
|
+
const content = typeof response === 'string' ? response : JSON.stringify(response, null, 2);
|
|
214
|
+
console.log(content);
|
|
215
|
+
console.log();
|
|
216
|
+
}
|
|
217
|
+
} catch (error) {
|
|
218
|
+
spinner.fail('Polling failed');
|
|
219
|
+
console.error(chalk.red(`\n Error: ${error.message}`));
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
program.parse();
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
const https = require('https');
|
|
2
|
+
const http = require('http');
|
|
3
|
+
|
|
4
|
+
class CeoClient {
|
|
5
|
+
constructor({ apiKey, endpoint }) {
|
|
6
|
+
this.apiKey = apiKey;
|
|
7
|
+
this.endpoint = endpoint;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Send a prompt
|
|
12
|
+
*/
|
|
13
|
+
async prompt(text, options = {}) {
|
|
14
|
+
const body = {
|
|
15
|
+
prompt: text,
|
|
16
|
+
...(options.ragMode !== undefined && { ragMode: options.ragMode }),
|
|
17
|
+
...(options.conversationHistory && { conversationHistory: options.conversationHistory })
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const response = await this._request('POST', this.endpoint, body);
|
|
21
|
+
|
|
22
|
+
if (response.error) {
|
|
23
|
+
const error = new Error(response.error || response.message);
|
|
24
|
+
error.details = response.details;
|
|
25
|
+
error.statusCode = response.statusCode;
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return response.data || response;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Poll a presigned URL until content is available
|
|
34
|
+
*/
|
|
35
|
+
async pollForResult(presignedUrl, options = {}) {
|
|
36
|
+
const {
|
|
37
|
+
interval = 2000,
|
|
38
|
+
timeout = 120000,
|
|
39
|
+
onPoll = null
|
|
40
|
+
} = options;
|
|
41
|
+
|
|
42
|
+
const startTime = Date.now();
|
|
43
|
+
let attempt = 0;
|
|
44
|
+
|
|
45
|
+
while (Date.now() - startTime < timeout) {
|
|
46
|
+
attempt++;
|
|
47
|
+
|
|
48
|
+
if (onPoll) {
|
|
49
|
+
onPoll(attempt);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const result = await this._fetch(presignedUrl);
|
|
54
|
+
// S3 returns the content when ready
|
|
55
|
+
if (result !== null) {
|
|
56
|
+
// Try to parse as JSON
|
|
57
|
+
try {
|
|
58
|
+
return JSON.parse(result);
|
|
59
|
+
} catch {
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
// 403/404 means the object doesn't exist yet - keep polling
|
|
65
|
+
if (error.statusCode === 403 || error.statusCode === 404) {
|
|
66
|
+
// Expected - not ready yet
|
|
67
|
+
} else {
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
await this._sleep(interval);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
throw new Error(`Polling timed out after ${timeout}ms (${attempt} attempts)`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Make an HTTP request to the API
|
|
80
|
+
*/
|
|
81
|
+
_request(method, url, body = null) {
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const parsedUrl = new URL(url);
|
|
84
|
+
const isHttps = parsedUrl.protocol === 'https:';
|
|
85
|
+
const lib = isHttps ? https : http;
|
|
86
|
+
|
|
87
|
+
const payload = body ? JSON.stringify(body) : null;
|
|
88
|
+
|
|
89
|
+
const options = {
|
|
90
|
+
hostname: parsedUrl.hostname,
|
|
91
|
+
port: parsedUrl.port || (isHttps ? 443 : 80),
|
|
92
|
+
path: parsedUrl.pathname + parsedUrl.search,
|
|
93
|
+
method,
|
|
94
|
+
headers: {
|
|
95
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
96
|
+
'Content-Type': 'application/json',
|
|
97
|
+
...(payload && { 'Content-Length': Buffer.byteLength(payload) })
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const req = lib.request(options, (res) => {
|
|
102
|
+
let data = '';
|
|
103
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
104
|
+
res.on('end', () => {
|
|
105
|
+
try {
|
|
106
|
+
const parsed = JSON.parse(data);
|
|
107
|
+
if (res.statusCode >= 400) {
|
|
108
|
+
parsed.statusCode = res.statusCode;
|
|
109
|
+
}
|
|
110
|
+
resolve(parsed);
|
|
111
|
+
} catch {
|
|
112
|
+
resolve({ data: data, statusCode: res.statusCode });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
req.on('error', reject);
|
|
118
|
+
req.setTimeout(30000, () => {
|
|
119
|
+
req.destroy();
|
|
120
|
+
reject(new Error('Request timed out'));
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (payload) req.write(payload);
|
|
124
|
+
req.end();
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Fetch a URL (for presigned URL polling)
|
|
130
|
+
*/
|
|
131
|
+
_fetch(url) {
|
|
132
|
+
return new Promise((resolve, reject) => {
|
|
133
|
+
const parsedUrl = new URL(url);
|
|
134
|
+
const isHttps = parsedUrl.protocol === 'https:';
|
|
135
|
+
const lib = isHttps ? https : http;
|
|
136
|
+
|
|
137
|
+
const req = lib.get(url, (res) => {
|
|
138
|
+
if (res.statusCode === 403 || res.statusCode === 404) {
|
|
139
|
+
const error = new Error('Not ready');
|
|
140
|
+
error.statusCode = res.statusCode;
|
|
141
|
+
res.resume(); // Drain response
|
|
142
|
+
reject(error);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let data = '';
|
|
147
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
148
|
+
res.on('end', () => {
|
|
149
|
+
if (res.statusCode >= 200 && res.statusCode < 300 && data.length > 0) {
|
|
150
|
+
resolve(data);
|
|
151
|
+
} else {
|
|
152
|
+
resolve(null);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
req.on('error', reject);
|
|
158
|
+
req.setTimeout(10000, () => {
|
|
159
|
+
req.destroy();
|
|
160
|
+
reject(new Error('Fetch timed out'));
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
_sleep(ms) {
|
|
166
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = { CeoClient };
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
class ConfigManager {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.configDir = path.join(os.homedir(), '.ceo-ai');
|
|
8
|
+
this.configFile = path.join(this.configDir, 'config.json');
|
|
9
|
+
this._ensureConfigDir();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
_ensureConfigDir() {
|
|
13
|
+
if (!fs.existsSync(this.configDir)) {
|
|
14
|
+
fs.mkdirSync(this.configDir, { recursive: true, mode: 0o700 });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_read() {
|
|
19
|
+
try {
|
|
20
|
+
if (fs.existsSync(this.configFile)) {
|
|
21
|
+
const content = fs.readFileSync(this.configFile, 'utf8');
|
|
22
|
+
return JSON.parse(content);
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
// Corrupted config, start fresh
|
|
26
|
+
}
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_write(data) {
|
|
31
|
+
fs.writeFileSync(
|
|
32
|
+
this.configFile,
|
|
33
|
+
JSON.stringify(data, null, 2),
|
|
34
|
+
{ mode: 0o600 } // Owner read/write only
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get(key) {
|
|
39
|
+
const data = this._read();
|
|
40
|
+
return data[key];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
set(key, value) {
|
|
44
|
+
const data = this._read();
|
|
45
|
+
data[key] = value;
|
|
46
|
+
this._write(data);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getAll() {
|
|
50
|
+
return this._read();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
clear() {
|
|
54
|
+
this._write({});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { ConfigManager };
|
package/package.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name":"@ceo-ai/cli","version":"1.0.2","description":"CLI tool for CEO.AI API","bin":{"ceo":"./bin/ceo.js"},"files":["bin/","lib/"],"scripts":{"test":"node test/index.test.js"},"keywords":["ceo-ai","cli","ai","api"],"license":"MIT","dependencies":{"commander":"^12.1.0","chalk":"^4.1.2","conf":"^10.2.0","ora":"^5.4.1"}}
|