vargai 0.4.0-alpha85 → 0.4.0-alpha87
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/hello.tsx +35 -0
- package/package.json +1 -1
- package/src/ai-sdk/examples/garry-tan-varg.ts +61 -0
- package/src/ai-sdk/providers/varg.ts +14 -1
- package/src/cli/commands/balance.ts +102 -0
- package/src/cli/commands/index.ts +4 -0
- package/src/cli/commands/login.tsx +563 -0
- package/src/cli/commands/logout.ts +57 -0
- package/src/cli/commands/render.tsx +13 -2
- package/src/cli/commands/topup.ts +83 -0
- package/src/cli/credentials.ts +112 -0
- package/src/cli/index.ts +8 -0
- package/src/react/examples/omnihuman15-react-test.tsx +58 -0
- package/.claude/settings.local.json +0 -7
- package/.env.example +0 -33
- package/.github/workflows/ci.yml +0 -23
- package/.husky/README.md +0 -102
- package/.husky/commit-msg +0 -6
- package/.husky/pre-commit +0 -9
- package/.husky/pre-push +0 -6
package/hello.tsx
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** @jsxImportSource vargai */
|
|
2
|
+
import { Render, Clip, Image, Video, assets } from "vargai/react";
|
|
3
|
+
import { fal } from "vargai/ai";
|
|
4
|
+
|
|
5
|
+
const girl = Image({
|
|
6
|
+
prompt: {
|
|
7
|
+
text: `Using the attached reference images, generate a photorealistic three-quarter editorial portrait of the exact same character — maintain identical face, hairstyle, and proportions from Image 1.
|
|
8
|
+
|
|
9
|
+
Framing: Head and shoulders, cropped at upper chest. Direct eye contact with camera.
|
|
10
|
+
|
|
11
|
+
Natural confident expression, relaxed shoulders.
|
|
12
|
+
Preserve the outfit neckline and visible clothing details from reference.
|
|
13
|
+
|
|
14
|
+
Background: Deep black with two contrasting orange gradient accents matching Reference 2. Soft gradient bleed, no hard edges.
|
|
15
|
+
|
|
16
|
+
Shot on 85mm f/1.4 lens, shallow depth of field. Clean studio lighting — soft key light on face, subtle rim light on hair and shoulders for separation. High-end fashion editorial aesthetic.`,
|
|
17
|
+
images: [assets.characters.orangeGirl, assets.backgrounds.orangeGradient],
|
|
18
|
+
},
|
|
19
|
+
model: fal.imageModel("nano-banana-pro/edit"),
|
|
20
|
+
aspectRatio: "9:16",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export default (
|
|
24
|
+
<Render width={1080} height={1920}>
|
|
25
|
+
<Clip duration={5}>
|
|
26
|
+
<Video
|
|
27
|
+
prompt={{
|
|
28
|
+
text: "She waves hello warmly, natural smile, friendly expression. Studio lighting, authentic confident slightly playful atmosphere. Camera static. Intense orange lighting.",
|
|
29
|
+
images: [girl],
|
|
30
|
+
}}
|
|
31
|
+
model={fal.videoModel("kling-v2.5")}
|
|
32
|
+
/>
|
|
33
|
+
</Clip>
|
|
34
|
+
</Render>
|
|
35
|
+
);
|
package/package.json
CHANGED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Garry Tan Talking Head Video
|
|
3
|
+
* Generate a video of Garry Tan saying "varg.ai is cool!"
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
generateImage,
|
|
8
|
+
experimental_generateSpeech as generateSpeech,
|
|
9
|
+
} from "ai";
|
|
10
|
+
import { elevenlabs, File, fal, generateVideo } from "../index";
|
|
11
|
+
|
|
12
|
+
async function main() {
|
|
13
|
+
const script = `varg.ai is cool!`;
|
|
14
|
+
|
|
15
|
+
console.log("generating Garry Tan image and voice in parallel...");
|
|
16
|
+
const [imageResult, speechResult] = await Promise.all([
|
|
17
|
+
generateImage({
|
|
18
|
+
model: fal.imageModel("flux-schnell"),
|
|
19
|
+
prompt:
|
|
20
|
+
"Garry Tan, Y Combinator CEO, Asian American man, short dark hair, glasses, friendly smile, professional headshot, studio lighting, clean background, looking at camera",
|
|
21
|
+
n: 1,
|
|
22
|
+
}),
|
|
23
|
+
generateSpeech({
|
|
24
|
+
model: elevenlabs.speechModel("turbo"),
|
|
25
|
+
text: script,
|
|
26
|
+
voice: "adam",
|
|
27
|
+
}),
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const firstImage = imageResult.images[0];
|
|
31
|
+
if (!firstImage) throw new Error("No image generated");
|
|
32
|
+
const image = File.from(firstImage);
|
|
33
|
+
const audio = File.from(speechResult.audio);
|
|
34
|
+
|
|
35
|
+
console.log(`image: ${(await image.data()).byteLength} bytes`);
|
|
36
|
+
console.log(`audio: ${(await audio.data()).byteLength} bytes`);
|
|
37
|
+
|
|
38
|
+
await Bun.write("output/garry-tan-image.png", await image.data());
|
|
39
|
+
await Bun.write("output/garry-tan-voice.mp3", await audio.data());
|
|
40
|
+
|
|
41
|
+
console.log("\nanimating Garry Tan (5 seconds)...");
|
|
42
|
+
const { video } = await generateVideo({
|
|
43
|
+
model: fal.videoModel("wan-2.5"),
|
|
44
|
+
prompt: {
|
|
45
|
+
text: "man talking naturally, moving mouth while speaking, subtle head movements, professional demeanor, blinking naturally",
|
|
46
|
+
images: [await image.data()],
|
|
47
|
+
},
|
|
48
|
+
duration: 5,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const output = File.from(video);
|
|
52
|
+
console.log(`video: ${(await output.data()).byteLength} bytes`);
|
|
53
|
+
await Bun.write("output/garry-tan-varg.mp4", await output.data());
|
|
54
|
+
|
|
55
|
+
console.log("\ndone! files saved to output/");
|
|
56
|
+
console.log("- output/garry-tan-image.png");
|
|
57
|
+
console.log("- output/garry-tan-voice.mp3");
|
|
58
|
+
console.log("- output/garry-tan-varg.mp4");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
main().catch(console.error);
|
|
@@ -43,7 +43,20 @@ class VargAPIError extends Error {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
function resolveConfig(settings: VargProviderSettings = {}) {
|
|
46
|
-
|
|
46
|
+
let apiKey = settings.apiKey ?? process.env.VARG_API_KEY ?? "";
|
|
47
|
+
|
|
48
|
+
// Fallback to global credentials (~/.varg/credentials) if no key from settings or env
|
|
49
|
+
if (!apiKey) {
|
|
50
|
+
try {
|
|
51
|
+
const { getGlobalApiKey } = require("../../cli/credentials") as {
|
|
52
|
+
getGlobalApiKey: () => string | null;
|
|
53
|
+
};
|
|
54
|
+
apiKey = getGlobalApiKey() ?? "";
|
|
55
|
+
} catch {
|
|
56
|
+
// credentials module may not be available in all contexts (e.g., browser)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
47
60
|
const baseUrl = settings.baseUrl ?? "https://api.varg.ai/v1";
|
|
48
61
|
return { apiKey, baseUrl };
|
|
49
62
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vargai balance — check your credit balance
|
|
3
|
+
*
|
|
4
|
+
* Fetches the current balance from the Gateway API using the saved API key.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { defineCommand } from "citty";
|
|
8
|
+
import { getCredentials, getGlobalApiKey } from "../credentials";
|
|
9
|
+
|
|
10
|
+
const GATEWAY_URL = process.env.VARG_GATEWAY_URL ?? "https://api.varg.ai";
|
|
11
|
+
|
|
12
|
+
const COLORS = {
|
|
13
|
+
reset: "\x1b[0m",
|
|
14
|
+
bold: "\x1b[1m",
|
|
15
|
+
dim: "\x1b[2m",
|
|
16
|
+
green: "\x1b[32m",
|
|
17
|
+
yellow: "\x1b[33m",
|
|
18
|
+
red: "\x1b[31m",
|
|
19
|
+
cyan: "\x1b[36m",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function formatCents(cents: number): string {
|
|
23
|
+
return `$${(cents / 100).toLocaleString("en-US", { minimumFractionDigits: 2 })}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const balanceCmd = defineCommand({
|
|
27
|
+
meta: {
|
|
28
|
+
name: "balance",
|
|
29
|
+
description: "check your credit balance",
|
|
30
|
+
},
|
|
31
|
+
async run() {
|
|
32
|
+
const apiKey = process.env.VARG_API_KEY ?? getGlobalApiKey();
|
|
33
|
+
const creds = getCredentials();
|
|
34
|
+
|
|
35
|
+
if (!apiKey) {
|
|
36
|
+
console.log();
|
|
37
|
+
console.log(
|
|
38
|
+
`${COLORS.yellow} !${COLORS.reset} Not logged in. Run ${COLORS.cyan}vargai login${COLORS.reset} first.`,
|
|
39
|
+
);
|
|
40
|
+
console.log();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
process.stdout.write(
|
|
45
|
+
`\n${COLORS.dim} ● Fetching balance...${COLORS.reset}`,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const res = await fetch(`${GATEWAY_URL}/v1/balance`, {
|
|
50
|
+
headers: {
|
|
51
|
+
Authorization: `Bearer ${apiKey}`,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
process.stdout.write("\r\x1b[K");
|
|
57
|
+
const body = (await res.json().catch(() => ({}))) as {
|
|
58
|
+
error?: string | { message?: string };
|
|
59
|
+
};
|
|
60
|
+
const errMsg =
|
|
61
|
+
typeof body.error === "string"
|
|
62
|
+
? body.error
|
|
63
|
+
: (body.error?.message ??
|
|
64
|
+
`Failed to fetch balance (${res.status})`);
|
|
65
|
+
console.log(`${COLORS.red} ✗${COLORS.reset} ${errMsg}`);
|
|
66
|
+
console.log();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const data = (await res.json()) as { balance_cents: number };
|
|
71
|
+
|
|
72
|
+
process.stdout.write("\r\x1b[K");
|
|
73
|
+
|
|
74
|
+
console.log(
|
|
75
|
+
`${COLORS.bold}${COLORS.cyan}varg${COLORS.reset}${COLORS.dim} — account balance${COLORS.reset}`,
|
|
76
|
+
);
|
|
77
|
+
console.log();
|
|
78
|
+
|
|
79
|
+
if (creds?.email) {
|
|
80
|
+
console.log(` ${COLORS.dim}Account:${COLORS.reset} ${creds.email}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log(
|
|
84
|
+
` ${COLORS.dim}Balance:${COLORS.reset} ${COLORS.bold}${data.balance_cents.toLocaleString()} credits${COLORS.reset} (${formatCents(data.balance_cents)})`,
|
|
85
|
+
);
|
|
86
|
+
console.log();
|
|
87
|
+
|
|
88
|
+
if (data.balance_cents <= 0) {
|
|
89
|
+
console.log(
|
|
90
|
+
` ${COLORS.yellow}No credits remaining.${COLORS.reset} Run ${COLORS.cyan}vargai topup${COLORS.reset} to add more.`,
|
|
91
|
+
);
|
|
92
|
+
console.log();
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
process.stdout.write("\r\x1b[K");
|
|
96
|
+
console.log(
|
|
97
|
+
`${COLORS.red} ✗${COLORS.reset} Failed to connect to gateway. Check your connection.`,
|
|
98
|
+
);
|
|
99
|
+
console.log();
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
});
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
export { balanceCmd } from "./balance.ts";
|
|
1
2
|
export { findCmd, showFindHelp } from "./find.tsx";
|
|
2
3
|
export { frameCmd, showFrameHelp } from "./frame.tsx";
|
|
3
4
|
export { helloCmd } from "./hello.ts";
|
|
4
5
|
export { helpCmd, showHelp } from "./help.tsx";
|
|
5
6
|
export { initCmd, showInitHelp } from "./init.tsx";
|
|
6
7
|
export { listCmd, showListHelp } from "./list.tsx";
|
|
8
|
+
export { loginCmd } from "./login.tsx";
|
|
9
|
+
export { logoutCmd } from "./logout.ts";
|
|
7
10
|
export {
|
|
8
11
|
previewCmd,
|
|
9
12
|
renderCmd,
|
|
@@ -13,4 +16,5 @@ export {
|
|
|
13
16
|
export { runCmd, showRunHelp, showTargetHelp } from "./run.tsx";
|
|
14
17
|
export { showStoryboardHelp, storyboardCmd } from "./storyboard.tsx";
|
|
15
18
|
export { studioCmd } from "./studio.ts";
|
|
19
|
+
export { topupCmd } from "./topup.ts";
|
|
16
20
|
export { showWhichHelp, whichCmd } from "./which.tsx";
|
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vargai login — agent-first authentication
|
|
3
|
+
*
|
|
4
|
+
* Two modes:
|
|
5
|
+
* 1. Email OTP — sign in via email magic code (creates account + API key)
|
|
6
|
+
* 2. API key — paste an existing API key directly
|
|
7
|
+
*
|
|
8
|
+
* The `runLogin()` function is exported so `init` can embed the login flow.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { defineCommand } from "citty";
|
|
12
|
+
import {
|
|
13
|
+
getCredentials,
|
|
14
|
+
getCredentialsPath,
|
|
15
|
+
saveCredentials,
|
|
16
|
+
} from "../credentials";
|
|
17
|
+
|
|
18
|
+
const APP_URL = process.env.VARG_APP_URL ?? "https://app.varg.ai";
|
|
19
|
+
const GATEWAY_URL = process.env.VARG_GATEWAY_URL ?? "https://api.varg.ai";
|
|
20
|
+
|
|
21
|
+
export const COLORS = {
|
|
22
|
+
reset: "\x1b[0m",
|
|
23
|
+
bold: "\x1b[1m",
|
|
24
|
+
dim: "\x1b[2m",
|
|
25
|
+
green: "\x1b[32m",
|
|
26
|
+
yellow: "\x1b[33m",
|
|
27
|
+
blue: "\x1b[34m",
|
|
28
|
+
red: "\x1b[31m",
|
|
29
|
+
cyan: "\x1b[36m",
|
|
30
|
+
gray: "\x1b[90m",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const log = {
|
|
34
|
+
info: (msg: string) =>
|
|
35
|
+
console.log(`${COLORS.blue}info${COLORS.reset} ${msg}`),
|
|
36
|
+
success: (msg: string) =>
|
|
37
|
+
console.log(`${COLORS.green} ✓${COLORS.reset} ${msg}`),
|
|
38
|
+
error: (msg: string) => console.log(`${COLORS.red} ✗${COLORS.reset} ${msg}`),
|
|
39
|
+
warn: (msg: string) =>
|
|
40
|
+
console.log(`${COLORS.yellow} !${COLORS.reset} ${msg}`),
|
|
41
|
+
step: (msg: string) =>
|
|
42
|
+
console.log(
|
|
43
|
+
`\n${COLORS.bold}${COLORS.cyan}==>${COLORS.reset} ${COLORS.bold}${msg}${COLORS.reset}`,
|
|
44
|
+
),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Credit packages (mirrored from app/src/config/credit-packages.ts)
|
|
48
|
+
const CREDIT_PACKAGES = [
|
|
49
|
+
{
|
|
50
|
+
id: "credits-2000",
|
|
51
|
+
credits: 2000,
|
|
52
|
+
amountCents: 2000,
|
|
53
|
+
label: "2,000 credits",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "credits-5000",
|
|
57
|
+
credits: 5000,
|
|
58
|
+
amountCents: 5000,
|
|
59
|
+
label: "5,000 credits",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "credits-10000",
|
|
63
|
+
credits: 10000,
|
|
64
|
+
amountCents: 10000,
|
|
65
|
+
label: "10,000 credits",
|
|
66
|
+
popular: true,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "credits-20000",
|
|
70
|
+
credits: 20000,
|
|
71
|
+
amountCents: 20000,
|
|
72
|
+
label: "20,000 credits",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "credits-50000",
|
|
76
|
+
credits: 50000,
|
|
77
|
+
amountCents: 50000,
|
|
78
|
+
label: "50,000 credits",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "credits-100000",
|
|
82
|
+
credits: 100000,
|
|
83
|
+
amountCents: 100000,
|
|
84
|
+
label: "100,000 credits",
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
function formatCents(cents: number): string {
|
|
89
|
+
return `$${(cents / 100).toLocaleString("en-US", { minimumFractionDigits: 0 })}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function maskApiKey(key: string): string {
|
|
93
|
+
if (key.length <= 16) return key;
|
|
94
|
+
return `${key.slice(0, 12)}...${key.slice(-4)}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function readLine(prompt: string): Promise<string> {
|
|
98
|
+
process.stdout.write(prompt);
|
|
99
|
+
return new Promise<string>((resolve) => {
|
|
100
|
+
process.stdin.setEncoding("utf8");
|
|
101
|
+
process.stdin.ref();
|
|
102
|
+
process.stdin.resume();
|
|
103
|
+
process.stdin.once("data", (data) => {
|
|
104
|
+
process.stdin.pause();
|
|
105
|
+
resolve(data.toString().trim());
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function openBrowser(url: string): Promise<void> {
|
|
111
|
+
const platform = process.platform;
|
|
112
|
+
try {
|
|
113
|
+
if (platform === "darwin") {
|
|
114
|
+
Bun.spawn(["open", url]);
|
|
115
|
+
} else if (platform === "linux") {
|
|
116
|
+
Bun.spawn(["xdg-open", url]);
|
|
117
|
+
} else if (platform === "win32") {
|
|
118
|
+
Bun.spawn(["cmd", "/c", "start", url]);
|
|
119
|
+
} else {
|
|
120
|
+
log.warn(`Could not open browser. Visit this URL manually:\n ${url}`);
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
log.warn(`Could not open browser. Visit this URL manually:\n ${url}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ──── Login Result ────
|
|
128
|
+
|
|
129
|
+
export interface LoginResult {
|
|
130
|
+
apiKey: string;
|
|
131
|
+
email: string;
|
|
132
|
+
balanceCents: number;
|
|
133
|
+
/** Only available after email OTP login, not API key login */
|
|
134
|
+
accessToken: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ──── API Key Login ────
|
|
138
|
+
|
|
139
|
+
async function loginWithApiKey(): Promise<LoginResult | null> {
|
|
140
|
+
console.log();
|
|
141
|
+
console.log(
|
|
142
|
+
`${COLORS.dim} Paste your API key from ${COLORS.reset}${COLORS.cyan}https://app.varg.ai${COLORS.reset}`,
|
|
143
|
+
);
|
|
144
|
+
console.log();
|
|
145
|
+
|
|
146
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
147
|
+
const key = await readLine(` API key: `);
|
|
148
|
+
|
|
149
|
+
if (!key) {
|
|
150
|
+
log.error("No key entered.");
|
|
151
|
+
if (attempt < 2) {
|
|
152
|
+
console.log(
|
|
153
|
+
`${COLORS.dim} Try again (${2 - attempt} attempts left)${COLORS.reset}`,
|
|
154
|
+
);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Validate by calling the gateway balance endpoint
|
|
161
|
+
process.stdout.write(
|
|
162
|
+
`${COLORS.dim} ● Validating API key...${COLORS.reset}`,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const res = await fetch(`${GATEWAY_URL}/v1/balance`, {
|
|
167
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (!res.ok) {
|
|
171
|
+
process.stdout.write("\r\x1b[K");
|
|
172
|
+
if (res.status === 401 || res.status === 403) {
|
|
173
|
+
log.error("Invalid API key.");
|
|
174
|
+
} else {
|
|
175
|
+
const body = (await res.json().catch(() => ({}))) as {
|
|
176
|
+
error?: string | { message?: string };
|
|
177
|
+
};
|
|
178
|
+
const errMsg =
|
|
179
|
+
typeof body.error === "string"
|
|
180
|
+
? body.error
|
|
181
|
+
: (body.error?.message ?? `Validation failed (${res.status})`);
|
|
182
|
+
log.error(errMsg);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (attempt < 2) {
|
|
186
|
+
console.log(
|
|
187
|
+
`${COLORS.dim} Try again (${2 - attempt} attempts left)${COLORS.reset}`,
|
|
188
|
+
);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const data = (await res.json()) as { balance_cents: number };
|
|
195
|
+
process.stdout.write("\r\x1b[K");
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
apiKey: key,
|
|
199
|
+
email: "", // unknown for direct API key login
|
|
200
|
+
balanceCents: data.balance_cents,
|
|
201
|
+
accessToken: "", // not available for API key login
|
|
202
|
+
};
|
|
203
|
+
} catch {
|
|
204
|
+
process.stdout.write("\r\x1b[K");
|
|
205
|
+
log.error("Failed to connect to gateway. Check your connection.");
|
|
206
|
+
if (attempt < 2) {
|
|
207
|
+
console.log(
|
|
208
|
+
`${COLORS.dim} Try again (${2 - attempt} attempts left)${COLORS.reset}`,
|
|
209
|
+
);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ──── Email OTP Login ────
|
|
220
|
+
|
|
221
|
+
async function loginWithEmail(): Promise<LoginResult | null> {
|
|
222
|
+
console.log();
|
|
223
|
+
|
|
224
|
+
const email = await readLine(` Enter your email: `);
|
|
225
|
+
|
|
226
|
+
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
227
|
+
log.error("Invalid email address.");
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Send OTP
|
|
232
|
+
console.log();
|
|
233
|
+
process.stdout.write(
|
|
234
|
+
`${COLORS.dim} ● Sending verification code...${COLORS.reset}`,
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
const sendRes = await fetch(`${APP_URL}/api/auth/cli/send-otp`, {
|
|
238
|
+
method: "POST",
|
|
239
|
+
headers: { "Content-Type": "application/json" },
|
|
240
|
+
body: JSON.stringify({ email }),
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
if (!sendRes.ok) {
|
|
244
|
+
const err = (await sendRes.json().catch(() => ({}))) as { error?: string };
|
|
245
|
+
process.stdout.write("\r\x1b[K");
|
|
246
|
+
log.error(err.error ?? "Failed to send verification code.");
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
process.stdout.write("\r\x1b[K");
|
|
251
|
+
log.success("Code sent! Check your inbox.");
|
|
252
|
+
console.log();
|
|
253
|
+
|
|
254
|
+
// Get OTP code (up to 3 attempts)
|
|
255
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
256
|
+
const code = await readLine(` Enter the 6-digit code: `);
|
|
257
|
+
|
|
258
|
+
if (!code || !/^\d{6}$/.test(code)) {
|
|
259
|
+
log.error("Code must be 6 digits.");
|
|
260
|
+
if (attempt < 2) {
|
|
261
|
+
console.log(
|
|
262
|
+
`${COLORS.dim} Try again (${2 - attempt} attempts left)${COLORS.reset}`,
|
|
263
|
+
);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
process.stdout.write(`${COLORS.dim} ● Verifying...${COLORS.reset}`);
|
|
270
|
+
|
|
271
|
+
const verifyRes = await fetch(`${APP_URL}/api/auth/cli/verify-otp`, {
|
|
272
|
+
method: "POST",
|
|
273
|
+
headers: { "Content-Type": "application/json" },
|
|
274
|
+
body: JSON.stringify({ email, code }),
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
if (!verifyRes.ok) {
|
|
278
|
+
const err = (await verifyRes.json().catch(() => ({}))) as {
|
|
279
|
+
error?: string;
|
|
280
|
+
};
|
|
281
|
+
process.stdout.write("\r\x1b[K");
|
|
282
|
+
|
|
283
|
+
if (verifyRes.status === 401 && attempt < 2) {
|
|
284
|
+
log.error(err.error ?? "Invalid code.");
|
|
285
|
+
console.log(
|
|
286
|
+
`${COLORS.dim} Try again (${2 - attempt} attempts left)${COLORS.reset}`,
|
|
287
|
+
);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
log.error(err.error ?? "Verification failed.");
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const result = (await verifyRes.json()) as {
|
|
296
|
+
api_key: string;
|
|
297
|
+
email: string;
|
|
298
|
+
balance_cents: number;
|
|
299
|
+
access_token: string;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
process.stdout.write("\r\x1b[K");
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
apiKey: result.api_key,
|
|
306
|
+
email: result.email,
|
|
307
|
+
balanceCents: result.balance_cents,
|
|
308
|
+
accessToken: result.access_token,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ──── Credit Package Selector ────
|
|
316
|
+
|
|
317
|
+
export async function showCreditPackages(accessToken: string): Promise<void> {
|
|
318
|
+
// Need an access token for Stripe checkout — only available after email login
|
|
319
|
+
if (!accessToken) {
|
|
320
|
+
console.log();
|
|
321
|
+
console.log(
|
|
322
|
+
`${COLORS.dim} Add credits anytime with ${COLORS.cyan}vargai topup${COLORS.reset}${COLORS.dim} or at ${COLORS.cyan}https://app.varg.ai${COLORS.reset}`,
|
|
323
|
+
);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
console.log();
|
|
328
|
+
console.log(
|
|
329
|
+
`${COLORS.dim}───${COLORS.reset} ${COLORS.bold}Add credits${COLORS.reset} ${COLORS.dim}${"─".repeat(40)}${COLORS.reset}`,
|
|
330
|
+
);
|
|
331
|
+
console.log();
|
|
332
|
+
|
|
333
|
+
for (let i = 0; i < CREDIT_PACKAGES.length; i++) {
|
|
334
|
+
const pkg = CREDIT_PACKAGES[i]!;
|
|
335
|
+
const num = `[${i + 1}]`;
|
|
336
|
+
const popular = pkg.popular
|
|
337
|
+
? ` ${COLORS.yellow}★ popular${COLORS.reset}`
|
|
338
|
+
: "";
|
|
339
|
+
const price = formatCents(pkg.amountCents).padStart(7);
|
|
340
|
+
console.log(
|
|
341
|
+
` ${COLORS.cyan}${num}${COLORS.reset} ${pkg.label.padEnd(18)} ${COLORS.dim}-${COLORS.reset} ${COLORS.bold}${price}${COLORS.reset}${popular}`,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
console.log();
|
|
346
|
+
console.log(` ${COLORS.dim}[s] Skip for now${COLORS.reset}`);
|
|
347
|
+
console.log();
|
|
348
|
+
|
|
349
|
+
const selection = await readLine(
|
|
350
|
+
` Select a package (1-${CREDIT_PACKAGES.length}) or [s] to skip: `,
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
if (selection.toLowerCase() === "s" || selection === "") {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const pkgIndex = parseInt(selection, 10) - 1;
|
|
358
|
+
if (isNaN(pkgIndex) || pkgIndex < 0 || pkgIndex >= CREDIT_PACKAGES.length) {
|
|
359
|
+
log.warn("Invalid selection. Skipping.");
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const selectedPkg = CREDIT_PACKAGES[pkgIndex]!;
|
|
364
|
+
|
|
365
|
+
// Create Stripe checkout session
|
|
366
|
+
process.stdout.write(
|
|
367
|
+
`\n${COLORS.dim} ● Creating checkout session...${COLORS.reset}`,
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
const checkoutRes = await fetch(`${APP_URL}/api/billing/checkout`, {
|
|
371
|
+
method: "POST",
|
|
372
|
+
headers: {
|
|
373
|
+
"Content-Type": "application/json",
|
|
374
|
+
Authorization: `Bearer ${accessToken}`,
|
|
375
|
+
Origin: APP_URL,
|
|
376
|
+
},
|
|
377
|
+
body: JSON.stringify({ packageId: selectedPkg.id }),
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
if (!checkoutRes.ok) {
|
|
381
|
+
process.stdout.write("\r\x1b[K");
|
|
382
|
+
const err = (await checkoutRes.json().catch(() => ({}))) as {
|
|
383
|
+
error?: string;
|
|
384
|
+
};
|
|
385
|
+
log.error(err.error ?? "Failed to create checkout session.");
|
|
386
|
+
console.log();
|
|
387
|
+
log.info(
|
|
388
|
+
`You can add credits anytime with ${COLORS.cyan}vargai topup${COLORS.reset} or at ${COLORS.cyan}https://app.varg.ai${COLORS.reset}`,
|
|
389
|
+
);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const { url } = (await checkoutRes.json()) as { url: string };
|
|
394
|
+
|
|
395
|
+
process.stdout.write("\r\x1b[K");
|
|
396
|
+
|
|
397
|
+
log.success("Opening Stripe checkout in your browser...");
|
|
398
|
+
console.log();
|
|
399
|
+
|
|
400
|
+
await openBrowser(url);
|
|
401
|
+
|
|
402
|
+
console.log(
|
|
403
|
+
`${COLORS.dim} If the browser didn't open, visit:${COLORS.reset}`,
|
|
404
|
+
);
|
|
405
|
+
console.log(` ${COLORS.cyan}${url}${COLORS.reset}`);
|
|
406
|
+
console.log();
|
|
407
|
+
log.info("Credits will be added to your account after payment.");
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ──── Main Login Flow (exported for use by init) ────
|
|
411
|
+
|
|
412
|
+
export interface RunLoginOptions {
|
|
413
|
+
/** Show credit package selector after login. Default: true */
|
|
414
|
+
showPackages?: boolean;
|
|
415
|
+
/** Show the header banner. Default: true */
|
|
416
|
+
showHeader?: boolean;
|
|
417
|
+
/** Skip the "already logged in" check. Default: false */
|
|
418
|
+
forceLogin?: boolean;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Run the interactive login flow. Returns the login result, or null if
|
|
423
|
+
* the user cancelled / was already logged in and chose to keep credentials.
|
|
424
|
+
*
|
|
425
|
+
* Can be called from `vargai login` or embedded in `vargai init`.
|
|
426
|
+
*/
|
|
427
|
+
export async function runLogin(
|
|
428
|
+
options: RunLoginOptions = {},
|
|
429
|
+
): Promise<LoginResult | null> {
|
|
430
|
+
const {
|
|
431
|
+
showPackages = true,
|
|
432
|
+
showHeader = true,
|
|
433
|
+
forceLogin = false,
|
|
434
|
+
} = options;
|
|
435
|
+
|
|
436
|
+
if (showHeader) {
|
|
437
|
+
console.log();
|
|
438
|
+
console.log(
|
|
439
|
+
`${COLORS.bold}${COLORS.cyan}varg${COLORS.reset}${COLORS.dim} — ai video infrastructure${COLORS.reset}`,
|
|
440
|
+
);
|
|
441
|
+
console.log();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Check if already logged in
|
|
445
|
+
if (!forceLogin) {
|
|
446
|
+
const existing = getCredentials();
|
|
447
|
+
if (existing) {
|
|
448
|
+
const emailLabel = existing.email
|
|
449
|
+
? existing.email
|
|
450
|
+
: maskApiKey(existing.api_key);
|
|
451
|
+
console.log(
|
|
452
|
+
`${COLORS.dim}Already logged in as ${COLORS.reset}${COLORS.bold}${emailLabel}${COLORS.reset}`,
|
|
453
|
+
);
|
|
454
|
+
if (existing.email) {
|
|
455
|
+
console.log(
|
|
456
|
+
`${COLORS.dim}API key: ${maskApiKey(existing.api_key)}${COLORS.reset}`,
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
console.log();
|
|
460
|
+
|
|
461
|
+
const answer = await readLine(
|
|
462
|
+
`${COLORS.yellow}Log in as a different account?${COLORS.reset} (y/N): `,
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
if (answer.toLowerCase() !== "y") {
|
|
466
|
+
log.info("Keeping existing credentials.");
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
console.log();
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Mode selector
|
|
474
|
+
log.step("Sign in to varg.ai");
|
|
475
|
+
console.log();
|
|
476
|
+
console.log(
|
|
477
|
+
` ${COLORS.cyan}[1]${COLORS.reset} Email ${COLORS.dim}— sign in with your email (creates account if needed)${COLORS.reset}`,
|
|
478
|
+
);
|
|
479
|
+
console.log(
|
|
480
|
+
` ${COLORS.cyan}[2]${COLORS.reset} API key ${COLORS.dim}— paste an existing API key${COLORS.reset}`,
|
|
481
|
+
);
|
|
482
|
+
console.log();
|
|
483
|
+
|
|
484
|
+
const mode = await readLine(` Select login method (1-2): `);
|
|
485
|
+
|
|
486
|
+
let result: LoginResult | null = null;
|
|
487
|
+
|
|
488
|
+
if (mode === "2") {
|
|
489
|
+
result = await loginWithApiKey();
|
|
490
|
+
} else {
|
|
491
|
+
// Default to email login (mode "1" or anything else)
|
|
492
|
+
result = await loginWithEmail();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (!result) {
|
|
496
|
+
log.error("Login failed.");
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Save credentials
|
|
501
|
+
saveCredentials({
|
|
502
|
+
api_key: result.apiKey,
|
|
503
|
+
email: result.email,
|
|
504
|
+
created_at: new Date().toISOString(),
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
console.log();
|
|
508
|
+
if (result.email) {
|
|
509
|
+
log.success(`Logged in as ${COLORS.bold}${result.email}${COLORS.reset}`);
|
|
510
|
+
} else {
|
|
511
|
+
log.success("API key validated and saved.");
|
|
512
|
+
}
|
|
513
|
+
log.success(
|
|
514
|
+
`API key saved to ${COLORS.dim}${getCredentialsPath()}${COLORS.reset}`,
|
|
515
|
+
);
|
|
516
|
+
log.success(
|
|
517
|
+
`Balance: ${COLORS.bold}${result.balanceCents.toLocaleString()} credits${COLORS.reset} (${formatCents(result.balanceCents)})`,
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
// Credit packages
|
|
521
|
+
if (showPackages) {
|
|
522
|
+
await showCreditPackages(result.accessToken);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return result;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// ──── Get Started Message ────
|
|
529
|
+
|
|
530
|
+
function printGetStarted(): void {
|
|
531
|
+
console.log();
|
|
532
|
+
console.log(`${COLORS.bold}Get started:${COLORS.reset}`);
|
|
533
|
+
console.log(
|
|
534
|
+
` ${COLORS.cyan}vargai init${COLORS.reset} ${COLORS.dim}Set up a new project${COLORS.reset}`,
|
|
535
|
+
);
|
|
536
|
+
console.log(
|
|
537
|
+
` ${COLORS.cyan}vargai render${COLORS.reset} ${COLORS.dim}Render a video${COLORS.reset}`,
|
|
538
|
+
);
|
|
539
|
+
console.log(
|
|
540
|
+
` ${COLORS.cyan}vargai topup${COLORS.reset} ${COLORS.dim}Add credits to your account${COLORS.reset}`,
|
|
541
|
+
);
|
|
542
|
+
console.log();
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// ──── Command Definition ────
|
|
546
|
+
|
|
547
|
+
export const loginCmd = defineCommand({
|
|
548
|
+
meta: {
|
|
549
|
+
name: "login",
|
|
550
|
+
description: "sign in to varg.ai and get your API key",
|
|
551
|
+
},
|
|
552
|
+
async run() {
|
|
553
|
+
try {
|
|
554
|
+
const result = await runLogin({ showPackages: true, showHeader: true });
|
|
555
|
+
if (result) {
|
|
556
|
+
printGetStarted();
|
|
557
|
+
}
|
|
558
|
+
} finally {
|
|
559
|
+
// Allow process to exit cleanly after interactive prompts
|
|
560
|
+
process.stdin.unref();
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vargai logout — clear saved credentials
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { defineCommand } from "citty";
|
|
6
|
+
import {
|
|
7
|
+
clearCredentials,
|
|
8
|
+
getCredentials,
|
|
9
|
+
getCredentialsPath,
|
|
10
|
+
} from "../credentials";
|
|
11
|
+
|
|
12
|
+
const COLORS = {
|
|
13
|
+
reset: "\x1b[0m",
|
|
14
|
+
bold: "\x1b[1m",
|
|
15
|
+
dim: "\x1b[2m",
|
|
16
|
+
green: "\x1b[32m",
|
|
17
|
+
yellow: "\x1b[33m",
|
|
18
|
+
cyan: "\x1b[36m",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const logoutCmd = defineCommand({
|
|
22
|
+
meta: {
|
|
23
|
+
name: "logout",
|
|
24
|
+
description: "sign out and remove saved API key",
|
|
25
|
+
},
|
|
26
|
+
async run() {
|
|
27
|
+
const creds = getCredentials();
|
|
28
|
+
|
|
29
|
+
if (!creds) {
|
|
30
|
+
console.log(
|
|
31
|
+
`\n${COLORS.dim}Not logged in. No credentials to remove.${COLORS.reset}\n`,
|
|
32
|
+
);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const removed = clearCredentials();
|
|
37
|
+
|
|
38
|
+
if (removed) {
|
|
39
|
+
console.log();
|
|
40
|
+
console.log(
|
|
41
|
+
`${COLORS.green} ✓${COLORS.reset} Logged out. Credentials removed from ${COLORS.dim}${getCredentialsPath()}${COLORS.reset}`,
|
|
42
|
+
);
|
|
43
|
+
console.log(
|
|
44
|
+
`${COLORS.dim} Previously logged in as ${creds.email}${COLORS.reset}`,
|
|
45
|
+
);
|
|
46
|
+
console.log();
|
|
47
|
+
console.log(
|
|
48
|
+
`${COLORS.dim}To log in again: ${COLORS.reset}${COLORS.cyan}vargai login${COLORS.reset}`,
|
|
49
|
+
);
|
|
50
|
+
console.log();
|
|
51
|
+
} else {
|
|
52
|
+
console.log(
|
|
53
|
+
`\n${COLORS.yellow} !${COLORS.reset} No credentials file found.\n`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
});
|
|
@@ -18,7 +18,18 @@ async function detectDefaultModels(): Promise<DefaultModels | undefined> {
|
|
|
18
18
|
const defaults: DefaultModels = {};
|
|
19
19
|
|
|
20
20
|
// Gateway provider — single key for all models (recommended)
|
|
21
|
-
|
|
21
|
+
// Check env var first, then global credentials (~/.varg/credentials)
|
|
22
|
+
let hasVargKey = !!process.env.VARG_API_KEY;
|
|
23
|
+
if (!hasVargKey) {
|
|
24
|
+
try {
|
|
25
|
+
const { getGlobalApiKey } = await import("../credentials");
|
|
26
|
+
hasVargKey = !!getGlobalApiKey();
|
|
27
|
+
} catch {
|
|
28
|
+
// credentials module may not be available
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (hasVargKey) {
|
|
22
33
|
const { varg } = await import("../../ai-sdk/providers/varg");
|
|
23
34
|
defaults.image = varg.imageModel("nano-banana-pro");
|
|
24
35
|
defaults.video = varg.videoModel("kling-v3");
|
|
@@ -83,7 +94,7 @@ async function loadComponent(filePath: string): Promise<VargElement> {
|
|
|
83
94
|
|
|
84
95
|
if (hasVargaiImport) {
|
|
85
96
|
const tmpFile = `${tmpDir}/${Date.now()}.tsx`;
|
|
86
|
-
// Resolve
|
|
97
|
+
// Resolve @jsxImportSource to absolute path so it works from the cache dir
|
|
87
98
|
const runtimeDir = resolve(pkgDir, "src/react/runtime");
|
|
88
99
|
const resolvedSource = source.replace(
|
|
89
100
|
/@jsxImportSource\s+vargai/,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vargai topup — add credits to your account
|
|
3
|
+
*
|
|
4
|
+
* Opens the app billing page in the browser where the user can purchase credits.
|
|
5
|
+
* If the user isn't logged in yet, directs them to `vargai login` first.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { defineCommand } from "citty";
|
|
9
|
+
import { getCredentials } from "../credentials";
|
|
10
|
+
|
|
11
|
+
const APP_URL = process.env.VARG_APP_URL ?? "https://app.varg.ai";
|
|
12
|
+
|
|
13
|
+
const COLORS = {
|
|
14
|
+
reset: "\x1b[0m",
|
|
15
|
+
bold: "\x1b[1m",
|
|
16
|
+
dim: "\x1b[2m",
|
|
17
|
+
green: "\x1b[32m",
|
|
18
|
+
yellow: "\x1b[33m",
|
|
19
|
+
cyan: "\x1b[36m",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
async function openBrowser(url: string): Promise<void> {
|
|
23
|
+
const platform = process.platform;
|
|
24
|
+
try {
|
|
25
|
+
if (platform === "darwin") {
|
|
26
|
+
Bun.spawn(["open", url]);
|
|
27
|
+
} else if (platform === "linux") {
|
|
28
|
+
Bun.spawn(["xdg-open", url]);
|
|
29
|
+
} else if (platform === "win32") {
|
|
30
|
+
Bun.spawn(["cmd", "/c", "start", url]);
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
// silently fail — URL is printed below
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const topupCmd = defineCommand({
|
|
38
|
+
meta: {
|
|
39
|
+
name: "topup",
|
|
40
|
+
description: "add credits to your account",
|
|
41
|
+
},
|
|
42
|
+
async run() {
|
|
43
|
+
const creds = getCredentials();
|
|
44
|
+
|
|
45
|
+
if (!creds) {
|
|
46
|
+
console.log();
|
|
47
|
+
console.log(
|
|
48
|
+
`${COLORS.yellow} !${COLORS.reset} Not logged in. Run ${COLORS.cyan}vargai login${COLORS.reset} first.`,
|
|
49
|
+
);
|
|
50
|
+
console.log();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log();
|
|
55
|
+
console.log(
|
|
56
|
+
`${COLORS.bold}${COLORS.cyan}varg${COLORS.reset}${COLORS.dim} — add credits${COLORS.reset}`,
|
|
57
|
+
);
|
|
58
|
+
console.log();
|
|
59
|
+
console.log(
|
|
60
|
+
`${COLORS.dim} Logged in as ${COLORS.reset}${COLORS.bold}${creds.email}${COLORS.reset}`,
|
|
61
|
+
);
|
|
62
|
+
console.log();
|
|
63
|
+
|
|
64
|
+
const billingUrl = `${APP_URL}/dashboard?tab=billing`;
|
|
65
|
+
|
|
66
|
+
console.log(
|
|
67
|
+
`${COLORS.green} ✓${COLORS.reset} Opening billing page in your browser...`,
|
|
68
|
+
);
|
|
69
|
+
console.log();
|
|
70
|
+
|
|
71
|
+
await openBrowser(billingUrl);
|
|
72
|
+
|
|
73
|
+
console.log(
|
|
74
|
+
`${COLORS.dim} If the browser didn't open, visit:${COLORS.reset}`,
|
|
75
|
+
);
|
|
76
|
+
console.log(` ${COLORS.cyan}${billingUrl}${COLORS.reset}`);
|
|
77
|
+
console.log();
|
|
78
|
+
console.log(
|
|
79
|
+
`${COLORS.dim} Log in with ${COLORS.reset}${creds.email}${COLORS.dim} to manage credits.${COLORS.reset}`,
|
|
80
|
+
);
|
|
81
|
+
console.log();
|
|
82
|
+
},
|
|
83
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global credential management for vargai CLI.
|
|
3
|
+
*
|
|
4
|
+
* Stores and retrieves the user's API key from ~/.varg/credentials.
|
|
5
|
+
* File is created with 0600 permissions (owner read/write only).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
existsSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
unlinkSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
} from "node:fs";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { dirname, join } from "node:path";
|
|
17
|
+
|
|
18
|
+
export interface VargCredentials {
|
|
19
|
+
api_key: string;
|
|
20
|
+
email: string;
|
|
21
|
+
created_at: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const CREDENTIALS_DIR = join(homedir(), ".varg");
|
|
25
|
+
const CREDENTIALS_PATH = join(CREDENTIALS_DIR, "credentials");
|
|
26
|
+
|
|
27
|
+
// Module-level cache to avoid repeated file reads
|
|
28
|
+
let _cached: VargCredentials | null | undefined;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get the full credentials object from ~/.varg/credentials.
|
|
32
|
+
* Returns null if the file doesn't exist or is malformed.
|
|
33
|
+
* Result is cached in memory after first read.
|
|
34
|
+
*/
|
|
35
|
+
export function getCredentials(): VargCredentials | null {
|
|
36
|
+
if (_cached !== undefined) return _cached;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
if (!existsSync(CREDENTIALS_PATH)) {
|
|
40
|
+
_cached = null;
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const raw = readFileSync(CREDENTIALS_PATH, "utf-8");
|
|
45
|
+
const parsed = JSON.parse(raw) as Partial<VargCredentials>;
|
|
46
|
+
|
|
47
|
+
if (!parsed.api_key || typeof parsed.api_key !== "string") {
|
|
48
|
+
_cached = null;
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_cached = {
|
|
53
|
+
api_key: parsed.api_key,
|
|
54
|
+
email: parsed.email ?? "",
|
|
55
|
+
created_at: parsed.created_at ?? new Date().toISOString(),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return _cached;
|
|
59
|
+
} catch {
|
|
60
|
+
_cached = null;
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get the global API key from ~/.varg/credentials.
|
|
67
|
+
* Returns null if not logged in.
|
|
68
|
+
*/
|
|
69
|
+
export function getGlobalApiKey(): string | null {
|
|
70
|
+
const creds = getCredentials();
|
|
71
|
+
return creds?.api_key ?? null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Save credentials to ~/.varg/credentials.
|
|
76
|
+
* Creates the ~/.varg/ directory if it doesn't exist.
|
|
77
|
+
* File is written with 0600 permissions (owner read/write only).
|
|
78
|
+
*/
|
|
79
|
+
export function saveCredentials(creds: VargCredentials): void {
|
|
80
|
+
if (!existsSync(CREDENTIALS_DIR)) {
|
|
81
|
+
mkdirSync(CREDENTIALS_DIR, { recursive: true });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
writeFileSync(CREDENTIALS_PATH, JSON.stringify(creds, null, 2) + "\n", {
|
|
85
|
+
mode: 0o600,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Invalidate cache
|
|
89
|
+
_cached = creds;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Delete ~/.varg/credentials and clear cache.
|
|
94
|
+
* Returns true if the file was deleted, false if it didn't exist.
|
|
95
|
+
*/
|
|
96
|
+
export function clearCredentials(): boolean {
|
|
97
|
+
_cached = null;
|
|
98
|
+
|
|
99
|
+
if (!existsSync(CREDENTIALS_PATH)) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
unlinkSync(CREDENTIALS_PATH);
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get the path to the credentials file (for display purposes).
|
|
109
|
+
*/
|
|
110
|
+
export function getCredentialsPath(): string {
|
|
111
|
+
return CREDENTIALS_PATH;
|
|
112
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -12,12 +12,15 @@ import { defineCommand, runMain } from "citty";
|
|
|
12
12
|
import { registry } from "../core/registry";
|
|
13
13
|
import { allDefinitions } from "../definitions";
|
|
14
14
|
import {
|
|
15
|
+
balanceCmd,
|
|
15
16
|
findCmd,
|
|
16
17
|
frameCmd,
|
|
17
18
|
helloCmd,
|
|
18
19
|
helpCmd,
|
|
19
20
|
initCmd,
|
|
20
21
|
listCmd,
|
|
22
|
+
loginCmd,
|
|
23
|
+
logoutCmd,
|
|
21
24
|
previewCmd,
|
|
22
25
|
renderCmd,
|
|
23
26
|
runCmd,
|
|
@@ -34,6 +37,7 @@ import {
|
|
|
34
37
|
showWhichHelp,
|
|
35
38
|
storyboardCmd,
|
|
36
39
|
studioCmd,
|
|
40
|
+
topupCmd,
|
|
37
41
|
whichCmd,
|
|
38
42
|
} from "./commands";
|
|
39
43
|
|
|
@@ -116,6 +120,10 @@ const main = defineCommand({
|
|
|
116
120
|
description: "ai video generation sdk",
|
|
117
121
|
},
|
|
118
122
|
subCommands: {
|
|
123
|
+
login: loginCmd,
|
|
124
|
+
logout: logoutCmd,
|
|
125
|
+
balance: balanceCmd,
|
|
126
|
+
topup: topupCmd,
|
|
119
127
|
hello: helloCmd,
|
|
120
128
|
init: initCmd,
|
|
121
129
|
render: renderCmd,
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OmniHuman v1.5 React syntax test
|
|
3
|
+
*
|
|
4
|
+
* Uses a local image + local audio file to generate a talking video.
|
|
5
|
+
*
|
|
6
|
+
* Run: bun run src/react/examples/omnihuman15-react-test.tsx
|
|
7
|
+
* Output: output/omnihuman15-react-test.mp4
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { fal } from "../../ai-sdk/providers/fal";
|
|
11
|
+
import { Clip, Render, render, Video } from "..";
|
|
12
|
+
|
|
13
|
+
const IMAGE_PATH = "output/garry-tan-image.png";
|
|
14
|
+
const AUDIO_PATH = "output/garry-tan-voice.mp3";
|
|
15
|
+
|
|
16
|
+
const video = (
|
|
17
|
+
<Render width={720} height={1280}>
|
|
18
|
+
<Clip duration={5}>
|
|
19
|
+
<Video
|
|
20
|
+
model={fal.videoModel("omnihuman-v1.5")}
|
|
21
|
+
prompt={{
|
|
22
|
+
text: "friendly professional talking head, natural blinking, subtle head movement",
|
|
23
|
+
images: [IMAGE_PATH],
|
|
24
|
+
audio: AUDIO_PATH,
|
|
25
|
+
}}
|
|
26
|
+
providerOptions={{
|
|
27
|
+
fal: {
|
|
28
|
+
resolution: "720p",
|
|
29
|
+
turbo_mode: true,
|
|
30
|
+
},
|
|
31
|
+
}}
|
|
32
|
+
/>
|
|
33
|
+
</Clip>
|
|
34
|
+
</Render>
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
async function main() {
|
|
38
|
+
if (!process.env.FAL_API_KEY && !process.env.FAL_KEY) {
|
|
39
|
+
console.error("ERROR: FAL_API_KEY/FAL_KEY not found in environment");
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const result = await render(video, {
|
|
44
|
+
output: "output/omnihuman15-react-test.mp4",
|
|
45
|
+
cache: ".cache/ai",
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
console.log(
|
|
49
|
+
`ok: output/omnihuman15-react-test.mp4 (${(result.video.byteLength / 1024 / 1024).toFixed(2)} MB)`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (import.meta.main) {
|
|
54
|
+
main().catch((err) => {
|
|
55
|
+
console.error(err);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
});
|
|
58
|
+
}
|
package/.env.example
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# fal.ai api key
|
|
2
|
-
FAL_API_KEY=fal_xxx
|
|
3
|
-
|
|
4
|
-
# higgsfield credentials
|
|
5
|
-
HIGGSFIELD_API_KEY=hf_xxx
|
|
6
|
-
HIGGSFIELD_SECRET=secret_xxx
|
|
7
|
-
|
|
8
|
-
# elevenlabs api key
|
|
9
|
-
ELEVENLABS_API_KEY=el_xxx
|
|
10
|
-
|
|
11
|
-
# groq api key (ultra-fast whisper transcription)
|
|
12
|
-
GROQ_API_KEY=gsk_xxx
|
|
13
|
-
|
|
14
|
-
# fireworks api key (word-level transcription with timestamps)
|
|
15
|
-
FIREWORKS_API_KEY=fw_xxx
|
|
16
|
-
|
|
17
|
-
# cloudflare r2 / s3 storage
|
|
18
|
-
CLOUDFLARE_R2_API_URL=https://xxx.r2.cloudflarestorage.com
|
|
19
|
-
CLOUDFLARE_ACCESS_KEY_ID=xxx
|
|
20
|
-
CLOUDFLARE_ACCESS_SECRET=xxx
|
|
21
|
-
CLOUDFLARE_R2_BUCKET=m
|
|
22
|
-
|
|
23
|
-
# replicate (optional)
|
|
24
|
-
REPLICATE_API_TOKEN=r8_xxx
|
|
25
|
-
|
|
26
|
-
# apify (web scraping actors)
|
|
27
|
-
APIFY_TOKEN=apify_api_xxx
|
|
28
|
-
|
|
29
|
-
# decart ai (real-time & batch video/image)
|
|
30
|
-
DECART_API_KEY=decart_xxx
|
|
31
|
-
|
|
32
|
-
# together ai (fast flux-schnell, no queue)
|
|
33
|
-
TOGETHER_API_KEY=together_xxx
|
package/.github/workflows/ci.yml
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [main]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [main]
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
lint-and-format:
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
steps:
|
|
13
|
-
- uses: actions/checkout@v4
|
|
14
|
-
|
|
15
|
-
- uses: oven-sh/setup-bun@v2
|
|
16
|
-
with:
|
|
17
|
-
bun-version: latest
|
|
18
|
-
|
|
19
|
-
- name: Install dependencies
|
|
20
|
-
run: bun install
|
|
21
|
-
|
|
22
|
-
- name: Check
|
|
23
|
-
run: bun run check
|
package/.husky/README.md
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
# Git Hooks Configuration
|
|
2
|
-
|
|
3
|
-
This project uses [Husky](https://typicode.github.io/husky/) to manage Git hooks for maintaining code quality and security.
|
|
4
|
-
|
|
5
|
-
## Installed Hooks
|
|
6
|
-
|
|
7
|
-
### `pre-commit`
|
|
8
|
-
Runs before each commit:
|
|
9
|
-
- **Gitleaks** - Scans staged files for secrets and credentials
|
|
10
|
-
- **Lint-staged** - Runs Biome linter/formatter on staged files
|
|
11
|
-
|
|
12
|
-
### `commit-msg`
|
|
13
|
-
Validates commit messages:
|
|
14
|
-
- **Commitlint** - Enforces [Conventional Commits](https://www.conventionalcommits.org/) format
|
|
15
|
-
|
|
16
|
-
### `pre-push`
|
|
17
|
-
Runs before pushing to remote:
|
|
18
|
-
- **TypeScript type checking** - Ensures no type errors before push
|
|
19
|
-
|
|
20
|
-
## Commit Message Format
|
|
21
|
-
|
|
22
|
-
Follow the Conventional Commits specification:
|
|
23
|
-
|
|
24
|
-
```
|
|
25
|
-
<type>(<scope>): <subject>
|
|
26
|
-
|
|
27
|
-
<body>
|
|
28
|
-
|
|
29
|
-
<footer>
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
### Types
|
|
33
|
-
- `feat`: New feature
|
|
34
|
-
- `fix`: Bug fix
|
|
35
|
-
- `docs`: Documentation changes
|
|
36
|
-
- `style`: Code style changes (formatting, etc)
|
|
37
|
-
- `refactor`: Code refactoring
|
|
38
|
-
- `perf`: Performance improvements
|
|
39
|
-
- `test`: Test changes
|
|
40
|
-
- `build`: Build system changes
|
|
41
|
-
- `ci`: CI/CD changes
|
|
42
|
-
- `chore`: Other changes
|
|
43
|
-
- `revert`: Revert previous commit
|
|
44
|
-
|
|
45
|
-
### Examples
|
|
46
|
-
```bash
|
|
47
|
-
feat: add video generation API
|
|
48
|
-
fix(transcribe): handle empty audio files
|
|
49
|
-
docs: update installation guide
|
|
50
|
-
refactor: simplify audio processing pipeline
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
## Available Scripts
|
|
54
|
-
|
|
55
|
-
```bash
|
|
56
|
-
# Run linter
|
|
57
|
-
bun run lint
|
|
58
|
-
|
|
59
|
-
# Format code
|
|
60
|
-
bun run format
|
|
61
|
-
|
|
62
|
-
# Type check
|
|
63
|
-
bun run type-check
|
|
64
|
-
|
|
65
|
-
# Check bundle size
|
|
66
|
-
bun run size
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
## Bypassing Hooks
|
|
70
|
-
|
|
71
|
-
⚠️ **Not recommended** - Only use when absolutely necessary:
|
|
72
|
-
|
|
73
|
-
```bash
|
|
74
|
-
# Skip all hooks
|
|
75
|
-
git commit --no-verify -m "emergency fix"
|
|
76
|
-
|
|
77
|
-
# Skip specific checks by setting env vars
|
|
78
|
-
HUSKY=0 git commit -m "skip all hooks"
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
## Troubleshooting
|
|
82
|
-
|
|
83
|
-
If hooks aren't running:
|
|
84
|
-
|
|
85
|
-
```bash
|
|
86
|
-
# Reinstall hooks
|
|
87
|
-
rm -rf .husky/_
|
|
88
|
-
bun run prepare
|
|
89
|
-
chmod +x .husky/pre-commit .husky/commit-msg .husky/pre-push
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
## Size Limits
|
|
93
|
-
|
|
94
|
-
Bundle size limits are defined in `.size-limit.json`. Check size before publishing:
|
|
95
|
-
|
|
96
|
-
```bash
|
|
97
|
-
bun run size
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
package/.husky/commit-msg
DELETED
package/.husky/pre-commit
DELETED