egaki 0.5.0 → 0.7.0

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.
Files changed (280) hide show
  1. package/CHANGELOG.md +188 -0
  2. package/dist/cli/cache-utils.d.ts +33 -0
  3. package/dist/cli/cache-utils.d.ts.map +1 -0
  4. package/dist/cli/cache-utils.js +157 -0
  5. package/dist/cli/cached-generate.d.ts +105 -0
  6. package/dist/cli/cached-generate.d.ts.map +1 -0
  7. package/dist/cli/cached-generate.js +208 -0
  8. package/dist/cli/cached-generate.test.d.ts +11 -0
  9. package/dist/cli/cached-generate.test.d.ts.map +1 -0
  10. package/dist/cli/cached-generate.test.js +431 -0
  11. package/dist/cli/cartesia-provider.d.ts +16 -0
  12. package/dist/cli/cartesia-provider.d.ts.map +1 -0
  13. package/dist/cli/cartesia-provider.js +184 -0
  14. package/dist/cli/chatgpt-auth.d.ts.map +1 -0
  15. package/dist/cli/cli.d.ts +4 -0
  16. package/dist/cli/cli.d.ts.map +1 -0
  17. package/dist/cli/cli.js +1329 -0
  18. package/dist/cli/clone-generate.d.ts +26 -0
  19. package/dist/cli/clone-generate.d.ts.map +1 -0
  20. package/dist/cli/clone-generate.js +148 -0
  21. package/dist/{credentials.d.ts → cli/credentials.d.ts} +11 -2
  22. package/dist/cli/credentials.d.ts.map +1 -0
  23. package/dist/{credentials.js → cli/credentials.js} +88 -5
  24. package/dist/cli/elevenlabs-speech-provider.d.ts +26 -0
  25. package/dist/cli/elevenlabs-speech-provider.d.ts.map +1 -0
  26. package/dist/cli/elevenlabs-speech-provider.js +199 -0
  27. package/dist/cli/generate.d.ts +140 -0
  28. package/dist/cli/generate.d.ts.map +1 -0
  29. package/dist/cli/generate.js +604 -0
  30. package/dist/cli/generate.test.d.ts +2 -0
  31. package/dist/cli/generate.test.d.ts.map +1 -0
  32. package/dist/cli/generate.test.js +131 -0
  33. package/dist/cli/login.d.ts.map +1 -0
  34. package/dist/{login.js → cli/login.js} +45 -4
  35. package/dist/cli/main.d.ts +3 -0
  36. package/dist/cli/main.d.ts.map +1 -0
  37. package/dist/cli/main.js +7 -0
  38. package/dist/cli/model-catalog.d.ts +141 -0
  39. package/dist/cli/model-catalog.d.ts.map +1 -0
  40. package/dist/cli/model-catalog.js +2135 -0
  41. package/dist/{models.d.ts → cli/models.d.ts} +16 -9
  42. package/dist/cli/models.d.ts.map +1 -0
  43. package/dist/cli/models.js +313 -0
  44. package/dist/cli/open-browser.d.ts.map +1 -0
  45. package/dist/cli/openai-speech-provider.d.ts +3 -0
  46. package/dist/cli/openai-speech-provider.d.ts.map +1 -0
  47. package/dist/cli/openai-speech-provider.js +28 -0
  48. package/dist/cli/separation-generate.d.ts +34 -0
  49. package/dist/cli/separation-generate.d.ts.map +1 -0
  50. package/dist/cli/separation-generate.js +117 -0
  51. package/dist/cli/speech-catalog.d.ts +35 -0
  52. package/dist/cli/speech-catalog.d.ts.map +1 -0
  53. package/dist/cli/speech-catalog.js +195 -0
  54. package/dist/cli/speech-generate.d.ts +86 -0
  55. package/dist/cli/speech-generate.d.ts.map +1 -0
  56. package/dist/cli/speech-generate.js +156 -0
  57. package/dist/cli/speech-models.d.ts +8 -0
  58. package/dist/cli/speech-models.d.ts.map +1 -0
  59. package/dist/cli/speech-models.js +109 -0
  60. package/dist/cli/speech-provider.test.d.ts +2 -0
  61. package/dist/cli/speech-provider.test.d.ts.map +1 -0
  62. package/dist/cli/speech-provider.test.js +197 -0
  63. package/dist/cli/subscription.d.ts.map +1 -0
  64. package/dist/cli/transcription-catalog.d.ts +35 -0
  65. package/dist/cli/transcription-catalog.d.ts.map +1 -0
  66. package/dist/cli/transcription-catalog.js +192 -0
  67. package/dist/cli/transcription-generate.d.ts +62 -0
  68. package/dist/cli/transcription-generate.d.ts.map +1 -0
  69. package/dist/cli/transcription-generate.js +144 -0
  70. package/dist/cli/transcription-models.d.ts +9 -0
  71. package/dist/cli/transcription-models.d.ts.map +1 -0
  72. package/dist/cli/transcription-models.js +100 -0
  73. package/dist/cli/transcription-providers.d.ts +22 -0
  74. package/dist/cli/transcription-providers.d.ts.map +1 -0
  75. package/dist/cli/transcription-providers.js +290 -0
  76. package/dist/cli/video-model-catalog.d.ts +2 -0
  77. package/dist/cli/video-model-catalog.d.ts.map +1 -0
  78. package/dist/cli/video-model-catalog.js +5 -0
  79. package/dist/cli/xai-auth.d.ts +29 -0
  80. package/dist/cli/xai-auth.d.ts.map +1 -0
  81. package/dist/cli/xai-auth.js +325 -0
  82. package/dist/vite/angled-screen.d.ts +57 -0
  83. package/dist/vite/angled-screen.d.ts.map +1 -0
  84. package/dist/vite/angled-screen.js +115 -0
  85. package/dist/vite/app.d.ts +51 -0
  86. package/dist/vite/app.d.ts.map +1 -0
  87. package/dist/vite/app.js +150 -0
  88. package/dist/vite/bands-shader.d.ts +47 -0
  89. package/dist/vite/bands-shader.d.ts.map +1 -0
  90. package/dist/vite/bands-shader.js +267 -0
  91. package/dist/vite/code-block.d.ts +107 -0
  92. package/dist/vite/code-block.d.ts.map +1 -0
  93. package/dist/vite/code-block.js +641 -0
  94. package/dist/vite/components.d.ts +36 -0
  95. package/dist/vite/components.d.ts.map +1 -0
  96. package/dist/vite/components.js +113 -0
  97. package/dist/vite/dispersion-rings-shader.d.ts +44 -0
  98. package/dist/vite/dispersion-rings-shader.d.ts.map +1 -0
  99. package/dist/vite/dispersion-rings-shader.js +282 -0
  100. package/dist/vite/easing-curves.d.ts +129 -0
  101. package/dist/vite/easing-curves.d.ts.map +1 -0
  102. package/dist/vite/easing-curves.js +584 -0
  103. package/dist/vite/easing-curves.test.d.ts +2 -0
  104. package/dist/vite/easing-curves.test.d.ts.map +1 -0
  105. package/dist/vite/easing-curves.test.js +200 -0
  106. package/dist/vite/generated-media-client.d.ts +35 -0
  107. package/dist/vite/generated-media-client.d.ts.map +1 -0
  108. package/dist/vite/generated-media-client.js +76 -0
  109. package/dist/vite/keyframes.d.ts +129 -0
  110. package/dist/vite/keyframes.d.ts.map +1 -0
  111. package/dist/vite/keyframes.js +194 -0
  112. package/dist/vite/layout-editor.d.ts +45 -0
  113. package/dist/vite/layout-editor.d.ts.map +1 -0
  114. package/dist/vite/layout-editor.js +587 -0
  115. package/dist/vite/layout-transition.d.ts +136 -0
  116. package/dist/vite/layout-transition.d.ts.map +1 -0
  117. package/dist/vite/layout-transition.js +581 -0
  118. package/dist/vite/liquid-gradient-shader.d.ts +62 -0
  119. package/dist/vite/liquid-gradient-shader.d.ts.map +1 -0
  120. package/dist/vite/liquid-gradient-shader.js +413 -0
  121. package/dist/vite/mdx-client.d.ts +15 -0
  122. package/dist/vite/mdx-client.d.ts.map +1 -0
  123. package/dist/vite/mdx-client.js +265 -0
  124. package/dist/vite/mdx-parse.d.ts +102 -0
  125. package/dist/vite/mdx-parse.d.ts.map +1 -0
  126. package/dist/vite/mdx-parse.js +251 -0
  127. package/dist/vite/mdx-provided-components.d.ts +18 -0
  128. package/dist/vite/mdx-provided-components.d.ts.map +1 -0
  129. package/dist/vite/mdx-provided-components.js +14 -0
  130. package/dist/vite/mdx-video.d.ts +327 -0
  131. package/dist/vite/mdx-video.d.ts.map +1 -0
  132. package/dist/vite/mdx-video.js +596 -0
  133. package/dist/vite/mdx-video.test.d.ts +9 -0
  134. package/dist/vite/mdx-video.test.d.ts.map +1 -0
  135. package/dist/vite/mdx-video.test.js +1945 -0
  136. package/dist/vite/media-components.d.ts +45 -0
  137. package/dist/vite/media-components.d.ts.map +1 -0
  138. package/dist/vite/media-components.js +355 -0
  139. package/dist/vite/media-duration-store.d.ts +66 -0
  140. package/dist/vite/media-duration-store.d.ts.map +1 -0
  141. package/dist/vite/media-duration-store.js +204 -0
  142. package/dist/vite/player-page.d.ts +35 -0
  143. package/dist/vite/player-page.d.ts.map +1 -0
  144. package/dist/vite/player-page.js +720 -0
  145. package/dist/vite/render-client.d.ts +18 -0
  146. package/dist/vite/render-client.d.ts.map +1 -0
  147. package/dist/vite/render-client.js +67 -0
  148. package/dist/vite/sdk.d.ts +221 -0
  149. package/dist/vite/sdk.d.ts.map +1 -0
  150. package/dist/vite/sdk.js +355 -0
  151. package/dist/vite/server-components.d.ts +55 -0
  152. package/dist/vite/server-components.d.ts.map +1 -0
  153. package/dist/vite/server-components.js +156 -0
  154. package/dist/vite/server-mdx.d.ts +74 -0
  155. package/dist/vite/server-mdx.d.ts.map +1 -0
  156. package/dist/vite/server-mdx.js +259 -0
  157. package/dist/vite/shader-renderer.d.ts +48 -0
  158. package/dist/vite/shader-renderer.d.ts.map +1 -0
  159. package/dist/vite/shader-renderer.js +411 -0
  160. package/dist/vite/store-hooks.d.ts +12 -0
  161. package/dist/vite/store-hooks.d.ts.map +1 -0
  162. package/dist/vite/store-hooks.js +93 -0
  163. package/dist/vite/store.d.ts +75 -0
  164. package/dist/vite/store.d.ts.map +1 -0
  165. package/dist/vite/store.js +52 -0
  166. package/dist/vite/store.test.d.ts +6 -0
  167. package/dist/vite/store.test.d.ts.map +1 -0
  168. package/dist/vite/store.test.js +172 -0
  169. package/dist/vite/tweakpane-hook.d.ts +94 -0
  170. package/dist/vite/tweakpane-hook.d.ts.map +1 -0
  171. package/dist/vite/tweakpane-hook.js +450 -0
  172. package/dist/vite/vite-plugin.d.ts +28 -0
  173. package/dist/vite/vite-plugin.d.ts.map +1 -0
  174. package/dist/vite/vite-plugin.js +538 -0
  175. package/dist/vite/wave-gradient-shader.d.ts +41 -0
  176. package/dist/vite/wave-gradient-shader.d.ts.map +1 -0
  177. package/dist/vite/wave-gradient-shader.js +207 -0
  178. package/package.json +103 -25
  179. package/skills/egaki/SKILL.md +162 -1
  180. package/src/cli/cache-utils.ts +169 -0
  181. package/src/cli/cached-generate.test.ts +531 -0
  182. package/src/cli/cached-generate.ts +348 -0
  183. package/src/cli/cartesia-provider.ts +249 -0
  184. package/src/cli/cli.ts +1818 -0
  185. package/src/cli/clone-generate.ts +202 -0
  186. package/src/{credentials.ts → cli/credentials.ts} +90 -7
  187. package/src/cli/elevenlabs-speech-provider.ts +243 -0
  188. package/src/cli/generate.test.ts +166 -0
  189. package/src/cli/generate.ts +912 -0
  190. package/src/{login.ts → cli/login.ts} +48 -3
  191. package/src/cli/main.ts +8 -0
  192. package/src/cli/model-catalog.ts +2336 -0
  193. package/src/cli/models.ts +408 -0
  194. package/src/cli/openai-speech-provider.ts +34 -0
  195. package/src/cli/separation-generate.ts +187 -0
  196. package/src/cli/speech-catalog.ts +253 -0
  197. package/src/cli/speech-generate.ts +275 -0
  198. package/src/cli/speech-models.ts +146 -0
  199. package/src/cli/speech-provider.test.ts +226 -0
  200. package/src/cli/transcription-catalog.ts +250 -0
  201. package/src/cli/transcription-generate.ts +214 -0
  202. package/src/cli/transcription-models.ts +136 -0
  203. package/src/cli/transcription-providers.ts +427 -0
  204. package/src/cli/video-model-catalog.ts +14 -0
  205. package/src/cli/xai-auth.ts +396 -0
  206. package/src/vite/angled-screen.tsx +214 -0
  207. package/src/vite/app.tsx +197 -0
  208. package/src/vite/bands-shader.tsx +319 -0
  209. package/src/vite/code-block.tsx +939 -0
  210. package/src/vite/components.tsx +278 -0
  211. package/src/vite/dispersion-rings-shader.tsx +331 -0
  212. package/src/vite/easing-curves.test.ts +232 -0
  213. package/src/vite/easing-curves.ts +743 -0
  214. package/src/vite/generated-media-client.tsx +121 -0
  215. package/src/vite/keyframes.tsx +347 -0
  216. package/src/vite/layout-editor.tsx +709 -0
  217. package/src/vite/layout-transition.tsx +706 -0
  218. package/src/vite/liquid-gradient-shader.tsx +474 -0
  219. package/src/vite/mdx-client.tsx +420 -0
  220. package/src/vite/mdx-parse.ts +343 -0
  221. package/src/vite/mdx-provided-components.ts +19 -0
  222. package/src/vite/mdx-video.test.tsx +2219 -0
  223. package/src/vite/mdx-video.tsx +976 -0
  224. package/src/vite/media-components.tsx +473 -0
  225. package/src/vite/media-duration-store.ts +225 -0
  226. package/src/vite/motion-timing.ts +194 -0
  227. package/src/vite/player-page.tsx +1107 -0
  228. package/src/vite/render-client.ts +84 -0
  229. package/src/vite/sdk.ts +598 -0
  230. package/src/vite/server-components.tsx +241 -0
  231. package/src/vite/server-mdx.ts +285 -0
  232. package/src/vite/shader-renderer.tsx +535 -0
  233. package/src/vite/store-hooks.ts +117 -0
  234. package/src/vite/store.test.ts +202 -0
  235. package/src/vite/store.ts +115 -0
  236. package/src/vite/styles.css +21 -0
  237. package/src/vite/tweakpane-hook.tsx +593 -0
  238. package/src/vite/virtual-modules.d.ts +30 -0
  239. package/src/vite/vite-plugin.ts +584 -0
  240. package/src/vite/wave-gradient-shader.tsx +253 -0
  241. package/README.md +0 -266
  242. package/dist/antigravity-auth.d.ts +0 -15
  243. package/dist/antigravity-auth.d.ts.map +0 -1
  244. package/dist/antigravity-auth.js +0 -544
  245. package/dist/antigravity-image.d.ts +0 -11
  246. package/dist/antigravity-image.d.ts.map +0 -1
  247. package/dist/antigravity-image.js +0 -285
  248. package/dist/chatgpt-auth.d.ts.map +0 -1
  249. package/dist/cli.d.ts +0 -3
  250. package/dist/cli.d.ts.map +0 -1
  251. package/dist/cli.js +0 -968
  252. package/dist/credentials.d.ts.map +0 -1
  253. package/dist/gateway-model-candidates.generated.d.ts +0 -11
  254. package/dist/gateway-model-candidates.generated.d.ts.map +0 -1
  255. package/dist/gateway-model-candidates.generated.js +0 -64
  256. package/dist/login.d.ts.map +0 -1
  257. package/dist/model-catalog.d.ts +0 -42
  258. package/dist/model-catalog.d.ts.map +0 -1
  259. package/dist/model-catalog.js +0 -1001
  260. package/dist/models.d.ts.map +0 -1
  261. package/dist/models.js +0 -246
  262. package/dist/open-browser.d.ts.map +0 -1
  263. package/dist/subscription.d.ts.map +0 -1
  264. package/dist/video-model-catalog.d.ts +0 -52
  265. package/dist/video-model-catalog.d.ts.map +0 -1
  266. package/dist/video-model-catalog.js +0 -306
  267. package/src/cli.ts +0 -1378
  268. package/src/model-catalog.ts +0 -1077
  269. package/src/models.ts +0 -309
  270. package/src/video-model-catalog.ts +0 -363
  271. /package/dist/{chatgpt-auth.d.ts → cli/chatgpt-auth.d.ts} +0 -0
  272. /package/dist/{chatgpt-auth.js → cli/chatgpt-auth.js} +0 -0
  273. /package/dist/{login.d.ts → cli/login.d.ts} +0 -0
  274. /package/dist/{open-browser.d.ts → cli/open-browser.d.ts} +0 -0
  275. /package/dist/{open-browser.js → cli/open-browser.js} +0 -0
  276. /package/dist/{subscription.d.ts → cli/subscription.d.ts} +0 -0
  277. /package/dist/{subscription.js → cli/subscription.js} +0 -0
  278. /package/src/{chatgpt-auth.ts → cli/chatgpt-auth.ts} +0 -0
  279. /package/src/{open-browser.ts → cli/open-browser.ts} +0 -0
  280. /package/src/{subscription.ts → cli/subscription.ts} +0 -0
@@ -0,0 +1,325 @@
1
+ // xAI OAuth authentication for egaki.
2
+ // Implements the same OAuth 2.0 PKCE browser flow used by xAI's Grok CLI
3
+ // and the OpenCode xAI plugin. Tokens grant access to the full xAI API
4
+ // (api.x.ai/v1) including image and video generation endpoints.
5
+ //
6
+ // Two auth methods:
7
+ // 1. Browser flow: loopback server on 127.0.0.1:56121 (pinned to match Grok-CLI registration)
8
+ // 2. Device code flow (RFC 8628): headless/VPS environments, no loopback needed
9
+ //
10
+ // Tokens are stored in credentials.json under the 'xai-oauth' key as a structured
11
+ // XaiAuth object. The access token is injected as a bearer token via custom fetch
12
+ // when creating the @ai-sdk/xai provider instance.
13
+ import { createServer } from 'node:http';
14
+ import { spinner, log, note } from '@clack/prompts';
15
+ import pc from 'picocolors';
16
+ import { z } from 'zod';
17
+ import { openUrlInBrowser } from './open-browser.js';
18
+ import pkg from '../../package.json' with { type: 'json' };
19
+ // Public Grok-CLI OAuth client. xAI's auth server rejects loopback OAuth from
20
+ // non-allowlisted clients, so we reuse the Grok-CLI client_id that xAI ships
21
+ // for desktop OAuth flows.
22
+ const CLIENT_ID = 'b1a00492-073a-47ea-816f-4c329264a828';
23
+ const AUTHORIZE_URL = 'https://auth.x.ai/oauth2/authorize';
24
+ const TOKEN_URL = 'https://auth.x.ai/oauth2/token';
25
+ const DEVICE_AUTHORIZATION_URL = 'https://auth.x.ai/oauth2/device/code';
26
+ const SCOPE = 'openid profile email offline_access grok-cli:access api:access';
27
+ // The loopback server must bind to this exact host:port pair — it's registered
28
+ // with xAI for the Grok-CLI client.
29
+ const OAUTH_HOST = '127.0.0.1';
30
+ const OAUTH_PORT = 56121;
31
+ const OAUTH_REDIRECT_PATH = '/callback';
32
+ const REDIRECT_URI = `http://${OAUTH_HOST}:${OAUTH_PORT}${OAUTH_REDIRECT_PATH}`;
33
+ const ACCESS_TOKEN_REFRESH_SKEW_MS = 120_000;
34
+ const tokenResponseSchema = z.object({
35
+ access_token: z.string(),
36
+ refresh_token: z.string().optional(),
37
+ id_token: z.string().optional(),
38
+ expires_in: z.number().optional(),
39
+ });
40
+ // ─── PKCE helpers ────────────────────────────────────────────────────────────
41
+ function randomString(length) {
42
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
43
+ const bytes = crypto.getRandomValues(new Uint8Array(length));
44
+ return Array.from(bytes)
45
+ .map((b) => chars[b % chars.length])
46
+ .join('');
47
+ }
48
+ function base64UrlEncode(buffer) {
49
+ const bytes = new Uint8Array(buffer);
50
+ const binary = String.fromCharCode(...bytes);
51
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
52
+ }
53
+ async function generatePKCE() {
54
+ const verifier = randomString(64);
55
+ const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
56
+ return { verifier, challenge: base64UrlEncode(hash) };
57
+ }
58
+ function generateState() {
59
+ return base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)).buffer);
60
+ }
61
+ function authHeaders() {
62
+ return {
63
+ 'Content-Type': 'application/x-www-form-urlencoded',
64
+ Accept: 'application/json',
65
+ 'User-Agent': `egaki/${pkg.version}`,
66
+ };
67
+ }
68
+ // ─── JWT helpers ─────────────────────────────────────────────────────────────
69
+ function parseJwtClaims(token) {
70
+ const parts = token.split('.');
71
+ if (parts.length < 2)
72
+ return undefined;
73
+ let payload = parts[1].replace(/-/g, '+').replace(/_/g, '/');
74
+ while (payload.length % 4 !== 0)
75
+ payload += '=';
76
+ try {
77
+ return JSON.parse(Buffer.from(payload, 'base64').toString('utf8'));
78
+ }
79
+ catch {
80
+ return undefined;
81
+ }
82
+ }
83
+ function extractEmailFromToken(idToken) {
84
+ if (!idToken)
85
+ return undefined;
86
+ const claims = parseJwtClaims(idToken);
87
+ return typeof claims?.email === 'string' ? claims.email : undefined;
88
+ }
89
+ /** Returns true if the access token's exp claim is within the skew window. */
90
+ export function accessTokenIsExpiring(token, skewMs = ACCESS_TOKEN_REFRESH_SKEW_MS) {
91
+ if (!token)
92
+ return false;
93
+ const claims = parseJwtClaims(token);
94
+ if (typeof claims?.exp !== 'number')
95
+ return false;
96
+ return claims.exp * 1000 <= Date.now() + Math.max(0, skewMs);
97
+ }
98
+ // ─── token exchange ──────────────────────────────────────────────────────────
99
+ async function exchangeCodeForTokens(code, verifier) {
100
+ const response = await fetch(TOKEN_URL, {
101
+ method: 'POST',
102
+ headers: authHeaders(),
103
+ body: new URLSearchParams({
104
+ grant_type: 'authorization_code',
105
+ code,
106
+ redirect_uri: REDIRECT_URI,
107
+ client_id: CLIENT_ID,
108
+ code_verifier: verifier,
109
+ }).toString(),
110
+ });
111
+ if (!response.ok) {
112
+ const detail = await response.text().catch(() => '');
113
+ throw new Error(`xAI token exchange failed (${response.status})${detail ? `: ${detail}` : ''}`);
114
+ }
115
+ const tokens = tokenResponseSchema.parse(await response.json());
116
+ const refreshToken = tokens.refresh_token;
117
+ if (!refreshToken)
118
+ throw new Error('xAI token response was missing refresh_token');
119
+ return { ...tokens, refresh_token: refreshToken };
120
+ }
121
+ export async function refreshXaiToken(refreshToken) {
122
+ const response = await fetch(TOKEN_URL, {
123
+ method: 'POST',
124
+ headers: authHeaders(),
125
+ body: new URLSearchParams({
126
+ grant_type: 'refresh_token',
127
+ refresh_token: refreshToken,
128
+ client_id: CLIENT_ID,
129
+ }).toString(),
130
+ });
131
+ if (!response.ok) {
132
+ const detail = await response.text().catch(() => '');
133
+ throw new Error(`xAI token refresh failed (${response.status})${detail ? `: ${detail}` : ''}`);
134
+ }
135
+ return tokenResponseSchema.parse(await response.json());
136
+ }
137
+ // ─── loopback server + browser flow ──────────────────────────────────────────
138
+ const HTML_SUCCESS = `<!doctype html>
139
+ <html><head><title>egaki - xAI Authorization Successful</title>
140
+ <style>body{font-family:system-ui,-apple-system,sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#131010;color:#f1ecec}.container{text-align:center;padding:2rem}h1{margin-bottom:1rem}p{color:#b7b1b1}</style>
141
+ </head><body><div class="container"><h1>Authorization Successful</h1><p>You can close this window and return to egaki.</p></div>
142
+ <script>setTimeout(()=>window.close(),2000)</script></body></html>`;
143
+ function htmlError(error) {
144
+ const escaped = error.replace(/[&<>"']/g, (c) => {
145
+ const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
146
+ return map[c] || c;
147
+ });
148
+ return `<!doctype html>
149
+ <html><head><title>egaki - xAI Authorization Failed</title>
150
+ <style>body{font-family:system-ui,-apple-system,sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#131010;color:#f1ecec}.container{text-align:center;padding:2rem}h1{color:#fc533a;margin-bottom:1rem}p{color:#b7b1b1}.error{color:#ff917b;font-family:monospace;margin-top:1rem;padding:1rem;background:#3c140d;border-radius:.5rem}</style>
151
+ </head><body><div class="container"><h1>Authorization Failed</h1><p>An error occurred during authorization.</p><div class="error">${escaped}</div></div></body></html>`;
152
+ }
153
+ let pendingOAuth;
154
+ function startOAuthServer() {
155
+ return new Promise((resolve, reject) => {
156
+ const server = createServer((req, res) => {
157
+ const url = new URL(req.url || '/', `http://${OAUTH_HOST}:${OAUTH_PORT}`);
158
+ if (url.pathname !== OAUTH_REDIRECT_PATH) {
159
+ res.writeHead(404);
160
+ res.end('Not found');
161
+ return;
162
+ }
163
+ const code = url.searchParams.get('code');
164
+ const state = url.searchParams.get('state');
165
+ const error = url.searchParams.get('error');
166
+ const errorDescription = url.searchParams.get('error_description');
167
+ if (error) {
168
+ const msg = errorDescription || error;
169
+ pendingOAuth?.reject(new Error(msg));
170
+ pendingOAuth = undefined;
171
+ res.writeHead(200, { 'Content-Type': 'text/html' });
172
+ res.end(htmlError(msg));
173
+ return;
174
+ }
175
+ if (!code) {
176
+ // Missing code but no explicit error. Don't destroy pendingOAuth;
177
+ // could be a stray browser request, not the real callback.
178
+ res.writeHead(400, { 'Content-Type': 'text/html' });
179
+ res.end(htmlError('Missing authorization code'));
180
+ return;
181
+ }
182
+ if (!pendingOAuth || state !== pendingOAuth.state) {
183
+ // Bad state: respond 400 but keep pendingOAuth intact so the real
184
+ // callback can still succeed. Clearing it here would let a random
185
+ // local request to :56121/callback?code=x&state=bad kill the
186
+ // legitimate login attempt (local DoS).
187
+ res.writeHead(400, { 'Content-Type': 'text/html' });
188
+ res.end(htmlError('Invalid state — potential CSRF attack'));
189
+ return;
190
+ }
191
+ const current = pendingOAuth;
192
+ pendingOAuth = undefined;
193
+ exchangeCodeForTokens(code, current.verifier)
194
+ .then((tokens) => current.resolve(tokens))
195
+ .catch((err) => current.reject(err));
196
+ res.writeHead(200, { 'Content-Type': 'text/html' });
197
+ res.end(HTML_SUCCESS);
198
+ });
199
+ server.once('error', reject);
200
+ server.listen(OAUTH_PORT, OAUTH_HOST, () => {
201
+ server.removeListener('error', reject);
202
+ // Swallow subsequent errors so they don't crash the process
203
+ server.on('error', () => { });
204
+ resolve(server);
205
+ });
206
+ });
207
+ }
208
+ function waitForOAuthCallback(verifier, state) {
209
+ if (pendingOAuth) {
210
+ pendingOAuth.reject(new Error('Superseded by a newer xAI authorize request'));
211
+ pendingOAuth = undefined;
212
+ }
213
+ return new Promise((resolve, reject) => {
214
+ const timeout = setTimeout(() => {
215
+ if (pendingOAuth) {
216
+ pendingOAuth = undefined;
217
+ reject(new Error('OAuth callback timeout — authorization took too long'));
218
+ }
219
+ }, 5 * 60 * 1000);
220
+ pendingOAuth = {
221
+ verifier,
222
+ state,
223
+ resolve: (tokens) => {
224
+ clearTimeout(timeout);
225
+ resolve(tokens);
226
+ },
227
+ reject: (error) => {
228
+ clearTimeout(timeout);
229
+ reject(error);
230
+ },
231
+ };
232
+ });
233
+ }
234
+ // ─── public login function ───────────────────────────────────────────────────
235
+ /**
236
+ * Run the xAI OAuth browser flow. Opens the system browser for authorization,
237
+ * waits for the callback, and returns an XaiAuth object with tokens.
238
+ */
239
+ export async function xaiOAuthLogin() {
240
+ const s = spinner();
241
+ s.start('Starting xAI OAuth server...');
242
+ let server;
243
+ try {
244
+ server = await startOAuthServer();
245
+ }
246
+ catch (err) {
247
+ s.stop('Failed to start OAuth server');
248
+ throw new Error(`Could not bind to ${OAUTH_HOST}:${OAUTH_PORT}. Is Grok CLI or another egaki instance running? (${err})`);
249
+ }
250
+ const pkce = await generatePKCE();
251
+ const state = generateState();
252
+ const nonce = generateState();
253
+ // `plan=generic` opts into xAI's generic OAuth plan tier.
254
+ // `referrer=egaki` lets xAI attribute egaki-originated logins.
255
+ const params = new URLSearchParams({
256
+ response_type: 'code',
257
+ client_id: CLIENT_ID,
258
+ redirect_uri: REDIRECT_URI,
259
+ scope: SCOPE,
260
+ code_challenge: pkce.challenge,
261
+ code_challenge_method: 'S256',
262
+ state,
263
+ nonce,
264
+ plan: 'generic',
265
+ referrer: 'egaki',
266
+ });
267
+ const authUrl = `${AUTHORIZE_URL}?${params.toString()}`;
268
+ s.stop('OAuth server ready');
269
+ log.info('Opening browser for xAI authorization...');
270
+ note(authUrl, 'If the browser does not open, visit this URL manually');
271
+ // Register the callback listener BEFORE opening the browser. If the user
272
+ // already has an active xAI session, the browser can redirect back to
273
+ // the loopback server almost instantly. Registering after openUrlInBrowser
274
+ // creates a race where the callback arrives before pendingOAuth exists.
275
+ const callbackPromise = waitForOAuthCallback(pkce.verifier, state);
276
+ await openUrlInBrowser(authUrl);
277
+ s.start('Waiting for authorization...');
278
+ try {
279
+ const tokens = await callbackPromise;
280
+ s.stop('Authorization successful');
281
+ const email = extractEmailFromToken(tokens.id_token);
282
+ if (email) {
283
+ log.info(`Signed in as ${pc.bold(email)}`);
284
+ }
285
+ return {
286
+ email,
287
+ access: tokens.access_token,
288
+ refresh: tokens.refresh_token,
289
+ expires: Date.now() + (tokens.expires_in ?? 3600) * 1000,
290
+ };
291
+ }
292
+ catch (err) {
293
+ s.stop('Authorization failed');
294
+ throw err;
295
+ }
296
+ finally {
297
+ server.close();
298
+ }
299
+ }
300
+ // ─── token refresh ───────────────────────────────────────────────────────────
301
+ /**
302
+ * Returns a valid XaiAuth, refreshing the access token if it's expired or
303
+ * about to expire. Calls `save` to persist the refreshed tokens.
304
+ */
305
+ export async function getValidXaiAuth(auth, save) {
306
+ const expiresSoon = !auth.expires ||
307
+ auth.expires - Date.now() <= ACCESS_TOKEN_REFRESH_SKEW_MS ||
308
+ accessTokenIsExpiring(auth.access);
309
+ if (!expiresSoon)
310
+ return auth;
311
+ try {
312
+ const tokens = await refreshXaiToken(auth.refresh);
313
+ const refreshed = {
314
+ email: auth.email,
315
+ access: tokens.access_token,
316
+ refresh: tokens.refresh_token || auth.refresh,
317
+ expires: Date.now() + (tokens.expires_in ?? 3600) * 1000,
318
+ };
319
+ save(refreshed);
320
+ return refreshed;
321
+ }
322
+ catch (err) {
323
+ return new Error(`Failed to refresh xAI token. Please run \`egaki login\` and select xAI again. (${err})`);
324
+ }
325
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * AngledScreen — CSS 3D perspective wrapper with fake bokeh depth-of-field.
3
+ *
4
+ * Wraps any children (images, videos, divs) in a 3D-transformed plane using
5
+ * CSS `perspective` + `transform: rotateX/Y/Z`. Simulates depth-of-field
6
+ * via `backdrop-filter: blur()` masked with `mask-image: linear-gradient()`
7
+ * so only the far edge of the content gets blurred.
8
+ *
9
+ * The blur direction is auto-detected from the rotation angles: whichever
10
+ * edge is furthest from the camera gets blurred. Override with bokehDirection.
11
+ *
12
+ * IMPORTANT: the blur overlay lives OUTSIDE the 3D transform as a sibling
13
+ * in the outer container. backdrop-filter does not work inside CSS 3D
14
+ * transformed elements (the transform compositing layer breaks backdrop
15
+ * sampling and blurs everything regardless of mask). Placing it outside
16
+ * means it blurs the already-composited 3D scene in normal 2D space.
17
+ *
18
+ * Set `debug` to visualize the mask gradient as green (sharp) / red (blur).
19
+ */
20
+ import { type CSSProperties, type ReactNode } from 'react';
21
+ export interface AngledScreenProps {
22
+ children: ReactNode;
23
+ /** CSS perspective distance in px. Smaller = more dramatic. Default 1200. */
24
+ perspective?: number;
25
+ /** X-axis rotation in degrees (tilts top/bottom). Default 8. */
26
+ rotateX?: number;
27
+ /** Y-axis rotation in degrees (angles left/right). Default -12. */
28
+ rotateY?: number;
29
+ /** Z-axis rotation in degrees. Default 0. */
30
+ rotateZ?: number;
31
+ /** Push the plane forward/back in px. Positive = closer/larger,
32
+ * negative = further/smaller (same visual effect as scale). Default 0. */
33
+ translateZ?: number;
34
+ /** Transform origin. Default '50% 50%'. */
35
+ transformOrigin?: string;
36
+ /** Enable bokeh blur overlay. Default true. */
37
+ bokeh?: boolean;
38
+ /** Max blur radius in px at the far edge. Default 12. */
39
+ bokehBlur?: number;
40
+ /** Override which edge gets blurred. Auto-detected from rotation if omitted. */
41
+ bokehDirection?: 'top' | 'bottom' | 'left' | 'right';
42
+ /** Where blur starts (0-1). 0 = blur from the very near edge, 1 = no blur.
43
+ * The transition ramp is a fixed 25% after this point. Default 0.3. */
44
+ bokehOffset?: number;
45
+ /** Background color behind the tilted screen. Default '#000000'. */
46
+ backgroundColor?: string;
47
+ /** Width of the screen content. Default '80%'. */
48
+ width?: string | number;
49
+ /** Height of the screen content. Default 'auto'. */
50
+ height?: string | number;
51
+ /** Show the blur mask as green (sharp) / red (blur) overlay for debugging. */
52
+ debug?: boolean;
53
+ /** Optional inline style on the outer container. */
54
+ style?: CSSProperties;
55
+ }
56
+ export declare function AngledScreen(props: AngledScreenProps): import("react").JSX.Element;
57
+ //# sourceMappingURL=angled-screen.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"angled-screen.d.ts","sourceRoot":"","sources":["../../src/vite/angled-screen.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAG1D,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,SAAS,CAAA;IAGnB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,gEAAgE;IAChE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,6CAA6C;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;+EAC2E;IAC3E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,2CAA2C;IAC3C,eAAe,CAAC,EAAE,MAAM,CAAA;IAGxB,+CAA+C;IAC/C,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gFAAgF;IAChF,cAAc,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAA;IACpD;4EACwE;IACxE,WAAW,CAAC,EAAE,MAAM,CAAA;IAGpB,oEAAoE;IACpE,eAAe,CAAC,EAAE,MAAM,CAAA;IAGxB,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACvB,oDAAoD;IACpD,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IAExB,8EAA8E;IAC9E,KAAK,CAAC,EAAE,OAAO,CAAA;IAEf,oDAAoD;IACpD,KAAK,CAAC,EAAE,aAAa,CAAA;CACtB;AAwBD,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,+BAwHpD"}
@@ -0,0 +1,115 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /**
4
+ * AngledScreen — CSS 3D perspective wrapper with fake bokeh depth-of-field.
5
+ *
6
+ * Wraps any children (images, videos, divs) in a 3D-transformed plane using
7
+ * CSS `perspective` + `transform: rotateX/Y/Z`. Simulates depth-of-field
8
+ * via `backdrop-filter: blur()` masked with `mask-image: linear-gradient()`
9
+ * so only the far edge of the content gets blurred.
10
+ *
11
+ * The blur direction is auto-detected from the rotation angles: whichever
12
+ * edge is furthest from the camera gets blurred. Override with bokehDirection.
13
+ *
14
+ * IMPORTANT: the blur overlay lives OUTSIDE the 3D transform as a sibling
15
+ * in the outer container. backdrop-filter does not work inside CSS 3D
16
+ * transformed elements (the transform compositing layer breaks backdrop
17
+ * sampling and blurs everything regardless of mask). Placing it outside
18
+ * means it blurs the already-composited 3D scene in normal 2D space.
19
+ *
20
+ * Set `debug` to visualize the mask gradient as green (sharp) / red (blur).
21
+ */
22
+ import {} from 'react';
23
+ import { useTweakpane } from "./tweakpane-hook.js";
24
+ const GRADIENT_DIRECTIONS = {
25
+ top: 'to top',
26
+ bottom: 'to bottom',
27
+ left: 'to left',
28
+ right: 'to right',
29
+ };
30
+ /**
31
+ * Auto-detect which edge is furthest from camera based on rotation.
32
+ * CSS rotateY(-20deg): right edge comes toward viewer, LEFT is far → blur left.
33
+ * CSS rotateY(+20deg): left edge comes toward viewer, RIGHT is far → blur right.
34
+ * CSS rotateX(+10deg): top edge comes toward viewer, BOTTOM is far → blur bottom.
35
+ * CSS rotateX(-10deg): bottom edge comes toward viewer, TOP is far → blur top.
36
+ * Picks the axis with the larger rotation angle.
37
+ */
38
+ function autoDirection(rotateX, rotateY) {
39
+ if (Math.abs(rotateY) >= Math.abs(rotateX)) {
40
+ return rotateY < 0 ? 'left' : 'right';
41
+ }
42
+ return rotateX > 0 ? 'bottom' : 'top';
43
+ }
44
+ export function AngledScreen(props) {
45
+ const { children, transformOrigin = '50% 50%', bokehDirection, backgroundColor = '#000000', width = '80%', height = 'auto', style, } = props;
46
+ const tp = useTweakpane('AngledScreen', {
47
+ perspective: { value: props.perspective ?? 1200, min: 100, max: 3000, step: 10 },
48
+ rotateX: { value: props.rotateX ?? 8, min: -90, max: 90, step: 0.5 },
49
+ rotateY: { value: props.rotateY ?? -12, min: -90, max: 90, step: 0.5 },
50
+ rotateZ: { value: props.rotateZ ?? 0, min: -180, max: 180, step: 0.5 },
51
+ translateZ: { value: props.translateZ ?? 0, min: -500, max: 500, step: 1 },
52
+ bokeh: props.bokeh ?? true,
53
+ bokehBlur: { value: props.bokehBlur ?? 12, min: 0, max: 50, step: 0.5 },
54
+ bokehOffset: { value: props.bokehOffset ?? 0.3, min: 0, max: 1, step: 0.01 },
55
+ debug: props.debug ?? false,
56
+ });
57
+ const { perspective, rotateX, rotateY, rotateZ, translateZ } = tp;
58
+ const { bokeh, bokehBlur, bokehOffset } = tp;
59
+ const showDebug = tp.debug;
60
+ const direction = bokehDirection ?? autoDirection(rotateX, rotateY);
61
+ const gradientDir = GRADIENT_DIRECTIONS[direction] || 'to right';
62
+ const startPct = `${bokehOffset * 100}%`;
63
+ const endPct = `${Math.min((bokehOffset + 0.25) * 100, 100)}%`;
64
+ return (_jsxs("div", { style: {
65
+ position: 'relative',
66
+ width: '100%',
67
+ height: '100%',
68
+ overflow: 'hidden',
69
+ }, children: [_jsx("div", { style: {
70
+ position: 'absolute',
71
+ inset: 0,
72
+ backgroundColor,
73
+ display: 'flex',
74
+ alignItems: 'center',
75
+ justifyContent: 'center',
76
+ ...style,
77
+ }, children: _jsx("div", { style: {
78
+ perspective: `${perspective}px`,
79
+ perspectiveOrigin: '50% 50%',
80
+ display: 'flex',
81
+ alignItems: 'center',
82
+ justifyContent: 'center',
83
+ width: '100%',
84
+ height: '100%',
85
+ }, children: _jsx("div", { style: {
86
+ transform: [
87
+ `rotateX(${rotateX}deg)`,
88
+ `rotateY(${rotateY}deg)`,
89
+ `rotateZ(${rotateZ}deg)`,
90
+ translateZ !== 0 ? `translateZ(${translateZ}px)` : '',
91
+ ]
92
+ .filter(Boolean)
93
+ .join(' '),
94
+ transformOrigin,
95
+ width: typeof width === 'number' ? `${width}px` : width,
96
+ height: typeof height === 'number' ? `${height}px` : height,
97
+ position: 'relative',
98
+ borderRadius: '12px',
99
+ overflow: 'hidden',
100
+ }, children: children }) }) }), bokeh && bokehBlur > 0 && !showDebug && (_jsx("div", { style: {
101
+ position: 'absolute',
102
+ inset: 0,
103
+ backdropFilter: `blur(${bokehBlur}px)`,
104
+ WebkitBackdropFilter: `blur(${bokehBlur}px)`,
105
+ maskImage: `linear-gradient(${gradientDir}, transparent ${startPct}, black ${endPct})`,
106
+ WebkitMaskImage: `linear-gradient(${gradientDir}, transparent ${startPct}, black ${endPct})`,
107
+ pointerEvents: 'none',
108
+ } })), showDebug && (_jsx("div", { style: {
109
+ position: 'absolute',
110
+ inset: 0,
111
+ background: `linear-gradient(${gradientDir}, rgba(0,255,0,0.4) ${startPct}, rgba(255,0,0,0.4) ${endPct})`,
112
+ pointerEvents: 'none',
113
+ zIndex: 999,
114
+ } }))] }));
115
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Spiceflow entry for the video framework.
3
+ *
4
+ * The server has two jobs:
5
+ *
6
+ * 1. Deliver the MDX source string to the client through the RSC flight
7
+ * payload. All regular MDX processing (parsing, section splitting,
8
+ * expression evaluation, safe-mdx rendering) happens in the browser
9
+ * inside MdxClientApp (mdx-client.tsx, 'use client'), so MDX expression
10
+ * props can be functions and user components don't need 'use client'.
11
+ *
12
+ * 2. Render <Server> slots. <Server> is a reserved MDX element marking a
13
+ * subtree as server components: its children are rendered HERE in the
14
+ * RSC environment (async allowed, promises stream through flight) and
15
+ * passed to the client as serverSlots keyed by the node's start
16
+ * position. The MDX string sent to the client has each <Server> block
17
+ * blanked to a self-closing marker with newline padding, so every
18
+ * position (slot keys, data-markdown-line, sourcemaps) stays aligned
19
+ * with the original file and the client never parses server-only
20
+ * content.
21
+ *
22
+ * NOTE: Relative imports MUST include file extensions (.tsx, .ts) for the
23
+ * RSC module runner to resolve them correctly within noExternal packages.
24
+ */
25
+ import { Spiceflow } from 'spiceflow';
26
+ import { type GenerationProgressEvent } from '../cli/cached-generate.js';
27
+ export declare const app: Spiceflow<"", true, {
28
+ state: {};
29
+ }, {
30
+ type: {};
31
+ error: {};
32
+ }, {
33
+ schema: {};
34
+ macro: {};
35
+ macroFn: {};
36
+ loaderData: {};
37
+ }, {
38
+ api: {
39
+ "generation-progress": {
40
+ get: {
41
+ request: unknown;
42
+ params: {};
43
+ query: unknown;
44
+ response: {
45
+ 200: AsyncGenerator<GenerationProgressEvent, any, any>;
46
+ };
47
+ };
48
+ };
49
+ };
50
+ }, "" | "/" | "/api/generation-progress", Record<"/api/generation-progress", unknown>>;
51
+ //# sourceMappingURL=app.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../src/vite/app.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAKH,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAqBrC,OAAO,EAGL,KAAK,uBAAuB,EAC7B,MAAM,2BAA2B,CAAA;AA+IlC,eAAO,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;sFAAa,CAAA"}