@vtriv/cli 0.1.1 → 0.1.3
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 +240 -30
- package/index.ts +341 -93
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# vtriv CLI
|
|
2
2
|
|
|
3
|
-
Command-line interface for vtriv backend services.
|
|
3
|
+
Command-line interface for vtriv backend services. Uses account-based authentication to manage projects.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -16,53 +16,263 @@ bun link
|
|
|
16
16
|
vtriv --help
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
## Authentication Model
|
|
20
|
+
|
|
21
|
+
The CLI uses a two-tier authentication system:
|
|
22
|
+
|
|
23
|
+
1. **Account Key** (`vtriv_ak_...`) - Used to create and manage projects
|
|
24
|
+
2. **API Keys** (`vtriv_sk_...`) - Per-project, used to mint JWTs for service calls
|
|
25
|
+
|
|
26
|
+
Configuration is stored in `~/.config/vtriv/config.json` with `0600` permissions.
|
|
27
|
+
|
|
28
|
+
### How It Works
|
|
29
|
+
|
|
30
|
+
1. Run `vtriv config` to set up your account key
|
|
31
|
+
2. Run `vtriv init <name>` to create a project and API key
|
|
32
|
+
3. CLI uses the API key to mint short-lived JWTs
|
|
33
|
+
4. JWTs are cached for 55 minutes (tokens valid for 60)
|
|
34
|
+
|
|
35
|
+
Project names are automatically prefixed with your account slug (e.g., `a1b2c3d4-my-app`), but you work with short names in the CLI.
|
|
36
|
+
|
|
19
37
|
## Setup
|
|
20
38
|
|
|
39
|
+
### Configure Account
|
|
40
|
+
|
|
21
41
|
```bash
|
|
22
|
-
|
|
23
|
-
vtriv init
|
|
42
|
+
vtriv config
|
|
24
43
|
```
|
|
25
44
|
|
|
26
|
-
|
|
45
|
+
You'll be prompted for:
|
|
46
|
+
- Base URL (default: `http://localhost:3000`)
|
|
47
|
+
- Account key (`vtriv_ak_...`)
|
|
48
|
+
|
|
49
|
+
The CLI verifies the account key and fetches your account slug.
|
|
27
50
|
|
|
28
|
-
|
|
51
|
+
### Create a Project
|
|
29
52
|
|
|
30
53
|
```bash
|
|
31
|
-
|
|
32
|
-
|
|
54
|
+
vtriv init my-app
|
|
55
|
+
```
|
|
33
56
|
|
|
34
|
-
|
|
35
|
-
|
|
57
|
+
This:
|
|
58
|
+
1. Creates project `{slug}-my-app` on the server
|
|
59
|
+
2. Creates an API key for the project
|
|
60
|
+
3. Saves the profile locally
|
|
36
61
|
|
|
37
|
-
|
|
38
|
-
vtriv auth user:create my-app admin@example.com secretpass
|
|
62
|
+
### Multiple Projects
|
|
39
63
|
|
|
40
|
-
|
|
41
|
-
|
|
64
|
+
```bash
|
|
65
|
+
# Create another project
|
|
66
|
+
vtriv init staging --default
|
|
42
67
|
|
|
43
|
-
#
|
|
44
|
-
|
|
68
|
+
# Use a specific profile
|
|
69
|
+
vtriv --profile my-app db ls posts
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### View Status
|
|
45
73
|
|
|
46
|
-
|
|
47
|
-
vtriv
|
|
74
|
+
```bash
|
|
75
|
+
vtriv status
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Shows your account, configured profiles, and which is default.
|
|
79
|
+
|
|
80
|
+
### Manage Projects
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# List all projects
|
|
84
|
+
vtriv project ls
|
|
85
|
+
|
|
86
|
+
# Delete a project
|
|
87
|
+
vtriv project rm my-app
|
|
48
88
|
```
|
|
49
89
|
|
|
50
90
|
## Commands
|
|
51
91
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
92
|
+
### Global Options
|
|
93
|
+
|
|
94
|
+
| Option | Description |
|
|
95
|
+
|--------|-------------|
|
|
96
|
+
| `--json` | Output raw JSON (for scripts/agents) |
|
|
97
|
+
| `--profile <name>` | Use a specific profile |
|
|
98
|
+
| `--debug` | Show HTTP curl equivalents |
|
|
99
|
+
|
|
100
|
+
### Token Commands
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# Mint and display a JWT
|
|
104
|
+
vtriv token
|
|
105
|
+
|
|
106
|
+
# Mint with specific template
|
|
107
|
+
vtriv token --template ai-enabled
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Template Commands
|
|
111
|
+
|
|
112
|
+
Manage token templates for your project.
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# List all templates
|
|
116
|
+
vtriv template ls
|
|
117
|
+
|
|
118
|
+
# Get a specific template
|
|
119
|
+
vtriv template get default
|
|
120
|
+
|
|
121
|
+
# Create/update a template (reads JSON from stdin)
|
|
122
|
+
vtriv template set ai-enabled <<< '{"x-db": true, "x-blob": true, "x-ai": true}'
|
|
123
|
+
|
|
124
|
+
# Delete a template (cannot delete 'default')
|
|
125
|
+
vtriv template rm ai-enabled
|
|
126
|
+
|
|
127
|
+
# Reset templates to server defaults (useful after server updates)
|
|
128
|
+
vtriv template reset
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Database Commands
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
# List documents
|
|
135
|
+
vtriv db ls posts
|
|
136
|
+
vtriv db ls posts --filter '{"status":"published"}' --limit 10 --sort -_created_at
|
|
137
|
+
|
|
138
|
+
# Get a document
|
|
139
|
+
vtriv db get posts abc-123
|
|
140
|
+
|
|
141
|
+
# Create a document (reads JSON from stdin)
|
|
142
|
+
echo '{"title":"Hello","body":"World"}' | vtriv db put posts
|
|
143
|
+
|
|
144
|
+
# Update a document
|
|
145
|
+
echo '{"title":"Updated"}' | vtriv db put posts abc-123
|
|
146
|
+
|
|
147
|
+
# Delete a document
|
|
148
|
+
vtriv db rm posts abc-123
|
|
149
|
+
|
|
150
|
+
# Get/set collection schema
|
|
151
|
+
vtriv db schema posts
|
|
152
|
+
echo '{"indexes":["status"],"unique":["slug"]}' | vtriv db schema posts --set
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Blob Commands
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
# Upload a file
|
|
159
|
+
vtriv blob put images/photo.jpg ./local-photo.jpg
|
|
160
|
+
|
|
161
|
+
# Download a file (outputs to stdout)
|
|
162
|
+
vtriv blob get images/photo.jpg > downloaded.jpg
|
|
163
|
+
|
|
164
|
+
# Delete a file
|
|
165
|
+
vtriv blob rm images/photo.jpg
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Cron Commands
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
# List jobs
|
|
172
|
+
vtriv cron ls
|
|
173
|
+
|
|
174
|
+
# View run history
|
|
175
|
+
vtriv cron runs
|
|
176
|
+
vtriv cron runs --limit 50 --job daily-backup
|
|
177
|
+
|
|
178
|
+
# Manually trigger a job
|
|
179
|
+
vtriv cron trigger daily-backup
|
|
180
|
+
|
|
181
|
+
# View a script
|
|
182
|
+
vtriv cron script:cat backup.ts
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### AI Commands
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
# Simple chat
|
|
189
|
+
vtriv ai chat "What is the capital of France?"
|
|
190
|
+
|
|
191
|
+
# With specific model
|
|
192
|
+
vtriv ai chat "Explain quantum computing" --model anthropic/claude-3.5-sonnet
|
|
193
|
+
|
|
194
|
+
# View usage statistics
|
|
195
|
+
vtriv ai stats
|
|
196
|
+
vtriv ai stats --period 7
|
|
197
|
+
|
|
198
|
+
# Get/set AI config
|
|
199
|
+
vtriv ai config
|
|
200
|
+
vtriv ai config --model openai/gpt-4o --rate-limit 10.0
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Search Commands
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
# Get index configuration
|
|
207
|
+
vtriv search config posts
|
|
208
|
+
|
|
209
|
+
# Set index configuration (reads JSON from stdin)
|
|
210
|
+
echo '{"model":"openai/text-embedding-3-small","dimensions":1536,"input_fields":["title","body"],"store_fields":["id","title","slug"]}' | vtriv search config posts --set
|
|
211
|
+
|
|
212
|
+
# Search an index
|
|
213
|
+
vtriv search query posts "semantic search" --limit 20
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Output Formats
|
|
217
|
+
|
|
218
|
+
By default, the CLI formats output as tables:
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
$ vtriv db ls posts
|
|
222
|
+
id title status
|
|
223
|
+
-----------------------------------------
|
|
224
|
+
abc-123 Hello World published
|
|
225
|
+
def-456 Draft Post draft
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Use `--json` for machine-readable output:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
$ vtriv db ls posts --json
|
|
232
|
+
[
|
|
233
|
+
{"id": "abc-123", "title": "Hello World", "status": "published"},
|
|
234
|
+
{"id": "def-456", "title": "Draft Post", "status": "draft"}
|
|
235
|
+
]
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Debugging
|
|
239
|
+
|
|
240
|
+
Use `--debug` to see the curl equivalent of each request:
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
$ vtriv --debug db ls posts
|
|
244
|
+
curl -X GET "http://localhost:3000/db/a1b2c3d4-my-app/posts" -H "Authorization: Bearer <token>"
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Configuration File
|
|
248
|
+
|
|
249
|
+
The config file at `~/.config/vtriv/config.json`:
|
|
250
|
+
|
|
251
|
+
```json
|
|
252
|
+
{
|
|
253
|
+
"baseUrl": "http://localhost:3000",
|
|
254
|
+
"accountKey": "vtriv_ak_...",
|
|
255
|
+
"accountSlug": "a1b2c3d4",
|
|
256
|
+
"default": "my-app",
|
|
257
|
+
"profiles": {
|
|
258
|
+
"my-app": {
|
|
259
|
+
"apiKey": "vtriv_sk_..."
|
|
260
|
+
},
|
|
261
|
+
"staging": {
|
|
262
|
+
"apiKey": "vtriv_sk_..."
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Migration from Old Config
|
|
59
269
|
|
|
60
|
-
|
|
270
|
+
If you have an old `~/.vtrivrc` file, you'll need to:
|
|
271
|
+
1. Run `vtriv config` with your account key
|
|
272
|
+
2. Run `vtriv init <name>` for each project
|
|
61
273
|
|
|
62
|
-
|
|
63
|
-
- `--profile <name>` - Use a specific profile
|
|
64
|
-
- `--debug` - Show HTTP curl equivalents
|
|
274
|
+
The old config format is not compatible with the new account-based system.
|
|
65
275
|
|
|
66
|
-
##
|
|
276
|
+
## License
|
|
67
277
|
|
|
68
|
-
|
|
278
|
+
Private - Part of vtriv meta-project
|
package/index.ts
CHANGED
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* vtriv-cli - Command-line interface for vtriv services
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Configuration hierarchy:
|
|
6
|
+
* 1. Account key (vtriv_ak_*) - set via `vtriv config`
|
|
7
|
+
* 2. Project profiles - created via `vtriv init <name>`
|
|
8
|
+
*
|
|
9
|
+
* The CLI auto-creates projects prefixed with account slug,
|
|
10
|
+
* and translates short names to full names transparently.
|
|
8
11
|
*/
|
|
9
12
|
|
|
10
|
-
import { existsSync, readFileSync, writeFileSync, chmodSync } from "node:fs";
|
|
13
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from "node:fs";
|
|
11
14
|
import { homedir } from "node:os";
|
|
12
|
-
import { join } from "node:path";
|
|
15
|
+
import { join, dirname } from "node:path";
|
|
13
16
|
import { Command } from "commander";
|
|
14
17
|
import chalk from "chalk";
|
|
15
18
|
|
|
@@ -18,13 +21,14 @@ import chalk from "chalk";
|
|
|
18
21
|
// =============================================================================
|
|
19
22
|
|
|
20
23
|
interface Profile {
|
|
21
|
-
baseUrl: string;
|
|
22
|
-
project: string;
|
|
23
24
|
apiKey: string; // vtriv_sk_*
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
interface Config {
|
|
27
|
-
|
|
28
|
+
baseUrl: string;
|
|
29
|
+
accountKey?: string; // vtriv_ak_*
|
|
30
|
+
accountSlug?: string;
|
|
31
|
+
default?: string;
|
|
28
32
|
profiles: Record<string, Profile>;
|
|
29
33
|
}
|
|
30
34
|
|
|
@@ -38,36 +42,55 @@ interface GlobalOptions {
|
|
|
38
42
|
// Configuration
|
|
39
43
|
// =============================================================================
|
|
40
44
|
|
|
41
|
-
|
|
45
|
+
function getConfigPath(): string {
|
|
46
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
47
|
+
return join(xdgConfig, "vtriv", "config.json");
|
|
48
|
+
}
|
|
42
49
|
|
|
43
50
|
function loadConfig(): Config | null {
|
|
44
|
-
|
|
51
|
+
const configPath = getConfigPath();
|
|
52
|
+
if (!existsSync(configPath)) return null;
|
|
45
53
|
try {
|
|
46
|
-
return JSON.parse(readFileSync(
|
|
54
|
+
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
47
55
|
} catch {
|
|
48
56
|
return null;
|
|
49
57
|
}
|
|
50
58
|
}
|
|
51
59
|
|
|
52
60
|
function saveConfig(config: Config): void {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
const configPath = getConfigPath();
|
|
62
|
+
const configDir = dirname(configPath);
|
|
63
|
+
if (!existsSync(configDir)) {
|
|
64
|
+
mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
65
|
+
}
|
|
66
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
67
|
+
chmodSync(configPath, 0o600);
|
|
56
68
|
}
|
|
57
69
|
|
|
58
|
-
function
|
|
70
|
+
function getConfig(): Config {
|
|
59
71
|
const config = loadConfig();
|
|
60
72
|
if (!config) {
|
|
61
|
-
console.error(chalk.red("No config found. Run 'vtriv
|
|
73
|
+
console.error(chalk.red("No config found. Run 'vtriv config' first."));
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
return config;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getProfile(profileName?: string): { config: Config; profile: Profile; fullProjectName: string; shortName: string } {
|
|
80
|
+
const config = getConfig();
|
|
81
|
+
const shortName = profileName || config.default;
|
|
82
|
+
if (!shortName) {
|
|
83
|
+
console.error(chalk.red("No default profile. Run 'vtriv init <name>' to create one."));
|
|
62
84
|
process.exit(1);
|
|
63
85
|
}
|
|
64
|
-
const
|
|
65
|
-
const profile = config.profiles[name];
|
|
86
|
+
const profile = config.profiles[shortName];
|
|
66
87
|
if (!profile) {
|
|
67
|
-
console.error(chalk.red(`Profile '${
|
|
88
|
+
console.error(chalk.red(`Profile '${shortName}' not found. Run 'vtriv init ${shortName}' to create it.`));
|
|
68
89
|
process.exit(1);
|
|
69
90
|
}
|
|
70
|
-
|
|
91
|
+
// Expand short name to full project name
|
|
92
|
+
const fullProjectName = config.accountSlug ? `${config.accountSlug}-${shortName}` : shortName;
|
|
93
|
+
return { config, profile, fullProjectName, shortName };
|
|
71
94
|
}
|
|
72
95
|
|
|
73
96
|
// =============================================================================
|
|
@@ -127,10 +150,10 @@ function error(msg: string, opts: GlobalOptions): never {
|
|
|
127
150
|
const tokenCache = new Map<string, { token: string; expires: number }>();
|
|
128
151
|
|
|
129
152
|
async function getToken(opts: GlobalOptions, template?: string): Promise<string> {
|
|
130
|
-
const profile = getProfile(opts.profile);
|
|
153
|
+
const { config, profile, fullProjectName } = getProfile(opts.profile);
|
|
131
154
|
|
|
132
155
|
// Cache key includes template
|
|
133
|
-
const cacheKey = template ? `${
|
|
156
|
+
const cacheKey = template ? `${fullProjectName}:${template}` : fullProjectName;
|
|
134
157
|
const cached = tokenCache.get(cacheKey);
|
|
135
158
|
if (cached && cached.expires > Date.now()) {
|
|
136
159
|
return cached.token;
|
|
@@ -142,7 +165,7 @@ async function getToken(opts: GlobalOptions, template?: string): Promise<string>
|
|
|
142
165
|
body.template = template;
|
|
143
166
|
}
|
|
144
167
|
|
|
145
|
-
const mintRes = await fetch(`${
|
|
168
|
+
const mintRes = await fetch(`${config.baseUrl}/auth/${fullProjectName}/token`, {
|
|
146
169
|
method: "POST",
|
|
147
170
|
headers: {
|
|
148
171
|
Authorization: `Bearer ${profile.apiKey}`,
|
|
@@ -185,8 +208,8 @@ async function request(
|
|
|
185
208
|
},
|
|
186
209
|
opts: GlobalOptions
|
|
187
210
|
): Promise<unknown> {
|
|
188
|
-
const
|
|
189
|
-
const url = `${
|
|
211
|
+
const { config, fullProjectName } = getProfile(opts.profile);
|
|
212
|
+
const url = `${config.baseUrl}/${service}/${fullProjectName}${path}`;
|
|
190
213
|
const method = options.method || "GET";
|
|
191
214
|
|
|
192
215
|
const headers: Record<string, string> = {
|
|
@@ -242,18 +265,16 @@ program
|
|
|
242
265
|
.option("--debug", "Show HTTP requests");
|
|
243
266
|
|
|
244
267
|
// =============================================================================
|
|
245
|
-
//
|
|
268
|
+
// Config Command
|
|
246
269
|
// =============================================================================
|
|
247
270
|
|
|
248
271
|
program
|
|
249
|
-
.command("
|
|
250
|
-
.description("
|
|
251
|
-
.
|
|
252
|
-
.option("--default", "Set as default profile")
|
|
253
|
-
.action(async (cmdOpts: { name?: string; default?: boolean }) => {
|
|
272
|
+
.command("config")
|
|
273
|
+
.description("Configure account key and base URL")
|
|
274
|
+
.action(async () => {
|
|
254
275
|
const existing = loadConfig();
|
|
255
276
|
|
|
256
|
-
console.log(chalk.bold("vtriv
|
|
277
|
+
console.log(chalk.bold("vtriv Account Configuration\n"));
|
|
257
278
|
|
|
258
279
|
const readline = await import("node:readline");
|
|
259
280
|
const rl = readline.createInterface({
|
|
@@ -265,49 +286,143 @@ program
|
|
|
265
286
|
new Promise((resolve) => rl.question(q, resolve));
|
|
266
287
|
|
|
267
288
|
const baseUrl =
|
|
268
|
-
(await question(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (!project) {
|
|
276
|
-
console.error(chalk.red("Project name is required."));
|
|
289
|
+
(await question(`Base URL [${existing?.baseUrl || "http://localhost:3000"}]: `)) ||
|
|
290
|
+
existing?.baseUrl ||
|
|
291
|
+
"http://localhost:3000";
|
|
292
|
+
|
|
293
|
+
const accountKey = await question("Account Key (vtriv_ak_...): ");
|
|
294
|
+
if (!accountKey) {
|
|
295
|
+
console.error(chalk.red("Account key is required."));
|
|
277
296
|
rl.close();
|
|
278
297
|
process.exit(1);
|
|
279
298
|
}
|
|
280
299
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
console.error(chalk.red("API key is required."));
|
|
300
|
+
if (!accountKey.startsWith("vtriv_ak_")) {
|
|
301
|
+
console.error(chalk.red("Account key must start with 'vtriv_ak_'."));
|
|
284
302
|
rl.close();
|
|
285
303
|
process.exit(1);
|
|
286
304
|
}
|
|
287
305
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
306
|
+
rl.close();
|
|
307
|
+
|
|
308
|
+
// Fetch account info to get slug
|
|
309
|
+
console.log(chalk.dim("\nVerifying account key..."));
|
|
310
|
+
const res = await fetch(`${baseUrl}/auth/accounts/me`, {
|
|
311
|
+
headers: { Authorization: `Bearer ${accountKey}` },
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
if (!res.ok) {
|
|
315
|
+
const err = await res.text();
|
|
316
|
+
let msg: string;
|
|
317
|
+
try {
|
|
318
|
+
msg = JSON.parse(err).error || err;
|
|
319
|
+
} catch {
|
|
320
|
+
msg = err;
|
|
321
|
+
}
|
|
322
|
+
console.error(chalk.red(`Failed to verify account: ${msg}`));
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const accountInfo = (await res.json()) as { id: string; slug: string; name: string | null };
|
|
327
|
+
|
|
328
|
+
const config: Config = {
|
|
329
|
+
baseUrl,
|
|
330
|
+
accountKey,
|
|
331
|
+
accountSlug: accountInfo.slug,
|
|
332
|
+
default: existing?.default,
|
|
333
|
+
profiles: existing?.profiles || {},
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
saveConfig(config);
|
|
337
|
+
console.log(chalk.green(`\nAccount configured!`));
|
|
338
|
+
console.log(chalk.dim(` Slug: ${accountInfo.slug}`));
|
|
339
|
+
console.log(chalk.dim(` Name: ${accountInfo.name || "(none)"}`));
|
|
340
|
+
console.log(chalk.dim(` Config: ${getConfigPath()}`));
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// =============================================================================
|
|
344
|
+
// Init Command
|
|
345
|
+
// =============================================================================
|
|
346
|
+
|
|
347
|
+
program
|
|
348
|
+
.command("init")
|
|
349
|
+
.description("Create a project and configure profile")
|
|
350
|
+
.argument("<name>", "Project name (short name, will be prefixed with account slug)")
|
|
351
|
+
.option("--default", "Set as default profile")
|
|
352
|
+
.action(async (name: string, cmdOpts: { default?: boolean }) => {
|
|
353
|
+
const config = loadConfig();
|
|
354
|
+
|
|
355
|
+
if (!config?.accountKey || !config?.accountSlug) {
|
|
356
|
+
console.error(chalk.red("No account configured. Run 'vtriv config' first."));
|
|
291
357
|
process.exit(1);
|
|
292
358
|
}
|
|
293
359
|
|
|
294
|
-
|
|
360
|
+
// Validate project name
|
|
361
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
362
|
+
console.error(chalk.red("Invalid project name. Use alphanumeric, dash, or underscore only."));
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const fullProjectName = `${config.accountSlug}-${name}`;
|
|
295
367
|
|
|
296
|
-
|
|
297
|
-
|
|
368
|
+
// Try to create project
|
|
369
|
+
console.log(chalk.dim(`Creating project ${fullProjectName}...`));
|
|
370
|
+
const createRes = await fetch(`${config.baseUrl}/auth/projects`, {
|
|
371
|
+
method: "POST",
|
|
372
|
+
headers: {
|
|
373
|
+
Authorization: `Bearer ${config.accountKey}`,
|
|
374
|
+
"Content-Type": "application/json",
|
|
375
|
+
},
|
|
376
|
+
body: JSON.stringify({ name }),
|
|
377
|
+
});
|
|
298
378
|
|
|
299
|
-
|
|
300
|
-
|
|
379
|
+
let projectExists = false;
|
|
380
|
+
if (!createRes.ok) {
|
|
381
|
+
const err = (await createRes.json()) as { error?: string };
|
|
382
|
+
if (err.error === "Project already exists") {
|
|
383
|
+
projectExists = true;
|
|
384
|
+
console.log(chalk.yellow(`Project ${fullProjectName} already exists.`));
|
|
385
|
+
} else {
|
|
386
|
+
console.error(chalk.red(`Failed to create project: ${err.error}`));
|
|
387
|
+
process.exit(1);
|
|
388
|
+
}
|
|
389
|
+
} else {
|
|
390
|
+
console.log(chalk.green(`Created project ${fullProjectName}`));
|
|
301
391
|
}
|
|
302
392
|
|
|
303
|
-
|
|
304
|
-
console.log(chalk.
|
|
393
|
+
// Create or fetch API key
|
|
394
|
+
console.log(chalk.dim("Creating API key..."));
|
|
395
|
+
const keyRes = await fetch(`${config.baseUrl}/auth/${fullProjectName}/api-keys`, {
|
|
396
|
+
method: "POST",
|
|
397
|
+
headers: {
|
|
398
|
+
Authorization: `Bearer ${config.accountKey}`,
|
|
399
|
+
"Content-Type": "application/json",
|
|
400
|
+
},
|
|
401
|
+
body: JSON.stringify({ name: "CLI" }),
|
|
402
|
+
});
|
|
305
403
|
|
|
306
|
-
if (
|
|
307
|
-
|
|
404
|
+
if (!keyRes.ok) {
|
|
405
|
+
const err = (await keyRes.json()) as { error?: string };
|
|
406
|
+
console.error(chalk.red(`Failed to create API key: ${err.error}`));
|
|
407
|
+
process.exit(1);
|
|
308
408
|
}
|
|
309
409
|
|
|
310
|
-
|
|
410
|
+
const keyData = (await keyRes.json()) as { api_key: string };
|
|
411
|
+
|
|
412
|
+
// Save profile
|
|
413
|
+
config.profiles[name] = { apiKey: keyData.api_key };
|
|
414
|
+
if (cmdOpts.default || !config.default) {
|
|
415
|
+
config.default = name;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
saveConfig(config);
|
|
419
|
+
console.log(chalk.green(`\nProfile '${name}' saved.`));
|
|
420
|
+
if (config.default === name) {
|
|
421
|
+
console.log(chalk.dim("Set as default profile."));
|
|
422
|
+
}
|
|
423
|
+
if (projectExists) {
|
|
424
|
+
console.log(chalk.dim("Note: A new API key was created for the existing project."));
|
|
425
|
+
}
|
|
311
426
|
});
|
|
312
427
|
|
|
313
428
|
// =============================================================================
|
|
@@ -316,23 +431,125 @@ program
|
|
|
316
431
|
|
|
317
432
|
program
|
|
318
433
|
.command("status")
|
|
319
|
-
.description("Show
|
|
434
|
+
.description("Show configuration and profiles")
|
|
320
435
|
.action(() => {
|
|
321
436
|
const opts = program.opts<GlobalOptions>();
|
|
322
437
|
const config = loadConfig();
|
|
323
|
-
if (!config
|
|
324
|
-
console.log(chalk.dim("No
|
|
438
|
+
if (!config) {
|
|
439
|
+
console.log(chalk.dim("No config found. Run 'vtriv config' first."));
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (opts.json) {
|
|
444
|
+
// Don't expose the actual keys
|
|
445
|
+
const safeConfig = {
|
|
446
|
+
baseUrl: config.baseUrl,
|
|
447
|
+
accountSlug: config.accountSlug,
|
|
448
|
+
hasAccountKey: !!config.accountKey,
|
|
449
|
+
default: config.default,
|
|
450
|
+
profiles: Object.keys(config.profiles),
|
|
451
|
+
};
|
|
452
|
+
console.log(JSON.stringify(safeConfig, null, 2));
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
console.log(chalk.bold("Configuration"));
|
|
457
|
+
console.log(` Base URL: ${config.baseUrl}`);
|
|
458
|
+
console.log(` Account: ${config.accountSlug || chalk.dim("(not configured)")}`);
|
|
459
|
+
console.log(` Default: ${config.default || chalk.dim("(none)")}`);
|
|
460
|
+
console.log();
|
|
461
|
+
|
|
462
|
+
if (Object.keys(config.profiles).length === 0) {
|
|
463
|
+
console.log(chalk.dim("No profiles. Run 'vtriv init <name>' to create one."));
|
|
325
464
|
return;
|
|
326
465
|
}
|
|
327
466
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
467
|
+
console.log(chalk.bold("Profiles"));
|
|
468
|
+
for (const [name] of Object.entries(config.profiles)) {
|
|
469
|
+
const fullName = config.accountSlug ? `${config.accountSlug}-${name}` : name;
|
|
470
|
+
const isDefault = name === config.default ? chalk.green(" (default)") : "";
|
|
471
|
+
console.log(` ${name}${isDefault}`);
|
|
472
|
+
console.log(chalk.dim(` → ${fullName}`));
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// =============================================================================
|
|
477
|
+
// Project Commands
|
|
478
|
+
// =============================================================================
|
|
479
|
+
|
|
480
|
+
const project = program.command("project").description("Project management");
|
|
481
|
+
|
|
482
|
+
project
|
|
483
|
+
.command("ls")
|
|
484
|
+
.description("List projects")
|
|
485
|
+
.action(async () => {
|
|
486
|
+
const opts = program.opts<GlobalOptions>();
|
|
487
|
+
const config = getConfig();
|
|
488
|
+
|
|
489
|
+
if (!config.accountKey) {
|
|
490
|
+
console.error(chalk.red("No account configured. Run 'vtriv config' first."));
|
|
491
|
+
process.exit(1);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const res = await fetch(`${config.baseUrl}/auth/projects`, {
|
|
495
|
+
headers: { Authorization: `Bearer ${config.accountKey}` },
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
if (!res.ok) {
|
|
499
|
+
const err = (await res.json()) as { error?: string };
|
|
500
|
+
error(err.error || "Failed to list projects", opts);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const data = (await res.json()) as { projects: Array<{ name: string; short_name: string; created_at: string }> };
|
|
504
|
+
|
|
505
|
+
if (opts.json) {
|
|
506
|
+
console.log(JSON.stringify(data.projects, null, 2));
|
|
507
|
+
} else {
|
|
508
|
+
const rows = data.projects.map((p) => ({
|
|
509
|
+
name: p.short_name,
|
|
510
|
+
full_name: p.name,
|
|
511
|
+
created: p.created_at.split("T")[0],
|
|
512
|
+
profile: config.profiles[p.short_name] ? "✓" : "",
|
|
513
|
+
}));
|
|
514
|
+
printTable(rows);
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
project
|
|
519
|
+
.command("rm")
|
|
520
|
+
.description("Delete a project")
|
|
521
|
+
.argument("<name>", "Project name (short name)")
|
|
522
|
+
.action(async (name: string) => {
|
|
523
|
+
const opts = program.opts<GlobalOptions>();
|
|
524
|
+
const config = getConfig();
|
|
525
|
+
|
|
526
|
+
if (!config.accountKey || !config.accountSlug) {
|
|
527
|
+
console.error(chalk.red("No account configured. Run 'vtriv config' first."));
|
|
528
|
+
process.exit(1);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const fullName = `${config.accountSlug}-${name}`;
|
|
532
|
+
|
|
533
|
+
const res = await fetch(`${config.baseUrl}/auth/projects/${fullName}`, {
|
|
534
|
+
method: "DELETE",
|
|
535
|
+
headers: { Authorization: `Bearer ${config.accountKey}` },
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
if (!res.ok) {
|
|
539
|
+
const err = (await res.json()) as { error?: string };
|
|
540
|
+
error(err.error || "Failed to delete project", opts);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Remove from local profiles
|
|
544
|
+
if (config.profiles[name]) {
|
|
545
|
+
delete config.profiles[name];
|
|
546
|
+
if (config.default === name) {
|
|
547
|
+
config.default = Object.keys(config.profiles)[0];
|
|
548
|
+
}
|
|
549
|
+
saveConfig(config);
|
|
550
|
+
}
|
|
334
551
|
|
|
335
|
-
|
|
552
|
+
console.log(chalk.green(`Deleted project ${fullName}`));
|
|
336
553
|
});
|
|
337
554
|
|
|
338
555
|
// =============================================================================
|
|
@@ -364,9 +581,9 @@ template
|
|
|
364
581
|
.description("List all templates")
|
|
365
582
|
.action(async () => {
|
|
366
583
|
const opts = program.opts<GlobalOptions>();
|
|
367
|
-
const profile = getProfile(opts.profile);
|
|
584
|
+
const { config, profile, fullProjectName } = getProfile(opts.profile);
|
|
368
585
|
|
|
369
|
-
const res = await fetch(`${
|
|
586
|
+
const res = await fetch(`${config.baseUrl}/auth/${fullProjectName}/templates`, {
|
|
370
587
|
headers: { Authorization: `Bearer ${profile.apiKey}` },
|
|
371
588
|
});
|
|
372
589
|
|
|
@@ -394,9 +611,9 @@ template
|
|
|
394
611
|
.argument("<name>", "Template name")
|
|
395
612
|
.action(async (name: string) => {
|
|
396
613
|
const opts = program.opts<GlobalOptions>();
|
|
397
|
-
const profile = getProfile(opts.profile);
|
|
614
|
+
const { config, profile, fullProjectName } = getProfile(opts.profile);
|
|
398
615
|
|
|
399
|
-
const res = await fetch(`${
|
|
616
|
+
const res = await fetch(`${config.baseUrl}/auth/${fullProjectName}/templates/${name}`, {
|
|
400
617
|
headers: { Authorization: `Bearer ${profile.apiKey}` },
|
|
401
618
|
});
|
|
402
619
|
|
|
@@ -415,7 +632,7 @@ template
|
|
|
415
632
|
.argument("<name>", "Template name")
|
|
416
633
|
.action(async (name: string) => {
|
|
417
634
|
const opts = program.opts<GlobalOptions>();
|
|
418
|
-
const profile = getProfile(opts.profile);
|
|
635
|
+
const { config, profile, fullProjectName } = getProfile(opts.profile);
|
|
419
636
|
|
|
420
637
|
// Read claims JSON from stdin
|
|
421
638
|
const chunks: Buffer[] = [];
|
|
@@ -435,7 +652,7 @@ template
|
|
|
435
652
|
error("Invalid JSON on stdin", opts);
|
|
436
653
|
}
|
|
437
654
|
|
|
438
|
-
const res = await fetch(`${
|
|
655
|
+
const res = await fetch(`${config.baseUrl}/auth/${fullProjectName}/templates/${name}`, {
|
|
439
656
|
method: "PUT",
|
|
440
657
|
headers: {
|
|
441
658
|
Authorization: `Bearer ${profile.apiKey}`,
|
|
@@ -458,13 +675,13 @@ template
|
|
|
458
675
|
.argument("<name>", "Template name")
|
|
459
676
|
.action(async (name: string) => {
|
|
460
677
|
const opts = program.opts<GlobalOptions>();
|
|
461
|
-
const profile = getProfile(opts.profile);
|
|
678
|
+
const { config, profile, fullProjectName } = getProfile(opts.profile);
|
|
462
679
|
|
|
463
680
|
if (name === "default") {
|
|
464
681
|
error("Cannot delete the default template", opts);
|
|
465
682
|
}
|
|
466
683
|
|
|
467
|
-
const res = await fetch(`${
|
|
684
|
+
const res = await fetch(`${config.baseUrl}/auth/${fullProjectName}/templates/${name}`, {
|
|
468
685
|
method: "DELETE",
|
|
469
686
|
headers: { Authorization: `Bearer ${profile.apiKey}` },
|
|
470
687
|
});
|
|
@@ -477,6 +694,37 @@ template
|
|
|
477
694
|
console.log(chalk.green(`Template '${name}' deleted.`));
|
|
478
695
|
});
|
|
479
696
|
|
|
697
|
+
template
|
|
698
|
+
.command("reset")
|
|
699
|
+
.description("Reset templates to server defaults")
|
|
700
|
+
.action(async () => {
|
|
701
|
+
const opts = program.opts<GlobalOptions>();
|
|
702
|
+
const { config, profile, fullProjectName } = getProfile(opts.profile);
|
|
703
|
+
|
|
704
|
+
const res = await fetch(`${config.baseUrl}/auth/${fullProjectName}/templates/_reset`, {
|
|
705
|
+
method: "POST",
|
|
706
|
+
headers: { Authorization: `Bearer ${profile.apiKey}` },
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
if (!res.ok) {
|
|
710
|
+
const data = await res.json();
|
|
711
|
+
error((data as { error?: string }).error || "Failed to reset templates", opts);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const data = (await res.json()) as { templates: Record<string, { claims: Record<string, unknown> }> };
|
|
715
|
+
|
|
716
|
+
if (opts.json) {
|
|
717
|
+
console.log(JSON.stringify(data.templates, null, 2));
|
|
718
|
+
} else {
|
|
719
|
+
console.log(chalk.green("Templates reset to defaults:"));
|
|
720
|
+
const rows = Object.entries(data.templates).map(([name, tmpl]) => ({
|
|
721
|
+
name,
|
|
722
|
+
claims: JSON.stringify(tmpl.claims),
|
|
723
|
+
}));
|
|
724
|
+
printTable(rows);
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
|
|
480
728
|
// =============================================================================
|
|
481
729
|
// DB Commands
|
|
482
730
|
// =============================================================================
|
|
@@ -594,7 +842,7 @@ blob
|
|
|
594
842
|
.argument("<file>", "Local file path")
|
|
595
843
|
.action(async (remotePath: string, localFile: string) => {
|
|
596
844
|
const opts = program.opts<GlobalOptions>();
|
|
597
|
-
const
|
|
845
|
+
const { config, fullProjectName } = getProfile(opts.profile);
|
|
598
846
|
const token = await getToken(opts);
|
|
599
847
|
|
|
600
848
|
const file = Bun.file(localFile);
|
|
@@ -602,7 +850,7 @@ blob
|
|
|
602
850
|
error(`File not found: ${localFile}`, opts);
|
|
603
851
|
}
|
|
604
852
|
|
|
605
|
-
const url = `${
|
|
853
|
+
const url = `${config.baseUrl}/blob/${fullProjectName}/${remotePath}`;
|
|
606
854
|
|
|
607
855
|
if (opts.debug) {
|
|
608
856
|
console.error(chalk.dim(`curl -X PUT "${url}" -T "${localFile}"`));
|
|
@@ -630,10 +878,10 @@ blob
|
|
|
630
878
|
.argument("<path>", "Remote path")
|
|
631
879
|
.action(async (remotePath: string) => {
|
|
632
880
|
const opts = program.opts<GlobalOptions>();
|
|
633
|
-
const
|
|
881
|
+
const { config, fullProjectName } = getProfile(opts.profile);
|
|
634
882
|
const token = await getToken(opts);
|
|
635
883
|
|
|
636
|
-
const url = `${
|
|
884
|
+
const url = `${config.baseUrl}/blob/${fullProjectName}/${remotePath}`;
|
|
637
885
|
|
|
638
886
|
if (opts.debug) {
|
|
639
887
|
console.error(chalk.dim(`curl "${url}"`));
|
|
@@ -667,10 +915,10 @@ blob
|
|
|
667
915
|
.argument("<path>", "Remote path")
|
|
668
916
|
.action(async (remotePath: string) => {
|
|
669
917
|
const opts = program.opts<GlobalOptions>();
|
|
670
|
-
const
|
|
918
|
+
const { config, fullProjectName } = getProfile(opts.profile);
|
|
671
919
|
const token = await getToken(opts);
|
|
672
920
|
|
|
673
|
-
const url = `${
|
|
921
|
+
const url = `${config.baseUrl}/blob/${fullProjectName}/${remotePath}`;
|
|
674
922
|
|
|
675
923
|
const res = await fetch(url, {
|
|
676
924
|
method: "DELETE",
|
|
@@ -728,10 +976,10 @@ cron
|
|
|
728
976
|
.argument("<script>", "Script name")
|
|
729
977
|
.action(async (script: string) => {
|
|
730
978
|
const opts = program.opts<GlobalOptions>();
|
|
731
|
-
const
|
|
979
|
+
const { config, fullProjectName } = getProfile(opts.profile);
|
|
732
980
|
const token = await getToken(opts);
|
|
733
981
|
|
|
734
|
-
const url = `${
|
|
982
|
+
const url = `${config.baseUrl}/cron/${fullProjectName}/scripts/${script}`;
|
|
735
983
|
const res = await fetch(url, {
|
|
736
984
|
headers: { Authorization: `Bearer ${token}` },
|
|
737
985
|
});
|
|
@@ -755,10 +1003,10 @@ ai.command("stats")
|
|
|
755
1003
|
.option("-p, --period <days>", "Period in days", "30")
|
|
756
1004
|
.action(async (cmdOpts: { period: string }) => {
|
|
757
1005
|
const opts = program.opts<GlobalOptions>();
|
|
758
|
-
const
|
|
1006
|
+
const { config } = getProfile(opts.profile);
|
|
759
1007
|
const token = await getToken(opts);
|
|
760
1008
|
|
|
761
|
-
const url = `${
|
|
1009
|
+
const url = `${config.baseUrl}/ai/usage/stats?period=${cmdOpts.period}d`;
|
|
762
1010
|
const res = await fetch(url, {
|
|
763
1011
|
headers: { Authorization: `Bearer ${token}` },
|
|
764
1012
|
});
|
|
@@ -777,24 +1025,24 @@ ai.command("chat")
|
|
|
777
1025
|
.option("-m, --model <model>", "Model to use (defaults to project config)")
|
|
778
1026
|
.action(async (prompt: string, cmdOpts: { model?: string }) => {
|
|
779
1027
|
const opts = program.opts<GlobalOptions>();
|
|
780
|
-
const
|
|
1028
|
+
const { config } = getProfile(opts.profile);
|
|
781
1029
|
const token = await getToken(opts);
|
|
782
1030
|
|
|
783
1031
|
// Get default model from project config if not specified
|
|
784
1032
|
let model = cmdOpts.model;
|
|
785
1033
|
if (!model) {
|
|
786
|
-
const configRes = await fetch(`${
|
|
1034
|
+
const configRes = await fetch(`${config.baseUrl}/ai/config`, {
|
|
787
1035
|
headers: { Authorization: `Bearer ${token}` },
|
|
788
1036
|
});
|
|
789
1037
|
if (configRes.ok) {
|
|
790
|
-
const
|
|
791
|
-
model =
|
|
1038
|
+
const aiConfig = (await configRes.json()) as { default_model?: string };
|
|
1039
|
+
model = aiConfig.default_model || "openai/gpt-4o-mini";
|
|
792
1040
|
} else {
|
|
793
1041
|
model = "openai/gpt-4o-mini";
|
|
794
1042
|
}
|
|
795
1043
|
}
|
|
796
1044
|
|
|
797
|
-
const url = `${
|
|
1045
|
+
const url = `${config.baseUrl}/ai/v1/chat/completions`;
|
|
798
1046
|
|
|
799
1047
|
if (opts.debug) {
|
|
800
1048
|
console.error(chalk.dim(`curl -X POST "${url}" ...`));
|
|
@@ -833,7 +1081,7 @@ ai.command("config")
|
|
|
833
1081
|
.option("--key <key>", "Set custom OpenRouter API key")
|
|
834
1082
|
.action(async (cmdOpts: { model?: string; rateLimit?: string; key?: string }) => {
|
|
835
1083
|
const opts = program.opts<GlobalOptions>();
|
|
836
|
-
const
|
|
1084
|
+
const { config } = getProfile(opts.profile);
|
|
837
1085
|
const token = await getToken(opts);
|
|
838
1086
|
|
|
839
1087
|
// If any options provided, update config
|
|
@@ -843,7 +1091,7 @@ ai.command("config")
|
|
|
843
1091
|
if (cmdOpts.rateLimit) body.rate_limit_usd = Number.parseFloat(cmdOpts.rateLimit);
|
|
844
1092
|
if (cmdOpts.key) body.key = cmdOpts.key;
|
|
845
1093
|
|
|
846
|
-
const updateRes = await fetch(`${
|
|
1094
|
+
const updateRes = await fetch(`${config.baseUrl}/ai/config`, {
|
|
847
1095
|
method: "PUT",
|
|
848
1096
|
headers: {
|
|
849
1097
|
Authorization: `Bearer ${token}`,
|
|
@@ -861,7 +1109,7 @@ ai.command("config")
|
|
|
861
1109
|
}
|
|
862
1110
|
|
|
863
1111
|
// Always show current config
|
|
864
|
-
const res = await fetch(`${
|
|
1112
|
+
const res = await fetch(`${config.baseUrl}/ai/config`, {
|
|
865
1113
|
headers: { Authorization: `Bearer ${token}` },
|
|
866
1114
|
});
|
|
867
1115
|
output(await res.json(), opts);
|