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 +64 -84
- package/dist/bootstrap.js +28 -47
- package/dist/cli.js +1 -1
- package/dist/install-skill.js +22 -53
- package/dist/lib/config.js +13 -10
- package/dist/lib/distill.js +44 -0
- package/dist/lib/env.js +0 -1
- package/dist/lib/llm.js +90 -47
- package/dist/lib/skill-install.js +116 -108
- package/dist/yt-rating.js +42 -8
- package/dist/yt-search.js +8 -9
- package/dist/yt-sweep.js +10 -10
- package/docs/sync-across-machines.md +10 -17
- package/package.json +8 -7
- package/plugin/.claude-plugin/plugin.json +9 -0
- package/plugin/hooks/engine.ts +4 -0
- package/plugin/hooks/hooks.json +1 -0
- package/plugin/hooks/register.tsx +199 -0
- package/plugin/types/index.d.ts +28 -0
- package/.claude/skills/yt/SKILL.md +0 -69
- package/dist/lib/summary-gate.js +0 -61
- package/dist/yt-summary-gate.js +0 -77
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,
|
|
32
|
-
[
|
|
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
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
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
|
|
134
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
|
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.
|
|
11
|
+
* 3. Installs /yt (the rating pane) + /yt-transcribe + /yt-search into this Claude Code project
|
|
12
12
|
*
|
|
13
|
-
* It does NOT touch keys:
|
|
14
|
-
* reads
|
|
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,
|
|
21
|
-
import {
|
|
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 —
|
|
45
|
-
//
|
|
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.
|
|
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
|
|
110
|
-
//
|
|
111
|
-
//
|
|
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
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
for (const
|
|
119
|
-
console.log(`
|
|
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
|
|
110
|
+
console.log(` ! Couldn't install into .claude/skills (${e.message}) — run yt-briefing install-skill later.`);
|
|
131
111
|
}
|
|
132
|
-
// Preflight
|
|
133
|
-
//
|
|
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(
|
|
115
|
+
const missingKeys = missingEnv(REQUIRED_YOUTUBE);
|
|
137
116
|
if (missingKeys.length) {
|
|
138
|
-
console.log('\n ⚠
|
|
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} ·
|
|
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(
|
|
145
|
-
console.log(' 2.
|
|
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
|
|
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
|
package/dist/install-skill.js
CHANGED
|
@@ -1,63 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* install-skill —
|
|
4
|
-
*
|
|
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:
|
|
6
|
+
* yt-briefing install-skill # interactive: which project folder
|
|
10
7
|
*
|
|
11
|
-
* `
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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');
|
package/dist/lib/config.js
CHANGED
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* User preferences for yt-briefing —
|
|
3
|
-
*
|
|
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
|
-
*
|
|
6
|
-
*
|
|
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
|
-
|
|
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 →
|
|
17
|
+
catch { /* malformed → defaults */ }
|
|
19
18
|
}
|
|
20
|
-
|
|
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) {
|