@heylemon/lemonade 0.2.3 → 0.2.4
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/dist/build-info.json +3 -3
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/package.json +1 -1
- package/skills/youtube-watcher/SKILL.md +5 -0
- package/skills/brave-search/SKILL.md +0 -57
- package/skills/brave-search/content.js +0 -86
- package/skills/brave-search/package.json +0 -14
- package/skills/brave-search/search.js +0 -179
- package/skills/caldav-calendar/SKILL.md +0 -104
- package/skills/tavily-search/SKILL.md +0 -38
- package/skills/tavily-search/scripts/extract.mjs +0 -59
- package/skills/tavily-search/scripts/search.mjs +0 -101
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
aad422f21104b00c3575f122b2f36572b3d50edc6e9dc94b5c2fb91fd996d4b9
|
package/package.json
CHANGED
|
@@ -39,8 +39,13 @@ python3 {baseDir}/scripts/get_transcript.py "https://www.youtube.com/watch?v=VID
|
|
|
39
39
|
1. Get the transcript.
|
|
40
40
|
2. Search the text for keywords or answer the user's question based on the content.
|
|
41
41
|
|
|
42
|
+
## Important
|
|
43
|
+
|
|
44
|
+
**If `lemon-youtube` is available**, prefer using `lemon-youtube transcript <url>` instead — it uses the authenticated YouTube API and is more reliable. Use this skill as a fallback when the YouTube integration is not connected or `lemon-youtube` is unavailable.
|
|
45
|
+
|
|
42
46
|
## Notes
|
|
43
47
|
|
|
44
48
|
- Requires `yt-dlp` to be installed and available in the PATH.
|
|
45
49
|
- Works with videos that have closed captions (CC) or auto-generated subtitles.
|
|
46
50
|
- If a video has no subtitles, the script will fail with an error message.
|
|
51
|
+
- No API key required — uses yt-dlp to fetch publicly available subtitles.
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: brave-search
|
|
3
|
-
description: Web search and content extraction via Brave Search API. Use for searching documentation, facts, or any web content. Lightweight, no browser required.
|
|
4
|
-
metadata: {"lemonade":{"emoji":"🦁","requires":{"bins":["node"]}}}
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# Brave Search
|
|
8
|
-
|
|
9
|
-
Headless web search and content extraction using Brave Search. No browser required.
|
|
10
|
-
|
|
11
|
-
## Setup
|
|
12
|
-
|
|
13
|
-
Run once before first use:
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
cd {baseDir}
|
|
17
|
-
npm ci
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
## Search
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
node {baseDir}/search.js "query" # Basic search (5 results)
|
|
24
|
-
node {baseDir}/search.js "query" -n 10 # More results
|
|
25
|
-
node {baseDir}/search.js "query" --content # Include page content as markdown
|
|
26
|
-
node {baseDir}/search.js "query" -n 3 --content # Combined
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## Extract Page Content
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
node {baseDir}/content.js https://example.com/article
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Fetches a URL and extracts readable content as markdown.
|
|
36
|
-
|
|
37
|
-
## Output Format
|
|
38
|
-
|
|
39
|
-
```
|
|
40
|
-
--- Result 1 ---
|
|
41
|
-
Title: Page Title
|
|
42
|
-
Link: https://example.com/page
|
|
43
|
-
Snippet: Description from search results
|
|
44
|
-
Content: (if --content flag used)
|
|
45
|
-
Markdown content extracted from the page...
|
|
46
|
-
|
|
47
|
-
--- Result 2 ---
|
|
48
|
-
...
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
## When to Use
|
|
52
|
-
|
|
53
|
-
- Searching for documentation or API references
|
|
54
|
-
- Looking up facts or current information
|
|
55
|
-
- Fetching content from specific URLs
|
|
56
|
-
- Any task requiring web search without interactive browsing
|
|
57
|
-
```
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { Readability } from "@mozilla/readability";
|
|
4
|
-
import { JSDOM } from "jsdom";
|
|
5
|
-
import TurndownService from "turndown";
|
|
6
|
-
import { gfm } from "turndown-plugin-gfm";
|
|
7
|
-
|
|
8
|
-
const url = process.argv[2];
|
|
9
|
-
|
|
10
|
-
if (!url) {
|
|
11
|
-
console.log("Usage: content.js <url>");
|
|
12
|
-
console.log("\nExtracts readable content from a webpage as markdown.");
|
|
13
|
-
console.log("\nExamples:");
|
|
14
|
-
console.log(" content.js https://example.com/article");
|
|
15
|
-
console.log(" content.js https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html");
|
|
16
|
-
process.exit(1);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function htmlToMarkdown(html) {
|
|
20
|
-
const turndown = new TurndownService({ headingStyle: "atx", codeBlockStyle: "fenced" });
|
|
21
|
-
turndown.use(gfm);
|
|
22
|
-
turndown.addRule("removeEmptyLinks", {
|
|
23
|
-
filter: (node) => node.nodeName === "A" && !node.textContent?.trim(),
|
|
24
|
-
replacement: () => "",
|
|
25
|
-
});
|
|
26
|
-
return turndown
|
|
27
|
-
.turndown(html)
|
|
28
|
-
.replace(/\[\\?\[\s*\\?\]\]\([^)]*\)/g, "")
|
|
29
|
-
.replace(/ +/g, " ")
|
|
30
|
-
.replace(/\s+,/g, ",")
|
|
31
|
-
.replace(/\s+\./g, ".")
|
|
32
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
33
|
-
.trim();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
try {
|
|
37
|
-
const response = await fetch(url, {
|
|
38
|
-
headers: {
|
|
39
|
-
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
40
|
-
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
41
|
-
"Accept-Language": "en-US,en;q=0.9",
|
|
42
|
-
},
|
|
43
|
-
signal: AbortSignal.timeout(15000),
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
if (!response.ok) {
|
|
47
|
-
console.error(`HTTP ${response.status}: ${response.statusText}`);
|
|
48
|
-
process.exit(1);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const html = await response.text();
|
|
52
|
-
const dom = new JSDOM(html, { url });
|
|
53
|
-
const reader = new Readability(dom.window.document);
|
|
54
|
-
const article = reader.parse();
|
|
55
|
-
|
|
56
|
-
if (article && article.content) {
|
|
57
|
-
if (article.title) {
|
|
58
|
-
console.log(`# ${article.title}\n`);
|
|
59
|
-
}
|
|
60
|
-
console.log(htmlToMarkdown(article.content));
|
|
61
|
-
process.exit(0);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Fallback: try to extract main content
|
|
65
|
-
const fallbackDoc = new JSDOM(html, { url });
|
|
66
|
-
const body = fallbackDoc.window.document;
|
|
67
|
-
body.querySelectorAll("script, style, noscript, nav, header, footer, aside").forEach(el => el.remove());
|
|
68
|
-
|
|
69
|
-
const title = body.querySelector("title")?.textContent?.trim();
|
|
70
|
-
const main = body.querySelector("main, article, [role='main'], .content, #content") || body.body;
|
|
71
|
-
|
|
72
|
-
if (title) {
|
|
73
|
-
console.log(`# ${title}\n`);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const text = main?.innerHTML || "";
|
|
77
|
-
if (text.trim().length > 100) {
|
|
78
|
-
console.log(htmlToMarkdown(text));
|
|
79
|
-
} else {
|
|
80
|
-
console.error("Could not extract readable content from this page.");
|
|
81
|
-
process.exit(1);
|
|
82
|
-
}
|
|
83
|
-
} catch (e) {
|
|
84
|
-
console.error(`Error: ${e.message}`);
|
|
85
|
-
process.exit(1);
|
|
86
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "brave-search",
|
|
3
|
-
"version": "1.0.0",
|
|
4
|
-
"type": "module",
|
|
5
|
-
"description": "Headless web search via Brave Search - no browser required",
|
|
6
|
-
"author": "Mario Zechner",
|
|
7
|
-
"license": "MIT",
|
|
8
|
-
"dependencies": {
|
|
9
|
-
"@mozilla/readability": "^0.6.0",
|
|
10
|
-
"jsdom": "^27.0.1",
|
|
11
|
-
"turndown": "^7.2.2",
|
|
12
|
-
"turndown-plugin-gfm": "^1.0.2"
|
|
13
|
-
}
|
|
14
|
-
}
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { Readability } from "@mozilla/readability";
|
|
4
|
-
import { JSDOM } from "jsdom";
|
|
5
|
-
import TurndownService from "turndown";
|
|
6
|
-
import { gfm } from "turndown-plugin-gfm";
|
|
7
|
-
|
|
8
|
-
const args = process.argv.slice(2);
|
|
9
|
-
|
|
10
|
-
const contentIndex = args.indexOf("--content");
|
|
11
|
-
const fetchContent = contentIndex !== -1;
|
|
12
|
-
if (fetchContent) args.splice(contentIndex, 1);
|
|
13
|
-
|
|
14
|
-
let numResults = 5;
|
|
15
|
-
const nIndex = args.indexOf("-n");
|
|
16
|
-
if (nIndex !== -1 && args[nIndex + 1]) {
|
|
17
|
-
numResults = parseInt(args[nIndex + 1], 10);
|
|
18
|
-
args.splice(nIndex, 2);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const query = args.join(" ");
|
|
22
|
-
|
|
23
|
-
if (!query) {
|
|
24
|
-
console.log("Usage: search.js [-n] [--content]");
|
|
25
|
-
console.log("\nOptions:");
|
|
26
|
-
console.log(" -n Number of results (default: 5)");
|
|
27
|
-
console.log(" --content Fetch readable content as markdown");
|
|
28
|
-
console.log("\nExamples:");
|
|
29
|
-
console.log(' search.js "javascript async await"');
|
|
30
|
-
console.log(' search.js "rust programming" -n 10');
|
|
31
|
-
console.log(' search.js "climate change" --content');
|
|
32
|
-
process.exit(1);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function fetchBraveResults(query, numResults) {
|
|
36
|
-
const url = `https://search.brave.com/search?q=${encodeURIComponent(query)}`;
|
|
37
|
-
|
|
38
|
-
const response = await fetch(url, {
|
|
39
|
-
headers: {
|
|
40
|
-
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36",
|
|
41
|
-
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
|
42
|
-
"Accept-Language": "en-US,en;q=0.9",
|
|
43
|
-
"sec-ch-ua": '"Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"',
|
|
44
|
-
"sec-ch-ua-mobile": "?0",
|
|
45
|
-
"sec-ch-ua-platform": '"macOS"',
|
|
46
|
-
"sec-fetch-dest": "document",
|
|
47
|
-
"sec-fetch-mode": "navigate",
|
|
48
|
-
"sec-fetch-site": "none",
|
|
49
|
-
"sec-fetch-user": "?1",
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
if (!response.ok) {
|
|
54
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const html = await response.text();
|
|
58
|
-
const dom = new JSDOM(html);
|
|
59
|
-
const doc = dom.window.document;
|
|
60
|
-
|
|
61
|
-
const results = [];
|
|
62
|
-
|
|
63
|
-
// Find all search result snippets with data-type="web"
|
|
64
|
-
const snippets = doc.querySelectorAll('div.snippet[data-type="web"]');
|
|
65
|
-
|
|
66
|
-
for (const snippet of snippets) {
|
|
67
|
-
if (results.length >= numResults) break;
|
|
68
|
-
|
|
69
|
-
// Get the main link and title
|
|
70
|
-
const titleLink = snippet.querySelector('a.svelte-14r20fy');
|
|
71
|
-
if (!titleLink) continue;
|
|
72
|
-
|
|
73
|
-
const link = titleLink.getAttribute('href');
|
|
74
|
-
if (!link || link.includes('brave.com')) continue;
|
|
75
|
-
|
|
76
|
-
const titleEl = titleLink.querySelector('.title');
|
|
77
|
-
const title = titleEl?.textContent?.trim() || titleLink.textContent?.trim() || '';
|
|
78
|
-
|
|
79
|
-
// Get the snippet/description
|
|
80
|
-
const descEl = snippet.querySelector('.generic-snippet .content');
|
|
81
|
-
let snippetText = descEl?.textContent?.trim() || '';
|
|
82
|
-
// Remove date prefix if present
|
|
83
|
-
snippetText = snippetText.replace(/^[A-Z][a-z]+ \d+, \d{4} -\s*/, '');
|
|
84
|
-
|
|
85
|
-
if (title && link) {
|
|
86
|
-
results.push({ title, link, snippet: snippetText });
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return results;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function htmlToMarkdown(html) {
|
|
94
|
-
const turndown = new TurndownService({ headingStyle: "atx", codeBlockStyle: "fenced" });
|
|
95
|
-
turndown.use(gfm);
|
|
96
|
-
turndown.addRule("removeEmptyLinks", {
|
|
97
|
-
filter: (node) => node.nodeName === "A" && !node.textContent?.trim(),
|
|
98
|
-
replacement: () => "",
|
|
99
|
-
});
|
|
100
|
-
return turndown
|
|
101
|
-
.turndown(html)
|
|
102
|
-
.replace(/\[\\?\[\s*\\?\]\]\([^)]*\)/g, "")
|
|
103
|
-
.replace(/ +/g, " ")
|
|
104
|
-
.replace(/\s+,/g, ",")
|
|
105
|
-
.replace(/\s+\./g, ".")
|
|
106
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
107
|
-
.trim();
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
async function fetchPageContent(url) {
|
|
111
|
-
try {
|
|
112
|
-
const response = await fetch(url, {
|
|
113
|
-
headers: {
|
|
114
|
-
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
|
|
115
|
-
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
116
|
-
},
|
|
117
|
-
signal: AbortSignal.timeout(10000),
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
if (!response.ok) {
|
|
121
|
-
return `(HTTP ${response.status})`;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const html = await response.text();
|
|
125
|
-
const dom = new JSDOM(html, { url });
|
|
126
|
-
const reader = new Readability(dom.window.document);
|
|
127
|
-
const article = reader.parse();
|
|
128
|
-
|
|
129
|
-
if (article && article.content) {
|
|
130
|
-
return htmlToMarkdown(article.content).substring(0, 5000);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Fallback: try to get main content
|
|
134
|
-
const fallbackDoc = new JSDOM(html, { url });
|
|
135
|
-
const body = fallbackDoc.window.document;
|
|
136
|
-
body.querySelectorAll("script, style, noscript, nav, header, footer, aside").forEach(el => el.remove());
|
|
137
|
-
const main = body.querySelector("main, article, [role='main'], .content, #content") || body.body;
|
|
138
|
-
const text = main?.textContent || "";
|
|
139
|
-
|
|
140
|
-
if (text.trim().length > 100) {
|
|
141
|
-
return text.trim().substring(0, 5000);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return "(Could not extract content)";
|
|
145
|
-
} catch (e) {
|
|
146
|
-
return `(Error: ${e.message})`;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Main
|
|
151
|
-
try {
|
|
152
|
-
const results = await fetchBraveResults(query, numResults);
|
|
153
|
-
|
|
154
|
-
if (results.length === 0) {
|
|
155
|
-
console.error("No results found.");
|
|
156
|
-
process.exit(0);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (fetchContent) {
|
|
160
|
-
for (const result of results) {
|
|
161
|
-
result.content = await fetchPageContent(result.link);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
for (let i = 0; i < results.length; i++) {
|
|
166
|
-
const r = results[i];
|
|
167
|
-
console.log(`--- Result ${i + 1} ---`);
|
|
168
|
-
console.log(`Title: ${r.title}`);
|
|
169
|
-
console.log(`Link: ${r.link}`);
|
|
170
|
-
console.log(`Snippet: ${r.snippet}`);
|
|
171
|
-
if (r.content) {
|
|
172
|
-
console.log(`Content:\n${r.content}`);
|
|
173
|
-
}
|
|
174
|
-
console.log("");
|
|
175
|
-
}
|
|
176
|
-
} catch (e) {
|
|
177
|
-
console.error(`Error: ${e.message}`);
|
|
178
|
-
process.exit(1);
|
|
179
|
-
}
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: caldav-calendar
|
|
3
|
-
description: Sync and query CalDAV calendars (iCloud, Google, Fastmail, Nextcloud, etc.) using vdirsyncer + khal. Works on Linux.
|
|
4
|
-
metadata: {"lemonade":{"emoji":"📅","os":["linux"],"requires":{"bins":["vdirsyncer","khal"]},"install":[{"id":"apt","kind":"apt","packages":["vdirsyncer","khal"],"bins":["vdirsyncer","khal"],"label":"Install vdirsyncer + khal via apt"}]}}
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# CalDAV Calendar (vdirsyncer + khal)
|
|
8
|
-
|
|
9
|
-
**vdirsyncer** syncs CalDAV calendars to local `.ics` files. **khal** reads and writes them.
|
|
10
|
-
|
|
11
|
-
## Sync First
|
|
12
|
-
|
|
13
|
-
Always sync before querying or after making changes:
|
|
14
|
-
```bash
|
|
15
|
-
vdirsyncer sync
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
## View Events
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
khal list # Today
|
|
22
|
-
khal list today 7d # Next 7 days
|
|
23
|
-
khal list tomorrow # Tomorrow
|
|
24
|
-
khal list 2026-01-15 2026-01-20 # Date range
|
|
25
|
-
khal list -a Work today # Specific calendar
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## Search
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
khal search "meeting"
|
|
32
|
-
khal search "dentist" --format "{start-date} {title}"
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
## Create Events
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
khal new 2026-01-15 10:00 11:00 "Meeting title"
|
|
39
|
-
khal new 2026-01-15 "All day event"
|
|
40
|
-
khal new tomorrow 14:00 15:30 "Call" -a Work
|
|
41
|
-
khal new 2026-01-15 10:00 11:00 "With notes" :: Description goes here
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
After creating, sync to push changes:
|
|
45
|
-
```bash
|
|
46
|
-
vdirsyncer sync
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## Initial Setup
|
|
50
|
-
|
|
51
|
-
### 1. Configure vdirsyncer (`~/.config/vdirsyncer/config`)
|
|
52
|
-
|
|
53
|
-
Example for iCloud:
|
|
54
|
-
```ini
|
|
55
|
-
[general]
|
|
56
|
-
status_path = "~/.local/share/vdirsyncer/status/"
|
|
57
|
-
|
|
58
|
-
[pair icloud_calendar]
|
|
59
|
-
a = "icloud_remote"
|
|
60
|
-
b = "icloud_local"
|
|
61
|
-
collections = ["from a", "from b"]
|
|
62
|
-
conflict_resolution = "a wins"
|
|
63
|
-
|
|
64
|
-
[storage icloud_remote]
|
|
65
|
-
type = "caldav"
|
|
66
|
-
url = "https://caldav.icloud.com/"
|
|
67
|
-
username = "you@icloud.com"
|
|
68
|
-
password.fetch = ["command", "cat", "~/.config/vdirsyncer/icloud_password"]
|
|
69
|
-
|
|
70
|
-
[storage icloud_local]
|
|
71
|
-
type = "filesystem"
|
|
72
|
-
path = "~/.local/share/vdirsyncer/calendars/"
|
|
73
|
-
fileext = ".ics"
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
Provider URLs:
|
|
77
|
-
- iCloud: `https://caldav.icloud.com/`
|
|
78
|
-
- Google: Use `google_calendar` storage type
|
|
79
|
-
- Fastmail: `https://caldav.fastmail.com/dav/calendars/user/EMAIL/`
|
|
80
|
-
- Nextcloud: `https://YOUR.CLOUD/remote.php/dav/calendars/USERNAME/`
|
|
81
|
-
|
|
82
|
-
### 2. Configure khal (`~/.config/khal/config`)
|
|
83
|
-
|
|
84
|
-
```ini
|
|
85
|
-
[calendars]
|
|
86
|
-
[[my_calendars]]
|
|
87
|
-
path = ~/.local/share/vdirsyncer/calendars/*
|
|
88
|
-
type = discover
|
|
89
|
-
|
|
90
|
-
[default]
|
|
91
|
-
default_calendar = Home
|
|
92
|
-
highlight_event_days = True
|
|
93
|
-
|
|
94
|
-
[locale]
|
|
95
|
-
timeformat = %H:%M
|
|
96
|
-
dateformat = %Y-%m-%d
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
### 3. Discover and sync
|
|
100
|
-
|
|
101
|
-
```bash
|
|
102
|
-
vdirsyncer discover
|
|
103
|
-
vdirsyncer sync
|
|
104
|
-
```
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: tavily
|
|
3
|
-
description: AI-optimized web search via Tavily API. Returns concise, relevant results for AI agents.
|
|
4
|
-
homepage: https://tavily.com
|
|
5
|
-
metadata: {"lemonade":{"emoji":"🔍","requires":{"bins":["node"],"env":["TAVILY_API_KEY"]},"primaryEnv":"TAVILY_API_KEY"}}
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# Tavily Search
|
|
9
|
-
|
|
10
|
-
AI-optimized web search using Tavily API. Designed for AI agents - returns clean, relevant content.
|
|
11
|
-
|
|
12
|
-
## Search
|
|
13
|
-
|
|
14
|
-
```bash
|
|
15
|
-
node {baseDir}/scripts/search.mjs "query"
|
|
16
|
-
node {baseDir}/scripts/search.mjs "query" -n 10
|
|
17
|
-
node {baseDir}/scripts/search.mjs "query" --deep
|
|
18
|
-
node {baseDir}/scripts/search.mjs "query" --topic news
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## Options
|
|
22
|
-
|
|
23
|
-
- `-n <count>`: Number of results (default: 5, max: 20)
|
|
24
|
-
- `--deep`: Use advanced search for deeper research (slower, more comprehensive)
|
|
25
|
-
- `--topic <type>`: Search topic - `general` (default) or `news`
|
|
26
|
-
- `--days <n>`: For news topic, limit to last n days
|
|
27
|
-
|
|
28
|
-
## Extract content from URL
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
node {baseDir}/scripts/extract.mjs "https://example.com/article"
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
Notes:
|
|
35
|
-
- Needs `TAVILY_API_KEY` from https://tavily.com
|
|
36
|
-
- Tavily is optimized for AI - returns clean, relevant snippets
|
|
37
|
-
- Use `--deep` for complex research questions
|
|
38
|
-
- Use `--topic news` for current events
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
function usage() {
|
|
4
|
-
console.error(`Usage: extract.mjs "url1" ["url2" ...]`);
|
|
5
|
-
process.exit(2);
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const args = process.argv.slice(2);
|
|
9
|
-
if (args.length === 0 || args[0] === "-h" || args[0] === "--help") usage();
|
|
10
|
-
|
|
11
|
-
const urls = args.filter(a => !a.startsWith("-"));
|
|
12
|
-
|
|
13
|
-
if (urls.length === 0) {
|
|
14
|
-
console.error("No URLs provided");
|
|
15
|
-
usage();
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const apiKey = (process.env.TAVILY_API_KEY ?? "").trim();
|
|
19
|
-
if (!apiKey) {
|
|
20
|
-
console.error("Missing TAVILY_API_KEY");
|
|
21
|
-
process.exit(1);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const resp = await fetch("https://api.tavily.com/extract", {
|
|
25
|
-
method: "POST",
|
|
26
|
-
headers: {
|
|
27
|
-
"Content-Type": "application/json",
|
|
28
|
-
},
|
|
29
|
-
body: JSON.stringify({
|
|
30
|
-
api_key: apiKey,
|
|
31
|
-
urls: urls,
|
|
32
|
-
}),
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
if (!resp.ok) {
|
|
36
|
-
const text = await resp.text().catch(() => "");
|
|
37
|
-
throw new Error(`Tavily Extract failed (${resp.status}): ${text}`);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const data = await resp.json();
|
|
41
|
-
|
|
42
|
-
const results = data.results ?? [];
|
|
43
|
-
const failed = data.failed_results ?? [];
|
|
44
|
-
|
|
45
|
-
for (const r of results) {
|
|
46
|
-
const url = String(r?.url ?? "").trim();
|
|
47
|
-
const content = String(r?.raw_content ?? "").trim();
|
|
48
|
-
|
|
49
|
-
console.log(`# ${url}\n`);
|
|
50
|
-
console.log(content || "(no content extracted)");
|
|
51
|
-
console.log("\n---\n");
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (failed.length > 0) {
|
|
55
|
-
console.log("## Failed URLs\n");
|
|
56
|
-
for (const f of failed) {
|
|
57
|
-
console.log(`- ${f.url}: ${f.error}`);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
function usage() {
|
|
4
|
-
console.error(`Usage: search.mjs "query" [-n 5] [--deep] [--topic general|news] [--days 7]`);
|
|
5
|
-
process.exit(2);
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const args = process.argv.slice(2);
|
|
9
|
-
if (args.length === 0 || args[0] === "-h" || args[0] === "--help") usage();
|
|
10
|
-
|
|
11
|
-
const query = args[0];
|
|
12
|
-
let n = 5;
|
|
13
|
-
let searchDepth = "basic";
|
|
14
|
-
let topic = "general";
|
|
15
|
-
let days = null;
|
|
16
|
-
|
|
17
|
-
for (let i = 1; i < args.length; i++) {
|
|
18
|
-
const a = args[i];
|
|
19
|
-
if (a === "-n") {
|
|
20
|
-
n = Number.parseInt(args[i + 1] ?? "5", 10);
|
|
21
|
-
i++;
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
if (a === "--deep") {
|
|
25
|
-
searchDepth = "advanced";
|
|
26
|
-
continue;
|
|
27
|
-
}
|
|
28
|
-
if (a === "--topic") {
|
|
29
|
-
topic = args[i + 1] ?? "general";
|
|
30
|
-
i++;
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
|
-
if (a === "--days") {
|
|
34
|
-
days = Number.parseInt(args[i + 1] ?? "7", 10);
|
|
35
|
-
i++;
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
console.error(`Unknown arg: ${a}`);
|
|
39
|
-
usage();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const apiKey = (process.env.TAVILY_API_KEY ?? "").trim();
|
|
43
|
-
if (!apiKey) {
|
|
44
|
-
console.error("Missing TAVILY_API_KEY");
|
|
45
|
-
process.exit(1);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const body = {
|
|
49
|
-
api_key: apiKey,
|
|
50
|
-
query: query,
|
|
51
|
-
search_depth: searchDepth,
|
|
52
|
-
topic: topic,
|
|
53
|
-
max_results: Math.max(1, Math.min(n, 20)),
|
|
54
|
-
include_answer: true,
|
|
55
|
-
include_raw_content: false,
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
if (topic === "news" && days) {
|
|
59
|
-
body.days = days;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const resp = await fetch("https://api.tavily.com/search", {
|
|
63
|
-
method: "POST",
|
|
64
|
-
headers: {
|
|
65
|
-
"Content-Type": "application/json",
|
|
66
|
-
},
|
|
67
|
-
body: JSON.stringify(body),
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
if (!resp.ok) {
|
|
71
|
-
const text = await resp.text().catch(() => "");
|
|
72
|
-
throw new Error(`Tavily Search failed (${resp.status}): ${text}`);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const data = await resp.json();
|
|
76
|
-
|
|
77
|
-
// Print AI-generated answer if available
|
|
78
|
-
if (data.answer) {
|
|
79
|
-
console.log("## Answer\n");
|
|
80
|
-
console.log(data.answer);
|
|
81
|
-
console.log("\n---\n");
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Print results
|
|
85
|
-
const results = (data.results ?? []).slice(0, n);
|
|
86
|
-
console.log("## Sources\n");
|
|
87
|
-
|
|
88
|
-
for (const r of results) {
|
|
89
|
-
const title = String(r?.title ?? "").trim();
|
|
90
|
-
const url = String(r?.url ?? "").trim();
|
|
91
|
-
const content = String(r?.content ?? "").trim();
|
|
92
|
-
const score = r?.score ? ` (relevance: ${(r.score * 100).toFixed(0)}%)` : "";
|
|
93
|
-
|
|
94
|
-
if (!title || !url) continue;
|
|
95
|
-
console.log(`- **${title}**${score}`);
|
|
96
|
-
console.log(` ${url}`);
|
|
97
|
-
if (content) {
|
|
98
|
-
console.log(` ${content.slice(0, 300)}${content.length > 300 ? "..." : ""}`);
|
|
99
|
-
}
|
|
100
|
-
console.log();
|
|
101
|
-
}
|