@duckmind/dm-windows-x64 0.63.0 → 0.63.6
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.
- package/dm.exe +0 -0
- package/extensions/.dm-extensions.json +200 -4
- package/extensions/dm-context/package.json +1 -1
- package/extensions/dm-context/skills/context-management/SKILL.md +173 -223
- package/extensions/dm-context/skills/context-management/references/development-and-troubleshooting.md +75 -0
- package/extensions/dm-context/skills/context-management/references/interleaved-async-work.md +143 -0
- package/extensions/dm-context/skills/context-management/references/planning-and-execution.md +81 -0
- package/extensions/dm-context/skills/context-management/references/repeated-items-and-batch-work.md +69 -0
- package/extensions/dm-context/skills/context-management/references/retry-branch-and-pivot.md +80 -0
- package/extensions/dm-context/skills/context-management/references/search-research-and-reading.md +103 -0
- package/extensions/dm-context/skills/context-management/references/task-switching-and-cleanup.md +73 -0
- package/extensions/dm-context/src/context.js +3 -2
- package/extensions/dm-context/src/index.js +168 -84
- package/extensions/dm-skills-manager/THIRD_PARTY_NOTICES.md +27 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/components.js +265 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/constants.js +31 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/creation-fallback.js +20 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/creation.js +145 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/dialog.js +738 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/dm-ai-compat.js +15 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/format.js +125 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/glyphs.js +142 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/layout.js +93 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/paths.js +76 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/registry.js +95 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/settings.js +93 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/startup.js +32 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/toggle.js +54 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/types.js +14 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/ui.js +121 -0
- package/extensions/dm-skills-manager/extensions/skills-manager.js +111 -0
- package/extensions/dm-skills-manager/package.json +121 -0
- package/extensions/dm-web-access/SECURITY.md +5 -0
- package/extensions/dm-web-access/activity.js +65 -0
- package/extensions/dm-web-access/anysearch.js +158 -0
- package/extensions/dm-web-access/auth-fetch.js +131 -0
- package/extensions/dm-web-access/bocha.js +214 -0
- package/extensions/dm-web-access/brave.js +196 -0
- package/extensions/dm-web-access/brightdata-unlocker.js +202 -0
- package/extensions/dm-web-access/brightdata.js +334 -0
- package/extensions/dm-web-access/chrome-cookies.js +627 -0
- package/extensions/dm-web-access/content-find.js +114 -0
- package/extensions/dm-web-access/credential-source.js +150 -0
- package/extensions/dm-web-access/curator-page.js +3559 -0
- package/extensions/dm-web-access/curator-server.js +691 -0
- package/extensions/dm-web-access/data-uri-sanitize.js +312 -0
- package/extensions/dm-web-access/datalab-pdf-extract.js +346 -0
- package/extensions/dm-web-access/declared-web-links.js +167 -0
- package/extensions/dm-web-access/dm-web-fetch-demo.mp4 +0 -0
- package/extensions/dm-web-access/duckduckgo.js +118 -0
- package/extensions/dm-web-access/exa.js +401 -0
- package/extensions/dm-web-access/extract.js +1220 -0
- package/extensions/dm-web-access/feature-config.js +24 -0
- package/extensions/dm-web-access/fetch-params.js +81 -0
- package/extensions/dm-web-access/firecrawl.js +378 -0
- package/extensions/dm-web-access/gemini-adc.js +241 -0
- package/extensions/dm-web-access/gemini-api.js +258 -0
- package/extensions/dm-web-access/gemini-pdf-extract.js +74 -0
- package/extensions/dm-web-access/gemini-search.js +889 -0
- package/extensions/dm-web-access/gemini-url-context.js +97 -0
- package/extensions/dm-web-access/gemini-web-config.js +84 -0
- package/extensions/dm-web-access/gemini-web.js +351 -0
- package/extensions/dm-web-access/github-api.js +166 -0
- package/extensions/dm-web-access/github-extract.js +991 -0
- package/extensions/dm-web-access/github-issue-pr.js +750 -0
- package/extensions/dm-web-access/index.js +3117 -0
- package/extensions/dm-web-access/jina-search.js +242 -0
- package/extensions/dm-web-access/kagi.js +255 -0
- package/extensions/dm-web-access/kimi-search.js +214 -0
- package/extensions/dm-web-access/ollama.js +209 -0
- package/extensions/dm-web-access/openai-search.js +510 -0
- package/extensions/dm-web-access/package.json +34 -0
- package/extensions/dm-web-access/page-query.js +124 -0
- package/extensions/dm-web-access/parallel-mcp.js +223 -0
- package/extensions/dm-web-access/parallel.js +345 -0
- package/extensions/dm-web-access/pdf-extract.js +257 -0
- package/extensions/dm-web-access/perplexity.js +151 -0
- package/extensions/dm-web-access/querit.js +327 -0
- package/extensions/dm-web-access/query-rewrite.js +40 -0
- package/extensions/dm-web-access/render-search-error.js +80 -0
- package/extensions/dm-web-access/rsc-extract.js +347 -0
- package/extensions/dm-web-access/search1api.js +245 -0
- package/extensions/dm-web-access/searchinfinity.js +221 -0
- package/extensions/dm-web-access/searxng.js +223 -0
- package/extensions/dm-web-access/serpbase.js +205 -0
- package/extensions/dm-web-access/serpdive.js +238 -0
- package/extensions/dm-web-access/serper.js +200 -0
- package/extensions/dm-web-access/source-check.js +198 -0
- package/extensions/dm-web-access/ssrf-protection.js +436 -0
- package/extensions/dm-web-access/storage.js +451 -0
- package/extensions/dm-web-access/summary-model-scope.js +83 -0
- package/extensions/dm-web-access/summary-review.js +364 -0
- package/extensions/dm-web-access/tavily.js +199 -0
- package/extensions/dm-web-access/tinyfish.js +325 -0
- package/extensions/dm-web-access/utils.js +476 -0
- package/extensions/dm-web-access/valyu.js +189 -0
- package/extensions/dm-web-access/video-extract.js +336 -0
- package/extensions/dm-web-access/xai-search.js +285 -0
- package/extensions/dm-web-access/xcrawl.js +221 -0
- package/extensions/dm-web-access/youtube-extract.js +279 -0
- package/package.json +9 -1
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import { generateCuratorPage } from "./curator-page.js";
|
|
3
|
+
import { resolveCuratorNetworkConfig } from "./utils.js";
|
|
4
|
+
const STALE_THRESHOLD_MS = 30000;
|
|
5
|
+
const WATCHDOG_INTERVAL_MS = 1000;
|
|
6
|
+
const MAX_BODY_SIZE = 64 * 1024;
|
|
7
|
+
function sendJson(res, status, payload) {
|
|
8
|
+
res.writeHead(status, {
|
|
9
|
+
"Content-Type": "application/json",
|
|
10
|
+
"Cache-Control": "no-store"
|
|
11
|
+
});
|
|
12
|
+
res.end(JSON.stringify(payload));
|
|
13
|
+
}
|
|
14
|
+
function parseJSONBody(req) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
let body = "";
|
|
17
|
+
let size = 0;
|
|
18
|
+
req.on("data", (chunk) => {
|
|
19
|
+
size += chunk.length;
|
|
20
|
+
if (size > MAX_BODY_SIZE) {
|
|
21
|
+
req.destroy();
|
|
22
|
+
reject(new Error("Request body too large"));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
body += chunk.toString();
|
|
26
|
+
});
|
|
27
|
+
req.on("end", () => {
|
|
28
|
+
try {
|
|
29
|
+
resolve(JSON.parse(body));
|
|
30
|
+
} catch (err) {
|
|
31
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
32
|
+
reject(new Error(`Invalid JSON: ${message}`));
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
req.on("error", reject);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
async function parseBodyOrSend(req, res) {
|
|
39
|
+
try {
|
|
40
|
+
return await parseJSONBody(req);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
const message = err instanceof Error ? err.message : "Invalid body";
|
|
43
|
+
const status = message === "Request body too large" ? 413 : 400;
|
|
44
|
+
sendJson(res, status, { ok: false, error: message });
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function normalizeSelectedIndices(value, options) {
|
|
49
|
+
if (!Array.isArray(value)) {
|
|
50
|
+
return { ok: false, error: "Invalid selection" };
|
|
51
|
+
}
|
|
52
|
+
if (!options.allowEmpty && value.length === 0) {
|
|
53
|
+
return { ok: false, error: "Invalid selection" };
|
|
54
|
+
}
|
|
55
|
+
const normalized = [];
|
|
56
|
+
const seen = new Set;
|
|
57
|
+
for (const item of value) {
|
|
58
|
+
if (typeof item !== "number" || !Number.isInteger(item) || item < 0) {
|
|
59
|
+
return { ok: false, error: "Invalid selection" };
|
|
60
|
+
}
|
|
61
|
+
if (item >= options.maxExclusive) {
|
|
62
|
+
return { ok: false, error: "Invalid selection" };
|
|
63
|
+
}
|
|
64
|
+
if (seen.has(item)) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
seen.add(item);
|
|
68
|
+
normalized.push(item);
|
|
69
|
+
}
|
|
70
|
+
if (!options.allowEmpty && normalized.length === 0) {
|
|
71
|
+
return { ok: false, error: "Invalid selection" };
|
|
72
|
+
}
|
|
73
|
+
return { ok: true, indices: normalized };
|
|
74
|
+
}
|
|
75
|
+
function normalizeSummaryMeta(value) {
|
|
76
|
+
if (!value || typeof value !== "object")
|
|
77
|
+
return null;
|
|
78
|
+
const meta = value;
|
|
79
|
+
const model = meta.model === null ? null : typeof meta.model === "string" ? meta.model : undefined;
|
|
80
|
+
if (model === undefined)
|
|
81
|
+
return null;
|
|
82
|
+
const durationMs = meta.durationMs;
|
|
83
|
+
if (typeof durationMs !== "number" || !Number.isFinite(durationMs) || durationMs < 0)
|
|
84
|
+
return null;
|
|
85
|
+
const tokenEstimate = meta.tokenEstimate;
|
|
86
|
+
if (typeof tokenEstimate !== "number" || !Number.isFinite(tokenEstimate) || tokenEstimate < 0)
|
|
87
|
+
return null;
|
|
88
|
+
const fallbackUsed = meta.fallbackUsed;
|
|
89
|
+
if (typeof fallbackUsed !== "boolean")
|
|
90
|
+
return null;
|
|
91
|
+
const fallbackReason = typeof meta.fallbackReason === "string" ? meta.fallbackReason : undefined;
|
|
92
|
+
if (meta.fallbackReason !== undefined && fallbackReason === undefined)
|
|
93
|
+
return null;
|
|
94
|
+
const phase = meta.phase === "summary-model" || meta.phase === "deterministic-fallback" ? meta.phase : undefined;
|
|
95
|
+
if (meta.phase !== undefined && phase === undefined)
|
|
96
|
+
return null;
|
|
97
|
+
if (phase === "deterministic-fallback" && fallbackUsed !== true)
|
|
98
|
+
return null;
|
|
99
|
+
if (phase === "summary-model" && fallbackUsed !== false)
|
|
100
|
+
return null;
|
|
101
|
+
const edited = typeof meta.edited === "boolean" ? meta.edited : undefined;
|
|
102
|
+
if (meta.edited !== undefined && edited === undefined)
|
|
103
|
+
return null;
|
|
104
|
+
return {
|
|
105
|
+
model,
|
|
106
|
+
durationMs,
|
|
107
|
+
tokenEstimate,
|
|
108
|
+
fallbackUsed,
|
|
109
|
+
...fallbackReason !== undefined ? { fallbackReason } : {},
|
|
110
|
+
...phase !== undefined ? { phase } : {},
|
|
111
|
+
...edited !== undefined ? { edited } : {}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export function startCuratorServer(options, callbacks) {
|
|
115
|
+
const {
|
|
116
|
+
queries,
|
|
117
|
+
sessionToken,
|
|
118
|
+
timeout,
|
|
119
|
+
availableProviders,
|
|
120
|
+
defaultProvider,
|
|
121
|
+
searchProvider,
|
|
122
|
+
summaryModels,
|
|
123
|
+
defaultSummaryModel
|
|
124
|
+
} = options;
|
|
125
|
+
let browserConnected = false;
|
|
126
|
+
let lastHeartbeatAt = Date.now();
|
|
127
|
+
let stateChangedAt = Date.now();
|
|
128
|
+
let clientIdleMs = null;
|
|
129
|
+
let clientTimeoutSeconds = timeout;
|
|
130
|
+
let completed = false;
|
|
131
|
+
let watchdog = null;
|
|
132
|
+
let state = "SEARCHING";
|
|
133
|
+
let sseResponse = null;
|
|
134
|
+
const streamedEventsByResultIndex = new Map;
|
|
135
|
+
let searchStreamDone = queries.length === 0;
|
|
136
|
+
let nextQueryIndex = queries.length;
|
|
137
|
+
let summarizeAbortController = null;
|
|
138
|
+
let summarizeRequestSeq = 0;
|
|
139
|
+
let sseKeepalive = null;
|
|
140
|
+
const abortInFlightSummarize = () => {
|
|
141
|
+
if (!summarizeAbortController)
|
|
142
|
+
return;
|
|
143
|
+
summarizeAbortController.abort();
|
|
144
|
+
summarizeAbortController = null;
|
|
145
|
+
};
|
|
146
|
+
const markCompleted = () => {
|
|
147
|
+
if (completed)
|
|
148
|
+
return false;
|
|
149
|
+
completed = true;
|
|
150
|
+
state = "COMPLETED";
|
|
151
|
+
stateChangedAt = Date.now();
|
|
152
|
+
if (watchdog) {
|
|
153
|
+
clearInterval(watchdog);
|
|
154
|
+
watchdog = null;
|
|
155
|
+
}
|
|
156
|
+
if (sseKeepalive) {
|
|
157
|
+
clearInterval(sseKeepalive);
|
|
158
|
+
sseKeepalive = null;
|
|
159
|
+
}
|
|
160
|
+
abortInFlightSummarize();
|
|
161
|
+
if (sseResponse) {
|
|
162
|
+
try {
|
|
163
|
+
sseResponse.end();
|
|
164
|
+
} catch {}
|
|
165
|
+
sseResponse = null;
|
|
166
|
+
}
|
|
167
|
+
return true;
|
|
168
|
+
};
|
|
169
|
+
const touchHeartbeat = () => {
|
|
170
|
+
lastHeartbeatAt = Date.now();
|
|
171
|
+
browserConnected = true;
|
|
172
|
+
};
|
|
173
|
+
const getEffectiveTimeoutMs = () => Math.max(1000, Math.floor(clientTimeoutSeconds) * 1000);
|
|
174
|
+
const shouldTimeoutFromClientIdle = () => state === "RESULT_SELECTION" && clientIdleMs !== null && clientIdleMs >= getEffectiveTimeoutMs();
|
|
175
|
+
function validateToken(body, res) {
|
|
176
|
+
if (!body || typeof body !== "object") {
|
|
177
|
+
sendJson(res, 400, { ok: false, error: "Invalid body" });
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
if (body.token !== sessionToken) {
|
|
181
|
+
sendJson(res, 403, { ok: false, error: "Invalid session" });
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
function isAvailableProvider(provider) {
|
|
187
|
+
if (provider === "all")
|
|
188
|
+
return availableProviders.all;
|
|
189
|
+
if (provider === "openai")
|
|
190
|
+
return availableProviders.openai;
|
|
191
|
+
if (provider === "brave")
|
|
192
|
+
return availableProviders.brave;
|
|
193
|
+
if (provider === "parallel")
|
|
194
|
+
return availableProviders.parallel;
|
|
195
|
+
if (provider === "parallel-mcp")
|
|
196
|
+
return availableProviders["parallel-mcp"];
|
|
197
|
+
if (provider === "tinyfish")
|
|
198
|
+
return availableProviders.tinyfish;
|
|
199
|
+
if (provider === "search1api")
|
|
200
|
+
return availableProviders.search1api;
|
|
201
|
+
if (provider === "searchinfinity")
|
|
202
|
+
return availableProviders.searchinfinity;
|
|
203
|
+
if (provider === "querit")
|
|
204
|
+
return availableProviders.querit;
|
|
205
|
+
if (provider === "tavily")
|
|
206
|
+
return availableProviders.tavily;
|
|
207
|
+
if (provider === "firecrawl")
|
|
208
|
+
return availableProviders.firecrawl;
|
|
209
|
+
if (provider === "jina")
|
|
210
|
+
return availableProviders.jina;
|
|
211
|
+
if (provider === "serpdive")
|
|
212
|
+
return availableProviders.serpdive;
|
|
213
|
+
if (provider === "kagi")
|
|
214
|
+
return availableProviders.kagi;
|
|
215
|
+
if (provider === "bocha")
|
|
216
|
+
return availableProviders.bocha;
|
|
217
|
+
if (provider === "ollama")
|
|
218
|
+
return availableProviders.ollama;
|
|
219
|
+
if (provider === "searxng")
|
|
220
|
+
return availableProviders.searxng;
|
|
221
|
+
if (provider === "duckduckgo")
|
|
222
|
+
return availableProviders.duckduckgo;
|
|
223
|
+
if (provider === "perplexity")
|
|
224
|
+
return availableProviders.perplexity;
|
|
225
|
+
if (provider === "exa")
|
|
226
|
+
return availableProviders.exa;
|
|
227
|
+
if (provider === "gemini")
|
|
228
|
+
return availableProviders.gemini;
|
|
229
|
+
if (provider === "kimi")
|
|
230
|
+
return availableProviders.kimi;
|
|
231
|
+
if (provider === "anysearch")
|
|
232
|
+
return availableProviders.anysearch;
|
|
233
|
+
if (provider === "xcrawl")
|
|
234
|
+
return availableProviders.xcrawl;
|
|
235
|
+
if (provider === "xai")
|
|
236
|
+
return availableProviders.xai;
|
|
237
|
+
if (provider === "brightdata")
|
|
238
|
+
return availableProviders.brightdata;
|
|
239
|
+
if (provider === "serpbase")
|
|
240
|
+
return availableProviders.serpbase;
|
|
241
|
+
if (provider === "serper")
|
|
242
|
+
return availableProviders.serper;
|
|
243
|
+
if (provider === "valyu")
|
|
244
|
+
return availableProviders.valyu;
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
function writeSSE(res, event, data) {
|
|
248
|
+
const payload = `event: ${event}
|
|
249
|
+
data: ${JSON.stringify(data)}
|
|
250
|
+
|
|
251
|
+
`;
|
|
252
|
+
try {
|
|
253
|
+
res.write(payload);
|
|
254
|
+
return true;
|
|
255
|
+
} catch {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function sendSSE(event, data) {
|
|
260
|
+
const res = sseResponse;
|
|
261
|
+
if (res && !res.writableEnded && res.socket && !res.socket.destroyed && writeSSE(res, event, data))
|
|
262
|
+
return;
|
|
263
|
+
if (sseResponse === res)
|
|
264
|
+
sseResponse = null;
|
|
265
|
+
}
|
|
266
|
+
function retainStreamedEvent(event) {
|
|
267
|
+
streamedEventsByResultIndex.set(event.data.queryIndex, event);
|
|
268
|
+
}
|
|
269
|
+
function getStreamedEvents() {
|
|
270
|
+
return [...streamedEventsByResultIndex.values()];
|
|
271
|
+
}
|
|
272
|
+
function replaySSE(res) {
|
|
273
|
+
for (const item of getStreamedEvents()) {
|
|
274
|
+
if (!writeSSE(res, item.event, item.data))
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (searchStreamDone)
|
|
278
|
+
writeSSE(res, "done", {});
|
|
279
|
+
}
|
|
280
|
+
const pageHtml = generateCuratorPage(queries, sessionToken, timeout, availableProviders, defaultProvider, searchProvider, summaryModels, defaultSummaryModel);
|
|
281
|
+
const server = http.createServer(async (req, res) => {
|
|
282
|
+
try {
|
|
283
|
+
const method = req.method || "GET";
|
|
284
|
+
const url = new URL(req.url || "/", `http://${req.headers.host || "127.0.0.1"}`);
|
|
285
|
+
if (method === "GET" && url.pathname === "/") {
|
|
286
|
+
const token = url.searchParams.get("session");
|
|
287
|
+
if (token !== sessionToken) {
|
|
288
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
289
|
+
res.end("Invalid session");
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
touchHeartbeat();
|
|
293
|
+
res.writeHead(200, {
|
|
294
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
295
|
+
"Cache-Control": "no-store"
|
|
296
|
+
});
|
|
297
|
+
res.end(pageHtml);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (method === "GET" && url.pathname === "/events") {
|
|
301
|
+
const token = url.searchParams.get("session");
|
|
302
|
+
if (token !== sessionToken) {
|
|
303
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
304
|
+
res.end("Invalid session");
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (state === "COMPLETED") {
|
|
308
|
+
sendJson(res, 409, { ok: false, error: "No events available" });
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (sseResponse) {
|
|
312
|
+
try {
|
|
313
|
+
sseResponse.end();
|
|
314
|
+
} catch {}
|
|
315
|
+
}
|
|
316
|
+
res.writeHead(200, {
|
|
317
|
+
"Content-Type": "text/event-stream",
|
|
318
|
+
"Cache-Control": "no-cache",
|
|
319
|
+
Connection: "keep-alive",
|
|
320
|
+
"X-Accel-Buffering": "no"
|
|
321
|
+
});
|
|
322
|
+
res.flushHeaders();
|
|
323
|
+
if (res.socket)
|
|
324
|
+
res.socket.setNoDelay(true);
|
|
325
|
+
sseResponse = res;
|
|
326
|
+
replaySSE(res);
|
|
327
|
+
if (sseKeepalive)
|
|
328
|
+
clearInterval(sseKeepalive);
|
|
329
|
+
sseKeepalive = setInterval(() => {
|
|
330
|
+
if (sseResponse) {
|
|
331
|
+
try {
|
|
332
|
+
sseResponse.write(`:keepalive
|
|
333
|
+
|
|
334
|
+
`);
|
|
335
|
+
} catch {}
|
|
336
|
+
}
|
|
337
|
+
}, 15000);
|
|
338
|
+
req.on("close", () => {
|
|
339
|
+
if (sseResponse === res)
|
|
340
|
+
sseResponse = null;
|
|
341
|
+
});
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (method === "GET" && url.pathname === "/state") {
|
|
345
|
+
const token = url.searchParams.get("session");
|
|
346
|
+
if (token !== sessionToken) {
|
|
347
|
+
sendJson(res, 403, { ok: false, error: "Invalid session" });
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
touchHeartbeat();
|
|
351
|
+
sendJson(res, 200, { ok: true, events: getStreamedEvents(), done: searchStreamDone });
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (method === "POST" && url.pathname === "/heartbeat") {
|
|
355
|
+
const body = await parseBodyOrSend(req, res);
|
|
356
|
+
if (!body)
|
|
357
|
+
return;
|
|
358
|
+
if (!validateToken(body, res))
|
|
359
|
+
return;
|
|
360
|
+
touchHeartbeat();
|
|
361
|
+
const heartbeat = body;
|
|
362
|
+
if (typeof heartbeat.timeoutSec === "number" && Number.isFinite(heartbeat.timeoutSec) && heartbeat.timeoutSec > 0) {
|
|
363
|
+
clientTimeoutSeconds = Math.min(600, Math.floor(heartbeat.timeoutSec));
|
|
364
|
+
}
|
|
365
|
+
if (typeof heartbeat.idleMs === "number" && Number.isFinite(heartbeat.idleMs) && heartbeat.idleMs >= 0) {
|
|
366
|
+
clientIdleMs = Math.floor(heartbeat.idleMs);
|
|
367
|
+
}
|
|
368
|
+
const timedOut = shouldTimeoutFromClientIdle();
|
|
369
|
+
sendJson(res, 200, { ok: true });
|
|
370
|
+
if (timedOut && markCompleted()) {
|
|
371
|
+
setImmediate(() => callbacks.onCancel("timeout"));
|
|
372
|
+
}
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (method === "POST" && url.pathname === "/provider") {
|
|
376
|
+
const body = await parseBodyOrSend(req, res);
|
|
377
|
+
if (!body)
|
|
378
|
+
return;
|
|
379
|
+
if (!validateToken(body, res))
|
|
380
|
+
return;
|
|
381
|
+
const { provider } = body;
|
|
382
|
+
if (typeof provider !== "string" || provider.length === 0) {
|
|
383
|
+
sendJson(res, 400, { ok: false, error: "Invalid provider" });
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (!isAvailableProvider(provider)) {
|
|
387
|
+
sendJson(res, 400, { ok: false, error: `Provider unavailable: ${provider}` });
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
setImmediate(() => callbacks.onProviderChange(provider));
|
|
391
|
+
sendJson(res, 200, { ok: true });
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (method === "POST" && url.pathname === "/search") {
|
|
395
|
+
const body = await parseBodyOrSend(req, res);
|
|
396
|
+
if (!body)
|
|
397
|
+
return;
|
|
398
|
+
if (!validateToken(body, res))
|
|
399
|
+
return;
|
|
400
|
+
if (state === "COMPLETED") {
|
|
401
|
+
sendJson(res, 409, { ok: false, error: "Session closed" });
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const { query, provider } = body;
|
|
405
|
+
if (typeof query !== "string" || query.trim().length === 0) {
|
|
406
|
+
sendJson(res, 400, { ok: false, error: "Invalid query" });
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (provider !== undefined) {
|
|
410
|
+
if (typeof provider !== "string" || provider.length === 0) {
|
|
411
|
+
sendJson(res, 400, { ok: false, error: "Invalid provider" });
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (!isAvailableProvider(provider)) {
|
|
415
|
+
sendJson(res, 400, { ok: false, error: `Provider unavailable: ${provider}` });
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const qi = nextQueryIndex++;
|
|
420
|
+
const trimmedQuery = query.trim();
|
|
421
|
+
touchHeartbeat();
|
|
422
|
+
try {
|
|
423
|
+
const results = await callbacks.onAddSearch(trimmedQuery, provider);
|
|
424
|
+
if (results.length === 0)
|
|
425
|
+
throw new Error("Search returned no provider results");
|
|
426
|
+
const entries = results.map((result, index) => ({
|
|
427
|
+
...result,
|
|
428
|
+
queryIndex: index === 0 ? qi : nextQueryIndex++,
|
|
429
|
+
query: trimmedQuery
|
|
430
|
+
}));
|
|
431
|
+
callbacks.onAddSearchResults(entries);
|
|
432
|
+
sendJson(res, 200, { ok: true, ...entries[0], entries });
|
|
433
|
+
} catch (err) {
|
|
434
|
+
const message = err instanceof Error ? err.message : "Search failed";
|
|
435
|
+
const entry = {
|
|
436
|
+
queryIndex: qi,
|
|
437
|
+
query: trimmedQuery,
|
|
438
|
+
answer: "",
|
|
439
|
+
results: [],
|
|
440
|
+
error: message,
|
|
441
|
+
provider: typeof provider === "string" && provider.length > 0 ? provider : defaultProvider
|
|
442
|
+
};
|
|
443
|
+
callbacks.onAddSearchResults([entry]);
|
|
444
|
+
sendJson(res, 200, { ok: true, ...entry, entries: [entry] });
|
|
445
|
+
}
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (method === "POST" && url.pathname === "/summarize") {
|
|
449
|
+
const body = await parseBodyOrSend(req, res);
|
|
450
|
+
if (!body)
|
|
451
|
+
return;
|
|
452
|
+
if (!validateToken(body, res))
|
|
453
|
+
return;
|
|
454
|
+
if (state === "COMPLETED") {
|
|
455
|
+
sendJson(res, 409, { ok: false, error: "Session closed" });
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const parsed = normalizeSelectedIndices(body.selected, {
|
|
459
|
+
allowEmpty: false,
|
|
460
|
+
maxExclusive: nextQueryIndex
|
|
461
|
+
});
|
|
462
|
+
if ("error" in parsed) {
|
|
463
|
+
sendJson(res, 400, { ok: false, error: parsed.error });
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
let model;
|
|
467
|
+
const bodyModel = body.model;
|
|
468
|
+
if (bodyModel !== undefined) {
|
|
469
|
+
if (typeof bodyModel !== "string") {
|
|
470
|
+
sendJson(res, 400, { ok: false, error: "Invalid model" });
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const trimmedModel = bodyModel.trim();
|
|
474
|
+
model = trimmedModel.length > 0 ? trimmedModel : undefined;
|
|
475
|
+
}
|
|
476
|
+
const bodyFeedback = body.feedback;
|
|
477
|
+
const feedback = typeof bodyFeedback === "string" && bodyFeedback.trim().length > 0 ? bodyFeedback.trim() : undefined;
|
|
478
|
+
abortInFlightSummarize();
|
|
479
|
+
const controller = new AbortController;
|
|
480
|
+
summarizeAbortController = controller;
|
|
481
|
+
const requestId = ++summarizeRequestSeq;
|
|
482
|
+
try {
|
|
483
|
+
const result = await callbacks.onSummarize(parsed.indices, controller.signal, model, feedback);
|
|
484
|
+
if (requestId !== summarizeRequestSeq || completed) {
|
|
485
|
+
sendJson(res, 409, { ok: false, error: "Summarize request superseded" });
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
sendJson(res, 200, {
|
|
489
|
+
ok: true,
|
|
490
|
+
summary: result.summary,
|
|
491
|
+
meta: result.meta
|
|
492
|
+
});
|
|
493
|
+
} catch (err) {
|
|
494
|
+
const message = err instanceof Error ? err.message : "Summary generation failed";
|
|
495
|
+
const status = controller.signal.aborted ? 409 : 500;
|
|
496
|
+
sendJson(res, status, { ok: false, error: message });
|
|
497
|
+
} finally {
|
|
498
|
+
if (summarizeAbortController === controller) {
|
|
499
|
+
summarizeAbortController = null;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (method === "POST" && url.pathname === "/rewrite") {
|
|
505
|
+
const body = await parseBodyOrSend(req, res);
|
|
506
|
+
if (!body)
|
|
507
|
+
return;
|
|
508
|
+
if (!validateToken(body, res))
|
|
509
|
+
return;
|
|
510
|
+
if (state === "COMPLETED") {
|
|
511
|
+
sendJson(res, 409, { ok: false, error: "Session closed" });
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const { query } = body;
|
|
515
|
+
if (typeof query !== "string" || query.trim().length === 0) {
|
|
516
|
+
sendJson(res, 400, { ok: false, error: "Invalid query" });
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
const controller = new AbortController;
|
|
520
|
+
req.on("close", () => controller.abort());
|
|
521
|
+
touchHeartbeat();
|
|
522
|
+
try {
|
|
523
|
+
const rewritten = await callbacks.onRewriteQuery(query.trim(), controller.signal);
|
|
524
|
+
sendJson(res, 200, { ok: true, query: rewritten });
|
|
525
|
+
} catch (err) {
|
|
526
|
+
const message = err instanceof Error ? err.message : "Rewrite failed";
|
|
527
|
+
const status = controller.signal.aborted ? 409 : 500;
|
|
528
|
+
sendJson(res, status, { ok: false, error: message });
|
|
529
|
+
}
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
if (method === "POST" && url.pathname === "/submit") {
|
|
533
|
+
const body = await parseBodyOrSend(req, res);
|
|
534
|
+
if (!body)
|
|
535
|
+
return;
|
|
536
|
+
if (!validateToken(body, res))
|
|
537
|
+
return;
|
|
538
|
+
const parsed = normalizeSelectedIndices(body.selected, {
|
|
539
|
+
allowEmpty: true,
|
|
540
|
+
maxExclusive: nextQueryIndex
|
|
541
|
+
});
|
|
542
|
+
if ("error" in parsed) {
|
|
543
|
+
sendJson(res, 400, { ok: false, error: parsed.error });
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
let summary;
|
|
547
|
+
const bodySummary = body.summary;
|
|
548
|
+
if (bodySummary !== undefined) {
|
|
549
|
+
if (typeof bodySummary !== "string") {
|
|
550
|
+
sendJson(res, 400, { ok: false, error: "Invalid summary" });
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const trimmedSummary = bodySummary.trim();
|
|
554
|
+
summary = trimmedSummary.length > 0 ? trimmedSummary : undefined;
|
|
555
|
+
}
|
|
556
|
+
let summaryMeta;
|
|
557
|
+
const bodySummaryMeta = body.summaryMeta;
|
|
558
|
+
if (bodySummaryMeta !== undefined) {
|
|
559
|
+
const parsedSummaryMeta = normalizeSummaryMeta(bodySummaryMeta);
|
|
560
|
+
if (!parsedSummaryMeta) {
|
|
561
|
+
sendJson(res, 400, { ok: false, error: "Invalid summaryMeta" });
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
summaryMeta = parsedSummaryMeta;
|
|
565
|
+
}
|
|
566
|
+
if (state !== "SEARCHING" && state !== "RESULT_SELECTION") {
|
|
567
|
+
sendJson(res, 409, { ok: false, error: "Cannot submit in current state" });
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (!markCompleted()) {
|
|
571
|
+
sendJson(res, 409, { ok: false, error: "Session closed" });
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
const rawResults = body.rawResults === true;
|
|
575
|
+
sendJson(res, 200, { ok: true });
|
|
576
|
+
setImmediate(() => callbacks.onSubmit({
|
|
577
|
+
selectedQueryIndices: parsed.indices,
|
|
578
|
+
...summary !== undefined ? { summary } : {},
|
|
579
|
+
...summaryMeta !== undefined ? { summaryMeta } : {},
|
|
580
|
+
rawResults
|
|
581
|
+
}));
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (method === "POST" && url.pathname === "/cancel") {
|
|
585
|
+
const body = await parseBodyOrSend(req, res);
|
|
586
|
+
if (!body)
|
|
587
|
+
return;
|
|
588
|
+
if (!validateToken(body, res))
|
|
589
|
+
return;
|
|
590
|
+
if (!markCompleted()) {
|
|
591
|
+
sendJson(res, 200, { ok: true });
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
const { reason } = body;
|
|
595
|
+
sendJson(res, 200, { ok: true });
|
|
596
|
+
const cancelReason = reason === "timeout" ? "timeout" : "user";
|
|
597
|
+
setImmediate(() => callbacks.onCancel(cancelReason));
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
601
|
+
res.end("Not found");
|
|
602
|
+
} catch (err) {
|
|
603
|
+
const message = err instanceof Error ? err.message : "Server error";
|
|
604
|
+
sendJson(res, 500, { ok: false, error: message });
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
return new Promise((resolve, reject) => {
|
|
608
|
+
const onError = (err) => {
|
|
609
|
+
reject(new Error(`Curator server failed to start: ${err.message}`));
|
|
610
|
+
};
|
|
611
|
+
const networkConfig = resolveCuratorNetworkConfig();
|
|
612
|
+
server.once("error", onError);
|
|
613
|
+
server.listen(0, networkConfig.bind, () => {
|
|
614
|
+
server.off("error", onError);
|
|
615
|
+
const addr = server.address();
|
|
616
|
+
if (!addr || typeof addr === "string") {
|
|
617
|
+
reject(new Error("Curator server: invalid address"));
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
const url = `http://${networkConfig.host}:${addr.port}/?session=${sessionToken}`;
|
|
621
|
+
watchdog = setInterval(() => {
|
|
622
|
+
if (completed)
|
|
623
|
+
return;
|
|
624
|
+
if (!browserConnected) {
|
|
625
|
+
const noBrowserTimeoutMs = Math.max(5000, getEffectiveTimeoutMs());
|
|
626
|
+
if (state !== "RESULT_SELECTION")
|
|
627
|
+
return;
|
|
628
|
+
if (Date.now() - stateChangedAt <= noBrowserTimeoutMs)
|
|
629
|
+
return;
|
|
630
|
+
if (!markCompleted())
|
|
631
|
+
return;
|
|
632
|
+
setImmediate(() => callbacks.onCancel("timeout"));
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (shouldTimeoutFromClientIdle()) {
|
|
636
|
+
if (!markCompleted())
|
|
637
|
+
return;
|
|
638
|
+
setImmediate(() => callbacks.onCancel("timeout"));
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (Date.now() - lastHeartbeatAt <= STALE_THRESHOLD_MS)
|
|
642
|
+
return;
|
|
643
|
+
const staleReason = state === "RESULT_SELECTION" ? "timeout" : "stale";
|
|
644
|
+
if (!markCompleted())
|
|
645
|
+
return;
|
|
646
|
+
setImmediate(() => callbacks.onCancel(staleReason));
|
|
647
|
+
}, WATCHDOG_INTERVAL_MS);
|
|
648
|
+
resolve({
|
|
649
|
+
server,
|
|
650
|
+
url,
|
|
651
|
+
close: () => {
|
|
652
|
+
const wasOpen = markCompleted();
|
|
653
|
+
try {
|
|
654
|
+
server.close();
|
|
655
|
+
} catch {}
|
|
656
|
+
if (wasOpen) {
|
|
657
|
+
setImmediate(() => callbacks.onCancel("stale"));
|
|
658
|
+
}
|
|
659
|
+
},
|
|
660
|
+
pushResult: (queryIndex, data) => {
|
|
661
|
+
if (completed)
|
|
662
|
+
return;
|
|
663
|
+
nextQueryIndex = Math.max(nextQueryIndex, queryIndex + 1);
|
|
664
|
+
const eventData = { ...data, queryIndex, query: data.query ?? queries[queryIndex] ?? "" };
|
|
665
|
+
retainStreamedEvent({ event: "result", data: eventData });
|
|
666
|
+
sendSSE("result", eventData);
|
|
667
|
+
},
|
|
668
|
+
pushError: (queryIndex, error, provider, meta) => {
|
|
669
|
+
if (completed)
|
|
670
|
+
return;
|
|
671
|
+
nextQueryIndex = Math.max(nextQueryIndex, queryIndex + 1);
|
|
672
|
+
const eventData = { queryIndex, query: meta?.query ?? queries[queryIndex] ?? "", error, provider, slotIndex: meta?.slotIndex };
|
|
673
|
+
retainStreamedEvent({ event: "search-error", data: eventData });
|
|
674
|
+
sendSSE("search-error", eventData);
|
|
675
|
+
},
|
|
676
|
+
searchesDone: () => {
|
|
677
|
+
if (completed)
|
|
678
|
+
return;
|
|
679
|
+
searchStreamDone = true;
|
|
680
|
+
sendSSE("done", {});
|
|
681
|
+
state = "RESULT_SELECTION";
|
|
682
|
+
stateChangedAt = Date.now();
|
|
683
|
+
},
|
|
684
|
+
getConnectionState: () => ({
|
|
685
|
+
browserConnected,
|
|
686
|
+
lastHeartbeatAgeMs: Date.now() - lastHeartbeatAt
|
|
687
|
+
})
|
|
688
|
+
});
|
|
689
|
+
});
|
|
690
|
+
});
|
|
691
|
+
}
|