veogent 1.1.2 → 1.1.4

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.
@@ -0,0 +1,8 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(grep -oE 'Skill: `[a-z][-a-z]*' /Users/tuan.nguyen12/Documents/tuan.nguyen12/Repo/ISEMI/veogent-cli/.claude/worktrees/vigorous-hellman/skills/SKILL.md)",
5
+ "Bash(sed 's/Skill: `//')"
6
+ ]
7
+ }
8
+ }
@@ -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
+ }