yt-briefing 0.7.0 → 0.9.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/README.md +27 -30
- package/dist/bootstrap.js +13 -83
- package/package.json +1 -1
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
|
|
|
@@ -64,12 +77,12 @@ npx yt-briefing list # show the current l
|
|
|
64
77
|
|
|
65
78
|
## One-off: transcribe a single video
|
|
66
79
|
|
|
67
|
-
Just want one video
|
|
80
|
+
Just want one video distilled — no channels, no queue, no rating? Run `/yt-transcribe` and
|
|
68
81
|
paste a URL or video ID. It pulls that video's transcript and writes a journalist-grade
|
|
69
|
-
|
|
82
|
+
briefing in the language you chose at setup (the same `output_lang` as `/yt`). Want a one-off in
|
|
70
83
|
another language? Just say so when you run it (e.g. `/yt-transcribe <url> in German`) — it
|
|
71
84
|
won't change your setup. `--lang pl|en` is separate — it picks which caption track to fetch,
|
|
72
|
-
not the
|
|
85
|
+
not the language it's written in.
|
|
73
86
|
|
|
74
87
|
The skill is installed alongside `/yt` by `init` / `install-skill`. From the plain CLI:
|
|
75
88
|
|
|
@@ -110,32 +123,16 @@ YT_BRIEFING_LLM_MODEL=gemini-2.5-flash
|
|
|
110
123
|
> or switch to a paid key (enable billing, same model) to avoid it.
|
|
111
124
|
|
|
112
125
|
Want something else? Change those three lines for OpenRouter (`https://openrouter.ai/api/v1`),
|
|
113
|
-
OpenAI (`https://api.openai.com/v1`), or a local Ollama (`http://localhost:11434/v1`).
|
|
114
|
-
|
|
115
|
-
### Where the keys live
|
|
116
|
-
|
|
117
|
-
Keys are read from your project's **root `.env`** only — there is no fallback file. Put them there
|
|
118
|
-
(or export them in the shell / CI — an exported var wins). `bun run init` writes them into your
|
|
119
|
-
root `.env`, merging without clobbering any other variables already in it.
|
|
120
|
-
|
|
121
|
-
These are **required**, and missing one fails fast naming exactly which (no silent defaults):
|
|
122
|
-
|
|
123
|
-
```ini
|
|
124
|
-
YT_BRIEFING_LLM_BASE_URL=https://generativelanguage.googleapis.com/v1beta/openai
|
|
125
|
-
YT_BRIEFING_LLM_API_KEY=<your-key>
|
|
126
|
-
YT_BRIEFING_LLM_MODEL=gemini-2.5-flash
|
|
127
|
-
YT_BRIEFING_YOUTUBE_API_KEY=<your-key>
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
`YT_BRIEFING_PROXY` and `YT_BRIEFING_DLP_PATH` are optional. Keep `.env` gitignored.
|
|
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)).
|
|
131
128
|
|
|
132
129
|
## Why an API, not the agent's native model
|
|
133
130
|
|
|
134
|
-
The filtering and the
|
|
131
|
+
The filtering and the briefings go through a plain OpenAI-compatible API call from the engine,
|
|
135
132
|
not through the coding agent's own model. Two reasons.
|
|
136
133
|
|
|
137
134
|
Speed. The engine works ahead in the background. It expands channels in parallel and starts
|
|
138
|
-
|
|
135
|
+
on the next video's briefing while you rate the current one, so the following step is usually
|
|
139
136
|
ready with no wait. An agent's turn-by-turn loop cannot prefetch like that, and every step pays
|
|
140
137
|
its own cold start, which adds up across a whole queue.
|
|
141
138
|
|
package/dist/bootstrap.js
CHANGED
|
@@ -6,18 +6,18 @@
|
|
|
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
|
-
import { existsSync,
|
|
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
|
-
import { loadEnv } from "./lib/env.js";
|
|
21
21
|
import { question } from "./lib/prompt.js";
|
|
22
22
|
import { normalizeHandle, slugify, serializeChannels, serializeState, profileBody, baselineStateRow } from "./lib/channels.js";
|
|
23
23
|
const ask = (q, def = '') => {
|
|
@@ -30,34 +30,8 @@ const askYN = (q, def = true) => {
|
|
|
30
30
|
return def;
|
|
31
31
|
return a.startsWith('y');
|
|
32
32
|
};
|
|
33
|
-
// Secrets: if already provided by the environment (root .env or an exported var), offer to keep it
|
|
34
|
-
// without echoing the value back to the terminal. Returns the kept/typed value.
|
|
35
|
-
const askSecret = (q, preset) => {
|
|
36
|
-
if (preset)
|
|
37
|
-
return question(`${q} [Enter = keep the value from your environment]:`).trim() || preset;
|
|
38
|
-
return ask(q);
|
|
39
|
-
};
|
|
40
|
-
// Merge vars into the project root .env without clobbering: only append keys not already present
|
|
41
|
-
// (so a value the user keeps from their existing root .env stays the single source). Skips empties.
|
|
42
|
-
function mergeRootEnv(vars) {
|
|
43
|
-
const existing = existsSync(ROOT_ENV_PATH) ? readFileSync(ROOT_ENV_PATH, 'utf8') : '';
|
|
44
|
-
const present = new Set(existing.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'))
|
|
45
|
-
.map(l => l.slice(0, l.indexOf('=')).trim()).filter(Boolean));
|
|
46
|
-
const additions = vars.filter(([k, v]) => v !== '' && !present.has(k)).map(([k, v]) => `${k}=${v}`);
|
|
47
|
-
if (additions.length) {
|
|
48
|
-
const sep = existing && !existing.endsWith('\n') ? '\n' : '';
|
|
49
|
-
writeFileSync(ROOT_ENV_PATH, existing + sep + additions.join('\n') + '\n', 'utf8');
|
|
50
|
-
}
|
|
51
|
-
return additions.map(l => l.slice(0, l.indexOf('=')));
|
|
52
|
-
}
|
|
53
33
|
function main() {
|
|
54
34
|
console.log('\n yt-briefing — onboarding\n ' + '─'.repeat(40) + '\n');
|
|
55
|
-
// See what the environment already provides (root .env / exported vars) so we can offer to keep
|
|
56
|
-
// those secrets instead of re-asking, and avoid baking a second copy into .yt-briefing/.env.
|
|
57
|
-
loadEnv();
|
|
58
|
-
const presetLlmKey = process.env.YT_BRIEFING_LLM_API_KEY ?? '';
|
|
59
|
-
const presetYtKey = process.env.YT_BRIEFING_YOUTUBE_API_KEY ?? '';
|
|
60
|
-
const presetProxy = process.env.YT_BRIEFING_PROXY ?? '';
|
|
61
35
|
if (existsSync(CHANNELS_MD)) {
|
|
62
36
|
console.log(` Existing data found at ${DATA_DIR}`);
|
|
63
37
|
if (!askYN(' Overwrite it?', false)) {
|
|
@@ -66,47 +40,15 @@ function main() {
|
|
|
66
40
|
}
|
|
67
41
|
console.log('');
|
|
68
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.
|
|
69
45
|
// 1. Language ----------------------------------------------------------------
|
|
70
46
|
console.log(' 1) Language');
|
|
71
47
|
const outputLang = ask(' Output language for summaries and ratings', 'English');
|
|
72
|
-
// 2.
|
|
73
|
-
// Pick a provider → we prefill its endpoint + a sensible model and ask ONLY for the
|
|
74
|
-
// key (with the exact link to get it). The recommended free path is the default, so
|
|
75
|
-
// pressing Enter lands on it — no long URL to paste, nothing to guess.
|
|
76
|
-
console.log('\n 2) Which AI writes the filtering + summaries? Pick a provider:\n');
|
|
77
|
-
console.log(' 1) Gemini — FREE key, best way to start · https://aistudio.google.com/apikey');
|
|
78
|
-
console.log(' 2) OpenRouter — one key for Gemini + GPT + … · https://openrouter.ai/keys');
|
|
79
|
-
console.log(' 3) OpenAI — GPT models · https://platform.openai.com/api-keys');
|
|
80
|
-
console.log(' 4) Other / local (Ollama or any custom endpoint)\n');
|
|
81
|
-
console.log(' Type 1, 2, 3 or 4 and press Enter. (Just press Enter for 1 — Gemini, recommended.)');
|
|
82
|
-
const PROVIDERS = {
|
|
83
|
-
'1': { name: 'Gemini', base: 'https://generativelanguage.googleapis.com/v1beta/openai', model: 'gemini-2.5-flash', keyUrl: 'https://aistudio.google.com/apikey' },
|
|
84
|
-
'2': { name: 'OpenRouter', base: 'https://openrouter.ai/api/v1', model: 'google/gemini-2.5-flash', keyUrl: 'https://openrouter.ai/keys' },
|
|
85
|
-
'3': { name: 'OpenAI', base: 'https://api.openai.com/v1', model: 'gpt-4o-mini', keyUrl: 'https://platform.openai.com/api-keys' },
|
|
86
|
-
};
|
|
87
|
-
let llmBaseUrl, llmModel, llmKey;
|
|
88
|
-
const picked = PROVIDERS[ask(' Your choice', '1')];
|
|
89
|
-
if (picked) {
|
|
90
|
-
console.log(`\n → ${picked.name}. Get your key here: ${picked.keyUrl}`);
|
|
91
|
-
llmKey = askSecret(' Paste your API key', presetLlmKey);
|
|
92
|
-
llmBaseUrl = picked.base;
|
|
93
|
-
llmModel = ask(' Model (Enter to accept)', picked.model);
|
|
94
|
-
}
|
|
95
|
-
else {
|
|
96
|
-
console.log('\n → Custom / local endpoint (e.g. Ollama at http://localhost:11434/v1)');
|
|
97
|
-
llmBaseUrl = ask(' YT_BRIEFING_LLM_BASE_URL', 'http://localhost:11434/v1');
|
|
98
|
-
llmModel = ask(' YT_BRIEFING_LLM_MODEL', 'llama3.1');
|
|
99
|
-
llmKey = askSecret(' YT_BRIEFING_LLM_API_KEY (blank for local)', presetLlmKey);
|
|
100
|
-
}
|
|
101
|
-
console.log('\n 3) YouTube Data API (needed to list channel uploads)');
|
|
102
|
-
console.log(' Get a key: https://console.cloud.google.com → YouTube Data API v3');
|
|
103
|
-
const ytKey = askSecret(' YT_BRIEFING_YOUTUBE_API_KEY', presetYtKey);
|
|
104
|
-
console.log('\n 4) Proxy (optional — only needed on datacenter/VPS IPs; see docs/warp-proxy.md)');
|
|
105
|
-
const ytProxy = askSecret(' YT_BRIEFING_PROXY (blank = direct)', presetProxy);
|
|
106
|
-
// 5. Channels ----------------------------------------------------------------
|
|
48
|
+
// 2. Channels ----------------------------------------------------------------
|
|
107
49
|
// Just collect a flat list. No categories, no per-channel rules to define up front —
|
|
108
50
|
// each channel's profile LEARNS what to skip as you rate it (## Skip titles / ## Notes).
|
|
109
|
-
console.log('\n
|
|
51
|
+
console.log('\n 2) Channels you follow');
|
|
110
52
|
console.log(' Add one per line — paste whichever form you have, all work as-is:');
|
|
111
53
|
console.log(' eg. @betterstack, betterstack or https://www.youtube.com/@betterstack');
|
|
112
54
|
console.log(' (Paste the full URL directly — it reads the @handle for you) Empty line to finish.');
|
|
@@ -132,31 +74,20 @@ function main() {
|
|
|
132
74
|
if (channels.length === 0) {
|
|
133
75
|
console.log('\n No channels added — you can add them later by editing data/channels.md.\n');
|
|
134
76
|
}
|
|
135
|
-
//
|
|
77
|
+
// 3. Coding agent ------------------------------------------------------------
|
|
136
78
|
// Place the skill INTO THIS PROJECT (the package folder you open in the agent) — never a
|
|
137
79
|
// home-global dir (that's the npm -g antipattern: machine-wide, invisible, easy to forget).
|
|
138
80
|
// SKILL.md is the cross-agent standard, so the shipped skill runs in any compatible agent —
|
|
139
81
|
// we just install it into that agent's skills dir (.claude/skills, .cursor/skills, .codex/skills).
|
|
140
82
|
// 1/2/3 = known agents; 4 = any other compatible agent (a project folder you name).
|
|
141
|
-
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)');
|
|
142
84
|
console.log(' 1) Claude Code 2) Cursor 3) Codex 4) Custom folder (any other agent)\n');
|
|
143
85
|
const agentKey = ask(' Your agent', '1');
|
|
144
86
|
// For a custom target, ask the folder now (keeps all prompts in the interactive block).
|
|
145
87
|
const customDir = AGENTS[agentKey] ? '' : ask(' Skills folder to install into', customSkillsRootDefault());
|
|
146
|
-
//
|
|
88
|
+
// 4. Write everything --------------------------------------------------------
|
|
147
89
|
mkdirSync(CHANNELS_DIR, { recursive: true });
|
|
148
|
-
// Secrets live in the project ROOT .env (the only file the engine reads). Merge into it without
|
|
149
|
-
// clobbering — keys already there (e.g. kept from the environment) stay the single source. The
|
|
150
|
-
// engine has no fallback file, so a key not set here (or exported) is a hard error at run time.
|
|
151
|
-
const envAdded = mergeRootEnv([
|
|
152
|
-
['YT_BRIEFING_LLM_BASE_URL', llmBaseUrl],
|
|
153
|
-
['YT_BRIEFING_LLM_API_KEY', llmKey],
|
|
154
|
-
['YT_BRIEFING_LLM_MODEL', llmModel],
|
|
155
|
-
['YT_BRIEFING_YOUTUBE_API_KEY', ytKey],
|
|
156
|
-
['YT_BRIEFING_PROXY', ytProxy],
|
|
157
|
-
]);
|
|
158
90
|
// Keep the throwaway cache out of git for the consume layout. data/ stays versionable (for sync).
|
|
159
|
-
// No secrets live under .yt-briefing/ anymore, so nothing else needs ignoring here.
|
|
160
91
|
if (BASE_DIR !== PKG_ROOT) {
|
|
161
92
|
writeFileSync(join(BASE_DIR, '.gitignore'), 'data/.cache/\n', 'utf8');
|
|
162
93
|
}
|
|
@@ -170,7 +101,6 @@ function main() {
|
|
|
170
101
|
writeFileSync(profilePath(c.slug), profileBody(c.handle, c.slug), 'utf8');
|
|
171
102
|
console.log(' ' + '─'.repeat(40));
|
|
172
103
|
console.log(` Done. Wrote:`);
|
|
173
|
-
console.log(` ${ROOT_ENV_PATH} ${envAdded.length ? `(+${envAdded.join(', ')})` : '(no new keys — already set)'}`);
|
|
174
104
|
console.log(` ${CONFIG_JSON}`);
|
|
175
105
|
console.log(` ${CHANNELS_MD}`);
|
|
176
106
|
console.log(` ${STATE_MD}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yt-briefing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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": {
|