@marcfargas/skills 0.2.0 → 0.3.0
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/LICENSE +1 -1
- package/LICENSE-CC0 +118 -0
- package/README.md +43 -13
- package/package.json +11 -3
- package/process/pm2/SKILL.md +240 -0
- package/search/web-search/SKILL.md +107 -0
- package/search/web-search/content.js +86 -0
- package/search/web-search/package-lock.json +617 -0
- package/search/web-search/package.json +13 -0
- package/search/web-search/search.js +215 -0
- package/terminal/vhs/SKILL.md +89 -1
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from "child_process";
|
|
4
|
+
import { readFileSync, unlinkSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { tmpdir } from "os";
|
|
7
|
+
import { randomBytes } from "crypto";
|
|
8
|
+
import { Readability } from "@mozilla/readability";
|
|
9
|
+
import { JSDOM } from "jsdom";
|
|
10
|
+
import TurndownService from "turndown";
|
|
11
|
+
import { gfm } from "turndown-plugin-gfm";
|
|
12
|
+
|
|
13
|
+
const args = process.argv.slice(2);
|
|
14
|
+
|
|
15
|
+
// Parse --content flag
|
|
16
|
+
const contentIndex = args.indexOf("--content");
|
|
17
|
+
const fetchContent = contentIndex !== -1;
|
|
18
|
+
if (fetchContent) args.splice(contentIndex, 1);
|
|
19
|
+
|
|
20
|
+
// Parse --news flag
|
|
21
|
+
const newsIndex = args.indexOf("--news");
|
|
22
|
+
const isNews = newsIndex !== -1;
|
|
23
|
+
if (isNews) args.splice(newsIndex, 1);
|
|
24
|
+
|
|
25
|
+
// Parse -n <num>
|
|
26
|
+
let numResults = 5;
|
|
27
|
+
const nIndex = args.indexOf("-n");
|
|
28
|
+
if (nIndex !== -1 && args[nIndex + 1]) {
|
|
29
|
+
numResults = parseInt(args[nIndex + 1], 10);
|
|
30
|
+
args.splice(nIndex, 2);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Parse -r <region>
|
|
34
|
+
let region = null;
|
|
35
|
+
const rIndex = args.indexOf("-r");
|
|
36
|
+
if (rIndex !== -1 && args[rIndex + 1]) {
|
|
37
|
+
region = args[rIndex + 1];
|
|
38
|
+
args.splice(rIndex, 2);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Parse -t <timelimit>
|
|
42
|
+
let timelimit = null;
|
|
43
|
+
const tIndex = args.indexOf("-t");
|
|
44
|
+
if (tIndex !== -1 && args[tIndex + 1]) {
|
|
45
|
+
timelimit = args[tIndex + 1];
|
|
46
|
+
args.splice(tIndex, 2);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Parse -b <backend>
|
|
50
|
+
let backend = null;
|
|
51
|
+
const bIndex = args.indexOf("-b");
|
|
52
|
+
if (bIndex !== -1 && args[bIndex + 1]) {
|
|
53
|
+
backend = args[bIndex + 1];
|
|
54
|
+
args.splice(bIndex, 2);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const query = args.join(" ");
|
|
58
|
+
|
|
59
|
+
if (!query) {
|
|
60
|
+
console.log("Usage: search.js [--news] <query> [-n <num>] [--content] [-r <region>] [-t <timelimit>] [-b <backend>]");
|
|
61
|
+
console.log("\nOptions:");
|
|
62
|
+
console.log(" --news News search instead of web search");
|
|
63
|
+
console.log(" -n <num> Number of results (default: 5)");
|
|
64
|
+
console.log(" --content Fetch readable content as markdown");
|
|
65
|
+
console.log(" -r <region> Region: us-en, es-es, de-de, fr-fr, etc.");
|
|
66
|
+
console.log(" -t <timelimit> Time filter: d (day), w (week), m (month), y (year)");
|
|
67
|
+
console.log(" -b <backend> Backend: auto, all, bing, brave, duckduckgo, google, etc.");
|
|
68
|
+
console.log("\nExamples:");
|
|
69
|
+
console.log(' search.js "javascript async await"');
|
|
70
|
+
console.log(' search.js "rust programming" -n 10');
|
|
71
|
+
console.log(' search.js "climate change" --content');
|
|
72
|
+
console.log(' search.js --news "AI agents" -t w');
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function htmlToMarkdown(html) {
|
|
77
|
+
const turndown = new TurndownService({ headingStyle: "atx", codeBlockStyle: "fenced" });
|
|
78
|
+
turndown.use(gfm);
|
|
79
|
+
turndown.addRule("removeEmptyLinks", {
|
|
80
|
+
filter: (node) => node.nodeName === "A" && !node.textContent?.trim(),
|
|
81
|
+
replacement: () => "",
|
|
82
|
+
});
|
|
83
|
+
return turndown
|
|
84
|
+
.turndown(html)
|
|
85
|
+
.replace(/\[\\?\[\s*\\?\]\]\([^)]*\)/g, "")
|
|
86
|
+
.replace(/ +/g, " ")
|
|
87
|
+
.replace(/\s+,/g, ",")
|
|
88
|
+
.replace(/\s+\./g, ".")
|
|
89
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
90
|
+
.trim();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function fetchPageContent(url) {
|
|
94
|
+
try {
|
|
95
|
+
const response = await fetch(url, {
|
|
96
|
+
headers: {
|
|
97
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
|
|
98
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
99
|
+
},
|
|
100
|
+
signal: AbortSignal.timeout(10000),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
return `(HTTP ${response.status})`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const html = await response.text();
|
|
108
|
+
const dom = new JSDOM(html, { url });
|
|
109
|
+
const reader = new Readability(dom.window.document);
|
|
110
|
+
const article = reader.parse();
|
|
111
|
+
|
|
112
|
+
if (article && article.content) {
|
|
113
|
+
return htmlToMarkdown(article.content).substring(0, 5000);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Fallback: try to get main content
|
|
117
|
+
const fallbackDoc = new JSDOM(html, { url });
|
|
118
|
+
const body = fallbackDoc.window.document;
|
|
119
|
+
body.querySelectorAll("script, style, noscript, nav, header, footer, aside").forEach((el) => el.remove());
|
|
120
|
+
const main = body.querySelector("main, article, [role='main'], .content, #content") || body.body;
|
|
121
|
+
const text = main?.textContent || "";
|
|
122
|
+
|
|
123
|
+
if (text.trim().length > 100) {
|
|
124
|
+
return text.trim().substring(0, 5000);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return "(Could not extract content)";
|
|
128
|
+
} catch (e) {
|
|
129
|
+
return `(Error: ${e.message})`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function runDdgs(query, numResults, isNews, region, timelimit, backend) {
|
|
134
|
+
const tmpFile = join(tmpdir(), `ddgs-${randomBytes(6).toString("hex")}.json`);
|
|
135
|
+
const subcommand = isNews ? "news" : "text";
|
|
136
|
+
|
|
137
|
+
const ddgsArgs = [subcommand, "-q", query, "-m", String(numResults), "-o", tmpFile];
|
|
138
|
+
|
|
139
|
+
if (region) {
|
|
140
|
+
ddgsArgs.push("-r", region);
|
|
141
|
+
}
|
|
142
|
+
if (timelimit) {
|
|
143
|
+
ddgsArgs.push("-t", timelimit);
|
|
144
|
+
}
|
|
145
|
+
if (backend) {
|
|
146
|
+
ddgsArgs.push("-b", backend);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
execFileSync("ddgs", ddgsArgs, {
|
|
151
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
152
|
+
timeout: 30000,
|
|
153
|
+
});
|
|
154
|
+
} catch (e) {
|
|
155
|
+
// ddgs may write output to file even if it exits with error
|
|
156
|
+
// (e.g. "Aborted!" after printing results)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const raw = readFileSync(tmpFile, "utf-8");
|
|
161
|
+
unlinkSync(tmpFile);
|
|
162
|
+
return JSON.parse(raw);
|
|
163
|
+
} catch {
|
|
164
|
+
try {
|
|
165
|
+
unlinkSync(tmpFile);
|
|
166
|
+
} catch {}
|
|
167
|
+
throw new Error("ddgs produced no results. Is ddgs installed? Run: uv tool install ddgs");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Main
|
|
172
|
+
try {
|
|
173
|
+
const rawResults = runDdgs(query, numResults, isNews, region, timelimit, backend);
|
|
174
|
+
|
|
175
|
+
if (!rawResults || rawResults.length === 0) {
|
|
176
|
+
console.error("No results found.");
|
|
177
|
+
process.exit(0);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Normalize results to a common shape
|
|
181
|
+
const results = rawResults.map((r) => ({
|
|
182
|
+
title: r.title || "",
|
|
183
|
+
link: r.href || r.url || "",
|
|
184
|
+
snippet: r.body || "",
|
|
185
|
+
date: r.date || "",
|
|
186
|
+
source: r.source || "",
|
|
187
|
+
}));
|
|
188
|
+
|
|
189
|
+
if (fetchContent) {
|
|
190
|
+
for (const result of results) {
|
|
191
|
+
result.content = await fetchPageContent(result.link);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
for (let i = 0; i < results.length; i++) {
|
|
196
|
+
const r = results[i];
|
|
197
|
+
console.log(`--- Result ${i + 1} ---`);
|
|
198
|
+
console.log(`Title: ${r.title}`);
|
|
199
|
+
console.log(`Link: ${r.link}`);
|
|
200
|
+
if (r.date) {
|
|
201
|
+
console.log(`Date: ${r.date}`);
|
|
202
|
+
}
|
|
203
|
+
if (r.source) {
|
|
204
|
+
console.log(`Source: ${r.source}`);
|
|
205
|
+
}
|
|
206
|
+
console.log(`Snippet: ${r.snippet}`);
|
|
207
|
+
if (r.content) {
|
|
208
|
+
console.log(`Content:\n${r.content}`);
|
|
209
|
+
}
|
|
210
|
+
console.log("");
|
|
211
|
+
}
|
|
212
|
+
} catch (e) {
|
|
213
|
+
console.error(`Error: ${e.message}`);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
package/terminal/vhs/SKILL.md
CHANGED
|
@@ -106,13 +106,23 @@ Require git # Fail if git not in PATH
|
|
|
106
106
|
Require node
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
+
## Windows: Use `Set Shell "cmd"`
|
|
110
|
+
|
|
111
|
+
VHS's default `bash` resolves to WSL bash on Windows — a separate environment with no access to Windows-installed tools (node, scoop packages, etc.). **Always use `cmd`** on Windows:
|
|
112
|
+
|
|
113
|
+
```tape
|
|
114
|
+
Set Shell "cmd"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
This gives VHS the full Windows PATH so all installed CLIs work.
|
|
118
|
+
|
|
109
119
|
## Recommended Defaults
|
|
110
120
|
|
|
111
121
|
For README demos and documentation:
|
|
112
122
|
|
|
113
123
|
```tape
|
|
114
124
|
Output demo.gif
|
|
115
|
-
Set Shell "bash"
|
|
125
|
+
Set Shell "cmd" # Windows — use "bash" on Linux/macOS
|
|
116
126
|
Set FontSize 14
|
|
117
127
|
Set Width 1100
|
|
118
128
|
Set Height 600
|
|
@@ -211,6 +221,82 @@ vhs record > my-session.tape
|
|
|
211
221
|
vhs my-session.tape
|
|
212
222
|
```
|
|
213
223
|
|
|
224
|
+
### Recording TUI apps with holdpty
|
|
225
|
+
|
|
226
|
+
Use [holdpty](https://github.com/marcfargas/holdpty) to record full-color TUI applications — apps that need a real PTY for colors, take time to start, or are already running. holdpty provides the PTY, VHS captures the output.
|
|
227
|
+
|
|
228
|
+
**Why this matters:**
|
|
229
|
+
- TUI apps (pi, htop, k9s, lazygit) need a real PTY for colors and layout
|
|
230
|
+
- Slow-starting apps (AI agents, servers) can be ready before recording begins
|
|
231
|
+
- Already-running processes can be recorded on demand for monitoring/sharing
|
|
232
|
+
|
|
233
|
+
**The pattern:**
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
# Step 1: Launch the app in holdpty (or it's already running)
|
|
237
|
+
holdpty launch --bg --name demo -- my-tui-app
|
|
238
|
+
|
|
239
|
+
# Step 2: Wait for it to be ready (or it already is)
|
|
240
|
+
sleep 5
|
|
241
|
+
|
|
242
|
+
# Step 3: Record with VHS
|
|
243
|
+
vhs record-demo.tape
|
|
244
|
+
|
|
245
|
+
# Step 4: Clean up (or leave it running)
|
|
246
|
+
holdpty stop demo
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**Tape file — attach to a running session:**
|
|
250
|
+
|
|
251
|
+
```tape
|
|
252
|
+
Output demo.gif
|
|
253
|
+
Output demo.mp4
|
|
254
|
+
Set Shell "cmd"
|
|
255
|
+
Set FontSize 14
|
|
256
|
+
Set Width 1200
|
|
257
|
+
Set Height 700
|
|
258
|
+
Set Theme "Dracula"
|
|
259
|
+
Set Padding 15
|
|
260
|
+
Set WindowBar "Colorful"
|
|
261
|
+
Set BorderRadius 8
|
|
262
|
+
|
|
263
|
+
# Attach to the running holdpty session
|
|
264
|
+
Type "holdpty attach demo"
|
|
265
|
+
Enter
|
|
266
|
+
Sleep 2s
|
|
267
|
+
|
|
268
|
+
# Interact with the app (VHS types, app responds with full TUI)
|
|
269
|
+
Type "your input here"
|
|
270
|
+
Enter
|
|
271
|
+
Sleep 15s
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**Tape file — view-only snapshot of a running process:**
|
|
275
|
+
|
|
276
|
+
```tape
|
|
277
|
+
Output snapshot.gif
|
|
278
|
+
Set Shell "cmd"
|
|
279
|
+
Set FontSize 14
|
|
280
|
+
Set Width 1200
|
|
281
|
+
Set Height 700
|
|
282
|
+
Set Theme "Dracula"
|
|
283
|
+
Set Padding 15
|
|
284
|
+
|
|
285
|
+
# View is read-only — just captures current output
|
|
286
|
+
Type "holdpty view demo"
|
|
287
|
+
Enter
|
|
288
|
+
Sleep 10s
|
|
289
|
+
Ctrl+C
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**Use cases:**
|
|
293
|
+
- **README demos** — launch app, let it start, record the interesting part
|
|
294
|
+
- **Monitoring snapshots** — capture what a running service looks like right now
|
|
295
|
+
- **Bug reports** — record a live reproduction with full colors
|
|
296
|
+
- **Agent recordings** — capture an AI agent working with its full TUI (thinking indicators, tool calls, colored output)
|
|
297
|
+
|
|
298
|
+
> On Windows, use `node.exe <path/to/cli.js>` instead of CLI names when launching via holdpty — see the [holdpty skill](https://github.com/marcfargas/holdpty) for the `.cmd` wrapper gotcha.
|
|
299
|
+
|
|
214
300
|
## Themes
|
|
215
301
|
|
|
216
302
|
Popular themes for demos:
|
|
@@ -241,8 +327,10 @@ List all: `vhs themes`
|
|
|
241
327
|
|
|
242
328
|
## Gotchas
|
|
243
329
|
|
|
330
|
+
- **Windows: `Set Shell "bash"` uses WSL** — VHS resolves `bash` to WSL bash, not Git Bash. Use `Set Shell "cmd"` to get the full Windows PATH. Custom shell paths (`Set Shell "C:/path/to/bash.exe"`) are rejected.
|
|
244
331
|
- **No shell expansion in Type** — `Type "echo $HOME"` types the literal string; variable expansion happens when bash executes it, not in the tape
|
|
245
332
|
- **Quoting** — avoid nested quotes in Type. Use wrapper scripts for complex commands
|
|
246
333
|
- **Windows paths** — use forward slashes in Type strings (`C:/dev/...` not `C:\dev\...`)
|
|
247
334
|
- **Long recordings** — GIFs get huge fast. Keep demos under 30 seconds. Use `Set PlaybackSpeed 2.0` to compress
|
|
248
335
|
- **Terminal size** — if output wraps weird, increase Width or reduce FontSize
|
|
336
|
+
- **TUI apps without holdpty** — apps that need a real PTY for colors (pi, htop, lazygit) will render without colors if launched directly from VHS. Use the holdpty pattern above.
|