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 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 (the plugin re-runs the sweep after the briefing turn to open the rating
476
- // dialog) or warmed by the background child? Use it and skip the live fetch + content filter.
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",
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; give it the engine's full budget. */
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
- /** One engine sweep: the next ratable video, or the end state. Throws when the engine does not answer. */
30
- async function runSweep($: EngineInterface): Promise<SweepOut> {
31
- const run = await $.process.run(engine('yt-sweep'), { timeoutMs: SWEEP_MS })
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}\`; 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).`,
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 runSweep($)
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) {