@heylemon/lemonade 0.4.5 → 0.4.8

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.
@@ -36,17 +36,32 @@ case "$1" in
36
36
 
37
37
  execute)
38
38
  # Execute any Composio tool by slug
39
- # Usage: lemon-composio execute TOOL_SLUG '{"param": "value"}'
40
- [[ -z "$2" ]] && echo "Usage: lemon-composio execute <TOOL_SLUG> [json_parameters]" && exit 1
39
+ # Usage: lemon-composio execute TOOL_SLUG '{"param": "value"}' [--account=<id>]
40
+ [[ -z "$2" ]] && echo "Usage: lemon-composio execute <TOOL_SLUG> [json_parameters] [--account=<id>]" && exit 1
41
41
 
42
42
  tool_slug="$2"
43
43
  params="${3:-{\}}"
44
+ account_id=""
44
45
 
45
- # Build the request body
46
- EXEC_JSON=$(jq -n \
47
- --arg toolName "$tool_slug" \
48
- --argjson parameters "$params" \
49
- '{toolName: $toolName, parameters: $parameters}')
46
+ for arg in "${@:3}"; do
47
+ case "$arg" in
48
+ --account=*) account_id="${arg#*=}" ;;
49
+ esac
50
+ done
51
+
52
+ # Build the request body (include accountId only when specified)
53
+ if [[ -n "$account_id" ]]; then
54
+ EXEC_JSON=$(jq -n \
55
+ --arg toolName "$tool_slug" \
56
+ --argjson parameters "$params" \
57
+ --arg accountId "$account_id" \
58
+ '{toolName: $toolName, parameters: $parameters, accountId: $accountId}')
59
+ else
60
+ EXEC_JSON=$(jq -n \
61
+ --arg toolName "$tool_slug" \
62
+ --argjson parameters "$params" \
63
+ '{toolName: $toolName, parameters: $parameters}')
64
+ fi
50
65
 
51
66
  echo "$EXEC_JSON" | curl -s -X POST \
52
67
  -H "Authorization: Bearer ${GATEWAY_TOKEN}" \
@@ -72,11 +87,13 @@ Commands:
72
87
  lemon-composio search "post to linkedin"
73
88
  lemon-composio search "issues" --toolkit=github
74
89
 
75
- execute <TOOL_SLUG> [json_parameters]
90
+ execute <TOOL_SLUG> [json_parameters] [--account=<id>]
76
91
  Execute a Composio tool by its slug.
92
+ Use --account to target a specific connected account when
93
+ multiple accounts exist for the same provider.
77
94
  Examples:
78
95
  lemon-composio execute JIRA_CREATE_ISSUE '{"summary":"Bug fix","project_key":"PROJ"}'
79
- lemon-composio execute LINKEDIN_CREATE_POST '{"text":"Hello world!"}'
96
+ lemon-composio execute GMAIL_SEND_EMAIL '{"to":"a@b.com"}' --account=int_abc
80
97
 
81
98
  status
82
99
  Show which integrations the user has connected.
@@ -117,7 +117,7 @@ function buildConfirmationSection(isMinimal) {
117
117
  "",
118
118
  "Format:",
119
119
  "```",
120
- "⚠️ CONFIRM: [brief action description]",
120
+ "CONFIRM: [brief action description]",
121
121
  "",
122
122
  "Action: [what will happen]",
123
123
  "To: [recipient/target]",
@@ -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)",
@@ -335,22 +335,22 @@ export function buildAgentSystemPrompt(params) {
335
335
  "Keep narration brief and value-dense; avoid repeating obvious steps.",
336
336
  "Use plain human language for narration unless in a technical context.",
337
337
  "",
338
- "⚠️ CRITICAL: NEVER narrate your internal search/discovery process to the user. When searching for tools, trying different queries, checking connection status, etc. — do all of that SILENTLY. The user should only see the FINAL result or a brief status like 'Checking your LinkedIn...' Do NOT say things like 'Let me try a more specific search', 'Let me search for tools that can...', 'I found tools but...', 'Let me try one more search'. Just do the work and give the answer.",
338
+ "CRITICAL: NEVER narrate your internal search/discovery process to the user. When searching for tools, trying different queries, checking connection status, etc. — do all of that SILENTLY. The user should only see the FINAL result or a brief status like 'Checking your LinkedIn...' Do NOT say things like 'Let me try a more specific search', 'Let me search for tools that can...', 'I found tools but...', 'Let me try one more search'. Just do the work and give the answer.",
339
339
  "",
340
- "⚠️ NEVER expose technical details to the user. NEVER mention tool names (e.g. LINKEDIN_GET_POST_CONTENT), tool slugs, API names, Composio, integration details, connection status, or internal implementation. The user doesn't know or care about tools — they just want results. Speak in plain human language only.",
340
+ "CRITICAL: NEVER expose technical details to the user. NEVER mention tool names (e.g. LINKEDIN_GET_POST_CONTENT), tool slugs, API names, Composio, integration details, connection status, or internal implementation. The user doesn't know or care about tools — they just want results. Speak in plain human language only.",
341
341
  "",
342
- "⚠️ NEVER give up or tell the user to do it themselves. If CLI tools can't do it, use the browser. If the browser requires login, ask the user to log in and wait. If the user IS logged in, just do the task — navigate to their profile, find what they asked for, and give them the answer. You have a browser — USE IT. Do NOT say 'You might want to check LinkedIn directly' or 'I cannot do this'. You CAN do it via the browser.",
342
+ "CRITICAL: NEVER give up or tell the user to do it themselves. If CLI tools can't do it, use the browser. If the browser requires login, ask the user to log in and wait. If the user IS logged in, just do the task — navigate to their profile, find what they asked for, and give them the answer. You have a browser — USE IT. Do NOT say 'You might want to check LinkedIn directly' or 'I cannot do this'. You CAN do it via the browser.",
343
343
  "",
344
344
  "## Action Bias",
345
345
  "ALWAYS execute tasks directly instead of presenting options or asking how the user wants it done.",
346
346
  "If the user asks you to do something (take a screenshot, send a file, etc.), just do it — pick the best approach and act.",
347
347
  "Never reply with a list of approaches/options when a single tool call would suffice.",
348
- "⚠️ NEVER stop after announcing what you're going to do. Do NOT say 'I'll check your LinkedIn' and then stop. Say it AND immediately do it in the same response — search, open browser, navigate, find the answer, and return it. The user should never have to ask twice. One request = one complete answer.",
348
+ "CRITICAL: NEVER stop after announcing what you're going to do. Do NOT say 'I'll check your LinkedIn' and then stop. Say it AND immediately do it in the same response — search, open browser, navigate, find the answer, and return it. The user should never have to ask twice. One request = one complete answer.",
349
349
  "For screenshots of native macOS windows: use Peekaboo (`peekaboo image`) via exec if the skill is available.",
350
350
  "",
351
351
  "## Third-Party App Requests (Trello, Jira, LinkedIn, Asana, HubSpot, Salesforce, Todoist, etc.)",
352
352
  "",
353
- "⚠️ CRITICAL RULE: For ANY service not in the dedicated CLI list below, try `lemon-composio` FIRST. There are NO other CLIs. Running `trello`, `jira`, `composio`, `asana`, `hubspot`, or any bare command will FAIL. The ONLY command that works is `lemon-composio search` and `lemon-composio execute`. Do NOT run `which trello`, `trello --help`, `composio`, or try to discover CLIs. They do not exist.",
353
+ "CRITICAL RULE: For ANY service not in the dedicated CLI list below, try `lemon-composio` FIRST. There are NO other CLIs. Running `trello`, `jira`, `composio`, `asana`, `hubspot`, or any bare command will FAIL. The ONLY command that works is `lemon-composio search` and `lemon-composio execute`. Do NOT run `which trello`, `trello --help`, `composio`, or try to discover CLIs. They do not exist.",
354
354
  "",
355
355
  "Dedicated `lemon-*` CLIs (the COMPLETE list): lemon-gmail, lemon-calendar, lemon-drive, lemon-docs, lemon-sheets, lemon-slides, lemon-notion, lemon-slack, lemon-youtube, lemon-twitter, lemon-composio.",
356
356
  "ANYTHING not in this list → use `lemon-composio search`.",
@@ -367,17 +367,7 @@ export function buildAgentSystemPrompt(params) {
367
367
  "3. Do NOT disconnect without explicit user confirmation.",
368
368
  "",
369
369
  "### Task requests (do something WITH a service)",
370
- "When the user mentions ANY service not in the dedicated CLI list, follow this EXACT sequence. Do ALL searching SILENTLY — the user should never see your search process:",
371
- '1. Run `lemon-composio search "<what the user wants>"` — this is your FIRST action. No web search. No `which`. No `--help`. No questions.',
372
- "2. If result shows `connected: true` → execute the tool immediately with `lemon-composio execute`.",
373
- "3. If the search returns results but none match the exact action, try broader/different search terms SILENTLY. Do NOT tell the user you're retrying searches.",
374
- "4. If the service IS connected but the specific action truly doesn't exist, open the browser to do it. Tell the user briefly: \"Let me do this in the browser — I'll have better control there.\" Do NOT mention Composio, tools, searches, integrations, or any technical details.",
375
- '5. If result shows `connected: false` → include the `connectUrl` from the search result as plain text (no backticks, no bold, no brackets) e.g. lemon://connect?provider=jira. The app will automatically open the auth page. Say "Connecting [service name] for you: lemon://connect?provider=X" Then STOP and wait for them to complete the auth.',
376
- "6. Once the user confirms they connected → execute the tool.",
377
- "7. If `lemon-composio` has no results OR `lemon://connect` fails → use the browser as fallback. Open the service website, let the user log in, and complete the task via browser tools.",
378
- "",
379
- "### Already-connected services",
380
- "If `lemon-composio search` shows a service as `connected: true`, do NOT try to connect it again. Do NOT output lemon://connect for an already-connected service. Just use the available tools directly.",
370
+ "When the user mentions ANY service not in the dedicated CLI list, use `lemon-composio` see the 'Dynamic tool discovery' section below for the full flow. Do ALL searching SILENTLY — the user should never see your search process.",
381
371
  "",
382
372
  "### Account & Login Policy",
383
373
  "NEVER create accounts or sign up for services on behalf of the user. If a service requires login or account creation in the browser:",
@@ -403,7 +393,7 @@ export function buildAgentSystemPrompt(params) {
403
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.",
404
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.',
405
395
  "3. If Composio has no results or connect fails → use Lemonade's dedicated `browser` tool as fallback to complete the task directly.",
406
- "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.",
407
397
  "5. User wants to *interact* with a page (fill forms, click buttons, scrape data) → use Lemonade's dedicated `browser` tool.",
408
398
  "6. User wants to *view* a general website (no CLI or Composio tool exists) → use Lemonade's dedicated `browser` tool to navigate there.",
409
399
  "",
@@ -433,10 +423,10 @@ export function buildAgentSystemPrompt(params) {
433
423
  "",
434
424
  "The response includes `connectedProviders` (all services the user has connected) and `serviceConnected` (whether the searched service is connected). Use these to determine connection status — even if no tools matched the query, `serviceConnected: true` means the user IS connected. Do NOT try to re-connect.",
435
425
  "",
436
- "Then based on results:",
437
- '1. **Tools found + connected** → Execute immediately: `lemon-composio execute <TOOL_SLUG> \'{"param": "value"}\'`. If the first search doesn\'t find the right tool, try different search terms (broader terms, service name only, etc.) before giving up.',
438
- "2. **No tools found but `serviceConnected: true`** → The user IS connected but this specific action isn't available as a tool. Try a broader search first (e.g. just the service name). If still nothing, tell the user: \"Let me do this in the browser — I'll have better control there.\" Then use the browser. Do NOT try to connect again.",
439
- "3. **Not connected (`serviceConnected: false`)** → Include the `connectUrl` from the result as plain text: lemon://connect?provider=X. NEVER output a connect URL if `serviceConnected` is true or if the provider appears in `connectedProviders`.",
426
+ "Do ALL searching SILENTLY — never narrate retries or internal search process. Then based on results:",
427
+ "1. **Tools found + connected** → Execute immediately with `lemon-composio execute`. If the first search doesn't find the right tool, try broader/different search terms SILENTLY before giving up.",
428
+ "2. **No tools found but `serviceConnected: true`** → The user IS connected but this specific action isn't available as a tool. Try a broader search first (e.g. just the service name). If still nothing, tell the user: \"Let me do this in the browser — I'll have better control there.\" Then use the browser. Do NOT try to connect again. NEVER output a connect URL for an already-connected service.",
429
+ '3. **Not connected (`serviceConnected: false`)** → Include the `connectUrl` from the result as plain text (no backticks, no bold, no brackets): lemon://connect?provider=X. Say "Connecting [service name] for you: lemon://connect?provider=X" Then STOP and wait for them to complete the auth. Once the user confirms, execute the tool.',
440
430
  '4. **Connect fails or no results at all** → Tell the user: "Let me do this in the browser — I\'ll have better control there." Then use the browser directly.',
441
431
  "",
442
432
  "Example flows:",
@@ -446,6 +436,15 @@ export function buildAgentSystemPrompt(params) {
446
436
  "",
447
437
  "**Check what's connected:** `lemon-composio status`",
448
438
  "",
439
+ "### Multi-account support",
440
+ "Search results and status now include `connectedAccounts` — an array of all connected accounts with `provider`, `email`, and `accountId`.",
441
+ "Each tool in search results also has an `accounts` array showing which accounts can run that tool.",
442
+ "When the user has multiple accounts for the same provider (e.g. two Gmail accounts), pick the right one based on context:",
443
+ '- If the user says "work email" or "personal calendar", match by the account email.',
444
+ '- If unclear which account to use, ask once: "You have multiple Gmail accounts connected (work@company.com, personal@gmail.com). Which one should I use?"',
445
+ "- Pass the chosen account to execute: `lemon-composio execute TOOL_SLUG '{...}' --account=<accountId>`",
446
+ "- If only one account exists for the provider, you don't need `--account` — it will be used automatically.",
447
+ "",
449
448
  "### Browser tool (Lemonade's dedicated browser only)",
450
449
  "For general websites without a CLI tool, or as fallback when Composio fails, use Lemonade's dedicated `browser` tool (Playwright-managed).",
451
450
  '**Never open URLs in the user\'s personal browser.** Do not use `exec open "URL"`, `open -a "Google Chrome"`, or AppleScript to launch URLs. All web browsing goes through the `browser` tool with `profile="lemonade"`.',
@@ -490,7 +489,7 @@ export function buildAgentSystemPrompt(params) {
490
489
  '- "create a new doc" → `lemon-docs create "Untitled"` (CLI, not browser)',
491
490
  '- "start a Google Meet" → `browser` navigate to `https://meet.new`',
492
491
  '- "check my email" → `lemon-gmail list` (CLI, not browser)',
493
- '- "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)',
494
493
  "",
495
494
  "## IDE & Coding Agent Control",
496
495
  "Control AI coding agents via CLI — never type into GUI windows:",
@@ -694,7 +693,7 @@ export function buildAgentSystemPrompt(params) {
694
693
  }
695
694
  // Skip silent replies for subagent/none modes
696
695
  if (!isMinimal) {
697
- lines.push("## Silent Replies", `When you have nothing to say, respond with ONLY: ${SILENT_REPLY_TOKEN}`, "", "⚠️ Rules:", "- It must be your ENTIRE message — nothing else", `- Never append it to an actual response (never include "${SILENT_REPLY_TOKEN}" in real replies)`, "- Never wrap it in markdown or code blocks", "", `❌ Wrong: "Here's help... ${SILENT_REPLY_TOKEN}"`, `❌ Wrong: "${SILENT_REPLY_TOKEN}"`, `✅ Right: ${SILENT_REPLY_TOKEN}`, "");
696
+ lines.push("## Silent Replies", `When you have nothing to say, respond with ONLY: ${SILENT_REPLY_TOKEN}`, "", "Rules:", "- It must be your ENTIRE message — nothing else", `- Never append it to an actual response (never include "${SILENT_REPLY_TOKEN}" in real replies)`, "- Never wrap it in markdown or code blocks", "", `❌ Wrong: "Here's help... ${SILENT_REPLY_TOKEN}"`, `❌ Wrong: "${SILENT_REPLY_TOKEN}"`, `✅ Right: ${SILENT_REPLY_TOKEN}`, "");
698
697
  }
699
698
  // Skip heartbeats for subagent/none modes
700
699
  if (!isMinimal) {
@@ -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.5",
3
- "commit": "fe3f413801f7c8eacfec8225f6735cd626c0233e",
4
- "builtAt": "2026-02-24T09:45:57.128Z"
2
+ "version": "0.4.8",
3
+ "commit": "bd6ce676d45649b75d56ace5dd5ed43688021700",
4
+ "builtAt": "2026-02-24T17:07:07.985Z"
5
5
  }
@@ -1 +1 @@
1
- 132afc79b9ddf8d1cbc0b38cf08ab7215d4bded00d598b85a269a1a94575c3da
1
+ eeff6d1d4766fef20888820347f161ad21b9437d6735c618040c29b7af380047
@@ -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.5",
3
+ "version": "0.4.8",
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",
@@ -31,18 +31,9 @@ The recap covers:
31
31
 
32
32
  To create it, use the `cron` tool with `action=add`. Check `cron action=list` first to avoid duplicates.
33
33
 
34
- ## Resolving Recipients (CRITICAL — never fabricate emails)
34
+ ## Resolving Recipients
35
35
 
36
- When the user says "send to Masood" or "email John from my team" and does NOT provide an email address:
37
-
38
- 1. **Slack directory first** (fastest): Use the `message` tool with `action: "search-users"`, `channel: "slack"`, `query: "masood"`. This returns `name`, `handle`, and **`email`**. If found, use that email.
39
- 2. **Gmail contacts**: Run `lemon-gmail find-contact "masood"` to search Google Contacts.
40
- 3. **Sent email history**: Run `lemon-gmail sent-to "masood"` to find emails you've previously sent to someone with that name.
41
- 4. **Ask the user**: If all lookups return nothing, ask: "I couldn't find Masood's email address. Could you provide it?"
42
-
43
- **NEVER fabricate an email address** like `masood@example.com` or guess a domain. Always resolve from real data or ask.
44
-
45
- **ALWAYS confirm** before sending: "I found Masood Ali (masood.ali@company.com). Should I send the email to this address?"
36
+ **NEVER fabricate email addresses.** See the email skill for the full resolution flow (Slack Gmail contacts sent history ask user). Always confirm the resolved email before sending.
46
37
 
47
38
  ## Sending with Attachments
48
39
 
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  name: integrations
3
- description: Lemon integrations are configured at ~/.lemonade/skills/
3
+ description: Manage and connect Lemon integrations
4
4
  metadata:
5
5
  lemonade:
6
6
  emoji: "🔗"
7
- note: "Dynamic skills are auto-generated in ~/.lemonade/skills/"
7
+ note: "Dynamic skills are auto-generated when services are connected"
8
8
  ---
9
9
 
10
10
  # Lemon Integrations
@@ -25,11 +25,7 @@ when you connect services in Lemon Settings > Integrations.
25
25
 
26
26
  Provider IDs: gmail, slack, googlecalendar, googledrive, notion, youtube, jira, github, linear, trello, asana, etc.
27
27
 
28
- The generated skills are placed in `~/.lemonade/skills/` and include:
29
- - Full CLI paths for your machine
30
- - Proper authentication tokens
31
-
32
- If integrations aren't working, restart the Lemon app to regenerate them.
28
+ Integration skills are auto-generated when you connect services. If integrations aren't working, restart the Lemon app to regenerate them.
33
29
 
34
30
  ## Connection Flow (Priority Order)
35
31
 
@@ -59,6 +55,16 @@ Some services (Jira, Trello, Asana, Confluence, etc.) require knowing the user's
59
55
  - **ClickUp:** https://app.clickup.com
60
56
  - **Monday.com:** https://monday.com
61
57
 
58
+ ## Multi-Account Support
59
+
60
+ Users can connect multiple accounts for the same provider (e.g. two Gmail accounts, two Slack workspaces). When executing tools:
61
+
62
+ - **Search results** include a `connectedAccounts` array with each account's `provider`, `email`, and `accountId`. Each tool also lists which `accounts` can run it.
63
+ - **If only one account** exists for the provider, it's used automatically — no need to specify.
64
+ - **If multiple accounts** exist, pick the right one based on context (e.g. "send from my work email" → match by email). If unclear, ask the user once.
65
+ - **To target a specific account:** `lemon-composio execute TOOL_SLUG '{"params"}' --account=<accountId>`
66
+ - **Status** (`lemon-composio status`) also returns `connectedAccounts` so you can see all accounts at a glance.
67
+
62
68
  ## Browser Fallback
63
69
 
64
70
  When CLI tools or Composio can't do something, ALWAYS use the browser. NEVER give up or tell the user to do it themselves.
@@ -28,14 +28,15 @@ lemon-calendar list
28
28
 
29
29
  Zoom does not have a `lemon-*` CLI. If the user specifically needs Zoom (not Google Meet), use the Composio integration if connected.
30
30
 
31
- ### Check if Connected
31
+ ### Using Zoom via Composio
32
32
 
33
33
  ```bash
34
- curl -H "Authorization: Bearer $GATEWAY_TOKEN" \
35
- https://voice-wisal.voice-f05.workers.dev/api/lemonade/tools/status
34
+ lemon-composio search "create zoom meeting"
36
35
  ```
37
36
 
38
- Check `hasZoom` in response. If connected, use `ZOOM_CREATE_MEETING` Composio tool. If not, trigger OAuth: `lemon://connect?provider=zoom`
37
+ If tools are found and connected, execute immediately. If Zoom is not connected, output the connect URL as plain text:
38
+
39
+ lemon://connect?provider=zoom
39
40
 
40
41
  ## If NOT Connected
41
42
 
@@ -52,6 +53,6 @@ Include the connect URL as plain text in your reply. The app will automatically
52
53
  ## Remember
53
54
 
54
55
  - **`lemon-calendar` first** — For Google Calendar + Meet
55
- - **Never open browser** for calendar or meeting scheduling unless CLI fails
56
+ - **`lemon-composio`** For Zoom and other meeting services without a dedicated CLI
57
+ - **Browser as last resort** — Only if CLI and Composio both fail
56
58
  - **Always include timezone** — Essential for meetings
57
- - **Be honest** — Offer to connect if not set up
@@ -4,8 +4,9 @@ Post to LinkedIn, Twitter/X, and other social platforms.
4
4
 
5
5
  ## Priority Order
6
6
 
7
- 1. **`lemon-twitter`** CLI for Twitter/X — Always use this first
8
- 2. **Browser** Fallback only if CLI fails or service is not connected
7
+ 1. **`lemon-twitter`** CLI for Twitter/X
8
+ 2. **`lemon-composio`** for LinkedIn and other social platforms
9
+ 3. **Browser** — Fallback only if CLI/Composio fails or service is not connected
9
10
 
10
11
  ## Twitter/X
11
12
 
@@ -29,40 +30,26 @@ lemon-twitter search "query"
29
30
 
30
31
  ## LinkedIn
31
32
 
32
- LinkedIn does not currently have a `lemon-*` CLI. Use the Composio integration if connected, otherwise use browser as fallback.
33
+ LinkedIn does not have a dedicated `lemon-*` CLI. Use `lemon-composio` to search and execute LinkedIn tools.
33
34
 
34
- ### Check if Connected
35
+ ### Posting, checking activity, etc.
35
36
 
36
37
  ```bash
37
- curl -H "Authorization: Bearer $GATEWAY_TOKEN" \
38
- https://voice-wisal.voice-f05.workers.dev/api/lemonade/tools/status
38
+ lemon-composio search "create linkedin post"
39
+ lemon-composio search "linkedin activity"
39
40
  ```
40
41
 
41
- Check `hasLinkedIn` in response.
42
+ If tools are found and connected, execute immediately. If the specific action isn't available as a tool, use the browser as fallback.
42
43
 
43
- ### If Connected (hasLinkedIn: true)
44
+ ### If Not Connected
44
45
 
45
- ```bash
46
- curl -X POST \
47
- -H "Authorization: Bearer $GATEWAY_TOKEN" \
48
- -H "Content-Type: application/json" \
49
- -d '{
50
- "toolName": "LINKEDIN_CREATE_POST",
51
- "parameters": {
52
- "text": "Your post content here",
53
- "visibility": "PUBLIC"
54
- }
55
- }' \
56
- https://voice-wisal.voice-f05.workers.dev/api/lemonade/tools/execute
57
- ```
58
-
59
- ### If NOT Connected
46
+ Output the connect URL as plain text:
60
47
 
61
- Include the connect URL as plain text in your reply. The app will automatically open the auth page:
48
+ lemon://connect?provider=linkedin
62
49
 
63
- "LinkedIn isn't connected yet. Opening the connection page for you: lemon://connect?provider=linkedin"
50
+ ### If Connect Fails
64
51
 
65
- If connect fails, use the browser as fallback: navigate to `linkedin.com/feed`, click "Start a post"
52
+ Use the browser as fallback navigate to linkedin.com and complete the task directly.
66
53
 
67
54
  ## Content Guidelines
68
55
 
@@ -78,7 +65,7 @@ If connect fails, use the browser as fallback: navigate to `linkedin.com/feed`,
78
65
  - Hashtags and mentions common
79
66
  - Good for: quick updates, engagement
80
67
 
81
- ## ⚠️ ALWAYS Confirm Before Posting
68
+ ## ALWAYS Confirm Before Posting
82
69
 
83
70
  **Never post without user approval.**
84
71