yt-briefing 1.2.5 → 1.3.1
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 +30 -38
- package/dist/bootstrap.js +2 -2
- package/dist/cli.js +1 -1
- package/dist/install-skill.js +2 -2
- package/dist/lib/distill.js +2 -2
- package/dist/lib/skill-install.js +3 -3
- package/dist/yt-sweep.js +1 -0
- package/docs/sync-across-machines.md +1 -1
- package/package.json +2 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/hooks/engine.ts +1 -1
- package/plugin/hooks/register.tsx +13 -167
- package/plugin/types/index.d.ts +0 -21
package/README.md
CHANGED
|
@@ -16,8 +16,8 @@ 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`
|
|
20
|
-
|
|
19
|
+
yt-briefing runs inside [Claude Code](https://claude.com/claude-code). `/yt` runs the briefing in
|
|
20
|
+
your chat, and the filtering and summaries run on your own Claude Code login. There
|
|
21
21
|
is no separate model, provider or LLM key to set up.
|
|
22
22
|
|
|
23
23
|
## First run vs later
|
|
@@ -120,32 +120,25 @@ it to go deeper, lower it for a quicker pass:
|
|
|
120
120
|
|
|
121
121
|
## Run it
|
|
122
122
|
|
|
123
|
-
Open your project in Claude Code and type `/yt`.
|
|
124
|
-
under it
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
|
130
|
-
|
|
131
|
-
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
Claude's message, a short dialog asks for the rating (OK / Weak / Research / Stop), and a typed
|
|
143
|
-
**Other** answer works as the Comment field. This path uses the model, so it counts toward plan usage.
|
|
144
|
-
|
|
145
|
-
Each step is the engine, not a chat turn: rating a video takes no turn and no context of your
|
|
146
|
-
session, and the next summary is usually ready before you have finished reading the current one.
|
|
147
|
-
The summaries themselves still count toward your Claude plan's usage, like any other Claude Code
|
|
148
|
-
work.
|
|
123
|
+
Open your project in Claude Code and type `/yt`. Each summary arrives as Claude's message in the
|
|
124
|
+
chat, and a short dialog under it asks for the rating:
|
|
125
|
+
|
|
126
|
+
| Answer | What it does |
|
|
127
|
+
|--------|--------------|
|
|
128
|
+
| OK | Neutral. The video is marked as seen, the next one loads. |
|
|
129
|
+
| Weak | Worthless. The title goes to the channel's skip examples, so the filter learns to drop titles like it. |
|
|
130
|
+
| Research | Ends the loop and hands this video to Claude, see below. |
|
|
131
|
+
| Stop | Ends the loop. The next `/yt` resumes where you stopped. |
|
|
132
|
+
|
|
133
|
+
The dialog's **Other** field takes anything else. Type what you think in your own words ("too many
|
|
134
|
+
panel shows, skip those"): Claude turns it into a standing rule for that channel and infers the
|
|
135
|
+
rating. `? your question` starts research with that question, `stop` ends the loop. A prompt you
|
|
136
|
+
type in the chat also ends it.
|
|
137
|
+
|
|
138
|
+
It works the same in the terminal, the desktop app, claude.ai/code and the phone app. Each summary
|
|
139
|
+
is a turn of its own that ends with the summary, so the phone app shows it whole instead of folding
|
|
140
|
+
it into a one-line digest. Claude pastes the summary the engine wrote, so a turn is short, but it
|
|
141
|
+
still counts toward your Claude plan's usage, like the summaries themselves.
|
|
149
142
|
|
|
150
143
|
`/yt` is a Claude Code mod (a plugin in `.claude/skills/yt-briefing/`). Claude Code loads it on
|
|
151
144
|
its own once you trust the project folder. If `/yt` is not listed, start a fresh session. To
|
|
@@ -156,8 +149,8 @@ installs `/yt`, `/yt-transcribe` and `/yt-search`).
|
|
|
156
149
|
|
|
157
150
|
Tech channels announce something new every week, and the usual fate is "looks interesting" →
|
|
158
151
|
to-do list → never. So next to OK/Weak there is a third key: **Research**. Press it, or type
|
|
159
|
-
`? your question` into the
|
|
160
|
-
|
|
152
|
+
`? your question` into the dialog's Other field, and the loop ends there: the video lands in your
|
|
153
|
+
chat with its briefing and the command for its full transcript. Claude works your
|
|
161
154
|
question with you, against your own codebase if you ask "would this fit my project", against the
|
|
162
155
|
web if the claims need checking. A quick feedback loop instead of a shelf. The video is marked as
|
|
163
156
|
seen, and the next `/yt` resumes the queue right where you broke off.
|
|
@@ -166,8 +159,8 @@ seen, and the next `/yt` resumes the queue right where you broke off.
|
|
|
166
159
|
|
|
167
160
|
1.0 runs on Claude Code only. The OpenAI-compatible provider and its three `YT_BRIEFING_LLM_*`
|
|
168
161
|
keys are gone (delete them from `.env`), and the chat-driven `/yt` skill with its rating popup is
|
|
169
|
-
replaced by the
|
|
170
|
-
|
|
162
|
+
replaced by the `/yt` mod. Run `npx yt-briefing install-skill` once in your project: it installs
|
|
163
|
+
the mod, and removes the old `/yt` skill and the summary-gate hook from `.claude/settings.json`. Your
|
|
171
164
|
channels, profiles and ratings in `.yt-briefing/data/` carry over unchanged.
|
|
172
165
|
|
|
173
166
|
## Why Claude Code, and nothing else
|
|
@@ -179,13 +172,12 @@ is set in your environment, the engine removes it for that call, because Claude
|
|
|
179
172
|
otherwise bill it as API usage instead of using your login.
|
|
180
173
|
|
|
181
174
|
The engine still works ahead in the background. It expands channels in parallel and summarizes the
|
|
182
|
-
next video while you rate the current one, so each step is usually ready with no wait. The
|
|
183
|
-
|
|
175
|
+
next video while you rate the current one, so each step is usually ready with no wait. The mod
|
|
176
|
+
asks for the rating and records it itself, so the model never has to remember the protocol.
|
|
184
177
|
|
|
185
|
-
Supporting every agent that reads `SKILL.md` meant
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
which is why 1.0 drops the other agents.
|
|
178
|
+
Supporting every agent that reads `SKILL.md` meant leaving the rating to the model: it asked the
|
|
179
|
+
question and recorded the answer, plus a hook to make sure the summary was really shown. A Claude
|
|
180
|
+
Code mod drives the loop itself, which is why 1.0 drops the other agents.
|
|
189
181
|
|
|
190
182
|
## Why one transcript at a time
|
|
191
183
|
|
package/dist/bootstrap.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
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. Installs /yt (the rating
|
|
11
|
+
* 3. Installs /yt (the rating loop) + /yt-transcribe + /yt-search into this Claude Code project
|
|
12
12
|
*
|
|
13
13
|
* It does NOT touch keys: the one key (YouTube Data API) lives in your project root .env (see README →
|
|
14
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.
|
|
@@ -124,7 +124,7 @@ function main() {
|
|
|
124
124
|
console.log(`\n ⚠ ${noClaude}`);
|
|
125
125
|
console.log('\n Next:');
|
|
126
126
|
console.log(' 1. Open this folder in Claude Code (trust it when asked).');
|
|
127
|
-
console.log(' 2. Type /yt — the briefing
|
|
127
|
+
console.log(' 2. Type /yt — the briefing runs in the chat. (/yt-transcribe <url> for one video)\n');
|
|
128
128
|
}
|
|
129
129
|
try {
|
|
130
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 /yt
|
|
9
|
+
* yt-briefing install-skill install /yt + 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,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* install-skill — install yt-briefing into a Claude Code project: the `/yt` mod (the rating
|
|
3
|
+
* install-skill — install yt-briefing into a Claude Code project: the `/yt` mod (the rating loop)
|
|
4
4
|
* plus the `/yt-transcribe` and `/yt-search` skills, all under `<project>/.claude/skills/`.
|
|
5
5
|
*
|
|
6
6
|
* yt-briefing install-skill # interactive: which project folder
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { installAll, isPackageDevCwd } from "./lib/skill-install.js";
|
|
14
14
|
import { question } from "./lib/prompt.js";
|
|
15
15
|
const ask = (q, def = '') => question(def ? `${q} [${def}]:` : `${q}:`).trim() || def;
|
|
16
|
-
console.log('\n Install /yt
|
|
16
|
+
console.log('\n Install /yt + /yt-transcribe + /yt-search into a Claude Code project.\n');
|
|
17
17
|
console.log(' 1) This project (current folder) — recommended');
|
|
18
18
|
console.log(' 2) Another project folder\n');
|
|
19
19
|
const other = ask(' Where', '1') === '2';
|
package/dist/lib/distill.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Turn a raw rating comment ("too much politics, skip these panels") into what the profile
|
|
3
3
|
* stores: one clean, generalizable rule for `## Notes`, plus the rating it implies.
|
|
4
4
|
*
|
|
5
|
-
* This used to be the agent's job in the chat loop.
|
|
6
|
-
*
|
|
5
|
+
* This used to be the agent's job in the chat loop. The mod records ratings itself, with no agent
|
|
6
|
+
* step for it, so the engine does it with the same `claude -p` call it uses everywhere.
|
|
7
7
|
*/
|
|
8
8
|
import { chat } from "./llm.js";
|
|
9
9
|
export function distillPrompt(comment, video) {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* `<project>/.claude/skills/`:
|
|
7
7
|
*
|
|
8
8
|
* yt-briefing/ the Claude Code mod (a plugin of function hooks) behind `/yt`: the rating loop
|
|
9
|
-
*
|
|
9
|
+
* in the chat. Claude Code loads a plugin from the project's skills folder by itself
|
|
10
10
|
* (as `yt-briefing@skills-dir`) once the workspace is trusted.
|
|
11
11
|
* yt-transcribe/ one-shot skill: a single video's transcript → summary.
|
|
12
12
|
* yt-search/ skill: search within one channel → triage → comparison.
|
|
@@ -79,7 +79,7 @@ export function skillBody(name, dist = false) {
|
|
|
79
79
|
.replace(/data\//g, toProjectRel(DATA_DIR) + '/');
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
82
|
-
* The mod's `hooks/engine.ts`: how the
|
|
82
|
+
* The mod's `hooks/engine.ts`: how the mod runs the engine. `dist=false` is the shipped dev form;
|
|
83
83
|
* `dist=true` points at the compiled scripts, relative to the project root (the session's cwd).
|
|
84
84
|
*/
|
|
85
85
|
export function engineModule(dist = false) {
|
|
@@ -88,7 +88,7 @@ export function engineModule(dist = false) {
|
|
|
88
88
|
return shipped;
|
|
89
89
|
const rel = toProjectRel(DIST_DIR);
|
|
90
90
|
return [
|
|
91
|
-
'// How the
|
|
91
|
+
'// How the mod runs the engine, relative to the project root (the session\'s working directory).',
|
|
92
92
|
'// Written by `yt-briefing install-skill`; re-run it after moving the project or switching runtime.',
|
|
93
93
|
`export const engine = (name: string): string[] => ['${runtimeName()}', \`${rel}/\${name}.js\`]`,
|
|
94
94
|
'',
|
package/dist/yt-sweep.js
CHANGED
|
@@ -296,6 +296,7 @@ Steps:
|
|
|
296
296
|
- 2-5 numbered thematic sections × 2-5 sentences each
|
|
297
297
|
- At most 5-8 short quotes from the transcript
|
|
298
298
|
- No timestamps
|
|
299
|
+
- No section of your own on the material's credibility, sourcing or bias (no "reliability notes", no "caveats"): report what the video says. Only a channel directive can ask for such an assessment.
|
|
299
300
|
3. Apply the channel directives above, if any — they shape the briefing's content, emphasis, and tone, but not the header/subtitle/section skeleton. Put any directive-driven assessment in its own short, clearly-labeled section.
|
|
300
301
|
4. Language: natural ${LANG}. Avoid calques/anglicisms; use foreign words only for proper nouns or established technical terms. Section headers should be verb phrases, not noun stacks.
|
|
301
302
|
5. Output: ONLY the briefing itself OR 'OFFTOPIC: ...'. No preamble, and no meta-commentary about these instructions.`;
|
|
@@ -99,7 +99,7 @@ version, so every machine gets the setting):
|
|
|
99
99
|
}
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
-
The engine runs `after_rate` after every recorded rating, from
|
|
102
|
+
The engine runs `after_rate` after every recorded rating, from `/yt` and from the CLI
|
|
103
103
|
(`yt-briefing rate`) alike. It runs detached from the project root, so a slow push never holds the
|
|
104
104
|
next video, and a failing script never fails the rating (the rating is already on disk). The
|
|
105
105
|
engine itself still never runs git: the command is yours.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yt-briefing",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "A self-learning YouTube briefing for Claude Code: it sweeps the channels you follow, filters noise in two stages (title, then transcript), gives you a short briefing of the rest in your language in
|
|
3
|
+
"version": "1.3.1",
|
|
4
|
+
"description": "A self-learning YouTube briefing for Claude Code: it sweeps the channels you follow, filters noise in two stages (title, then transcript), gives you a short briefing of the rest in your language in the chat, and adapts to your ratings — one video at a time.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"yt-briefing": "dist/cli.js"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yt-briefing",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "The /yt briefing loop
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "The /yt briefing loop in the chat: summary, rating dialog, research hand-off.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Michal Ryzio"
|
|
7
7
|
},
|
package/plugin/hooks/engine.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// How the
|
|
1
|
+
// How the mod runs the engine, relative to the project root (the session's working directory).
|
|
2
2
|
// This is the dev-clone form; `yt-briefing install-skill` / `init` rewrites it for the project it
|
|
3
3
|
// installs into (the compiled dist/ under node_modules, with the runtime that ran the installer).
|
|
4
4
|
export const engine = (name: string): string[] => ['bun', `src/${name}.ts`]
|
|
@@ -1,38 +1,23 @@
|
|
|
1
|
-
import { atom, read, update } from 'claude-code'
|
|
2
1
|
import type { EngineInterface, Register } from 'claude-code'
|
|
3
2
|
|
|
4
|
-
import type { Pending
|
|
3
|
+
import type { Pending } from '../types'
|
|
5
4
|
import { engine } from './engine.ts'
|
|
6
5
|
|
|
7
|
-
// The /yt rating loop
|
|
8
|
-
//
|
|
9
|
-
// the
|
|
10
|
-
//
|
|
11
|
-
// loop runs in the engine's own question dialog instead.
|
|
12
|
-
|
|
13
|
-
const PANE = 'yt-briefing'
|
|
14
|
-
const view = atom({ plugin: 'yt-briefing', key: 'view' } as const, { phase: 'idle' } as View)
|
|
6
|
+
// The /yt rating loop, in the chat on every surface. Each briefing is the model's whole answer to a
|
|
7
|
+
// prompt this plugin submits, so the summary shows in full in the terminal, the desktop app and on
|
|
8
|
+
// a phone; the rating dialog opens once that turn has ended. Ratings and comments are written by
|
|
9
|
+
// the engine, never by this module.
|
|
15
10
|
|
|
16
11
|
/** A sweep can expand channels, fetch transcripts and summarize; give it the engine's full budget. */
|
|
17
12
|
const SWEEP_MS = 10 * 60_000
|
|
18
13
|
/** A rating is a file write; a raw comment adds one `claude -p` call to distill it. */
|
|
19
14
|
const RATE_MS = 3 * 60_000
|
|
20
15
|
|
|
21
|
-
type Skip = { channel: string; title: string; reason: string }
|
|
22
16
|
type SweepOut = {
|
|
23
17
|
status: string
|
|
24
18
|
summary?: string
|
|
25
19
|
pending?: Pending
|
|
26
20
|
lang?: string
|
|
27
|
-
error?: string
|
|
28
|
-
skipped?: number
|
|
29
|
-
skips?: Skip[]
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const STATUS_TEXT: Record<string, string> = {
|
|
33
|
-
done: 'Sweep finished: nothing left to rate.',
|
|
34
|
-
rate_limited: 'YouTube is blocking this IP (429 / captcha). See README → Proxy.',
|
|
35
|
-
tooling_error: 'Transcript tooling failed (proxy, yt-dlp or network). See README → Proxy.',
|
|
36
21
|
}
|
|
37
22
|
|
|
38
23
|
/** The engine prints one JSON line on stdout; take the last non-empty line to be safe. */
|
|
@@ -41,64 +26,12 @@ function parseOut(stdout: string): SweepOut {
|
|
|
41
26
|
return JSON.parse(line) as SweepOut
|
|
42
27
|
}
|
|
43
28
|
|
|
44
|
-
/** Replace the whole view; the one writer for a fresh phase. */
|
|
45
|
-
async function setView($: EngineInterface, next: View) {
|
|
46
|
-
await update($, view, () => next)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function skipLine(out: SweepOut): string | undefined {
|
|
50
|
-
if (!out.skipped || !out.skips?.length) return undefined
|
|
51
|
-
const list = out.skips.map(s => `${s.channel} «${s.title}» — ${s.reason}`).join('; ')
|
|
52
|
-
return `Skipped ${out.skipped}: ${list}`
|
|
53
|
-
}
|
|
54
|
-
|
|
55
29
|
/** One engine sweep: the next ratable video, or the end state. Throws when the engine does not answer. */
|
|
56
|
-
async function runSweep($: EngineInterface
|
|
57
|
-
const run = await $.process.run(
|
|
30
|
+
async function runSweep($: EngineInterface): Promise<SweepOut> {
|
|
31
|
+
const run = await $.process.run(engine('yt-sweep'), { timeoutMs: SWEEP_MS })
|
|
58
32
|
return parseOut(run.stdout)
|
|
59
33
|
}
|
|
60
34
|
|
|
61
|
-
/** What a sweep that found nothing to rate says, skips included. */
|
|
62
|
-
function endText(out: SweepOut): string {
|
|
63
|
-
const text = out.status === 'done' ? STATUS_TEXT.done : out.error ?? STATUS_TEXT[out.status] ?? `Engine status: ${out.status}`
|
|
64
|
-
const skipped = skipLine(out)
|
|
65
|
-
return skipped ? `${text}\n${skipped}` : text
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Advance to the next ratable video and put it (or the end state) in the view. */
|
|
69
|
-
async function sweep($: EngineInterface, reset: boolean) {
|
|
70
|
-
await setView($, { phase: 'loading' })
|
|
71
|
-
let out: SweepOut
|
|
72
|
-
try {
|
|
73
|
-
out = await runSweep($, reset)
|
|
74
|
-
} catch (err) {
|
|
75
|
-
await setView($, { phase: 'error', message: `The engine did not answer: ${String(err)}` })
|
|
76
|
-
return
|
|
77
|
-
}
|
|
78
|
-
const skipped = skipLine(out)
|
|
79
|
-
if (out.status === 'rating_needed' && out.summary && out.pending) {
|
|
80
|
-
await setView($, { phase: 'rating', summary: out.summary, pending: out.pending, lang: out.lang, skipped })
|
|
81
|
-
} else if (out.status === 'done') {
|
|
82
|
-
await setView($, { phase: 'done', message: STATUS_TEXT.done, skipped })
|
|
83
|
-
} else {
|
|
84
|
-
await setView($, { phase: 'error', message: out.error ?? STATUS_TEXT[out.status] ?? `Engine status: ${out.status}`, skipped })
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Record a rating (engine args), then move on; on failure stay on the video and say why. */
|
|
89
|
-
async function rate($: EngineInterface, args: string[], thenNext = true): Promise<{ rule?: string } | null> {
|
|
90
|
-
const v = await read($, view)
|
|
91
|
-
if (v.phase !== 'rating' || v.busy) return null
|
|
92
|
-
await update($, view, cur => ({ ...cur, busy: args[0] === '--raw-comment' ? 'Turning the comment into a rule…' : 'Saving…' }))
|
|
93
|
-
const result = await record($, args)
|
|
94
|
-
if (!result) {
|
|
95
|
-
await update($, view, cur => ({ ...cur, busy: undefined }))
|
|
96
|
-
return null
|
|
97
|
-
}
|
|
98
|
-
if (thenNext) void sweep($, false)
|
|
99
|
-
return result
|
|
100
|
-
}
|
|
101
|
-
|
|
102
35
|
/** Run the rating engine; on failure toast why and resolve null. */
|
|
103
36
|
async function record($: EngineInterface, args: string[]): Promise<{ rule?: string } | null> {
|
|
104
37
|
const run = await $.process.run([...engine('yt-rating'), ...args], { timeoutMs: RATE_MS }).catch(err => ({
|
|
@@ -113,15 +46,6 @@ async function record($: EngineInterface, args: string[]): Promise<{ rule?: stri
|
|
|
113
46
|
return result
|
|
114
47
|
}
|
|
115
48
|
|
|
116
|
-
/** Research: mark the video seen, close the pane and hand the video to the session. */
|
|
117
|
-
async function research($: EngineInterface, question?: string) {
|
|
118
|
-
const v = await read($, view)
|
|
119
|
-
if (!v.pending || !v.summary) return
|
|
120
|
-
if (!(await rate($, ['--rating', '1'], false))) return
|
|
121
|
-
await $.ui.close({ id: PANE })
|
|
122
|
-
await handOff($, v.pending, v.summary, v.lang, question)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
49
|
/** Give the video to the session as the person's prompt, to research it together. */
|
|
126
50
|
async function handOff($: EngineInterface, p: Pending, summary: string, lang: string | undefined, question?: string) {
|
|
127
51
|
const transcript = [...engine('yt-transcript'), p.videoId, '--lang', 'auto'].join(' ')
|
|
@@ -144,25 +68,8 @@ async function handOff($: EngineInterface, p: Pending, summary: string, lang: st
|
|
|
144
68
|
}).catch(err => $.ui.toast(`Could not hand the video to the session: ${String(err)}`))
|
|
145
69
|
}
|
|
146
70
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (!text) return
|
|
150
|
-
if (text.toLowerCase() === 'stop') return void $.ui.close({ id: PANE })
|
|
151
|
-
if (text.startsWith('?')) return research($, text.slice(1).trim() || undefined)
|
|
152
|
-
const saved = await rate($, ['--raw-comment', text])
|
|
153
|
-
if (saved?.rule) $.ui.toast(`Rule saved: ${saved.rule}`)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** True when the person typed /yt through Remote Control and nothing attached can show a pane. */
|
|
157
|
-
async function paneUnseen($: EngineInterface, origin: { kind: string } | undefined): Promise<boolean> {
|
|
158
|
-
if (origin?.kind !== 'bridge') return false
|
|
159
|
-
return !(await $.session.surfaces()).some(s => s !== 'terminal')
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Where no pane can be seen the loop runs in the chat. A phone or web client draws a plugin's
|
|
163
|
-
// command output and its dialogs, but not its log lines, and it folds text written mid-turn into a
|
|
164
|
-
// one-line digest; only a turn's last message shows whole. So each briefing is the model's whole
|
|
165
|
-
// answer to a prompt this plugin submits, and the rating dialog opens once that turn has ended.
|
|
71
|
+
// A phone or web client draws a plugin's command output and its dialogs, but not its log lines,
|
|
72
|
+
// and it folds text written mid-turn into a one-line digest; only a turn's last message shows whole.
|
|
166
73
|
|
|
167
74
|
/** True while the chat loop runs; a prompt the person types ends it. */
|
|
168
75
|
let looping = false
|
|
@@ -201,7 +108,7 @@ function submitLater($: EngineInterface, reset: boolean) {
|
|
|
201
108
|
async function rateAfterTurn($: EngineInterface) {
|
|
202
109
|
let out: SweepOut
|
|
203
110
|
try {
|
|
204
|
-
out = await runSweep(
|
|
111
|
+
out = await runSweep($)
|
|
205
112
|
} catch {
|
|
206
113
|
looping = false
|
|
207
114
|
return
|
|
@@ -246,25 +153,17 @@ export const register: Register = on => {
|
|
|
246
153
|
on('session.start', async ($, e, next) => {
|
|
247
154
|
await $.command.register({
|
|
248
155
|
name: 'yt',
|
|
249
|
-
description: 'Rate the next videos from the YouTube channels you follow
|
|
156
|
+
description: 'Rate the next videos from the YouTube channels you follow',
|
|
250
157
|
})
|
|
251
158
|
|
|
252
159
|
return next(e)
|
|
253
160
|
})
|
|
254
161
|
|
|
255
|
-
on('command.run', { command: 'yt' }, async (
|
|
256
|
-
if (!(await paneUnseen($, e.origin))) {
|
|
257
|
-
const opened = await $.ui.open({ id: PANE, title: 'yt-briefing', focus: true, closeOnEscape: true })
|
|
258
|
-
if (opened.isPlaced) {
|
|
259
|
-
void sweep($, true)
|
|
260
|
-
return { text: 'Briefing opened in a pane.' }
|
|
261
|
-
}
|
|
262
|
-
await $.ui.close({ id: PANE })
|
|
263
|
-
}
|
|
162
|
+
on('command.run', { command: 'yt' }, async ($) => {
|
|
264
163
|
looping = true
|
|
265
164
|
submitLater($, true)
|
|
266
165
|
|
|
267
|
-
return { text: '
|
|
166
|
+
return { text: 'The briefing runs in the chat.' }
|
|
268
167
|
})
|
|
269
168
|
|
|
270
169
|
// The engine shows a plugin none of its own prompts, so a prompt seen here is the person's.
|
|
@@ -284,57 +183,4 @@ export const register: Register = on => {
|
|
|
284
183
|
if (looping && !e.agentId && e.reason === 'answer') void rateAfterTurn($)
|
|
285
184
|
return next(e)
|
|
286
185
|
})
|
|
287
|
-
|
|
288
|
-
on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
|
|
289
|
-
const { Box, Text, Button, Markdown } = $.ui.resolve(e)
|
|
290
|
-
const v = await read($, view)
|
|
291
|
-
const close = () => void $.ui.close({ id: PANE })
|
|
292
|
-
|
|
293
|
-
if (v.phase === 'idle' || v.phase === 'loading') {
|
|
294
|
-
return <Text dimColor>Finding the next video…</Text>
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if (v.phase !== 'rating') {
|
|
298
|
-
return (
|
|
299
|
-
<Box flexDirection="column">
|
|
300
|
-
{v.skipped && <Text dimColor>{v.skipped}</Text>}
|
|
301
|
-
<Text>{v.message ?? ''}</Text>
|
|
302
|
-
<Button key="close" label="Close" hotkey="c" role="dismiss" onPress={close} />
|
|
303
|
-
</Box>
|
|
304
|
-
)
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const idle = !v.busy
|
|
308
|
-
const buttons = (
|
|
309
|
-
<Box flexDirection="row" gap={2}>
|
|
310
|
-
<Button key="ok" label="OK" hotkey="1" variant="primary" dimColor={!idle}
|
|
311
|
-
onPress={() => void rate($, ['--rating', '1'])} />
|
|
312
|
-
<Button key="weak" label="Weak" hotkey="2" dimColor={!idle}
|
|
313
|
-
onPress={() => void rate($, ['--rating', '0'])} />
|
|
314
|
-
<Button key="research" label="Research" hotkey="3" dimColor={!idle}
|
|
315
|
-
onPress={() => void research($)} />
|
|
316
|
-
<Button key="stop" label="Stop" hotkey="4" role="dismiss" onPress={close} />
|
|
317
|
-
</Box>
|
|
318
|
-
)
|
|
319
|
-
|
|
320
|
-
let field = null
|
|
321
|
-
if (e.surface !== 'mobile') {
|
|
322
|
-
const { Input } = $.ui.resolve(e)
|
|
323
|
-
field = (
|
|
324
|
-
<Input key="comment" label="Comment" submitLabel="save rule"
|
|
325
|
-
placeholder="becomes a rule for this channel · ?question = research · stop"
|
|
326
|
-
onSubmit={value => void comment($, value)} />
|
|
327
|
-
)
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
return (
|
|
331
|
-
<Box flexDirection="column" gap={1}>
|
|
332
|
-
{v.skipped && <Text dimColor>{v.skipped}</Text>}
|
|
333
|
-
<Markdown text={(v.summary ?? '').slice(0, 10000)} />
|
|
334
|
-
{v.busy ? <Text dimColor>{v.busy}</Text> : buttons}
|
|
335
|
-
{field}
|
|
336
|
-
<Text dimColor>OK = neutral · Weak = teach the filter to skip titles like this · Research = dig into it with Claude</Text>
|
|
337
|
-
</Box>
|
|
338
|
-
)
|
|
339
|
-
})
|
|
340
186
|
}
|
package/plugin/types/index.d.ts
CHANGED
|
@@ -5,24 +5,3 @@ export type Pending = {
|
|
|
5
5
|
type: string
|
|
6
6
|
publishedAt: string
|
|
7
7
|
}
|
|
8
|
-
|
|
9
|
-
export type View = {
|
|
10
|
-
phase: 'idle' | 'loading' | 'rating' | 'done' | 'error'
|
|
11
|
-
/** Briefing markdown of the video awaiting a rating. */
|
|
12
|
-
summary?: string
|
|
13
|
-
pending?: Pending
|
|
14
|
-
/** Language the engine writes in; the research hand-off asks for it too. */
|
|
15
|
-
lang?: string
|
|
16
|
-
/** Videos the filters dropped on the way to this one, one line. */
|
|
17
|
-
skipped?: string
|
|
18
|
-
/** Error or status text for the done/error phases. */
|
|
19
|
-
message?: string
|
|
20
|
-
/** Set while a rating or comment is being written; presses are ignored meanwhile. */
|
|
21
|
-
busy?: string
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
declare module 'claude-code' {
|
|
25
|
-
interface PluginState {
|
|
26
|
-
'yt-briefing': { view: View }
|
|
27
|
-
}
|
|
28
|
-
}
|