@undefineds.co/linx 0.3.4 → 0.3.7

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/README.md +58 -23
  2. package/dist/generated/version.js +1 -1
  3. package/dist/generated/version.js.map +1 -1
  4. package/dist/index.js +334 -162
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/account-session.js +4 -8
  7. package/dist/lib/account-session.js.map +1 -1
  8. package/dist/lib/ai-command.js +228 -178
  9. package/dist/lib/ai-command.js.map +1 -1
  10. package/dist/lib/auto-mode/archive.js +38 -7
  11. package/dist/lib/auto-mode/archive.js.map +1 -1
  12. package/dist/lib/auto-mode/auth.js.map +1 -1
  13. package/dist/lib/auto-mode/display.js +71 -45
  14. package/dist/lib/auto-mode/display.js.map +1 -1
  15. package/dist/lib/auto-mode/format.js +9 -7
  16. package/dist/lib/auto-mode/format.js.map +1 -1
  17. package/dist/lib/auto-mode/hooks/claude.js +12 -2
  18. package/dist/lib/auto-mode/hooks/claude.js.map +1 -1
  19. package/dist/lib/auto-mode/hooks/codex.js +17 -7
  20. package/dist/lib/auto-mode/hooks/codex.js.map +1 -1
  21. package/dist/lib/auto-mode/hooks/index.js +28 -8
  22. package/dist/lib/auto-mode/hooks/index.js.map +1 -1
  23. package/dist/lib/auto-mode/pod-ai.js +20 -37
  24. package/dist/lib/auto-mode/pod-ai.js.map +1 -1
  25. package/dist/lib/auto-mode/pod-approval.js +124 -195
  26. package/dist/lib/auto-mode/pod-approval.js.map +1 -1
  27. package/dist/lib/auto-mode/pod-persistence.js +169 -90
  28. package/dist/lib/auto-mode/pod-persistence.js.map +1 -1
  29. package/dist/lib/auto-mode/runner.js +683 -81
  30. package/dist/lib/auto-mode/runner.js.map +1 -1
  31. package/dist/lib/auto-mode/secretary.js +186 -41
  32. package/dist/lib/auto-mode/secretary.js.map +1 -1
  33. package/dist/lib/auto-mode-command.js +32 -32
  34. package/dist/lib/auto-mode-command.js.map +1 -1
  35. package/dist/lib/chat-api.js +242 -50
  36. package/dist/lib/chat-api.js.map +1 -1
  37. package/dist/lib/codex-plugin/bridge.js +164 -17
  38. package/dist/lib/codex-plugin/bridge.js.map +1 -1
  39. package/dist/lib/codex-plugin/codex-native-proxy.js +370 -34
  40. package/dist/lib/codex-plugin/codex-native-proxy.js.map +1 -1
  41. package/dist/lib/credentials-store.js +33 -42
  42. package/dist/lib/credentials-store.js.map +1 -1
  43. package/dist/lib/linx-cloud-errors.js +61 -0
  44. package/dist/lib/linx-cloud-errors.js.map +1 -0
  45. package/dist/lib/linx-tui-contract.js +8 -5
  46. package/dist/lib/linx-tui-contract.js.map +1 -1
  47. package/dist/lib/login-command.js +9 -2
  48. package/dist/lib/login-command.js.map +1 -1
  49. package/dist/lib/models.js +3 -20
  50. package/dist/lib/models.js.map +1 -1
  51. package/dist/lib/oidc-auth.js +143 -17
  52. package/dist/lib/oidc-auth.js.map +1 -1
  53. package/dist/lib/oidc-session-storage.js +2 -6
  54. package/dist/lib/oidc-session-storage.js.map +1 -1
  55. package/dist/lib/pi-adapter/auto-input-controller.js +988 -0
  56. package/dist/lib/pi-adapter/auto-input-controller.js.map +1 -0
  57. package/dist/lib/pi-adapter/backend-command.js +2 -0
  58. package/dist/lib/pi-adapter/backend-command.js.map +1 -0
  59. package/dist/lib/pi-adapter/backend-credentials.js +80 -0
  60. package/dist/lib/pi-adapter/backend-credentials.js.map +1 -0
  61. package/dist/lib/pi-adapter/branding.js +246 -108
  62. package/dist/lib/pi-adapter/branding.js.map +1 -1
  63. package/dist/lib/pi-adapter/control-state.js +72 -0
  64. package/dist/lib/pi-adapter/control-state.js.map +1 -0
  65. package/dist/lib/pi-adapter/interactive.js +2634 -30
  66. package/dist/lib/pi-adapter/interactive.js.map +1 -1
  67. package/dist/lib/pi-adapter/pod-approval.js +382 -210
  68. package/dist/lib/pi-adapter/pod-approval.js.map +1 -1
  69. package/dist/lib/pi-adapter/pod-mirror-mapping.js +71 -17
  70. package/dist/lib/pi-adapter/pod-mirror-mapping.js.map +1 -1
  71. package/dist/lib/pi-adapter/pod-mirror.js +531 -64
  72. package/dist/lib/pi-adapter/pod-mirror.js.map +1 -1
  73. package/dist/lib/pi-adapter/pod-native.js +81 -85
  74. package/dist/lib/pi-adapter/pod-native.js.map +1 -1
  75. package/dist/lib/pi-adapter/pod-status-output.js +54 -0
  76. package/dist/lib/pi-adapter/pod-status-output.js.map +1 -0
  77. package/dist/lib/pi-adapter/runtime.js +458 -228
  78. package/dist/lib/pi-adapter/runtime.js.map +1 -1
  79. package/dist/lib/pi-adapter/session-control.js +509 -0
  80. package/dist/lib/pi-adapter/session-control.js.map +1 -0
  81. package/dist/lib/pi-adapter/session.js +35 -22
  82. package/dist/lib/pi-adapter/session.js.map +1 -1
  83. package/dist/lib/pi-adapter/stream.js +89 -32
  84. package/dist/lib/pi-adapter/stream.js.map +1 -1
  85. package/dist/lib/pi-adapter/sync-recovery.js +89 -0
  86. package/dist/lib/pi-adapter/sync-recovery.js.map +1 -0
  87. package/dist/lib/pi-adapter/web-fetch.js +13 -14
  88. package/dist/lib/pi-adapter/web-fetch.js.map +1 -1
  89. package/dist/lib/pod-chat-store.js +254 -78
  90. package/dist/lib/pod-chat-store.js.map +1 -1
  91. package/dist/lib/pod-data-session.js +156 -35
  92. package/dist/lib/pod-data-session.js.map +1 -1
  93. package/dist/lib/solid-auth-store.js +27 -0
  94. package/dist/lib/solid-auth-store.js.map +1 -0
  95. package/dist/lib/solid-auth.js +2 -4
  96. package/dist/lib/solid-auth.js.map +1 -1
  97. package/dist/lib/solid-client-credentials-login.js +100 -0
  98. package/dist/lib/solid-client-credentials-login.js.map +1 -0
  99. package/dist/lib/solid-local-store.js +31 -0
  100. package/dist/lib/solid-local-store.js.map +1 -0
  101. package/dist/lib/symphony/archive.js +328 -18
  102. package/dist/lib/symphony/archive.js.map +1 -1
  103. package/dist/lib/symphony/pod-projection.js +2222 -0
  104. package/dist/lib/symphony/pod-projection.js.map +1 -0
  105. package/dist/lib/symphony-command.js +602 -178
  106. package/dist/lib/symphony-command.js.map +1 -1
  107. package/dist/lib/sync-checkpoint-store.js +74 -0
  108. package/dist/lib/sync-checkpoint-store.js.map +1 -0
  109. package/dist/skills/symphony/SKILL.md +665 -0
  110. package/package.json +15 -9
  111. package/vendor/agent-runtime/dist/agent-runtime.d.ts +137 -0
  112. package/vendor/agent-runtime/dist/agent-runtime.js +211 -0
  113. package/vendor/agent-runtime/dist/auto-mode.d.ts +78 -13
  114. package/vendor/agent-runtime/dist/auto-mode.js +288 -31
  115. package/vendor/agent-runtime/dist/control-plane.d.ts +28 -0
  116. package/vendor/agent-runtime/dist/control-plane.js +79 -0
  117. package/vendor/agent-runtime/dist/file-sync.d.ts +157 -0
  118. package/vendor/agent-runtime/dist/file-sync.js +314 -0
  119. package/vendor/agent-runtime/dist/index.d.ts +7 -0
  120. package/vendor/agent-runtime/dist/index.js +7 -0
  121. package/vendor/agent-runtime/dist/reconciler.d.ts +117 -0
  122. package/vendor/agent-runtime/dist/reconciler.js +361 -0
  123. package/vendor/agent-runtime/dist/symphony.d.ts +128 -8
  124. package/vendor/agent-runtime/dist/symphony.js +362 -57
  125. package/vendor/agent-runtime/dist/sync.d.ts +271 -0
  126. package/vendor/agent-runtime/dist/sync.js +550 -0
  127. package/vendor/agent-runtime/dist/thread-reconciler-controller.d.ts +58 -0
  128. package/vendor/agent-runtime/dist/thread-reconciler-controller.js +137 -0
  129. package/vendor/agent-runtime/dist/turn-controller.js +2 -2
  130. package/vendor/agent-runtime/dist/wake-scheduler.d.ts +67 -0
  131. package/vendor/agent-runtime/dist/wake-scheduler.js +194 -0
  132. package/vendor/agent-runtime/package.json +8 -1
  133. package/vendor/pi-web-access/CHANGELOG.md +387 -0
  134. package/vendor/pi-web-access/LICENSE +21 -0
  135. package/vendor/pi-web-access/README.md +352 -0
  136. package/vendor/pi-web-access/activity.ts +101 -0
  137. package/vendor/pi-web-access/banner.png +0 -0
  138. package/vendor/pi-web-access/chrome-cookies.ts +322 -0
  139. package/vendor/pi-web-access/code-search.ts +107 -0
  140. package/vendor/pi-web-access/curator-page.ts +3359 -0
  141. package/vendor/pi-web-access/curator-server.ts +605 -0
  142. package/vendor/pi-web-access/exa.ts +520 -0
  143. package/vendor/pi-web-access/extract.ts +641 -0
  144. package/vendor/pi-web-access/gemini-api.ts +112 -0
  145. package/vendor/pi-web-access/gemini-search.ts +361 -0
  146. package/vendor/pi-web-access/gemini-url-context.ts +126 -0
  147. package/vendor/pi-web-access/gemini-web-config.ts +52 -0
  148. package/vendor/pi-web-access/gemini-web.ts +396 -0
  149. package/vendor/pi-web-access/github-api.ts +196 -0
  150. package/vendor/pi-web-access/github-extract.ts +634 -0
  151. package/vendor/pi-web-access/index.ts +2346 -0
  152. package/vendor/pi-web-access/package.json +45 -0
  153. package/vendor/pi-web-access/pdf-extract.ts +192 -0
  154. package/vendor/pi-web-access/perplexity.ts +195 -0
  155. package/vendor/pi-web-access/pi-web-fetch-demo.mp4 +0 -0
  156. package/vendor/pi-web-access/rsc-extract.ts +338 -0
  157. package/vendor/pi-web-access/skills/librarian/SKILL.md +195 -0
  158. package/vendor/pi-web-access/storage.ts +72 -0
  159. package/vendor/pi-web-access/summary-review.ts +276 -0
  160. package/vendor/pi-web-access/test/gemini-web-cookie-opt-in.test.mjs +41 -0
  161. package/vendor/pi-web-access/test/pdf-extract.test.mjs +95 -0
  162. package/vendor/pi-web-access/utils.ts +44 -0
  163. package/vendor/pi-web-access/video-extract.ts +378 -0
  164. package/vendor/pi-web-access/youtube-extract.ts +310 -0
  165. package/dist/lib/pi-adapter/auth.js +0 -68
  166. package/dist/lib/pi-adapter/auth.js.map +0 -1
  167. package/dist/lib/pi-adapter/pod-tools.js +0 -140
  168. package/dist/lib/pi-adapter/pod-tools.js.map +0 -1
  169. package/dist/skills/drizzle-solid/SKILL.md +0 -340
  170. package/dist/skills/pod-storage/SKILL.md +0 -100
  171. package/dist/skills/solid-modeling/SKILL.md +0 -274
  172. package/dist/skills/xpod-componentsjs/SKILL.md +0 -284
@@ -0,0 +1,605 @@
1
+ import http, { type IncomingMessage, type ServerResponse } from "node:http";
2
+ import { generateCuratorPage } from "./curator-page.js";
3
+ import type { SummaryMeta } from "./summary-review.js";
4
+
5
+ const STALE_THRESHOLD_MS = 30000;
6
+ const WATCHDOG_INTERVAL_MS = 5000;
7
+ const MAX_BODY_SIZE = 64 * 1024;
8
+
9
+ type ServerState = "SEARCHING" | "RESULT_SELECTION" | "COMPLETED";
10
+
11
+ export interface CuratorServerOptions {
12
+ queries: string[];
13
+ sessionToken: string;
14
+ timeout: number;
15
+ availableProviders: { perplexity: boolean; exa: boolean; gemini: boolean };
16
+ defaultProvider: string;
17
+ summaryModels: Array<{ value: string; label: string }>;
18
+ defaultSummaryModel: string | null;
19
+ }
20
+
21
+ export interface CuratorServerCallbacks {
22
+ onSubmit: (payload: { selectedQueryIndices: number[]; summary?: string; summaryMeta?: SummaryMeta; rawResults?: boolean }) => void;
23
+ onCancel: (reason: "user" | "timeout" | "stale") => void;
24
+ onProviderChange: (provider: string) => void;
25
+ onAddSearch: (query: string, queryIndex: number, provider?: string) => Promise<{
26
+ answer: string;
27
+ results: Array<{ title: string; url: string; domain: string }>;
28
+ provider: string;
29
+ }>;
30
+ onSummarize: (
31
+ selectedQueryIndices: number[],
32
+ signal: AbortSignal,
33
+ model?: string,
34
+ feedback?: string,
35
+ ) => Promise<{ summary: string; meta: SummaryMeta }>;
36
+ onRewriteQuery: (query: string, signal: AbortSignal) => Promise<string>;
37
+ }
38
+
39
+ export interface CuratorServerHandle {
40
+ server: http.Server;
41
+ url: string;
42
+ close: () => void;
43
+ pushResult: (queryIndex: number, data: { answer: string; results: Array<{ title: string; url: string; domain: string }>; provider: string }) => void;
44
+ pushError: (queryIndex: number, error: string, provider?: string) => void;
45
+ searchesDone: () => void;
46
+ }
47
+
48
+ function sendJson(res: ServerResponse, status: number, payload: unknown): void {
49
+ res.writeHead(status, {
50
+ "Content-Type": "application/json",
51
+ "Cache-Control": "no-store",
52
+ });
53
+ res.end(JSON.stringify(payload));
54
+ }
55
+
56
+ function parseJSONBody(req: IncomingMessage): Promise<unknown> {
57
+ return new Promise((resolve, reject) => {
58
+ let body = "";
59
+ let size = 0;
60
+ req.on("data", (chunk: Buffer) => {
61
+ size += chunk.length;
62
+ if (size > MAX_BODY_SIZE) {
63
+ req.destroy();
64
+ reject(new Error("Request body too large"));
65
+ return;
66
+ }
67
+ body += chunk.toString();
68
+ });
69
+ req.on("end", () => {
70
+ try {
71
+ resolve(JSON.parse(body));
72
+ } catch (err) {
73
+ const message = err instanceof Error ? err.message : String(err);
74
+ reject(new Error(`Invalid JSON: ${message}`));
75
+ }
76
+ });
77
+ req.on("error", reject);
78
+ });
79
+ }
80
+
81
+ async function parseBodyOrSend(req: IncomingMessage, res: ServerResponse): Promise<unknown | null> {
82
+ try {
83
+ return await parseJSONBody(req);
84
+ } catch (err) {
85
+ const message = err instanceof Error ? err.message : "Invalid body";
86
+ const status = message === "Request body too large" ? 413 : 400;
87
+ sendJson(res, status, { ok: false, error: message });
88
+ return null;
89
+ }
90
+ }
91
+
92
+ function normalizeSelectedIndices(
93
+ value: unknown,
94
+ options: { allowEmpty: boolean; maxExclusive: number },
95
+ ): { ok: true; indices: number[] } | { ok: false; error: string } {
96
+ if (!Array.isArray(value)) {
97
+ return { ok: false, error: "Invalid selection" };
98
+ }
99
+
100
+ if (!options.allowEmpty && value.length === 0) {
101
+ return { ok: false, error: "Invalid selection" };
102
+ }
103
+
104
+ const normalized: number[] = [];
105
+ const seen = new Set<number>();
106
+ for (const item of value) {
107
+ if (typeof item !== "number" || !Number.isInteger(item) || item < 0) {
108
+ return { ok: false, error: "Invalid selection" };
109
+ }
110
+ if (item >= options.maxExclusive) {
111
+ return { ok: false, error: "Invalid selection" };
112
+ }
113
+ if (seen.has(item)) {
114
+ continue;
115
+ }
116
+ seen.add(item);
117
+ normalized.push(item);
118
+ }
119
+
120
+ if (!options.allowEmpty && normalized.length === 0) {
121
+ return { ok: false, error: "Invalid selection" };
122
+ }
123
+
124
+ return { ok: true, indices: normalized };
125
+ }
126
+
127
+ function normalizeSummaryMeta(value: unknown): SummaryMeta | null {
128
+ if (!value || typeof value !== "object") return null;
129
+ const meta = value as Record<string, unknown>;
130
+
131
+ const model = meta.model;
132
+ if (model !== null && typeof model !== "string") return null;
133
+
134
+ const durationMs = meta.durationMs;
135
+ if (typeof durationMs !== "number" || !Number.isFinite(durationMs) || durationMs < 0) return null;
136
+
137
+ const tokenEstimate = meta.tokenEstimate;
138
+ if (typeof tokenEstimate !== "number" || !Number.isFinite(tokenEstimate) || tokenEstimate < 0) return null;
139
+
140
+ const fallbackUsed = meta.fallbackUsed;
141
+ if (typeof fallbackUsed !== "boolean") return null;
142
+
143
+ const fallbackReason = meta.fallbackReason;
144
+ if (fallbackReason !== undefined && typeof fallbackReason !== "string") return null;
145
+
146
+ const edited = meta.edited;
147
+ if (edited !== undefined && typeof edited !== "boolean") return null;
148
+
149
+ return {
150
+ model,
151
+ durationMs,
152
+ tokenEstimate,
153
+ fallbackUsed,
154
+ fallbackReason,
155
+ edited,
156
+ };
157
+ }
158
+
159
+ export function startCuratorServer(
160
+ options: CuratorServerOptions,
161
+ callbacks: CuratorServerCallbacks,
162
+ ): Promise<CuratorServerHandle> {
163
+ const {
164
+ queries,
165
+ sessionToken,
166
+ timeout,
167
+ availableProviders,
168
+ defaultProvider,
169
+ summaryModels,
170
+ defaultSummaryModel,
171
+ } = options;
172
+ let browserConnected = false;
173
+ let lastHeartbeatAt = Date.now();
174
+ let completed = false;
175
+ let watchdog: NodeJS.Timeout | null = null;
176
+ let state: ServerState = "SEARCHING";
177
+ let sseResponse: ServerResponse | null = null;
178
+ const sseBuffer: string[] = [];
179
+ let nextQueryIndex = queries.length;
180
+ let summarizeAbortController: AbortController | null = null;
181
+ let summarizeRequestSeq = 0;
182
+
183
+ let sseKeepalive: NodeJS.Timeout | null = null;
184
+
185
+ const abortInFlightSummarize = (): void => {
186
+ if (!summarizeAbortController) return;
187
+ summarizeAbortController.abort();
188
+ summarizeAbortController = null;
189
+ };
190
+
191
+ const markCompleted = (): boolean => {
192
+ if (completed) return false;
193
+ completed = true;
194
+ state = "COMPLETED";
195
+ if (watchdog) {
196
+ clearInterval(watchdog);
197
+ watchdog = null;
198
+ }
199
+ if (sseKeepalive) {
200
+ clearInterval(sseKeepalive);
201
+ sseKeepalive = null;
202
+ }
203
+ abortInFlightSummarize();
204
+ if (sseResponse) {
205
+ try { sseResponse.end(); } catch {}
206
+ sseResponse = null;
207
+ }
208
+ return true;
209
+ };
210
+
211
+ const touchHeartbeat = (): void => {
212
+ lastHeartbeatAt = Date.now();
213
+ browserConnected = true;
214
+ };
215
+
216
+ function validateToken(body: unknown, res: ServerResponse): boolean {
217
+ if (!body || typeof body !== "object") {
218
+ sendJson(res, 400, { ok: false, error: "Invalid body" });
219
+ return false;
220
+ }
221
+ if ((body as { token?: string }).token !== sessionToken) {
222
+ sendJson(res, 403, { ok: false, error: "Invalid session" });
223
+ return false;
224
+ }
225
+ return true;
226
+ }
227
+
228
+ function isAvailableProvider(provider: string): boolean {
229
+ if (provider === "perplexity") return availableProviders.perplexity;
230
+ if (provider === "exa") return availableProviders.exa;
231
+ if (provider === "gemini") return availableProviders.gemini;
232
+ return false;
233
+ }
234
+
235
+ function sendSSE(event: string, data: unknown): void {
236
+ const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
237
+ const res = sseResponse;
238
+ if (res && !res.writableEnded && res.socket && !res.socket.destroyed) {
239
+ try { res.write(payload); return; } catch {}
240
+ }
241
+ sseBuffer.push(payload);
242
+ }
243
+
244
+ const pageHtml = generateCuratorPage(
245
+ queries,
246
+ sessionToken,
247
+ timeout,
248
+ availableProviders,
249
+ defaultProvider,
250
+ summaryModels,
251
+ defaultSummaryModel,
252
+ );
253
+
254
+ const server = http.createServer(async (req, res) => {
255
+ try {
256
+ const method = req.method || "GET";
257
+ const url = new URL(req.url || "/", `http://${req.headers.host || "127.0.0.1"}`);
258
+
259
+ if (method === "GET" && url.pathname === "/") {
260
+ const token = url.searchParams.get("session");
261
+ if (token !== sessionToken) {
262
+ res.writeHead(403, { "Content-Type": "text/plain" });
263
+ res.end("Invalid session");
264
+ return;
265
+ }
266
+ touchHeartbeat();
267
+ res.writeHead(200, {
268
+ "Content-Type": "text/html; charset=utf-8",
269
+ "Cache-Control": "no-store",
270
+ });
271
+ res.end(pageHtml);
272
+ return;
273
+ }
274
+
275
+ if (method === "GET" && url.pathname === "/events") {
276
+ const token = url.searchParams.get("session");
277
+ if (token !== sessionToken) {
278
+ res.writeHead(403, { "Content-Type": "text/plain" });
279
+ res.end("Invalid session");
280
+ return;
281
+ }
282
+ if (state === "COMPLETED") {
283
+ sendJson(res, 409, { ok: false, error: "No events available" });
284
+ return;
285
+ }
286
+ if (sseResponse) {
287
+ try { sseResponse.end(); } catch {}
288
+ }
289
+ res.writeHead(200, {
290
+ "Content-Type": "text/event-stream",
291
+ "Cache-Control": "no-cache",
292
+ Connection: "keep-alive",
293
+ "X-Accel-Buffering": "no",
294
+ });
295
+ res.flushHeaders();
296
+ if (res.socket) res.socket.setNoDelay(true);
297
+ sseResponse = res;
298
+ if (sseBuffer.length > 0) {
299
+ const pending = sseBuffer.splice(0, sseBuffer.length);
300
+ for (let i = 0; i < pending.length; i++) {
301
+ const msg = pending[i];
302
+ try {
303
+ res.write(msg);
304
+ } catch {
305
+ sseBuffer.unshift(...pending.slice(i));
306
+ break;
307
+ }
308
+ }
309
+ }
310
+ if (sseKeepalive) clearInterval(sseKeepalive);
311
+ sseKeepalive = setInterval(() => {
312
+ if (sseResponse) {
313
+ try { sseResponse.write(":keepalive\n\n"); } catch {}
314
+ }
315
+ }, 15000);
316
+ req.on("close", () => {
317
+ if (sseResponse === res) sseResponse = null;
318
+ });
319
+ return;
320
+ }
321
+
322
+ if (method === "POST" && url.pathname === "/heartbeat") {
323
+ const body = await parseBodyOrSend(req, res);
324
+ if (!body) return;
325
+ if (!validateToken(body, res)) return;
326
+ touchHeartbeat();
327
+ sendJson(res, 200, { ok: true });
328
+ return;
329
+ }
330
+
331
+ if (method === "POST" && url.pathname === "/provider") {
332
+ const body = await parseBodyOrSend(req, res);
333
+ if (!body) return;
334
+ if (!validateToken(body, res)) return;
335
+ const { provider } = body as { provider?: string };
336
+ if (typeof provider !== "string" || provider.length === 0) {
337
+ sendJson(res, 400, { ok: false, error: "Invalid provider" });
338
+ return;
339
+ }
340
+ if (!isAvailableProvider(provider)) {
341
+ sendJson(res, 400, { ok: false, error: `Provider unavailable: ${provider}` });
342
+ return;
343
+ }
344
+ setImmediate(() => callbacks.onProviderChange(provider));
345
+ sendJson(res, 200, { ok: true });
346
+ return;
347
+ }
348
+
349
+ if (method === "POST" && url.pathname === "/search") {
350
+ const body = await parseBodyOrSend(req, res);
351
+ if (!body) return;
352
+ if (!validateToken(body, res)) return;
353
+ if (state === "COMPLETED") {
354
+ sendJson(res, 409, { ok: false, error: "Session closed" });
355
+ return;
356
+ }
357
+ const { query, provider } = body as { query?: string; provider?: string };
358
+ if (typeof query !== "string" || query.trim().length === 0) {
359
+ sendJson(res, 400, { ok: false, error: "Invalid query" });
360
+ return;
361
+ }
362
+ if (provider !== undefined) {
363
+ if (typeof provider !== "string" || provider.length === 0) {
364
+ sendJson(res, 400, { ok: false, error: "Invalid provider" });
365
+ return;
366
+ }
367
+ if (!isAvailableProvider(provider)) {
368
+ sendJson(res, 400, { ok: false, error: `Provider unavailable: ${provider}` });
369
+ return;
370
+ }
371
+ }
372
+ const qi = nextQueryIndex++;
373
+ touchHeartbeat();
374
+ try {
375
+ const result = await callbacks.onAddSearch(query.trim(), qi, provider);
376
+ sendJson(res, 200, {
377
+ ok: true,
378
+ queryIndex: qi,
379
+ answer: result.answer,
380
+ results: result.results,
381
+ provider: result.provider,
382
+ });
383
+ } catch (err) {
384
+ const message = err instanceof Error ? err.message : "Search failed";
385
+ sendJson(res, 200, {
386
+ ok: true,
387
+ queryIndex: qi,
388
+ error: message,
389
+ provider: typeof provider === "string" && provider.length > 0 ? provider : undefined,
390
+ });
391
+ }
392
+ return;
393
+ }
394
+
395
+ if (method === "POST" && url.pathname === "/summarize") {
396
+ const body = await parseBodyOrSend(req, res);
397
+ if (!body) return;
398
+ if (!validateToken(body, res)) return;
399
+ if (state === "COMPLETED") {
400
+ sendJson(res, 409, { ok: false, error: "Session closed" });
401
+ return;
402
+ }
403
+
404
+ const parsed = normalizeSelectedIndices((body as { selected?: unknown }).selected, {
405
+ allowEmpty: false,
406
+ maxExclusive: nextQueryIndex,
407
+ });
408
+ if (!parsed.ok) {
409
+ sendJson(res, 400, { ok: false, error: parsed.error });
410
+ return;
411
+ }
412
+
413
+ let model: string | undefined;
414
+ const bodyModel = (body as { model?: unknown }).model;
415
+ if (bodyModel !== undefined) {
416
+ if (typeof bodyModel !== "string") {
417
+ sendJson(res, 400, { ok: false, error: "Invalid model" });
418
+ return;
419
+ }
420
+ const trimmedModel = bodyModel.trim();
421
+ model = trimmedModel.length > 0 ? trimmedModel : undefined;
422
+ }
423
+
424
+ const bodyFeedback = (body as { feedback?: unknown }).feedback;
425
+ const feedback = typeof bodyFeedback === "string" && bodyFeedback.trim().length > 0
426
+ ? bodyFeedback.trim()
427
+ : undefined;
428
+
429
+ abortInFlightSummarize();
430
+ const controller = new AbortController();
431
+ summarizeAbortController = controller;
432
+ const requestId = ++summarizeRequestSeq;
433
+
434
+ try {
435
+ const result = await callbacks.onSummarize(parsed.indices, controller.signal, model, feedback);
436
+ if (requestId !== summarizeRequestSeq || state === "COMPLETED") {
437
+ sendJson(res, 409, { ok: false, error: "Summarize request superseded" });
438
+ return;
439
+ }
440
+ sendJson(res, 200, {
441
+ ok: true,
442
+ summary: result.summary,
443
+ meta: result.meta,
444
+ });
445
+ } catch (err) {
446
+ const message = err instanceof Error ? err.message : "Summary generation failed";
447
+ const status = controller.signal.aborted ? 409 : 500;
448
+ sendJson(res, status, { ok: false, error: message });
449
+ } finally {
450
+ if (summarizeAbortController === controller) {
451
+ summarizeAbortController = null;
452
+ }
453
+ }
454
+ return;
455
+ }
456
+
457
+ if (method === "POST" && url.pathname === "/rewrite") {
458
+ const body = await parseBodyOrSend(req, res);
459
+ if (!body) return;
460
+ if (!validateToken(body, res)) return;
461
+ if (state === "COMPLETED") {
462
+ sendJson(res, 409, { ok: false, error: "Session closed" });
463
+ return;
464
+ }
465
+ const { query } = body as { query?: unknown };
466
+ if (typeof query !== "string" || query.trim().length === 0) {
467
+ sendJson(res, 400, { ok: false, error: "Invalid query" });
468
+ return;
469
+ }
470
+ const controller = new AbortController();
471
+ req.on("close", () => controller.abort());
472
+ touchHeartbeat();
473
+ try {
474
+ const rewritten = await callbacks.onRewriteQuery(query.trim(), controller.signal);
475
+ sendJson(res, 200, { ok: true, query: rewritten });
476
+ } catch (err) {
477
+ const message = err instanceof Error ? err.message : "Rewrite failed";
478
+ const status = controller.signal.aborted ? 409 : 500;
479
+ sendJson(res, status, { ok: false, error: message });
480
+ }
481
+ return;
482
+ }
483
+
484
+ if (method === "POST" && url.pathname === "/submit") {
485
+ const body = await parseBodyOrSend(req, res);
486
+ if (!body) return;
487
+ if (!validateToken(body, res)) return;
488
+
489
+ const parsed = normalizeSelectedIndices((body as { selected?: unknown }).selected, {
490
+ allowEmpty: true,
491
+ maxExclusive: nextQueryIndex,
492
+ });
493
+ if (!parsed.ok) {
494
+ sendJson(res, 400, { ok: false, error: parsed.error });
495
+ return;
496
+ }
497
+
498
+ let summary: string | undefined;
499
+ const bodySummary = (body as { summary?: unknown }).summary;
500
+ if (bodySummary !== undefined) {
501
+ if (typeof bodySummary !== "string") {
502
+ sendJson(res, 400, { ok: false, error: "Invalid summary" });
503
+ return;
504
+ }
505
+ const trimmedSummary = bodySummary.trim();
506
+ summary = trimmedSummary.length > 0 ? trimmedSummary : undefined;
507
+ }
508
+
509
+ let summaryMeta: SummaryMeta | undefined;
510
+ const bodySummaryMeta = (body as { summaryMeta?: unknown }).summaryMeta;
511
+ if (bodySummaryMeta !== undefined) {
512
+ const parsedSummaryMeta = normalizeSummaryMeta(bodySummaryMeta);
513
+ if (!parsedSummaryMeta) {
514
+ sendJson(res, 400, { ok: false, error: "Invalid summaryMeta" });
515
+ return;
516
+ }
517
+ summaryMeta = parsedSummaryMeta;
518
+ }
519
+
520
+ if (state !== "SEARCHING" && state !== "RESULT_SELECTION") {
521
+ sendJson(res, 409, { ok: false, error: "Cannot submit in current state" });
522
+ return;
523
+ }
524
+ if (!markCompleted()) {
525
+ sendJson(res, 409, { ok: false, error: "Session closed" });
526
+ return;
527
+ }
528
+ const rawResults = (body as { rawResults?: unknown }).rawResults === true;
529
+ sendJson(res, 200, { ok: true });
530
+ setImmediate(() => callbacks.onSubmit({ selectedQueryIndices: parsed.indices, summary, summaryMeta, rawResults }));
531
+ return;
532
+ }
533
+
534
+ if (method === "POST" && url.pathname === "/cancel") {
535
+ const body = await parseBodyOrSend(req, res);
536
+ if (!body) return;
537
+ if (!validateToken(body, res)) return;
538
+ if (!markCompleted()) {
539
+ sendJson(res, 200, { ok: true });
540
+ return;
541
+ }
542
+ const { reason } = body as { reason?: string };
543
+ sendJson(res, 200, { ok: true });
544
+ const cancelReason = reason === "timeout" ? "timeout" : "user";
545
+ setImmediate(() => callbacks.onCancel(cancelReason));
546
+ return;
547
+ }
548
+
549
+ res.writeHead(404, { "Content-Type": "text/plain" });
550
+ res.end("Not found");
551
+ } catch (err) {
552
+ const message = err instanceof Error ? err.message : "Server error";
553
+ sendJson(res, 500, { ok: false, error: message });
554
+ }
555
+ });
556
+
557
+ return new Promise((resolve, reject) => {
558
+ const onError = (err: Error) => {
559
+ reject(new Error(`Curator server failed to start: ${err.message}`));
560
+ };
561
+
562
+ server.once("error", onError);
563
+ server.listen(0, "127.0.0.1", () => {
564
+ server.off("error", onError);
565
+ const addr = server.address();
566
+ if (!addr || typeof addr === "string") {
567
+ reject(new Error("Curator server: invalid address"));
568
+ return;
569
+ }
570
+ const url = `http://localhost:${addr.port}/?session=${sessionToken}`;
571
+
572
+ watchdog = setInterval(() => {
573
+ if (completed || !browserConnected) return;
574
+ if (Date.now() - lastHeartbeatAt <= STALE_THRESHOLD_MS) return;
575
+ if (!markCompleted()) return;
576
+ setImmediate(() => callbacks.onCancel("stale"));
577
+ }, WATCHDOG_INTERVAL_MS);
578
+
579
+ resolve({
580
+ server,
581
+ url,
582
+ close: () => {
583
+ const wasOpen = markCompleted();
584
+ try { server.close(); } catch {}
585
+ if (wasOpen) {
586
+ setImmediate(() => callbacks.onCancel("stale"));
587
+ }
588
+ },
589
+ pushResult: (queryIndex, data) => {
590
+ if (completed) return;
591
+ sendSSE("result", { queryIndex, query: queries[queryIndex] ?? "", ...data });
592
+ },
593
+ pushError: (queryIndex, error, provider) => {
594
+ if (completed) return;
595
+ sendSSE("search-error", { queryIndex, query: queries[queryIndex] ?? "", error, provider });
596
+ },
597
+ searchesDone: () => {
598
+ if (completed) return;
599
+ sendSSE("done", {});
600
+ state = "RESULT_SELECTION";
601
+ },
602
+ });
603
+ });
604
+ });
605
+ }