@ahkohd/yagami 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.beads/.beads-credential-key +1 -0
- package/.beads/README.md +81 -0
- package/.beads/config.yaml +54 -0
- package/.beads/hooks/post-checkout +24 -0
- package/.beads/hooks/post-merge +24 -0
- package/.beads/hooks/pre-commit +24 -0
- package/.beads/hooks/pre-push +24 -0
- package/.beads/hooks/prepare-commit-msg +24 -0
- package/.beads/metadata.json +7 -0
- package/.github/workflows/ci.yml +43 -0
- package/.github/workflows/release.yml +115 -0
- package/AGENTS.md +150 -0
- package/README.md +210 -0
- package/biome.json +36 -0
- package/config/mcporter.json +8 -0
- package/dist/cli/theme.js +202 -0
- package/dist/cli/theme.js.map +1 -0
- package/dist/cli.js +1883 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.js +223 -0
- package/dist/config.js.map +1 -0
- package/dist/daemon.js +745 -0
- package/dist/daemon.js.map +1 -0
- package/dist/engine/constants.js +131 -0
- package/dist/engine/constants.js.map +1 -0
- package/dist/engine/deep-research.js +167 -0
- package/dist/engine/deep-research.js.map +1 -0
- package/dist/engine/defuddle-utils.js +57 -0
- package/dist/engine/defuddle-utils.js.map +1 -0
- package/dist/engine/github-fetch.js +232 -0
- package/dist/engine/github-fetch.js.map +1 -0
- package/dist/engine/helpers.js +372 -0
- package/dist/engine/helpers.js.map +1 -0
- package/dist/engine/limiter.js +75 -0
- package/dist/engine/limiter.js.map +1 -0
- package/dist/engine/policy.js +313 -0
- package/dist/engine/policy.js.map +1 -0
- package/dist/engine/runtime-utils.js +65 -0
- package/dist/engine/runtime-utils.js.map +1 -0
- package/dist/engine/search-discovery.js +275 -0
- package/dist/engine/search-discovery.js.map +1 -0
- package/dist/engine/url-utils.js +72 -0
- package/dist/engine/url-utils.js.map +1 -0
- package/dist/engine.js +2030 -0
- package/dist/engine.js.map +1 -0
- package/dist/mcp.js +282 -0
- package/dist/mcp.js.map +1 -0
- package/dist/types/cli.js +2 -0
- package/dist/types/cli.js.map +1 -0
- package/dist/types/config.js +2 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/daemon.js +2 -0
- package/dist/types/daemon.js.map +1 -0
- package/dist/types/engine.js +2 -0
- package/dist/types/engine.js.map +1 -0
- package/package.json +66 -0
- package/packages/pi-yagami-search/README.md +39 -0
- package/packages/pi-yagami-search/extensions/yagami-search.ts +273 -0
- package/packages/pi-yagami-search/package.json +41 -0
- package/src/cli/theme.ts +260 -0
- package/src/cli.ts +2226 -0
- package/src/config.ts +250 -0
- package/src/daemon.ts +990 -0
- package/src/engine/constants.ts +147 -0
- package/src/engine/deep-research.ts +207 -0
- package/src/engine/defuddle-utils.ts +75 -0
- package/src/engine/github-fetch.ts +265 -0
- package/src/engine/helpers.ts +394 -0
- package/src/engine/limiter.ts +97 -0
- package/src/engine/policy.ts +392 -0
- package/src/engine/runtime-utils.ts +79 -0
- package/src/engine/search-discovery.ts +351 -0
- package/src/engine/url-utils.ts +86 -0
- package/src/engine.ts +2516 -0
- package/src/mcp.ts +337 -0
- package/src/shims-cli.d.ts +3 -0
- package/src/types/cli.ts +7 -0
- package/src/types/config.ts +53 -0
- package/src/types/daemon.ts +22 -0
- package/src/types/engine.ts +194 -0
- package/tsconfig.json +18 -0
package/src/daemon.ts
ADDED
|
@@ -0,0 +1,990 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import fsp from "node:fs/promises";
|
|
6
|
+
import http from "node:http";
|
|
7
|
+
|
|
8
|
+
import { getConfig } from "./config.js";
|
|
9
|
+
import { YagamiEngine } from "./engine.js";
|
|
10
|
+
import {
|
|
11
|
+
MCP_DEFAULT_PROTOCOL_VERSION,
|
|
12
|
+
MCP_SUPPORTED_PROTOCOL_VERSIONS,
|
|
13
|
+
MCP_TOOL_DEFINITIONS,
|
|
14
|
+
executeMcpTool,
|
|
15
|
+
isKnownMcpTool,
|
|
16
|
+
} from "./mcp.js";
|
|
17
|
+
import type { RuntimeConfig } from "./types/config.js";
|
|
18
|
+
import type { NdjsonEvent } from "./types/daemon.js";
|
|
19
|
+
|
|
20
|
+
const startupConfig = getConfig();
|
|
21
|
+
await fsp.mkdir(startupConfig.runtimeDir, { recursive: true });
|
|
22
|
+
|
|
23
|
+
const serverVersion = await fsp
|
|
24
|
+
.readFile(new URL("../package.json", import.meta.url), "utf8")
|
|
25
|
+
.then((raw) => {
|
|
26
|
+
const parsed = JSON.parse(raw) as { version?: unknown };
|
|
27
|
+
return String(parsed.version || "0.0.0");
|
|
28
|
+
})
|
|
29
|
+
.catch(() => "0.0.0");
|
|
30
|
+
|
|
31
|
+
let runtimeConfig: RuntimeConfig = startupConfig;
|
|
32
|
+
let engine = new YagamiEngine(runtimeConfig, console);
|
|
33
|
+
|
|
34
|
+
let shuttingDown = false;
|
|
35
|
+
let reloading = false;
|
|
36
|
+
|
|
37
|
+
type JsonObject = Record<string, unknown>;
|
|
38
|
+
type JsonRpcId = string | number | null;
|
|
39
|
+
|
|
40
|
+
type McpSessionRecord = {
|
|
41
|
+
sessionId: string;
|
|
42
|
+
protocolVersion: string;
|
|
43
|
+
initialized: boolean;
|
|
44
|
+
createdAt: number;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const mcpSessions = new Map<string, McpSessionRecord>();
|
|
48
|
+
|
|
49
|
+
function sendJson(res: http.ServerResponse, statusCode: number, payload: JsonObject): void {
|
|
50
|
+
const body = JSON.stringify(payload, null, 2);
|
|
51
|
+
res.writeHead(statusCode, {
|
|
52
|
+
"content-type": "application/json; charset=utf-8",
|
|
53
|
+
"content-length": Buffer.byteLength(body),
|
|
54
|
+
});
|
|
55
|
+
res.end(body);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function sendNdjson(res: http.ServerResponse, payload: NdjsonEvent): void {
|
|
59
|
+
res.write(`${JSON.stringify(payload)}\n`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function readJsonBody(req: http.IncomingMessage): Promise<JsonObject> {
|
|
63
|
+
const chunks: Buffer[] = [];
|
|
64
|
+
for await (const chunk of req) {
|
|
65
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (chunks.length === 0) return {};
|
|
69
|
+
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
70
|
+
if (!raw) return {};
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const parsed = JSON.parse(raw);
|
|
74
|
+
return typeof parsed === "object" && parsed !== null ? (parsed as JsonObject) : {};
|
|
75
|
+
} catch {
|
|
76
|
+
throw new Error("Invalid JSON body");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getTrimmedString(value: unknown): string {
|
|
81
|
+
return String(value ?? "").trim();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parsePositiveInt(value: unknown, fallback: number, max = 1000): number {
|
|
85
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
86
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
|
87
|
+
return Math.min(parsed, max);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function toErrorMessage(error: unknown): string {
|
|
91
|
+
return error instanceof Error ? error.message : String(error);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getHeaderString(value: string | string[] | undefined): string {
|
|
95
|
+
if (Array.isArray(value)) {
|
|
96
|
+
return String(value[0] || "").trim();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return String(value || "").trim();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function hasOwnKey(object: JsonObject, key: string): boolean {
|
|
103
|
+
return Object.hasOwn(object, key);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function normalizeJsonRpcId(value: unknown): JsonRpcId | undefined {
|
|
107
|
+
if (typeof value === "string" || typeof value === "number" || value === null) {
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function sendMcpJson(
|
|
115
|
+
res: http.ServerResponse,
|
|
116
|
+
statusCode: number,
|
|
117
|
+
payload: JsonObject,
|
|
118
|
+
headers: JsonObject = {},
|
|
119
|
+
): void {
|
|
120
|
+
const body = JSON.stringify(payload);
|
|
121
|
+
|
|
122
|
+
const finalHeaders: Record<string, string | number> = {
|
|
123
|
+
"content-type": "application/json; charset=utf-8",
|
|
124
|
+
"content-length": Buffer.byteLength(body),
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
128
|
+
if (value === undefined || value === null) continue;
|
|
129
|
+
finalHeaders[key] = String(value);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
res.writeHead(statusCode, finalHeaders);
|
|
133
|
+
res.end(body);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function sendMcpSuccess(
|
|
137
|
+
res: http.ServerResponse,
|
|
138
|
+
id: JsonRpcId,
|
|
139
|
+
result: unknown,
|
|
140
|
+
options: {
|
|
141
|
+
sessionId?: string;
|
|
142
|
+
protocolVersion?: string;
|
|
143
|
+
statusCode?: number;
|
|
144
|
+
} = {},
|
|
145
|
+
): void {
|
|
146
|
+
const headers: JsonObject = {};
|
|
147
|
+
if (options.sessionId) headers["Mcp-Session-Id"] = options.sessionId;
|
|
148
|
+
if (options.protocolVersion) headers["MCP-Protocol-Version"] = options.protocolVersion;
|
|
149
|
+
|
|
150
|
+
sendMcpJson(
|
|
151
|
+
res,
|
|
152
|
+
options.statusCode ?? 200,
|
|
153
|
+
{
|
|
154
|
+
jsonrpc: "2.0",
|
|
155
|
+
id,
|
|
156
|
+
result,
|
|
157
|
+
},
|
|
158
|
+
headers,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function sendMcpError(
|
|
163
|
+
res: http.ServerResponse,
|
|
164
|
+
id: JsonRpcId,
|
|
165
|
+
error: { code: number; message: string; data?: unknown },
|
|
166
|
+
options: {
|
|
167
|
+
sessionId?: string;
|
|
168
|
+
protocolVersion?: string;
|
|
169
|
+
statusCode?: number;
|
|
170
|
+
} = {},
|
|
171
|
+
): void {
|
|
172
|
+
const headers: JsonObject = {};
|
|
173
|
+
if (options.sessionId) headers["Mcp-Session-Id"] = options.sessionId;
|
|
174
|
+
if (options.protocolVersion) headers["MCP-Protocol-Version"] = options.protocolVersion;
|
|
175
|
+
|
|
176
|
+
const payloadError: JsonObject = {
|
|
177
|
+
code: error.code,
|
|
178
|
+
message: error.message,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
if (error.data !== undefined) {
|
|
182
|
+
payloadError.data = error.data;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
sendMcpJson(
|
|
186
|
+
res,
|
|
187
|
+
options.statusCode ?? 200,
|
|
188
|
+
{
|
|
189
|
+
jsonrpc: "2.0",
|
|
190
|
+
id,
|
|
191
|
+
error: payloadError,
|
|
192
|
+
},
|
|
193
|
+
headers,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function sendAccepted(res: http.ServerResponse, sessionId?: string, protocolVersion?: string): void {
|
|
198
|
+
const headers: Record<string, string> = {};
|
|
199
|
+
if (sessionId) headers["Mcp-Session-Id"] = sessionId;
|
|
200
|
+
if (protocolVersion) headers["MCP-Protocol-Version"] = protocolVersion;
|
|
201
|
+
|
|
202
|
+
res.writeHead(202, headers);
|
|
203
|
+
res.end();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function getMcpInitializeResult(protocolVersion: string): JsonObject {
|
|
207
|
+
return {
|
|
208
|
+
protocolVersion,
|
|
209
|
+
capabilities: {
|
|
210
|
+
tools: {
|
|
211
|
+
listChanged: false,
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
serverInfo: {
|
|
215
|
+
name: "yagami",
|
|
216
|
+
version: serverVersion,
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function summarizeConfig(config: RuntimeConfig): JsonObject {
|
|
222
|
+
return {
|
|
223
|
+
llmApi: config.llmApi,
|
|
224
|
+
llmBaseUrl: config.llmBaseUrl,
|
|
225
|
+
llmModel: config.llmModel,
|
|
226
|
+
searchEngine: config.searchEngine,
|
|
227
|
+
searchEngineUrlTemplate: config.searchEngineUrlTemplate,
|
|
228
|
+
browseLinkTimeoutMs: config.browseLinkTimeoutMs,
|
|
229
|
+
queryTimeoutMs: config.queryTimeoutMs,
|
|
230
|
+
cacheTtlMs: config.cacheTtlMs,
|
|
231
|
+
maxHtmlChars: config.maxHtmlChars,
|
|
232
|
+
maxMarkdownChars: config.maxMarkdownChars,
|
|
233
|
+
operationConcurrency: config.operationConcurrency,
|
|
234
|
+
browseConcurrency: config.browseConcurrency,
|
|
235
|
+
researchMaxPages: config.researchMaxPages,
|
|
236
|
+
researchMaxHops: config.researchMaxHops,
|
|
237
|
+
researchSameDomainOnly: config.researchSameDomainOnly,
|
|
238
|
+
toolExecutionMode: config.toolExecutionMode,
|
|
239
|
+
lightpandaCdpUrl: config.lightpandaCdpUrl,
|
|
240
|
+
lightpandaAutoStart: config.lightpandaAutoStart,
|
|
241
|
+
lightpandaAutoStop: config.lightpandaAutoStop,
|
|
242
|
+
theme: config.theme,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function collectRestartOnlyChanges(previous: RuntimeConfig, next: RuntimeConfig): string[] {
|
|
247
|
+
const restartFields: string[] = [];
|
|
248
|
+
|
|
249
|
+
if (previous.host !== next.host) restartFields.push("host");
|
|
250
|
+
if (previous.port !== next.port) restartFields.push("port");
|
|
251
|
+
if (previous.runtimeDir !== next.runtimeDir) restartFields.push("runtimeDir");
|
|
252
|
+
if (previous.configFile !== next.configFile) restartFields.push("configFile");
|
|
253
|
+
if (previous.pidFile !== next.pidFile) restartFields.push("pidFile");
|
|
254
|
+
if (previous.logFile !== next.logFile) restartFields.push("logFile");
|
|
255
|
+
|
|
256
|
+
return restartFields;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function reloadRuntimeConfig(): Promise<JsonObject> {
|
|
260
|
+
if (reloading) {
|
|
261
|
+
throw new Error("reload already in progress");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
reloading = true;
|
|
265
|
+
|
|
266
|
+
const previous = runtimeConfig;
|
|
267
|
+
const next = getConfig();
|
|
268
|
+
const restartOnlyChanges = collectRestartOnlyChanges(previous, next);
|
|
269
|
+
|
|
270
|
+
await fsp.mkdir(next.runtimeDir, { recursive: true }).catch(() => {});
|
|
271
|
+
|
|
272
|
+
const nextEngine = new YagamiEngine(next, console);
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
await nextEngine.init();
|
|
276
|
+
|
|
277
|
+
const oldEngine = engine;
|
|
278
|
+
engine = nextEngine;
|
|
279
|
+
runtimeConfig = next;
|
|
280
|
+
|
|
281
|
+
void oldEngine
|
|
282
|
+
.enqueueOperation(async () => {
|
|
283
|
+
await oldEngine.close();
|
|
284
|
+
})
|
|
285
|
+
.catch(() => {});
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
reloaded: true,
|
|
289
|
+
applied: summarizeConfig(runtimeConfig),
|
|
290
|
+
previous: summarizeConfig(previous),
|
|
291
|
+
restartOnlyChanges,
|
|
292
|
+
message:
|
|
293
|
+
restartOnlyChanges.length > 0
|
|
294
|
+
? `Reloaded runtime settings. Restart daemon to apply: ${restartOnlyChanges.join(", ")}`
|
|
295
|
+
: "Reloaded runtime settings.",
|
|
296
|
+
};
|
|
297
|
+
} catch (error) {
|
|
298
|
+
await nextEngine.close().catch(() => {});
|
|
299
|
+
throw error;
|
|
300
|
+
} finally {
|
|
301
|
+
reloading = false;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function streamQuery(
|
|
306
|
+
req: http.IncomingMessage,
|
|
307
|
+
res: http.ServerResponse,
|
|
308
|
+
query: string,
|
|
309
|
+
options: JsonObject = {},
|
|
310
|
+
): Promise<void> {
|
|
311
|
+
res.writeHead(200, {
|
|
312
|
+
"content-type": "application/x-ndjson; charset=utf-8",
|
|
313
|
+
"cache-control": "no-cache",
|
|
314
|
+
connection: "keep-alive",
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const requestAbort = new AbortController();
|
|
318
|
+
let disconnected = false;
|
|
319
|
+
|
|
320
|
+
const abortRequest = () => {
|
|
321
|
+
if (disconnected) return;
|
|
322
|
+
disconnected = true;
|
|
323
|
+
requestAbort.abort();
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const onRequestAborted = () => {
|
|
327
|
+
abortRequest();
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const onRequestClose = () => {
|
|
331
|
+
if (!req.complete) abortRequest();
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const onResponseClose = () => {
|
|
335
|
+
if (!res.writableEnded) abortRequest();
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
req.on("aborted", onRequestAborted);
|
|
339
|
+
req.on("close", onRequestClose);
|
|
340
|
+
res.on("close", onResponseClose);
|
|
341
|
+
|
|
342
|
+
const emit = (payload: NdjsonEvent): void => {
|
|
343
|
+
if (disconnected || res.writableEnded) return;
|
|
344
|
+
sendNdjson(res, payload);
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
emit({ type: "start", pid: process.pid, startedAt: Date.now() });
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
const result = await engine.enqueueQuery(query, {
|
|
351
|
+
...options,
|
|
352
|
+
abortSignal: requestAbort.signal,
|
|
353
|
+
onProgress: (event: Record<string, unknown>) => emit({ type: "progress", event }),
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
emit({ type: "result", result: result as Record<string, unknown> });
|
|
357
|
+
} catch (error) {
|
|
358
|
+
if (!requestAbort.signal.aborted) {
|
|
359
|
+
emit({ type: "error", error: toErrorMessage(error) });
|
|
360
|
+
}
|
|
361
|
+
} finally {
|
|
362
|
+
req.off("aborted", onRequestAborted);
|
|
363
|
+
req.off("close", onRequestClose);
|
|
364
|
+
res.off("close", onResponseClose);
|
|
365
|
+
|
|
366
|
+
if (!res.writableEnded) res.end();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function handleMcpHttp(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
371
|
+
const protocolHeader = getHeaderString(req.headers["mcp-protocol-version"]);
|
|
372
|
+
if (protocolHeader && !MCP_SUPPORTED_PROTOCOL_VERSIONS.has(protocolHeader)) {
|
|
373
|
+
return sendJson(res, 400, {
|
|
374
|
+
ok: false,
|
|
375
|
+
error: `Unsupported MCP-Protocol-Version: ${protocolHeader}`,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (req.method === "GET") {
|
|
380
|
+
res.writeHead(405, {
|
|
381
|
+
allow: "POST, DELETE",
|
|
382
|
+
});
|
|
383
|
+
res.end();
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (req.method === "DELETE") {
|
|
388
|
+
const sessionId = getHeaderString(req.headers["mcp-session-id"]);
|
|
389
|
+
if (!sessionId) {
|
|
390
|
+
return sendJson(res, 400, {
|
|
391
|
+
ok: false,
|
|
392
|
+
error: "Missing Mcp-Session-Id header",
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (!mcpSessions.has(sessionId)) {
|
|
397
|
+
res.writeHead(404);
|
|
398
|
+
res.end();
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
mcpSessions.delete(sessionId);
|
|
403
|
+
res.writeHead(204);
|
|
404
|
+
res.end();
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (req.method !== "POST") {
|
|
409
|
+
res.writeHead(405, {
|
|
410
|
+
allow: "POST, GET, DELETE",
|
|
411
|
+
});
|
|
412
|
+
res.end();
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const body = await readJsonBody(req);
|
|
417
|
+
const jsonrpc = getTrimmedString(body.jsonrpc);
|
|
418
|
+
const method = getTrimmedString(body.method);
|
|
419
|
+
const hasId = hasOwnKey(body, "id");
|
|
420
|
+
const id = normalizeJsonRpcId(body.id);
|
|
421
|
+
|
|
422
|
+
if (jsonrpc !== "2.0" || !method) {
|
|
423
|
+
if (hasId && id !== undefined) {
|
|
424
|
+
return sendMcpError(
|
|
425
|
+
res,
|
|
426
|
+
id,
|
|
427
|
+
{
|
|
428
|
+
code: -32600,
|
|
429
|
+
message: "Invalid Request",
|
|
430
|
+
},
|
|
431
|
+
{ statusCode: 400 },
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return sendJson(res, 400, {
|
|
436
|
+
ok: false,
|
|
437
|
+
error: "Invalid JSON-RPC payload",
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (method === "initialize") {
|
|
442
|
+
if (!hasId || id === undefined) {
|
|
443
|
+
return sendMcpError(
|
|
444
|
+
res,
|
|
445
|
+
null,
|
|
446
|
+
{
|
|
447
|
+
code: -32600,
|
|
448
|
+
message: "initialize must include a valid id",
|
|
449
|
+
},
|
|
450
|
+
{ statusCode: 400 },
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const params = typeof body.params === "object" && body.params !== null ? (body.params as JsonObject) : {};
|
|
455
|
+
const requestedProtocol = getTrimmedString(params.protocolVersion);
|
|
456
|
+
const protocolVersion = MCP_SUPPORTED_PROTOCOL_VERSIONS.has(requestedProtocol)
|
|
457
|
+
? requestedProtocol
|
|
458
|
+
: MCP_DEFAULT_PROTOCOL_VERSION;
|
|
459
|
+
|
|
460
|
+
const sessionId = randomUUID();
|
|
461
|
+
mcpSessions.set(sessionId, {
|
|
462
|
+
sessionId,
|
|
463
|
+
protocolVersion,
|
|
464
|
+
initialized: false,
|
|
465
|
+
createdAt: Date.now(),
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
return sendMcpSuccess(res, id, getMcpInitializeResult(protocolVersion), {
|
|
469
|
+
sessionId,
|
|
470
|
+
protocolVersion,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const sessionId = getHeaderString(req.headers["mcp-session-id"]);
|
|
475
|
+
if (!sessionId) {
|
|
476
|
+
if (hasId && id !== undefined) {
|
|
477
|
+
return sendMcpError(
|
|
478
|
+
res,
|
|
479
|
+
id,
|
|
480
|
+
{
|
|
481
|
+
code: -32000,
|
|
482
|
+
message: "Missing Mcp-Session-Id header",
|
|
483
|
+
},
|
|
484
|
+
{ statusCode: 400 },
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return sendJson(res, 400, {
|
|
489
|
+
ok: false,
|
|
490
|
+
error: "Missing Mcp-Session-Id header",
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const session = mcpSessions.get(sessionId);
|
|
495
|
+
if (!session) {
|
|
496
|
+
if (hasId && id !== undefined) {
|
|
497
|
+
return sendMcpError(
|
|
498
|
+
res,
|
|
499
|
+
id,
|
|
500
|
+
{
|
|
501
|
+
code: -32001,
|
|
502
|
+
message: "Unknown MCP session",
|
|
503
|
+
},
|
|
504
|
+
{ statusCode: 404 },
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
res.writeHead(404);
|
|
509
|
+
res.end();
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const protocolVersion = protocolHeader || session.protocolVersion || MCP_DEFAULT_PROTOCOL_VERSION;
|
|
514
|
+
|
|
515
|
+
if (!hasId) {
|
|
516
|
+
if (method === "notifications/initialized") {
|
|
517
|
+
session.initialized = true;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return sendAccepted(res, session.sessionId, protocolVersion);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (id === undefined) {
|
|
524
|
+
return sendMcpError(
|
|
525
|
+
res,
|
|
526
|
+
null,
|
|
527
|
+
{
|
|
528
|
+
code: -32600,
|
|
529
|
+
message: "Invalid Request id",
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
sessionId: session.sessionId,
|
|
533
|
+
protocolVersion,
|
|
534
|
+
statusCode: 400,
|
|
535
|
+
},
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (method === "ping") {
|
|
540
|
+
return sendMcpSuccess(
|
|
541
|
+
res,
|
|
542
|
+
id,
|
|
543
|
+
{},
|
|
544
|
+
{
|
|
545
|
+
sessionId: session.sessionId,
|
|
546
|
+
protocolVersion,
|
|
547
|
+
},
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (method === "tools/list") {
|
|
552
|
+
return sendMcpSuccess(
|
|
553
|
+
res,
|
|
554
|
+
id,
|
|
555
|
+
{
|
|
556
|
+
tools: MCP_TOOL_DEFINITIONS,
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
sessionId: session.sessionId,
|
|
560
|
+
protocolVersion,
|
|
561
|
+
},
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (method === "tools/call") {
|
|
566
|
+
const params = typeof body.params === "object" && body.params !== null ? (body.params as JsonObject) : {};
|
|
567
|
+
const toolName = getTrimmedString(params.name);
|
|
568
|
+
const toolArgs = typeof params.arguments === "object" && params.arguments !== null ? params.arguments : {};
|
|
569
|
+
|
|
570
|
+
if (!toolName) {
|
|
571
|
+
return sendMcpError(
|
|
572
|
+
res,
|
|
573
|
+
id,
|
|
574
|
+
{
|
|
575
|
+
code: -32602,
|
|
576
|
+
message: "Missing required field: name",
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
sessionId: session.sessionId,
|
|
580
|
+
protocolVersion,
|
|
581
|
+
statusCode: 400,
|
|
582
|
+
},
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (!isKnownMcpTool(toolName)) {
|
|
587
|
+
return sendMcpError(
|
|
588
|
+
res,
|
|
589
|
+
id,
|
|
590
|
+
{
|
|
591
|
+
code: -32602,
|
|
592
|
+
message: `Unknown tool: ${toolName}`,
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
sessionId: session.sessionId,
|
|
596
|
+
protocolVersion,
|
|
597
|
+
statusCode: 400,
|
|
598
|
+
},
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
try {
|
|
603
|
+
const result = await executeMcpTool(engine, toolName, toolArgs);
|
|
604
|
+
return sendMcpSuccess(res, id, result, {
|
|
605
|
+
sessionId: session.sessionId,
|
|
606
|
+
protocolVersion,
|
|
607
|
+
});
|
|
608
|
+
} catch (error) {
|
|
609
|
+
return sendMcpSuccess(
|
|
610
|
+
res,
|
|
611
|
+
id,
|
|
612
|
+
{
|
|
613
|
+
content: [
|
|
614
|
+
{
|
|
615
|
+
type: "text",
|
|
616
|
+
text: `${toolName} error: ${toErrorMessage(error)}`,
|
|
617
|
+
},
|
|
618
|
+
],
|
|
619
|
+
isError: true,
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
sessionId: session.sessionId,
|
|
623
|
+
protocolVersion,
|
|
624
|
+
},
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return sendMcpError(
|
|
630
|
+
res,
|
|
631
|
+
id,
|
|
632
|
+
{
|
|
633
|
+
code: -32601,
|
|
634
|
+
message: `Method not found: ${method}`,
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
sessionId: session.sessionId,
|
|
638
|
+
protocolVersion,
|
|
639
|
+
statusCode: 404,
|
|
640
|
+
},
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const server = http.createServer(async (req, res) => {
|
|
645
|
+
try {
|
|
646
|
+
if (
|
|
647
|
+
reloading &&
|
|
648
|
+
!(req.method === "GET" && req.url === "/health") &&
|
|
649
|
+
!(req.method === "POST" && req.url === "/stats") &&
|
|
650
|
+
!(req.method === "POST" && req.url === "/reload") &&
|
|
651
|
+
!(req.method === "POST" && req.url === "/stop")
|
|
652
|
+
) {
|
|
653
|
+
return sendJson(res, 503, {
|
|
654
|
+
ok: false,
|
|
655
|
+
error: "daemon reload in progress; retry shortly",
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (req.url === "/mcp") {
|
|
660
|
+
await handleMcpHttp(req, res);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (req.method === "GET" && req.url === "/health") {
|
|
665
|
+
return sendJson(res, 200, {
|
|
666
|
+
ok: true,
|
|
667
|
+
pid: process.pid,
|
|
668
|
+
reloading,
|
|
669
|
+
...(engine.getHealth() as JsonObject),
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (req.method === "POST" && req.url === "/stats") {
|
|
674
|
+
const body = await readJsonBody(req);
|
|
675
|
+
const includeCacheEntries = Boolean(body.includeCacheEntries);
|
|
676
|
+
const cacheEntriesLimit = parsePositiveInt(body.cacheEntriesLimit, 20, 500);
|
|
677
|
+
|
|
678
|
+
return sendJson(res, 200, {
|
|
679
|
+
ok: true,
|
|
680
|
+
pid: process.pid,
|
|
681
|
+
reloading,
|
|
682
|
+
result: engine.getHealth({
|
|
683
|
+
includeCacheEntries,
|
|
684
|
+
cacheEntriesLimit,
|
|
685
|
+
}) as JsonObject,
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (req.method === "POST" && req.url === "/reload") {
|
|
690
|
+
const result = await reloadRuntimeConfig();
|
|
691
|
+
return sendJson(res, 200, { ok: true, result });
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (req.method === "POST" && req.url === "/search/stream") {
|
|
695
|
+
const body = await readJsonBody(req);
|
|
696
|
+
const query = getTrimmedString(body.query);
|
|
697
|
+
|
|
698
|
+
if (!query) {
|
|
699
|
+
return sendJson(res, 400, { ok: false, error: "Missing required field: query" });
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
await streamQuery(req, res, query, {});
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (req.method === "POST" && req.url === "/search") {
|
|
707
|
+
const body = await readJsonBody(req);
|
|
708
|
+
const query = getTrimmedString(body.query);
|
|
709
|
+
|
|
710
|
+
if (!query) {
|
|
711
|
+
return sendJson(res, 400, { ok: false, error: "Missing required field: query" });
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const result = await engine.enqueueQuery(query, {});
|
|
715
|
+
return sendJson(res, 200, { ok: true, result: result as Record<string, unknown> });
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (req.method === "POST" && req.url === "/search/advanced/stream") {
|
|
719
|
+
const body = await readJsonBody(req);
|
|
720
|
+
const query = getTrimmedString(body.query);
|
|
721
|
+
|
|
722
|
+
if (!query) {
|
|
723
|
+
return sendJson(res, 400, { ok: false, error: "Missing required field: query" });
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
await streamQuery(req, res, query, {
|
|
727
|
+
researchPolicy: body,
|
|
728
|
+
});
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (req.method === "POST" && req.url === "/search/advanced") {
|
|
733
|
+
const body = await readJsonBody(req);
|
|
734
|
+
const query = getTrimmedString(body.query);
|
|
735
|
+
|
|
736
|
+
if (!query) {
|
|
737
|
+
return sendJson(res, 400, { ok: false, error: "Missing required field: query" });
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const result = await engine.enqueueQuery(query, {
|
|
741
|
+
researchPolicy: body,
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
return sendJson(res, 200, { ok: true, result: result as Record<string, unknown> });
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (req.method === "POST" && req.url === "/fetch") {
|
|
748
|
+
const body = await readJsonBody(req);
|
|
749
|
+
const url = getTrimmedString(body.url);
|
|
750
|
+
|
|
751
|
+
if (!url) {
|
|
752
|
+
return sendJson(res, 400, { ok: false, error: "Missing required field: url" });
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const result = await engine.enqueueOperation(() =>
|
|
756
|
+
engine.fetchContent(url, {
|
|
757
|
+
maxCharacters: body.maxCharacters,
|
|
758
|
+
noCache: body.noCache,
|
|
759
|
+
}),
|
|
760
|
+
);
|
|
761
|
+
|
|
762
|
+
return sendJson(res, 200, { ok: true, result: result as Record<string, unknown> });
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
if (req.method === "POST" && req.url === "/code-context/stream") {
|
|
766
|
+
const body = await readJsonBody(req);
|
|
767
|
+
const query = getTrimmedString(body.query);
|
|
768
|
+
|
|
769
|
+
if (!query) {
|
|
770
|
+
return sendJson(res, 400, { ok: false, error: "Missing required field: query" });
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
await streamQuery(req, res, query, {
|
|
774
|
+
researchPolicy: {
|
|
775
|
+
mode: "code",
|
|
776
|
+
...body,
|
|
777
|
+
},
|
|
778
|
+
});
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
if (req.method === "POST" && req.url === "/code-context") {
|
|
783
|
+
const body = await readJsonBody(req);
|
|
784
|
+
const query = getTrimmedString(body.query);
|
|
785
|
+
|
|
786
|
+
if (!query) {
|
|
787
|
+
return sendJson(res, 400, { ok: false, error: "Missing required field: query" });
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const result = await engine.enqueueQuery(query, {
|
|
791
|
+
researchPolicy: {
|
|
792
|
+
mode: "code",
|
|
793
|
+
...body,
|
|
794
|
+
},
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
return sendJson(res, 200, { ok: true, result: result as Record<string, unknown> });
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (req.method === "POST" && req.url === "/company-research/stream") {
|
|
801
|
+
const body = await readJsonBody(req);
|
|
802
|
+
const companyName = getTrimmedString(body.companyName || body.query);
|
|
803
|
+
|
|
804
|
+
if (!companyName) {
|
|
805
|
+
return sendJson(res, 400, { ok: false, error: "Missing required field: companyName" });
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const query = getTrimmedString(body.query || companyName);
|
|
809
|
+
|
|
810
|
+
await streamQuery(req, res, query, {
|
|
811
|
+
researchPolicy: {
|
|
812
|
+
mode: "company",
|
|
813
|
+
...body,
|
|
814
|
+
companyName,
|
|
815
|
+
},
|
|
816
|
+
});
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if (req.method === "POST" && req.url === "/company-research") {
|
|
821
|
+
const body = await readJsonBody(req);
|
|
822
|
+
const companyName = getTrimmedString(body.companyName || body.query);
|
|
823
|
+
|
|
824
|
+
if (!companyName) {
|
|
825
|
+
return sendJson(res, 400, { ok: false, error: "Missing required field: companyName" });
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const query = getTrimmedString(body.query || companyName);
|
|
829
|
+
|
|
830
|
+
const result = await engine.enqueueQuery(query, {
|
|
831
|
+
researchPolicy: {
|
|
832
|
+
mode: "company",
|
|
833
|
+
...body,
|
|
834
|
+
companyName,
|
|
835
|
+
},
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
return sendJson(res, 200, { ok: true, result: result as Record<string, unknown> });
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (req.method === "POST" && req.url === "/find-similar/stream") {
|
|
842
|
+
const body = await readJsonBody(req);
|
|
843
|
+
const url = getTrimmedString(body.url);
|
|
844
|
+
|
|
845
|
+
if (!url) {
|
|
846
|
+
return sendJson(res, 400, { ok: false, error: "Missing required field: url" });
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
const query = getTrimmedString(
|
|
850
|
+
body.query ||
|
|
851
|
+
`Find web pages similar to ${url}. Focus on same product category, target users, and use-case overlap. Avoid dictionary/synonym pages.`,
|
|
852
|
+
);
|
|
853
|
+
|
|
854
|
+
await streamQuery(req, res, query, {
|
|
855
|
+
researchPolicy: {
|
|
856
|
+
mode: "similar",
|
|
857
|
+
...body,
|
|
858
|
+
seedUrls: [url, ...(Array.isArray(body.seedUrls) ? body.seedUrls : [])],
|
|
859
|
+
},
|
|
860
|
+
});
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
if (req.method === "POST" && req.url === "/find-similar") {
|
|
865
|
+
const body = await readJsonBody(req);
|
|
866
|
+
const url = getTrimmedString(body.url);
|
|
867
|
+
|
|
868
|
+
if (!url) {
|
|
869
|
+
return sendJson(res, 400, { ok: false, error: "Missing required field: url" });
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const query = getTrimmedString(
|
|
873
|
+
body.query ||
|
|
874
|
+
`Find web pages similar to ${url}. Focus on same product category, target users, and use-case overlap. Avoid dictionary/synonym pages.`,
|
|
875
|
+
);
|
|
876
|
+
|
|
877
|
+
const result = await engine.enqueueQuery(query, {
|
|
878
|
+
researchPolicy: {
|
|
879
|
+
mode: "similar",
|
|
880
|
+
...body,
|
|
881
|
+
seedUrls: [url, ...(Array.isArray(body.seedUrls) ? body.seedUrls : [])],
|
|
882
|
+
},
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
return sendJson(res, 200, { ok: true, result: result as Record<string, unknown> });
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
if (req.method === "POST" && req.url === "/deep-research/start") {
|
|
889
|
+
const body = await readJsonBody(req);
|
|
890
|
+
const instructions = getTrimmedString(body.instructions);
|
|
891
|
+
|
|
892
|
+
if (!instructions) {
|
|
893
|
+
return sendJson(res, 400, { ok: false, error: "Missing required field: instructions" });
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
if (body.model !== undefined) {
|
|
897
|
+
return sendJson(res, 400, {
|
|
898
|
+
ok: false,
|
|
899
|
+
error: "Field 'model' has been removed. Use 'effort' (fast|balanced|thorough).",
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const result = await engine.deepResearchStart(instructions, {
|
|
904
|
+
effort: body.effort,
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
return sendJson(res, 200, { ok: true, result: result as unknown as Record<string, unknown> });
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
if (req.method === "POST" && req.url === "/deep-research/check") {
|
|
911
|
+
const body = await readJsonBody(req);
|
|
912
|
+
const researchId = getTrimmedString(body.researchId);
|
|
913
|
+
|
|
914
|
+
if (!researchId) {
|
|
915
|
+
return sendJson(res, 400, { ok: false, error: "Missing required field: researchId" });
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const result = await engine.deepResearchCheck(researchId);
|
|
919
|
+
return sendJson(res, 200, { ok: true, result: result as unknown as Record<string, unknown> });
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
if (req.method === "POST" && req.url === "/stop") {
|
|
923
|
+
sendJson(res, 200, { ok: true, stopping: true });
|
|
924
|
+
setTimeout(() => {
|
|
925
|
+
void shutdown("api-stop");
|
|
926
|
+
}, 25);
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
return sendJson(res, 404, { ok: false, error: "Not found" });
|
|
931
|
+
} catch (error) {
|
|
932
|
+
return sendJson(res, 500, {
|
|
933
|
+
ok: false,
|
|
934
|
+
error: toErrorMessage(error),
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
async function writePidFile(): Promise<void> {
|
|
940
|
+
await fsp.writeFile(startupConfig.pidFile, `${process.pid}\n`, "utf8");
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
async function removePidFile(): Promise<void> {
|
|
944
|
+
if (!fs.existsSync(startupConfig.pidFile)) return;
|
|
945
|
+
|
|
946
|
+
try {
|
|
947
|
+
const current = (await fsp.readFile(startupConfig.pidFile, "utf8")).trim();
|
|
948
|
+
if (current === String(process.pid)) {
|
|
949
|
+
await fsp.unlink(startupConfig.pidFile);
|
|
950
|
+
}
|
|
951
|
+
} catch {
|
|
952
|
+
// ignore cleanup errors
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
async function shutdown(reason: string): Promise<void> {
|
|
957
|
+
if (shuttingDown) return;
|
|
958
|
+
shuttingDown = true;
|
|
959
|
+
|
|
960
|
+
console.log(`[yagami] shutdown requested (${reason})`);
|
|
961
|
+
|
|
962
|
+
await new Promise<void>((resolve) => {
|
|
963
|
+
server.close(() => resolve());
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
await engine.close().catch(() => {});
|
|
967
|
+
await removePidFile();
|
|
968
|
+
|
|
969
|
+
process.exit(0);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
process.on("SIGTERM", () => {
|
|
973
|
+
void shutdown("sigterm");
|
|
974
|
+
});
|
|
975
|
+
process.on("SIGINT", () => {
|
|
976
|
+
void shutdown("sigint");
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
await engine.init();
|
|
980
|
+
await writePidFile();
|
|
981
|
+
|
|
982
|
+
server.listen(startupConfig.port, startupConfig.host, () => {
|
|
983
|
+
console.log(`[yagami] daemon listening on ${startupConfig.daemonUrl}`);
|
|
984
|
+
console.log(`[yagami] LLM: api=${runtimeConfig.llmApi} baseUrl=${runtimeConfig.llmBaseUrl}`);
|
|
985
|
+
console.log(`[yagami] Search engine: ${runtimeConfig.searchEngine || "duckduckgo"}`);
|
|
986
|
+
console.log(`[yagami] CDP: ${runtimeConfig.lightpandaCdpUrl}`);
|
|
987
|
+
console.log(
|
|
988
|
+
`[yagami] Lightpanda auto-start: ${runtimeConfig.lightpandaAutoStart} (${runtimeConfig.lightpandaHost}:${runtimeConfig.lightpandaPort})`,
|
|
989
|
+
);
|
|
990
|
+
});
|