yt-briefing 1.3.3 → 1.3.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/dist/cli.js +1 -0
- package/dist/yt-sweep.js +31 -6
- package/package.json +1 -1
- package/plugin/hooks/register.tsx +10 -7
package/dist/cli.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
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
|
|
13
|
+
* yt-briefing sweep --pending read-only: the video already shown and not yet rated
|
|
13
14
|
* yt-briefing rate --rating 0|1 [...] record a rating for the pending video
|
|
14
15
|
* yt-briefing transcribe <url|id> print a single video's transcript
|
|
15
16
|
* yt-briefing search "<intent>" --channel <@handle|url> search within one channel → triage → compare
|
package/dist/yt-sweep.js
CHANGED
|
@@ -8,12 +8,14 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Usage:
|
|
10
10
|
* bun src/yt-sweep.ts [--reset]
|
|
11
|
+
* bun src/yt-sweep.ts --pending (read-only: the video already emitted, still unrated)
|
|
11
12
|
* bun src/yt-sweep.ts --prefetch <videoId> (internal: detached next-video warmup)
|
|
12
13
|
* bun src/yt-sweep.ts --fill (internal: detached queue builder)
|
|
13
14
|
*
|
|
14
15
|
* Output (stdout, single JSON line):
|
|
15
16
|
* {"status":"rating_needed","summary":"<md>","pending":{channel,videoId,title,type,publishedAt,is_baseline}} (+ "lang")
|
|
16
17
|
* {"status":"done"}
|
|
18
|
+
* {"status":"no_pending"} (--pending only: no emitted video awaits a rating)
|
|
17
19
|
* {"status":"rate_limited"}
|
|
18
20
|
*
|
|
19
21
|
* The engine ONLY writes files under DATA_DIR — it never runs git or any VCS. If you
|
|
@@ -68,6 +70,7 @@ const CONCURRENCY = 6;
|
|
|
68
70
|
const argv = process.argv.slice(2);
|
|
69
71
|
const reset = argv.includes('--reset');
|
|
70
72
|
const fillMode = argv.includes('--fill');
|
|
73
|
+
const pendingMode = argv.includes('--pending');
|
|
71
74
|
const pfIdx = argv.indexOf('--prefetch');
|
|
72
75
|
const prefetchTarget = pfIdx !== -1 ? argv[pfIdx + 1] : null;
|
|
73
76
|
const today = new Date().toISOString().slice(0, 10);
|
|
@@ -458,6 +461,13 @@ async function processItem(item) {
|
|
|
458
461
|
return { kind: 'skip', status: 'content_skip', reason: summary.slice(9).trim() };
|
|
459
462
|
return { kind: 'ratable', summary };
|
|
460
463
|
}
|
|
464
|
+
/** The video's metadata as `rating_needed` reports it and yt-rating reads it from pending.json. */
|
|
465
|
+
function pendingOf(item) {
|
|
466
|
+
return {
|
|
467
|
+
channel: item.channel, videoId: item.videoId, title: item.title,
|
|
468
|
+
type: item.type, publishedAt: item.publishedAt, is_baseline: item.is_baseline,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
461
471
|
async function advance(queue) {
|
|
462
472
|
while (true) {
|
|
463
473
|
// Pull in the background fill (or expand a channel ourselves) until there's an
|
|
@@ -472,8 +482,8 @@ async function advance(queue) {
|
|
|
472
482
|
dropHead(queue);
|
|
473
483
|
continue;
|
|
474
484
|
}
|
|
475
|
-
// Already emitted (
|
|
476
|
-
//
|
|
485
|
+
// Already emitted (a repeat sweep before the rating) or warmed by the background child? Use it
|
|
486
|
+
// and skip the live fetch + content filter.
|
|
477
487
|
const emitted = loadCachedSummary(PENDING_FILE, item.videoId);
|
|
478
488
|
const cached = emitted ?? loadPrefetch(item.videoId);
|
|
479
489
|
if (cached)
|
|
@@ -497,10 +507,7 @@ async function advance(queue) {
|
|
|
497
507
|
}
|
|
498
508
|
// Ratable — hand off to the frontend. Leave item at queue head (rating bumps state,
|
|
499
509
|
// next call detects pointer === videoId and drops it).
|
|
500
|
-
const pending =
|
|
501
|
-
channel: item.channel, videoId: item.videoId, title: item.title,
|
|
502
|
-
type: item.type, publishedAt: item.publishedAt, is_baseline: item.is_baseline,
|
|
503
|
-
};
|
|
510
|
+
const pending = pendingOf(item);
|
|
504
511
|
writeAtomic(PENDING_FILE, { ...pending, summary: result.summary, built_at: today });
|
|
505
512
|
writeFileSync(QUEUE_FILE, JSON.stringify(queue));
|
|
506
513
|
// Warm the NEXT video in the background while the user rates this one. A repeat emit of the
|
|
@@ -516,6 +523,21 @@ async function advance(queue) {
|
|
|
516
523
|
clearPrefetch();
|
|
517
524
|
emit({ status: 'done' });
|
|
518
525
|
}
|
|
526
|
+
/**
|
|
527
|
+
* --pending mode: the video the sweep already emitted, if it still awaits a rating. The plugin asks
|
|
528
|
+
* this after a briefing turn to open the rating dialog. Read-only and offline on purpose: a real
|
|
529
|
+
* sweep there rebuilt the queue once `done` had removed it, and the dialog then asked about a new
|
|
530
|
+
* video the chat never showed.
|
|
531
|
+
*/
|
|
532
|
+
function runPending() {
|
|
533
|
+
const queue = loadQueue();
|
|
534
|
+
const head = queue?.items[0];
|
|
535
|
+
const summary = head && loadCachedSummary(PENDING_FILE, head.videoId);
|
|
536
|
+
if (!queue || !head || !summary || isResolved(statePointer(head), head.videoId, queue.seen)) {
|
|
537
|
+
emit({ status: 'no_pending' });
|
|
538
|
+
}
|
|
539
|
+
emit({ status: 'rating_needed', summary, pending: pendingOf(head), lang: LANG });
|
|
540
|
+
}
|
|
519
541
|
/**
|
|
520
542
|
* --prefetch mode: compute the summary for one specific queued video and cache it, with
|
|
521
543
|
* NO side effects on state.md / queue / pending. Runs detached while the user rates the
|
|
@@ -573,6 +595,9 @@ if (prefetchTarget) {
|
|
|
573
595
|
if (fillMode) {
|
|
574
596
|
await runFill(); // detached background queue builder — never returns
|
|
575
597
|
}
|
|
598
|
+
if (pendingMode) {
|
|
599
|
+
runPending(); // read-only answer for the rating dialog — never returns
|
|
600
|
+
}
|
|
576
601
|
if (reset) {
|
|
577
602
|
if (existsSync(QUEUE_FILE))
|
|
578
603
|
rmSync(QUEUE_FILE);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yt-briefing",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.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 the chat, and adapts to your ratings — one video at a time.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,8 +8,10 @@ import { engine } from './engine.ts'
|
|
|
8
8
|
// a phone; the rating dialog opens once that turn has ended. Ratings and comments are written by
|
|
9
9
|
// the engine, never by this module.
|
|
10
10
|
|
|
11
|
-
/** A sweep can expand channels, fetch transcripts and summarize;
|
|
11
|
+
/** A sweep can expand channels, fetch transcripts and summarize; the model's call gets this budget. */
|
|
12
12
|
const SWEEP_MS = 10 * 60_000
|
|
13
|
+
/** Local files only, no network. */
|
|
14
|
+
const PENDING_MS = 30_000
|
|
13
15
|
/** A rating is a file write; a raw comment adds one `claude -p` call to distill it. */
|
|
14
16
|
const RATE_MS = 3 * 60_000
|
|
15
17
|
|
|
@@ -26,9 +28,9 @@ function parseOut(stdout: string): SweepOut {
|
|
|
26
28
|
return JSON.parse(line) as SweepOut
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
/**
|
|
30
|
-
async function
|
|
31
|
-
const run = await $.process.run(engine('yt-sweep'), { timeoutMs:
|
|
31
|
+
/** The video this run already showed and not yet rated. Never a real sweep: that could fetch a new one. */
|
|
32
|
+
async function shownVideo($: EngineInterface): Promise<SweepOut> {
|
|
33
|
+
const run = await $.process.run([...engine('yt-sweep'), '--pending'], { timeoutMs: PENDING_MS })
|
|
32
34
|
return parseOut(run.stdout)
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -93,7 +95,7 @@ function loopSection(): string {
|
|
|
93
95
|
const sweep = engine('yt-sweep').join(' ')
|
|
94
96
|
return [
|
|
95
97
|
`The yt-briefing plugin is running the person's YouTube briefing. A user message reading exactly "${FIRST_TEXT}" or "${NEXT_TEXT}" is its request for the next video:`,
|
|
96
|
-
`for "${FIRST_TEXT}" run \`${sweep} --reset\`, for "${NEXT_TEXT}" run \`${sweep}
|
|
98
|
+
`for "${FIRST_TEXT}" run \`${sweep} --reset\`, for "${NEXT_TEXT}" run \`${sweep}\`, in the foreground with a ${SWEEP_MS / 60_000}-minute timeout; it prints one JSON line. 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 plugin opens the rating dialog itself).`,
|
|
97
99
|
'Otherwise answer with one short line saying why there is nothing to rate. Write in the language of the summaries.',
|
|
98
100
|
].join('\n')
|
|
99
101
|
}
|
|
@@ -110,9 +112,10 @@ function submitLater($: EngineInterface, reset: boolean) {
|
|
|
110
112
|
async function rateAfterTurn($: EngineInterface) {
|
|
111
113
|
let out: SweepOut
|
|
112
114
|
try {
|
|
113
|
-
out = await
|
|
114
|
-
} catch {
|
|
115
|
+
out = await shownVideo($)
|
|
116
|
+
} catch (err) {
|
|
115
117
|
looping = false
|
|
118
|
+
$.ui.toast(`Could not open the rating: ${String(err).slice(0, 200)}`)
|
|
116
119
|
return
|
|
117
120
|
}
|
|
118
121
|
if (out.status !== 'rating_needed' || !out.pending || !out.summary) {
|