droid-acp 0.1.0 → 0.2.0

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/README.md CHANGED
@@ -12,6 +12,7 @@ Use Droid from any [ACP-compatible](https://agentclientprotocol.com) clients suc
12
12
  - Image prompts (e.g. paste screenshots in Zed)
13
13
  - Multiple model support
14
14
  - Session modes (Spec, Manual, Auto Low/Medium/High)
15
+ - Optional WebSearch proxy (Smithery Exa MCP / custom forward)
15
16
 
16
17
  ## Installation
17
18
 
@@ -24,11 +25,13 @@ npm install droid-acp
24
25
  ### Prerequisites
25
26
 
26
27
  1. Install Droid CLI from [Factory](https://factory.ai)
27
- 2. Set your Factory API key:
28
+ 2. (Recommended) Set your Factory API key for Factory-hosted features:
28
29
  ```bash
29
30
  export FACTORY_API_KEY=fk-...
30
31
  ```
31
32
 
33
+ > If you only need WebSearch via the built-in proxy + Smithery Exa MCP, a valid Factory key is not required (droid-acp injects a dummy key into the spawned droid process to satisfy droid's local auth gate).
34
+
32
35
  ### Running
33
36
 
34
37
  ```bash
@@ -93,6 +96,25 @@ Add to your Zed `settings.json`:
93
96
  }
94
97
  ```
95
98
 
99
+ **Enable WebSearch proxy (Smithery Exa MCP):**
100
+
101
+ ```json
102
+ {
103
+ "agent_servers": {
104
+ "Droid WebSearch": {
105
+ "type": "custom",
106
+ "command": "npx",
107
+ "args": ["droid-acp"],
108
+ "env": {
109
+ "DROID_ACP_WEBSEARCH": "1",
110
+ "SMITHERY_API_KEY": "your_smithery_key",
111
+ "SMITHERY_PROFILE": "your_profile_id"
112
+ }
113
+ }
114
+ }
115
+ }
116
+ ```
117
+
96
118
  ### Modes
97
119
 
98
120
  | Command | Mode | Custom Models | Description |
@@ -104,9 +126,58 @@ Add to your Zed `settings.json`:
104
126
 
105
127
  ### Environment Variables
106
128
 
107
- - `FACTORY_API_KEY` - Your Factory API key (required)
129
+ - `FACTORY_API_KEY` - Your Factory API key (recommended for Factory-hosted features)
108
130
  - `DROID_EXECUTABLE` - Path to the droid binary (optional, defaults to `droid` in PATH)
109
131
 
132
+ - `DROID_ACP_WEBSEARCH` - Enable local proxy to optionally intercept Droid websearch (`/api/tools/exa/search`)
133
+ - `DROID_ACP_WEBSEARCH_FORWARD_URL` - Optional forward target for websearch (base URL or full URL)
134
+ - `DROID_ACP_WEBSEARCH_FORWARD_MODE` - Forward mode for `DROID_ACP_WEBSEARCH_FORWARD_URL` (`http` or `mcp`, default: `http`)
135
+ - `DROID_ACP_WEBSEARCH_UPSTREAM_URL` - Optional upstream Factory API base URL (default: `FACTORY_API_BASE_URL_OVERRIDE` or `https://api.factory.ai`)
136
+ - `DROID_ACP_WEBSEARCH_HOST` - Optional proxy bind host (default: `127.0.0.1`)
137
+ - `DROID_ACP_WEBSEARCH_PORT` - Optional proxy bind port (default: auto-assign an available port)
138
+ - `DROID_ACP_WEBSEARCH_DEBUG` - Emit a WebSearch status message in the ACP UI (e.g. Zed) for debugging
139
+
140
+ - `SMITHERY_API_KEY` - Optional (recommended) Smithery Exa MCP API key (enables high-quality websearch)
141
+ - `SMITHERY_PROFILE` - Optional Smithery Exa MCP profile id
142
+
143
+ ### WebSearch Proxy (optional)
144
+
145
+ Enable the built-in proxy to intercept `POST /api/tools/exa/search` and serve results from Smithery Exa MCP (recommended):
146
+
147
+ ```bash
148
+ export SMITHERY_API_KEY="your_smithery_key"
149
+ export SMITHERY_PROFILE="your_profile_id"
150
+ DROID_ACP_WEBSEARCH=1 npx droid-acp
151
+ ```
152
+
153
+ To debug proxy wiring (shows `proxyBaseUrl` and a `/health` link in the ACP UI):
154
+
155
+ ```bash
156
+ DROID_ACP_WEBSEARCH=1 DROID_ACP_WEBSEARCH_DEBUG=1 npx droid-acp
157
+ ```
158
+
159
+ To forward WebSearch to your own HTTP handler instead:
160
+
161
+ ```bash
162
+ DROID_ACP_WEBSEARCH=1 \
163
+ DROID_ACP_WEBSEARCH_FORWARD_URL="http://127.0.0.1:20002" \
164
+ npx droid-acp
165
+ ```
166
+
167
+ To forward WebSearch to an MCP endpoint (JSON-RPC `tools/call`), set:
168
+
169
+ ```bash
170
+ DROID_ACP_WEBSEARCH=1 \
171
+ DROID_ACP_WEBSEARCH_FORWARD_MODE=mcp \
172
+ DROID_ACP_WEBSEARCH_FORWARD_URL="http://127.0.0.1:20002" \
173
+ npx droid-acp
174
+ ```
175
+
176
+ Notes:
177
+
178
+ - The proxy exposes `GET /health` on `proxyBaseUrl` (handy for troubleshooting).
179
+ - When `DROID_ACP_WEBSEARCH=1`, droid-acp injects a dummy `FACTORY_API_KEY` into the spawned droid process if none is set, so WebSearch requests can reach the proxy even without Factory login.
180
+
110
181
  ## Session Modes
111
182
 
112
183
  | Mode | Description | Droid autonomy level |
@@ -1,8 +1,12 @@
1
+ import { createRequire } from "node:module";
1
2
  import { spawn } from "node:child_process";
2
3
  import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
3
4
  import { createInterface } from "node:readline";
4
5
  import { randomUUID } from "node:crypto";
5
6
  import { ReadableStream, WritableStream } from "node:stream/web";
7
+ import { createServer } from "node:http";
8
+ import { Readable } from "node:stream";
9
+ import { pipeline } from "node:stream/promises";
6
10
 
7
11
  //#region src/utils.ts
8
12
  var Pushable = class {
@@ -61,6 +65,16 @@ function nodeToWebReadable(nodeStream) {
61
65
  nodeStream.on("error", (err) => controller.error(err));
62
66
  } });
63
67
  }
68
+ function isEnvEnabled(value) {
69
+ if (!value) return false;
70
+ switch (value.trim().toLowerCase()) {
71
+ case "1":
72
+ case "true":
73
+ case "yes":
74
+ case "on": return true;
75
+ default: return false;
76
+ }
77
+ }
64
78
  /** Check if running on Windows */
65
79
  const isWindows = process.platform === "win32";
66
80
  /**
@@ -73,11 +87,343 @@ function findDroidExecutable() {
73
87
  return "droid";
74
88
  }
75
89
 
90
+ //#endregion
91
+ //#region src/websearch-proxy.ts
92
+ function parseSseJsonPayload(raw) {
93
+ const lines = raw.split(/\r?\n/);
94
+ let currentData = [];
95
+ const flush = (acc) => {
96
+ if (currentData.length === 0) return;
97
+ const joined = currentData.join("\n");
98
+ try {
99
+ acc.push(JSON.parse(joined));
100
+ } catch {}
101
+ currentData = [];
102
+ };
103
+ const parsed = [];
104
+ for (const line of lines) {
105
+ if (line.length === 0) {
106
+ flush(parsed);
107
+ continue;
108
+ }
109
+ if (line.startsWith("event:")) continue;
110
+ if (line.startsWith("data:")) {
111
+ const data = line.slice(5).replace(/^ /, "");
112
+ currentData.push(data);
113
+ continue;
114
+ }
115
+ }
116
+ flush(parsed);
117
+ return parsed.length > 0 ? parsed[parsed.length - 1] : null;
118
+ }
119
+ function parseHttpUrl(value, name) {
120
+ let url;
121
+ try {
122
+ url = new URL(value);
123
+ } catch {
124
+ throw new Error(`${name} must be a valid URL: ${value}`);
125
+ }
126
+ if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error(`${name} must be http(s): ${value}`);
127
+ return url;
128
+ }
129
+ function resolveForwardTarget(forward, requestPathAndQuery) {
130
+ if (forward.pathname === "/" && forward.search === "" && forward.hash === "") return new URL(requestPathAndQuery, forward);
131
+ const target = new URL(forward.toString());
132
+ if (!target.search && requestPathAndQuery.includes("?")) target.search = requestPathAndQuery.slice(requestPathAndQuery.indexOf("?"));
133
+ return target;
134
+ }
135
+ function isBodylessMethod(method) {
136
+ return method === "GET" || method === "HEAD";
137
+ }
138
+ function toNonEmptyString(value) {
139
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
140
+ }
141
+ function parseWebsearchRequestBody(bodyBuffer) {
142
+ let input;
143
+ try {
144
+ input = JSON.parse(bodyBuffer.toString("utf8"));
145
+ } catch {
146
+ return null;
147
+ }
148
+ const query = toNonEmptyString(input?.query);
149
+ if (!query) return null;
150
+ const numResultsRaw = input?.numResults;
151
+ return {
152
+ query,
153
+ numResults: typeof numResultsRaw === "number" && Number.isFinite(numResultsRaw) ? Math.max(1, numResultsRaw) : 10
154
+ };
155
+ }
156
+ async function readBody(req, maxBytes) {
157
+ const chunks = [];
158
+ let total = 0;
159
+ for await (const chunk of req) {
160
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
161
+ total += buf.length;
162
+ if (total > maxBytes) throw new Error(`Request body too large (${total} bytes)`);
163
+ chunks.push(buf);
164
+ }
165
+ return Buffer.concat(chunks);
166
+ }
167
+ function parseSearchResultsText(text, numResults) {
168
+ try {
169
+ const json = JSON.parse(text);
170
+ if (Array.isArray(json)) return json.slice(0, numResults);
171
+ } catch {}
172
+ const matches = [...text.matchAll(/^Title:\s*(.*)$/gm)];
173
+ if (matches.length === 0) return null;
174
+ const results = [];
175
+ for (let i = 0; i < matches.length && results.length < numResults; i += 1) {
176
+ const start = matches[i]?.index ?? 0;
177
+ const end = matches[i + 1]?.index ?? text.length;
178
+ const chunk = text.slice(start, end).trim();
179
+ const title = matches[i]?.[1]?.trim() ?? "";
180
+ const url = (chunk.match(/^URL:\s*(.*)$/m)?.[1] ?? "").trim();
181
+ const snippet = (chunk.match(/^Text:\s*(.*)$/m)?.[1] ?? "").trim();
182
+ results.push({
183
+ title,
184
+ url,
185
+ snippet,
186
+ text: snippet
187
+ });
188
+ }
189
+ return results;
190
+ }
191
+ async function tryHandleWebsearchWithMcp(res, logger, mcpEndpoint, bodyBuffer) {
192
+ const parsed = parseWebsearchRequestBody(bodyBuffer);
193
+ if (!parsed) return { handled: false };
194
+ const { query, numResults } = parsed;
195
+ const requestBody = {
196
+ jsonrpc: "2.0",
197
+ id: 1,
198
+ method: "tools/call",
199
+ params: {
200
+ name: "web_search_exa",
201
+ arguments: {
202
+ query,
203
+ numResults
204
+ }
205
+ }
206
+ };
207
+ let mcpResponse;
208
+ try {
209
+ const response = await fetch(mcpEndpoint, {
210
+ method: "POST",
211
+ headers: {
212
+ "Content-Type": "application/json",
213
+ Accept: "application/json, text/event-stream"
214
+ },
215
+ body: JSON.stringify(requestBody)
216
+ });
217
+ if ((response.headers.get("content-type")?.toLowerCase() ?? "").includes("text/event-stream")) {
218
+ const parsedSse = parseSseJsonPayload(await response.text());
219
+ if (!parsedSse) throw new Error("Invalid MCP SSE response: missing JSON payload");
220
+ mcpResponse = parsedSse;
221
+ } else mcpResponse = await response.json();
222
+ } catch (error) {
223
+ const message = error instanceof Error ? error.message : String(error);
224
+ logger.error("[websearch] MCP request failed:", message);
225
+ return {
226
+ handled: false,
227
+ error: message
228
+ };
229
+ }
230
+ const resultContent = mcpResponse?.result?.content;
231
+ const textBlock = (Array.isArray(resultContent) ? resultContent : []).find((c) => !!c && typeof c === "object" && c.type === "text" && typeof c.text === "string");
232
+ if (!textBlock) {
233
+ const errorMessage = mcpResponse?.error?.message ?? mcpResponse?.error ?? null;
234
+ const message = typeof errorMessage === "string" ? errorMessage : `Invalid MCP response: ${JSON.stringify(mcpResponse)}`;
235
+ logger.error("[websearch] MCP response missing text content:", message);
236
+ return {
237
+ handled: false,
238
+ error: message
239
+ };
240
+ }
241
+ const parsedItems = parseSearchResultsText(textBlock.text, numResults);
242
+ if (!parsedItems) {
243
+ logger.error("[websearch] Failed to parse MCP text payload");
244
+ return {
245
+ handled: false,
246
+ error: "Invalid MCP payload: unsupported format"
247
+ };
248
+ }
249
+ const results = parsedItems.slice(0, numResults).map((item) => {
250
+ const title = toNonEmptyString(item.title) ?? "";
251
+ const url = toNonEmptyString(item.url) ?? "";
252
+ const highlights = Array.isArray(item.highlights) ? item.highlights.filter((v) => typeof v === "string") : [];
253
+ const content = toNonEmptyString(item.text) ?? toNonEmptyString(item.snippet) ?? (highlights.length > 0 ? highlights.join(" ") : "");
254
+ return {
255
+ title,
256
+ url,
257
+ content,
258
+ snippet: content,
259
+ publishedDate: item.publishedDate ?? null,
260
+ author: item.author ?? null,
261
+ score: item.score ?? null
262
+ };
263
+ });
264
+ res.writeHead(200, { "Content-Type": "application/json" });
265
+ res.end(JSON.stringify({ results }));
266
+ return { handled: true };
267
+ }
268
+ async function startWebsearchProxy(options) {
269
+ const logger = options.logger ?? console;
270
+ const host = options.host ?? "127.0.0.1";
271
+ const port = options.port ?? 0;
272
+ const upstreamBase = parseHttpUrl(options.upstreamBaseUrl, "DROID_ACP_WEBSEARCH_UPSTREAM_URL");
273
+ const forward = options.websearchForwardUrl ? parseHttpUrl(options.websearchForwardUrl, "DROID_ACP_WEBSEARCH_FORWARD_URL") : null;
274
+ const forwardMode = options.websearchForwardMode ?? "http";
275
+ const smitheryApiKey = toNonEmptyString(options.smitheryApiKey);
276
+ const smitheryProfile = toNonEmptyString(options.smitheryProfile);
277
+ const smitheryEndpoint = smitheryApiKey && smitheryProfile ? new URL(`https://server.smithery.ai/exa/mcp?api_key=${encodeURIComponent(smitheryApiKey)}&profile=${encodeURIComponent(smitheryProfile)}`) : null;
278
+ let totalRequests = 0;
279
+ let websearchRequests = 0;
280
+ let lastWebsearchAt = null;
281
+ let lastWebsearchOutcome = null;
282
+ const server = createServer((req, res) => {
283
+ (async () => {
284
+ totalRequests += 1;
285
+ const rawUrl = req.url;
286
+ if (!rawUrl) {
287
+ res.writeHead(400, { "Content-Type": "application/json" });
288
+ res.end(JSON.stringify({ error: "Missing URL" }));
289
+ return;
290
+ }
291
+ const requestUrl = new URL(rawUrl, `http://${req.headers.host ?? "127.0.0.1"}`);
292
+ const pathAndQuery = `${requestUrl.pathname}${requestUrl.search || ""}`;
293
+ if (requestUrl.pathname === "/health") {
294
+ res.writeHead(200, { "Content-Type": "application/json" });
295
+ res.end(JSON.stringify({
296
+ status: "ok",
297
+ upstreamBaseUrl: upstreamBase.toString(),
298
+ websearchForwardUrl: forward?.toString() ?? null,
299
+ websearchForwardMode: forwardMode,
300
+ smitheryEnabled: Boolean(smitheryEndpoint),
301
+ requests: {
302
+ total: totalRequests,
303
+ websearch: websearchRequests,
304
+ lastWebsearchAt,
305
+ lastWebsearchOutcome
306
+ }
307
+ }));
308
+ return;
309
+ }
310
+ const isWebsearch = requestUrl.pathname.startsWith("/api/tools/exa/search") && req.method === "POST";
311
+ const targetUrl = isWebsearch && forward && forwardMode === "http" ? resolveForwardTarget(forward, pathAndQuery) : new URL(pathAndQuery, upstreamBase);
312
+ const headers = new Headers();
313
+ for (const [key, value] of Object.entries(req.headers)) {
314
+ if (value === void 0) continue;
315
+ if (key.toLowerCase() === "host") continue;
316
+ if (Array.isArray(value)) for (const v of value) headers.append(key, v);
317
+ else headers.set(key, value);
318
+ }
319
+ const controller = new AbortController();
320
+ req.on("aborted", () => controller.abort());
321
+ let bodyBuffer = null;
322
+ if (isWebsearch) {
323
+ websearchRequests += 1;
324
+ lastWebsearchAt = (/* @__PURE__ */ new Date()).toISOString();
325
+ try {
326
+ bodyBuffer = await readBody(req, 1e6);
327
+ } catch {
328
+ res.writeHead(413, { "Content-Type": "application/json" });
329
+ res.end(JSON.stringify({ error: "Request body too large" }));
330
+ lastWebsearchOutcome = "rejected: body too large";
331
+ return;
332
+ }
333
+ if (forward && forwardMode === "mcp") {
334
+ const r = await tryHandleWebsearchWithMcp(res, logger, forward, bodyBuffer);
335
+ if (r.handled) {
336
+ lastWebsearchOutcome = "handled: mcp forward";
337
+ return;
338
+ }
339
+ res.writeHead(502, { "Content-Type": "application/json" });
340
+ res.end(JSON.stringify({
341
+ error: "WebSearch MCP forward failed",
342
+ message: r.error ?? "Unknown error"
343
+ }));
344
+ lastWebsearchOutcome = `error: mcp forward (${r.error ?? "unknown"})`;
345
+ return;
346
+ }
347
+ if (smitheryEndpoint) {
348
+ const r = await tryHandleWebsearchWithMcp(res, logger, smitheryEndpoint, bodyBuffer);
349
+ if (r.handled) {
350
+ lastWebsearchOutcome = "handled: smithery exa mcp";
351
+ return;
352
+ }
353
+ res.writeHead(502, { "Content-Type": "application/json" });
354
+ res.end(JSON.stringify({
355
+ error: "Smithery Exa MCP websearch failed",
356
+ message: r.error ?? "Unknown error"
357
+ }));
358
+ lastWebsearchOutcome = `error: smithery exa mcp (${r.error ?? "unknown"})`;
359
+ return;
360
+ }
361
+ logger.log("[websearch] proxying", pathAndQuery, "->", targetUrl.toString());
362
+ lastWebsearchOutcome = "proxied: upstream";
363
+ } else logger.log("[factory-proxy]", req.method ?? "GET", pathAndQuery);
364
+ let upstreamResponse;
365
+ try {
366
+ upstreamResponse = await fetch(targetUrl, {
367
+ method: req.method,
368
+ headers,
369
+ body: isBodylessMethod(req.method) ? void 0 : bodyBuffer ? bodyBuffer : req,
370
+ redirect: "manual",
371
+ signal: controller.signal,
372
+ duplex: "half"
373
+ });
374
+ } catch (error) {
375
+ const message = error instanceof Error ? error.message : String(error);
376
+ res.writeHead(502, { "Content-Type": "application/json" });
377
+ res.end(JSON.stringify({
378
+ error: "Upstream request failed",
379
+ message
380
+ }));
381
+ return;
382
+ }
383
+ const setCookie = upstreamResponse.headers.getSetCookie?.();
384
+ if (setCookie && setCookie.length > 0) res.setHeader("set-cookie", setCookie);
385
+ for (const [key, value] of upstreamResponse.headers) {
386
+ if (key.toLowerCase() === "set-cookie") continue;
387
+ res.setHeader(key, value);
388
+ }
389
+ res.statusCode = upstreamResponse.status;
390
+ if (!upstreamResponse.body) {
391
+ res.end();
392
+ return;
393
+ }
394
+ try {
395
+ await pipeline(Readable.fromWeb(upstreamResponse.body), res);
396
+ } catch {}
397
+ })();
398
+ });
399
+ await new Promise((resolve, reject) => {
400
+ server.once("error", reject);
401
+ server.listen(port, host, () => {
402
+ server.off("error", reject);
403
+ resolve();
404
+ });
405
+ });
406
+ const address = server.address();
407
+ if (!address || typeof address === "string") {
408
+ server.close();
409
+ throw new Error("Failed to bind websearch proxy server");
410
+ }
411
+ const baseUrl = `http://${host}:${address.port}`;
412
+ logger.log("[websearch] proxy listening on", baseUrl);
413
+ return {
414
+ baseUrl,
415
+ close: () => new Promise((resolve) => {
416
+ server.close(() => resolve());
417
+ })
418
+ };
419
+ }
420
+
76
421
  //#endregion
77
422
  //#region src/droid-adapter.ts
78
423
  function createDroidAdapter(options) {
79
424
  let process$1 = null;
80
425
  let sessionId = null;
426
+ let websearchProxy = null;
81
427
  const machineId = randomUUID();
82
428
  const logger = options.logger ?? console;
83
429
  const notificationHandlers = [];
@@ -287,6 +633,12 @@ function createDroidAdapter(options) {
287
633
  logger.error("Parse error:", err.message);
288
634
  }
289
635
  };
636
+ const stopWebsearchProxy = () => {
637
+ if (!websearchProxy) return;
638
+ const proxy = websearchProxy;
639
+ websearchProxy = null;
640
+ proxy.close().catch((err) => logger.error("[websearch] proxy close failed:", err));
641
+ };
290
642
  return {
291
643
  async start() {
292
644
  const executable = findDroidExecutable();
@@ -300,16 +652,45 @@ function createDroidAdapter(options) {
300
652
  options.cwd
301
653
  ];
302
654
  logger.log("Starting droid:", executable, args.join(" "));
655
+ const env = {
656
+ ...globalThis.process.env,
657
+ FORCE_COLOR: "0"
658
+ };
659
+ if (isEnvEnabled(env.DROID_ACP_WEBSEARCH)) {
660
+ stopWebsearchProxy();
661
+ const upstreamBaseUrl = env.DROID_ACP_WEBSEARCH_UPSTREAM_URL ?? env.FACTORY_API_BASE_URL_OVERRIDE ?? "https://api.factory.ai";
662
+ const websearchForwardUrl = env.DROID_ACP_WEBSEARCH_FORWARD_URL;
663
+ const forwardModeRaw = env.DROID_ACP_WEBSEARCH_FORWARD_MODE;
664
+ const websearchForwardMode = typeof forwardModeRaw === "string" && forwardModeRaw.trim().toLowerCase() === "mcp" ? "mcp" : "http";
665
+ const host = env.DROID_ACP_WEBSEARCH_HOST ?? "127.0.0.1";
666
+ const portRaw = env.DROID_ACP_WEBSEARCH_PORT;
667
+ let port;
668
+ if (typeof portRaw === "string" && portRaw.length > 0) {
669
+ const parsed = Number.parseInt(portRaw, 10);
670
+ if (Number.isNaN(parsed) || parsed < 0 || parsed > 65535) throw new Error(`Invalid DROID_ACP_WEBSEARCH_PORT: ${portRaw}`);
671
+ port = parsed;
672
+ }
673
+ websearchProxy = await startWebsearchProxy({
674
+ upstreamBaseUrl,
675
+ websearchForwardUrl,
676
+ websearchForwardMode,
677
+ smitheryApiKey: env.SMITHERY_API_KEY,
678
+ smitheryProfile: env.SMITHERY_PROFILE,
679
+ host,
680
+ port,
681
+ logger
682
+ });
683
+ if (!env.FACTORY_API_KEY) env.FACTORY_API_KEY = "droid-acp-websearch";
684
+ env.FACTORY_API_BASE_URL_OVERRIDE = websearchProxy.baseUrl;
685
+ env.FACTORY_API_BASE_URL = websearchProxy.baseUrl;
686
+ }
303
687
  process$1 = spawn(executable, args, {
304
688
  stdio: [
305
689
  "pipe",
306
690
  "pipe",
307
691
  "pipe"
308
692
  ],
309
- env: {
310
- ...globalThis.process.env,
311
- FORCE_COLOR: "0"
312
- },
693
+ env,
313
694
  shell: isWindows,
314
695
  windowsHide: true
315
696
  });
@@ -321,6 +702,7 @@ function createDroidAdapter(options) {
321
702
  process$1.on("exit", (code) => {
322
703
  logger.log("Droid exit:", code);
323
704
  process$1 = null;
705
+ stopWebsearchProxy();
324
706
  exitHandlers.forEach((h) => h(code));
325
707
  });
326
708
  return new Promise((resolve, reject) => {
@@ -352,6 +734,9 @@ function createDroidAdapter(options) {
352
734
  if (Array.isArray(message.images) && message.images.length > 0) params.images = message.images;
353
735
  send("droid.add_user_message", params);
354
736
  },
737
+ getWebsearchProxyBaseUrl() {
738
+ return websearchProxy?.baseUrl ?? null;
739
+ },
355
740
  setMode(level) {
356
741
  if (!sessionId) return;
357
742
  send("droid.update_session_settings", {
@@ -384,6 +769,7 @@ function createDroidAdapter(options) {
384
769
  process$1.kill("SIGTERM");
385
770
  process$1 = null;
386
771
  }
772
+ stopWebsearchProxy();
387
773
  },
388
774
  isRunning() {
389
775
  return process$1 !== null && !process$1.killed;
@@ -406,10 +792,7 @@ const ACP_MODES = [
406
792
 
407
793
  //#endregion
408
794
  //#region src/acp-agent.ts
409
- const packageJson = {
410
- name: "droid-acp",
411
- version: "0.1.0"
412
- };
795
+ const packageJson = createRequire(import.meta.url)("../package.json");
413
796
  function normalizeBase64DataUrl(data, fallbackMimeType) {
414
797
  const trimmed = data.trim();
415
798
  const match = trimmed.match(/^data:([^;,]+);base64,(.*)$/s);
@@ -562,6 +945,34 @@ var DroidAcpAgent = class {
562
945
  });
563
946
  this.sessions.set(sessionId, session);
564
947
  this.logger.log("Session created:", sessionId);
948
+ if (isEnvEnabled(process.env.DROID_ACP_WEBSEARCH_DEBUG) || isEnvEnabled(process.env.DROID_DEBUG)) {
949
+ const websearchProxyBaseUrl = droid.getWebsearchProxyBaseUrl();
950
+ const parentFactoryApiKey = process.env.FACTORY_API_KEY;
951
+ const willInjectDummyFactoryApiKey = isEnvEnabled(process.env.DROID_ACP_WEBSEARCH) && !parentFactoryApiKey;
952
+ setTimeout(() => {
953
+ this.client.sessionUpdate({
954
+ sessionId,
955
+ update: {
956
+ sessionUpdate: "agent_message_chunk",
957
+ content: {
958
+ type: "text",
959
+ text: [
960
+ "[droid-acp] WebSearch 状态",
961
+ `- DROID_ACP_WEBSEARCH: ${process.env.DROID_ACP_WEBSEARCH ?? "<unset>"}`,
962
+ `- DROID_ACP_WEBSEARCH_PORT: ${process.env.DROID_ACP_WEBSEARCH_PORT ?? "<unset>"}`,
963
+ `- DROID_ACP_WEBSEARCH_FORWARD_MODE: ${process.env.DROID_ACP_WEBSEARCH_FORWARD_MODE ?? "<unset>"}`,
964
+ `- DROID_ACP_WEBSEARCH_FORWARD_URL: ${process.env.DROID_ACP_WEBSEARCH_FORWARD_URL ?? "<unset>"}`,
965
+ `- FACTORY_API_KEY: ${parentFactoryApiKey ? "set" : "<unset>"}${willInjectDummyFactoryApiKey ? " (droid child auto-inject dummy)" : ""}`,
966
+ `- SMITHERY_API_KEY: ${process.env.SMITHERY_API_KEY ? "set" : "<unset>"}`,
967
+ `- SMITHERY_PROFILE: ${process.env.SMITHERY_PROFILE ? "set" : "<unset>"}`,
968
+ `- proxyBaseUrl: ${websearchProxyBaseUrl ?? "<not running>"}`,
969
+ websearchProxyBaseUrl ? `- health: ${websearchProxyBaseUrl}/health` : null
970
+ ].filter((l) => typeof l === "string").join("\n") + "\n"
971
+ }
972
+ }
973
+ });
974
+ }, 0);
975
+ }
565
976
  setTimeout(() => {
566
977
  this.client.sessionUpdate({
567
978
  sessionId,
@@ -1625,5 +2036,5 @@ function runAcp() {
1625
2036
  }
1626
2037
 
1627
2038
  //#endregion
1628
- export { Pushable as a, createDroidAdapter as i, runAcp as n, findDroidExecutable as o, ACP_MODES as r, isWindows as s, DroidAcpAgent as t };
1629
- //# sourceMappingURL=acp-agent-Ddz1S3Jm.mjs.map
2039
+ export { startWebsearchProxy as a, isEnvEnabled as c, createDroidAdapter as i, isWindows as l, runAcp as n, Pushable as o, ACP_MODES as r, findDroidExecutable as s, DroidAcpAgent as t };
2040
+ //# sourceMappingURL=acp-agent-DUNNYT3R.mjs.map