yt-briefing 1.2.2 → 1.2.4
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 +4 -3
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/hooks/register.tsx +79 -55
package/README.md
CHANGED
|
@@ -137,9 +137,10 @@ The **Comment** field takes anything else. Type what you think in your own words
|
|
|
137
137
|
shows, skip those") and press Enter: Claude turns it into a standing rule for that channel and
|
|
138
138
|
infers the rating. `? your question` starts research with that question, `stop` closes the pane.
|
|
139
139
|
Claude Code on a phone has no text fields, so there the pane shows the four keys only.
|
|
140
|
-
Where no pane can be seen (`/yt` sent from claude.ai/code over Remote Control, or
|
|
141
|
-
places no panes), the same loop
|
|
142
|
-
OK / Weak / Research / Stop
|
|
140
|
+
Where no pane can be seen (`/yt` sent from claude.ai/code or the phone app over Remote Control, or
|
|
141
|
+
a surface that places no panes), `/yt` hands the same loop to Claude in the chat: each summary is
|
|
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.
|
|
143
144
|
|
|
144
145
|
Each step is the engine, not a chat turn: rating a video takes no turn and no context of your
|
|
145
146
|
session, and the next summary is usually ready before you have finished reading the current one.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yt-briefing",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.4",
|
|
4
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 a pane, and adapts to your ratings — one video at a time.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -153,64 +153,86 @@ async function comment($: EngineInterface, raw: string) {
|
|
|
153
153
|
if (saved?.rule) $.ui.toast(`Rule saved: ${saved.rule}`)
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
/** The dialog's choices; anything else is text typed under "Other". */
|
|
157
|
-
const CHOICES = ['OK', 'Weak', 'Research', 'Stop'] as const
|
|
158
|
-
|
|
159
|
-
/** The dialog draws plain text: drop the briefing's Markdown marks so no `**` or `###` shows. */
|
|
160
|
-
function plain(md: string): string {
|
|
161
|
-
return md
|
|
162
|
-
.replace(/^#{1,6}\s+/gm, '')
|
|
163
|
-
.replace(/\*\*(.+?)\*\*/g, '$1')
|
|
164
|
-
.replace(/^_(.+)_$/gm, '$1')
|
|
165
|
-
.replace(/^(\d+\.)\s+/gm, '$1 ')
|
|
166
|
-
.replace(/(\d{4}-\d{2}-\d{2})T[\d:.]+Z/g, '$1')
|
|
167
|
-
}
|
|
168
|
-
|
|
169
156
|
/** True when the person typed /yt through Remote Control and nothing attached can show a pane. */
|
|
170
157
|
async function paneUnseen($: EngineInterface, origin: { kind: string } | undefined): Promise<boolean> {
|
|
171
158
|
if (origin?.kind !== 'bridge') return false
|
|
172
159
|
return !(await $.session.surfaces()).some(s => s !== 'terminal')
|
|
173
160
|
}
|
|
174
161
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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.
|
|
166
|
+
|
|
167
|
+
/** True while the chat loop runs; a prompt the person types ends it. */
|
|
168
|
+
let looping = false
|
|
169
|
+
|
|
170
|
+
/** An answer this soon after its dialog opened is a tap meant for the dialog before it. */
|
|
171
|
+
const STRAY_TAP_MS = 1500
|
|
172
|
+
|
|
173
|
+
const CHOICES = ['OK', 'Weak', 'Research', 'Stop'] as const
|
|
174
|
+
|
|
175
|
+
/** The prompt for one briefing: the model sweeps and answers with the summary alone. */
|
|
176
|
+
function fetchPrompt(reset: boolean): string {
|
|
177
|
+
const sweep = [...engine('yt-sweep'), ...(reset ? ['--reset'] : [])].join(' ')
|
|
178
|
+
return [
|
|
179
|
+
`Next video of my YouTube briefing: run \`${sweep}\`. It prints one JSON line.`,
|
|
180
|
+
'If `status` is `rating_needed`, answer with `summary` verbatim as your whole message, Markdown kept: nothing before or after it, no question, no tool call after it (the rating dialog follows on its own).',
|
|
181
|
+
'Otherwise answer with one short line saying why there is nothing to rate. Write in the language of the summaries.',
|
|
182
|
+
].join('\n')
|
|
179
183
|
}
|
|
180
184
|
|
|
181
|
-
/**
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
185
|
+
/** Submit a prompt once the hook that wants it has answered: one submitted from inside would wait on it. */
|
|
186
|
+
function submitLater($: EngineInterface, text: string) {
|
|
187
|
+
$.clock.after(0, () => void $.prompt.submit({ text }).catch(err => {
|
|
188
|
+
looping = false
|
|
189
|
+
$.ui.toast(`Could not continue the briefing: ${String(err)}`)
|
|
190
|
+
}))
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** After a briefing turn: ask for the rating, record it and fetch the next one. */
|
|
194
|
+
async function rateAfterTurn($: EngineInterface) {
|
|
195
|
+
let out: SweepOut
|
|
196
|
+
try {
|
|
197
|
+
out = await runSweep($, false)
|
|
198
|
+
} catch {
|
|
199
|
+
looping = false
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
if (out.status !== 'rating_needed' || !out.pending || !out.summary) {
|
|
203
|
+
looping = false
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
const title = out.pending.title.length > 80 ? `${out.pending.title.slice(0, 79)}…` : out.pending.title
|
|
207
|
+
let answer: string
|
|
208
|
+
for (;;) {
|
|
209
|
+
const opened = Date.now()
|
|
192
210
|
try {
|
|
193
|
-
answer = (await $.ui.ask(
|
|
211
|
+
answer = (await $.ui.ask(`«${title}» — rating?`, { header: 'yt-briefing', options: CHOICES })).trim()
|
|
194
212
|
} catch {
|
|
213
|
+
looping = false
|
|
195
214
|
return
|
|
196
215
|
}
|
|
216
|
+
if (!looping) return
|
|
217
|
+
if (Date.now() - opened >= STRAY_TAP_MS) break
|
|
218
|
+
}
|
|
197
219
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
220
|
+
if (answer === 'Stop' || answer.toLowerCase() === 'stop' || !answer) {
|
|
221
|
+
looping = false
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
if (answer === 'Research' || answer.startsWith('?')) {
|
|
225
|
+
looping = false
|
|
226
|
+
if (!(await record($, ['--rating', '1']))) return
|
|
227
|
+
const q = answer.startsWith('?') ? answer.slice(1).trim() || undefined : undefined
|
|
228
|
+
return handOff($, out.pending, out.summary, out.lang, q)
|
|
229
|
+
}
|
|
230
|
+
const args = answer === 'OK' ? ['--rating', '1'] : answer === 'Weak' ? ['--rating', '0'] : ['--raw-comment', answer]
|
|
231
|
+
if (!(await record($, args))) {
|
|
232
|
+
looping = false
|
|
233
|
+
return
|
|
213
234
|
}
|
|
235
|
+
submitLater($, fetchPrompt(false))
|
|
214
236
|
}
|
|
215
237
|
|
|
216
238
|
export const register: Register = on => {
|
|
@@ -232,18 +254,20 @@ export const register: Register = on => {
|
|
|
232
254
|
}
|
|
233
255
|
await $.ui.close({ id: PANE })
|
|
234
256
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
} catch (err) {
|
|
241
|
-
return { text: `The engine did not answer: ${String(err)}` }
|
|
242
|
-
}
|
|
243
|
-
if (first.status !== 'rating_needed') return { text: endText(first) }
|
|
244
|
-
void dialogLoop($, first)
|
|
257
|
+
looping = true
|
|
258
|
+
submitLater($, fetchPrompt(true))
|
|
259
|
+
|
|
260
|
+
return { text: 'No pane here: the briefing runs in the chat.' }
|
|
261
|
+
})
|
|
245
262
|
|
|
246
|
-
|
|
263
|
+
on('prompt.submit', async ($, e, next) => {
|
|
264
|
+
if (looping && e.origin?.kind !== 'plugin') looping = false
|
|
265
|
+
return next(e)
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
on('turn.complete', async ($, e, next) => {
|
|
269
|
+
if (looping && !e.agentId && e.reason === 'answer') void rateAfterTurn($)
|
|
270
|
+
return next(e)
|
|
247
271
|
})
|
|
248
272
|
|
|
249
273
|
on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
|