yt-briefing 0.6.0 → 0.8.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/.claude/skills/yt-search/SKILL.md +1 -1
- package/README.md +29 -24
- package/dist/bootstrap.js +14 -58
- package/dist/lib/env.js +38 -0
- package/dist/lib/llm.js +10 -9
- package/dist/lib/paths.js +7 -0
- package/dist/lib/yt-api.js +2 -2
- package/dist/yt-channel-pending.js +3 -3
- package/dist/yt-channel-videos.js +2 -3
- package/dist/yt-rating.js +3 -3
- package/dist/yt-search.js +20 -10
- package/dist/yt-sweep.js +11 -9
- package/dist/yt-transcript.js +3 -3
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: yt-search
|
|
3
3
|
description: Search WITHIN one YouTube channel by intent — name a channel and what you're after; the engine lists that channel's uploads, ranks them against your intent (metadata only, no transcript yet), then lazily yields ONE matching video at a time with a rich summary. You keep or skip each; at the end it synthesizes a comparison from everything you kept. Channel-scoped, not whole-YouTube. Same transcript engine + proxy as /yt; lazy on purpose (no transcript bursts → no IP block). Summaries and prompts use the language chosen at onboarding.
|
|
4
|
-
argument-hint: A channel (@handle or URL) and a descriptive intent, e.g. "@t3dotgg which terminal for AI coding". Optional --
|
|
4
|
+
argument-hint: A channel (@handle or URL) and a descriptive intent, e.g. "@t3dotgg which terminal for AI coding". Optional --top N (default 10), --scan N, --since YYYY-MM-DD.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## How it works
|
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# yt-briefing
|
|
2
2
|
|
|
3
|
-
Save hours on YouTube. yt-briefing watches the channels you follow so you don't have to.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
of everything and only watch what's actually worth it.
|
|
3
|
+
Save hours on YouTube. yt-briefing watches the channels you follow so you don't have to. For
|
|
4
|
+
each new video it distills the essence in your own language — every point that matters, with
|
|
5
|
+
only the filler cut, so nothing important is lost. Reading it takes a fraction of the time the
|
|
6
|
+
video would, so you stay on top of everything and only watch what's actually worth it.
|
|
7
7
|
|
|
8
|
-
It also gets better the more you use it. You give each
|
|
8
|
+
It also gets better the more you use it. You give each briefing a quick rating, worth my time
|
|
9
9
|
or not, and from that it learns what to keep showing you and what to drop. Over time the queue
|
|
10
10
|
becomes yours: less noise, more of what you care about.
|
|
11
11
|
|
|
@@ -46,13 +46,26 @@ yarn add yt-briefing
|
|
|
46
46
|
bun add yt-briefing
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
3.
|
|
49
|
+
3. Put your keys in a `.env` at your project root — all four are required:
|
|
50
|
+
|
|
51
|
+
```ini
|
|
52
|
+
YT_BRIEFING_LLM_BASE_URL=https://generativelanguage.googleapis.com/v1beta/openai
|
|
53
|
+
YT_BRIEFING_LLM_API_KEY=<key> # free at https://aistudio.google.com/apikey
|
|
54
|
+
YT_BRIEFING_LLM_MODEL=gemini-2.5-flash
|
|
55
|
+
YT_BRIEFING_YOUTUBE_API_KEY=<key> # console.cloud.google.com → enable "YouTube Data API v3"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Any OpenAI-compatible endpoint works — see [Providers](#providers) to use OpenRouter, OpenAI, or a
|
|
59
|
+
local Ollama instead of Gemini. Miss a key and the engine tells you exactly which one. Keep `.env`
|
|
60
|
+
gitignored; `YT_BRIEFING_PROXY` (datacenter/VPS IPs) is the only optional extra.
|
|
61
|
+
|
|
62
|
+
4. Onboard:
|
|
50
63
|
|
|
51
64
|
```bash
|
|
52
65
|
npx yt-briefing init # or: bunx yt-briefing init
|
|
53
66
|
```
|
|
54
67
|
|
|
55
|
-
`init` asks for your language,
|
|
68
|
+
`init` asks for your language, the channels to follow, and which tool runs `/yt`.
|
|
56
69
|
|
|
57
70
|
Add or remove channels anytime:
|
|
58
71
|
|
|
@@ -79,24 +92,15 @@ npx yt-briefing transcribe <url-or-id> --lang auto # prints the transcript to
|
|
|
79
92
|
|
|
80
93
|
## Search within a channel
|
|
81
94
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
`@t3dotgg which terminal for AI coding`. It lists that channel's uploads, ranks them against your
|
|
85
|
-
intent (titles/descriptions — no transcripts yet, matching is **descriptive, not exact keywords**),
|
|
86
|
-
then hands you **one matching video at a time** with a rich summary; you **Keep** or **Skip** each.
|
|
87
|
-
At the end it synthesizes a **comparison** from everything you kept.
|
|
88
|
-
|
|
89
|
-
It's channel-scoped on purpose (you choose where to look) and lazy — one transcript per step, never
|
|
90
|
-
a burst (a burst gets your IP blocked, same as `/yt`). Summaries and the comparison use the
|
|
91
|
-
language you chose at setup.
|
|
95
|
+
Mine one channel's videos for a topic and get a comparison. Run `/yt-search` with a channel and
|
|
96
|
+
an intent — e.g. `/yt-search @betterstack which terminal for AI coding`.
|
|
92
97
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
98
|
+
It covers the channel's **whole history** (not just recent uploads), re-ranks every upload against
|
|
99
|
+
your intent, then lazily yields one matching video at a time to keep or skip — and synthesizes a
|
|
100
|
+
comparison from everything you kept.
|
|
96
101
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
> `--since` widens by date.
|
|
102
|
+
The one flag is `--top N` — how many of the top re-ranked matches to triage (**default 10**). Raise
|
|
103
|
+
it to go deeper, lower it for a quicker pass: `/yt-search @betterstack which terminal --top 20`.
|
|
100
104
|
|
|
101
105
|
## Run it
|
|
102
106
|
|
|
@@ -119,7 +123,8 @@ YT_BRIEFING_LLM_MODEL=gemini-2.5-flash
|
|
|
119
123
|
> or switch to a paid key (enable billing, same model) to avoid it.
|
|
120
124
|
|
|
121
125
|
Want something else? Change those three lines for OpenRouter (`https://openrouter.ai/api/v1`),
|
|
122
|
-
OpenAI (`https://api.openai.com/v1`), or a local Ollama (`http://localhost:11434/v1`).
|
|
126
|
+
OpenAI (`https://api.openai.com/v1`), or a local Ollama (`http://localhost:11434/v1`). Set
|
|
127
|
+
`YT_BRIEFING_LLM_BASE_URL`, `_API_KEY`, and `_MODEL` in your root `.env` (see [Setup](#setup)).
|
|
123
128
|
|
|
124
129
|
## Why an API, not the agent's native model
|
|
125
130
|
|
package/dist/bootstrap.js
CHANGED
|
@@ -6,16 +6,17 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Asks for, and writes:
|
|
8
8
|
* 1. Output language for summaries + ratings → DATA_DIR/config.json
|
|
9
|
-
* 2.
|
|
10
|
-
* 3. The channels you follow — just a flat list of handles
|
|
9
|
+
* 2. The channels you follow — just a flat list of handles
|
|
11
10
|
* → DATA_DIR/channels.md, DATA_DIR/state.md, DATA_DIR/channels/<slug>.md
|
|
11
|
+
* 3. Which agent runs /yt — installs the skill into its skills dir
|
|
12
12
|
*
|
|
13
|
-
*
|
|
13
|
+
* It does NOT touch keys: those live in your project root .env (see README → Setup); the engine
|
|
14
|
+
* reads them at run time. Re-running is safe: it warns before overwriting existing data and bails.
|
|
14
15
|
* Everything it writes is plain Markdown / JSON you can also edit by hand afterwards.
|
|
15
16
|
*/
|
|
16
17
|
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
17
18
|
import { join } from 'node:path';
|
|
18
|
-
import { DATA_DIR, BASE_DIR, PKG_ROOT, CHANNELS_DIR, CHANNELS_MD, STATE_MD, CONFIG_JSON,
|
|
19
|
+
import { DATA_DIR, BASE_DIR, PKG_ROOT, CHANNELS_DIR, CHANNELS_MD, STATE_MD, CONFIG_JSON, profilePath, } from "./lib/paths.js";
|
|
19
20
|
import { AGENTS, installSkills, projectSkillsRoot, customSkillsRootDefault, isPackageDevCwd } from "./lib/skill-install.js";
|
|
20
21
|
import { question } from "./lib/prompt.js";
|
|
21
22
|
import { normalizeHandle, slugify, serializeChannels, serializeState, profileBody, baselineStateRow } from "./lib/channels.js";
|
|
@@ -39,47 +40,15 @@ function main() {
|
|
|
39
40
|
}
|
|
40
41
|
console.log('');
|
|
41
42
|
}
|
|
43
|
+
// Keys are NOT asked here — they live in your project root .env (LLM + YouTube; see README
|
|
44
|
+
// → Setup). The engine reads them at run time and fails fast naming any that are missing.
|
|
42
45
|
// 1. Language ----------------------------------------------------------------
|
|
43
46
|
console.log(' 1) Language');
|
|
44
47
|
const outputLang = ask(' Output language for summaries and ratings', 'English');
|
|
45
|
-
// 2.
|
|
46
|
-
// Pick a provider → we prefill its endpoint + a sensible model and ask ONLY for the
|
|
47
|
-
// key (with the exact link to get it). The recommended free path is the default, so
|
|
48
|
-
// pressing Enter lands on it — no long URL to paste, nothing to guess.
|
|
49
|
-
console.log('\n 2) Which AI writes the filtering + summaries? Pick a provider:\n');
|
|
50
|
-
console.log(' 1) Gemini — FREE key, best way to start · https://aistudio.google.com/apikey');
|
|
51
|
-
console.log(' 2) OpenRouter — one key for Gemini + GPT + … · https://openrouter.ai/keys');
|
|
52
|
-
console.log(' 3) OpenAI — GPT models · https://platform.openai.com/api-keys');
|
|
53
|
-
console.log(' 4) Other / local (Ollama or any custom endpoint)\n');
|
|
54
|
-
console.log(' Type 1, 2, 3 or 4 and press Enter. (Just press Enter for 1 — Gemini, recommended.)');
|
|
55
|
-
const PROVIDERS = {
|
|
56
|
-
'1': { name: 'Gemini', base: 'https://generativelanguage.googleapis.com/v1beta/openai', model: 'gemini-2.5-flash', keyUrl: 'https://aistudio.google.com/apikey' },
|
|
57
|
-
'2': { name: 'OpenRouter', base: 'https://openrouter.ai/api/v1', model: 'google/gemini-2.5-flash', keyUrl: 'https://openrouter.ai/keys' },
|
|
58
|
-
'3': { name: 'OpenAI', base: 'https://api.openai.com/v1', model: 'gpt-4o-mini', keyUrl: 'https://platform.openai.com/api-keys' },
|
|
59
|
-
};
|
|
60
|
-
let llmBaseUrl, llmModel, llmKey;
|
|
61
|
-
const picked = PROVIDERS[ask(' Your choice', '1')];
|
|
62
|
-
if (picked) {
|
|
63
|
-
console.log(`\n → ${picked.name}. Get your key here: ${picked.keyUrl}`);
|
|
64
|
-
llmKey = ask(' Paste your API key');
|
|
65
|
-
llmBaseUrl = picked.base;
|
|
66
|
-
llmModel = ask(' Model (Enter to accept)', picked.model);
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
console.log('\n → Custom / local endpoint (e.g. Ollama at http://localhost:11434/v1)');
|
|
70
|
-
llmBaseUrl = ask(' YT_BRIEFING_LLM_BASE_URL', 'http://localhost:11434/v1');
|
|
71
|
-
llmModel = ask(' YT_BRIEFING_LLM_MODEL', 'llama3.1');
|
|
72
|
-
llmKey = ask(' YT_BRIEFING_LLM_API_KEY (blank for local)', '');
|
|
73
|
-
}
|
|
74
|
-
console.log('\n 3) YouTube Data API (needed to list channel uploads)');
|
|
75
|
-
console.log(' Get a key: https://console.cloud.google.com → YouTube Data API v3');
|
|
76
|
-
const ytKey = ask(' YT_BRIEFING_YOUTUBE_API_KEY');
|
|
77
|
-
console.log('\n 4) Proxy (optional — only needed on datacenter/VPS IPs; see docs/warp-proxy.md)');
|
|
78
|
-
const ytProxy = ask(' YT_BRIEFING_PROXY (blank = direct)', '');
|
|
79
|
-
// 5. Channels ----------------------------------------------------------------
|
|
48
|
+
// 2. Channels ----------------------------------------------------------------
|
|
80
49
|
// Just collect a flat list. No categories, no per-channel rules to define up front —
|
|
81
50
|
// each channel's profile LEARNS what to skip as you rate it (## Skip titles / ## Notes).
|
|
82
|
-
console.log('\n
|
|
51
|
+
console.log('\n 2) Channels you follow');
|
|
83
52
|
console.log(' Add one per line — paste whichever form you have, all work as-is:');
|
|
84
53
|
console.log(' eg. @betterstack, betterstack or https://www.youtube.com/@betterstack');
|
|
85
54
|
console.log(' (Paste the full URL directly — it reads the @handle for you) Empty line to finish.');
|
|
@@ -105,34 +74,22 @@ function main() {
|
|
|
105
74
|
if (channels.length === 0) {
|
|
106
75
|
console.log('\n No channels added — you can add them later by editing data/channels.md.\n');
|
|
107
76
|
}
|
|
108
|
-
//
|
|
77
|
+
// 3. Coding agent ------------------------------------------------------------
|
|
109
78
|
// Place the skill INTO THIS PROJECT (the package folder you open in the agent) — never a
|
|
110
79
|
// home-global dir (that's the npm -g antipattern: machine-wide, invisible, easy to forget).
|
|
111
80
|
// SKILL.md is the cross-agent standard, so the shipped skill runs in any compatible agent —
|
|
112
81
|
// we just install it into that agent's skills dir (.claude/skills, .cursor/skills, .codex/skills).
|
|
113
82
|
// 1/2/3 = known agents; 4 = any other compatible agent (a project folder you name).
|
|
114
|
-
console.log('\n
|
|
83
|
+
console.log('\n 3) Which agent will you run /yt in? (it ships a standard Agent Skill — any compatible agent works)');
|
|
115
84
|
console.log(' 1) Claude Code 2) Cursor 3) Codex 4) Custom folder (any other agent)\n');
|
|
116
85
|
const agentKey = ask(' Your agent', '1');
|
|
117
86
|
// For a custom target, ask the folder now (keeps all prompts in the interactive block).
|
|
118
87
|
const customDir = AGENTS[agentKey] ? '' : ask(' Skills folder to install into', customSkillsRootDefault());
|
|
119
|
-
//
|
|
88
|
+
// 4. Write everything --------------------------------------------------------
|
|
120
89
|
mkdirSync(CHANNELS_DIR, { recursive: true });
|
|
121
|
-
// .
|
|
122
|
-
const envBody = [
|
|
123
|
-
`YT_BRIEFING_LLM_BASE_URL=${llmBaseUrl}`,
|
|
124
|
-
`YT_BRIEFING_LLM_API_KEY=${llmKey}`,
|
|
125
|
-
`YT_BRIEFING_LLM_MODEL=${llmModel}`,
|
|
126
|
-
`YT_BRIEFING_YOUTUBE_API_KEY=${ytKey}`,
|
|
127
|
-
`YT_BRIEFING_PROXY=${ytProxy}`,
|
|
128
|
-
'',
|
|
129
|
-
].join('\n');
|
|
130
|
-
writeFileSync(ENV_PATH, envBody, 'utf8');
|
|
131
|
-
// Secret-safety for the consume layout: drop a .gitignore inside .yt-briefing/ so .env never
|
|
132
|
-
// gets committed regardless of the host project's own ignore rules. data/ stays versionable
|
|
133
|
-
// (for sync). In a dev clone (BASE_DIR === PKG_ROOT) the repo's own .gitignore already covers it.
|
|
90
|
+
// Keep the throwaway cache out of git for the consume layout. data/ stays versionable (for sync).
|
|
134
91
|
if (BASE_DIR !== PKG_ROOT) {
|
|
135
|
-
writeFileSync(join(BASE_DIR, '.gitignore'), '
|
|
92
|
+
writeFileSync(join(BASE_DIR, '.gitignore'), 'data/.cache/\n', 'utf8');
|
|
136
93
|
}
|
|
137
94
|
// config.json
|
|
138
95
|
writeFileSync(CONFIG_JSON, JSON.stringify({ output_lang: outputLang }, null, 2) + '\n', 'utf8');
|
|
@@ -144,7 +101,6 @@ function main() {
|
|
|
144
101
|
writeFileSync(profilePath(c.slug), profileBody(c.handle, c.slug), 'utf8');
|
|
145
102
|
console.log(' ' + '─'.repeat(40));
|
|
146
103
|
console.log(` Done. Wrote:`);
|
|
147
|
-
console.log(` ${ENV_PATH}`);
|
|
148
104
|
console.log(` ${CONFIG_JSON}`);
|
|
149
105
|
console.log(` ${CHANNELS_MD}`);
|
|
150
106
|
console.log(` ${STATE_MD}`);
|
package/dist/lib/env.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Env loader + required-variable preflight — the one place that reads secrets off disk.
|
|
3
|
+
*
|
|
4
|
+
* Keys come from the project's root `.env` ONLY. There is no fallback file: dotenv loads root
|
|
5
|
+
* `.env` into process.env (anything already exported wins, since dotenv never overrides). Missing
|
|
6
|
+
* a required variable is a hard error that names exactly which one — never a silent default.
|
|
7
|
+
*
|
|
8
|
+
* Call `loadEnv()` once at every entrypoint, then `requireEnv([...])` for what that command needs.
|
|
9
|
+
*/
|
|
10
|
+
import dotenv from 'dotenv';
|
|
11
|
+
import { ROOT_ENV_PATH } from "./paths.js";
|
|
12
|
+
let done = false;
|
|
13
|
+
/** Load the project's root `.env` into process.env. The only file we read. */
|
|
14
|
+
export function loadEnv() {
|
|
15
|
+
if (done)
|
|
16
|
+
return;
|
|
17
|
+
done = true;
|
|
18
|
+
dotenv.config({ path: ROOT_ENV_PATH });
|
|
19
|
+
}
|
|
20
|
+
/** The required vars per capability — single source of truth for the preflight checks. */
|
|
21
|
+
export const REQUIRED_LLM = ['YT_BRIEFING_LLM_BASE_URL', 'YT_BRIEFING_LLM_API_KEY', 'YT_BRIEFING_LLM_MODEL'];
|
|
22
|
+
export const REQUIRED_YOUTUBE = ['YT_BRIEFING_YOUTUBE_API_KEY'];
|
|
23
|
+
/** Names from `names` that are missing or empty in the environment, in order. */
|
|
24
|
+
export function missingEnv(names) {
|
|
25
|
+
return names.filter(n => !process.env[n]);
|
|
26
|
+
}
|
|
27
|
+
/** Human-readable error for a set of missing vars — names each one and where to set it. */
|
|
28
|
+
export function missingEnvMessage(missing) {
|
|
29
|
+
const plural = missing.length > 1;
|
|
30
|
+
return `Missing required environment variable${plural ? 's' : ''}: ${missing.join(', ')}. ` +
|
|
31
|
+
`Set ${plural ? 'them' : 'it'} in your project root .env (see README → Providers → Where the keys live).`;
|
|
32
|
+
}
|
|
33
|
+
/** Throw a clear, named error if any required var is missing. */
|
|
34
|
+
export function requireEnv(names) {
|
|
35
|
+
const missing = missingEnv(names);
|
|
36
|
+
if (missing.length)
|
|
37
|
+
throw new Error(missingEnvMessage(missing));
|
|
38
|
+
}
|
package/dist/lib/llm.js
CHANGED
|
@@ -10,21 +10,22 @@
|
|
|
10
10
|
* the default — cheap and fast enough for the batch title filter, capable enough for
|
|
11
11
|
* the summaries.
|
|
12
12
|
*
|
|
13
|
-
* Env (see .env.example):
|
|
14
|
-
* YT_BRIEFING_LLM_BASE_URL
|
|
13
|
+
* Env (see .env.example) — all required, no defaults:
|
|
14
|
+
* YT_BRIEFING_LLM_BASE_URL required (e.g. https://openrouter.ai/api/v1)
|
|
15
15
|
* YT_BRIEFING_LLM_API_KEY required
|
|
16
|
-
* YT_BRIEFING_LLM_MODEL
|
|
16
|
+
* YT_BRIEFING_LLM_MODEL required (e.g. google/gemini-2.5-flash)
|
|
17
17
|
*/
|
|
18
|
-
|
|
19
|
-
const DEFAULT_MODEL = "google/gemini-2.5-flash";
|
|
18
|
+
import { requireEnv, REQUIRED_LLM } from "./env.js";
|
|
20
19
|
export function getModel() {
|
|
21
|
-
|
|
20
|
+
const model = process.env.YT_BRIEFING_LLM_MODEL;
|
|
21
|
+
if (!model)
|
|
22
|
+
throw new Error("Missing required environment variable: YT_BRIEFING_LLM_MODEL. Set it in your project root .env.");
|
|
23
|
+
return model;
|
|
22
24
|
}
|
|
23
25
|
export async function chat(prompt, opts = {}) {
|
|
24
|
-
|
|
26
|
+
requireEnv(REQUIRED_LLM);
|
|
27
|
+
const baseUrl = process.env.YT_BRIEFING_LLM_BASE_URL.replace(/\/+$/, "");
|
|
25
28
|
const apiKey = process.env.YT_BRIEFING_LLM_API_KEY;
|
|
26
|
-
if (!apiKey)
|
|
27
|
-
throw new Error("YT_BRIEFING_LLM_API_KEY not set (see .env.example)");
|
|
28
29
|
const model = opts.model || getModel();
|
|
29
30
|
const messages = [];
|
|
30
31
|
if (opts.system)
|
package/dist/lib/paths.js
CHANGED
|
@@ -33,6 +33,13 @@ export const BASE_DIR = process.env.YT_BRIEFING_BASE_DIR
|
|
|
33
33
|
: CONSUMED ? join(process.cwd(), '.yt-briefing') : PKG_ROOT;
|
|
34
34
|
process.env.YT_BRIEFING_BASE_DIR = BASE_DIR; // pin for children (their cwd differs)
|
|
35
35
|
export const ENV_PATH = join(BASE_DIR, '.env');
|
|
36
|
+
/**
|
|
37
|
+
* The project's root `.env` — the conventional, user-owned home for secrets (12-factor). When
|
|
38
|
+
* consumed, BASE_DIR is `<project>/.yt-briefing`, so the root is its parent; in a dev clone
|
|
39
|
+
* BASE_DIR === PKG_ROOT, so the root `.env` *is* ENV_PATH (one file, loaded once). Read-only:
|
|
40
|
+
* the loader never writes here, so it can't clobber the user's other variables. See lib/env.ts.
|
|
41
|
+
*/
|
|
42
|
+
export const ROOT_ENV_PATH = CONSUMED ? join(dirname(BASE_DIR), '.env') : ENV_PATH;
|
|
36
43
|
export const DATA_DIR = process.env.YT_BRIEFING_DATA_DIR
|
|
37
44
|
? resolve(process.env.YT_BRIEFING_DATA_DIR)
|
|
38
45
|
: join(BASE_DIR, 'data');
|
package/dist/lib/yt-api.js
CHANGED
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
* cost ~7s to load from a cold FS cache on every fresh process. The Data API is a
|
|
6
6
|
* trivial REST surface, so direct fetch keeps cold-start near the runtime's own startup.
|
|
7
7
|
*
|
|
8
|
-
* Auth: YT_BRIEFING_YOUTUBE_API_KEY — the entrypoint loads it (
|
|
8
|
+
* Auth: YT_BRIEFING_YOUTUBE_API_KEY — the entrypoint loads it (loadEnv() in lib/env.ts) before
|
|
9
9
|
* calling; this module only reads process.env at call time.
|
|
10
10
|
*/
|
|
11
11
|
const API = 'https://www.googleapis.com/youtube/v3';
|
|
12
12
|
function apiKey() {
|
|
13
13
|
const k = process.env.YT_BRIEFING_YOUTUBE_API_KEY;
|
|
14
14
|
if (!k)
|
|
15
|
-
throw new Error('YT_BRIEFING_YOUTUBE_API_KEY
|
|
15
|
+
throw new Error('Missing required environment variable: YT_BRIEFING_YOUTUBE_API_KEY. Set it in your project root .env (see README → Providers → Where the keys live).');
|
|
16
16
|
return k;
|
|
17
17
|
}
|
|
18
18
|
async function get(path, params) {
|
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
* Empty result is valid (channel has no new content).
|
|
13
13
|
*/
|
|
14
14
|
import { readFileSync } from 'fs';
|
|
15
|
-
import
|
|
15
|
+
import { loadEnv } from "./lib/env.js";
|
|
16
16
|
import { parseState } from "./lib/yt-lib.js";
|
|
17
|
-
import { STATE_MD
|
|
17
|
+
import { STATE_MD } from "./lib/paths.js";
|
|
18
18
|
import { fetchChannelVideos } from "./lib/yt-api.js";
|
|
19
|
-
|
|
19
|
+
loadEnv();
|
|
20
20
|
const handle = process.argv[2];
|
|
21
21
|
if (!handle) {
|
|
22
22
|
console.error('Usage: yt-channel-pending @HANDLE (internal helper)');
|
|
@@ -17,10 +17,9 @@
|
|
|
17
17
|
*
|
|
18
18
|
* Quota: ~1 unit per page (playlistItems.list) + 1 unit per 50 videos (videos.list).
|
|
19
19
|
*/
|
|
20
|
-
import
|
|
21
|
-
import { ENV_PATH } from "./lib/paths.js";
|
|
20
|
+
import { loadEnv } from "./lib/env.js";
|
|
22
21
|
import { fetchChannelVideos } from "./lib/yt-api.js";
|
|
23
|
-
|
|
22
|
+
loadEnv();
|
|
24
23
|
const args = process.argv.slice(2);
|
|
25
24
|
const handleOrId = args[0];
|
|
26
25
|
const sinceIdx = args.indexOf('--since');
|
package/dist/yt-rating.js
CHANGED
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
* bullets are de-duplicated; a state.md re-bump is a no-op.
|
|
18
18
|
*/
|
|
19
19
|
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
20
|
-
import
|
|
20
|
+
import { loadEnv } from "./lib/env.js";
|
|
21
21
|
import { parseChannels, appendSkipTitle, appendNote, bumpStatePointer } from "./lib/yt-lib.js";
|
|
22
|
-
import { CHANNELS_MD, STATE_MD, PENDING_FILE,
|
|
23
|
-
|
|
22
|
+
import { CHANNELS_MD, STATE_MD, PENDING_FILE, profilePath } from "./lib/paths.js";
|
|
23
|
+
loadEnv();
|
|
24
24
|
function getArg(args, name) {
|
|
25
25
|
const idx = args.indexOf(name);
|
|
26
26
|
return idx !== -1 && args[idx + 1] !== undefined ? args[idx + 1] : null;
|
package/dist/yt-search.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* the channel's videos by intent (no exact-keyword needed).
|
|
17
17
|
*
|
|
18
18
|
* Usage (the skill / CLI drives these; one JSON line per call):
|
|
19
|
-
* yt-search "<intent>" --channel <@handle|url> [--reset] [--
|
|
19
|
+
* yt-search "<intent>" --channel <@handle|url> [--reset] [--top N] [--scan N] [--since DATE] [--lang auto]
|
|
20
20
|
* yt-search --keep record the pending candidate, advance, yield next
|
|
21
21
|
* yt-search --skip drop the pending candidate, advance, yield next
|
|
22
22
|
* yt-search --compare synthesize a comparison from everything kept
|
|
@@ -34,18 +34,18 @@
|
|
|
34
34
|
*/
|
|
35
35
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
36
36
|
import { spawn } from 'node:child_process';
|
|
37
|
-
import
|
|
37
|
+
import { loadEnv, missingEnv, missingEnvMessage, REQUIRED_LLM, REQUIRED_YOUTUBE } from "./lib/env.js";
|
|
38
38
|
import { chat, getModel } from "./lib/llm.js";
|
|
39
39
|
import { outputLang } from "./lib/config.js";
|
|
40
40
|
import { fetchChannelVideos } from "./lib/yt-api.js";
|
|
41
41
|
import { normalizeHandle } from "./lib/channels.js";
|
|
42
|
-
import { PKG_ROOT,
|
|
43
|
-
|
|
42
|
+
import { PKG_ROOT, CACHE_DIR, SEARCH_QUEUE_FILE, SEARCH_PENDING_FILE, SEARCH_KEPT_FILE, script, } from "./lib/paths.js";
|
|
43
|
+
loadEnv();
|
|
44
44
|
mkdirSync(CACHE_DIR, { recursive: true });
|
|
45
45
|
const RUNTIME = process.execPath;
|
|
46
46
|
const LANG = outputLang();
|
|
47
47
|
const argv = process.argv.slice(2);
|
|
48
|
-
const VALUE_FLAGS = new Set(['--channel', '--
|
|
48
|
+
const VALUE_FLAGS = new Set(['--channel', '--top', '--scan', '--since', '--lang']);
|
|
49
49
|
const has = (f) => argv.includes(f);
|
|
50
50
|
const flagVal = (f) => {
|
|
51
51
|
const i = argv.indexOf(f);
|
|
@@ -72,7 +72,7 @@ const KEEP = has('--keep');
|
|
|
72
72
|
const SKIP = has('--skip');
|
|
73
73
|
const COMPARE = has('--compare');
|
|
74
74
|
const CHANNEL = flagVal('--channel');
|
|
75
|
-
const
|
|
75
|
+
const TOP = Math.max(1, parseInt(flagVal('--top') || '10', 10));
|
|
76
76
|
const SCAN = Math.max(1, parseInt(flagVal('--scan') || '50', 10)); // recent uploads to consider when no --since
|
|
77
77
|
const SINCE = flagVal('--since');
|
|
78
78
|
const LANGTRACK = flagVal('--lang') || 'auto';
|
|
@@ -224,6 +224,14 @@ async function yieldNext(queue) {
|
|
|
224
224
|
emit({ status: 'done', kept: loadKept().length });
|
|
225
225
|
}
|
|
226
226
|
async function main() {
|
|
227
|
+
// LLM is needed on every path (rerank, summaries, compare). Fail fast naming any missing var
|
|
228
|
+
// (the throw is turned into a status:"error" by the .catch below). YouTube is checked separately,
|
|
229
|
+
// only when building a fresh queue (the compare/keep/skip paths work off cache, no API).
|
|
230
|
+
{
|
|
231
|
+
const missing = missingEnv(REQUIRED_LLM);
|
|
232
|
+
if (missing.length)
|
|
233
|
+
emit({ status: 'error', error: missingEnvMessage(missing) });
|
|
234
|
+
}
|
|
227
235
|
// --compare: synthesize from kept summaries.
|
|
228
236
|
if (COMPARE) {
|
|
229
237
|
const queue = loadQueue();
|
|
@@ -255,9 +263,11 @@ async function main() {
|
|
|
255
263
|
if (existing && !RESET && (!intentArg || intentArg === existing.intent) && (!CHANNEL || normalizeHandle(CHANNEL) === existing.channel)) {
|
|
256
264
|
await yieldNext(existing);
|
|
257
265
|
}
|
|
258
|
-
// Fresh search: needs an intent AND a channel.
|
|
259
|
-
|
|
260
|
-
|
|
266
|
+
// Fresh search: needs an intent AND a channel AND the YouTube key (to list the channel).
|
|
267
|
+
{
|
|
268
|
+
const missing = missingEnv(REQUIRED_YOUTUBE);
|
|
269
|
+
if (missing.length)
|
|
270
|
+
emit({ status: 'error', error: missingEnvMessage(missing) });
|
|
261
271
|
}
|
|
262
272
|
if (!intentArg)
|
|
263
273
|
emit({ status: 'error', error: 'Provide an intent: yt-search "<what to look for>" --channel <@handle|url>.' });
|
|
@@ -276,7 +286,7 @@ async function main() {
|
|
|
276
286
|
if (videos.length === 0)
|
|
277
287
|
emit({ status: 'no_results' });
|
|
278
288
|
const pool = videos.map(v => ({ videoId: v.videoId, title: v.title, channelTitle: handle, publishedAt: v.publishedAt, description: v.description }));
|
|
279
|
-
const ranked = (await rerank(intentArg, pool)).slice(0,
|
|
289
|
+
const ranked = (await rerank(intentArg, pool)).slice(0, TOP);
|
|
280
290
|
if (ranked.length === 0)
|
|
281
291
|
emit({ status: 'no_results' });
|
|
282
292
|
const queue = { built_at: new Date().toISOString(), intent: intentArg, channel: handle, candidates: ranked, cursor: 0 };
|
package/dist/yt-sweep.js
CHANGED
|
@@ -49,12 +49,12 @@
|
|
|
49
49
|
*/
|
|
50
50
|
import { readFileSync, writeFileSync, existsSync, rmSync, mkdirSync, renameSync, appendFileSync } from 'node:fs';
|
|
51
51
|
import { spawn } from 'node:child_process';
|
|
52
|
-
import
|
|
52
|
+
import { loadEnv, missingEnv, missingEnvMessage, REQUIRED_LLM, REQUIRED_YOUTUBE } from "./lib/env.js";
|
|
53
53
|
import { parseChannels, parseState, bumpStatePointer } from "./lib/yt-lib.js";
|
|
54
54
|
import { chat, getModel } from "./lib/llm.js";
|
|
55
55
|
import { outputLang } from "./lib/config.js";
|
|
56
|
-
import { PKG_ROOT,
|
|
57
|
-
|
|
56
|
+
import { PKG_ROOT, CHANNELS_MD, STATE_MD, CACHE_DIR, QUEUE_FILE, REST_FILE, PENDING_FILE, PREFETCH_FILE, LOG_FILE, profilePath, script, } from "./lib/paths.js";
|
|
57
|
+
loadEnv();
|
|
58
58
|
mkdirSync(CACHE_DIR, { recursive: true });
|
|
59
59
|
// Re-invoke sibling scripts with the SAME runtime that launched us (bun/node/deno),
|
|
60
60
|
// never a hardcoded binary — the tool must run wherever the user installed it.
|
|
@@ -542,12 +542,14 @@ if (reset) {
|
|
|
542
542
|
clearRest();
|
|
543
543
|
clearPrefetch();
|
|
544
544
|
}
|
|
545
|
-
// Fatal config
|
|
546
|
-
// exited above). A missing
|
|
547
|
-
// error
|
|
548
|
-
//
|
|
549
|
-
|
|
550
|
-
|
|
545
|
+
// Fatal config preflight, foreground only (the detached --fill / --prefetch children already
|
|
546
|
+
// exited above). A missing key would otherwise surface as a misleading `status:"done"` ("no new
|
|
547
|
+
// videos") — the YouTube error is collapsed by the per-channel catch, and a missing LLM key is
|
|
548
|
+
// swallowed by the title-filter's keep-all fallback. Fail fast naming every missing var instead.
|
|
549
|
+
{
|
|
550
|
+
const missing = missingEnv([...REQUIRED_LLM, ...REQUIRED_YOUTUBE]);
|
|
551
|
+
if (missing.length)
|
|
552
|
+
emit({ status: 'error', error: missingEnvMessage(missing) });
|
|
551
553
|
}
|
|
552
554
|
const queue = loadQueue() ?? buildQueue();
|
|
553
555
|
await advance(queue);
|
package/dist/yt-transcript.js
CHANGED
|
@@ -25,11 +25,11 @@ import { spawnSync } from 'node:child_process';
|
|
|
25
25
|
import { mkdtempSync, mkdirSync, readdirSync, readFileSync, rmSync, existsSync } from 'node:fs';
|
|
26
26
|
import { join, dirname } from 'node:path';
|
|
27
27
|
import { fileURLToPath } from 'node:url';
|
|
28
|
-
import
|
|
29
|
-
import { CACHE_DIR
|
|
28
|
+
import { loadEnv } from "./lib/env.js";
|
|
29
|
+
import { CACHE_DIR } from "./lib/paths.js";
|
|
30
30
|
// Load .env so YT_BRIEFING_PROXY is set when run standalone under Node (Bun auto-loads it; Node doesn't).
|
|
31
31
|
// When spawned by yt-sweep, the parent already loaded it and the child inherits the env.
|
|
32
|
-
|
|
32
|
+
loadEnv();
|
|
33
33
|
/** Resolve the yt-dlp binary: explicit env → project-local ./bin → PATH (Windows-aware). */
|
|
34
34
|
function resolveYtDlp() {
|
|
35
35
|
if (process.env.YT_BRIEFING_DLP_PATH)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yt-briefing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "A self-learning YouTube briefing engine: it sweeps the channels you follow, filters noise in two stages (title, then transcript), summarizes the rest in your language, and adapts to your ratings — one video at a time.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|