@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 (
|
|
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` (
|
|
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` (
|
|
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
|
-
|
|
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
|
|
298
|
-
|
|
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
|
-
|
|
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 &&
|
|
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,
|
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
edeb39165d712bc22ac4db917397e97d960bc33ac988beebcfb2148e4ca02cf6
|
package/dist/config/schema.js
CHANGED
|
@@ -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": "
|
|
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
|
|
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
|
|
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.
|
|
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",
|