@agile-vibe-coding/avc 0.1.1 → 0.3.1

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 (239) hide show
  1. package/cli/agent-loader.js +21 -0
  2. package/cli/agents/agent-selector.md +152 -0
  3. package/cli/agents/architecture-recommender.md +418 -0
  4. package/cli/agents/code-implementer.md +117 -0
  5. package/cli/agents/code-validator.md +80 -0
  6. package/cli/agents/context-reviewer-epic.md +101 -0
  7. package/cli/agents/context-reviewer-story.md +92 -0
  8. package/cli/agents/context-writer-epic.md +145 -0
  9. package/cli/agents/context-writer-story.md +111 -0
  10. package/cli/agents/database-deep-dive.md +470 -0
  11. package/cli/agents/database-recommender.md +634 -0
  12. package/cli/agents/doc-distributor.md +176 -0
  13. package/cli/agents/doc-writer-epic.md +42 -0
  14. package/cli/agents/doc-writer-story.md +43 -0
  15. package/cli/agents/documentation-updater.md +203 -0
  16. package/cli/agents/duplicate-detector.md +110 -0
  17. package/cli/agents/epic-story-decomposer.md +559 -0
  18. package/cli/agents/feature-context-generator.md +91 -0
  19. package/cli/agents/gap-checker-epic.md +52 -0
  20. package/cli/agents/impact-checker-story.md +51 -0
  21. package/cli/agents/migration-guide-generator.md +305 -0
  22. package/cli/agents/mission-scope-generator.md +143 -0
  23. package/cli/agents/mission-scope-validator.md +146 -0
  24. package/cli/agents/project-context-extractor.md +122 -0
  25. package/cli/agents/project-documentation-creator.json +226 -0
  26. package/cli/agents/project-documentation-creator.md +595 -0
  27. package/cli/agents/question-prefiller.md +269 -0
  28. package/cli/agents/refiner-epic.md +39 -0
  29. package/cli/agents/refiner-story.md +42 -0
  30. package/cli/agents/scaffolding-generator.md +99 -0
  31. package/cli/agents/seed-validator.md +71 -0
  32. package/cli/agents/story-doc-enricher.md +133 -0
  33. package/cli/agents/story-scope-reviewer.md +147 -0
  34. package/cli/agents/story-splitter.md +83 -0
  35. package/cli/agents/suggestion-business-analyst.md +88 -0
  36. package/cli/agents/suggestion-deployment-architect.md +263 -0
  37. package/cli/agents/suggestion-product-manager.md +129 -0
  38. package/cli/agents/suggestion-security-specialist.md +156 -0
  39. package/cli/agents/suggestion-technical-architect.md +269 -0
  40. package/cli/agents/suggestion-ux-researcher.md +93 -0
  41. package/cli/agents/task-subtask-decomposer.md +188 -0
  42. package/cli/agents/validator-documentation.json +183 -0
  43. package/cli/agents/validator-documentation.md +455 -0
  44. package/cli/agents/validator-selector.md +211 -0
  45. package/cli/ansi-colors.js +21 -0
  46. package/cli/api-reference-tool.js +368 -0
  47. package/cli/build-docs.js +29 -8
  48. package/cli/ceremony-history.js +369 -0
  49. package/cli/checks/catalog.json +76 -0
  50. package/cli/checks/code/quality.json +26 -0
  51. package/cli/checks/code/testing.json +14 -0
  52. package/cli/checks/code/traceability.json +26 -0
  53. package/cli/checks/cross-refs/epic.json +171 -0
  54. package/cli/checks/cross-refs/story.json +149 -0
  55. package/cli/checks/epic/api.json +114 -0
  56. package/cli/checks/epic/backend.json +126 -0
  57. package/cli/checks/epic/cloud.json +126 -0
  58. package/cli/checks/epic/data.json +102 -0
  59. package/cli/checks/epic/database.json +114 -0
  60. package/cli/checks/epic/developer.json +182 -0
  61. package/cli/checks/epic/devops.json +174 -0
  62. package/cli/checks/epic/frontend.json +162 -0
  63. package/cli/checks/epic/mobile.json +102 -0
  64. package/cli/checks/epic/qa.json +90 -0
  65. package/cli/checks/epic/security.json +184 -0
  66. package/cli/checks/epic/solution-architect.json +192 -0
  67. package/cli/checks/epic/test-architect.json +90 -0
  68. package/cli/checks/epic/ui.json +102 -0
  69. package/cli/checks/epic/ux.json +90 -0
  70. package/cli/checks/fixes/epic-fix-template.md +10 -0
  71. package/cli/checks/fixes/story-fix-template.md +10 -0
  72. package/cli/checks/story/api.json +186 -0
  73. package/cli/checks/story/backend.json +102 -0
  74. package/cli/checks/story/cloud.json +102 -0
  75. package/cli/checks/story/data.json +210 -0
  76. package/cli/checks/story/database.json +102 -0
  77. package/cli/checks/story/developer.json +168 -0
  78. package/cli/checks/story/devops.json +102 -0
  79. package/cli/checks/story/frontend.json +174 -0
  80. package/cli/checks/story/mobile.json +102 -0
  81. package/cli/checks/story/qa.json +210 -0
  82. package/cli/checks/story/security.json +198 -0
  83. package/cli/checks/story/solution-architect.json +230 -0
  84. package/cli/checks/story/test-architect.json +210 -0
  85. package/cli/checks/story/ui.json +102 -0
  86. package/cli/checks/story/ux.json +102 -0
  87. package/cli/coding-order.js +401 -0
  88. package/cli/command-logger.js +49 -12
  89. package/cli/components/static-output.js +63 -0
  90. package/cli/console-output-manager.js +94 -0
  91. package/cli/dependency-checker.js +72 -0
  92. package/cli/docs-sync.js +306 -0
  93. package/cli/epic-story-validator.js +659 -0
  94. package/cli/evaluation-prompts.js +1008 -0
  95. package/cli/execution-context.js +195 -0
  96. package/cli/generate-summary-table.js +340 -0
  97. package/cli/init-model-config.js +704 -0
  98. package/cli/init.js +1737 -278
  99. package/cli/kanban-server-manager.js +227 -0
  100. package/cli/llm-claude.js +150 -1
  101. package/cli/llm-gemini.js +109 -0
  102. package/cli/llm-local.js +493 -0
  103. package/cli/llm-mock.js +233 -0
  104. package/cli/llm-openai.js +454 -0
  105. package/cli/llm-provider.js +379 -3
  106. package/cli/llm-token-limits.js +211 -0
  107. package/cli/llm-verifier.js +662 -0
  108. package/cli/llm-xiaomi.js +143 -0
  109. package/cli/message-constants.js +49 -0
  110. package/cli/message-manager.js +334 -0
  111. package/cli/message-types.js +96 -0
  112. package/cli/messaging-api.js +291 -0
  113. package/cli/micro-check-fixer.js +335 -0
  114. package/cli/micro-check-runner.js +449 -0
  115. package/cli/micro-check-scorer.js +148 -0
  116. package/cli/micro-check-validator.js +538 -0
  117. package/cli/model-pricing.js +192 -0
  118. package/cli/model-query-engine.js +468 -0
  119. package/cli/model-recommendation-analyzer.js +495 -0
  120. package/cli/model-selector.js +270 -0
  121. package/cli/output-buffer.js +107 -0
  122. package/cli/process-manager.js +73 -2
  123. package/cli/prompt-logger.js +57 -0
  124. package/cli/repl-ink.js +4625 -1094
  125. package/cli/repl-old.js +3 -4
  126. package/cli/seed-processor.js +962 -0
  127. package/cli/sprint-planning-processor.js +4162 -0
  128. package/cli/template-processor.js +2149 -105
  129. package/cli/templates/project.md +25 -8
  130. package/cli/templates/vitepress-config.mts.template +5 -4
  131. package/cli/token-tracker.js +547 -0
  132. package/cli/tools/generate-story-validators.js +317 -0
  133. package/cli/tools/generate-validators.js +669 -0
  134. package/cli/update-checker.js +19 -17
  135. package/cli/update-notifier.js +4 -4
  136. package/cli/validation-router.js +667 -0
  137. package/cli/verification-tracker.js +563 -0
  138. package/cli/worktree-runner.js +654 -0
  139. package/kanban/README.md +386 -0
  140. package/kanban/client/README.md +205 -0
  141. package/kanban/client/components.json +20 -0
  142. package/kanban/client/dist/assets/index-D_KC5EQT.css +1 -0
  143. package/kanban/client/dist/assets/index-DjY5zqW7.js +351 -0
  144. package/kanban/client/dist/index.html +16 -0
  145. package/kanban/client/dist/vite.svg +1 -0
  146. package/kanban/client/index.html +15 -0
  147. package/kanban/client/package-lock.json +9442 -0
  148. package/kanban/client/package.json +44 -0
  149. package/kanban/client/postcss.config.js +6 -0
  150. package/kanban/client/public/vite.svg +1 -0
  151. package/kanban/client/src/App.jsx +651 -0
  152. package/kanban/client/src/components/ProjectFileEditorPopup.jsx +117 -0
  153. package/kanban/client/src/components/ceremony/AskArchPopup.jsx +420 -0
  154. package/kanban/client/src/components/ceremony/AskModelPopup.jsx +629 -0
  155. package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +1133 -0
  156. package/kanban/client/src/components/ceremony/EpicStorySelectionModal.jsx +254 -0
  157. package/kanban/client/src/components/ceremony/ProviderSwitcherButton.jsx +290 -0
  158. package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +686 -0
  159. package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +838 -0
  160. package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +150 -0
  161. package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +136 -0
  162. package/kanban/client/src/components/ceremony/steps/DatabaseStep.jsx +202 -0
  163. package/kanban/client/src/components/ceremony/steps/DeploymentStep.jsx +123 -0
  164. package/kanban/client/src/components/ceremony/steps/MissionStep.jsx +106 -0
  165. package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +329 -0
  166. package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +249 -0
  167. package/kanban/client/src/components/kanban/CardDetailModal.jsx +646 -0
  168. package/kanban/client/src/components/kanban/EpicSection.jsx +146 -0
  169. package/kanban/client/src/components/kanban/FilterToolbar.jsx +222 -0
  170. package/kanban/client/src/components/kanban/GroupingSelector.jsx +63 -0
  171. package/kanban/client/src/components/kanban/KanbanBoard.jsx +211 -0
  172. package/kanban/client/src/components/kanban/KanbanCard.jsx +147 -0
  173. package/kanban/client/src/components/kanban/KanbanColumn.jsx +90 -0
  174. package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +784 -0
  175. package/kanban/client/src/components/kanban/RunButton.jsx +162 -0
  176. package/kanban/client/src/components/kanban/SeedButton.jsx +176 -0
  177. package/kanban/client/src/components/layout/LoadingScreen.jsx +82 -0
  178. package/kanban/client/src/components/process/ProcessMonitorBar.jsx +80 -0
  179. package/kanban/client/src/components/settings/AgentEditorPopup.jsx +171 -0
  180. package/kanban/client/src/components/settings/AgentsTab.jsx +381 -0
  181. package/kanban/client/src/components/settings/ApiKeysTab.jsx +142 -0
  182. package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +105 -0
  183. package/kanban/client/src/components/settings/CheckEditorPopup.jsx +507 -0
  184. package/kanban/client/src/components/settings/CostThresholdsTab.jsx +95 -0
  185. package/kanban/client/src/components/settings/ModelPricingTab.jsx +269 -0
  186. package/kanban/client/src/components/settings/OpenAIAuthSection.jsx +412 -0
  187. package/kanban/client/src/components/settings/ServersTab.jsx +121 -0
  188. package/kanban/client/src/components/settings/SettingsModal.jsx +84 -0
  189. package/kanban/client/src/components/stats/CostModal.jsx +384 -0
  190. package/kanban/client/src/components/ui/badge.jsx +27 -0
  191. package/kanban/client/src/components/ui/dialog.jsx +121 -0
  192. package/kanban/client/src/components/ui/tabs.jsx +85 -0
  193. package/kanban/client/src/hooks/__tests__/useGrouping.test.js +232 -0
  194. package/kanban/client/src/hooks/useGrouping.js +177 -0
  195. package/kanban/client/src/hooks/useWebSocket.js +120 -0
  196. package/kanban/client/src/lib/__tests__/api.test.js +196 -0
  197. package/kanban/client/src/lib/__tests__/status-grouping.test.js +94 -0
  198. package/kanban/client/src/lib/api.js +515 -0
  199. package/kanban/client/src/lib/status-grouping.js +154 -0
  200. package/kanban/client/src/lib/utils.js +11 -0
  201. package/kanban/client/src/main.jsx +10 -0
  202. package/kanban/client/src/store/__tests__/kanbanStore.test.js +164 -0
  203. package/kanban/client/src/store/ceremonyStore.js +172 -0
  204. package/kanban/client/src/store/filterStore.js +201 -0
  205. package/kanban/client/src/store/kanbanStore.js +123 -0
  206. package/kanban/client/src/store/processStore.js +65 -0
  207. package/kanban/client/src/store/sprintPlanningStore.js +33 -0
  208. package/kanban/client/src/styles/globals.css +59 -0
  209. package/kanban/client/tailwind.config.js +77 -0
  210. package/kanban/client/vite.config.js +28 -0
  211. package/kanban/client/vitest.config.js +28 -0
  212. package/kanban/dev-start.sh +47 -0
  213. package/kanban/package.json +12 -0
  214. package/kanban/server/index.js +537 -0
  215. package/kanban/server/routes/ceremony.js +454 -0
  216. package/kanban/server/routes/costs.js +163 -0
  217. package/kanban/server/routes/openai-oauth.js +366 -0
  218. package/kanban/server/routes/processes.js +50 -0
  219. package/kanban/server/routes/settings.js +736 -0
  220. package/kanban/server/routes/websocket.js +281 -0
  221. package/kanban/server/routes/work-items.js +487 -0
  222. package/kanban/server/services/CeremonyService.js +1441 -0
  223. package/kanban/server/services/FileSystemScanner.js +95 -0
  224. package/kanban/server/services/FileWatcher.js +144 -0
  225. package/kanban/server/services/HierarchyBuilder.js +196 -0
  226. package/kanban/server/services/ProcessRegistry.js +122 -0
  227. package/kanban/server/services/TaskRunnerService.js +261 -0
  228. package/kanban/server/services/WorkItemReader.js +123 -0
  229. package/kanban/server/services/WorkItemRefineService.js +510 -0
  230. package/kanban/server/start.js +49 -0
  231. package/kanban/server/utils/kanban-logger.js +132 -0
  232. package/kanban/server/utils/markdown.js +91 -0
  233. package/kanban/server/utils/status-grouping.js +107 -0
  234. package/kanban/server/workers/run-task-worker.js +121 -0
  235. package/kanban/server/workers/seed-worker.js +94 -0
  236. package/kanban/server/workers/sponsor-call-worker.js +92 -0
  237. package/kanban/server/workers/sprint-planning-worker.js +212 -0
  238. package/package.json +19 -7
  239. package/cli/agents/documentation.md +0 -302
@@ -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
+ }
@@ -0,0 +1,50 @@
1
+ import express from 'express';
2
+
3
+ /**
4
+ * Processes Router
5
+ * REST endpoints for monitoring and controlling forked ceremony/CLI processes.
6
+ * @param {ProcessRegistry} registry
7
+ */
8
+ export function createProcessesRouter(registry) {
9
+ const router = express.Router();
10
+
11
+ // GET /api/processes — list all process DTOs (no logs)
12
+ router.get('/', (req, res) => {
13
+ res.json(registry.list());
14
+ });
15
+
16
+ // GET /api/processes/:id — single process DTO
17
+ router.get('/:id', (req, res) => {
18
+ const dto = registry.getDTO(req.params.id);
19
+ if (!dto) return res.status(404).json({ error: 'Process not found' });
20
+ res.json(dto);
21
+ });
22
+
23
+ // DELETE /api/processes — clear all completed/error/cancelled records
24
+ router.delete('/', (req, res) => {
25
+ registry.clearCompleted();
26
+ res.json({ ok: true });
27
+ });
28
+
29
+ // DELETE /api/processes/:id — kill running process or clear finished record
30
+ router.delete('/:id', (req, res) => {
31
+ const record = registry.get(req.params.id);
32
+ if (!record) return res.status(404).json({ error: 'Process not found' });
33
+ registry.kill(req.params.id);
34
+ res.json({ ok: true });
35
+ });
36
+
37
+ // POST /api/processes/:id/pause
38
+ router.post('/:id/pause', (req, res) => {
39
+ const ok = registry.pause(req.params.id);
40
+ res.json({ ok });
41
+ });
42
+
43
+ // POST /api/processes/:id/resume
44
+ router.post('/:id/resume', (req, res) => {
45
+ const ok = registry.resume(req.params.id);
46
+ res.json({ ok });
47
+ });
48
+
49
+ return router;
50
+ }