droid-acp 0.5.0 → 0.6.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.
@@ -2,14 +2,14 @@ import { createRequire } from "node:module";
2
2
  import { spawn } from "node:child_process";
3
3
  import { ReadableStream, WritableStream } from "node:stream/web";
4
4
  import { open, readFile, stat } from "node:fs/promises";
5
- import os from "node:os";
6
- import path from "node:path";
5
+ import os, { homedir } from "node:os";
6
+ import path, { join } from "node:path";
7
7
  import { createInterface } from "node:readline";
8
8
  import { randomUUID } from "node:crypto";
9
9
  import { createServer } from "node:http";
10
10
  import { Readable } from "node:stream";
11
11
  import { pipeline } from "node:stream/promises";
12
- import { createReadStream, promises } from "node:fs";
12
+ import { createReadStream, promises, readFileSync, statSync } from "node:fs";
13
13
  import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
14
14
 
15
15
  //#region src/utils.ts
@@ -1534,7 +1534,7 @@ function parseWebsearchRequestBody(bodyBuffer) {
1534
1534
  numResults: typeof numResultsRaw === "number" && Number.isFinite(numResultsRaw) ? Math.max(1, numResultsRaw) : 10
1535
1535
  };
1536
1536
  }
1537
- async function readBody(req, maxBytes) {
1537
+ async function readBody$1(req, maxBytes) {
1538
1538
  const chunks = [];
1539
1539
  let total = 0;
1540
1540
  for await (const chunk of req) {
@@ -1706,7 +1706,7 @@ async function startWebsearchProxy(options) {
1706
1706
  websearchRequests += 1;
1707
1707
  lastWebsearchAt = (/* @__PURE__ */ new Date()).toISOString();
1708
1708
  try {
1709
- bodyBuffer = await readBody(req, 1e6);
1709
+ bodyBuffer = await readBody$1(req, 1e6);
1710
1710
  } catch {
1711
1711
  res.writeHead(413, { "Content-Type": "application/json" });
1712
1712
  res.end(JSON.stringify({ error: "Request body too large" }));
@@ -1803,6 +1803,310 @@ async function startWebsearchProxy(options) {
1803
1803
  };
1804
1804
  }
1805
1805
 
1806
+ //#endregion
1807
+ //#region src/websearch-native.ts
1808
+ /**
1809
+ * WebSearch Native Provider Mode (experimental)
1810
+ *
1811
+ * Uses model's native websearch capability based on ~/.factory/settings.json configuration.
1812
+ * Supported providers:
1813
+ * - Anthropic: web_search_20250305 server tool
1814
+ * - OpenAI: web_search tool via /responses endpoint
1815
+ */
1816
+ let cachedSettings = null;
1817
+ let settingsLastModified = 0;
1818
+ function getFactorySettings(logger) {
1819
+ const settingsPath = join(homedir(), ".factory", "settings.json");
1820
+ try {
1821
+ const stats = statSync(settingsPath);
1822
+ if (cachedSettings && stats.mtimeMs === settingsLastModified) return cachedSettings;
1823
+ cachedSettings = JSON.parse(readFileSync(settingsPath, "utf-8"));
1824
+ settingsLastModified = stats.mtimeMs;
1825
+ return cachedSettings;
1826
+ } catch (e) {
1827
+ logger.error("[websearch-native] Failed to load settings.json:", e.message);
1828
+ return null;
1829
+ }
1830
+ }
1831
+ function getCurrentModelConfig(logger) {
1832
+ const settings = getFactorySettings(logger);
1833
+ if (!settings) return null;
1834
+ const currentModelId = settings.sessionDefaultSettings?.model;
1835
+ if (!currentModelId) return null;
1836
+ const modelConfig = (settings.customModels || []).find((m) => m.id === currentModelId);
1837
+ if (modelConfig) {
1838
+ logger.log("[websearch-native] Model:", modelConfig.displayName || modelConfig.id, "| Provider:", modelConfig.provider);
1839
+ return modelConfig;
1840
+ }
1841
+ if (!currentModelId.startsWith("custom:")) return null;
1842
+ logger.log("[websearch-native] Model not found:", currentModelId);
1843
+ return null;
1844
+ }
1845
+ async function searchAnthropicNative(query, numResults, modelConfig, logger) {
1846
+ const { baseUrl, apiKey, model } = modelConfig;
1847
+ try {
1848
+ const requestBody = {
1849
+ model,
1850
+ max_tokens: 4096,
1851
+ stream: false,
1852
+ system: "You are a web search assistant. Use the web_search tool to find relevant information and return the results.",
1853
+ tools: [{
1854
+ type: "web_search_20250305",
1855
+ name: "web_search",
1856
+ max_uses: 1
1857
+ }],
1858
+ tool_choice: {
1859
+ type: "tool",
1860
+ name: "web_search"
1861
+ },
1862
+ messages: [{
1863
+ role: "user",
1864
+ content: `Search the web for: ${query}\n\nReturn up to ${numResults} relevant results.`
1865
+ }]
1866
+ };
1867
+ let endpoint = baseUrl;
1868
+ if (!endpoint.endsWith("/v1/messages")) endpoint = endpoint.replace(/\/$/, "") + "/v1/messages";
1869
+ logger.log("[websearch-native] Anthropic search:", query, "→", endpoint);
1870
+ const data = await (await fetch(endpoint, {
1871
+ method: "POST",
1872
+ headers: {
1873
+ "Content-Type": "application/json",
1874
+ "anthropic-version": "2023-06-01",
1875
+ "x-api-key": apiKey
1876
+ },
1877
+ body: JSON.stringify(requestBody)
1878
+ })).json();
1879
+ if (data.error) {
1880
+ logger.error("[websearch-native] Anthropic API error:", data.error.message);
1881
+ return null;
1882
+ }
1883
+ const results = [];
1884
+ for (const block of data.content || []) if (block.type === "web_search_tool_result") {
1885
+ for (const result of block.content || []) if (result.type === "web_search_result") results.push({
1886
+ title: result.title || "",
1887
+ url: result.url || "",
1888
+ content: result.snippet || result.page_content || ""
1889
+ });
1890
+ }
1891
+ logger.log("[websearch-native] Anthropic results:", results.length);
1892
+ return results.length > 0 ? results.slice(0, numResults) : null;
1893
+ } catch (e) {
1894
+ logger.error("[websearch-native] Anthropic error:", e.message);
1895
+ return null;
1896
+ }
1897
+ }
1898
+ async function searchOpenAINative(query, numResults, modelConfig, logger) {
1899
+ const { baseUrl, apiKey, model } = modelConfig;
1900
+ try {
1901
+ const requestBody = {
1902
+ model,
1903
+ stream: false,
1904
+ tools: [{ type: "web_search" }],
1905
+ tool_choice: "required",
1906
+ input: `Search the web for: ${query}\n\nReturn up to ${numResults} relevant results.`
1907
+ };
1908
+ let endpoint = baseUrl;
1909
+ if (!endpoint.endsWith("/responses")) endpoint = endpoint.replace(/\/$/, "") + "/responses";
1910
+ logger.log("[websearch-native] OpenAI search:", query, "→", endpoint);
1911
+ const data = await (await fetch(endpoint, {
1912
+ method: "POST",
1913
+ headers: {
1914
+ "Content-Type": "application/json",
1915
+ Authorization: `Bearer ${apiKey}`
1916
+ },
1917
+ body: JSON.stringify(requestBody)
1918
+ })).json();
1919
+ if (data.error) {
1920
+ logger.error("[websearch-native] OpenAI API error:", data.error.message);
1921
+ return null;
1922
+ }
1923
+ const results = [];
1924
+ for (const item of data.output || []) if (item.type === "message" && Array.isArray(item.content)) {
1925
+ for (const content of item.content) if (content.type === "output_text" && Array.isArray(content.annotations)) {
1926
+ for (const annotation of content.annotations) if (annotation.type === "url_citation" && annotation.url) results.push({
1927
+ title: annotation.title || "",
1928
+ url: annotation.url || "",
1929
+ content: annotation.title || ""
1930
+ });
1931
+ }
1932
+ }
1933
+ logger.log("[websearch-native] OpenAI results:", results.length);
1934
+ return results.length > 0 ? results.slice(0, numResults) : null;
1935
+ } catch (e) {
1936
+ logger.error("[websearch-native] OpenAI error:", e.message);
1937
+ return null;
1938
+ }
1939
+ }
1940
+ async function search(query, numResults, logger) {
1941
+ logger.log("[websearch-native] Search:", query);
1942
+ const modelConfig = getCurrentModelConfig(logger);
1943
+ if (!modelConfig) {
1944
+ logger.log("[websearch-native] No custom model configured");
1945
+ return {
1946
+ results: [],
1947
+ source: "none"
1948
+ };
1949
+ }
1950
+ const provider = modelConfig.provider;
1951
+ let results = null;
1952
+ if (provider === "anthropic") results = await searchAnthropicNative(query, numResults, modelConfig, logger);
1953
+ else if (provider === "openai") results = await searchOpenAINative(query, numResults, modelConfig, logger);
1954
+ else logger.log("[websearch-native] Unsupported provider:", provider);
1955
+ if (results && results.length > 0) return {
1956
+ results,
1957
+ source: `native-${provider}`
1958
+ };
1959
+ return {
1960
+ results: [],
1961
+ source: "none"
1962
+ };
1963
+ }
1964
+ function parseRequestBody(body) {
1965
+ try {
1966
+ const parsed = JSON.parse(body);
1967
+ const query = typeof parsed.query === "string" ? parsed.query.trim() : "";
1968
+ if (!query) return null;
1969
+ return {
1970
+ query,
1971
+ numResults: typeof parsed.numResults === "number" && Number.isFinite(parsed.numResults) ? Math.max(1, parsed.numResults) : 10
1972
+ };
1973
+ } catch {
1974
+ return null;
1975
+ }
1976
+ }
1977
+ async function readBody(req, maxBytes) {
1978
+ const chunks = [];
1979
+ let total = 0;
1980
+ for await (const chunk of req) {
1981
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1982
+ total += buf.length;
1983
+ if (total > maxBytes) throw new Error(`Request body too large (${total} bytes)`);
1984
+ chunks.push(buf);
1985
+ }
1986
+ return Buffer.concat(chunks).toString("utf-8");
1987
+ }
1988
+ async function startNativeWebsearchProxy(options) {
1989
+ const logger = options.logger ?? console;
1990
+ const host = options.host ?? "127.0.0.1";
1991
+ const port = options.port ?? 0;
1992
+ const factoryApiUrl = options.factoryApiUrl ?? "https://api.factory.ai";
1993
+ let totalRequests = 0;
1994
+ let websearchRequests = 0;
1995
+ let lastWebsearchAt = null;
1996
+ let lastWebsearchSource = null;
1997
+ const server = createServer((req, res) => {
1998
+ (async () => {
1999
+ totalRequests += 1;
2000
+ const rawUrl = req.url;
2001
+ if (!rawUrl) {
2002
+ res.writeHead(400, { "Content-Type": "application/json" });
2003
+ res.end(JSON.stringify({ error: "Missing URL" }));
2004
+ return;
2005
+ }
2006
+ const requestUrl = new URL(rawUrl, `http://${req.headers.host ?? "127.0.0.1"}`);
2007
+ if (requestUrl.pathname === "/health") {
2008
+ res.writeHead(200, { "Content-Type": "application/json" });
2009
+ res.end(JSON.stringify({
2010
+ status: "ok",
2011
+ mode: "native-provider",
2012
+ requests: {
2013
+ total: totalRequests,
2014
+ websearch: websearchRequests,
2015
+ lastWebsearchAt,
2016
+ lastWebsearchSource
2017
+ }
2018
+ }));
2019
+ return;
2020
+ }
2021
+ if (requestUrl.pathname === "/api/tools/exa/search" && req.method === "POST") {
2022
+ websearchRequests += 1;
2023
+ lastWebsearchAt = (/* @__PURE__ */ new Date()).toISOString();
2024
+ try {
2025
+ const parsed = parseRequestBody(await readBody(req, 1e6));
2026
+ if (!parsed) {
2027
+ res.writeHead(400, { "Content-Type": "application/json" });
2028
+ res.end(JSON.stringify({
2029
+ error: "Invalid request body",
2030
+ results: []
2031
+ }));
2032
+ return;
2033
+ }
2034
+ const result = await search(parsed.query, parsed.numResults, logger);
2035
+ lastWebsearchSource = result.source;
2036
+ logger.log("[websearch-native] Results:", result.results.length, "from", result.source);
2037
+ res.writeHead(200, { "Content-Type": "application/json" });
2038
+ res.end(JSON.stringify({ results: result.results }));
2039
+ } catch (e) {
2040
+ logger.error("[websearch-native] Search error:", e.message);
2041
+ res.writeHead(500, { "Content-Type": "application/json" });
2042
+ res.end(JSON.stringify({
2043
+ error: String(e),
2044
+ results: []
2045
+ }));
2046
+ }
2047
+ return;
2048
+ }
2049
+ const pathAndQuery = `${requestUrl.pathname}${requestUrl.search || ""}`;
2050
+ const targetUrl = new URL(pathAndQuery, factoryApiUrl);
2051
+ logger.log("[websearch-native] Proxy:", req.method ?? "GET", pathAndQuery);
2052
+ const headers = new Headers();
2053
+ for (const [key, value] of Object.entries(req.headers)) {
2054
+ if (value === void 0) continue;
2055
+ if (key.toLowerCase() === "host") continue;
2056
+ if (key.toLowerCase() === "accept-encoding") continue;
2057
+ if (Array.isArray(value)) for (const v of value) headers.append(key, v);
2058
+ else headers.set(key, value);
2059
+ }
2060
+ headers.set("accept-encoding", "identity");
2061
+ try {
2062
+ let body;
2063
+ if (req.method !== "GET" && req.method !== "HEAD") body = await readBody(req, 1e7);
2064
+ const response = await fetch(targetUrl, {
2065
+ method: req.method,
2066
+ headers,
2067
+ body,
2068
+ redirect: "manual"
2069
+ });
2070
+ for (const [key, value] of response.headers) {
2071
+ if (key.toLowerCase() === "content-encoding") continue;
2072
+ if (key.toLowerCase() === "content-length") continue;
2073
+ res.setHeader(key, value);
2074
+ }
2075
+ res.statusCode = response.status;
2076
+ if (!response.body) {
2077
+ res.end();
2078
+ return;
2079
+ }
2080
+ const arrayBuffer = await response.arrayBuffer();
2081
+ res.end(Buffer.from(arrayBuffer));
2082
+ } catch (e) {
2083
+ res.writeHead(502, { "Content-Type": "application/json" });
2084
+ res.end(JSON.stringify({ error: "Proxy failed: " + e.message }));
2085
+ }
2086
+ })();
2087
+ });
2088
+ await new Promise((resolve, reject) => {
2089
+ server.once("error", reject);
2090
+ server.listen(port, host, () => {
2091
+ server.off("error", reject);
2092
+ resolve();
2093
+ });
2094
+ });
2095
+ const address = server.address();
2096
+ if (!address || typeof address === "string") {
2097
+ server.close();
2098
+ throw new Error("Failed to bind native websearch proxy server");
2099
+ }
2100
+ const baseUrl = `http://${host}:${address.port}`;
2101
+ logger.log("[websearch-native] proxy listening on", baseUrl);
2102
+ return {
2103
+ baseUrl,
2104
+ close: () => new Promise((resolve) => {
2105
+ server.close(() => resolve());
2106
+ })
2107
+ };
2108
+ }
2109
+
1806
2110
  //#endregion
1807
2111
  //#region src/droid-adapter.ts
1808
2112
  function createDroidAdapter(options) {
@@ -2044,18 +2348,8 @@ function createDroidAdapter(options) {
2044
2348
  };
2045
2349
  const reasoningEffort = env.DROID_ACP_REASONING_EFFORT;
2046
2350
  if (typeof reasoningEffort === "string" && reasoningEffort.trim().length > 0) args.push("--reasoning-effort", reasoningEffort.trim());
2047
- const hasExplicitToggle = typeof env.DROID_ACP_WEBSEARCH === "string";
2048
- const smitheryConfigured = typeof env.SMITHERY_API_KEY === "string" && env.SMITHERY_API_KEY.trim().length > 0 && typeof env.SMITHERY_PROFILE === "string" && env.SMITHERY_PROFILE.trim().length > 0;
2049
- const forwardConfigured = typeof env.DROID_ACP_WEBSEARCH_FORWARD_URL === "string" && env.DROID_ACP_WEBSEARCH_FORWARD_URL.trim().length > 0;
2050
- if (hasExplicitToggle ? isEnvEnabled(env.DROID_ACP_WEBSEARCH) : smitheryConfigured || forwardConfigured) {
2351
+ if (isEnvEnabled(env.DROID_ACP_WEBSEARCH_NATIVE)) {
2051
2352
  stopWebsearchProxy();
2052
- const upstreamBaseUrl = env.DROID_ACP_WEBSEARCH_UPSTREAM_URL ?? env.FACTORY_API_BASE_URL_OVERRIDE ?? "https://api.factory.ai";
2053
- const forwardModeRaw = env.DROID_ACP_WEBSEARCH_FORWARD_MODE;
2054
- const forwardUrlRaw = typeof env.DROID_ACP_WEBSEARCH_FORWARD_URL === "string" ? env.DROID_ACP_WEBSEARCH_FORWARD_URL.trim() : "";
2055
- const forwardPrefixMatch = forwardUrlRaw.match(/^mcp:(.*)$/i);
2056
- const websearchForwardUrl = forwardUrlRaw.length > 0 ? forwardUrlRaw : void 0;
2057
- const forwardModeNormalized = typeof forwardModeRaw === "string" ? forwardModeRaw.trim().toLowerCase() : "";
2058
- const websearchForwardMode = forwardModeNormalized === "mcp" ? "mcp" : forwardModeNormalized === "http" ? "http" : forwardPrefixMatch ? "mcp" : "http";
2059
2353
  const host = env.DROID_ACP_WEBSEARCH_HOST ?? "127.0.0.1";
2060
2354
  const portRaw = env.DROID_ACP_WEBSEARCH_PORT;
2061
2355
  let port;
@@ -2064,12 +2358,10 @@ function createDroidAdapter(options) {
2064
2358
  if (Number.isNaN(parsed) || parsed < 0 || parsed > 65535) throw new Error(`Invalid DROID_ACP_WEBSEARCH_PORT: ${portRaw}`);
2065
2359
  port = parsed;
2066
2360
  }
2067
- websearchProxy = await startWebsearchProxy({
2068
- upstreamBaseUrl,
2069
- websearchForwardUrl: forwardPrefixMatch ? forwardPrefixMatch[1]?.trim() : websearchForwardUrl,
2070
- websearchForwardMode,
2071
- smitheryApiKey: env.SMITHERY_API_KEY,
2072
- smitheryProfile: env.SMITHERY_PROFILE,
2361
+ const factoryApiUrl = env.DROID_ACP_WEBSEARCH_UPSTREAM_URL ?? env.FACTORY_API_BASE_URL_OVERRIDE ?? "https://api.factory.ai";
2362
+ logger.log("[websearch] Starting native provider proxy (experimental)...");
2363
+ websearchProxy = await startNativeWebsearchProxy({
2364
+ factoryApiUrl,
2073
2365
  host,
2074
2366
  port,
2075
2367
  logger
@@ -2077,6 +2369,41 @@ function createDroidAdapter(options) {
2077
2369
  if (!env.FACTORY_API_KEY) env.FACTORY_API_KEY = "droid-acp-websearch";
2078
2370
  env.FACTORY_API_BASE_URL_OVERRIDE = websearchProxy.baseUrl;
2079
2371
  env.FACTORY_API_BASE_URL = websearchProxy.baseUrl;
2372
+ } else {
2373
+ const hasExplicitToggle = typeof env.DROID_ACP_WEBSEARCH === "string";
2374
+ const smitheryConfigured = typeof env.SMITHERY_API_KEY === "string" && env.SMITHERY_API_KEY.trim().length > 0 && typeof env.SMITHERY_PROFILE === "string" && env.SMITHERY_PROFILE.trim().length > 0;
2375
+ const forwardConfigured = typeof env.DROID_ACP_WEBSEARCH_FORWARD_URL === "string" && env.DROID_ACP_WEBSEARCH_FORWARD_URL.trim().length > 0;
2376
+ if (hasExplicitToggle ? isEnvEnabled(env.DROID_ACP_WEBSEARCH) : smitheryConfigured || forwardConfigured) {
2377
+ stopWebsearchProxy();
2378
+ const upstreamBaseUrl = env.DROID_ACP_WEBSEARCH_UPSTREAM_URL ?? env.FACTORY_API_BASE_URL_OVERRIDE ?? "https://api.factory.ai";
2379
+ const forwardModeRaw = env.DROID_ACP_WEBSEARCH_FORWARD_MODE;
2380
+ const forwardUrlRaw = typeof env.DROID_ACP_WEBSEARCH_FORWARD_URL === "string" ? env.DROID_ACP_WEBSEARCH_FORWARD_URL.trim() : "";
2381
+ const forwardPrefixMatch = forwardUrlRaw.match(/^mcp:(.*)$/i);
2382
+ const websearchForwardUrl = forwardUrlRaw.length > 0 ? forwardUrlRaw : void 0;
2383
+ const forwardModeNormalized = typeof forwardModeRaw === "string" ? forwardModeRaw.trim().toLowerCase() : "";
2384
+ const websearchForwardMode = forwardModeNormalized === "mcp" ? "mcp" : forwardModeNormalized === "http" ? "http" : forwardPrefixMatch ? "mcp" : "http";
2385
+ const host = env.DROID_ACP_WEBSEARCH_HOST ?? "127.0.0.1";
2386
+ const portRaw = env.DROID_ACP_WEBSEARCH_PORT;
2387
+ let port;
2388
+ if (typeof portRaw === "string" && portRaw.length > 0) {
2389
+ const parsed = Number.parseInt(portRaw, 10);
2390
+ if (Number.isNaN(parsed) || parsed < 0 || parsed > 65535) throw new Error(`Invalid DROID_ACP_WEBSEARCH_PORT: ${portRaw}`);
2391
+ port = parsed;
2392
+ }
2393
+ websearchProxy = await startWebsearchProxy({
2394
+ upstreamBaseUrl,
2395
+ websearchForwardUrl: forwardPrefixMatch ? forwardPrefixMatch[1]?.trim() : websearchForwardUrl,
2396
+ websearchForwardMode,
2397
+ smitheryApiKey: env.SMITHERY_API_KEY,
2398
+ smitheryProfile: env.SMITHERY_PROFILE,
2399
+ host,
2400
+ port,
2401
+ logger
2402
+ });
2403
+ if (!env.FACTORY_API_KEY) env.FACTORY_API_KEY = "droid-acp-websearch";
2404
+ env.FACTORY_API_BASE_URL_OVERRIDE = websearchProxy.baseUrl;
2405
+ env.FACTORY_API_BASE_URL = websearchProxy.baseUrl;
2406
+ }
2080
2407
  }
2081
2408
  process$1 = spawn(executable, args, {
2082
2409
  stdio: [
@@ -3858,5 +4185,5 @@ function runAcp() {
3858
4185
  }
3859
4186
 
3860
4187
  //#endregion
3861
- export { ACP_MODES as a, isEnvEnabled as c, startWebsearchProxy as i, isWindows as l, DroidAcpAgent as n, Pushable as o, createDroidAdapter as r, findDroidExecutable as s, runAcp as t };
3862
- //# sourceMappingURL=acp-agent-k2cYadeP.mjs.map
4188
+ export { startWebsearchProxy as a, findDroidExecutable as c, startNativeWebsearchProxy as i, isEnvEnabled as l, DroidAcpAgent as n, ACP_MODES as o, createDroidAdapter as r, Pushable as s, runAcp as t, isWindows as u };
4189
+ //# sourceMappingURL=acp-agent-DYlR7vNB.mjs.map