egaki 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/models.ts ADDED
@@ -0,0 +1,197 @@
1
+ // Model registry for egaki.
2
+ // Maps model IDs to their provider and generation strategy. The enum of
3
+ // supported models is derived from this registry so there's a single source
4
+ // of truth for both CLI validation and runtime provider resolution.
5
+ //
6
+ // Two generation strategies:
7
+ // - 'image': uses provider.image(modelId) + generateImage()
8
+ // - 'text': uses provider(modelId) + generateText() with responseModalities
9
+ //
10
+ // Provider resolution priority:
11
+ // 1. Provider-specific key (e.g. GOOGLE_GENERATIVE_AI_API_KEY) → direct SDK
12
+ // 2. Egaki API key (EGAKI_API_KEY) → route through egaki gateway
13
+ // 3. No key → error with subscription recommendation
14
+ //
15
+ // Model IDs are sourced from the TypeScript types exported by each @ai-sdk/*
16
+ // package. To add new models, check the *ModelId types in each package's
17
+ // dist/index.d.ts and add entries here.
18
+ import type { ImageModel, LanguageModel } from 'ai'
19
+ import pc from 'picocolors'
20
+ import { PROVIDERS, EGAKI_GATEWAY_URL } from './credentials.js'
21
+ import { CATALOG, findModel } from './model-catalog.js'
22
+ import type { ModelEntry } from './model-catalog.js'
23
+
24
+ export type { ModelEntry }
25
+
26
+ export const IMAGE_MODELS = CATALOG.map((m) => m.id) as [string, ...string[]]
27
+
28
+ export const DEFAULT_MODEL = 'nano-banana-pro-preview'
29
+
30
+ export function getModelConfig(modelId: string): ModelEntry {
31
+ const entry = findModel(modelId)
32
+ if (!entry) {
33
+ console.error(pc.red(`Unknown model: ${modelId}`))
34
+ process.exit(1)
35
+ }
36
+ return entry
37
+ }
38
+
39
+ /**
40
+ * Check if the egaki gateway API key is available (from env or stored credentials).
41
+ */
42
+ function hasEgakiKey(): boolean {
43
+ const info = PROVIDERS['egaki']
44
+ if (!info) return false
45
+ return Boolean(process.env[info.envVar])
46
+ }
47
+
48
+ /**
49
+ * Check if a direct provider key is available for the given provider.
50
+ */
51
+ function hasDirectProviderKey(providerName: string): boolean {
52
+ const info = PROVIDERS[providerName]
53
+ if (!info) return false
54
+ return Boolean(process.env[info.envVar])
55
+ }
56
+
57
+ // Check that the provider's API key is available before making API calls.
58
+ // Prints a user-friendly error with instructions on how to configure it.
59
+ // Prioritizes egaki subscription over individual provider keys.
60
+ export function ensureProviderKey(providerName: string): void {
61
+ // If direct provider key exists, we're good
62
+ if (hasDirectProviderKey(providerName)) return
63
+
64
+ // If egaki key exists, we'll route through the gateway
65
+ if (hasEgakiKey()) return
66
+
67
+ const info = PROVIDERS[providerName]
68
+
69
+ console.error('')
70
+ console.error(pc.red(pc.bold(`Missing API key for ${info?.label || providerName}`)))
71
+ console.error('')
72
+ console.error(` ${pc.bold('Recommended:')} Use Egaki subscription (all models, one key)`)
73
+ console.error('')
74
+ console.error(
75
+ ` ${pc.cyan('egaki subscribe')} get started in 30 seconds`,
76
+ )
77
+ console.error('')
78
+ console.error(` ${pc.dim('Or configure a provider key directly:')}`)
79
+ console.error('')
80
+ console.error(
81
+ ` ${pc.cyan('egaki login')} interactive setup`,
82
+ )
83
+ if (info) {
84
+ console.error(
85
+ ` ${pc.cyan(`${info.envVar}=...`)} egaki image ... inline env var`,
86
+ )
87
+ console.error(
88
+ ` ${pc.cyan(`egaki login --provider ${providerName} --key <key>`)}`,
89
+ )
90
+ console.error('')
91
+ console.error(` ${pc.dim(info.hint)}`)
92
+ }
93
+ console.error('')
94
+ process.exit(1)
95
+ }
96
+
97
+ /**
98
+ * Create a gateway-backed image model using createGateway from the AI SDK.
99
+ * The model ID is sent as provider/model format to the gateway.
100
+ */
101
+ async function createGatewayImageModel(modelId: string, provider: string): Promise<ImageModel> {
102
+ const { createGateway } = await import('ai')
103
+ const gateway = createGateway({
104
+ apiKey: process.env['EGAKI_API_KEY']!,
105
+ baseURL: EGAKI_GATEWAY_URL,
106
+ })
107
+ // Gateway expects provider/model format for routing
108
+ const gatewayModelId = `${provider}/${modelId}`
109
+ return gateway.image(gatewayModelId)
110
+ }
111
+
112
+ /**
113
+ * Create a gateway-backed text model using createGateway from the AI SDK.
114
+ */
115
+ async function createGatewayTextModel(modelId: string, provider: string): Promise<LanguageModel> {
116
+ const { createGateway } = await import('ai')
117
+ const gateway = createGateway({
118
+ apiKey: process.env['EGAKI_API_KEY']!,
119
+ baseURL: EGAKI_GATEWAY_URL,
120
+ })
121
+ const gatewayModelId = `${provider}/${modelId}`
122
+ return gateway(gatewayModelId)
123
+ }
124
+
125
+ // Lazily import the provider and create the right model instance.
126
+ // This avoids loading all provider SDKs upfront — only the one needed
127
+ // for the selected model gets imported.
128
+ //
129
+ // Priority: direct provider key > egaki gateway > error
130
+ export async function createImageModel(modelId: string): Promise<ImageModel> {
131
+ const config = getModelConfig(modelId)
132
+ ensureProviderKey(config.provider)
133
+
134
+ // If the user has a direct provider key, use the provider SDK directly
135
+ if (hasDirectProviderKey(config.provider)) {
136
+ switch (config.provider) {
137
+ case 'google': {
138
+ const { google } = await import('@ai-sdk/google')
139
+ return google.image(modelId)
140
+ }
141
+ case 'openai': {
142
+ const { openai } = await import('@ai-sdk/openai')
143
+ return openai.image(modelId)
144
+ }
145
+ case 'replicate': {
146
+ const { replicate } = await import('@ai-sdk/replicate')
147
+ return replicate.image(modelId)
148
+ }
149
+ case 'fal': {
150
+ const { fal } = await import('@ai-sdk/fal')
151
+ return fal.image(modelId)
152
+ }
153
+ }
154
+ }
155
+
156
+ // Fall back to egaki gateway
157
+ if (hasEgakiKey()) {
158
+ return createGatewayImageModel(modelId, config.provider)
159
+ }
160
+
161
+ // Should not reach here — ensureProviderKey would have exited
162
+ console.error(pc.red(`No API key available for provider: ${config.provider}`))
163
+ process.exit(1)
164
+ }
165
+
166
+ export async function createTextModel(
167
+ modelId: string,
168
+ ): Promise<LanguageModel> {
169
+ const config = getModelConfig(modelId)
170
+ ensureProviderKey(config.provider)
171
+
172
+ // Direct provider key takes priority
173
+ if (hasDirectProviderKey(config.provider)) {
174
+ switch (config.provider) {
175
+ case 'google': {
176
+ const { google } = await import('@ai-sdk/google')
177
+ return google(modelId)
178
+ }
179
+ default:
180
+ // Only Google supports generateText with responseModalities for images
181
+ console.error(
182
+ pc.red(
183
+ `Text+image generation is only supported for Google models, got provider: ${config.provider}`,
184
+ ),
185
+ )
186
+ process.exit(1)
187
+ }
188
+ }
189
+
190
+ // Fall back to egaki gateway
191
+ if (hasEgakiKey()) {
192
+ return createGatewayTextModel(modelId, config.provider)
193
+ }
194
+
195
+ console.error(pc.red(`No API key available for provider: ${config.provider}`))
196
+ process.exit(1)
197
+ }
@@ -0,0 +1,271 @@
1
+ // Subscription management for egaki.
2
+ // Handles the subscribe, unsubscribe, and usage flows.
3
+ // Communicates with the egaki gateway API at egaki.org.
4
+
5
+ import pc from 'picocolors'
6
+ import {
7
+ intro,
8
+ outro,
9
+ text,
10
+ select,
11
+ confirm,
12
+ isCancel,
13
+ cancel,
14
+ log,
15
+ note,
16
+ spinner,
17
+ } from '@clack/prompts'
18
+ import { EGAKI_GATEWAY_URL, saveProviderKey, getProviderKey } from './credentials.js'
19
+
20
+ // Strip /v1/ai suffix to get the base URL for non-proxy endpoints (/buy, /api/*)
21
+ const GATEWAY_BASE = EGAKI_GATEWAY_URL.replace(/\/v1\/ai$/, '')
22
+
23
+ type PlanInfo = {
24
+ id: string
25
+ name: string
26
+ price: number
27
+ spendingCap: number
28
+ markup: number
29
+ priceFormatted: string
30
+ }
31
+
32
+ type UsageInfo = {
33
+ plan: string
34
+ planName: string
35
+ dollarsUsed: number
36
+ spendingCap: number
37
+ dollarsRemaining: number
38
+ periodStart: number
39
+ status: string
40
+ }
41
+
42
+ type StatusInfo = {
43
+ status: string
44
+ plan: string
45
+ planName: string
46
+ email?: string
47
+ dollarsUsed: number
48
+ spendingCap: number
49
+ createdAt: number
50
+ }
51
+
52
+ // ── Subscribe ─────────────────────────────────────────────────────────────
53
+
54
+ export async function subscribeInteractive(): Promise<void> {
55
+ intro(pc.bold('egaki subscribe'))
56
+
57
+ // Fetch available plans
58
+ const s = spinner()
59
+ s.start('Fetching plans...')
60
+
61
+ let plans: PlanInfo[]
62
+ try {
63
+ const res = await fetch(`${GATEWAY_BASE}/api/plans`)
64
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
65
+ const data = (await res.json()) as { plans: PlanInfo[] }
66
+ plans = data.plans
67
+ s.stop('Plans loaded')
68
+ } catch {
69
+ s.stop('Failed to fetch plans')
70
+ log.error('Could not connect to egaki.org. Check your internet connection.')
71
+ process.exit(1)
72
+ }
73
+
74
+ // Select plan
75
+ const planId = await select({
76
+ message: 'Choose a plan',
77
+ options: plans.map((p) => ({
78
+ value: p.id,
79
+ label: `${p.name} — ${p.priceFormatted}`,
80
+ hint: `$${p.spendingCap} spending cap, all models`,
81
+ })),
82
+ })
83
+
84
+ if (isCancel(planId)) {
85
+ cancel('Subscribe cancelled.')
86
+ process.exit(0)
87
+ }
88
+
89
+ // Get email
90
+ const email = await text({
91
+ message: 'Email for the subscription',
92
+ placeholder: 'you@example.com',
93
+ validate: (value) => {
94
+ if (!value || !value.includes('@')) {
95
+ return 'Please enter a valid email'
96
+ }
97
+ },
98
+ })
99
+
100
+ if (isCancel(email)) {
101
+ cancel('Subscribe cancelled.')
102
+ process.exit(0)
103
+ }
104
+
105
+ // Show checkout URL
106
+ const checkoutUrl = `${GATEWAY_BASE}/buy?plan=${planId}&email=${encodeURIComponent(email)}`
107
+
108
+ log.info('')
109
+ note(
110
+ [
111
+ `Open this URL to complete payment:`,
112
+ '',
113
+ pc.cyan(checkoutUrl),
114
+ '',
115
+ `After payment, you'll receive your API key via email.`,
116
+ `Then run:`,
117
+ '',
118
+ ` ${pc.cyan('egaki login --provider egaki --key egaki_...')}`,
119
+ ].join('\n'),
120
+ 'Checkout',
121
+ )
122
+
123
+ // Ask if they want to enter the key now
124
+ const hasKey = await confirm({
125
+ message: 'Do you already have your API key?',
126
+ })
127
+
128
+ if (isCancel(hasKey)) {
129
+ outro('You can enter your key later with: egaki login --provider egaki --key <key>')
130
+ return
131
+ }
132
+
133
+ if (hasKey) {
134
+ const key = await text({
135
+ message: 'Paste your egaki API key',
136
+ placeholder: 'egaki_...',
137
+ validate: (value) => {
138
+ if (!value || !value.startsWith('egaki_')) {
139
+ return 'API key should start with egaki_'
140
+ }
141
+ },
142
+ })
143
+
144
+ if (isCancel(key)) {
145
+ outro('You can enter your key later with: egaki login --provider egaki --key <key>')
146
+ return
147
+ }
148
+
149
+ saveProviderKey('egaki', key.trim())
150
+ log.success('API key saved')
151
+ outro('You can now generate images with any model: egaki image "your prompt"')
152
+ } else {
153
+ outro('After payment, run: egaki login --provider egaki --key <your-key>')
154
+ }
155
+ }
156
+
157
+ export function subscribeNonInteractive(email?: string, plan?: string): void {
158
+ const emailParam = email ? `&email=${encodeURIComponent(email)}` : ''
159
+
160
+ if (plan) {
161
+ // Specific plan requested
162
+ const checkoutUrl = `${GATEWAY_BASE}/buy?plan=${plan}${emailParam}`
163
+ console.log(pc.bold('Egaki Subscription'))
164
+ console.log('')
165
+ console.log('Open this URL to complete payment:')
166
+ console.log('')
167
+ console.log(pc.cyan(checkoutUrl))
168
+ } else {
169
+ // No plan specified — show all options
170
+ console.log(pc.bold('Egaki Subscription'))
171
+ console.log('')
172
+ console.log('Choose a plan:')
173
+ console.log('')
174
+ console.log(` ${pc.bold('Plus')} $29/mo ${pc.cyan(`${GATEWAY_BASE}/buy?plan=plus${emailParam}`)}`)
175
+ console.log(` ${pc.bold('Pro')} $99/mo ${pc.cyan(`${GATEWAY_BASE}/buy?plan=pro${emailParam}`)}`)
176
+ }
177
+
178
+ console.log('')
179
+ console.log('After payment, you\'ll receive your API key via email.')
180
+ console.log(`Then run: ${pc.cyan('egaki login --provider egaki --key egaki_...')}`)
181
+ console.log('')
182
+ console.log('Tip: egaki supports both BYOK and subscription mode.')
183
+ console.log('BYOK = add provider keys (google/openai/replicate/fal) with egaki login.')
184
+ console.log('Subscription = one egaki key to access all models without per-provider key setup.')
185
+ }
186
+
187
+ // ── Unsubscribe ───────────────────────────────────────────────────────────
188
+
189
+ export async function unsubscribe(): Promise<void> {
190
+ const apiKey = getProviderKey('egaki') || process.env['EGAKI_API_KEY']
191
+
192
+ if (!apiKey) {
193
+ console.error(pc.red('No egaki API key found.'))
194
+ console.error(
195
+ `If you have a key, set it first: ${pc.cyan('egaki login --provider egaki --key <key>')}`,
196
+ )
197
+ process.exit(1)
198
+ }
199
+
200
+ console.error(pc.dim('Canceling subscription...'))
201
+
202
+ try {
203
+ const res = await fetch(`${GATEWAY_BASE}/api/cancel`, {
204
+ method: 'POST',
205
+ headers: { Authorization: `Bearer ${apiKey}` },
206
+ })
207
+
208
+ const data = (await res.json()) as { success?: boolean; message?: string; error?: string }
209
+
210
+ if (!res.ok) {
211
+ console.error(pc.red(data.error || 'Failed to cancel subscription'))
212
+ process.exit(1)
213
+ }
214
+
215
+ console.log(pc.green(data.message || 'Subscription canceled.'))
216
+ console.log(pc.dim('You can resubscribe anytime with: egaki subscribe'))
217
+ } catch {
218
+ console.error(pc.red('Could not connect to egaki.org. Check your internet connection.'))
219
+ process.exit(1)
220
+ }
221
+ }
222
+
223
+ // ── Usage ─────────────────────────────────────────────────────────────────
224
+
225
+ export async function showUsage(): Promise<void> {
226
+ const apiKey = getProviderKey('egaki') || process.env['EGAKI_API_KEY']
227
+
228
+ if (!apiKey) {
229
+ console.error(pc.red('No egaki API key found.'))
230
+ console.error(
231
+ `Set your key first: ${pc.cyan('egaki login --provider egaki --key <key>')}`,
232
+ )
233
+ console.error(
234
+ `Or subscribe: ${pc.cyan('egaki subscribe')}`,
235
+ )
236
+ process.exit(1)
237
+ }
238
+
239
+ try {
240
+ const res = await fetch(`${GATEWAY_BASE}/api/usage`, {
241
+ headers: { Authorization: `Bearer ${apiKey}` },
242
+ })
243
+
244
+ if (!res.ok) {
245
+ const data = (await res.json()) as { error?: string }
246
+ console.error(pc.red(data.error || 'Failed to fetch usage'))
247
+ process.exit(1)
248
+ }
249
+
250
+ const usage = (await res.json()) as UsageInfo
251
+ const pct = usage.spendingCap > 0
252
+ ? Math.round((usage.dollarsUsed / usage.spendingCap) * 100)
253
+ : 0
254
+ const remaining = Math.max(0, usage.spendingCap - usage.dollarsUsed)
255
+
256
+ console.log(pc.bold(`Egaki ${usage.planName} Plan\n`))
257
+ console.log(` Status: ${usage.status === 'active' ? pc.green('active') : pc.red(usage.status)}`)
258
+ console.log(` Spent: $${usage.dollarsUsed.toFixed(2)} / $${usage.spendingCap.toFixed(2)} (${pct}%)`)
259
+ console.log(` Remaining: $${remaining.toFixed(2)}`)
260
+ console.log(` Period: started ${new Date(usage.periodStart).toLocaleDateString()}`)
261
+
262
+ if (pct >= 90) {
263
+ console.log('')
264
+ console.log(pc.yellow(' Running low on budget! Consider upgrading your plan.'))
265
+ console.log(pc.dim(` Visit: https://egaki.org/buy`))
266
+ }
267
+ } catch {
268
+ console.error(pc.red('Could not connect to egaki.org. Check your internet connection.'))
269
+ process.exit(1)
270
+ }
271
+ }
package/index.js DELETED
@@ -1 +0,0 @@
1
- // placeholder