@heylemon/lemonade 0.4.7 → 0.4.9

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.
@@ -173,7 +173,7 @@ export function buildAgentSystemPrompt(params) {
173
173
  ls: "List directory contents",
174
174
  exec: "Run shell commands (use lemon-* CLIs for integrations; for third-party apps like Trello/Jira/Asana use ONLY lemon-composio, never bare commands)",
175
175
  process: "Manage background exec sessions",
176
- web_search: "Search the web (Brave API)",
176
+ web_search: "Search the web (DuckDuckGo by default, no API key needed)",
177
177
  web_fetch: "Fetch and extract readable content from a URL",
178
178
  // Channel docking: add login tools here when a channel needs interactive linking.
179
179
  browser: "Control Lemonade's dedicated browser (never the user's personal browser)",
@@ -393,7 +393,7 @@ export function buildAgentSystemPrompt(params) {
393
393
  "1. Service has a dedicated `lemon-*` CLI tool (Gmail, Calendar, Drive, Docs, Sheets, Slides, Notion, Slack, YouTube) → **always use the CLI tool**. Never open these in a browser or via URI.",
394
394
  '2. ANY other app/service (Trello, Jira, LinkedIn, HubSpot, Asana, Salesforce, Todoist, etc.) → **always run `lemon-composio search "<task>"`** first. Never say you can\'t do it without searching.',
395
395
  "3. If Composio has no results or connect fails → use Lemonade's dedicated `browser` tool as fallback to complete the task directly.",
396
- "4. User wants an *answer* from the web → use `web_search` (+ `web_fetch` if needed).",
396
+ "4. User wants an *answer* from the web → use `web_search` (works out of the box, no API key needed). If it fails, use the `browser` tool to search Google as fallback. Never ask the user for API keys.",
397
397
  "5. User wants to *interact* with a page (fill forms, click buttons, scrape data) → use Lemonade's dedicated `browser` tool.",
398
398
  "6. User wants to *view* a general website (no CLI or Composio tool exists) → use Lemonade's dedicated `browser` tool to navigate there.",
399
399
  "",
@@ -489,7 +489,7 @@ export function buildAgentSystemPrompt(params) {
489
489
  '- "create a new doc" → `lemon-docs create "Untitled"` (CLI, not browser)',
490
490
  '- "start a Google Meet" → `browser` navigate to `https://meet.new`',
491
491
  '- "check my email" → `lemon-gmail list` (CLI, not browser)',
492
- '- "what is the capital of France?" → use `web_search` (user wants the answer, not a page)',
492
+ '- "what is the capital of France?" → use `web_search` (always available)',
493
493
  "",
494
494
  "## IDE & Coding Agent Control",
495
495
  "Control AI coding agents via CLI — never type into GUI windows:",
@@ -537,6 +537,12 @@ export function buildAgentSystemPrompt(params) {
537
537
  "ALWAYS use the `cron` tool (action: add/update/remove/list/run/status) for managing cron jobs and reminders.",
538
538
  "NEVER use `exec` or shell commands to read/write cron files (e.g. ~/.lemonade/cron/jobs.json) directly — the gateway cron service will not detect the change.",
539
539
  "",
540
+ "### Choosing sessionTarget and payload kind",
541
+ "Pick the right type based on what the job needs to do:",
542
+ '- **Simple reminder/nudge** (just tell the user something) → `sessionTarget: "main"`, `payload.kind: "systemEvent"` with the reminder text.',
543
+ '- **Task that requires action** (send a message, fetch data, compose an email, etc.) → `sessionTarget: "isolated"`, `payload.kind: "agentTurn"` with the task as `message`. Set `deliver: true` and `channel` / `to` if the output should be sent to a channel (WhatsApp, Slack, email).',
544
+ "If a job needs the agent to actually DO something (send, search, compose, browse), it MUST be isolated+agentTurn. Using main+systemEvent for action tasks will silently fail because the agent doesn't get a full turn to execute tools.",
545
+ "",
540
546
  "### Cancelling / stopping reminders",
541
547
  "When the user asks to stop, cancel, turn off, or remove a reminder or recurring notification — in ANY phrasing — you MUST actually remove the cron job. Do NOT just acknowledge verbally.",
542
548
  "Steps: 1) `cron` action:list → find the matching job(s) by name/text/schedule, 2) `cron` action:remove with the jobId for each match.",
@@ -1,8 +1,9 @@
1
1
  import { Type } from "@sinclair/typebox";
2
+ import * as DDG from "duck-duck-scrape";
2
3
  import { formatCliCommand } from "../../cli/command-format.js";
3
4
  import { jsonResult, readNumberParam, readStringParam } from "./common.js";
4
5
  import { DEFAULT_CACHE_TTL_MINUTES, DEFAULT_TIMEOUT_SECONDS, normalizeCacheKey, readCache, readResponseText, resolveCacheTtlMs, resolveTimeoutSeconds, withTimeout, writeCache, } from "./web-shared.js";
5
- const SEARCH_PROVIDERS = ["brave", "perplexity"];
6
+ const SEARCH_PROVIDERS = ["brave", "perplexity", "duckduckgo"];
6
7
  const DEFAULT_SEARCH_COUNT = 5;
7
8
  const MAX_SEARCH_COUNT = 10;
8
9
  const BRAVE_SEARCH_ENDPOINT = "https://api.search.brave.com/res/v1/web/search";
@@ -74,7 +75,9 @@ function resolveSearchProvider(search) {
74
75
  return "perplexity";
75
76
  if (raw === "brave")
76
77
  return "brave";
77
- return "brave";
78
+ if (raw === "duckduckgo" || raw === "ddg")
79
+ return "duckduckgo";
80
+ return "duckduckgo";
78
81
  }
79
82
  function resolvePerplexityConfig(search) {
80
83
  if (!search || typeof search !== "object")
@@ -212,6 +215,25 @@ async function runPerplexitySearch(params) {
212
215
  const citations = data.citations ?? [];
213
216
  return { content, citations };
214
217
  }
218
+ async function runDuckDuckGoSearch(params) {
219
+ const start = Date.now();
220
+ const searchResults = await DDG.search(params.query, {
221
+ safeSearch: DDG.SafeSearchType.MODERATE,
222
+ });
223
+ const results = (searchResults.results ?? []).slice(0, params.count).map((r) => ({
224
+ title: r.title ?? "",
225
+ url: r.url ?? "",
226
+ description: r.description?.replace(/<\/?b>/g, "") ?? "",
227
+ siteName: resolveSiteName(r.url ?? ""),
228
+ }));
229
+ return {
230
+ query: params.query,
231
+ provider: "duckduckgo",
232
+ count: results.length,
233
+ tookMs: Date.now() - start,
234
+ results,
235
+ };
236
+ }
215
237
  async function runWebSearch(params) {
216
238
  const cacheKey = normalizeCacheKey(params.provider === "brave"
217
239
  ? `${params.provider}:${params.query}:${params.count}:${params.country || "default"}:${params.search_lang || "default"}:${params.ui_lang || "default"}:${params.freshness || "default"}`
@@ -239,6 +261,15 @@ async function runWebSearch(params) {
239
261
  writeCache(SEARCH_CACHE, cacheKey, payload, params.cacheTtlMs);
240
262
  return payload;
241
263
  }
264
+ if (params.provider === "duckduckgo") {
265
+ const ddgResult = await runDuckDuckGoSearch({
266
+ query: params.query,
267
+ count: params.count,
268
+ timeoutSeconds: params.timeoutSeconds,
269
+ });
270
+ writeCache(SEARCH_CACHE, cacheKey, ddgResult, params.cacheTtlMs);
271
+ return ddgResult;
272
+ }
242
273
  if (params.provider !== "brave") {
243
274
  throw new Error("Unsupported web search provider.");
244
275
  }
@@ -294,19 +325,22 @@ export function createWebSearchTool(options) {
294
325
  return null;
295
326
  const provider = resolveSearchProvider(search);
296
327
  const perplexityConfig = resolvePerplexityConfig(search);
297
- const description = provider === "perplexity"
298
- ? "Search the web using Perplexity Sonar (direct or via OpenRouter). Returns AI-synthesized answers with citations from real-time web search."
299
- : "Search the web using Brave Search API. Supports region-specific and localized search via country and language parameters. Returns titles, URLs, and snippets for fast research.";
328
+ const descriptions = {
329
+ perplexity: "Search the web using Perplexity Sonar (direct or via OpenRouter). Returns AI-synthesized answers with citations from real-time web search.",
330
+ brave: "Search the web using Brave Search API. Supports region-specific and localized search via country and language parameters. Returns titles, URLs, and snippets for fast research.",
331
+ duckduckgo: "Search the web using DuckDuckGo (no API key required). Returns titles, URLs, and snippets for fast research.",
332
+ };
300
333
  return {
301
334
  label: "Web Search",
302
335
  name: "web_search",
303
- description,
336
+ description: descriptions[provider] ?? descriptions.duckduckgo,
304
337
  parameters: WebSearchSchema,
305
338
  execute: async (_toolCallId, args) => {
339
+ let effectiveProvider = provider;
306
340
  const perplexityAuth = provider === "perplexity" ? resolvePerplexityApiKey(perplexityConfig) : undefined;
307
341
  const apiKey = provider === "perplexity" ? perplexityAuth?.apiKey : resolveSearchApiKey(search);
308
- if (!apiKey) {
309
- return jsonResult(missingSearchKeyPayload(provider));
342
+ if (!apiKey && provider !== "duckduckgo") {
343
+ effectiveProvider = "duckduckgo";
310
344
  }
311
345
  const params = args;
312
346
  const query = readStringParam(params, "query", { required: true });
@@ -315,7 +349,7 @@ export function createWebSearchTool(options) {
315
349
  const search_lang = readStringParam(params, "search_lang");
316
350
  const ui_lang = readStringParam(params, "ui_lang");
317
351
  const rawFreshness = readStringParam(params, "freshness");
318
- if (rawFreshness && provider !== "brave") {
352
+ if (rawFreshness && effectiveProvider !== "brave") {
319
353
  return jsonResult({
320
354
  error: "unsupported_freshness",
321
355
  message: "freshness is only supported by the Brave web_search provider.",
@@ -333,10 +367,10 @@ export function createWebSearchTool(options) {
333
367
  const result = await runWebSearch({
334
368
  query,
335
369
  count: resolveSearchCount(count, DEFAULT_SEARCH_COUNT),
336
- apiKey,
370
+ apiKey: apiKey ?? "",
337
371
  timeoutSeconds: resolveTimeoutSeconds(search?.timeoutSeconds, DEFAULT_TIMEOUT_SECONDS),
338
372
  cacheTtlMs: resolveCacheTtlMs(search?.cacheTtlMinutes, DEFAULT_CACHE_TTL_MINUTES),
339
- provider,
373
+ provider: effectiveProvider,
340
374
  country,
341
375
  search_lang,
342
376
  ui_lang,
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.4.7",
3
- "commit": "65ee54b903b6ee3cec348f46541ef82ccbcb229c",
4
- "builtAt": "2026-02-24T11:18:11.706Z"
2
+ "version": "0.4.9",
3
+ "commit": "3b6838d381879be6654e0402185e231a15bf7f3c",
4
+ "builtAt": "2026-02-25T05:17:09.702Z"
5
5
  }
@@ -1 +1 @@
1
- be64d0d57f69ca463e3b2ef7886f231d8d7b8e3ec39975cb49e8ce91204fc307
1
+ edeb39165d712bc22ac4db917397e97d960bc33ac988beebcfb2148e4ca02cf6
@@ -143,7 +143,7 @@ const FIELD_LABELS = {
143
143
  "tools.message.broadcast.enabled": "Enable Message Broadcast",
144
144
  "tools.web.search.enabled": "Enable Web Search Tool",
145
145
  "tools.web.search.provider": "Web Search Provider",
146
- "tools.web.search.apiKey": "Brave Search API Key",
146
+ "tools.web.search.apiKey": "Search API Key (Brave/Perplexity, optional)",
147
147
  "tools.web.search.maxResults": "Web Search Max Results",
148
148
  "tools.web.search.timeoutSeconds": "Web Search Timeout (sec)",
149
149
  "tools.web.search.cacheTtlMinutes": "Web Search Cache TTL (min)",
@@ -358,9 +358,9 @@ const FIELD_HELP = {
358
358
  "tools.message.crossContext.marker.prefix": 'Text prefix for cross-context markers (supports "{channel}").',
359
359
  "tools.message.crossContext.marker.suffix": 'Text suffix for cross-context markers (supports "{channel}").',
360
360
  "tools.message.broadcast.enabled": "Enable broadcast action (default: true).",
361
- "tools.web.search.enabled": "Enable the web_search tool (requires a provider API key).",
362
- "tools.web.search.provider": 'Search provider ("brave" or "perplexity").',
363
- "tools.web.search.apiKey": "Brave Search API key (fallback: BRAVE_API_KEY env var).",
361
+ "tools.web.search.enabled": "Enable the web_search tool.",
362
+ "tools.web.search.provider": 'Search provider ("duckduckgo", "brave", or "perplexity"). Default: "duckduckgo" (free, no API key).',
363
+ "tools.web.search.apiKey": "Brave Search API key (fallback: BRAVE_API_KEY env var). Only needed for Brave provider.",
364
364
  "tools.web.search.maxResults": "Default number of results to return (1-10).",
365
365
  "tools.web.search.timeoutSeconds": "Timeout in seconds for web_search requests.",
366
366
  "tools.web.search.cacheTtlMinutes": "Cache TTL in minutes for web_search results.",
@@ -149,7 +149,9 @@ export const ToolPolicySchema = ToolPolicyBaseSchema.superRefine((value, ctx) =>
149
149
  export const ToolsWebSearchSchema = z
150
150
  .object({
151
151
  enabled: z.boolean().optional(),
152
- provider: z.union([z.literal("brave"), z.literal("perplexity")]).optional(),
152
+ provider: z
153
+ .union([z.literal("brave"), z.literal("perplexity"), z.literal("duckduckgo")])
154
+ .optional(),
153
155
  apiKey: z.string().optional(),
154
156
  maxResults: z.number().int().positive().optional(),
155
157
  timeoutSeconds: z.number().int().positive().optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heylemon/lemonade",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
4
4
  "description": "AI gateway CLI for Lemon - local AI assistant with integrations",
5
5
  "publishConfig": {
6
6
  "access": "restricted"
@@ -198,6 +198,7 @@
198
198
  "detect-libc": "^2.1.2",
199
199
  "discord-api-types": "^0.38.37",
200
200
  "dotenv": "^17.2.3",
201
+ "duck-duck-scrape": "^2.2.7",
201
202
  "express": "^5.2.1",
202
203
  "file-type": "^21.3.0",
203
204
  "grammy": "^1.39.3",