askaipods 0.2.1 → 0.2.3
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/package.json +1 -1
- package/skill/askaipods/SKILL.md +21 -3
- package/src/cli.js +1 -1
- package/src/client.js +3 -2
- package/src/format.js +22 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "askaipods",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Search AI podcast quotes by topic — recent episode excerpts from Lex Fridman, Dwarkesh Patel, No Priors, Latent Space, and dozens more. 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
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/skill/askaipods/SKILL.md
CHANGED
|
@@ -21,6 +21,7 @@ Trigger eagerly. The Anthropic skill best-practices warn that models tend to **u
|
|
|
21
21
|
- "Who is discussing <model / company / paper / concept>?"
|
|
22
22
|
- "What are VCs / researchers / founders saying about <X>?"
|
|
23
23
|
- "Has anyone on a podcast talked about <X>?"
|
|
24
|
+
- "What does <person> think about <X>?" / "<人名>怎么看<X>?" — invoke even though the API does not return speaker attribution. The semantic search will still find quotes from episodes featuring that person; you just cannot confirm who in the episode said each line (see Honest limitations below).
|
|
24
25
|
- 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
|
|
|
26
27
|
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.
|
|
@@ -40,6 +41,8 @@ Run the bundled CLI and pass `--format json`. The flag matters because without i
|
|
|
40
41
|
npx askaipods search "<USER QUERY>" --format json
|
|
41
42
|
```
|
|
42
43
|
|
|
44
|
+
The `search` subcommand is optional — `npx askaipods "<USER QUERY>" --format json` works identically. Both forms are supported; use whichever reads better in context.
|
|
45
|
+
|
|
43
46
|
The package is published on npm as `askaipods`, so `npx` will resolve it regardless of whether the user has it installed globally. If `npx` is unavailable in the host environment, the user can install globally once with `npm install -g askaipods` and the skill will run the same command.
|
|
44
47
|
|
|
45
48
|
To restrict to recent episodes only, add `--days N`. When `--days` is passed, the API clamps the value to a maximum of 90 for anonymous tier (member tier accepts any value). When `--days` is omitted entirely, there is no time filter — the API returns all-time results.
|
|
@@ -48,6 +51,14 @@ To restrict to recent episodes only, add `--days N`. When `--days` is passed, th
|
|
|
48
51
|
npx askaipods search "<USER QUERY>" --days 90 --format json
|
|
49
52
|
```
|
|
50
53
|
|
|
54
|
+
To authenticate with a PodLens API key (member tier), pass `--api-key <key>` or set the `ASKAIPODS_API_KEY` environment variable. The flag takes priority over the env var when both are present.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npx askaipods search "<USER QUERY>" --api-key pk_abc123... --format json
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The query must be 1–300 characters after trimming. Longer queries are rejected locally (exit code 1) before reaching the API.
|
|
61
|
+
|
|
51
62
|
### Time-intent mapping (important)
|
|
52
63
|
|
|
53
64
|
When the user's query implies a time window, you MUST pass the appropriate `--days` value. Without it, the API returns all-time results regardless of recency words in the query.
|
|
@@ -87,7 +98,8 @@ Do NOT silently default every query to `--days 90` — omitting `--days` on broa
|
|
|
87
98
|
"total_returned": 20,
|
|
88
99
|
"quota": { "used": 3, "limit": 50, "period": "daily" },
|
|
89
100
|
"restrictions": null,
|
|
90
|
-
"query_hash": "..."
|
|
101
|
+
"query_hash": "...",
|
|
102
|
+
"window": { "requested_days": 7, "served_days": 30, "expanded": true, "reason_code": "expanded_on_empty_window" }
|
|
91
103
|
}
|
|
92
104
|
}
|
|
93
105
|
```
|
|
@@ -95,12 +107,14 @@ Do NOT silently default every query to `--days 90` — omitting `--days` on broa
|
|
|
95
107
|
Field notes that affect how you render:
|
|
96
108
|
|
|
97
109
|
- **`tier`** — `member` if the user has a valid API key, `anonymous` otherwise. Drives the rendering branch below. On exit `0`, `tier` is always one of these two values — there is no third "unknown" path to handle (the CLI validates the upstream response and exits `3` if the value is missing or unexpected).
|
|
110
|
+
- **`fetched_at`** — ISO-8601 timestamp set by the CLI at request time (not by the server). Use it for staleness: if the user asks about the same topic again later in the session, compare `fetched_at` against the current time to decide whether to re-query or reuse the cached output. A reasonable freshness threshold is ~30 minutes for time-sensitive queries and ~2 hours for broad research.
|
|
98
111
|
- **`render_hint`** — `dual_view` for member, `single_view` for anonymous. Honor this. The reason: anonymous results are sorted by `published_at` desc (newest-first) by the API, so `api_rank` reflects temporal order, not semantic relevance. Showing a "Top Most Relevant" section for anonymous tier would mislead the user. Member results arrive in similarity order, so `api_rank` is meaningful for relevance-based views.
|
|
99
112
|
- **`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.
|
|
100
113
|
- **`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.
|
|
101
114
|
- **`results[].date` format** — `YYYY-MM-DD` (or full ISO timestamp) for member tier; `YYYY-MM` only for anonymous tier (deliberately fuzzed by the API). Display whatever you got — don't guess a day.
|
|
102
115
|
- **`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.
|
|
103
116
|
- **`meta.restrictions`** — `null` for member tier; for anonymous tier, an object describing the cap (e.g., `{ max_results: 20, text_truncated: false, results_randomized: false, date_precision: "month", max_days: 90, order: "published_at_desc" }`). 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.
|
|
117
|
+
- **`meta.window`** — present when the API includes window expansion metadata (may be `null` for older server versions). When the user passes `--days` and the requested window has no results, the API automatically retries with wider windows (`[30, 60, 90]` days). The `window` object contains: `requested_days` (what was asked), `served_days` (what actually returned results), `expanded` (boolean — `true` when the window was widened), `reason_code` (`"expanded_on_empty_window"` when expanded), and optionally `truncated` (`true` when a fallback query errored mid-expansion). **When `expanded` is `true`**, tell the user: "No results in the requested N-day window; showing results from the last M days" (using `requested_days` and `served_days`). When `expanded` is `false` and results are empty, the API tried all available windows and genuinely found nothing.
|
|
104
118
|
- **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.
|
|
105
119
|
|
|
106
120
|
## How to render the response
|
|
@@ -191,11 +205,15 @@ The CLI uses stable exit codes so you can branch on the failure mode:
|
|
|
191
205
|
| Exit code | Meaning | What to tell the user |
|
|
192
206
|
|---|---|---|
|
|
193
207
|
| `0` | Success | Render the results normally |
|
|
194
|
-
| `1` | Usage error / invalid arguments / API key rejected | Surface the stderr message verbatim — it will be a clear actionable error |
|
|
208
|
+
| `1` | Usage error / invalid arguments / API key rejected | Surface the stderr message verbatim — it will be a clear actionable error. Common causes: query exceeds 300 characters (shorten it), empty query, or API key rejected by the server. |
|
|
195
209
|
| `2` | Daily quota exhausted | Surface the CLI's stderr message verbatim — it is already tier-aware (distinct copy for member vs anonymous) and includes the correct reset time and upgrade path. |
|
|
196
210
|
| `3` | Transient or unexpected failure (network error, rate-limit burst, service 503, protocol/shape error, or internal exception) | Retry once after a brief pause. If it fails again, surface the CLI's stderr message verbatim — it distinguishes "rate limited, retry in a minute" from "podlens.net temporarily unavailable" from "unexpected response shape" from internal exceptions, so the user sees the actionable detail. |
|
|
197
211
|
|
|
198
|
-
If the `results` array is empty (zero matches above the similarity threshold),
|
|
212
|
+
If the `results` array is empty (zero matches above the similarity threshold), check `meta.window` first:
|
|
213
|
+
- If `meta.window.expanded` is `true`: the API already widened the search window (e.g., from 7 to 30 days) and still found nothing — tell the user: "No quotes found. The API expanded the search from N to M days but found no matches. Try rephrasing or broadening the query."
|
|
214
|
+
- If `meta.window.truncated` is `true`: the expansion was interrupted by a transient error — tell the user to retry in a moment.
|
|
215
|
+
- Otherwise (no expansion, or `meta.window` is `null`): say "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."
|
|
216
|
+
Do not invent quotes to fill the gap.
|
|
199
217
|
|
|
200
218
|
Never silently swallow an error. Never fabricate quotes when the API returns nothing.
|
|
201
219
|
|
package/src/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ import { parseArgs } from "node:util";
|
|
|
14
14
|
import { search, AskaipodsError } from "./client.js";
|
|
15
15
|
import { renderJson, renderMarkdown } from "./format.js";
|
|
16
16
|
|
|
17
|
-
const VERSION = "0.2.
|
|
17
|
+
const VERSION = "0.2.3";
|
|
18
18
|
|
|
19
19
|
const HELP_TEXT = `askaipods ${VERSION} — search AI podcast quotes by topic
|
|
20
20
|
|
package/src/client.js
CHANGED
|
@@ -99,7 +99,8 @@ function isValidPublishedAt(v) {
|
|
|
99
99
|
//
|
|
100
100
|
// Optional (kept loose on purpose):
|
|
101
101
|
// data.meta.quota.period, data.meta.quota.next_reset,
|
|
102
|
-
// data.meta.query_hash, data.meta.restrictions, data.meta.cta
|
|
102
|
+
// data.meta.query_hash, data.meta.restrictions, data.meta.cta,
|
|
103
|
+
// data.meta.window
|
|
103
104
|
function isValidSuccessEnvelope(data) {
|
|
104
105
|
if (!isPlainObject(data)) return false;
|
|
105
106
|
if (typeof data.total !== "number" || !Number.isFinite(data.total)) return false;
|
|
@@ -136,7 +137,7 @@ export async function search({ query, days, apiKey, endpoint = PODLENS_ENDPOINT
|
|
|
136
137
|
|
|
137
138
|
const headers = {
|
|
138
139
|
"Content-Type": "application/json",
|
|
139
|
-
"User-Agent": "askaipods/0.2.
|
|
140
|
+
"User-Agent": "askaipods/0.2.3 (+https://github.com/Delibread0601/askaipods)",
|
|
140
141
|
};
|
|
141
142
|
if (apiKey) {
|
|
142
143
|
headers["X-PodLens-API-Key"] = apiKey;
|
package/src/format.js
CHANGED
|
@@ -91,6 +91,7 @@ export function toStructured(query, response) {
|
|
|
91
91
|
quota: response.meta.quota,
|
|
92
92
|
restrictions: response.meta.restrictions ?? null,
|
|
93
93
|
query_hash: response.meta.query_hash ?? null,
|
|
94
|
+
window: response.meta.window ?? null,
|
|
94
95
|
},
|
|
95
96
|
};
|
|
96
97
|
}
|
|
@@ -115,7 +116,18 @@ export function renderMarkdown(query, response) {
|
|
|
115
116
|
lines.push("");
|
|
116
117
|
|
|
117
118
|
if (data.results.length === 0) {
|
|
118
|
-
|
|
119
|
+
const win = data.meta.window;
|
|
120
|
+
if (win && win.expanded) {
|
|
121
|
+
lines.push(
|
|
122
|
+
`No results found. The API expanded the search window from ${win.requested_days} to ${win.served_days} days but still found no matches. Try a different phrasing or broader topic.`,
|
|
123
|
+
);
|
|
124
|
+
} else if (win && win.truncated) {
|
|
125
|
+
lines.push(
|
|
126
|
+
"No results found (search window expansion was interrupted by a transient error). Try again in a moment, or try a different phrasing.",
|
|
127
|
+
);
|
|
128
|
+
} else {
|
|
129
|
+
lines.push("No results found. Try a different phrasing or broader topic.");
|
|
130
|
+
}
|
|
119
131
|
if (data.tier === "anonymous") {
|
|
120
132
|
lines.push("");
|
|
121
133
|
lines.push(`> ${ANONYMOUS_NOTE}`);
|
|
@@ -123,6 +135,15 @@ export function renderMarkdown(query, response) {
|
|
|
123
135
|
return lines.join("\n");
|
|
124
136
|
}
|
|
125
137
|
|
|
138
|
+
// Surface window expansion so the user knows the actual time range
|
|
139
|
+
const win = data.meta.window;
|
|
140
|
+
if (win && win.expanded) {
|
|
141
|
+
lines.push(
|
|
142
|
+
`*Note: No results in the requested ${win.requested_days}-day window; showing results from the last ${win.served_days} days.*`,
|
|
143
|
+
);
|
|
144
|
+
lines.push("");
|
|
145
|
+
}
|
|
146
|
+
|
|
126
147
|
lines.push("## Results — newest first");
|
|
127
148
|
lines.push("");
|
|
128
149
|
|