askaipods 0.1.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 +21 -0
- package/README.md +180 -0
- package/bin/askaipods.js +8 -0
- package/examples/claude-code-install.md +54 -0
- package/examples/codex-install.md +47 -0
- package/examples/hermes-install.md +36 -0
- package/examples/openclaw-install.md +62 -0
- package/package.json +53 -0
- package/skill/askaipods/SKILL.md +195 -0
- package/src/cli.js +122 -0
- package/src/client.js +115 -0
- package/src/format.js +125 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Delibread0601
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# askaipods
|
|
2
|
+
|
|
3
|
+
> Search AI podcast quotes about a topic — find what real guests on Lex Fridman, Dwarkesh Patel, No Priors, Latent Space, and dozens of other AI podcasts are actually saying. A universal [agentskills.io](https://agentskills.io) skill compatible with Claude Code, OpenAI Codex, Hermes Agent, OpenClaw, and any other agent that supports the open skill standard. Powered by [podlens.net](https://podlens.net).
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
$ askaipods "what are people saying about test-time compute"
|
|
7
|
+
|
|
8
|
+
# askaipods · "what are people saying about test-time compute"
|
|
9
|
+
|
|
10
|
+
*Tier: anonymous · Results: 10 · Quota: 1/10 daily*
|
|
11
|
+
|
|
12
|
+
## Results — newest first
|
|
13
|
+
|
|
14
|
+
### 1. Lenny's Podcast — AI Engineering 101 with Chip Huyen
|
|
15
|
+
*2025-10*
|
|
16
|
+
|
|
17
|
+
> Test-time compute — spending more compute during inference by generating
|
|
18
|
+
> multiple answers and selecting the best, or allowing more reasoning/thinking ...
|
|
19
|
+
|
|
20
|
+
### 2. Latent Space — Better Data is All You Need (Ari Morcos, Datology)
|
|
21
|
+
*2025-08*
|
|
22
|
+
|
|
23
|
+
> Test-time compute as a paradigm pushes toward smaller base models because
|
|
24
|
+
> the cost of solving a prob...
|
|
25
|
+
|
|
26
|
+
(...8 more results, newest-first...)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Why this exists
|
|
30
|
+
|
|
31
|
+
Web search is bad at "what is the AI community thinking about X right now". You get blog posts, Reddit threads, and outdated news articles. What you actually want is the *real conversation* — what researchers, founders, and investors are saying on AI podcasts, in their own words.
|
|
32
|
+
|
|
33
|
+
`askaipods` is a thin CLI + agent skill that asks the [PodLens](https://podlens.net) semantic search API and returns the most relevant quote excerpts, sorted newest-first. The skill teaches your agent (Claude Code, OpenAI Codex, Hermes, OpenClaw, and any other [agentskills.io](https://agentskills.io)-compatible runtime) when to call the CLI, how to parse the output, and how to write a useful **Insights** section that summarizes the patterns across the returned quotes.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
### Option 1: as a CLI (works in any terminal)
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx askaipods "your query here"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
That's the entire install. `npx` fetches and runs the latest version each time. No global install needed.
|
|
44
|
+
|
|
45
|
+
To install globally (faster startup):
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install -g askaipods
|
|
49
|
+
askaipods "your query here"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Option 2: as an agent skill (Claude Code, Codex, Hermes, OpenClaw, etc.)
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
git clone https://github.com/Delibread0601/askaipods.git
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Then copy or symlink the `skill/askaipods/` directory into your agent's skills folder. Per-runtime instructions:
|
|
59
|
+
|
|
60
|
+
| Runtime | Skill folder | Install guide |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| Claude Code | `~/.claude/skills/askaipods/` | [examples/claude-code-install.md](examples/claude-code-install.md) |
|
|
63
|
+
| OpenAI Codex CLI | `~/.agents/skills/askaipods/` ✨ | [examples/codex-install.md](examples/codex-install.md) |
|
|
64
|
+
| OpenClaw | `~/.agents/skills/askaipods/` ✨ or `~/.openclaw/skills/askaipods/` | [examples/openclaw-install.md](examples/openclaw-install.md) |
|
|
65
|
+
| Hermes Agent | `~/.hermes/skills/askaipods/` | [examples/hermes-install.md](examples/hermes-install.md) |
|
|
66
|
+
| Any other agentskills.io-compatible runtime | per runtime docs | follow the agentskills.io standard — copy `skill/askaipods/` into your agent's skills directory |
|
|
67
|
+
|
|
68
|
+
✨ **Two-for-one tip**: Codex CLI and OpenClaw both read from `~/.agents/skills/`, so a single install at `~/.agents/skills/askaipods/` covers both runtimes simultaneously.
|
|
69
|
+
|
|
70
|
+
The skill folder is self-contained: it tells the host agent how to invoke `askaipods` (via `npx`), how to parse the JSON, and how to render the response with a **Latest** + **Top Relevant** + **Insights** structure.
|
|
71
|
+
|
|
72
|
+
## Usage
|
|
73
|
+
|
|
74
|
+
### As a CLI
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# Default: human-readable markdown to terminal
|
|
78
|
+
askaipods "what are VCs saying about reasoning models"
|
|
79
|
+
|
|
80
|
+
# JSON output (for scripts and agents)
|
|
81
|
+
askaipods "Anthropic safety research" --format json
|
|
82
|
+
|
|
83
|
+
# Restrict to recent episodes only (max 7 days for anonymous tier; member tier accepts any value)
|
|
84
|
+
askaipods "GPU shortage" --days 7
|
|
85
|
+
|
|
86
|
+
# Use a member-tier API key for 50/day instead of 10/day
|
|
87
|
+
ASKAIPODS_API_KEY=pk_xxx askaipods "your query"
|
|
88
|
+
askaipods "your query" --api-key pk_xxx
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### As an agent skill
|
|
92
|
+
|
|
93
|
+
Once the skill is installed in your agent's skills directory, simply ask:
|
|
94
|
+
|
|
95
|
+
> What are people saying about test-time compute on AI podcasts?
|
|
96
|
+
|
|
97
|
+
Your agent will recognize the trigger phrase, invoke `askaipods`, and present the results with a Latest section, a Top Relevant section, and an AI-generated Insights summary. No CLI knowledge required from the user.
|
|
98
|
+
|
|
99
|
+
## Tier comparison
|
|
100
|
+
|
|
101
|
+
| | Anonymous (default) | Member |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| **Daily quota** | 10 searches per IP | 50 searches per user |
|
|
104
|
+
| **Results returned** | 10 (randomized from top 20) | 20 (fixed top 20) |
|
|
105
|
+
| **Text length** | Truncated by rank (150/100/60 chars) | Full text |
|
|
106
|
+
| **Date precision** | Month only (`2025-10`) | Full date (`2025-10-15`) |
|
|
107
|
+
| **Setup** | Nothing | `ASKAIPODS_API_KEY` env var |
|
|
108
|
+
| **Sign up** | n/a | https://podlens.net |
|
|
109
|
+
|
|
110
|
+
The anonymous tier exists so you can try the skill end-to-end with zero setup. Sign up for member access only when you outgrow the 10/day quota or need full text and exact dates.
|
|
111
|
+
|
|
112
|
+
## Honest limitations
|
|
113
|
+
|
|
114
|
+
- **No speaker attribution.** The corpus indexes quotes at the episode level but does not attempt to identify *which guest* said each quote. The upstream pipeline avoids speaker labeling because automatic diarization is unreliable, and a wrong attribution is worse than no attribution.
|
|
115
|
+
- **No episode URLs.** The public API does not expose direct podcast or episode links. You will need to search the podcast and episode title in your podcast app of choice.
|
|
116
|
+
- **AI-focused corpus.** Coverage is dense for AI research, ML engineering, AI investing, and AI policy. Off-topic queries return sparse, noisy results.
|
|
117
|
+
- **Short quote excerpts.** Each result is typically 1-3 sentences (anonymous tier truncates further). For long-form context, listen to the episode.
|
|
118
|
+
|
|
119
|
+
These are not bugs. The skill surfaces them honestly so neither you nor your agent fabricate things the API does not provide.
|
|
120
|
+
|
|
121
|
+
## Exit codes
|
|
122
|
+
|
|
123
|
+
| Code | Meaning |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `0` | Success |
|
|
126
|
+
| `1` | Usage error / invalid arguments / API key rejected |
|
|
127
|
+
| `2` | Daily quota exhausted |
|
|
128
|
+
| `3` | Network error / podlens.net unavailable |
|
|
129
|
+
|
|
130
|
+
## How the skill renders results
|
|
131
|
+
|
|
132
|
+
For member tier (`render_hint: dual_view`), the host agent renders two sections plus insights:
|
|
133
|
+
|
|
134
|
+
```markdown
|
|
135
|
+
## 🆕 Latest 5
|
|
136
|
+
(5 most recent of the 20 returned results)
|
|
137
|
+
|
|
138
|
+
## 🎯 Top 5 Most Relevant
|
|
139
|
+
(5 results with api_rank 1-5, regardless of date)
|
|
140
|
+
|
|
141
|
+
## 💡 Insights
|
|
142
|
+
(3-5 bullets synthesizing patterns across the quotes)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
For anonymous tier (`render_hint: single_view`), only Latest and Insights — the Top Relevant section is intentionally suppressed because anonymous results are randomized within the top 20, so api_rank does not represent true semantic relevance.
|
|
146
|
+
|
|
147
|
+
See [`skill/askaipods/SKILL.md`](skill/askaipods/SKILL.md) for the full skill specification.
|
|
148
|
+
|
|
149
|
+
## Architecture
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
askaipods/
|
|
153
|
+
├── bin/askaipods.js ← CLI entry (shebang)
|
|
154
|
+
├── src/
|
|
155
|
+
│ ├── cli.js ← arg parsing, format auto-detection
|
|
156
|
+
│ ├── client.js ← podlens.net /api/search/semantic client
|
|
157
|
+
│ └── format.js ← time-desc sort + JSON / markdown rendering
|
|
158
|
+
├── skill/askaipods/
|
|
159
|
+
│ └── SKILL.md ← agentskills.io standard skill file
|
|
160
|
+
├── examples/ ← per-runtime install guides
|
|
161
|
+
├── package.json ← zero dependencies (Node 18+ stdlib only)
|
|
162
|
+
├── LICENSE ← MIT
|
|
163
|
+
└── README.md
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The CLI is intentionally zero-dependency (Node 18+ stdlib only) so `npx askaipods` cold-starts in under a second and the package install footprint is minimal.
|
|
167
|
+
|
|
168
|
+
## Contributing
|
|
169
|
+
|
|
170
|
+
Issues and PRs welcome at https://github.com/Delibread0601/askaipods.
|
|
171
|
+
|
|
172
|
+
If you find a runtime that conforms to [agentskills.io](https://agentskills.io) but is not yet listed in the install table above, please open an issue or PR with the install path and we'll add it.
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
MIT — see [LICENSE](LICENSE).
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
Powered by [podlens.net](https://podlens.net) — AI podcast intelligence.
|
package/bin/askaipods.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { run } from "../src/cli.js";
|
|
3
|
+
|
|
4
|
+
run(process.argv.slice(2)).catch((err) => {
|
|
5
|
+
const message = err?.message ?? String(err);
|
|
6
|
+
process.stderr.write(`askaipods: ${message}\n`);
|
|
7
|
+
process.exit(typeof err?.exitCode === "number" ? err.exitCode : 1);
|
|
8
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Install askaipods in Claude Code
|
|
2
|
+
|
|
3
|
+
## Personal install (available across all your projects)
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
git clone https://github.com/Delibread0601/askaipods.git ~/Code/askaipods
|
|
7
|
+
mkdir -p ~/.claude/skills
|
|
8
|
+
ln -s ~/Code/askaipods/skill/askaipods ~/.claude/skills/askaipods
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The symlink lets you `git pull` updates to the repo and have them picked up automatically without recopying.
|
|
12
|
+
|
|
13
|
+
If you prefer a copy over a symlink:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cp -r ~/Code/askaipods/skill/askaipods ~/.claude/skills/askaipods
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Project-only install (only for the current repo)
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
mkdir -p .claude/skills
|
|
23
|
+
cp -r /path/to/askaipods/skill/askaipods .claude/skills/askaipods
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Project-level skills override personal-level skills with the same name.
|
|
27
|
+
|
|
28
|
+
## Verify
|
|
29
|
+
|
|
30
|
+
In Claude Code, ask:
|
|
31
|
+
|
|
32
|
+
> What skills are available?
|
|
33
|
+
|
|
34
|
+
You should see `askaipods` in the list. Or invoke it directly:
|
|
35
|
+
|
|
36
|
+
> /askaipods test-time compute
|
|
37
|
+
|
|
38
|
+
Or trigger it organically:
|
|
39
|
+
|
|
40
|
+
> What are people saying about test-time compute on AI podcasts?
|
|
41
|
+
|
|
42
|
+
Claude Code should recognize the trigger phrase, run `npx askaipods search "..." --format json`, parse the response, and render the Latest / Top Relevant / Insights sections.
|
|
43
|
+
|
|
44
|
+
## Troubleshooting
|
|
45
|
+
|
|
46
|
+
- **Skill not appearing**: Make sure the parent directory name matches the `name` field in `SKILL.md` (both must be `askaipods`).
|
|
47
|
+
- **`npx askaipods` fails**: Check that Node.js 18+ is installed: `node --version`. The CLI uses zero dependencies so there are no other prereqs.
|
|
48
|
+
- **Anonymous quota exhausted**: Sign up at https://podlens.net for 50/day, then `export ASKAIPODS_API_KEY=pk_xxx`.
|
|
49
|
+
- **Skill triggers too rarely**: Front-load your prompt with the trigger phrases in `SKILL.md` description, or invoke directly with `/askaipods <query>`.
|
|
50
|
+
|
|
51
|
+
## Reference
|
|
52
|
+
|
|
53
|
+
- [Claude Code skills documentation](https://code.claude.com/docs/en/skills)
|
|
54
|
+
- [agentskills.io specification](https://agentskills.io/specification)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Install askaipods in OpenAI Codex CLI
|
|
2
|
+
|
|
3
|
+
Codex CLI typically looks in `~/.agents/skills/` (user-level) and `.agents/skills/` (project / repo level). Project-scoped skills win over user-scoped when both exist with the same name. For the authoritative scope list and any system-level paths your installed version supports, consult the [official Codex skills documentation](https://developers.openai.com/codex/skills/).
|
|
4
|
+
|
|
5
|
+
For most users, the **user-level** install is what you want — it makes `askaipods` available across every project.
|
|
6
|
+
|
|
7
|
+
## User-level install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
git clone https://github.com/Delibread0601/askaipods.git ~/Code/askaipods
|
|
11
|
+
mkdir -p ~/.agents/skills
|
|
12
|
+
ln -s ~/Code/askaipods/skill/askaipods ~/.agents/skills/askaipods
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Symlink (above) is recommended so `git pull` updates flow through automatically. Or copy:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
cp -r ~/Code/askaipods/skill/askaipods ~/.agents/skills/askaipods
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Project-only install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
mkdir -p .agents/skills
|
|
25
|
+
cp -r /path/to/askaipods/skill/askaipods .agents/skills/askaipods
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Verify
|
|
29
|
+
|
|
30
|
+
Codex CLI detects skill changes automatically. If the skill does not appear after install, restart the Codex session.
|
|
31
|
+
|
|
32
|
+
In Codex, ask:
|
|
33
|
+
|
|
34
|
+
> What are people saying about reasoning models on AI podcasts?
|
|
35
|
+
|
|
36
|
+
Codex should detect the trigger, run `npx askaipods search "..." --format json`, and render the structured response per `SKILL.md`.
|
|
37
|
+
|
|
38
|
+
## Troubleshooting
|
|
39
|
+
|
|
40
|
+
- **Skill not detected**: Restart Codex (per the official docs, "If an update doesn't appear, restart Codex").
|
|
41
|
+
- **Multiple skills with the same name across scopes**: Codex shows both in the selector — repository-scoped wins by default.
|
|
42
|
+
- **`npx askaipods` fails**: Check Node.js 18+: `node --version`.
|
|
43
|
+
|
|
44
|
+
## Reference
|
|
45
|
+
|
|
46
|
+
- [OpenAI Codex skills documentation](https://developers.openai.com/codex/skills/)
|
|
47
|
+
- [agentskills.io specification](https://agentskills.io/specification)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Install askaipods in Hermes Agent
|
|
2
|
+
|
|
3
|
+
[Hermes Agent](https://github.com/nousresearch/hermes-agent) is built by Nous Research and is compatible with the [agentskills.io](https://agentskills.io) open standard. Skills live in `~/.hermes/skills/`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
git clone https://github.com/Delibread0601/askaipods.git ~/Code/askaipods
|
|
9
|
+
mkdir -p ~/.hermes/skills
|
|
10
|
+
ln -s ~/Code/askaipods/skill/askaipods ~/.hermes/skills/askaipods
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or copy:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cp -r ~/Code/askaipods/skill/askaipods ~/.hermes/skills/askaipods
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Verify
|
|
20
|
+
|
|
21
|
+
In a Hermes session, ask:
|
|
22
|
+
|
|
23
|
+
> Find what AI podcasts are saying about test-time compute
|
|
24
|
+
|
|
25
|
+
Hermes should pick up the skill from `~/.hermes/skills/askaipods/`, shell out to `npx askaipods`, and present the structured results.
|
|
26
|
+
|
|
27
|
+
## Troubleshooting
|
|
28
|
+
|
|
29
|
+
- **`npx askaipods` fails**: Hermes is Python-based but the askaipods CLI is Node. Make sure Node.js 18+ is on PATH alongside Python: `node --version`.
|
|
30
|
+
- **Skill not picked up**: Hermes documentation indicates skills are loaded from `~/.hermes/skills/`. Restart the agent after install if needed.
|
|
31
|
+
- **Quota exhausted**: Set `ASKAIPODS_API_KEY` in your shell environment before launching Hermes so the variable propagates to subprocess calls.
|
|
32
|
+
|
|
33
|
+
## Reference
|
|
34
|
+
|
|
35
|
+
- [Hermes Agent README](https://github.com/nousresearch/hermes-agent)
|
|
36
|
+
- [agentskills.io specification](https://agentskills.io/specification)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Install askaipods in OpenClaw
|
|
2
|
+
|
|
3
|
+
[OpenClaw](https://github.com/openclaw/openclaw) is [agentskills.io](https://agentskills.io)-compatible. Per the [official docs](https://docs.openclaw.ai/tools/skills), it loads skills from four locations with this precedence (highest first):
|
|
4
|
+
|
|
5
|
+
1. `<workspace>/skills/` — workspace skills (highest)
|
|
6
|
+
2. `<workspace>/.agents/skills/` — project agent skills
|
|
7
|
+
3. `~/.agents/skills/` — personal agent skills (shared with OpenAI Codex CLI)
|
|
8
|
+
4. `~/.openclaw/skills/` — managed/local skills (shared across all agents on the machine)
|
|
9
|
+
|
|
10
|
+
For most users, **option 3 is the best choice** because the same `~/.agents/skills/askaipods/` install also makes the skill available in OpenAI Codex CLI — one install, two runtimes.
|
|
11
|
+
|
|
12
|
+
## Recommended install (shared with Codex)
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
git clone https://github.com/Delibread0601/askaipods.git ~/Code/askaipods
|
|
16
|
+
mkdir -p ~/.agents/skills
|
|
17
|
+
ln -s ~/Code/askaipods/skill/askaipods ~/.agents/skills/askaipods
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
If you don't already use Codex and prefer to keep OpenClaw skills separate, install into the OpenClaw-native location instead:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
mkdir -p ~/.openclaw/skills
|
|
24
|
+
ln -s ~/Code/askaipods/skill/askaipods ~/.openclaw/skills/askaipods
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or use the OpenClaw CLI once askaipods is published to the ClawHub registry (not yet — check back, or open an issue to track):
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
openclaw skills install askaipods # not yet available
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Workspace-only install
|
|
34
|
+
|
|
35
|
+
To make askaipods available only inside a specific OpenClaw workspace:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
mkdir -p <workspace>/skills
|
|
39
|
+
cp -r ~/Code/askaipods/skill/askaipods <workspace>/skills/askaipods
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
This wins over all user-level installs for that workspace.
|
|
43
|
+
|
|
44
|
+
## Verify
|
|
45
|
+
|
|
46
|
+
In OpenClaw, ask:
|
|
47
|
+
|
|
48
|
+
> What are AI podcasts saying about reasoning models?
|
|
49
|
+
|
|
50
|
+
OpenClaw should recognize the trigger phrase, shell out to `npx askaipods search "..." --format json`, and render the structured response per the SKILL.md template.
|
|
51
|
+
|
|
52
|
+
## Troubleshooting
|
|
53
|
+
|
|
54
|
+
- **Skill not detected**: Run `openclaw skills update --all` to refresh, or restart the OpenClaw session. Check that the directory name `askaipods` matches the `name` field in `SKILL.md`.
|
|
55
|
+
- **`npx askaipods` fails**: Make sure Node.js 18+ is on PATH: `node --version`.
|
|
56
|
+
- **Conflicting copies across precedence levels**: Only the highest-precedence one wins. If you have askaipods in both `~/.agents/skills/` and `<workspace>/skills/`, the workspace one takes effect.
|
|
57
|
+
|
|
58
|
+
## Reference
|
|
59
|
+
|
|
60
|
+
- [OpenClaw skills documentation](https://docs.openclaw.ai/tools/skills)
|
|
61
|
+
- [OpenClaw GitHub](https://github.com/openclaw/openclaw)
|
|
62
|
+
- [agentskills.io specification](https://agentskills.io/specification)
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "askaipods",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Search AI podcast quotes by topic — find what Lex Fridman, Dwarkesh Patel, No Priors, and Latent Space guests are actually saying. Universal agentskills.io skill compatible with Claude Code, OpenAI Codex, Hermes Agent, OpenClaw, and any other agent that supports the open skill standard. Powered by podlens.net.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"askaipods": "./bin/askaipods.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./src/cli.js",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/cli.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin",
|
|
15
|
+
"src",
|
|
16
|
+
"skill",
|
|
17
|
+
"examples",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"start": "node bin/askaipods.js",
|
|
26
|
+
"test": "echo 'no automated tests yet — see CONTRIBUTING for the test plan' && exit 0"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"ai",
|
|
30
|
+
"podcasts",
|
|
31
|
+
"search",
|
|
32
|
+
"agent-skill",
|
|
33
|
+
"agentskills",
|
|
34
|
+
"claude-code",
|
|
35
|
+
"codex",
|
|
36
|
+
"hermes",
|
|
37
|
+
"openclaw",
|
|
38
|
+
"podlens",
|
|
39
|
+
"semantic-search",
|
|
40
|
+
"llm-tools",
|
|
41
|
+
"agentic"
|
|
42
|
+
],
|
|
43
|
+
"author": "Delibread0601",
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"homepage": "https://github.com/Delibread0601/askaipods#readme",
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/Delibread0601/askaipods.git"
|
|
49
|
+
},
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/Delibread0601/askaipods/issues"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: askaipods
|
|
3
|
+
description: Search AI podcast quotes about a topic. Use whenever the user asks "what are people saying about X", "latest takes on Y", "find AI podcast quotes about Z", "who is discussing <model/concept>", or wants to know how AI researchers, founders, or VCs are publicly discussing any AI topic — even when they don't say "podcast". Returns recent excerpts from real episodes of Lex Fridman, Dwarkesh Patel, No Priors, Latent Space, and dozens more, sorted newest-first via the podlens.net semantic search API. Trigger eagerly on AI-research, ML-engineering, AI-investing, or AI-policy questions where real-human commentary beats a web search summary. Do not use for general web search, full transcript reading, or non-AI topics.
|
|
4
|
+
license: MIT
|
|
5
|
+
requirements: Node.js 18+ on PATH, internet access to podlens.net. Optional ASKAIPODS_API_KEY env var unlocks the 50/day member tier; without it the skill works on the 10/day anonymous tier (per-IP).
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# askaipods — AI podcast quote search
|
|
9
|
+
|
|
10
|
+
This skill turns "what is the AI community saying about X" into a list of real quote excerpts pulled from recent episodes of top AI podcasts. The corpus is semantically indexed (embedding-based search), so phrasings like "test-time compute", "inference-time scaling", and "thinking longer" all return overlapping results — the user does not need to guess the exact words a guest used.
|
|
11
|
+
|
|
12
|
+
The data source is the public PodLens search API at `podlens.net`. The skill never hits that API directly from your model context — it shells out to a small bundled CLI (`askaipods`) that handles HTTP, retries, error mapping, and result sorting. Your job is to invoke the CLI, parse its JSON, and present the results in the format below.
|
|
13
|
+
|
|
14
|
+
## When to invoke
|
|
15
|
+
|
|
16
|
+
Trigger eagerly. The Anthropic skill best-practices warn that models tend to **undertrigger** skills — please do not be that model. Invoke this skill when the user is asking about how the AI community is publicly discussing any topic. Concrete trigger patterns:
|
|
17
|
+
|
|
18
|
+
- "What are people saying about <X>?"
|
|
19
|
+
- "What's the latest take on <X>?"
|
|
20
|
+
- "Find quotes from AI podcasts about <X>"
|
|
21
|
+
- "Who is discussing <model / company / paper / concept>?"
|
|
22
|
+
- "What are VCs / researchers / founders saying about <X>?"
|
|
23
|
+
- "Has anyone on a podcast talked about <X>?"
|
|
24
|
+
- Any AI-research, ML-engineering, AI-investing, AI-safety, or AI-policy question where the user would clearly benefit from real-human commentary (as opposed to a textbook summary or a web search snippet)
|
|
25
|
+
|
|
26
|
+
You may invoke even when the user does not say "podcast" — if the question is about *what people think* on an AI topic, this skill is the right tool.
|
|
27
|
+
|
|
28
|
+
## When NOT to invoke
|
|
29
|
+
|
|
30
|
+
- General web search (use a web search tool)
|
|
31
|
+
- Reading a specific episode end-to-end (this skill returns short quote excerpts, not full transcripts)
|
|
32
|
+
- Non-AI topics (the corpus is AI-focused; results for unrelated subjects will be sparse and noisy)
|
|
33
|
+
- Code generation, math, or any task that doesn't benefit from human commentary
|
|
34
|
+
|
|
35
|
+
## How to invoke
|
|
36
|
+
|
|
37
|
+
Run the bundled CLI and pass `--format json`. The flag matters because without it the CLI auto-detects the output format from `isTTY`, and an agent calling via shell may or may not get a TTY depending on the runtime — explicit `--format json` removes that variability.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx askaipods search "<USER QUERY>" --format json
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
If `npx askaipods` is not available in the user's environment (e.g. the package was not yet published to npm), fall back to running the local CLI from this skill's parent directory:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
node <path-to-askaipods-repo>/bin/askaipods.js search "<USER QUERY>" --format json
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
To restrict to recent episodes only, add `--days N` (the API caps anonymous tier at 7 days; member tier accepts any value):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npx askaipods search "<USER QUERY>" --days 30 --format json
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## JSON shape returned by the CLI
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"tier": "anonymous" | "member",
|
|
60
|
+
"query": "the user's query string",
|
|
61
|
+
"fetched_at": "<ISO-8601 timestamp set by the CLI at request time>",
|
|
62
|
+
"render_hint": "single_view" | "dual_view",
|
|
63
|
+
"results": [
|
|
64
|
+
{
|
|
65
|
+
"podcast": "Dwarkesh Patel",
|
|
66
|
+
"episode": "Dario Amodei on the future of AI",
|
|
67
|
+
"date": "2026-03-15",
|
|
68
|
+
"text": "the actual quote excerpt ...",
|
|
69
|
+
"api_rank": 1
|
|
70
|
+
}
|
|
71
|
+
],
|
|
72
|
+
"meta": {
|
|
73
|
+
"total_returned": 20,
|
|
74
|
+
"quota": { "used": 3, "limit": 50, "period": "daily" },
|
|
75
|
+
"restrictions": null,
|
|
76
|
+
"query_hash": "..."
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Field notes that affect how you render:
|
|
82
|
+
|
|
83
|
+
- **`tier`** — `member` if the user has a valid API key, `anonymous` otherwise. Drives the rendering branch below. The CLI defaults `tier` to `"anonymous"` if the upstream response somehow lacks the field, so you will always land on one of the two documented branches — never on a third "unknown" path.
|
|
84
|
+
- **`render_hint`** — `dual_view` for member, `single_view` for anonymous. Honor this. The reason: anonymous results are a randomized 10-of-20 subset, so `api_rank` only describes order *within that random subset*, not true semantic relevance against the corpus. Showing a "Top Most Relevant" section for anonymous tier would mislead the user.
|
|
85
|
+
- **`results[]`** — already sorted **newest first** by the CLI. Each result carries `api_rank` (1 = most semantically relevant in API order) so you can derive a "Top Relevant" sub-view without re-querying.
|
|
86
|
+
- **`results[].podcast` / `episode` / `date`** — any of these may be `null` if the upstream record is incomplete. Render `Unknown podcast` / `Untitled episode` / `date unknown` rather than dropping the result. The CLI's own markdown renderer falls back the same way.
|
|
87
|
+
- **`results[].date` format** — `YYYY-MM-DD` (or full ISO timestamp) for member tier; `YYYY-MM` only for anonymous tier (deliberately fuzzed by the API for query privacy). Display whatever you got — don't guess a day.
|
|
88
|
+
- **`meta.quota`** — passed through from the podlens.net API. Sub-fields like `used`, `limit`, `period` are reliably present; other sub-fields (e.g., a reset timestamp) may or may not appear depending on the server version. Treat all sub-fields as optional and degrade gracefully.
|
|
89
|
+
- **`meta.restrictions`** — `null` for member tier; for anonymous tier, an object describing the cap (e.g., `{ max_results: 10, text_truncated: true, results_randomized: true }`). If non-null, the closing anonymous-tier note (templated below) is the right way to surface it; do not parse the object field-by-field.
|
|
90
|
+
- **No speaker name and no episode URL.** The corpus is indexed at the key-point level without per-speaker attribution (the upstream pipeline intentionally avoids attributing quotes to individuals because automatic speaker diarization is unreliable). Episode URLs are also not exposed by the public API. Render `Podcast — Episode` only; do not fabricate "Dario said" if the text doesn't already attribute itself.
|
|
91
|
+
|
|
92
|
+
## How to render the response
|
|
93
|
+
|
|
94
|
+
Output exactly this structure. It is required for consistency across runtimes — users of this skill across Claude Code, OpenAI Codex, Hermes Agent, OpenClaw, and any other agentskills.io-compatible agent should see the same shape regardless of which agent ran it.
|
|
95
|
+
|
|
96
|
+
(Note: the CLI's own `--format markdown` output uses a different layout — `### N. Podcast — Episode` headings — because that mode targets humans running `askaipods` directly in a terminal. As an agent you should always pass `--format json` and reformat the parsed payload yourself per the templates below; do not copy the CLI's markdown.)
|
|
97
|
+
|
|
98
|
+
### For `render_hint: "dual_view"` (member tier)
|
|
99
|
+
|
|
100
|
+
```markdown
|
|
101
|
+
## 🆕 Latest 5
|
|
102
|
+
|
|
103
|
+
1. **<podcast>** — *<episode>* · <date>
|
|
104
|
+
> "<quote text>"
|
|
105
|
+
|
|
106
|
+
2. ...
|
|
107
|
+
|
|
108
|
+
(continue through the 5 most recent of the returned results, which are the first 5 in the `results` array since the CLI already sorted by date desc)
|
|
109
|
+
|
|
110
|
+
## 🎯 Top 5 Most Relevant
|
|
111
|
+
|
|
112
|
+
1. **<podcast>** — *<episode>* · <date>
|
|
113
|
+
> "<quote text>"
|
|
114
|
+
|
|
115
|
+
2. ...
|
|
116
|
+
|
|
117
|
+
(these 5 are the results with `api_rank` 1 through 5, regardless of date — pull them from the same `results` array by filtering on `api_rank`)
|
|
118
|
+
|
|
119
|
+
## 💡 Insights
|
|
120
|
+
|
|
121
|
+
- <bullet 1>
|
|
122
|
+
- <bullet 2>
|
|
123
|
+
- <bullet 3>
|
|
124
|
+
- (3-5 bullets total — see Insights guidelines below)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
If the same result appears in both Latest and Top Relevant sections, that's fine and informative (it means a recent quote is also semantically central) — show it in both. Do not deduplicate.
|
|
128
|
+
|
|
129
|
+
### For `render_hint: "single_view"` (anonymous tier)
|
|
130
|
+
|
|
131
|
+
```markdown
|
|
132
|
+
## 🆕 Recent Quotes
|
|
133
|
+
|
|
134
|
+
1. **<podcast>** — *<episode>* · <date>
|
|
135
|
+
> "<quote text>"
|
|
136
|
+
|
|
137
|
+
2. ...
|
|
138
|
+
|
|
139
|
+
(all returned results, in `results` array order which is already newest-first; expect up to 10)
|
|
140
|
+
|
|
141
|
+
## 💡 Insights
|
|
142
|
+
|
|
143
|
+
- <bullet 1>
|
|
144
|
+
- <bullet 2>
|
|
145
|
+
- <bullet 3>
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
*Anonymous tier: 10 randomized results from top 20, text truncated by rank, dates fuzzed to month. Set `ASKAIPODS_API_KEY` for 50 searches/day with full text and full dates — sign up at https://podlens.net.*
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The closing note about the anonymous tier matters because it tells the user (a) why the text looks chopped, (b) why the dates are coarse, and (c) what the upgrade path is. Skipping it leaves the user wondering if the skill is broken.
|
|
153
|
+
|
|
154
|
+
## Insights guidelines
|
|
155
|
+
|
|
156
|
+
The Insights section is the most valuable part of your response — it is what differentiates this skill from a raw API call. The user could read 10 quotes themselves; what they cannot easily do is *spot the patterns across the 10*. That is your job.
|
|
157
|
+
|
|
158
|
+
Write 3-5 bullets, each one concrete and one sentence long. Cover at least three of these dimensions:
|
|
159
|
+
|
|
160
|
+
1. **Common themes** — what idea, framing, or concept is repeating across multiple quotes? Be specific: "three guests describe X as a 'phase transition'" beats "people are excited about X".
|
|
161
|
+
2. **Temporal trend** — is the conversation accelerating, shifting, or fading? Are recent quotes saying something different from older ones in the same set?
|
|
162
|
+
3. **Notable podcasts or episodes** — which shows are over-represented? An over-representation often signals which community is most engaged with the topic. (You cannot identify individual speakers, but you can identify which podcasts the topic clusters in.)
|
|
163
|
+
4. **Disagreements** — do quotes contradict each other? Where are the live debates?
|
|
164
|
+
5. **What's missing** — what obvious angle, counter-argument, or stakeholder voice is conspicuously absent from the returned results? Gaps are signals too.
|
|
165
|
+
|
|
166
|
+
What to avoid:
|
|
167
|
+
|
|
168
|
+
- Generic observations like "people are excited about AI" — the user could write that themselves.
|
|
169
|
+
- Restating individual quotes — the user already sees the quotes above.
|
|
170
|
+
- Confident claims about who said what — the API does not return speaker names; do not invent attribution.
|
|
171
|
+
- Bullet points that exceed one sentence — the goal is dense pattern-recognition, not paragraphs.
|
|
172
|
+
|
|
173
|
+
## Error handling
|
|
174
|
+
|
|
175
|
+
The CLI uses stable exit codes so you can branch on the failure mode:
|
|
176
|
+
|
|
177
|
+
| Exit code | Meaning | What to tell the user |
|
|
178
|
+
|---|---|---|
|
|
179
|
+
| `0` | Success | Render the results normally |
|
|
180
|
+
| `1` | Usage error / invalid arguments / API key rejected | Surface the stderr message verbatim — it will be a clear actionable error |
|
|
181
|
+
| `2` | Daily quota exhausted | "Daily search quota reached. Anonymous tier resets at 00:00 UTC; for 50 searches/day, set `ASKAIPODS_API_KEY` (sign up at https://podlens.net)." |
|
|
182
|
+
| `3` | Network error / podlens.net unavailable | Retry once after a brief pause; if it fails again, tell the user "PodLens search is temporarily unavailable. Try again in a few minutes." |
|
|
183
|
+
|
|
184
|
+
If the `results` array is empty (zero matches above the similarity threshold), say so explicitly: "No quotes found for that topic. The corpus is AI-focused — for non-AI topics, try a web search instead. For AI topics, try rephrasing or broadening the query." Do not invent quotes to fill the gap.
|
|
185
|
+
|
|
186
|
+
Never silently swallow an error. Never fabricate quotes when the API returns nothing.
|
|
187
|
+
|
|
188
|
+
## Honest limitations to set user expectations
|
|
189
|
+
|
|
190
|
+
- **No speaker attribution.** The API returns "podcast + episode + quote text" but not "who said it". The upstream pipeline avoids per-speaker attribution because automatic speaker diarization is unreliable — surfacing wrong attribution would be worse than no attribution.
|
|
191
|
+
- **No episode URLs.** The public API does not expose direct links to episodes. Users who want to listen will need to search the podcast and episode title in their podcast app of choice.
|
|
192
|
+
- **AI-focused corpus.** Coverage is dense for AI research, ML engineering, AI investing, and AI policy. Coverage for unrelated topics is sparse and noisy.
|
|
193
|
+
- **Short quote excerpts, not transcripts.** Each result is one extracted "key point" from an episode, typically 1-3 sentences (anonymous tier truncates further). For long-form context, the user will need to listen.
|
|
194
|
+
|
|
195
|
+
These limitations are not bugs — surfacing them honestly is better than the user discovering them mid-task and losing trust.
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// CLI entry point — argument parsing, format auto-detection, dispatch.
|
|
2
|
+
//
|
|
3
|
+
// Why it's structured this way:
|
|
4
|
+
// - parseArgs (Node 18+ stdlib) keeps the package zero-dependency,
|
|
5
|
+
// which matters for `npx askaipods` cold-start time and for the
|
|
6
|
+
// skill story (zero install footprint beyond Node itself).
|
|
7
|
+
// - Format auto-detects from `process.stdout.isTTY`: a human running
|
|
8
|
+
// `askaipods "..."` in a terminal gets markdown, a host agent that
|
|
9
|
+
// pipes the output gets JSON. SKILL.md still tells the agent to
|
|
10
|
+
// pass `--format json` explicitly so the contract isn't load-bearing
|
|
11
|
+
// on isTTY behavior across shells.
|
|
12
|
+
|
|
13
|
+
import { parseArgs } from "node:util";
|
|
14
|
+
import { search, AskaipodsError } from "./client.js";
|
|
15
|
+
import { renderJson, renderMarkdown } from "./format.js";
|
|
16
|
+
|
|
17
|
+
const VERSION = "0.1.0";
|
|
18
|
+
|
|
19
|
+
const HELP_TEXT = `askaipods ${VERSION} — search AI podcast quotes by topic
|
|
20
|
+
|
|
21
|
+
USAGE:
|
|
22
|
+
askaipods <query>
|
|
23
|
+
askaipods search <query> [options]
|
|
24
|
+
|
|
25
|
+
OPTIONS:
|
|
26
|
+
--format <json|markdown> Output format (default: markdown if TTY, json if piped)
|
|
27
|
+
--days <N> Only return results from the last N days (max 7 for anonymous tier; member tier accepts any value)
|
|
28
|
+
--api-key <key> PodLens API key (overrides ASKAIPODS_API_KEY env var)
|
|
29
|
+
-h, --help Show this message
|
|
30
|
+
-v, --version Show version
|
|
31
|
+
|
|
32
|
+
ENVIRONMENT:
|
|
33
|
+
ASKAIPODS_API_KEY PodLens API key. Without it: 10 searches/day per IP (anonymous).
|
|
34
|
+
With it: 50 searches/day per user (member).
|
|
35
|
+
Sign up at https://podlens.net to get one.
|
|
36
|
+
|
|
37
|
+
EXIT CODES:
|
|
38
|
+
0 success
|
|
39
|
+
1 usage error / invalid arguments / API key rejected
|
|
40
|
+
2 daily quota exhausted
|
|
41
|
+
3 network error / podlens.net unavailable
|
|
42
|
+
|
|
43
|
+
EXAMPLES:
|
|
44
|
+
askaipods "what are people saying about test-time compute"
|
|
45
|
+
askaipods search "Anthropic safety research" --days 30
|
|
46
|
+
askaipods "GPU shortage" --format json | jq .results
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
function usageError(message) {
|
|
50
|
+
const err = new AskaipodsError(`${message}\n\nRun 'askaipods --help' for usage.`, 1);
|
|
51
|
+
return err;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function run(argv) {
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = parseArgs({
|
|
58
|
+
args: argv,
|
|
59
|
+
options: {
|
|
60
|
+
format: { type: "string" },
|
|
61
|
+
days: { type: "string" },
|
|
62
|
+
"api-key": { type: "string" },
|
|
63
|
+
help: { type: "boolean", short: "h" },
|
|
64
|
+
version: { type: "boolean", short: "v" },
|
|
65
|
+
},
|
|
66
|
+
allowPositionals: true,
|
|
67
|
+
strict: true,
|
|
68
|
+
});
|
|
69
|
+
} catch (err) {
|
|
70
|
+
throw usageError(err?.message ?? "could not parse arguments");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const { values, positionals } = parsed;
|
|
74
|
+
|
|
75
|
+
if (values.help) {
|
|
76
|
+
process.stdout.write(HELP_TEXT);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (values.version) {
|
|
80
|
+
process.stdout.write(`askaipods ${VERSION}\n`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Allow `askaipods search "query"` or `askaipods "query"`. The
|
|
85
|
+
// `search` subcommand is purely a usability hint — there is only one
|
|
86
|
+
// operation today and adding it as a flag-free first positional means
|
|
87
|
+
// future subcommands (e.g., `askaipods quota`) won't break the v0
|
|
88
|
+
// muscle memory.
|
|
89
|
+
let query;
|
|
90
|
+
if (positionals[0] === "search") {
|
|
91
|
+
query = positionals.slice(1).join(" ").trim();
|
|
92
|
+
} else {
|
|
93
|
+
query = positionals.join(" ").trim();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!query) {
|
|
97
|
+
throw usageError("missing query");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let days;
|
|
101
|
+
if (values.days !== undefined) {
|
|
102
|
+
const n = Number.parseInt(values.days, 10);
|
|
103
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
104
|
+
throw usageError("--days must be a non-negative integer");
|
|
105
|
+
}
|
|
106
|
+
days = n;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const format = values.format ?? (process.stdout.isTTY ? "markdown" : "json");
|
|
110
|
+
if (format !== "json" && format !== "markdown") {
|
|
111
|
+
throw usageError(`--format must be 'json' or 'markdown', got '${format}'`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const apiKey = values["api-key"] ?? process.env.ASKAIPODS_API_KEY;
|
|
115
|
+
|
|
116
|
+
const response = await search({ query, days, apiKey });
|
|
117
|
+
|
|
118
|
+
const output = format === "json" ? renderJson(query, response) : renderMarkdown(query, response);
|
|
119
|
+
|
|
120
|
+
process.stdout.write(output);
|
|
121
|
+
if (!output.endsWith("\n")) process.stdout.write("\n");
|
|
122
|
+
}
|
package/src/client.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Thin client for podlens.net /api/search/semantic.
|
|
2
|
+
//
|
|
3
|
+
// Lives in its own file so the CLI layer never has to know about HTTP
|
|
4
|
+
// or status codes — it gets back either a parsed response object or a
|
|
5
|
+
// thrown error carrying a stable exitCode (1=usage, 2=quota, 3=network).
|
|
6
|
+
// That separation lets the test suite mock the network at this seam
|
|
7
|
+
// instead of patching `fetch` globally.
|
|
8
|
+
|
|
9
|
+
const PODLENS_ENDPOINT = "https://podlens.net/api/search/semantic";
|
|
10
|
+
|
|
11
|
+
// Hard caps mirror the server's own validation in
|
|
12
|
+
// functions/api/search/semantic.ts so the user gets a fast local error
|
|
13
|
+
// instead of a confusing 400 round-trip.
|
|
14
|
+
const MAX_QUERY_LEN = 300;
|
|
15
|
+
const MIN_QUERY_LEN = 1;
|
|
16
|
+
|
|
17
|
+
export class AskaipodsError extends Error {
|
|
18
|
+
constructor(message, exitCode) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "AskaipodsError";
|
|
21
|
+
this.exitCode = exitCode;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function exitErr(code, message) {
|
|
26
|
+
return new AskaipodsError(message, code);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function search({ query, days, apiKey, endpoint = PODLENS_ENDPOINT }) {
|
|
30
|
+
if (typeof query !== "string" || query.trim().length < MIN_QUERY_LEN) {
|
|
31
|
+
throw exitErr(1, "query is required (1-300 characters)");
|
|
32
|
+
}
|
|
33
|
+
if (query.length > MAX_QUERY_LEN) {
|
|
34
|
+
throw exitErr(1, `query too long (max ${MAX_QUERY_LEN} characters)`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const headers = {
|
|
38
|
+
"Content-Type": "application/json",
|
|
39
|
+
"User-Agent": "askaipods/0.1.0 (+https://github.com/Delibread0601/askaipods)",
|
|
40
|
+
};
|
|
41
|
+
if (apiKey) {
|
|
42
|
+
headers["X-PodLens-API-Key"] = apiKey;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const body = { q: query };
|
|
46
|
+
if (typeof days === "number" && days > 0) {
|
|
47
|
+
body.days = days;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let response;
|
|
51
|
+
try {
|
|
52
|
+
response = await fetch(endpoint, {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers,
|
|
55
|
+
body: JSON.stringify(body),
|
|
56
|
+
});
|
|
57
|
+
} catch (err) {
|
|
58
|
+
// fetch() throws TypeError on DNS / connection failure / abort.
|
|
59
|
+
// Treat all of these as exit code 3 (transient/network) so the
|
|
60
|
+
// SKILL.md can advise "retry in a moment" instead of looking like
|
|
61
|
+
// a usage error.
|
|
62
|
+
throw exitErr(3, `network error contacting podlens.net: ${err?.message ?? err}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The server always responds with JSON for both success and error
|
|
66
|
+
// paths (see jsonResponse() in functions/api/search/semantic.ts), so
|
|
67
|
+
// a non-JSON body means an upstream proxy/CDN is in the way.
|
|
68
|
+
let data;
|
|
69
|
+
try {
|
|
70
|
+
data = await response.json();
|
|
71
|
+
} catch {
|
|
72
|
+
throw exitErr(
|
|
73
|
+
3,
|
|
74
|
+
`unexpected non-JSON response from podlens.net (HTTP ${response.status}). ` +
|
|
75
|
+
"An upstream proxy may be interfering — retry in a moment.",
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (response.ok) {
|
|
80
|
+
return data;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Distinguish 429 cases by inspecting the message: the server uses
|
|
84
|
+
// distinct strings for "burst limit hit" vs "daily quota exhausted",
|
|
85
|
+
// and only the latter warrants telling the user about API keys.
|
|
86
|
+
if (response.status === 429) {
|
|
87
|
+
const msg = String(data?.error ?? "").toLowerCase();
|
|
88
|
+
if (msg.includes("quota")) {
|
|
89
|
+
throw exitErr(
|
|
90
|
+
2,
|
|
91
|
+
"daily search quota exhausted. Anonymous tier resets at 00:00 UTC. " +
|
|
92
|
+
"For 50 searches/day, set ASKAIPODS_API_KEY (sign up at https://podlens.net).",
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
throw exitErr(3, "rate limited by podlens.net (too many requests in a short window). Retry in a minute.");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (response.status === 401 || response.status === 403) {
|
|
99
|
+
throw exitErr(1, `API key rejected: ${data?.error ?? response.statusText}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (response.status === 413) {
|
|
103
|
+
throw exitErr(1, "request body too large (max 2 KB). Shorten the query.");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (response.status === 503) {
|
|
107
|
+
throw exitErr(3, `podlens.net temporarily unavailable: ${data?.error ?? "service unavailable"}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (response.status === 400) {
|
|
111
|
+
throw exitErr(1, `invalid request: ${data?.error ?? "bad request"}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
throw exitErr(3, `podlens.net error (HTTP ${response.status}): ${data?.error ?? response.statusText}`);
|
|
115
|
+
}
|
package/src/format.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Output formatting for askaipods.
|
|
2
|
+
//
|
|
3
|
+
// Two render targets:
|
|
4
|
+
// - JSON → consumed by host agents (Claude Code, Codex, etc.) that
|
|
5
|
+
// will parse the structured payload and reformat it per the
|
|
6
|
+
// instructions in SKILL.md
|
|
7
|
+
// - Markdown → consumed by humans running the CLI directly in a
|
|
8
|
+
// terminal; not used by the agent path
|
|
9
|
+
//
|
|
10
|
+
// Both share the same sort: time descending (newest first). The host
|
|
11
|
+
// agent's job is to optionally split that into "Latest" and "Top
|
|
12
|
+
// Relevant" sub-views — see SKILL.md.
|
|
13
|
+
|
|
14
|
+
const ANONYMOUS_NOTE =
|
|
15
|
+
"Anonymous tier: 10 randomized results from top 20, text truncated by rank, " +
|
|
16
|
+
"dates fuzzed to month. Set ASKAIPODS_API_KEY for 50 searches/day with full text and dates.";
|
|
17
|
+
|
|
18
|
+
// Lexical compare on YYYY-MM[-DD][THH:MM:SSZ] descending puts newest
|
|
19
|
+
// first regardless of whether the date is a full ISO timestamp (member
|
|
20
|
+
// tier) or a YYYY-MM month-prefix (anonymous tier). Nulls sort to the
|
|
21
|
+
// end so absent-date results don't crowd out the dated ones.
|
|
22
|
+
export function sortByDateDesc(items) {
|
|
23
|
+
return [...items].sort((a, b) => {
|
|
24
|
+
const ad = a?.published_at ?? "";
|
|
25
|
+
const bd = b?.published_at ?? "";
|
|
26
|
+
if (ad === bd) return 0;
|
|
27
|
+
if (!ad) return 1;
|
|
28
|
+
if (!bd) return -1;
|
|
29
|
+
return bd < ad ? -1 : 1;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Build the structured payload an agent will parse. Each result keeps
|
|
34
|
+
// its `api_rank` (1 = most semantically relevant in API order) so the
|
|
35
|
+
// SKILL.md can tell the agent to derive a "Top Relevant" sub-view for
|
|
36
|
+
// member tier without re-querying. For anonymous tier api_rank reflects
|
|
37
|
+
// only the relative order within a randomized subset, not the corpus
|
|
38
|
+
// rank — `render_hint` flags that distinction.
|
|
39
|
+
//
|
|
40
|
+
// `tier` defaults to "anonymous" rather than "unknown" if the upstream
|
|
41
|
+
// response is missing the field, so the SKILL.md tier branch (which
|
|
42
|
+
// only documents `anonymous` and `member`) always lands on a documented
|
|
43
|
+
// path. Anonymous is the safer default because it disables the
|
|
44
|
+
// "Top Relevant" view — better to under-promise relevance ranking than
|
|
45
|
+
// to render a misleading section based on randomized data.
|
|
46
|
+
export function toStructured(query, response) {
|
|
47
|
+
const tier = response?.meta?.tier ?? "anonymous";
|
|
48
|
+
const apiResults = Array.isArray(response?.results) ? response.results : [];
|
|
49
|
+
|
|
50
|
+
const withRank = apiResults.map((r, idx) => ({ ...r, api_rank: idx + 1 }));
|
|
51
|
+
const sorted = sortByDateDesc(withRank);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
tier,
|
|
55
|
+
query,
|
|
56
|
+
fetched_at: new Date().toISOString(),
|
|
57
|
+
render_hint: tier === "member" ? "dual_view" : "single_view",
|
|
58
|
+
results: sorted.map((r) => ({
|
|
59
|
+
podcast: r.podcast_name ?? null,
|
|
60
|
+
episode: r.episode_title ?? null,
|
|
61
|
+
date: r.published_at ?? null,
|
|
62
|
+
text: r.text ?? "",
|
|
63
|
+
api_rank: r.api_rank,
|
|
64
|
+
})),
|
|
65
|
+
meta: {
|
|
66
|
+
total_returned: typeof response?.total === "number" ? response.total : apiResults.length,
|
|
67
|
+
quota: response?.meta?.quota ?? null,
|
|
68
|
+
restrictions: response?.meta?.restrictions ?? null,
|
|
69
|
+
query_hash: response?.meta?.query_hash ?? null,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function renderJson(query, response) {
|
|
75
|
+
return JSON.stringify(toStructured(query, response), null, 2);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function renderMarkdown(query, response) {
|
|
79
|
+
const data = toStructured(query, response);
|
|
80
|
+
const lines = [];
|
|
81
|
+
|
|
82
|
+
lines.push(`# askaipods · "${query}"`);
|
|
83
|
+
lines.push("");
|
|
84
|
+
|
|
85
|
+
const quota = data.meta.quota;
|
|
86
|
+
const tierLabel = data.tier;
|
|
87
|
+
const quotaLabel = quota
|
|
88
|
+
? `${quota.used}/${quota.limit} ${quota.period ?? "daily"}`
|
|
89
|
+
: "unknown";
|
|
90
|
+
lines.push(`*Tier: ${tierLabel} · Results: ${data.results.length} · Quota: ${quotaLabel}*`);
|
|
91
|
+
lines.push("");
|
|
92
|
+
|
|
93
|
+
if (data.results.length === 0) {
|
|
94
|
+
lines.push("No results found. Try a different phrasing or broader topic.");
|
|
95
|
+
if (data.tier === "anonymous") {
|
|
96
|
+
lines.push("");
|
|
97
|
+
lines.push(`> ${ANONYMOUS_NOTE}`);
|
|
98
|
+
}
|
|
99
|
+
return lines.join("\n");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
lines.push("## Results — newest first");
|
|
103
|
+
lines.push("");
|
|
104
|
+
|
|
105
|
+
for (let i = 0; i < data.results.length; i++) {
|
|
106
|
+
const r = data.results[i];
|
|
107
|
+
const title = r.episode ?? "Untitled episode";
|
|
108
|
+
const podcast = r.podcast ?? "Unknown podcast";
|
|
109
|
+
const date = r.date ?? "date unknown";
|
|
110
|
+
lines.push(`### ${i + 1}. ${podcast} — ${title}`);
|
|
111
|
+
lines.push(`*${date}*`);
|
|
112
|
+
lines.push("");
|
|
113
|
+
// Quote-block the text and collapse newlines so the markdown stays compact.
|
|
114
|
+
const text = (r.text ?? "").replace(/\s+/g, " ").trim();
|
|
115
|
+
lines.push(`> ${text}`);
|
|
116
|
+
lines.push("");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (data.tier === "anonymous") {
|
|
120
|
+
lines.push("---");
|
|
121
|
+
lines.push(`*${ANONYMOUS_NOTE}*`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return lines.join("\n");
|
|
125
|
+
}
|