@aexol/spectral 0.9.230 → 0.9.231

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 (172) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/dist/designer/data/skills/blog-post.d.ts +3 -0
  3. package/dist/designer/data/skills/blog-post.d.ts.map +1 -0
  4. package/dist/designer/data/skills/blog-post.js +84 -0
  5. package/dist/designer/data/skills/blog-post.ts +87 -0
  6. package/dist/designer/data/skills/critique.d.ts +3 -0
  7. package/dist/designer/data/skills/critique.d.ts.map +1 -0
  8. package/dist/designer/data/skills/critique.js +109 -0
  9. package/dist/designer/data/skills/critique.ts +112 -0
  10. package/dist/designer/data/skills/dashboard.d.ts +3 -0
  11. package/dist/designer/data/skills/dashboard.d.ts.map +1 -0
  12. package/dist/designer/data/skills/dashboard.js +165 -0
  13. package/dist/designer/data/skills/dashboard.ts +168 -0
  14. package/dist/designer/data/skills/design-brief.d.ts +3 -0
  15. package/dist/designer/data/skills/design-brief.d.ts.map +1 -0
  16. package/dist/designer/data/skills/design-brief.js +141 -0
  17. package/dist/designer/data/skills/design-brief.ts +144 -0
  18. package/dist/designer/data/skills/design-skill.d.ts +10 -0
  19. package/dist/designer/data/skills/design-skill.d.ts.map +1 -0
  20. package/dist/designer/data/skills/design-skill.js +1 -0
  21. package/dist/designer/data/skills/design-skill.ts +9 -0
  22. package/dist/designer/data/skills/docs-page.d.ts +3 -0
  23. package/dist/designer/data/skills/docs-page.d.ts.map +1 -0
  24. package/dist/designer/data/skills/docs-page.js +68 -0
  25. package/dist/designer/data/skills/docs-page.ts +71 -0
  26. package/dist/designer/data/skills/index.d.ts +3 -0
  27. package/dist/designer/data/skills/index.d.ts.map +1 -0
  28. package/dist/designer/data/skills/index.js +28 -0
  29. package/dist/designer/data/skills/index.ts +30 -0
  30. package/dist/designer/data/skills/magazine-poster.d.ts +3 -0
  31. package/dist/designer/data/skills/magazine-poster.d.ts.map +1 -0
  32. package/dist/designer/data/skills/magazine-poster.js +70 -0
  33. package/dist/designer/data/skills/magazine-poster.ts +73 -0
  34. package/dist/designer/data/skills/mobile-app.d.ts +3 -0
  35. package/dist/designer/data/skills/mobile-app.d.ts.map +1 -0
  36. package/dist/designer/data/skills/mobile-app.js +229 -0
  37. package/dist/designer/data/skills/mobile-app.ts +232 -0
  38. package/dist/designer/data/skills/motion-frames.d.ts +3 -0
  39. package/dist/designer/data/skills/motion-frames.d.ts.map +1 -0
  40. package/dist/designer/data/skills/motion-frames.js +121 -0
  41. package/dist/designer/data/skills/motion-frames.ts +124 -0
  42. package/dist/designer/data/skills/pricing-page.d.ts +3 -0
  43. package/dist/designer/data/skills/pricing-page.d.ts.map +1 -0
  44. package/dist/designer/data/skills/pricing-page.js +76 -0
  45. package/dist/designer/data/skills/pricing-page.ts +79 -0
  46. package/dist/designer/data/skills/saas-landing.d.ts +3 -0
  47. package/dist/designer/data/skills/saas-landing.d.ts.map +1 -0
  48. package/dist/designer/data/skills/saas-landing.js +71 -0
  49. package/dist/designer/data/skills/saas-landing.ts +74 -0
  50. package/dist/designer/data/skills/slide-deck.d.ts +3 -0
  51. package/dist/designer/data/skills/slide-deck.d.ts.map +1 -0
  52. package/dist/designer/data/skills/slide-deck.js +213 -0
  53. package/dist/designer/data/skills/slide-deck.ts +216 -0
  54. package/dist/designer/data/skills/social-carousel.d.ts +3 -0
  55. package/dist/designer/data/skills/social-carousel.d.ts.map +1 -0
  56. package/dist/designer/data/skills/social-carousel.js +135 -0
  57. package/dist/designer/data/skills/social-carousel.ts +138 -0
  58. package/dist/designer/data/skills/web-prototype.d.ts +3 -0
  59. package/dist/designer/data/skills/web-prototype.d.ts.map +1 -0
  60. package/dist/designer/data/skills/web-prototype.js +159 -0
  61. package/dist/designer/data/skills/web-prototype.ts +162 -0
  62. package/dist/designer/skills.d.ts +4 -10
  63. package/dist/designer/skills.d.ts.map +1 -1
  64. package/dist/designer/skills.js +2 -1643
  65. package/dist/extensions/wizard/index.js +9 -1
  66. package/dist/generated/parser/ast/nodes.d.ts +115 -0
  67. package/dist/generated/parser/ast/nodes.d.ts.map +1 -0
  68. package/dist/generated/parser/ast/nodes.js +8 -0
  69. package/dist/generated/parser/lexer/lexer.d.ts +28 -0
  70. package/dist/generated/parser/lexer/lexer.d.ts.map +1 -0
  71. package/dist/generated/parser/{lexer.js → lexer/lexer.js} +1 -180
  72. package/dist/generated/parser/{lexer.d.ts → lexer/tokens.d.ts} +5 -24
  73. package/dist/generated/parser/lexer/tokens.d.ts.map +1 -0
  74. package/dist/generated/parser/lexer/tokens.js +187 -0
  75. package/dist/generated/parser/mod.d.ts +5 -7
  76. package/dist/generated/parser/mod.d.ts.map +1 -1
  77. package/dist/generated/parser/mod.js +2 -1
  78. package/dist/generated/parser/parser/cursor.d.ts +47 -0
  79. package/dist/generated/parser/parser/cursor.d.ts.map +1 -0
  80. package/dist/generated/parser/parser/cursor.js +12 -0
  81. package/dist/generated/parser/parser/definitions.d.ts +16 -0
  82. package/dist/generated/parser/parser/definitions.d.ts.map +1 -0
  83. package/dist/generated/parser/parser/definitions.js +353 -0
  84. package/dist/generated/parser/parser/expressions.d.ts +17 -0
  85. package/dist/generated/parser/parser/expressions.d.ts.map +1 -0
  86. package/dist/generated/parser/parser/expressions.js +165 -0
  87. package/dist/generated/parser/parser.d.ts +18 -133
  88. package/dist/generated/parser/parser.d.ts.map +1 -1
  89. package/dist/generated/parser/parser.js +9 -505
  90. package/dist/generated/parser/tokenizer.d.ts +1 -2
  91. package/dist/generated/parser/tokenizer.d.ts.map +1 -1
  92. package/dist/generated/parser/tokenizer.js +2 -1
  93. package/dist/generated/parser/validator.d.ts +1 -1
  94. package/dist/generated/parser/validator.d.ts.map +1 -1
  95. package/dist/mcp/glimpse-ui.js +9 -1
  96. package/dist/relay/dispatcher.d.ts +11 -104
  97. package/dist/relay/dispatcher.d.ts.map +1 -1
  98. package/dist/relay/dispatcher.js +10 -1130
  99. package/dist/relay/rest.d.ts +31 -0
  100. package/dist/relay/rest.d.ts.map +1 -0
  101. package/dist/relay/rest.js +85 -0
  102. package/dist/relay/routing.d.ts +109 -0
  103. package/dist/relay/routing.d.ts.map +1 -0
  104. package/dist/relay/routing.js +669 -0
  105. package/dist/relay/wizard-login.d.ts +17 -0
  106. package/dist/relay/wizard-login.d.ts.map +1 -0
  107. package/dist/relay/wizard-login.js +419 -0
  108. package/dist/sdk/agent-core/agent-loop.d.ts +2 -9
  109. package/dist/sdk/agent-core/agent-loop.d.ts.map +1 -1
  110. package/dist/sdk/agent-core/agent-loop.js +4 -288
  111. package/dist/sdk/agent-core/tool-execution.d.ts +33 -0
  112. package/dist/sdk/agent-core/tool-execution.d.ts.map +1 -0
  113. package/dist/sdk/agent-core/tool-execution.js +300 -0
  114. package/dist/sdk/ai/env-api-keys.js +9 -1
  115. package/dist/sdk/ai/providers/openai/reasoning-replay.d.ts +48 -0
  116. package/dist/sdk/ai/providers/openai/reasoning-replay.d.ts.map +1 -0
  117. package/dist/sdk/ai/providers/openai/reasoning-replay.js +209 -0
  118. package/dist/sdk/ai/providers/openai-completions.d.ts.map +1 -1
  119. package/dist/sdk/ai/providers/openai-completions.js +1 -203
  120. package/dist/sdk/coding-agent/core/compaction/compaction.d.ts +6 -118
  121. package/dist/sdk/coding-agent/core/compaction/compaction.d.ts.map +1 -1
  122. package/dist/sdk/coding-agent/core/compaction/compaction.js +11 -733
  123. package/dist/sdk/coding-agent/core/compaction/cut-points.d.ts +58 -0
  124. package/dist/sdk/coding-agent/core/compaction/cut-points.d.ts.map +1 -0
  125. package/dist/sdk/coding-agent/core/compaction/cut-points.js +191 -0
  126. package/dist/sdk/coding-agent/core/compaction/dedupe.d.ts +28 -0
  127. package/dist/sdk/coding-agent/core/compaction/dedupe.d.ts.map +1 -0
  128. package/dist/sdk/coding-agent/core/compaction/dedupe.js +156 -0
  129. package/dist/sdk/coding-agent/core/compaction/summarize.d.ts +21 -0
  130. package/dist/sdk/coding-agent/core/compaction/summarize.d.ts.map +1 -0
  131. package/dist/sdk/coding-agent/core/compaction/summarize.js +181 -0
  132. package/dist/sdk/coding-agent/core/compaction/tokens.d.ts +47 -0
  133. package/dist/sdk/coding-agent/core/compaction/tokens.d.ts.map +1 -0
  134. package/dist/sdk/coding-agent/core/compaction/tokens.js +224 -0
  135. package/dist/sdk/coding-agent/core/extensions/session-events.d.ts +157 -0
  136. package/dist/sdk/coding-agent/core/extensions/session-events.d.ts.map +1 -0
  137. package/dist/sdk/coding-agent/core/extensions/session-events.js +13 -0
  138. package/dist/sdk/coding-agent/core/extensions/types.d.ts +6 -365
  139. package/dist/sdk/coding-agent/core/extensions/types.d.ts.map +1 -1
  140. package/dist/sdk/coding-agent/core/extensions/types.js +2 -10
  141. package/dist/sdk/coding-agent/core/extensions/ui-types.d.ts +219 -0
  142. package/dist/sdk/coding-agent/core/extensions/ui-types.d.ts.map +1 -0
  143. package/dist/sdk/coding-agent/core/extensions/ui-types.js +4 -0
  144. package/dist/sdk/coding-agent/core/package/default-package-manager.d.ts +115 -0
  145. package/dist/sdk/coding-agent/core/package/default-package-manager.d.ts.map +1 -0
  146. package/dist/sdk/coding-agent/core/package/default-package-manager.js +1184 -0
  147. package/dist/sdk/coding-agent/core/package/discovery.d.ts +20 -0
  148. package/dist/sdk/coding-agent/core/package/discovery.d.ts.map +1 -0
  149. package/dist/sdk/coding-agent/core/package/discovery.js +283 -0
  150. package/dist/sdk/coding-agent/core/package/ignore.d.ts +29 -0
  151. package/dist/sdk/coding-agent/core/package/ignore.d.ts.map +1 -0
  152. package/dist/sdk/coding-agent/core/package/ignore.js +235 -0
  153. package/dist/sdk/coding-agent/core/package/types.d.ts +128 -0
  154. package/dist/sdk/coding-agent/core/package/types.d.ts.map +1 -0
  155. package/dist/sdk/coding-agent/core/package/types.js +18 -0
  156. package/dist/sdk/coding-agent/core/package/update-check.d.ts +43 -0
  157. package/dist/sdk/coding-agent/core/package/update-check.d.ts.map +1 -0
  158. package/dist/sdk/coding-agent/core/package/update-check.js +253 -0
  159. package/dist/sdk/coding-agent/core/package-manager.d.ts +2 -188
  160. package/dist/sdk/coding-agent/core/package-manager.d.ts.map +1 -1
  161. package/dist/sdk/coding-agent/core/package-manager.js +1 -1940
  162. package/dist/sdk/coding-agent/core/session/migrate.d.ts +18 -0
  163. package/dist/sdk/coding-agent/core/session/migrate.d.ts.map +1 -0
  164. package/dist/sdk/coding-agent/core/session/migrate.js +78 -0
  165. package/dist/sdk/coding-agent/core/session/types.d.ts +165 -0
  166. package/dist/sdk/coding-agent/core/session/types.d.ts.map +1 -0
  167. package/dist/sdk/coding-agent/core/session/types.js +4 -0
  168. package/dist/sdk/coding-agent/core/session-manager.d.ts +3 -161
  169. package/dist/sdk/coding-agent/core/session-manager.d.ts.map +1 -1
  170. package/dist/sdk/coding-agent/core/session-manager.js +4 -72
  171. package/package.json +2 -2
  172. package/dist/generated/parser/lexer.d.ts.map +0 -1
@@ -37,1145 +37,25 @@
37
37
  * - Backend cancels pending REST requests with `code:"timeout"` (30 s)
38
38
  * or `code:"machine_offline"` (CLI socket dropped). Both are
39
39
  * transparent to this layer — we just respond when we can.
40
+ *
41
+ * Path matching + the REST request handler + the wizard login flow live in
42
+ * sibling modules (`routing.ts`, `rest.ts`, `wizard-login.ts`) and are
43
+ * re-exported below so existing imports from `relay/dispatcher.js` keep
44
+ * working.
40
45
  */
41
- import { spawn } from "node:child_process";
42
- import { existsSync, readFileSync } from "node:fs";
43
- import { createRequire } from "node:module";
44
- import { homedir } from "node:os";
45
- import { dirname, isAbsolute, join } from "node:path";
46
- import { BadRequestError, NotFoundError } from "../server/handlers/errors.js";
47
- import { handleListMcpStatus } from "../server/handlers/mcp-status.js";
48
- import { handleReloadMcpConfig } from "../server/handlers/mcp-reload.js";
49
- import { handlePathAutocomplete } from "../server/handlers/paths-autocomplete.js";
50
- import { handleGetHomeDirectory } from "../server/handlers/paths-home.js";
51
- import { handleListDirectory } from "../server/handlers/paths-list.js";
52
- import { handleSearchProjectFiles } from "../server/handlers/files-search.js";
53
- import { handleBindStudioProject, handleUnbindStudioProject, handleCleanupProjectSessions, handleCreateProject, handleDeleteProject, handleGetProjectObservations, handleListProjectsAsync, handleListSessionsByProjectAsync, handleUpdateProject, } from "../server/handlers/projects.js";
54
- import { handleCompactSession, handleCleanupAllSessions, handleCreateSession, handleDeleteSession, handleForkSession, handleRememberAndDeleteSession, handleGetSessionDetailAsync, handleGetSessionMessagesAsync, handleGetSessionMemoryDetails, handleGetSessionMemoryStatus, handleUpdateSession, handleGetSessionActiveAgentAsync, handleUpdateSessionActiveAgentAsync, } from "../server/handlers/sessions.js";
55
- import { handleClearPromptQueue, handleEnqueuePrompt, handleGetPromptQueue, handleRemovePrompt, } from "../server/handlers/queue.js";
56
- import { handleGetAgentSettingsAsync, handleListAgents, handlePutAgentSettingsAsync, } from "../server/handlers/agent-settings.js";
57
- import { handleGetSettingsAsync, handlePutSettingsAsync, } from "../server/handlers/settings.js";
58
- import { handleGetPromptMutationSettings, handlePutPromptMutationSettings, } from "../server/handlers/prompt-mutation-settings.js";
59
46
  import { shutdownState } from "../server/shutdown.js";
60
47
  import { coerceImages, hasImages, MAX_INLINE_IMAGE_BASE64_CHARS, } from "../server/image-attachments.js";
61
48
  import { humanizeProviderError } from "../server/error-humanizer.js";
62
- import { handleKillDevProcess, handleKillDetectedDevServer, handleListDevProcesses, handleRestartDevProcess, handleScanDevServers, handleStartDevProcess, } from "../server/handlers/dev-processes.js";
63
49
  import { handleAutoResearch } from "./auto-research.js";
64
50
  import { handleAutoOptimize } from "./auto-optimizer.js";
65
51
  import { chunkHistory } from "./history-chunker.js";
66
52
  import { gzipEncodeMessages, shouldCompressHistory } from "./history-compression.js";
67
53
  import { projectHistoryForReplay } from "./history-projection.js";
68
- /**
69
- * Inline path matcher. Returns `null` for any path/method combination we
70
- * don't recognise; the caller turns that into a `404 Unknown route`.
71
- *
72
- * Intentionally literal — a regex table would be marginally fancier but
73
- * also marginally slower and harder to read for ~11 routes.
74
- */
75
- export function matchRoute(method, path) {
76
- // Strip query string for path matching but keep it for the handler.
77
- const qIdx = path.indexOf("?");
78
- const cleanPath = qIdx === -1 ? path : path.slice(0, qIdx);
79
- const query = qIdx >= 0 ? new URLSearchParams(path.slice(qIdx + 1)) : undefined;
80
- // /api/mcp-status
81
- if (cleanPath === "/api/mcp-status") {
82
- if (method === "GET")
83
- return { route: "list_mcp_status" };
84
- return null;
85
- }
86
- if (cleanPath === "/api/mcp-reload") {
87
- if (method === "POST")
88
- return { route: "reload_mcp_config" };
89
- return null;
90
- }
91
- if (cleanPath === "/api/native-extensions/refresh") {
92
- if (method === "POST")
93
- return { route: "refresh_native_extensions" };
94
- return null;
95
- }
96
- if (cleanPath === "/api/cloud-agents/refresh") {
97
- if (method === "POST")
98
- return { route: "refresh_cloud_agents" };
99
- return null;
100
- }
101
- if (cleanPath === "/api/wizard/login") {
102
- if (method === "POST")
103
- return { route: "wizard_login" };
104
- return null;
105
- }
106
- // /api/settings
107
- if (cleanPath === "/api/settings") {
108
- if (method === "GET")
109
- return { route: "get_settings" };
110
- if (method === "PUT")
111
- return { route: "put_settings" };
112
- return null;
113
- }
114
- // /api/agent-settings
115
- if (cleanPath === "/api/agent-settings") {
116
- if (method === "GET")
117
- return { route: "get_agent_settings" };
118
- if (method === "PUT")
119
- return { route: "put_agent_settings" };
120
- return null;
121
- }
122
- if (cleanPath === "/api/prompt-mutation-settings") {
123
- if (method === "GET")
124
- return { route: "get_prompt_mutation_settings" };
125
- if (method === "PUT")
126
- return { route: "put_prompt_mutation_settings" };
127
- return null;
128
- }
129
- // /api/agents
130
- if (cleanPath === "/api/agents") {
131
- if (method === "GET")
132
- return { route: "list_agents", query };
133
- return null;
134
- }
135
- // /api/paths/autocomplete
136
- if (cleanPath === "/api/paths/autocomplete") {
137
- if (method === "GET")
138
- return { route: "list_path_autocomplete", query };
139
- return null;
140
- }
141
- // /api/paths/list
142
- if (cleanPath === "/api/paths/list") {
143
- if (method === "POST")
144
- return { route: "list_directory" };
145
- return null;
146
- }
147
- // /api/paths/home
148
- if (cleanPath === "/api/paths/home") {
149
- if (method === "POST")
150
- return { route: "get_home_directory" };
151
- return null;
152
- }
153
- // /api/projects
154
- if (cleanPath === "/api/projects") {
155
- if (method === "GET")
156
- return { route: "list_projects" };
157
- if (method === "POST")
158
- return { route: "create_project" };
159
- return null;
160
- }
161
- // /api/sessions
162
- if (cleanPath === "/api/sessions") {
163
- if (method === "POST")
164
- return { route: "create_session" };
165
- return null;
166
- }
167
- // /api/projects/:id/files/search
168
- const projectFilesMatch = /^\/api\/projects\/([^/]+)\/files\/search$/.exec(cleanPath);
169
- if (projectFilesMatch) {
170
- const id = decodeURIComponent(projectFilesMatch[1]);
171
- if (method === "GET")
172
- return { route: "search_project_files", id, query };
173
- return null;
174
- }
175
- // /api/projects/:id/bind-studio and /api/projects/:id/unbind-studio
176
- const bindStudioMatch = /^\/api\/projects\/([^/]+)\/bind-studio$/.exec(cleanPath);
177
- if (bindStudioMatch) {
178
- const id = decodeURIComponent(bindStudioMatch[1]);
179
- if (method === "POST")
180
- return { route: "bind_studio", id };
181
- return null;
182
- }
183
- const unbindStudioMatch = /^\/api\/projects\/([^/]+)\/unbind-studio$/.exec(cleanPath);
184
- if (unbindStudioMatch) {
185
- const id = decodeURIComponent(unbindStudioMatch[1]);
186
- if (method === "POST")
187
- return { route: "unbind_studio", id };
188
- return null;
189
- }
190
- // /api/projects/:id/observations
191
- const projectObsMatch = /^\/api\/projects\/([^/]+)\/observations$/.exec(cleanPath);
192
- if (projectObsMatch) {
193
- const id = decodeURIComponent(projectObsMatch[1]);
194
- if (method === "GET")
195
- return { route: "get_project_observations", id };
196
- return null;
197
- }
198
- // /api/projects/:id and /api/projects/:id/sessions
199
- const projectMatch = /^\/api\/projects\/([^/]+)(\/sessions)?$/.exec(cleanPath);
200
- if (projectMatch) {
201
- const id = decodeURIComponent(projectMatch[1]);
202
- const isSessions = projectMatch[2] === "/sessions";
203
- if (isSessions) {
204
- if (method === "GET")
205
- return { route: "list_project_sessions", id };
206
- return null;
207
- }
208
- if (method === "PATCH")
209
- return { route: "update_project", id };
210
- if (method === "DELETE")
211
- return { route: "delete_project", id };
212
- return null;
213
- }
214
- // /api/projects/:id/cleanup-sessions
215
- const projectCleanupMatch = /^\/api\/projects\/([^/]+)\/cleanup-sessions$/.exec(cleanPath);
216
- if (projectCleanupMatch) {
217
- const id = decodeURIComponent(projectCleanupMatch[1]);
218
- if (method === "POST")
219
- return { route: "cleanup_project_sessions", id };
220
- return null;
221
- }
222
- // /api/sessions/cleanup (must precede the /api/sessions/:id matches —
223
- // "cleanup" would otherwise be captured as an :id)
224
- if (cleanPath === "/api/sessions/cleanup") {
225
- if (method === "POST")
226
- return { route: "cleanup_all_sessions" };
227
- return null;
228
- }
229
- // /api/sessions/:id
230
- const sessionMessagesMatch = /^\/api\/sessions\/([^/]+)\/messages$/.exec(cleanPath);
231
- if (sessionMessagesMatch) {
232
- const id = decodeURIComponent(sessionMessagesMatch[1]);
233
- if (method === "GET")
234
- return { route: "get_session_messages", id, query };
235
- return null;
236
- }
237
- const sessionMatch = /^\/api\/sessions\/([^/]+)$/.exec(cleanPath);
238
- if (sessionMatch) {
239
- const id = decodeURIComponent(sessionMatch[1]);
240
- if (method === "GET")
241
- return { route: "get_session", id };
242
- if (method === "PATCH")
243
- return { route: "update_session", id };
244
- if (method === "DELETE")
245
- return { route: "delete_session", id };
246
- return null;
247
- }
248
- const sessionAgentMatch = /^\/api\/sessions\/([^/]+)\/active-agent$/.exec(cleanPath);
249
- if (sessionAgentMatch) {
250
- const id = decodeURIComponent(sessionAgentMatch[1]);
251
- if (method === "GET")
252
- return { route: "get_session_active_agent", id };
253
- if (method === "PUT")
254
- return { route: "update_session_active_agent", id };
255
- return null;
256
- }
257
- // /api/sessions/:id/memory/details and /api/sessions/:id/memory and /api/sessions/:id/compact
258
- const sessionActionMatch = /^\/api\/sessions\/([^/]+)\/(memory(?:\/details)?|compact)$/.exec(cleanPath);
259
- if (sessionActionMatch) {
260
- const id = decodeURIComponent(sessionActionMatch[1]);
261
- const action = sessionActionMatch[2];
262
- if (action === "memory" && method === "GET")
263
- return { route: "get_session_memory", id };
264
- if (action === "memory/details" && method === "GET")
265
- return { route: "get_session_memory_details", id };
266
- if (action === "compact" && method === "POST")
267
- return { route: "compact_session", id };
268
- return null;
269
- }
270
- // /api/sessions/:id/remember-and-delete
271
- const rememberDeleteMatch = /^\/api\/sessions\/([^/]+)\/remember-and-delete$/.exec(cleanPath);
272
- if (rememberDeleteMatch) {
273
- const id = decodeURIComponent(rememberDeleteMatch[1]);
274
- if (method === "POST")
275
- return { route: "remember_and_delete_session", id };
276
- return null;
277
- }
278
- // /api/sessions/:id/fork
279
- const forkMatch = /^\/api\/sessions\/([^/]+)\/fork$/.exec(cleanPath);
280
- if (forkMatch) {
281
- const id = decodeURIComponent(forkMatch[1]);
282
- if (method === "POST")
283
- return { route: "fork_session", id };
284
- return null;
285
- }
286
- // /api/sessions/:id/queue and /api/sessions/:id/queue/:itemId
287
- const queueMatch = /^\/api\/sessions\/([^/]+)\/queue(\/([^/]+))?$/.exec(cleanPath);
288
- if (queueMatch) {
289
- const id = decodeURIComponent(queueMatch[1]);
290
- const itemId = queueMatch[3] ? decodeURIComponent(queueMatch[3]) : undefined;
291
- if (!itemId) {
292
- if (method === "GET")
293
- return { route: "get_prompt_queue", id };
294
- if (method === "POST")
295
- return { route: "enqueue_prompt", id };
296
- if (method === "DELETE")
297
- return { route: "clear_prompt_queue", id };
298
- }
299
- else {
300
- if (method === "DELETE")
301
- return { route: "remove_prompt", id, itemId };
302
- }
303
- return null;
304
- }
305
- // /api/dev-processes and /api/dev-processes/:id
306
- if (cleanPath === "/api/dev-processes") {
307
- if (method === "GET")
308
- return { route: "list_dev_processes" };
309
- if (method === "POST")
310
- return { route: "start_dev_process" };
311
- return null;
312
- }
313
- const restartDevProcessMatch = /^\/api\/dev-processes\/([^/]+)\/restart$/.exec(cleanPath);
314
- if (restartDevProcessMatch) {
315
- const id = decodeURIComponent(restartDevProcessMatch[1]);
316
- if (method === "POST")
317
- return { route: "restart_dev_process", id };
318
- return null;
319
- }
320
- const devProcessMatch = /^\/api\/dev-processes\/([^/]+)$/.exec(cleanPath);
321
- if (devProcessMatch) {
322
- const id = decodeURIComponent(devProcessMatch[1]);
323
- if (method === "DELETE")
324
- return { route: "kill_dev_process", id };
325
- return null;
326
- }
327
- // /api/dev-servers and /api/dev-servers/:pid
328
- if (cleanPath === "/api/dev-servers") {
329
- if (method === "GET")
330
- return { route: "scan_dev_servers" };
331
- return null;
332
- }
333
- const devServerMatch = /^\/api\/dev-servers\/([^/]+)$/.exec(cleanPath);
334
- if (devServerMatch) {
335
- const id = decodeURIComponent(devServerMatch[1]);
336
- if (method === "DELETE") {
337
- const rawPort = query?.get("port");
338
- const parsedPort = rawPort === null ? Number.NaN : Number(rawPort);
339
- const port = Number.isFinite(parsedPort) ? parsedPort : undefined;
340
- return port === undefined
341
- ? { route: "kill_dev_server", id }
342
- : { route: "kill_dev_server", id, port };
343
- }
344
- return null;
345
- }
346
- return null;
347
- }
348
- const WIZARD_PACKAGE_NAME = "@aexol/wizard-spectral";
349
- const WIZARD_CLI_BIN_NAME = "spectral-wizard";
350
- const WIZARD_LOGIN_STARTUP_TIMEOUT_MS = 5_000;
351
- const requireForWizard = createRequire(import.meta.url);
352
- let activeWizardLoginRun = null;
353
- let activeWizardLoginStartPromise = null;
354
- function getActiveWizardLoginRun() {
355
- const run = activeWizardLoginRun;
356
- if (!run)
357
- return null;
358
- if (run.child.exitCode !== null || run.child.killed) {
359
- activeWizardLoginRun = null;
360
- return null;
361
- }
362
- return run;
363
- }
364
- function toSafeErrorMessage(error) {
365
- if (error instanceof Error)
366
- return error.message;
367
- if (typeof error === "string")
368
- return error;
369
- return "Unknown error";
370
- }
371
- function asRecord(value) {
372
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
373
- return null;
374
- }
375
- return value;
376
- }
377
- function findWizardPackageJsonFromEntry(entryPath) {
378
- let current = dirname(entryPath);
379
- for (let depth = 0; depth < 8; depth += 1) {
380
- const candidate = join(current, "package.json");
381
- if (existsSync(candidate)) {
382
- try {
383
- const packageJson = JSON.parse(readFileSync(candidate, "utf8"));
384
- if (asRecord(packageJson)?.name === WIZARD_PACKAGE_NAME)
385
- return candidate;
386
- }
387
- catch {
388
- // Keep walking upward; malformed metadata is handled by the final reader.
389
- }
390
- }
391
- const parent = dirname(current);
392
- if (parent === current)
393
- break;
394
- current = parent;
395
- }
396
- return undefined;
397
- }
398
- function resolveWizardPackageJsonPath() {
399
- try {
400
- return { ok: true, path: requireForWizard.resolve(`${WIZARD_PACKAGE_NAME}/package.json`) };
401
- }
402
- catch (error) {
403
- try {
404
- const entryPath = requireForWizard.resolve(WIZARD_PACKAGE_NAME);
405
- const packageJsonPath = findWizardPackageJsonFromEntry(entryPath);
406
- if (packageJsonPath)
407
- return { ok: true, path: packageJsonPath };
408
- }
409
- catch {
410
- // Fall through to the original package-unavailable message.
411
- }
412
- return {
413
- ok: false,
414
- message: `${WIZARD_PACKAGE_NAME} is not available on this machine: ${toSafeErrorMessage(error)}`,
415
- };
416
- }
417
- }
418
- function readWizardPackageJson(packageJsonPath) {
419
- try {
420
- return { ok: true, json: JSON.parse(readFileSync(packageJsonPath, "utf8")) };
421
- }
422
- catch (error) {
423
- return {
424
- ok: false,
425
- message: `Could not read ${WIZARD_PACKAGE_NAME} package metadata: ${toSafeErrorMessage(error)}`,
426
- };
427
- }
428
- }
429
- function getWizardBinRelativePath(packageJson) {
430
- const pkg = asRecord(packageJson);
431
- const bin = pkg?.bin;
432
- if (typeof bin === "string" && bin.trim())
433
- return bin;
434
- const binRecord = asRecord(bin);
435
- const namedBin = binRecord?.[WIZARD_CLI_BIN_NAME];
436
- return typeof namedBin === "string" && namedBin.trim() ? namedBin : undefined;
437
- }
438
- function findWindowsBinShim(packageRoot) {
439
- let current = packageRoot;
440
- for (let depth = 0; depth < 8; depth += 1) {
441
- const candidate = join(current, "node_modules", ".bin", `${WIZARD_CLI_BIN_NAME}.cmd`);
442
- if (existsSync(candidate))
443
- return candidate;
444
- const parent = dirname(current);
445
- if (parent === current)
446
- break;
447
- current = parent;
448
- }
449
- return undefined;
450
- }
451
- function resolveWizardBinCommand() {
452
- const packageJsonPath = resolveWizardPackageJsonPath();
453
- if (!packageJsonPath.ok)
454
- return packageJsonPath;
455
- const packageJson = readWizardPackageJson(packageJsonPath.path);
456
- if (!packageJson.ok)
457
- return packageJson;
458
- const binRelativePath = getWizardBinRelativePath(packageJson.json);
459
- if (!binRelativePath) {
460
- return {
461
- ok: false,
462
- message: `${WIZARD_PACKAGE_NAME} is installed but does not expose a ${WIZARD_CLI_BIN_NAME} bin.`,
463
- };
464
- }
465
- const packageRoot = dirname(packageJsonPath.path);
466
- const packageBinPath = isAbsolute(binRelativePath)
467
- ? binRelativePath
468
- : join(packageRoot, binRelativePath);
469
- if (process.platform === "win32") {
470
- const cmdShim = findWindowsBinShim(packageRoot);
471
- if (cmdShim) {
472
- return {
473
- ok: true,
474
- bin: { command: cmdShim, argsPrefix: [] },
475
- };
476
- }
477
- }
478
- return {
479
- ok: true,
480
- bin: { command: packageBinPath, argsPrefix: [] },
481
- };
482
- }
483
- function collectWizardTools(value) {
484
- if (Array.isArray(value))
485
- return value;
486
- const record = asRecord(value);
487
- const tools = record?.tools;
488
- return Array.isArray(tools) ? tools : [];
489
- }
490
- function findWizardStatusTool(tools) {
491
- for (const tool of tools) {
492
- const record = asRecord(tool);
493
- if (record?.name !== "wizard_status" || typeof record.execute !== "function") {
494
- continue;
495
- }
496
- return {
497
- execute: record.execute,
498
- };
499
- }
500
- return null;
501
- }
502
- function normalizeWizardAuthStatus(value) {
503
- const normalized = value.trim().toLowerCase().replace(/[\s-]+/g, "_");
504
- if (normalized === "configured" ||
505
- normalized === "authenticated" ||
506
- normalized === "ready" ||
507
- normalized === "ok" ||
508
- normalized === "connected") {
509
- return "configured";
510
- }
511
- if (normalized === "missing_auth" ||
512
- normalized === "auth_missing" ||
513
- normalized === "needs_auth" ||
514
- normalized === "not_authenticated" ||
515
- normalized === "unauthenticated" ||
516
- normalized === "not_configured") {
517
- return "missing_auth";
518
- }
519
- return "unknown";
520
- }
521
- function parseWizardAuthStatusFromObject(record) {
522
- for (const key of ["status", "authStatus", "state"]) {
523
- const raw = record[key];
524
- if (typeof raw !== "string")
525
- continue;
526
- const status = normalizeWizardAuthStatus(raw);
527
- if (status !== "unknown")
528
- return status;
529
- }
530
- for (const key of ["configured", "authenticated", "isAuthenticated", "hasAuth"]) {
531
- const raw = record[key];
532
- if (typeof raw === "boolean")
533
- return raw ? "configured" : "missing_auth";
534
- }
535
- return "unknown";
536
- }
537
- function parseWizardAuthStatus(statusOutput) {
538
- const candidates = [statusOutput];
539
- const outputRecord = asRecord(statusOutput);
540
- if (outputRecord) {
541
- candidates.push(outputRecord.details, outputRecord.data, outputRecord.result);
542
- }
543
- for (const candidate of candidates) {
544
- const record = asRecord(candidate);
545
- if (!record)
546
- continue;
547
- const status = parseWizardAuthStatusFromObject(record);
548
- if (status !== "unknown")
549
- return status;
550
- }
551
- return "unknown";
552
- }
553
- async function checkWizardAuthStatus(directory) {
554
- let importedModule;
555
- try {
556
- importedModule = await import(WIZARD_PACKAGE_NAME);
557
- }
558
- catch (error) {
559
- return {
560
- status: "package_unavailable",
561
- message: `${WIZARD_PACKAGE_NAME} is not available on this machine: ${toSafeErrorMessage(error)}`,
562
- };
563
- }
564
- const buildSpectralTools = asRecord(importedModule)?.buildSpectralTools;
565
- if (typeof buildSpectralTools !== "function") {
566
- return {
567
- status: "unknown",
568
- message: "Wizard status API is unavailable; starting login.",
569
- };
570
- }
571
- try {
572
- const tools = collectWizardTools(await buildSpectralTools());
573
- const statusTool = findWizardStatusTool(tools);
574
- if (!statusTool) {
575
- return {
576
- status: "unknown",
577
- message: "Wizard status tool is unavailable; starting login.",
578
- };
579
- }
580
- const status = parseWizardAuthStatus(await statusTool.execute("wizard-ui-login-status", { directory }));
581
- return {
582
- status,
583
- message: status === "configured"
584
- ? "Wizard is already authenticated."
585
- : "Wizard authentication is missing; starting login.",
586
- };
587
- }
588
- catch (error) {
589
- return {
590
- status: "unknown",
591
- message: `Could not check Wizard auth status; starting login: ${toSafeErrorMessage(error)}`,
592
- };
593
- }
594
- }
595
- function wizardLoginResult(status, message, run) {
596
- return {
597
- status,
598
- packageName: WIZARD_PACKAGE_NAME,
599
- message,
600
- loginUrl: run?.loginUrl,
601
- startedAt: run?.startedAt,
602
- };
603
- }
604
- function extractLoginUrl(text) {
605
- const matches = text.match(/https?:\/\/[^\s"'<>]+/g) ?? [];
606
- for (const raw of matches) {
607
- const candidate = raw.replace(/[),.;]+$/, "");
608
- if (isSafeLoginUrl(candidate))
609
- return candidate;
610
- }
611
- return undefined;
612
- }
613
- function isSafeLoginUrl(value) {
614
- try {
615
- const url = new URL(value);
616
- if (url.protocol !== "https:" && url.protocol !== "http:")
617
- return false;
618
- const forbiddenQueryKeys = new Set([
619
- "access_token",
620
- "id_token",
621
- "refresh_token",
622
- "token",
623
- "jwt",
624
- "code",
625
- ]);
626
- for (const key of url.searchParams.keys()) {
627
- if (forbiddenQueryKeys.has(key.toLowerCase()))
628
- return false;
629
- }
630
- if (/(^|[#&])(access_token|id_token|refresh_token|token|jwt|code)=/i.test(url.hash)) {
631
- return false;
632
- }
633
- return true;
634
- }
635
- catch {
636
- return false;
637
- }
638
- }
639
- export async function startWizardLogin(directory = homedir()) {
640
- const existing = getActiveWizardLoginRun();
641
- if (existing) {
642
- return wizardLoginResult("already_running", existing.loginUrl
643
- ? "Wizard login is already running. Complete the browser flow."
644
- : "Wizard login is already running. Complete the browser flow if it has opened.", existing);
645
- }
646
- if (activeWizardLoginStartPromise)
647
- return await activeWizardLoginStartPromise;
648
- const startPromise = startWizardLoginInternal(directory);
649
- activeWizardLoginStartPromise = startPromise;
650
- try {
651
- return await startPromise;
652
- }
653
- finally {
654
- if (activeWizardLoginStartPromise === startPromise) {
655
- activeWizardLoginStartPromise = null;
656
- }
657
- }
658
- }
659
- async function startWizardLoginInternal(directory) {
660
- const authStatus = await checkWizardAuthStatus(directory);
661
- if (authStatus.status === "configured") {
662
- return {
663
- status: "configured",
664
- packageName: WIZARD_PACKAGE_NAME,
665
- message: "Wizard is already authenticated; no login needed.",
666
- };
667
- }
668
- if (authStatus.status === "package_unavailable") {
669
- return {
670
- status: "package_unavailable",
671
- packageName: WIZARD_PACKAGE_NAME,
672
- message: authStatus.message,
673
- };
674
- }
675
- const existing = getActiveWizardLoginRun();
676
- if (existing) {
677
- return wizardLoginResult("already_running", existing.loginUrl
678
- ? "Wizard login is already running. Complete the browser flow."
679
- : "Wizard login is already running. Complete the browser flow if it has opened.", existing);
680
- }
681
- const bin = resolveWizardBinCommand();
682
- if (!bin.ok) {
683
- return {
684
- status: "package_unavailable",
685
- packageName: WIZARD_PACKAGE_NAME,
686
- message: bin.message,
687
- };
688
- }
689
- const child = spawn(bin.bin.command, [...bin.bin.argsPrefix, "login"], {
690
- stdio: ["ignore", "pipe", "pipe"],
691
- env: process.env,
692
- cwd: directory,
693
- });
694
- const run = { child, startedAt: Date.now() };
695
- activeWizardLoginRun = run;
696
- return await new Promise((resolve) => {
697
- let settled = false;
698
- let timeout;
699
- const finish = (result) => {
700
- if (settled)
701
- return;
702
- settled = true;
703
- clearTimeout(timeout);
704
- resolve(result);
705
- };
706
- const onData = (chunk) => {
707
- if (run.loginUrl)
708
- return;
709
- const loginUrl = extractLoginUrl(chunk.toString("utf8"));
710
- if (!loginUrl)
711
- return;
712
- run.loginUrl = loginUrl;
713
- finish(wizardLoginResult("login_url_observed", "Wizard login started. Complete the browser flow to finish authentication.", run));
714
- };
715
- const onError = (error) => {
716
- if (activeWizardLoginRun === run)
717
- activeWizardLoginRun = null;
718
- child.stdout?.off("data", onData);
719
- child.stderr?.off("data", onData);
720
- finish({
721
- status: "spawn_failed",
722
- packageName: WIZARD_PACKAGE_NAME,
723
- message: `Could not start Wizard login: ${toSafeErrorMessage(error)}`,
724
- });
725
- };
726
- const onExit = (code, signal) => {
727
- if (activeWizardLoginRun === run)
728
- activeWizardLoginRun = null;
729
- child.stdout?.off("data", onData);
730
- child.stderr?.off("data", onData);
731
- const exitLabel = signal ? `signal ${signal}` : `code ${code ?? "unknown"}`;
732
- const completed = code === 0 && signal === null;
733
- finish(wizardLoginResult(completed ? "completed" : "exited", completed
734
- ? "Wizard login completed."
735
- : `Wizard login exited before emitting a login URL (${exitLabel}).`, run));
736
- };
737
- timeout = setTimeout(() => {
738
- finish(wizardLoginResult("started", "Wizard login started. Complete the browser flow if it has opened.", run));
739
- }, WIZARD_LOGIN_STARTUP_TIMEOUT_MS);
740
- child.stdout?.on("data", onData);
741
- child.stderr?.on("data", onData);
742
- child.once("error", onError);
743
- child.once("exit", onExit);
744
- });
745
- }
746
- /**
747
- * Dispatch a `rest_request` frame. Always resolves with a
748
- * `RestResponseFrame` — handler exceptions are caught and translated.
749
- *
750
- * Status mapping:
751
- * - 200: handler returned successfully (body is the handler's return value
752
- * or `{ ok: true }` for void returns)
753
- * - 400: `BadRequestError` thrown by handler, OR malformed body for a
754
- * route that requires one, OR unknown route
755
- * - 404: `NotFoundError` thrown by handler
756
- * - 405: route matches but method doesn't (returned as 404 since we don't
757
- * distinguish — the matcher treats it as "no route", consistent
758
- * with the original Hono router which also 404'd)
759
- * - 500: anything else; `error` field carries a sanitized message
760
- */
761
- export async function handleRestRequest(frame, deps) {
762
- const { reqId, method, path, body } = frame;
763
- const logger = deps.logger ?? console;
764
- const startedAt = performance.now();
765
- logger.info?.(`[rest] start machineId=${frame.machineId} method=${method} path=${path} reqId=${reqId}`);
766
- const match = matchRoute(method, path);
767
- if (!match) {
768
- logger.info?.(`[rest] finish machineId=${frame.machineId} method=${method} path=${path} reqId=${reqId} status=404 durationMs=${Math.round(performance.now() - startedAt)}`);
769
- return {
770
- kind: "rest_response",
771
- reqId,
772
- status: 404,
773
- body: { error: `Unknown route: ${method} ${path}` },
774
- };
775
- }
776
- try {
777
- const result = await dispatchRoute(match, body, deps);
778
- logger.info?.(`[rest] finish machineId=${frame.machineId} method=${method} path=${path} reqId=${reqId} status=200 durationMs=${Math.round(performance.now() - startedAt)}`);
779
- return {
780
- kind: "rest_response",
781
- reqId,
782
- status: 200,
783
- body: result,
784
- };
785
- }
786
- catch (err) {
787
- if (err instanceof BadRequestError) {
788
- logger.info?.(`[rest] finish machineId=${frame.machineId} method=${method} path=${path} reqId=${reqId} status=400 durationMs=${Math.round(performance.now() - startedAt)}`);
789
- return {
790
- kind: "rest_response",
791
- reqId,
792
- status: 400,
793
- body: { error: err.message, code: err.code },
794
- };
795
- }
796
- if (err instanceof NotFoundError) {
797
- logger.info?.(`[rest] finish machineId=${frame.machineId} method=${method} path=${path} reqId=${reqId} status=404 durationMs=${Math.round(performance.now() - startedAt)}`);
798
- return {
799
- kind: "rest_response",
800
- reqId,
801
- status: 404,
802
- body: { error: err.message, code: err.code },
803
- };
804
- }
805
- // Unexpected — log locally so operators can correlate, but only
806
- // return a sanitized message to the caller.
807
- logger.error?.(`[dispatcher] unexpected error in ${method} ${path}:`, err);
808
- logger.info?.(`[rest] finish machineId=${frame.machineId} method=${method} path=${path} reqId=${reqId} status=500 durationMs=${Math.round(performance.now() - startedAt)}`);
809
- return {
810
- kind: "rest_response",
811
- reqId,
812
- status: 500,
813
- body: { error: "Internal error" },
814
- };
815
- }
816
- }
817
- /**
818
- * Inner switch: route → handler call. Separated so `handleRestRequest`
819
- * owns ONE try/catch boundary for all error mapping.
820
- *
821
- * Meta publish is fired AFTER the handler returns successfully (i.e.
822
- * after SQLite has committed) so subscribers never see a "create" event
823
- * for a row the next read won't find. Publish failures are swallowed
824
- * inside `safePublish` — the wire response goes back regardless.
825
- *
826
- * The body is `unknown` from the wire; each handler validates its own
827
- * shape via the `BadRequestError`-throwing checks they already do.
828
- */
829
- async function dispatchRoute(match, body, deps) {
830
- const { store, manager, publishMetaEvent, logger } = deps;
831
- const id = match.id ?? "";
832
- switch (match.route) {
833
- case "list_projects": {
834
- const busyIds = manager.getBusySessionIds();
835
- const projects = await handleListProjectsAsync(store);
836
- // Batch all per-project session lookups into a SINGLE query (LEFT JOIN +
837
- // GROUP BY) instead of calling listSessionsByProject(p.id) in a loop,
838
- // which issued O(projects) queries each with a per-session correlated
839
- // COUNT(*) subquery and blocked the event loop past the 30s relay
840
- // deadline on large projects.
841
- const sessionsByProject = await store.listSessionsByProjectIdsAsync(projects.map((p) => p.id));
842
- return projects.map((p) => {
843
- const sessions = sessionsByProject.get(p.id) ?? [];
844
- const hasActiveSession = sessions.some((s) => busyIds.has(s.id));
845
- return { ...p, hasActiveSession, busy: hasActiveSession };
846
- });
847
- }
848
- case "create_project": {
849
- const project = handleCreateProject(store, asObject(body));
850
- safePublish(publishMetaEvent, logger, {
851
- type: "project_created",
852
- projectId: project.id,
853
- });
854
- return project;
855
- }
856
- case "update_project": {
857
- const project = handleUpdateProject(store, id, asObject(body));
858
- safePublish(publishMetaEvent, logger, {
859
- type: "project_renamed",
860
- projectId: project.id,
861
- });
862
- return project;
863
- }
864
- case "delete_project": {
865
- // Order matters: tear down live streams BEFORE the row is gone so
866
- // the bridges don't try to write to a deleted FK target. Mirrors
867
- // the ordering the deleted Hono route used.
868
- const project = await store.getProjectAsync(id);
869
- if (project) {
870
- const sessions = await store.listSessionsByProjectAsync(id);
871
- manager.disposeProjectStreams(sessions.map((s) => s.id));
872
- }
873
- const result = handleDeleteProject(store, id);
874
- // Publish AFTER the cascade has committed. We use `id` (the URL
875
- // param) as the projectId — `handleDeleteProject` would have
876
- // thrown 404 above if it didn't exist.
877
- safePublish(publishMetaEvent, logger, {
878
- type: "project_deleted",
879
- projectId: id,
880
- });
881
- return result;
882
- }
883
- case "bind_studio": {
884
- const result = await handleBindStudioProject(store, id, asObject(body));
885
- safePublish(publishMetaEvent, logger, {
886
- type: "project_studio_bound",
887
- projectId: id,
888
- studioProjectId: result.studioProjectId,
889
- studioProjectName: result.studioProjectName,
890
- });
891
- // Refresh the X-Project-Id header on active bridges so agent-session
892
- // credits are attributed to the newly bound project immediately.
893
- manager.reloadProjectBindings().catch((err) => {
894
- logger?.error?.(`[dispatcher] reloadProjectBindings failed: ${err instanceof Error ? err.message : String(err)}`);
895
- });
896
- // Reload MCP config so the aexol-mcp extension picks up the new binding.
897
- if (deps.mcpState) {
898
- handleReloadMcpConfig(deps.mcpState).catch((err) => {
899
- logger?.error?.(`[dispatcher] MCP reload after bind failed: ${err instanceof Error ? err.message : String(err)}`);
900
- });
901
- }
902
- return result;
903
- }
904
- case "unbind_studio": {
905
- const result = await handleUnbindStudioProject(store, id);
906
- if (result.studioProjectId) {
907
- safePublish(publishMetaEvent, logger, {
908
- type: "project_studio_unbound",
909
- projectId: id,
910
- studioProjectId: result.studioProjectId,
911
- });
912
- }
913
- // Clear the X-Project-Id header on active bridges.
914
- manager.reloadProjectBindings().catch((err) => {
915
- logger?.error?.(`[dispatcher] reloadProjectBindings failed: ${err instanceof Error ? err.message : String(err)}`);
916
- });
917
- // Reload MCP config so the aexol-mcp extension clears the old binding.
918
- if (deps.mcpState) {
919
- handleReloadMcpConfig(deps.mcpState).catch((err) => {
920
- logger?.error?.(`[dispatcher] MCP reload after unbind failed: ${err instanceof Error ? err.message : String(err)}`);
921
- });
922
- }
923
- return result;
924
- }
925
- case "get_project_observations": {
926
- const limitVal = Number(match.query?.get("limit") ?? 30);
927
- return handleGetProjectObservations(store, id, Math.max(1, Math.min(100, limitVal || 30)));
928
- }
929
- case "list_project_sessions": {
930
- const busyIds = manager.getBusySessionIds();
931
- const sessions = await handleListSessionsByProjectAsync(store, id);
932
- return sessions.map((s) => ({
933
- ...s,
934
- hasActiveTurn: busyIds.has(s.id),
935
- busy: busyIds.has(s.id),
936
- }));
937
- }
938
- case "cleanup_project_sessions": {
939
- const result = handleCleanupProjectSessions(store, id, asObject(body));
940
- // Tear down any in-flight streams for sessions that are being deleted.
941
- for (const sessionId of result.ids) {
942
- manager.disposeSessionStream(sessionId);
943
- }
944
- for (const sessionId of result.ids) {
945
- safePublish(publishMetaEvent, logger, {
946
- type: "session_deleted",
947
- projectId: id,
948
- sessionId,
949
- });
950
- }
951
- return { deleted: result.deleted };
952
- }
953
- case "cleanup_all_sessions": {
954
- const result = handleCleanupAllSessions(store, asObject(body));
955
- // Tear down any in-flight streams for sessions that are being deleted,
956
- // across every project.
957
- for (const sessionId of result.ids) {
958
- manager.disposeSessionStream(sessionId);
959
- }
960
- for (const [sessionId, projectId] of Object.entries(result.projectIds)) {
961
- safePublish(publishMetaEvent, logger, {
962
- type: "session_deleted",
963
- projectId,
964
- sessionId,
965
- });
966
- }
967
- return { deleted: result.deleted };
968
- }
969
- case "create_session": {
970
- const session = handleCreateSession(store, asObject(body));
971
- safePublish(publishMetaEvent, logger, {
972
- type: "session_created",
973
- projectId: session.projectId,
974
- sessionId: session.id,
975
- });
976
- return session;
977
- }
978
- case "get_session": {
979
- const detail = await handleGetSessionDetailAsync(store, id);
980
- return {
981
- ...detail,
982
- busy: manager.isBusy(id),
983
- runState: manager.getRunState(id),
984
- };
985
- }
986
- case "get_session_active_agent":
987
- return await handleGetSessionActiveAgentAsync(store, id, deps.cwd ?? homedir(), deps.agentDir);
988
- case "update_session_active_agent":
989
- return await handleUpdateSessionActiveAgentAsync(store, id, asObject(body), manager, deps.cwd ?? homedir(), deps.agentDir);
990
- case "get_session_messages":
991
- return await handleGetSessionMessagesAsync(store, id, {
992
- beforeSeq: match.query?.get("beforeSeq") ?? undefined,
993
- beforeCreatedAt: match.query?.get("beforeCreatedAt") ?? undefined,
994
- beforeId: match.query?.get("beforeId") ?? undefined,
995
- limit: match.query?.get("limit") ?? undefined,
996
- });
997
- case "get_session_memory":
998
- return handleGetSessionMemoryStatus(store, manager, id);
999
- case "get_session_memory_details":
1000
- return handleGetSessionMemoryDetails(store, manager, id);
1001
- case "update_session": {
1002
- const session = handleUpdateSession(store, id, asObject(body));
1003
- safePublish(publishMetaEvent, logger, {
1004
- type: "session_renamed",
1005
- projectId: session.projectId,
1006
- sessionId: session.id,
1007
- });
1008
- return session;
1009
- }
1010
- case "delete_session": {
1011
- // Capture projectId BEFORE the row is gone — otherwise the
1012
- // post-delete publish has no project to attribute to. Treat a
1013
- // missing session here the same way `handleDeleteSession` does
1014
- // (throws NotFoundError → 404), but capture first.
1015
- const detail = store.getSession(id);
1016
- // Same ordering rationale as delete_project.
1017
- manager.disposeSessionStream(id);
1018
- handleDeleteSession(store, id);
1019
- if (detail) {
1020
- safePublish(publishMetaEvent, logger, {
1021
- type: "session_deleted",
1022
- projectId: detail.projectId,
1023
- sessionId: id,
1024
- });
1025
- }
1026
- return { ok: true };
1027
- }
1028
- case "compact_session":
1029
- return await handleCompactSession(store, manager, id, body);
1030
- case "remember_and_delete_session": {
1031
- // Compact first — the compaction hook's persistProjectObservations
1032
- // runs inside compactSession, writing reflections to cross-session
1033
- // durable memory. On failure (e.g. no API key), we let the error
1034
- // propagate so the session is NOT deleted.
1035
- await handleRememberAndDeleteSession(store, manager, id);
1036
- const detail = store.getSession(id);
1037
- manager.disposeSessionStream(id);
1038
- handleDeleteSession(store, id);
1039
- if (detail) {
1040
- safePublish(publishMetaEvent, logger, {
1041
- type: "session_deleted",
1042
- projectId: detail.projectId,
1043
- sessionId: id,
1044
- });
1045
- }
1046
- return { ok: true };
1047
- }
1048
- case "fork_session": {
1049
- const session = handleForkSession(store, id, asObject(body));
1050
- safePublish(publishMetaEvent, logger, {
1051
- type: "session_created",
1052
- projectId: session.projectId,
1053
- sessionId: session.id,
1054
- });
1055
- return session;
1056
- }
1057
- case "list_path_autocomplete": {
1058
- const prefix = match.query?.get("prefix") ?? "";
1059
- return await handlePathAutocomplete(prefix);
1060
- }
1061
- case "list_directory":
1062
- return await handleListDirectory(asObject(body));
1063
- case "get_home_directory":
1064
- return handleGetHomeDirectory();
1065
- case "search_project_files": {
1066
- const searchQuery = match.query?.get("query") ?? "";
1067
- return handleSearchProjectFiles(store, id, searchQuery);
1068
- }
1069
- case "enqueue_prompt":
1070
- return handleEnqueuePrompt(store, manager, id, asObject(body));
1071
- case "get_prompt_queue":
1072
- return handleGetPromptQueue(store, id);
1073
- case "remove_prompt":
1074
- handleRemovePrompt(store, manager, id, match.itemId ?? "");
1075
- return { ok: true };
1076
- case "clear_prompt_queue":
1077
- handleClearPromptQueue(store, manager, id);
1078
- return { ok: true };
1079
- case "list_mcp_status": {
1080
- if (!deps.mcpState) {
1081
- return [];
1082
- }
1083
- return handleListMcpStatus(deps.mcpState);
1084
- }
1085
- case "reload_mcp_config": {
1086
- return await handleReloadMcpConfig(deps.mcpState);
1087
- }
1088
- case "refresh_native_extensions": {
1089
- if (!deps.refreshNativeExtensions) {
1090
- return { ok: true };
1091
- }
1092
- await deps.refreshNativeExtensions();
1093
- return { ok: true };
1094
- }
1095
- case "refresh_cloud_agents": {
1096
- const teamId = asObject(body).teamId;
1097
- if (typeof teamId !== "string" || teamId.length === 0) {
1098
- throw new BadRequestError("teamId is required");
1099
- }
1100
- if (deps.refreshCloudAgents)
1101
- await deps.refreshCloudAgents(teamId);
1102
- return { ok: true };
1103
- }
1104
- case "wizard_login": {
1105
- return await (deps.startWizardLogin ?? (() => startWizardLogin(deps.cwd ?? homedir())))();
1106
- }
1107
- case "get_settings":
1108
- return await handleGetSettingsAsync(deps.cwd ?? homedir());
1109
- case "put_settings":
1110
- return await handlePutSettingsAsync(deps.cwd ?? homedir(), asObject(body));
1111
- case "get_agent_settings":
1112
- return await handleGetAgentSettingsAsync(deps.cwd ?? homedir(), deps.agentDir);
1113
- case "put_agent_settings":
1114
- return await handlePutAgentSettingsAsync(deps.cwd ?? homedir(), asObject(body), deps.agentDir);
1115
- case "get_prompt_mutation_settings":
1116
- return handleGetPromptMutationSettings(deps.cwd ?? homedir(), deps.agentDir);
1117
- case "put_prompt_mutation_settings":
1118
- return handlePutPromptMutationSettings(deps.cwd ?? homedir(), asObject(body), deps.agentDir);
1119
- case "list_agents": {
1120
- const agentCwd = match.query?.get("cwd") || deps.cwd || homedir();
1121
- const teamId = match.query?.get("teamId") || undefined;
1122
- // Kick off cloud refresh in the background without blocking the response.
1123
- // The previous version awaited refreshCloudAgents (network → backend GraphQL)
1124
- // before returning, so every page refresh stalled the entire REST queue
1125
- // behind this one slow call while 5-7 other rest_requests timed out.
1126
- // Background refresh self-dedupes via CloudAgentCacheEntry.inFlight, so
1127
- // bursts coalesce; stale cache renders immediately and the picker catches
1128
- // up on the next meta_event/live refresh.
1129
- if (teamId && deps.refreshCloudAgents) {
1130
- void deps.refreshCloudAgents(teamId).catch((err) => {
1131
- logger?.error?.(`[dispatcher] cloud agent refresh failed:`, err);
1132
- });
1133
- }
1134
- const agents = handleListAgents(agentCwd, deps.agentDir, teamId);
1135
- return agents;
1136
- }
1137
- case "list_dev_processes":
1138
- return handleListDevProcesses(deps.devProcessRegistry);
1139
- case "start_dev_process":
1140
- return handleStartDevProcess(deps.devProcessRegistry, store, asObject(body), deps.cwd);
1141
- case "kill_dev_process":
1142
- return handleKillDevProcess(deps.devProcessRegistry, id);
1143
- case "restart_dev_process":
1144
- return handleRestartDevProcess(deps.devProcessRegistry, id);
1145
- case "scan_dev_servers":
1146
- return handleScanDevServers(store, deps.devProcessRegistry);
1147
- case "kill_dev_server":
1148
- return await handleKillDetectedDevServer(store, Number(id), match.port);
1149
- }
1150
- }
1151
- /**
1152
- * Invoke `publishMetaEvent` if provided, swallowing any throw. The
1153
- * caller has already returned a 200 to the browser; a publish failure
1154
- * (e.g. relay socket just dropped) must NEVER turn a successful
1155
- * mutation into a wire-level error.
1156
- */
1157
- function safePublish(publish, logger, event) {
1158
- if (!publish)
1159
- return;
1160
- try {
1161
- publish(event);
1162
- }
1163
- catch (err) {
1164
- (logger ?? console).error?.(`[dispatcher] publishMetaEvent failed for ${event.type}:`, err);
1165
- }
1166
- }
1167
- /**
1168
- * Coerce an `unknown` body to a plain object for the handler. Returns an
1169
- * empty object when the body is missing/non-object so the handler's own
1170
- * field checks produce the canonical error message ("name (string) is
1171
- * required") instead of a confusing "body must be object" from us.
1172
- */
1173
- function asObject(body) {
1174
- if (body !== null && typeof body === "object" && !Array.isArray(body)) {
1175
- return body;
1176
- }
1177
- return {};
1178
- }
54
+ // Extracted modules — re-exported for backwards compatibility with the
55
+ // original single-file dispatcher.
56
+ export { matchRoute } from "./routing.js";
57
+ export { handleRestRequest } from "./rest.js";
58
+ export { startWizardLogin } from "./wizard-login.js";
1179
59
  // ---------------------------------------------------------------------------
1180
60
  // Helpers
1181
61
  // ---------------------------------------------------------------------------