yt-briefing 0.7.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/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. It
4
- turns each new video into a short summary in your own language that keeps what matters and
5
- skips the filler. Reading it takes a fraction of the time the video would, so you stay on top
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 summary a quick rating, worth my time
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. Onboard:
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, your keys, the channels to follow, and which tool runs `/yt`.
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
 
@@ -110,24 +123,8 @@ 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
 
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. LLM provider / model / key, YouTube key, optional proxy → .env
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
- * Re-running is safe: it warns before overwriting existing data and lets you bail.
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, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
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, ROOT_ENV_PATH, profilePath, } from "./lib/paths.js";
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. LLM provider (.env) -----------------------------------------------------
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 5) Channels you follow');
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
- // 6. Coding agent ------------------------------------------------------------
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 6) Which agent will you run /yt in? (it ships a standard Agent Skill — any compatible agent works)');
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
- // 7. Write everything --------------------------------------------------------
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.7.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": {