veogent 1.1.2 → 1.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/.claude/worktrees/vigorous-hellman/.claude/settings.local.json +8 -0
- package/.claude/worktrees/vigorous-hellman/README.md +370 -0
- package/.claude/worktrees/vigorous-hellman/api.js +43 -0
- package/.claude/worktrees/vigorous-hellman/auth.js +77 -0
- package/.claude/worktrees/vigorous-hellman/config.js +37 -0
- package/.claude/worktrees/vigorous-hellman/device-auth.js +128 -0
- package/.claude/worktrees/vigorous-hellman/flow-extractor.js +66 -0
- package/.claude/worktrees/vigorous-hellman/index.js +1398 -0
- package/.claude/worktrees/vigorous-hellman/package-lock.json +3269 -0
- package/.claude/worktrees/vigorous-hellman/package.json +41 -0
- package/.claude/worktrees/vigorous-hellman/skills/SKILL.md +819 -0
- package/README.md +265 -94
- package/index.js +24 -0
- package/package.json +2 -2
- package/skills/SKILL.md +342 -841
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
# 🎬 Veogent CLI <a href="https://veogent.com"><img src="https://veogent.com/favicon.ico" width="24" height="24" align="center" /></a>
|
|
2
|
+
|
|
3
|
+
**The official Command-Line Interface for the [VEOGENT API](https://veogent.com)**.
|
|
4
|
+
|
|
5
|
+
Veogent CLI gives you (and your AI Agents) the power to manage full-scale AI video and story projects directly from the terminal. Connect to projects, orchestrate multi-frame scenes, edit image prompts with professional camera cues, and trigger large-scale generation jobs natively.
|
|
6
|
+
|
|
7
|
+
Perfectly engineered for **Agentic workflows** — enabling tools like OpenClaw, Claude, and Codex to autonomously generate JSON-driven movies from scratch.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 🚀 Installation
|
|
12
|
+
|
|
13
|
+
Install globally via npm:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g veogent
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 🔐 Authentication
|
|
22
|
+
|
|
23
|
+
Veogent CLI supports both browser callback login and headless device-code login.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Browser callback flow (desktop)
|
|
27
|
+
veogent login
|
|
28
|
+
|
|
29
|
+
# Device-code flow (VM/server/headless — recommended for AI Agents)
|
|
30
|
+
veogent login --device
|
|
31
|
+
|
|
32
|
+
# Verify your session
|
|
33
|
+
veogent status
|
|
34
|
+
|
|
35
|
+
# Clear saved credentials
|
|
36
|
+
veogent logout
|
|
37
|
+
|
|
38
|
+
# Setup/update Google Flow Key (required for video generation & AI features)
|
|
39
|
+
veogent setup-flow --flowkey "ya29.a0AW..." --tier "PAYGATE_TIER_TWO"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Global Flags
|
|
45
|
+
|
|
46
|
+
| Flag | Description |
|
|
47
|
+
|------|-------------|
|
|
48
|
+
| `--json` | Output machine-readable JSON only (suppress emoji/human text) |
|
|
49
|
+
| `--agent-safe` | Agent-safe mode — no emoji, no browser surprises, stable output. Auto-enables `--device` for login. |
|
|
50
|
+
| `--version` | Print CLI version |
|
|
51
|
+
|
|
52
|
+
> **For AI Agents:** always use `--agent-safe` to guarantee deterministic, parseable JSON output with no interactive prompts.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 📋 Complete Command Reference
|
|
57
|
+
|
|
58
|
+
### Authentication & Configuration
|
|
59
|
+
|
|
60
|
+
| Command | Description |
|
|
61
|
+
|---------|-------------|
|
|
62
|
+
| `login` | Login via browser callback or device code flow (`--device`) |
|
|
63
|
+
| `logout` | Clear saved credentials and remove stored access token |
|
|
64
|
+
| `status` | Show authenticated user, Flow Key details, and payment tier |
|
|
65
|
+
| `setup-flow` | Setup/update Google Flow Key and User Payment Tier |
|
|
66
|
+
|
|
67
|
+
### Capabilities & Discovery
|
|
68
|
+
|
|
69
|
+
| Command | Description |
|
|
70
|
+
|---------|-------------|
|
|
71
|
+
| `capabilities` | Get backend capabilities (models, orientations, speed modes, queue) |
|
|
72
|
+
| `image-models` | List supported image generation models |
|
|
73
|
+
| `video-models` | List supported video generation models |
|
|
74
|
+
| `image-materials` | Get all available Image Material styles (e.g., `CINEMATIC`, `PIXAR_3D`) |
|
|
75
|
+
| `custom-prompts` | Get custom prompt templates and story directives |
|
|
76
|
+
|
|
77
|
+
### Project Management
|
|
78
|
+
|
|
79
|
+
| Command | Description |
|
|
80
|
+
|---------|-------------|
|
|
81
|
+
| `projects` | List all your available projects |
|
|
82
|
+
| `project <id>` | Get details for a specific project |
|
|
83
|
+
| `create-project-description` | Generate AI description for a new project based on keywords |
|
|
84
|
+
| `create-project` | Create a new project with name, keywords, material, chapters, objects |
|
|
85
|
+
|
|
86
|
+
### Chapters
|
|
87
|
+
|
|
88
|
+
| Command | Description |
|
|
89
|
+
|---------|-------------|
|
|
90
|
+
| `chapters <projectId>` | Get all chapters for a project |
|
|
91
|
+
| `recent-chapters` | Get recently created or updated chapters |
|
|
92
|
+
| `create-chapter-content` | Generate content for a chapter (synchronous AI generation) |
|
|
93
|
+
|
|
94
|
+
### Characters
|
|
95
|
+
|
|
96
|
+
| Command | Description |
|
|
97
|
+
|---------|-------------|
|
|
98
|
+
| `characters <projectId>` | Get all characters with readiness info |
|
|
99
|
+
| `edit-character` | Update character description or edit generated image via AI prompt |
|
|
100
|
+
|
|
101
|
+
### Scenes
|
|
102
|
+
|
|
103
|
+
| Command | Description |
|
|
104
|
+
|---------|-------------|
|
|
105
|
+
| `scenes <projectId> <chapterId>` | Get all scenes for a chapter |
|
|
106
|
+
| `create-scene` | Create new scene(s) from text content |
|
|
107
|
+
| `add-scene` | Add a single scene to a chapter with a user prompt |
|
|
108
|
+
| `edit-scene` | Edit scene image prompt via AI assistance |
|
|
109
|
+
|
|
110
|
+
### Generation Requests (Project-scoped)
|
|
111
|
+
|
|
112
|
+
> **Clarification:** `request` (singular) = **CREATE** a new job. `requests` (plural) = **LIST** existing jobs.
|
|
113
|
+
|
|
114
|
+
| Command | Description |
|
|
115
|
+
|---------|-------------|
|
|
116
|
+
| `request` | Create a generation job: `GENERATE_IMAGES`, `GENERATE_VIDEO`, or `VIDEO_UPSCALE` |
|
|
117
|
+
| `requests` | List generation requests (filterable by project, chapter, status) |
|
|
118
|
+
| `upscale` | Upscale video output for a scene (shortcut for `request --type VIDEO_UPSCALE`) |
|
|
119
|
+
| `assets <projectId> <chapterId> <sceneId> <type>` | Check generated assets history/status for a scene |
|
|
120
|
+
|
|
121
|
+
### Standalone Generation (No Project Needed)
|
|
122
|
+
|
|
123
|
+
> These commands generate images/videos without a project. Ideal for quick one-off generations.
|
|
124
|
+
|
|
125
|
+
| Command | Description |
|
|
126
|
+
|---------|-------------|
|
|
127
|
+
| `gen-image` | Generate image from text prompt (standalone, 3 credits, Imagen 3.5) |
|
|
128
|
+
| `gen-video` | Generate video from text prompt (standalone, 5 credits, Veo 3.1 Fast) |
|
|
129
|
+
| `standalone-requests` | List your standalone generation requests |
|
|
130
|
+
| `standalone-request <requestId>` | Get details of a specific standalone request |
|
|
131
|
+
|
|
132
|
+
### Monitoring & Status
|
|
133
|
+
|
|
134
|
+
| Command | Description |
|
|
135
|
+
|---------|-------------|
|
|
136
|
+
| `scene-status` | Scene-level status with embedded asset URLs (image + video per scene) |
|
|
137
|
+
| `workflow-status` | Full workflow snapshot: scenes + requests + assets (agent helper) |
|
|
138
|
+
| `scene-materialization-status` | Check how many scenes have materialized for a chapter |
|
|
139
|
+
| `failed-requests` | List failed/error/rejected requests for a project/chapter (agent helper) |
|
|
140
|
+
| `wait-images` | Poll until all image requests in a chapter finish |
|
|
141
|
+
| `wait-videos` | Poll until all video requests in a chapter finish |
|
|
142
|
+
| `queue-status` | Get current queue/concurrency status |
|
|
143
|
+
|
|
144
|
+
### YouTube Integration
|
|
145
|
+
|
|
146
|
+
| Command | Description |
|
|
147
|
+
|---------|-------------|
|
|
148
|
+
| `generate-yt-metadata` | Generate YouTube metadata (Title, Description, Tags) for a chapter |
|
|
149
|
+
| `generate-yt-thumbnail` | Trigger request to generate a YouTube thumbnail |
|
|
150
|
+
| `yt-thumbnails <projectId> <chapterId>` | Get generated YouTube thumbnails for a chapter |
|
|
151
|
+
|
|
152
|
+
### Flow Token & Credits
|
|
153
|
+
|
|
154
|
+
| Command | Description |
|
|
155
|
+
|---------|-------------|
|
|
156
|
+
| `obtain-flow` | Auto-extract Flow Token (ya29.) using secure browser overlay |
|
|
157
|
+
| `flow-credits` | Fetch plan and credit info from Google AI Sandbox |
|
|
158
|
+
|
|
159
|
+
### Utility
|
|
160
|
+
|
|
161
|
+
| Command | Description |
|
|
162
|
+
|---------|-------------|
|
|
163
|
+
| `skill` | Print the native Agent SKILL.md guide |
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## 🛠️ Usage Examples
|
|
168
|
+
|
|
169
|
+
### Project Management
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
# List all your active projects
|
|
173
|
+
veogent projects
|
|
174
|
+
|
|
175
|
+
# View available Image Material styles (e.g., CINEMATIC, PIXAR_3D)
|
|
176
|
+
veogent image-materials
|
|
177
|
+
|
|
178
|
+
# Create a brand new AI Story Project
|
|
179
|
+
veogent create-project --name "Cyberpunk T-Rex" --keyword "T-rex, Neon, Sci-fi" \
|
|
180
|
+
--desc "A massive T-rex walking inside Tokyo" --lang "English" --material "CINEMATIC" --chapters 5 \
|
|
181
|
+
--objects '{"name":"Hero","entityType":"character","description":"brave knight"}'
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Storyboard, Chapters & Scenes
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
# Get all chapters within a project
|
|
188
|
+
veogent chapters <projectId>
|
|
189
|
+
|
|
190
|
+
# View characters (includes readiness info)
|
|
191
|
+
veogent characters <projectId>
|
|
192
|
+
# → { characters: [...], characterReadiness: {total, ready, allReady} }
|
|
193
|
+
|
|
194
|
+
# Check scene materialization status
|
|
195
|
+
veogent scene-materialization-status --project <projectId> --chapter <chapterId>
|
|
196
|
+
# → { expectedScenes, materializedScenes, status: "PROCESSING"|"READY"|"EMPTY" }
|
|
197
|
+
|
|
198
|
+
# Create scenes from narrative scripts
|
|
199
|
+
veogent create-scene --project <projectId> --chapter <chapterId> --flowkey \
|
|
200
|
+
--content "The T-Rex looks up at the sky." "A meteor shower begins."
|
|
201
|
+
|
|
202
|
+
# Add a single scene to an existing chapter
|
|
203
|
+
veogent add-scene --project <projectId> --chapter <chapterId> \
|
|
204
|
+
--userprompt "A meteor crashes into the city." --after-scene-id <sceneId>
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Directing & Editing
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
# Edit scene image prompt with camera direction
|
|
211
|
+
veogent edit-scene --project <projectId> --chapter <chapterId> --scene <sceneId> \
|
|
212
|
+
--userprompt "Low angle shot of the T-Rex, dramatic lighting."
|
|
213
|
+
|
|
214
|
+
# Edit character image directly via AI
|
|
215
|
+
veogent edit-character --project <projectId> --character "drelenavance" \
|
|
216
|
+
--userprompt "change outfit to dark leather jacket" --editimage
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Media Generation (Project-scoped)
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
# List supported models
|
|
223
|
+
veogent image-models # → { models: ["imagen3.5"] }
|
|
224
|
+
veogent video-models # → { models: ["veo_3_1_fast", "veo_3_1_fast_r2v"] }
|
|
225
|
+
|
|
226
|
+
# Generate Image (project-scoped)
|
|
227
|
+
veogent request --type "GENERATE_IMAGES" --project <projectId> --chapter <chapterId> --scene <sceneId> --imagemodel "imagen3.5"
|
|
228
|
+
|
|
229
|
+
# Generate Video (project-scoped)
|
|
230
|
+
veogent request --type "GENERATE_VIDEO" --project <projectId> --chapter <chapterId> --scene <sceneId> --videomodel "veo_3_1_fast"
|
|
231
|
+
|
|
232
|
+
# Upscale Video
|
|
233
|
+
veogent upscale --project <projectId> --chapter <chapterId> --scene <sceneId> --orientation "VERTICAL" --resolution "VIDEO_RESOLUTION_4K"
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Standalone Generation (No Project)
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
# Quick image generation (3 credits)
|
|
240
|
+
veogent gen-image --prompt "A futuristic city at sunset" --orientation "HORIZONTAL"
|
|
241
|
+
|
|
242
|
+
# Quick video generation (5 credits)
|
|
243
|
+
veogent gen-video --prompt "A cat playing piano" --orientation "HORIZONTAL"
|
|
244
|
+
|
|
245
|
+
# Check standalone request status
|
|
246
|
+
veogent standalone-requests
|
|
247
|
+
veogent standalone-request <requestId>
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Monitoring & Status
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
# List recent requests
|
|
254
|
+
veogent requests --limit 10
|
|
255
|
+
|
|
256
|
+
# Filter by project/chapter/status
|
|
257
|
+
veogent requests --project <projectId> --chapter <chapterId> --status FAILED
|
|
258
|
+
|
|
259
|
+
# Scene-level status with asset URLs
|
|
260
|
+
veogent scene-status --project <projectId> --chapter <chapterId>
|
|
261
|
+
|
|
262
|
+
# Full workflow snapshot (agent helper)
|
|
263
|
+
veogent workflow-status --project <projectId> --chapter <chapterId>
|
|
264
|
+
|
|
265
|
+
# Wait for all images/videos to complete (with success verification)
|
|
266
|
+
veogent wait-images --project <projectId> --chapter <chapterId> --require-success
|
|
267
|
+
veogent wait-videos --project <projectId> --chapter <chapterId> --require-success
|
|
268
|
+
|
|
269
|
+
# Queue concurrency status
|
|
270
|
+
veogent queue-status
|
|
271
|
+
|
|
272
|
+
# Flow credit/plan info
|
|
273
|
+
veogent flow-credits
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### YouTube Publishing
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
# Generate YouTube metadata
|
|
280
|
+
veogent generate-yt-metadata --project <projectId> --chapter <chapterId> --flowkey
|
|
281
|
+
|
|
282
|
+
# Generate YouTube thumbnail
|
|
283
|
+
veogent generate-yt-thumbnail --project <projectId> --chapter <chapterId>
|
|
284
|
+
|
|
285
|
+
# Retrieve generated thumbnails
|
|
286
|
+
veogent yt-thumbnails <projectId> <chapterId>
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## Agent Orchestration Notes
|
|
292
|
+
|
|
293
|
+
### Request/Scene Semantics
|
|
294
|
+
|
|
295
|
+
- `scene.requestId`: backward-compatible legacy pointer; may refer to latest relevant request but is **not type-specific**.
|
|
296
|
+
- `scene.latestImageRequestId`: latest image-related request (`GENERATE_IMAGES` / `EDIT_IMAGE`) regardless of status.
|
|
297
|
+
- `scene.latestVideoRequestId`: latest video request (`GENERATE_VIDEO`) regardless of status.
|
|
298
|
+
- `scene.latestSuccessfulImageRequestId`: latest image-related request in `COMPLETED`.
|
|
299
|
+
- `scene.latestSuccessfulVideoRequestId`: latest video request in `COMPLETED`.
|
|
300
|
+
|
|
301
|
+
For robust automation:
|
|
302
|
+
- Prefer `veogent scene-status` or `veogent workflow-status` for chapter-level overview.
|
|
303
|
+
- Use `latestSuccessful*` fields when selecting canonical output URIs.
|
|
304
|
+
|
|
305
|
+
### `request` (CREATE) vs `requests` (LIST)
|
|
306
|
+
|
|
307
|
+
- **`request`** — Creates a new generation job (POST). Requires `--type` flag.
|
|
308
|
+
- **`requests`** — Lists existing jobs (GET). Filterable with `--project`, `--chapter`, `--status`, `--limit`.
|
|
309
|
+
|
|
310
|
+
### Project-scoped vs Standalone Generation
|
|
311
|
+
|
|
312
|
+
| | Project-scoped | Standalone |
|
|
313
|
+
|---|---|---|
|
|
314
|
+
| **Image** | `request --type GENERATE_IMAGES --project ... --chapter ... --scene ...` | `gen-image --prompt "prompt"` |
|
|
315
|
+
| **Video** | `request --type GENERATE_VIDEO --project ... --chapter ... --scene ...` | `gen-video --prompt "prompt"` |
|
|
316
|
+
| **List** | `requests` | `standalone-requests` |
|
|
317
|
+
| **Detail** | `requests` with filters | `standalone-request <id>` |
|
|
318
|
+
|
|
319
|
+
### Status Commands: When to Use Which
|
|
320
|
+
|
|
321
|
+
| Command | Use When |
|
|
322
|
+
|---------|----------|
|
|
323
|
+
| `requests` | Check individual request status, filter by status/project |
|
|
324
|
+
| `scene-status` | Quick overview of image+video status per scene in a chapter |
|
|
325
|
+
| `workflow-status` | Full snapshot with scenes + requests + assets (best for agents) |
|
|
326
|
+
| `failed-requests` | Quick list of all failed/error/rejected requests for a chapter |
|
|
327
|
+
| `wait-images` / `wait-videos` | Block until all generation jobs finish (polling) |
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## 🤖 For AI Agents
|
|
332
|
+
|
|
333
|
+
Veogent CLI ships with a comprehensive **[`skills/SKILL.md`](./skills/SKILL.md)** guide optimized for LLM/Coding Agents context processing. It defines exact DTO validations, model enumerations, camera prompting techniques, the full production pipeline, and error handling — everything an agent needs to orchestrate complete projects without hitting `400 Bad Request`.
|
|
334
|
+
|
|
335
|
+
### Recommended Agent Workflow (Production)
|
|
336
|
+
|
|
337
|
+
1. Resolve optional IDs first (if missing):
|
|
338
|
+
- `veogent custom-prompts`
|
|
339
|
+
- `veogent image-materials` (default fallback: `CINEMATIC`)
|
|
340
|
+
2. Prepare story input (arc, summary, key notes).
|
|
341
|
+
3. Generate project description:
|
|
342
|
+
- `veogent create-project-description ...`
|
|
343
|
+
4. Create project from generated payload.
|
|
344
|
+
5. Create chapter content with scene count:
|
|
345
|
+
- `veogent create-chapter-content --scenes <sceneCount>`
|
|
346
|
+
- Rule: each scene maps to ~8s clip.
|
|
347
|
+
6. Create scenes from returned script list:
|
|
348
|
+
- `veogent create-scene --project <projectId> --chapter <chapterId> --content "scene 1" "scene 2" ... --flowkey`
|
|
349
|
+
7. Wait for character generation completion (`imageUri` required for all characters):
|
|
350
|
+
- `veogent characters <projectId>` — check `characterReadiness.allReady === true`
|
|
351
|
+
- `veogent scene-materialization-status --project <projectId> --chapter <chapterId>` — verify `status: "READY"`
|
|
352
|
+
- If missing/fail: inspect `veogent requests --limit 20`, recover via `veogent edit-character`.
|
|
353
|
+
8. Generate scene images:
|
|
354
|
+
- `veogent request --type "GENERATE_IMAGES" ...`
|
|
355
|
+
- If image is reported wrong, decode image to Base64 and send to AI reviewer for evaluation.
|
|
356
|
+
- If mismatch persists, fix via:
|
|
357
|
+
- `veogent edit-scene --userprompt "..."` (prompt refinement), or
|
|
358
|
+
- `veogent edit-scene --request <requestId> ...` (direct edit on prior generated image)
|
|
359
|
+
9. Generate video only after matching-orientation image exists successfully:
|
|
360
|
+
- `veogent request --type "GENERATE_VIDEO" ...`
|
|
361
|
+
|
|
362
|
+
> 📖 For the full detailed guide with all commands, options tables, and examples, see **[`skills/SKILL.md`](./skills/SKILL.md)**.
|
|
363
|
+
|
|
364
|
+
**Concurrency:** maximum **5** requests can be processed simultaneously. If the API reports maximum limit reached, treat it as queue-full (wait/retry), not a hard failure.
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## 📜 License
|
|
369
|
+
|
|
370
|
+
MIT License. Crafted with ❤️ by Pym & Tuan Nguyen.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { getToken } from './config.js';
|
|
3
|
+
|
|
4
|
+
// Define the environment variables
|
|
5
|
+
const API_URL = process.env.VGEN_API_URL || 'https://api.veogent.com';
|
|
6
|
+
|
|
7
|
+
// API instance helper
|
|
8
|
+
export const api = axios.create({
|
|
9
|
+
baseURL: API_URL,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// Interceptor to inject bearer token before every request
|
|
13
|
+
api.interceptors.request.use((config) => {
|
|
14
|
+
const token = getToken();
|
|
15
|
+
if (token) {
|
|
16
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
17
|
+
}
|
|
18
|
+
return config;
|
|
19
|
+
}, (error) => {
|
|
20
|
+
return Promise.reject(error);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Interceptor to normalize successful payloads and throw structured errors
|
|
24
|
+
api.interceptors.response.use(
|
|
25
|
+
(response) => response.data,
|
|
26
|
+
(error) => {
|
|
27
|
+
const status = error.response?.status || null;
|
|
28
|
+
const payload = error.response?.data || null;
|
|
29
|
+
const message = payload?.error?.message?.en
|
|
30
|
+
|| payload?.error?.message
|
|
31
|
+
|| payload?.message
|
|
32
|
+
|| error.message
|
|
33
|
+
|| 'Unknown API error';
|
|
34
|
+
|
|
35
|
+
const wrapped = new Error(message);
|
|
36
|
+
wrapped.name = 'VeogentApiError';
|
|
37
|
+
wrapped.status = status;
|
|
38
|
+
wrapped.payload = payload;
|
|
39
|
+
wrapped.code = payload?.errorCode || payload?.error?.code || null;
|
|
40
|
+
wrapped.retryable = status >= 500;
|
|
41
|
+
throw wrapped;
|
|
42
|
+
}
|
|
43
|
+
);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import cors from 'cors';
|
|
3
|
+
import open from 'open';
|
|
4
|
+
import crypto from 'crypto';
|
|
5
|
+
|
|
6
|
+
export class WebAuthFlow {
|
|
7
|
+
constructor(port = 7890) {
|
|
8
|
+
this.port = port;
|
|
9
|
+
this.app = express();
|
|
10
|
+
this.server = null;
|
|
11
|
+
// Generate a random token to verify the callback matches our local terminal
|
|
12
|
+
this.cliToken = crypto.randomBytes(32).toString('hex');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
start() {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
this.app.use(cors());
|
|
18
|
+
this.app.use(express.json());
|
|
19
|
+
|
|
20
|
+
// Callback endpoint hit by the Next.js frontend
|
|
21
|
+
this.app.post('/callback', (req, res) => {
|
|
22
|
+
const { uid, idToken, cliToken } = req.body;
|
|
23
|
+
|
|
24
|
+
// Basic CSRF/validation check
|
|
25
|
+
if (cliToken !== this.cliToken) {
|
|
26
|
+
res.status(403).json({ error: 'Mismatched CLI Token' });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!uid || !idToken) {
|
|
31
|
+
res.status(400).json({ error: 'Missing UID or Token' });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Send success immediately so the browser UI updates to "Success" quickly
|
|
36
|
+
res.status(200).json({ status: 'success' });
|
|
37
|
+
|
|
38
|
+
// Close the server and return credentials to the CLI
|
|
39
|
+
setTimeout(() => {
|
|
40
|
+
this.stop();
|
|
41
|
+
resolve({ uid, accessToken: idToken });
|
|
42
|
+
}, 1000);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
this.server = this.app.listen(this.port, () => {
|
|
46
|
+
// Open the default browser to the web app's authorization page (en is default)
|
|
47
|
+
const frontendUrl = process.env.VGEN_WEB_URL || 'https://veogent.com';
|
|
48
|
+
const authUrl = `${frontendUrl}/cli-auth?port=${this.port}&token=${this.cliToken}`;
|
|
49
|
+
|
|
50
|
+
console.log('🔄 Opening browser for authentication...');
|
|
51
|
+
console.log(`\nIf your browser doesn't open automatically, click this link:\n=> ${authUrl}\n`);
|
|
52
|
+
|
|
53
|
+
open(authUrl).catch(() => {
|
|
54
|
+
console.log('⚠️ Could not open browser automatically.');
|
|
55
|
+
});
|
|
56
|
+
}).on('error', (err) => {
|
|
57
|
+
if (err.code === 'EADDRINUSE') {
|
|
58
|
+
console.error(`\n❌ Error: Port ${this.port} is already in use.`);
|
|
59
|
+
reject(err);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Timeout after 5 minutes
|
|
64
|
+
setTimeout(() => {
|
|
65
|
+
this.stop();
|
|
66
|
+
reject(new Error('Authentication timeout.'));
|
|
67
|
+
}, 5 * 60 * 1000);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
stop() {
|
|
72
|
+
if (this.server) {
|
|
73
|
+
this.server.close();
|
|
74
|
+
this.server = null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
// Define the path for the config directory and file
|
|
5
|
+
const configDir = path.join(process.env.HOME || process.env.USERPROFILE, '.vgen-cli');
|
|
6
|
+
const configPath = path.join(configDir, 'config.json');
|
|
7
|
+
|
|
8
|
+
// Get the current configuration
|
|
9
|
+
export const getConfig = () => {
|
|
10
|
+
if (!fs.existsSync(configPath)) {
|
|
11
|
+
return {};
|
|
12
|
+
}
|
|
13
|
+
const data = fs.readFileSync(configPath, 'utf8');
|
|
14
|
+
return JSON.parse(data);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Get the stored token
|
|
18
|
+
export const getToken = () => {
|
|
19
|
+
const config = getConfig();
|
|
20
|
+
return config.token || null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Set and save the configuration
|
|
24
|
+
export const setConfig = (newConfig) => {
|
|
25
|
+
if (!fs.existsSync(configDir)) {
|
|
26
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
const config = { ...getConfig(), ...newConfig };
|
|
29
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Clear the configuration
|
|
33
|
+
export const clearConfig = () => {
|
|
34
|
+
if (fs.existsSync(configPath)) {
|
|
35
|
+
fs.unlinkSync(configPath);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
|
|
3
|
+
const API_URL = process.env.VGEN_API_URL || 'https://api.veogent.com';
|
|
4
|
+
|
|
5
|
+
export class DeviceAuthFlow {
|
|
6
|
+
constructor(options = {}) {
|
|
7
|
+
this.options = options;
|
|
8
|
+
this.http = axios.create({
|
|
9
|
+
baseURL: API_URL,
|
|
10
|
+
timeout: 15000,
|
|
11
|
+
validateStatus: () => true,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async start() {
|
|
16
|
+
const createResp = await this.http.post('/app/cli/device-code', {});
|
|
17
|
+
const payload = this.getSuccessData(createResp.data);
|
|
18
|
+
|
|
19
|
+
if (createResp.status < 200 || createResp.status >= 300 || !payload?.deviceCode) {
|
|
20
|
+
throw new Error(this.extractError(createResp.data) || 'Failed to initialize device authorization');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const {
|
|
24
|
+
deviceCode,
|
|
25
|
+
userCode,
|
|
26
|
+
verificationUri,
|
|
27
|
+
verificationUriComplete,
|
|
28
|
+
expiresIn,
|
|
29
|
+
interval,
|
|
30
|
+
} = payload;
|
|
31
|
+
|
|
32
|
+
const deviceInfo = {
|
|
33
|
+
userCode,
|
|
34
|
+
verificationUri,
|
|
35
|
+
verificationUriComplete: verificationUriComplete || verificationUri,
|
|
36
|
+
expiresIn: Number(expiresIn) || 600,
|
|
37
|
+
interval: Number(interval) || 5,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
if (typeof this.options.onDeviceCode === 'function') {
|
|
41
|
+
this.options.onDeviceCode(deviceInfo);
|
|
42
|
+
} else {
|
|
43
|
+
console.log('\n--- 🔐 VEOGENT CLI Device Login ---');
|
|
44
|
+
console.log(`1) Open this URL on any device/browser:\n ${verificationUriComplete || verificationUri}`);
|
|
45
|
+
console.log(`2) Confirm the code: ${userCode}`);
|
|
46
|
+
console.log('3) Approve access, then return to this terminal.\n');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return await this.pollForToken({
|
|
50
|
+
deviceCode,
|
|
51
|
+
interval: Number(interval) || 5,
|
|
52
|
+
expiresIn: Number(expiresIn) || 600,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async pollForToken({ deviceCode, interval, expiresIn }) {
|
|
57
|
+
const deadline = Date.now() + expiresIn * 1000;
|
|
58
|
+
let pollInterval = Math.max(1, interval);
|
|
59
|
+
|
|
60
|
+
while (Date.now() < deadline) {
|
|
61
|
+
const resp = await this.http.post('/app/cli/device-token', { deviceCode });
|
|
62
|
+
const successData = this.getSuccessData(resp.data);
|
|
63
|
+
|
|
64
|
+
if (resp.status >= 200 && resp.status < 300 && successData?.access_token) {
|
|
65
|
+
return successData;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const errorCode = this.extractErrorCode(resp.data);
|
|
69
|
+
|
|
70
|
+
if (errorCode === 'authorization_pending') {
|
|
71
|
+
await this.sleep(pollInterval * 1000);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (errorCode === 'slow_down') {
|
|
76
|
+
pollInterval += 2;
|
|
77
|
+
await this.sleep(pollInterval * 1000);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (errorCode === 'expired_token') {
|
|
82
|
+
throw new Error('Device code expired. Please run `veogent login --device` again.');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (errorCode === 'access_denied') {
|
|
86
|
+
throw new Error('Authorization was denied. Please run `veogent login --device` again.');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
throw new Error(this.extractError(resp.data) || 'Device authorization failed');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
throw new Error('Authentication timeout. Please run `veogent login --device` again.');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
getSuccessData(body) {
|
|
96
|
+
if (body?.data?.data) return body.data.data;
|
|
97
|
+
if (body?.data) return body.data;
|
|
98
|
+
return body;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
extractErrorCode(body) {
|
|
102
|
+
return (
|
|
103
|
+
body?.error?.code ||
|
|
104
|
+
body?.data?.error?.code ||
|
|
105
|
+
body?.errorCode ||
|
|
106
|
+
body?.data?.errorCode ||
|
|
107
|
+
body?.error?.message?.en ||
|
|
108
|
+
body?.data?.error?.message?.en ||
|
|
109
|
+
body?.error?.message ||
|
|
110
|
+
body?.message ||
|
|
111
|
+
''
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
extractError(body) {
|
|
116
|
+
return (
|
|
117
|
+
body?.error?.message?.en ||
|
|
118
|
+
body?.data?.error?.message?.en ||
|
|
119
|
+
body?.error?.message ||
|
|
120
|
+
body?.message ||
|
|
121
|
+
null
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
sleep(ms) {
|
|
126
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
127
|
+
}
|
|
128
|
+
}
|