@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.
- package/bin/lemon-composio +26 -9
- package/dist/agents/system-prompt.js +24 -25
- package/dist/agents/tools/web-search.js +45 -11
- package/dist/build-info.json +3 -3
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/dist/config/schema.js +4 -4
- package/dist/config/zod-schema.agent-runtime.js +3 -1
- package/package.json +2 -1
- package/skills/gmail/SKILL.md +2 -11
- package/skills/integrations/SKILL.md +13 -7
- package/skills/meetings/SKILL.md +7 -6
- package/skills/social-media/SKILL.md +14 -27
package/bin/lemon-composio
CHANGED
|
@@ -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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
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
|
-
"
|
|
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 (
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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,
|
|
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` (
|
|
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
|
-
|
|
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
|
-
|
|
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` (
|
|
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}`, "", "
|
|
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
|
-
|
|
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
|
+
eeff6d1d4766fef20888820347f161ad21b9437d6735c618040c29b7af380047
|
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.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",
|
package/skills/gmail/SKILL.md
CHANGED
|
@@ -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
|
|
34
|
+
## Resolving Recipients
|
|
35
35
|
|
|
36
|
-
|
|
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:
|
|
3
|
+
description: Manage and connect Lemon integrations
|
|
4
4
|
metadata:
|
|
5
5
|
lemonade:
|
|
6
6
|
emoji: "🔗"
|
|
7
|
-
note: "Dynamic skills are auto-generated
|
|
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
|
-
|
|
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.
|
package/skills/meetings/SKILL.md
CHANGED
|
@@ -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
|
-
###
|
|
31
|
+
### Using Zoom via Composio
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
|
-
|
|
35
|
-
https://voice-wisal.voice-f05.workers.dev/api/lemonade/tools/status
|
|
34
|
+
lemon-composio search "create zoom meeting"
|
|
36
35
|
```
|
|
37
36
|
|
|
38
|
-
|
|
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
|
-
-
|
|
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
|
|
8
|
-
2.
|
|
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
|
|
33
|
+
LinkedIn does not have a dedicated `lemon-*` CLI. Use `lemon-composio` to search and execute LinkedIn tools.
|
|
33
34
|
|
|
34
|
-
###
|
|
35
|
+
### Posting, checking activity, etc.
|
|
35
36
|
|
|
36
37
|
```bash
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
lemon-composio search "create linkedin post"
|
|
39
|
+
lemon-composio search "linkedin activity"
|
|
39
40
|
```
|
|
40
41
|
|
|
41
|
-
|
|
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
|
|
44
|
+
### If Not Connected
|
|
44
45
|
|
|
45
|
-
|
|
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
|
-
|
|
48
|
+
lemon://connect?provider=linkedin
|
|
62
49
|
|
|
63
|
-
|
|
50
|
+
### If Connect Fails
|
|
64
51
|
|
|
65
|
-
|
|
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
|
-
##
|
|
68
|
+
## ALWAYS Confirm Before Posting
|
|
82
69
|
|
|
83
70
|
**Never post without user approval.**
|
|
84
71
|
|