@agile-vibe-coding/avc 0.2.3 → 0.3.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.
Files changed (262) hide show
  1. package/README.md +475 -3
  2. package/cli/agents/agent-selector.md +23 -0
  3. package/cli/agents/code-implementer.md +117 -0
  4. package/cli/agents/code-validator.md +80 -0
  5. package/cli/agents/context-reviewer-epic.md +101 -0
  6. package/cli/agents/context-reviewer-story.md +92 -0
  7. package/cli/agents/context-writer-epic.md +145 -0
  8. package/cli/agents/context-writer-story.md +111 -0
  9. package/cli/agents/doc-writer-epic.md +42 -0
  10. package/cli/agents/doc-writer-story.md +43 -0
  11. package/cli/agents/duplicate-detector.md +110 -0
  12. package/cli/agents/epic-story-decomposer.md +318 -39
  13. package/cli/agents/mission-scope-generator.md +68 -4
  14. package/cli/agents/mission-scope-validator.md +40 -6
  15. package/cli/agents/project-context-extractor.md +21 -6
  16. package/cli/agents/scaffolding-generator.md +99 -0
  17. package/cli/agents/seed-validator.md +71 -0
  18. package/cli/agents/story-scope-reviewer.md +147 -0
  19. package/cli/agents/story-splitter.md +83 -0
  20. package/cli/agents/validator-documentation.json +31 -0
  21. package/cli/agents/validator-documentation.md +3 -1
  22. package/cli/api-reference-tool.js +368 -0
  23. package/cli/checks/catalog.json +76 -0
  24. package/cli/checks/code/quality.json +26 -0
  25. package/cli/checks/code/testing.json +14 -0
  26. package/cli/checks/code/traceability.json +26 -0
  27. package/cli/checks/cross-refs/epic.json +171 -0
  28. package/cli/checks/cross-refs/story.json +149 -0
  29. package/cli/checks/epic/api.json +114 -0
  30. package/cli/checks/epic/backend.json +126 -0
  31. package/cli/checks/epic/cloud.json +126 -0
  32. package/cli/checks/epic/data.json +102 -0
  33. package/cli/checks/epic/database.json +114 -0
  34. package/cli/checks/epic/developer.json +182 -0
  35. package/cli/checks/epic/devops.json +174 -0
  36. package/cli/checks/epic/frontend.json +162 -0
  37. package/cli/checks/epic/mobile.json +102 -0
  38. package/cli/checks/epic/qa.json +90 -0
  39. package/cli/checks/epic/security.json +184 -0
  40. package/cli/checks/epic/solution-architect.json +192 -0
  41. package/cli/checks/epic/test-architect.json +90 -0
  42. package/cli/checks/epic/ui.json +102 -0
  43. package/cli/checks/epic/ux.json +90 -0
  44. package/cli/checks/fixes/epic-fix-template.md +10 -0
  45. package/cli/checks/fixes/story-fix-template.md +10 -0
  46. package/cli/checks/story/api.json +186 -0
  47. package/cli/checks/story/backend.json +102 -0
  48. package/cli/checks/story/cloud.json +102 -0
  49. package/cli/checks/story/data.json +210 -0
  50. package/cli/checks/story/database.json +102 -0
  51. package/cli/checks/story/developer.json +168 -0
  52. package/cli/checks/story/devops.json +102 -0
  53. package/cli/checks/story/frontend.json +174 -0
  54. package/cli/checks/story/mobile.json +102 -0
  55. package/cli/checks/story/qa.json +210 -0
  56. package/cli/checks/story/security.json +198 -0
  57. package/cli/checks/story/solution-architect.json +230 -0
  58. package/cli/checks/story/test-architect.json +210 -0
  59. package/cli/checks/story/ui.json +102 -0
  60. package/cli/checks/story/ux.json +102 -0
  61. package/cli/coding-order.js +401 -0
  62. package/cli/dependency-checker.js +72 -0
  63. package/cli/epic-story-validator.js +284 -799
  64. package/cli/index.js +0 -0
  65. package/cli/init-model-config.js +17 -10
  66. package/cli/init.js +514 -92
  67. package/cli/kanban-server-manager.js +1 -2
  68. package/cli/llm-claude.js +98 -31
  69. package/cli/llm-gemini.js +29 -5
  70. package/cli/llm-local.js +493 -0
  71. package/cli/llm-openai.js +262 -41
  72. package/cli/llm-provider.js +147 -8
  73. package/cli/llm-token-limits.js +113 -4
  74. package/cli/llm-verifier.js +209 -1
  75. package/cli/llm-xiaomi.js +143 -0
  76. package/cli/message-constants.js +3 -12
  77. package/cli/messaging-api.js +6 -12
  78. package/cli/micro-check-fixer.js +335 -0
  79. package/cli/micro-check-runner.js +449 -0
  80. package/cli/micro-check-scorer.js +148 -0
  81. package/cli/micro-check-validator.js +538 -0
  82. package/cli/model-pricing.js +23 -0
  83. package/cli/model-selector.js +3 -2
  84. package/cli/prompt-logger.js +57 -0
  85. package/cli/repl-ink.js +106 -346
  86. package/cli/repl-old.js +1 -2
  87. package/cli/seed-processor.js +194 -24
  88. package/cli/sprint-planning-processor.js +2638 -289
  89. package/cli/template-processor.js +50 -3
  90. package/cli/token-tracker.js +50 -23
  91. package/cli/tools/generate-story-validators.js +1 -1
  92. package/cli/validation-router.js +70 -8
  93. package/cli/worktree-runner.js +654 -0
  94. package/kanban/client/dist/assets/index-D_KC5EQT.css +1 -0
  95. package/kanban/client/dist/assets/index-DjY5zqW7.js +351 -0
  96. package/kanban/client/dist/index.html +2 -2
  97. package/kanban/client/src/App.jsx +43 -14
  98. package/kanban/client/src/components/ceremony/AskArchPopup.jsx +7 -3
  99. package/kanban/client/src/components/ceremony/AskModelPopup.jsx +23 -10
  100. package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +320 -133
  101. package/kanban/client/src/components/ceremony/ProviderSwitcherButton.jsx +290 -0
  102. package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +80 -13
  103. package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +156 -22
  104. package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +11 -11
  105. package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +3 -21
  106. package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +214 -10
  107. package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +23 -2
  108. package/kanban/client/src/components/kanban/CardDetailModal.jsx +97 -10
  109. package/kanban/client/src/components/kanban/GroupingSelector.jsx +7 -1
  110. package/kanban/client/src/components/kanban/KanbanCard.jsx +23 -14
  111. package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +9 -14
  112. package/kanban/client/src/components/kanban/RunButton.jsx +162 -0
  113. package/kanban/client/src/components/kanban/SeedButton.jsx +176 -0
  114. package/kanban/client/src/components/settings/AgentsTab.jsx +103 -75
  115. package/kanban/client/src/components/settings/ApiKeysTab.jsx +31 -2
  116. package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +9 -2
  117. package/kanban/client/src/components/settings/CheckEditorPopup.jsx +507 -0
  118. package/kanban/client/src/components/settings/CostThresholdsTab.jsx +3 -2
  119. package/kanban/client/src/components/settings/ModelPricingTab.jsx +72 -7
  120. package/kanban/client/src/components/settings/OpenAIAuthSection.jsx +412 -0
  121. package/kanban/client/src/components/settings/SettingsModal.jsx +4 -4
  122. package/kanban/client/src/components/stats/CostModal.jsx +34 -3
  123. package/kanban/client/src/hooks/useGrouping.js +59 -0
  124. package/kanban/client/src/lib/api.js +118 -4
  125. package/kanban/client/src/lib/status-grouping.js +10 -0
  126. package/kanban/client/src/store/kanbanStore.js +8 -0
  127. package/kanban/server/index.js +23 -2
  128. package/kanban/server/routes/ceremony.js +153 -4
  129. package/kanban/server/routes/costs.js +9 -3
  130. package/kanban/server/routes/openai-oauth.js +366 -0
  131. package/kanban/server/routes/settings.js +447 -14
  132. package/kanban/server/routes/websocket.js +7 -2
  133. package/kanban/server/routes/work-items.js +141 -1
  134. package/kanban/server/services/CeremonyService.js +275 -24
  135. package/kanban/server/services/TaskRunnerService.js +261 -0
  136. package/kanban/server/workers/run-task-worker.js +121 -0
  137. package/kanban/server/workers/seed-worker.js +94 -0
  138. package/kanban/server/workers/sponsor-call-worker.js +14 -6
  139. package/kanban/server/workers/sprint-planning-worker.js +94 -12
  140. package/package.json +2 -3
  141. package/cli/agents/solver-epic-api.json +0 -15
  142. package/cli/agents/solver-epic-api.md +0 -39
  143. package/cli/agents/solver-epic-backend.json +0 -15
  144. package/cli/agents/solver-epic-backend.md +0 -39
  145. package/cli/agents/solver-epic-cloud.json +0 -15
  146. package/cli/agents/solver-epic-cloud.md +0 -39
  147. package/cli/agents/solver-epic-data.json +0 -15
  148. package/cli/agents/solver-epic-data.md +0 -39
  149. package/cli/agents/solver-epic-database.json +0 -15
  150. package/cli/agents/solver-epic-database.md +0 -39
  151. package/cli/agents/solver-epic-developer.json +0 -15
  152. package/cli/agents/solver-epic-developer.md +0 -39
  153. package/cli/agents/solver-epic-devops.json +0 -15
  154. package/cli/agents/solver-epic-devops.md +0 -39
  155. package/cli/agents/solver-epic-frontend.json +0 -15
  156. package/cli/agents/solver-epic-frontend.md +0 -39
  157. package/cli/agents/solver-epic-mobile.json +0 -15
  158. package/cli/agents/solver-epic-mobile.md +0 -39
  159. package/cli/agents/solver-epic-qa.json +0 -15
  160. package/cli/agents/solver-epic-qa.md +0 -39
  161. package/cli/agents/solver-epic-security.json +0 -15
  162. package/cli/agents/solver-epic-security.md +0 -39
  163. package/cli/agents/solver-epic-solution-architect.json +0 -15
  164. package/cli/agents/solver-epic-solution-architect.md +0 -39
  165. package/cli/agents/solver-epic-test-architect.json +0 -15
  166. package/cli/agents/solver-epic-test-architect.md +0 -39
  167. package/cli/agents/solver-epic-ui.json +0 -15
  168. package/cli/agents/solver-epic-ui.md +0 -39
  169. package/cli/agents/solver-epic-ux.json +0 -15
  170. package/cli/agents/solver-epic-ux.md +0 -39
  171. package/cli/agents/solver-story-api.json +0 -15
  172. package/cli/agents/solver-story-api.md +0 -39
  173. package/cli/agents/solver-story-backend.json +0 -15
  174. package/cli/agents/solver-story-backend.md +0 -39
  175. package/cli/agents/solver-story-cloud.json +0 -15
  176. package/cli/agents/solver-story-cloud.md +0 -39
  177. package/cli/agents/solver-story-data.json +0 -15
  178. package/cli/agents/solver-story-data.md +0 -39
  179. package/cli/agents/solver-story-database.json +0 -15
  180. package/cli/agents/solver-story-database.md +0 -39
  181. package/cli/agents/solver-story-developer.json +0 -15
  182. package/cli/agents/solver-story-developer.md +0 -39
  183. package/cli/agents/solver-story-devops.json +0 -15
  184. package/cli/agents/solver-story-devops.md +0 -39
  185. package/cli/agents/solver-story-frontend.json +0 -15
  186. package/cli/agents/solver-story-frontend.md +0 -39
  187. package/cli/agents/solver-story-mobile.json +0 -15
  188. package/cli/agents/solver-story-mobile.md +0 -39
  189. package/cli/agents/solver-story-qa.json +0 -15
  190. package/cli/agents/solver-story-qa.md +0 -39
  191. package/cli/agents/solver-story-security.json +0 -15
  192. package/cli/agents/solver-story-security.md +0 -39
  193. package/cli/agents/solver-story-solution-architect.json +0 -15
  194. package/cli/agents/solver-story-solution-architect.md +0 -39
  195. package/cli/agents/solver-story-test-architect.json +0 -15
  196. package/cli/agents/solver-story-test-architect.md +0 -39
  197. package/cli/agents/solver-story-ui.json +0 -15
  198. package/cli/agents/solver-story-ui.md +0 -39
  199. package/cli/agents/solver-story-ux.json +0 -15
  200. package/cli/agents/solver-story-ux.md +0 -39
  201. package/cli/agents/validator-epic-api.json +0 -93
  202. package/cli/agents/validator-epic-api.md +0 -137
  203. package/cli/agents/validator-epic-backend.json +0 -93
  204. package/cli/agents/validator-epic-backend.md +0 -130
  205. package/cli/agents/validator-epic-cloud.json +0 -93
  206. package/cli/agents/validator-epic-cloud.md +0 -137
  207. package/cli/agents/validator-epic-data.json +0 -93
  208. package/cli/agents/validator-epic-data.md +0 -130
  209. package/cli/agents/validator-epic-database.json +0 -93
  210. package/cli/agents/validator-epic-database.md +0 -137
  211. package/cli/agents/validator-epic-developer.json +0 -74
  212. package/cli/agents/validator-epic-developer.md +0 -153
  213. package/cli/agents/validator-epic-devops.json +0 -74
  214. package/cli/agents/validator-epic-devops.md +0 -153
  215. package/cli/agents/validator-epic-frontend.json +0 -74
  216. package/cli/agents/validator-epic-frontend.md +0 -153
  217. package/cli/agents/validator-epic-mobile.json +0 -93
  218. package/cli/agents/validator-epic-mobile.md +0 -130
  219. package/cli/agents/validator-epic-qa.json +0 -93
  220. package/cli/agents/validator-epic-qa.md +0 -130
  221. package/cli/agents/validator-epic-security.json +0 -74
  222. package/cli/agents/validator-epic-security.md +0 -154
  223. package/cli/agents/validator-epic-solution-architect.json +0 -74
  224. package/cli/agents/validator-epic-solution-architect.md +0 -156
  225. package/cli/agents/validator-epic-test-architect.json +0 -93
  226. package/cli/agents/validator-epic-test-architect.md +0 -130
  227. package/cli/agents/validator-epic-ui.json +0 -93
  228. package/cli/agents/validator-epic-ui.md +0 -130
  229. package/cli/agents/validator-epic-ux.json +0 -93
  230. package/cli/agents/validator-epic-ux.md +0 -130
  231. package/cli/agents/validator-story-api.json +0 -104
  232. package/cli/agents/validator-story-api.md +0 -152
  233. package/cli/agents/validator-story-backend.json +0 -104
  234. package/cli/agents/validator-story-backend.md +0 -152
  235. package/cli/agents/validator-story-cloud.json +0 -104
  236. package/cli/agents/validator-story-cloud.md +0 -152
  237. package/cli/agents/validator-story-data.json +0 -104
  238. package/cli/agents/validator-story-data.md +0 -152
  239. package/cli/agents/validator-story-database.json +0 -104
  240. package/cli/agents/validator-story-database.md +0 -152
  241. package/cli/agents/validator-story-developer.json +0 -104
  242. package/cli/agents/validator-story-developer.md +0 -152
  243. package/cli/agents/validator-story-devops.json +0 -104
  244. package/cli/agents/validator-story-devops.md +0 -152
  245. package/cli/agents/validator-story-frontend.json +0 -104
  246. package/cli/agents/validator-story-frontend.md +0 -152
  247. package/cli/agents/validator-story-mobile.json +0 -104
  248. package/cli/agents/validator-story-mobile.md +0 -152
  249. package/cli/agents/validator-story-qa.json +0 -104
  250. package/cli/agents/validator-story-qa.md +0 -152
  251. package/cli/agents/validator-story-security.json +0 -104
  252. package/cli/agents/validator-story-security.md +0 -152
  253. package/cli/agents/validator-story-solution-architect.json +0 -104
  254. package/cli/agents/validator-story-solution-architect.md +0 -152
  255. package/cli/agents/validator-story-test-architect.json +0 -104
  256. package/cli/agents/validator-story-test-architect.md +0 -152
  257. package/cli/agents/validator-story-ui.json +0 -104
  258. package/cli/agents/validator-story-ui.md +0 -152
  259. package/cli/agents/validator-story-ux.json +0 -104
  260. package/cli/agents/validator-story-ux.md +0 -152
  261. package/kanban/client/dist/assets/index-CiD8PS2e.js +0 -306
  262. package/kanban/client/dist/assets/index-nLh0m82Q.css +0 -1
@@ -0,0 +1,366 @@
1
+ import express from 'express';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+ import crypto from 'node:crypto';
5
+ import http from 'node:http';
6
+ import { exec } from 'node:child_process';
7
+
8
+ const CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
9
+ const AUTHORIZE_URL = 'https://auth.openai.com/oauth/authorize';
10
+ const TOKEN_URL = 'https://auth.openai.com/oauth/token';
11
+ const REDIRECT_URI = 'http://localhost:1455/auth/callback';
12
+ const SCOPE = 'openid profile email offline_access';
13
+
14
+ const oauthFilePath = (projectRoot) => path.join(projectRoot, '.avc', 'openai-oauth.json');
15
+
16
+ // Parse .env into key→value map
17
+ async function readEnv(envPath) {
18
+ try {
19
+ const lines = (await fs.readFile(envPath, 'utf8')).split('\n');
20
+ const map = {};
21
+ for (const line of lines) {
22
+ const m = line.match(/^([A-Z_]+)\s*=\s*(.*)$/);
23
+ if (m) map[m[1]] = m[2].replace(/^["']|["']$/g, '');
24
+ }
25
+ return map;
26
+ } catch {
27
+ return {};
28
+ }
29
+ }
30
+
31
+ // Update or insert a single key in .env, preserving all other lines
32
+ async function upsertEnvKey(envPath, key, value) {
33
+ let content = '';
34
+ try { content = await fs.readFile(envPath, 'utf8'); } catch {}
35
+ const lines = content.split('\n');
36
+ const idx = lines.findIndex(l => l.match(new RegExp(`^${key}\\s*=`)));
37
+ const newLine = value ? `${key}=${value}` : '';
38
+ if (idx >= 0) {
39
+ if (newLine) {
40
+ lines[idx] = newLine;
41
+ } else {
42
+ lines.splice(idx, 1);
43
+ }
44
+ } else if (newLine) {
45
+ lines.push(newLine);
46
+ }
47
+ await fs.writeFile(envPath, lines.join('\n'), 'utf8');
48
+ }
49
+
50
+ // Decode a JWT payload (no verification needed — we trust our own callback)
51
+ function decodeJwtPayload(token) {
52
+ try {
53
+ const [, payloadB64] = token.split('.');
54
+ const padded = payloadB64.replace(/-/g, '+').replace(/_/g, '/');
55
+ return JSON.parse(Buffer.from(padded, 'base64').toString('utf8'));
56
+ } catch {
57
+ return {};
58
+ }
59
+ }
60
+
61
+ // Open URL in the system default browser
62
+ function openBrowser(url) {
63
+ const platform = process.platform;
64
+ const cmd = platform === 'darwin' ? `open "${url}"`
65
+ : platform === 'win32' ? `start "" "${url}"`
66
+ : `xdg-open "${url}"`;
67
+ exec(cmd, (err) => {
68
+ if (err) console.error('[openai-oauth] Browser open failed:', err.message);
69
+ });
70
+ }
71
+
72
+ /**
73
+ * Spin up a temporary HTTP server on port 1455 to catch the OAuth callback.
74
+ * Returns a Promise that resolves with the authorization code, or rejects on timeout/error.
75
+ */
76
+ function waitForOAuthCallback(expectedState, timeoutMs = 300_000) {
77
+ return new Promise((resolve, reject) => {
78
+ const server = http.createServer((req, res) => {
79
+ const parsedUrl = new URL(req.url, 'http://localhost:1455');
80
+ if (parsedUrl.pathname !== '/auth/callback') {
81
+ res.writeHead(404);
82
+ res.end('Not found');
83
+ return;
84
+ }
85
+
86
+ const code = parsedUrl.searchParams.get('code');
87
+ const state = parsedUrl.searchParams.get('state');
88
+ const error = parsedUrl.searchParams.get('error');
89
+
90
+ res.writeHead(200, { 'Content-Type': 'text/html' });
91
+ res.end('<html><body><h2>Authentication complete. You can close this tab.</h2></body></html>');
92
+ server.close();
93
+
94
+ if (error) return reject(new Error(`OAuth error: ${error}`));
95
+ if (state !== expectedState) return reject(new Error('State mismatch — possible CSRF'));
96
+ if (!code) return reject(new Error('No code returned from OAuth server'));
97
+ resolve(code);
98
+ });
99
+
100
+ server.on('error', (err) => reject(err));
101
+
102
+ server.listen(1455, () => {
103
+ console.log('[openai-oauth] Callback server listening on http://localhost:1455');
104
+ });
105
+
106
+ const timer = setTimeout(() => {
107
+ server.close();
108
+ reject(new Error('OAuth login timed out after 5 minutes'));
109
+ }, timeoutMs);
110
+
111
+ server.on('close', () => clearTimeout(timer));
112
+ });
113
+ }
114
+
115
+ /**
116
+ * Exchange the authorization code for tokens.
117
+ */
118
+ async function exchangeCodeForTokens(code, codeVerifier) {
119
+ const body = new URLSearchParams({
120
+ grant_type: 'authorization_code',
121
+ client_id: CLIENT_ID,
122
+ code,
123
+ code_verifier: codeVerifier,
124
+ redirect_uri: REDIRECT_URI,
125
+ });
126
+
127
+ const resp = await fetch(TOKEN_URL, {
128
+ method: 'POST',
129
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
130
+ body: body.toString(),
131
+ });
132
+
133
+ if (!resp.ok) {
134
+ const text = await resp.text();
135
+ throw new Error(`Token exchange failed (${resp.status}): ${text}`);
136
+ }
137
+ return resp.json();
138
+ }
139
+
140
+ /**
141
+ * @param {string} projectRoot
142
+ * @param {() => object|null} getWebSocket - lazy getter; returns the websocket object or null
143
+ */
144
+ export function createOpenAIOAuthRouter(projectRoot, getWebSocket) {
145
+ const router = express.Router();
146
+ const envPath = path.join(projectRoot, '.env');
147
+
148
+ // POST /login — start PKCE flow
149
+ router.post('/login', async (req, res) => {
150
+ try {
151
+ // Generate PKCE
152
+ const verifier = crypto.randomBytes(32).toString('hex');
153
+ const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
154
+ const state = crypto.randomBytes(16).toString('hex');
155
+
156
+ // Build authorize URL
157
+ const params = new URLSearchParams({
158
+ response_type: 'code',
159
+ client_id: CLIENT_ID,
160
+ redirect_uri: REDIRECT_URI,
161
+ scope: SCOPE,
162
+ state,
163
+ code_challenge: challenge,
164
+ code_challenge_method: 'S256',
165
+ id_token_add_organizations: 'true',
166
+ codex_cli_simplified_flow: 'true',
167
+ });
168
+ const authorizeUrl = `${AUTHORIZE_URL}?${params.toString()}`;
169
+
170
+ // Open browser (best-effort)
171
+ openBrowser(authorizeUrl);
172
+
173
+ // Return immediately so the UI can show the URL
174
+ res.json({ status: 'pending', authorizeUrl });
175
+
176
+ // Handle callback asynchronously
177
+ (async () => {
178
+ try {
179
+ const code = await waitForOAuthCallback(state);
180
+ const tokens = await exchangeCodeForTokens(code, verifier);
181
+
182
+ // Extract accountId from JWT payload
183
+ const payload = decodeJwtPayload(tokens.access_token);
184
+ const accountId = payload['https://api.openai.com/auth']?.chatgpt_account_id ?? '';
185
+
186
+ // Persist tokens
187
+ const oauthData = {
188
+ access: tokens.access_token,
189
+ refresh: tokens.refresh_token,
190
+ expires: Date.now() + (tokens.expires_in || 3600) * 1000,
191
+ accountId,
192
+ };
193
+ await fs.writeFile(oauthFilePath(projectRoot), JSON.stringify(oauthData, null, 2), 'utf8');
194
+
195
+ // Set auth mode in .env
196
+ await upsertEnvKey(envPath, 'OPENAI_AUTH_MODE', 'oauth');
197
+
198
+ // Notify UI via WebSocket
199
+ const ws = getWebSocket();
200
+ if (ws?.broadcast) {
201
+ ws.broadcast({ type: 'openai-oauth-connected', accountId });
202
+ }
203
+
204
+ console.log(`[openai-oauth] Connected successfully (accountId: ${accountId})`);
205
+ } catch (err) {
206
+ console.error('[openai-oauth] Login flow error:', err.message);
207
+ const ws = getWebSocket();
208
+ if (ws?.broadcast) {
209
+ ws.broadcast({ type: 'openai-oauth-error', error: err.message });
210
+ }
211
+ }
212
+ })();
213
+ } catch (err) {
214
+ res.status(500).json({ error: err.message });
215
+ }
216
+ });
217
+
218
+ // GET /status
219
+ router.get('/status', async (req, res) => {
220
+ try {
221
+ const [env, oauthRaw] = await Promise.all([
222
+ readEnv(envPath),
223
+ fs.readFile(oauthFilePath(projectRoot), 'utf8').catch(() => null),
224
+ ]);
225
+
226
+ if (!oauthRaw || env.OPENAI_AUTH_MODE !== 'oauth') {
227
+ return res.json({ connected: false });
228
+ }
229
+
230
+ const { accountId, expires } = JSON.parse(oauthRaw);
231
+ res.json({
232
+ connected: true,
233
+ accountId,
234
+ expiresAt: expires,
235
+ expiresIn: Math.max(0, Math.round((expires - Date.now()) / 1000)),
236
+ fallback: env.OPENAI_OAUTH_FALLBACK === 'true',
237
+ });
238
+ } catch (err) {
239
+ res.status(500).json({ error: err.message });
240
+ }
241
+ });
242
+
243
+ // POST /test — fire a minimal prompt to verify the OAuth tokens work
244
+ router.post('/test', async (req, res) => {
245
+ try {
246
+ const oauthRaw = await fs.readFile(oauthFilePath(projectRoot), 'utf8').catch(() => null);
247
+ if (!oauthRaw) return res.status(400).json({ error: 'Not connected' });
248
+
249
+ let { access, accountId, expires, refresh } = JSON.parse(oauthRaw);
250
+
251
+ // Refresh if within 60s of expiry
252
+ if (expires - Date.now() < 60_000) {
253
+ const body = new URLSearchParams({
254
+ grant_type: 'refresh_token',
255
+ client_id: CLIENT_ID,
256
+ refresh_token: refresh,
257
+ });
258
+ const refreshResp = await fetch(TOKEN_URL, {
259
+ method: 'POST',
260
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
261
+ body: body.toString(),
262
+ });
263
+ if (!refreshResp.ok) return res.status(401).json({ error: 'Token refresh failed' });
264
+ const refreshed = await refreshResp.json();
265
+ access = refreshed.access_token;
266
+ refresh = refreshed.refresh_token || refresh;
267
+ expires = Date.now() + (refreshed.expires_in || 3600) * 1000;
268
+ await fs.writeFile(oauthFilePath(projectRoot), JSON.stringify({ access, refresh, expires, accountId }, null, 2), 'utf8');
269
+ }
270
+
271
+ const t0 = Date.now();
272
+ const apiResp = await fetch('https://chatgpt.com/backend-api/codex/responses', {
273
+ method: 'POST',
274
+ headers: {
275
+ 'Authorization': `Bearer ${access}`,
276
+ 'chatgpt-account-id': accountId,
277
+ 'Content-Type': 'application/json',
278
+ 'OpenAI-Beta': 'responses=experimental',
279
+ 'accept': 'application/json',
280
+ },
281
+ body: JSON.stringify({
282
+ model: 'gpt-5.2-codex',
283
+ instructions: 'You are a helpful assistant.',
284
+ input: [{ role: 'user', content: 'Reply with exactly: "AVC OAuth test OK"' }],
285
+ store: false,
286
+ stream: true,
287
+ }),
288
+ });
289
+
290
+ if (!apiResp.ok) {
291
+ const raw = await apiResp.text();
292
+ return res.status(502).json({ error: `API error ${apiResp.status}: ${raw}` });
293
+ }
294
+
295
+ // Parse SSE stream — accumulate deltas; use response.done for final text
296
+ const body = await apiResp.text();
297
+ let text = '';
298
+ let finalEvent = null;
299
+ for (const line of body.split('\n')) {
300
+ if (!line.startsWith('data: ')) continue;
301
+ const chunk = line.slice(6).trim();
302
+ if (chunk === '[DONE]') break;
303
+ try {
304
+ const event = JSON.parse(chunk);
305
+ if (event.type === 'response.output_text.delta') {
306
+ text += event.delta ?? '';
307
+ } else if (event.type === 'response.output_text.done') {
308
+ text = event.text ?? text;
309
+ } else if (event.type === 'response.done' || event.type === 'response.completed') {
310
+ finalEvent = event.response ?? event;
311
+ if (!text) {
312
+ text = finalEvent?.output_text ?? finalEvent?.output?.[0]?.content?.[0]?.text ?? '';
313
+ }
314
+ break;
315
+ }
316
+ } catch { /* skip malformed lines */ }
317
+ }
318
+ res.json({ ok: true, response: text.trim(), model: 'gpt-5.2-codex', elapsed: Date.now() - t0 });
319
+ } catch (err) {
320
+ res.status(500).json({ error: err.message });
321
+ }
322
+ });
323
+
324
+ // POST /fallback — enable or disable API-key fallback
325
+ router.post('/fallback', async (req, res) => {
326
+ try {
327
+ const { enabled } = req.body;
328
+ if (typeof enabled !== 'boolean') return res.status(400).json({ error: 'enabled must be boolean' });
329
+
330
+ if (enabled) {
331
+ // Verify the API key exists and can reach OpenAI before enabling
332
+ const env = await readEnv(envPath);
333
+ if (!env.OPENAI_API_KEY) {
334
+ return res.status(400).json({ error: 'OPENAI_API_KEY is not set. Add it first.' });
335
+ }
336
+ // Quick validation call
337
+ const verifyResp = await fetch('https://api.openai.com/v1/models', {
338
+ headers: { 'Authorization': `Bearer ${env.OPENAI_API_KEY}` },
339
+ });
340
+ if (!verifyResp.ok) {
341
+ return res.status(400).json({ error: `API key verification failed (${verifyResp.status}). Check the key.` });
342
+ }
343
+ await upsertEnvKey(envPath, 'OPENAI_OAUTH_FALLBACK', 'true');
344
+ } else {
345
+ await upsertEnvKey(envPath, 'OPENAI_OAUTH_FALLBACK', '');
346
+ }
347
+
348
+ res.json({ status: 'ok', enabled });
349
+ } catch (err) {
350
+ res.status(500).json({ error: err.message });
351
+ }
352
+ });
353
+
354
+ // POST /logout
355
+ router.post('/logout', async (req, res) => {
356
+ try {
357
+ try { await fs.unlink(oauthFilePath(projectRoot)); } catch {}
358
+ await upsertEnvKey(envPath, 'OPENAI_AUTH_MODE', '');
359
+ res.json({ status: 'ok' });
360
+ } catch (err) {
361
+ res.status(500).json({ error: err.message });
362
+ }
363
+ });
364
+
365
+ return router;
366
+ }