yt-briefing 0.15.1 → 1.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/README.md CHANGED
@@ -16,6 +16,10 @@ It also gets better the more you use it. You give each summary a quick rating, w
16
16
  or not, and from that it learns what to keep showing you and what to drop. Over time the queue
17
17
  becomes yours: less noise, more of what you care about.
18
18
 
19
+ yt-briefing runs inside [Claude Code](https://claude.com/claude-code). `/yt` opens the briefing in
20
+ a pane next to your chat, and the filtering and summaries run on your own Claude Code login. There
21
+ is no separate model, provider or LLM key to set up.
22
+
19
23
  ## First run vs later
20
24
 
21
25
  On a channel's first sweep there is no history, so yt-briefing takes the latest video of each
@@ -28,11 +32,8 @@ the last one left off.
28
32
 
29
33
  ## Setup
30
34
 
31
- You'll need Node 18+ or Bun, a YouTube Data API v3 key, an LLM key (a
32
- [free Gemini key](https://aistudio.google.com/apikey) works, see [Providers](#providers)), and
33
- a tool that runs skills: [Claude Code](https://claude.com/claude-code),
34
- [Cursor](https://cursor.com), [Codex](https://developers.openai.com/codex), or anything else
35
- that loads the standard `SKILL.md` (Agent Skills — 30+ agents).
35
+ You'll need Node 18+ or Bun, a YouTube Data API v3 key, and
36
+ [Claude Code](https://claude.com/claude-code) installed and logged in, with `claude` on your PATH.
36
37
 
37
38
  1. Install yt-dlp (it pulls the subtitles):
38
39
 
@@ -53,18 +54,15 @@ yarn add yt-briefing
53
54
  bun add yt-briefing
54
55
  ```
55
56
 
56
- 3. Put your keys in a `.env` at your project root — all four are required:
57
+ 3. Put your YouTube key in a `.env` at your project root:
57
58
 
58
59
  ```ini
59
- YT_BRIEFING_LLM_BASE_URL=https://generativelanguage.googleapis.com/v1beta/openai
60
- YT_BRIEFING_LLM_API_KEY=<key> # free at https://aistudio.google.com/apikey
61
- YT_BRIEFING_LLM_MODEL=gemini-2.5-flash
62
60
  YT_BRIEFING_YOUTUBE_API_KEY=<key> # console.cloud.google.com → enable "YouTube Data API v3"
63
61
  ```
64
62
 
65
- Any OpenAI-compatible endpoint works — see [Providers](#providers) to use OpenRouter, OpenAI,
66
- Anthropic, or a local Ollama instead of Gemini. `YT_BRIEFING_PROXY` (datacenter/VPS IPs) is the
67
- only optional extra.
63
+ Optional extras: `YT_BRIEFING_MODEL` picks the Claude model for filtering and summaries (default
64
+ `sonnet`, any alias or model name `claude --model` accepts, `haiku` for faster runs), and `YT_BRIEFING_PROXY` routes
65
+ transcript fetches through a proxy on datacenter/VPS IPs.
68
66
 
69
67
  4. Onboard:
70
68
 
@@ -72,7 +70,8 @@ only optional extra.
72
70
  npx yt-briefing init # or: bunx yt-briefing init
73
71
  ```
74
72
 
75
- `init` asks for your language, the channels to follow, and which tool runs `/yt`.
73
+ `init` asks for your language and the channels to follow, then installs `/yt` and the two
74
+ skills into your project's `.claude/skills/`.
76
75
 
77
76
  Add or remove channels anytime:
78
77
 
@@ -118,84 +117,64 @@ it to go deeper, lower it for a quicker pass:
118
117
  /yt-search @betterstack which terminal --top 5
119
118
  ```
120
119
 
121
- ## Don't shelve it — research it
122
-
123
- Tech channels announce something new every week, and the usual fate is "looks interesting" →
124
- to-do list → never. So the rating popup has a third option next to OK/Weak: **Research**. Pick
125
- it — or type `? your question` straight into the comment box — and the loop ends there: the
126
- agent pulls that video's full transcript and works your question with you. Against your own
127
- codebase if you ask "would this fit my project", against the web if the claims need checking —
128
- a quick feedback loop instead of a shelf. The video is marked as seen, and the next `/yt`
129
- resumes the queue right where you broke off.
130
-
131
120
  ## Run it
132
121
 
133
- Open your project in Claude Code or Cursor and run `/yt`. If it's not listed, start a fresh
134
- session. To install the skills again for another tool or project, run
135
- `npx yt-briefing install-skill` (it installs `/yt`, `/yt-transcribe`, and `/yt-search`).
136
-
137
- ## Rating gate
138
-
139
- The loop only works if you see the summary *before* you rate it, and that is the one step an
140
- agent can silently drop — the popup still appears, you still answer, and the rating is recorded
141
- against a summary nobody read. On Claude Code the installer wires a `PreToolUse` hook into your
142
- project's `.claude/settings.json` that refuses to *record* a rating unless the video's summary is
143
- in the chat. It merges with your existing hooks and updates itself on reinstall. If that file
144
- isn't valid JSON the installer leaves it alone and says so — add the entry yourself:
145
-
146
- ```json
147
- {
148
- "hooks": {
149
- "PreToolUse": [
150
- { "matcher": "Bash",
151
- "hooks": [ { "type": "command", "command": "node \"${CLAUDE_PROJECT_DIR}/node_modules/yt-briefing/dist/yt-summary-gate.js\"" } ] }
152
- ]
153
- }
154
- }
155
- ```
122
+ Open your project in Claude Code and type `/yt`. The briefing opens in a pane: the summary, and
123
+ under it four keys.
156
124
 
157
- It watches the rating write rather than the popup, because the popup cannot be gated: the agent
158
- pastes the summary and opens the popup in one message, and the harness only writes a message to
159
- the transcript once that message is complete — so the evidence does not exist yet at popup time.
160
- The rating write is a separate call in the next message, where it does. The matcher is `Bash`, so
161
- the hook is invoked on ordinary shell commands too; it reads the command line and exits before
162
- touching anything. The tradeoff: a genuinely skipped paste is caught after you have answered, so
163
- that one answer is wasted — but nothing reaches the channel profile unseen.
125
+ | Key | What it does |
126
+ |-----|--------------|
127
+ | `1` OK | Neutral. The video is marked as seen, the next one loads. |
128
+ | `2` Weak | Worthless. The title goes to the channel's skip examples, so the filter learns to drop titles like it. |
129
+ | `3` Research | Ends the loop and hands this video to Claude, see below. |
130
+ | `4` Stop | Closes the pane. The next `/yt` resumes where you stopped. |
164
131
 
165
- Other agents have no equivalent hook, so there the instruction in `SKILL.md` is what holds.
132
+ The **Comment** field takes anything else. Type what you think in your own words ("too many panel
133
+ shows, skip those") and press Enter: Claude turns it into a standing rule for that channel and
134
+ infers the rating. `? your question` starts research with that question, `stop` closes the pane.
166
135
 
167
- ## Providers
136
+ Each step is the engine, not a chat turn: rating a video costs no tokens of your session, and
137
+ the next summary is usually ready before you have finished reading the current one.
168
138
 
169
- Any OpenAI-compatible endpoint works. Gemini 2.5 Flash is the easy default. It's fast, cheap,
170
- and free to start at [Google AI Studio](https://aistudio.google.com/apikey):
139
+ `/yt` is a Claude Code mod (a plugin in `.claude/skills/yt-briefing/`). Claude Code loads it on
140
+ its own once you trust the project folder. If `/yt` is not listed, start a fresh session. To
141
+ install again, after an upgrade or into another project, run `npx yt-briefing install-skill` (it
142
+ installs `/yt`, `/yt-transcribe` and `/yt-search`).
171
143
 
172
- ```ini
173
- YT_BRIEFING_LLM_BASE_URL=https://generativelanguage.googleapis.com/v1beta/openai
174
- YT_BRIEFING_LLM_API_KEY=<gemini-key>
175
- YT_BRIEFING_LLM_MODEL=gemini-2.5-flash
176
- ```
177
-
178
- > On the free tier Gemini sometimes returns a "model is overloaded / high demand" error. Retry,
179
- > or switch to a paid key (enable billing, same model) to avoid it.
180
-
181
- Want something else? Change those three lines for OpenRouter (`https://openrouter.ai/api/v1`),
182
- OpenAI (`https://api.openai.com/v1`), Anthropic (`https://api.anthropic.com/v1/`,
183
- e.g. `claude-sonnet-5`), or a local Ollama (`http://localhost:11434/v1`). Set
184
- `YT_BRIEFING_LLM_BASE_URL`, `_API_KEY`, and `_MODEL` in your root `.env` (see [Setup](#setup)).
185
-
186
- ## Why an API, not the agent's native model
187
-
188
- The filtering and the summaries go through a plain OpenAI-compatible API call from the engine,
189
- not through the coding agent's own model. Two reasons.
190
-
191
- Speed. The engine works ahead in the background. It expands channels in parallel and starts
192
- summarizing the next video while you rate the current one, so the following step is usually
193
- ready with no wait. An agent's turn-by-turn loop cannot prefetch like that, and every step pays
194
- its own cold start, which adds up across a whole queue.
144
+ ## Don't shelve it — research it
195
145
 
196
- Compatibility. A standard API plus a standard `SKILL.md` means one engine runs everywhere: Claude
197
- Code, Cursor, Codex, any other Agent-Skills-compatible tool, or the plain CLI. A tool-native
198
- approach would tie it to that one tool and one model.
146
+ Tech channels announce something new every week, and the usual fate is "looks interesting" →
147
+ to-do list → never. So next to OK/Weak there is a third key: **Research**. Press it, or type
148
+ `? your question` into the comment field, and the loop ends there: the pane closes and the video
149
+ lands in your chat with its briefing and the command for its full transcript. Claude works your
150
+ question with you, against your own codebase if you ask "would this fit my project", against the
151
+ web if the claims need checking. A quick feedback loop instead of a shelf. The video is marked as
152
+ seen, and the next `/yt` resumes the queue right where you broke off.
153
+
154
+ ## Upgrading from 0.x
155
+
156
+ 1.0 runs on Claude Code only. The OpenAI-compatible provider and its three `YT_BRIEFING_LLM_*`
157
+ keys are gone (delete them from `.env`), and the chat-driven `/yt` skill with its rating popup is
158
+ replaced by the pane. Run `npx yt-briefing install-skill` once in your project: it installs the
159
+ pane, and removes the old `/yt` skill and the summary-gate hook from `.claude/settings.json`. Your
160
+ channels, profiles and ratings in `.yt-briefing/data/` carry over unchanged.
161
+
162
+ ## Why Claude Code, and nothing else
163
+
164
+ Filtering and summaries are a `claude -p` call from the engine: one prompt in, one answer out, with
165
+ no tools, no project settings or hooks, no MCP servers and no saved session. It runs on the login
166
+ you already have, so there is no second model to pay for or keep a key to. If `ANTHROPIC_API_KEY`
167
+ is set in your environment, the engine removes it for that call, because Claude Code would
168
+ otherwise bill it as API usage instead of using your login.
169
+
170
+ The engine still works ahead in the background. It expands channels in parallel and summarizes the
171
+ next video while you rate the current one, so each step is usually ready with no wait. The pane
172
+ only shows what the engine produced and sends your key presses back to it.
173
+
174
+ Supporting every agent that reads `SKILL.md` meant a chat loop for the rating: the model pasted
175
+ each summary, asked the question and recorded the answer, every video a full turn, plus a hook to
176
+ make sure the summary was really shown. A Claude Code pane does the same with no model in the loop,
177
+ which is why 1.0 drops the other agents.
199
178
 
200
179
  ## Why one transcript at a time
201
180
 
@@ -210,7 +189,8 @@ flowing.
210
189
  ## Sync across machines
211
190
 
212
191
  Your state is plain files in `.yt-briefing/data/`. Version that folder (or point `YT_BRIEFING_DATA_DIR`
213
- at a separate private repo) and commit after each rating. Recipe:
192
+ at a separate private repo) and commit after each rating: set `"after_rate"` in
193
+ `.yt-briefing/data/config.json` to a script and the engine runs it after every rating. Recipe:
214
194
  [docs/sync-across-machines.md](./docs/sync-across-machines.md).
215
195
 
216
196
  ## Running on a VPS
package/dist/bootstrap.js CHANGED
@@ -8,17 +8,18 @@
8
8
  * 1. Output language for summaries + ratings → DATA_DIR/config.json
9
9
  * 2. The channels you follow — just a flat list of handles
10
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
11
+ * 3. Installs /yt (the rating pane) + /yt-transcribe + /yt-search into this Claude Code project
12
12
  *
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.
13
+ * It does NOT touch keys: the one key (YouTube Data API) lives in your project root .env (see README →
14
+ * Setup); the engine reads it at run time. Filters and summaries run on your Claude Code login. Re-running is safe: it warns before overwriting existing data and bails.
15
15
  * Everything it writes is plain Markdown / JSON you can also edit by hand afterwards.
16
16
  */
17
17
  import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
18
18
  import { join } from 'node:path';
19
19
  import { DATA_DIR, BASE_DIR, PKG_ROOT, CHANNELS_DIR, CHANNELS_MD, STATE_MD, CONFIG_JSON, profilePath, ROOT_ENV_PATH, } from "./lib/paths.js";
20
- import { loadEnv, missingEnv, REQUIRED_LLM, REQUIRED_YOUTUBE } from "./lib/env.js";
21
- import { AGENTS, installSkills, projectSkillsRoot, customSkillsRootDefault, isPackageDevCwd, installClaudeGate, CLAUDE_CODE } from "./lib/skill-install.js";
20
+ import { loadEnv, missingEnv, REQUIRED_YOUTUBE } from "./lib/env.js";
21
+ import { installAll, isPackageDevCwd } from "./lib/skill-install.js";
22
+ import { claudeMissing } from "./lib/llm.js";
22
23
  import { question } from "./lib/prompt.js";
23
24
  import { normalizeHandle, slugify, serializeChannels, serializeState, profileBody, baselineStateRow } from "./lib/channels.js";
24
25
  const ask = (q, def = '') => {
@@ -41,8 +42,8 @@ function main() {
41
42
  }
42
43
  console.log('');
43
44
  }
44
- // Keys are NOT asked here — they live in your project root .env (LLM + YouTube; see README
45
- // → Setup). The engine reads them at run time and fails fast naming any that are missing.
45
+ // Keys are NOT asked here — the YouTube key lives in your project root .env (see README →
46
+ // Setup). The engine reads it at run time and fails fast naming it if missing.
46
47
  // 1. Language ----------------------------------------------------------------
47
48
  console.log(' 1) Language');
48
49
  const outputLang = ask(' Output language for summaries and ratings', 'English');
@@ -75,18 +76,7 @@ function main() {
75
76
  if (channels.length === 0) {
76
77
  console.log('\n No channels added — you can add them later by editing data/channels.md.\n');
77
78
  }
78
- // 3. Coding agent ------------------------------------------------------------
79
- // Place the skill INTO THIS PROJECT (the package folder you open in the agent) — never a
80
- // home-global dir (that's the npm -g antipattern: machine-wide, invisible, easy to forget).
81
- // SKILL.md is the cross-agent standard, so the shipped skill runs in any compatible agent —
82
- // we just install it into that agent's skills dir (.claude/skills, .cursor/skills, .codex/skills).
83
- // 1/2/3 = known agents; 4 = any other compatible agent (a project folder you name).
84
- console.log('\n 3) Which agent will you run /yt in? (it ships a standard Agent Skill — any compatible agent works)');
85
- console.log(' 1) Claude Code 2) Cursor 3) Codex 4) Custom folder (any other agent)\n');
86
- const agentKey = ask(' Your agent', '1');
87
- // For a custom target, ask the folder now (keeps all prompts in the interactive block).
88
- const customDir = AGENTS[agentKey] ? '' : ask(' Skills folder to install into', customSkillsRootDefault());
89
- // 4. Write everything --------------------------------------------------------
79
+ // 3. Write everything --------------------------------------------------------
90
80
  mkdirSync(CHANNELS_DIR, { recursive: true });
91
81
  // Keep the throwaway cache out of git for the consume layout. data/ stays versionable (for sync).
92
82
  if (BASE_DIR !== PKG_ROOT) {
@@ -106,44 +96,35 @@ function main() {
106
96
  console.log(` ${CHANNELS_MD}`);
107
97
  console.log(` ${STATE_MD}`);
108
98
  console.log(` ${channels.length} profile(s) in ${CHANNELS_DIR}/`);
109
- // Install the /yt + /yt-transcribe skills for the chosen agent (step 6), into THIS project —
110
- // process.cwd(), i.e. wherever you ran the command (the package clone in dev, or your own
111
- // project when the package is a dependency). The command baked in is the shipped `bun run src`
112
- // only for the dev-in-clone case; otherwise the compiled `dist/` command (so a consumed package works).
113
- const agent = AGENTS[agentKey];
99
+ // Install into THIS project — process.cwd(), wherever you ran the command (the package clone in
100
+ // dev, or your own project when the package is a dependency). The shipped dev commands only for
101
+ // the dev-in-clone case; otherwise the compiled `dist/` commands (so a consumed package works).
114
102
  try {
115
- const targets = agent
116
- ? installSkills(projectSkillsRoot(agentKey, process.cwd()), /* dist */ !isPackageDevCwd())
117
- : installSkills(customDir, /* dist */ true);
118
- for (const t of targets)
119
- console.log(` skill → ${t}`);
120
- // Claude Code also gets the summary gate — the hook that refuses a rating popup for a video
121
- // whose summary was never pasted into the chat. No other agent exposes PreToolUse.
122
- if (agentKey === CLAUDE_CODE) {
123
- const gate = installClaudeGate(process.cwd(), /* dist */ !isPackageDevCwd());
124
- console.log(gate
125
- ? ` gate → ${gate}`
126
- : ` ! .claude/settings.json isn't valid JSON — add the summary gate by hand (README → Rating gate).`);
127
- }
103
+ const { written, removed } = installAll(process.cwd(), /* dist */ !isPackageDevCwd());
104
+ for (const t of written)
105
+ console.log(` claude → ${t}`);
106
+ for (const r of removed)
107
+ console.log(` removed (replaced in 1.0) → ${r}`);
128
108
  }
129
109
  catch (e) {
130
- console.log(` ! Couldn't install the skills (${e.message}) — run yt-briefing install-skill later.`);
110
+ console.log(` ! Couldn't install into .claude/skills (${e.message}) — run yt-briefing install-skill later.`);
131
111
  }
132
- // Preflight the keys the engine needs at run time. init deliberately does NOT write them —
133
- // they live in the project root .env (12-factor) — but warn now, naming any still missing, so
134
- // the gap surfaces here instead of at the first /yt.
112
+ // Preflight what the engine needs at run time, so the gap surfaces here instead of at the
113
+ // first /yt: the YouTube key in the project root .env, and a logged-in `claude` CLI.
135
114
  loadEnv();
136
- const missingKeys = missingEnv([...REQUIRED_LLM, ...REQUIRED_YOUTUBE]);
115
+ const missingKeys = missingEnv(REQUIRED_YOUTUBE);
137
116
  if (missingKeys.length) {
138
- console.log('\n ⚠ Keys still needed before /yt will run — add them to your project root .env:');
117
+ console.log('\n ⚠ Key still needed before /yt will run — add it to your project root .env:');
139
118
  for (const k of missingKeys)
140
119
  console.log(` ${k}`);
141
- console.log(` Path: ${ROOT_ENV_PATH} · full block in README → Setup`);
120
+ console.log(` Path: ${ROOT_ENV_PATH} · see README → Setup`);
142
121
  }
122
+ const noClaude = claudeMissing();
123
+ if (noClaude)
124
+ console.log(`\n ⚠ ${noClaude}`);
143
125
  console.log('\n Next:');
144
- console.log(` 1. Open this folder in ${agent ? agent.name : 'your agent'}.`);
145
- console.log(' 2. Start a new chat and type /yt (or /yt-transcribe <url>)');
146
- console.log('\n No agent? Run it in the terminal instead — see the README.\n');
126
+ console.log(' 1. Open this folder in Claude Code (trust it when asked).');
127
+ console.log(' 2. Type /yt — the briefing opens in a pane. (/yt-transcribe <url> for one video)\n');
147
128
  }
148
129
  try {
149
130
  main();
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * or Bun), passing remaining args through.
7
7
  *
8
8
  * yt-briefing init interactive onboarding wizard
9
- * yt-briefing install-skill install the /yt skill into a coding agent
9
+ * yt-briefing install-skill install /yt (pane) + skills into a Claude Code project
10
10
  * yt-briefing add|remove <@handle|url> add or remove channels (also list)
11
11
  * yt-briefing list list the channels you follow
12
12
  * yt-briefing sweep [--reset] advance one step; prints a JSON status line
@@ -1,63 +1,32 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * install-skill — copy this package's skills (`/yt` + `/yt-transcribe`) into a coding agent's
4
- * skills directory so the agent detects them. For any target that isn't this package under Bun,
5
- * each command is baked to `"<this runtime>" "<abs>/dist/X.js"` (the compiled build), so it works
6
- * no matter the agent's working directory or runtime (the engine resolves data/.env from its own
7
- * location).
3
+ * install-skill — install yt-briefing into a Claude Code project: the `/yt` mod (the rating pane)
4
+ * plus the `/yt-transcribe` and `/yt-search` skills, all under `<project>/.claude/skills/`.
8
5
  *
9
- * yt-briefing install-skill # interactive: pick agent + scope
6
+ * yt-briefing install-skill # interactive: which project folder
10
7
  *
11
- * `bun run init` already installs these skills into the project as its final step; this
12
- * standalone command is for re-installing, a different project, or a second agent. There is
13
- * deliberately no home-global install — the skills live with the project that uses them.
14
- *
15
- * SKILL.md is the cross-agent Agent Skills standard, so the shipped skills run in any compatible
16
- * agent (Claude Code, Cursor, Codex, and 30+ others); this command just (re)places them in the
17
- * skills dir of whichever agent you pick.
8
+ * `init` already does this as its final step; this standalone command is for re-installing
9
+ * (e.g. after an upgrade), or for another project. There is deliberately no home-global install —
10
+ * everything lives with the project that uses it. Upgrading from 0.x also removes the old
11
+ * chat-driven `/yt` skill and the summary-gate hook it needed.
18
12
  */
19
- import { AGENTS, installSkills, projectSkillsRoot, customSkillsRootDefault, isPackageDevCwd, installClaudeGate, CLAUDE_CODE } from "./lib/skill-install.js";
13
+ import { installAll, isPackageDevCwd } from "./lib/skill-install.js";
20
14
  import { question } from "./lib/prompt.js";
21
15
  const ask = (q, def = '') => question(def ? `${q} [${def}]:` : `${q}:`).trim() || def;
22
- function done(targets, gate) {
23
- console.log('\n ✓ Installed:');
24
- for (const t of targets)
25
- console.log(` ${t}`);
26
- if (gate)
27
- console.log(` ${gate} (summary gate)`);
28
- if (gate === null)
29
- console.log(` ! .claude/settings.json isn't valid JSON — add the summary gate by hand (README → Rating gate).`);
30
- console.log(' Start a fresh agent session, then run /yt or /yt-transcribe\n');
31
- }
32
- /** Claude Code only: the PreToolUse hook that blocks a rating popup with no summary in the chat. */
33
- const gateFor = (key, projectDir, dist) => key === CLAUDE_CODE ? installClaudeGate(projectDir, dist) : undefined;
34
- // 1) which agent → which skills subdir
35
- console.log('\n Install the /yt + /yt-transcribe skills — which agent?\n');
36
- console.log(' 1) Claude Code');
37
- console.log(' 2) Cursor');
38
- console.log(' 3) Codex');
39
- console.log(' 4) Custom folder (any other compatible agent)\n');
40
- const agentKey = ask(' Agent', '1');
41
- const agent = AGENTS[agentKey];
42
- // 3) Custom — write the skills straight into a skills root the user names (their agent's dir).
43
- // Arbitrary location → bake the absolute dist commands so they work whatever the agent's cwd is.
44
- if (!agent) {
45
- done(installSkills(ask(' Skills folder to install into', customSkillsRootDefault()), true));
46
- process.exit(0);
47
- }
48
- // 2) Known agent → which project (default: the current folder). No home-global option by
49
- // design — the skills are always scoped to a project that uses them.
50
- console.log(`\n ${agent.name} — which project?\n`);
16
+ console.log('\n Install /yt (pane) + /yt-transcribe + /yt-search into a Claude Code project.\n');
51
17
  console.log(' 1) This project (current folder) — recommended');
52
18
  console.log(' 2) Another project folder\n');
53
- if (ask(' Where', '1') === '2') {
54
- // A different project → the agent's cwd won't be the package, so bake the absolute dist commands.
55
- const projectDir = ask(' Project folder', process.cwd());
56
- done(installSkills(projectSkillsRoot(agentKey, projectDir), true), gateFor(agentKey, projectDir, true));
57
- }
58
- else {
59
- // Current folder: shipped `bun run src` only when developing in the package clone under Bun;
60
- // otherwise (incl. consuming the package as a dependency) bake the compiled dist commands.
61
- const dist = !isPackageDevCwd();
62
- done(installSkills(projectSkillsRoot(agentKey, process.cwd()), dist), gateFor(agentKey, process.cwd(), dist));
19
+ const other = ask(' Where', '1') === '2';
20
+ const projectDir = other ? ask(' Project folder', process.cwd()) : process.cwd();
21
+ // The shipped dev commands only work developing inside the package clone under Bun; anything
22
+ // else (another folder, Node, the package consumed as a dependency) gets the compiled dist/ form.
23
+ const { written, removed } = installAll(projectDir, other || !isPackageDevCwd());
24
+ console.log('\n Installed:');
25
+ for (const t of written)
26
+ console.log(` ${t}`);
27
+ if (removed.length) {
28
+ console.log(' Removed (replaced in 1.0):');
29
+ for (const r of removed)
30
+ console.log(` ${r}`);
63
31
  }
32
+ console.log('\n Start a fresh Claude Code session in that project (trust the folder when asked), then run /yt\n');
@@ -1,23 +1,26 @@
1
1
  /**
2
- * User preferences for yt-briefing — currently just the output language, decided at
3
- * onboarding and stored in DATA_DIR/config.json. Kept separate from .env on purpose:
2
+ * User preferences for yt-briefing — the output language (decided at onboarding) and an optional
3
+ * after-rate command, stored in DATA_DIR/config.json. Kept separate from .env on purpose:
4
4
  * .env holds secrets (API keys, proxy), config.json holds non-secret preferences that
5
- * both the engine and the agent (skill) read. The skill reads `output_lang` to ask the
6
- * rating question in the user's language; the engine reads it to write summaries in it.
5
+ * the engine reads. `output_lang` is the language summaries are written in; `after_rate` is a shell
6
+ * command run (detached, from the project root) after every recorded rating, e.g. a script that
7
+ * commits DATA_DIR to git. The engine itself never runs VCS; the command is yours.
7
8
  */
8
9
  import { readFileSync, existsSync } from 'node:fs';
9
10
  import { CONFIG_JSON } from "./paths.js";
10
11
  export function loadConfig() {
12
+ let c = {};
11
13
  if (existsSync(CONFIG_JSON)) {
12
14
  try {
13
- const c = JSON.parse(readFileSync(CONFIG_JSON, 'utf8'));
14
- if (typeof c.output_lang === 'string' && c.output_lang.trim()) {
15
- return { output_lang: c.output_lang.trim() };
16
- }
15
+ c = JSON.parse(readFileSync(CONFIG_JSON, 'utf8'));
17
16
  }
18
- catch { /* malformed → fall through to env/default */ }
17
+ catch { /* malformed → defaults */ }
19
18
  }
20
- return { output_lang: process.env.OUTPUT_LANG?.trim() || 'English' };
19
+ const lang = typeof c.output_lang === 'string' && c.output_lang.trim()
20
+ ? c.output_lang.trim()
21
+ : process.env.OUTPUT_LANG?.trim() || 'English';
22
+ const afterRate = typeof c.after_rate === 'string' && c.after_rate.trim() ? c.after_rate.trim() : undefined;
23
+ return { output_lang: lang, ...(afterRate ? { after_rate: afterRate } : {}) };
21
24
  }
22
25
  /** The language summaries and ratings are written in. Default English. */
23
26
  export const outputLang = () => loadConfig().output_lang;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Turn a raw rating comment ("too much politics, skip these panels") into what the profile
3
+ * stores: one clean, generalizable rule for `## Notes`, plus the rating it implies.
4
+ *
5
+ * This used to be the agent's job in the chat loop. With the rating loop in a pane there is no
6
+ * agent turn per video, so the engine does it with the same `claude -p` call it uses everywhere.
7
+ */
8
+ import { chat } from "./llm.js";
9
+ export function distillPrompt(comment, video) {
10
+ return `A user rating videos from the YouTube channel ${video.channel} left this comment on the video "${video.title}" (${video.type}):
11
+
12
+ """${comment}"""
13
+
14
+ Turn it into ONE durable rule for this channel's profile — a standing instruction the briefing tool will follow for future videos of this channel (what to skip, what to keep, how to summarize). Generalize from this one video to the pattern the user means; keep the user's intent, drop chit-chat. Write the rule in the same language as the comment, one or two sentences.
15
+
16
+ Also decide the rating for THIS video: 0 if the comment is clearly negative about it (worthless, noise, should have been skipped), otherwise 1.
17
+
18
+ Output ONLY raw JSON, no fences: {"rating":0|1,"rule":"..."}`;
19
+ }
20
+ /** Parse the model's answer; null when it isn't the JSON asked for. */
21
+ export function parseDistilled(out) {
22
+ const start = out.indexOf('{'), end = out.lastIndexOf('}');
23
+ if (start === -1 || end === -1)
24
+ return null;
25
+ try {
26
+ const d = JSON.parse(out.slice(start, end + 1));
27
+ const rule = typeof d.rule === 'string' ? d.rule.trim() : '';
28
+ if (!rule || (d.rating !== 0 && d.rating !== 1))
29
+ return null;
30
+ return { rating: d.rating, rule };
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ export async function distillComment(comment, video) {
37
+ const out = await chat(distillPrompt(comment, video), {
38
+ system: 'You turn user feedback into concise profile rules. Output only the JSON asked for.',
39
+ });
40
+ const d = parseDistilled(out);
41
+ if (!d)
42
+ throw new Error(`could not distill the comment: ${out.slice(0, 200)}`);
43
+ return d;
44
+ }
package/dist/lib/env.js CHANGED
@@ -18,7 +18,6 @@ export function loadEnv() {
18
18
  dotenv.config({ path: ROOT_ENV_PATH });
19
19
  }
20
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
21
  export const REQUIRED_YOUTUBE = ['YT_BRIEFING_YOUTUBE_API_KEY'];
23
22
  /** Names from `names` that are missing or empty in the environment, in order. */
24
23
  export function missingEnv(names) {