yt-briefing 1.3.2 → 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/lib/paths.js +1 -0
- package/dist/lib/video-dates.js +45 -0
- package/dist/lib/yt-lib.js +18 -0
- package/dist/yt-channel-pending.js +9 -2
- package/dist/yt-rating.js +10 -2
- package/dist/yt-sweep.js +48 -18
- 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/lib/paths.js
CHANGED
|
@@ -54,6 +54,7 @@ export const REST_FILE = join(CACHE_DIR, 'queue-rest.json');
|
|
|
54
54
|
export const PENDING_FILE = join(CACHE_DIR, 'pending.json');
|
|
55
55
|
export const PREFETCH_FILE = join(CACHE_DIR, 'prefetch.json');
|
|
56
56
|
export const LOG_FILE = join(CACHE_DIR, 'sweep.log');
|
|
57
|
+
export const VIDEO_DATES_FILE = join(CACHE_DIR, 'video-dates.json'); // videoId → publishedAt, for the monotonic pointer
|
|
57
58
|
// /yt-search (ad-hoc topic search → lazy triage → compare). All throwaway, under .cache/.
|
|
58
59
|
export const SEARCH_QUEUE_FILE = join(CACHE_DIR, 'search-queue.json'); // ranked candidates + cursor
|
|
59
60
|
export const SEARCH_PENDING_FILE = join(CACHE_DIR, 'search-pending.json'); // current candidate awaiting keep/skip
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Publish dates of every video the sweep has listed, so a state pointer bump can refuse to
|
|
3
|
+
* move back in time (see pointerWouldRegress). Kept across --reset: the pointer outlives the
|
|
4
|
+
* queue. Best-effort cache — a missing or corrupt file only turns the guard off.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { dirname } from 'node:path';
|
|
8
|
+
import { VIDEO_DATES_FILE } from "./paths.js";
|
|
9
|
+
/** Entries older than this are dropped on write; the channel listing looks back 45 days. */
|
|
10
|
+
const KEEP_DAYS = 120;
|
|
11
|
+
export function loadVideoDates() {
|
|
12
|
+
if (!existsSync(VIDEO_DATES_FILE))
|
|
13
|
+
return {};
|
|
14
|
+
try {
|
|
15
|
+
const d = JSON.parse(readFileSync(VIDEO_DATES_FILE, 'utf8'));
|
|
16
|
+
return d && typeof d === 'object' ? d : {};
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Merge newly seen videos into the cache (atomic write); no-op when nothing is new. */
|
|
23
|
+
export function rememberVideoDates(videos) {
|
|
24
|
+
const dates = loadVideoDates();
|
|
25
|
+
let changed = false;
|
|
26
|
+
for (const v of videos) {
|
|
27
|
+
if (!v.publishedAt || !Number.isFinite(Date.parse(v.publishedAt)) || dates[v.videoId] === v.publishedAt)
|
|
28
|
+
continue;
|
|
29
|
+
dates[v.videoId] = v.publishedAt;
|
|
30
|
+
changed = true;
|
|
31
|
+
}
|
|
32
|
+
if (!changed)
|
|
33
|
+
return;
|
|
34
|
+
const cutoff = Date.now() - KEEP_DAYS * 24 * 60 * 60 * 1000;
|
|
35
|
+
for (const [id, at] of Object.entries(dates))
|
|
36
|
+
if (Date.parse(at) < cutoff)
|
|
37
|
+
delete dates[id];
|
|
38
|
+
try {
|
|
39
|
+
mkdirSync(dirname(VIDEO_DATES_FILE), { recursive: true });
|
|
40
|
+
const tmp = `${VIDEO_DATES_FILE}.${process.pid}.tmp`;
|
|
41
|
+
writeFileSync(tmp, JSON.stringify(dates));
|
|
42
|
+
renameSync(tmp, VIDEO_DATES_FILE);
|
|
43
|
+
}
|
|
44
|
+
catch { /* best-effort: the guard is only off for dates not written */ }
|
|
45
|
+
}
|
package/dist/lib/yt-lib.js
CHANGED
|
@@ -46,6 +46,24 @@ export function parseState(content) {
|
|
|
46
46
|
export function bumpStateFrontmatterDate(content, date) {
|
|
47
47
|
return content.replace(/^updated:\s*\d{4}-\d{2}-\d{2}/m, `updated: ${date}`);
|
|
48
48
|
}
|
|
49
|
+
/** The state pointer of one type for a channel row (null when unset or the row is missing). */
|
|
50
|
+
export function pointerFor(row, type) {
|
|
51
|
+
if (!row)
|
|
52
|
+
return null;
|
|
53
|
+
return type === 'longform' ? row.last_longform_id : type === 'short' ? row.last_short_id : row.last_live_id;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Would moving the pointer from `pointerId` to `videoId` move it back in time? `dates` maps
|
|
57
|
+
* videoIds to publishedAt. Unknown dates answer false (bump as before): only a known older
|
|
58
|
+
* video is refused. A stale skip applied after the pointer moved on used to rewind it, and
|
|
59
|
+
* the next queue listed already-rated videos again.
|
|
60
|
+
*/
|
|
61
|
+
export function pointerWouldRegress(pointerId, videoId, publishedAt, dates) {
|
|
62
|
+
if (!pointerId || pointerId === videoId || !publishedAt)
|
|
63
|
+
return false;
|
|
64
|
+
const at = dates[pointerId];
|
|
65
|
+
return !!at && Date.parse(at) > Date.parse(publishedAt);
|
|
66
|
+
}
|
|
49
67
|
/**
|
|
50
68
|
* Per-video pointer bump. Updates one type's last_id for one channel.
|
|
51
69
|
* Session counter increments only on first touch of the day (updated != date).
|
|
@@ -19,6 +19,7 @@ import { loadEnv } from "./lib/env.js";
|
|
|
19
19
|
import { parseState } from "./lib/yt-lib.js";
|
|
20
20
|
import { STATE_MD } from "./lib/paths.js";
|
|
21
21
|
import { fetchChannelVideos } from "./lib/yt-api.js";
|
|
22
|
+
import { loadVideoDates, rememberVideoDates } from "./lib/video-dates.js";
|
|
22
23
|
loadEnv();
|
|
23
24
|
const handle = process.argv[2];
|
|
24
25
|
if (!handle) {
|
|
@@ -47,6 +48,10 @@ catch (e) {
|
|
|
47
48
|
console.error(e.message);
|
|
48
49
|
process.exit(1);
|
|
49
50
|
}
|
|
51
|
+
// Date cache read before this listing is added, so a pointer that left the window still has its
|
|
52
|
+
// old date; then every listed video (the pointer too) is remembered for the monotonic bump.
|
|
53
|
+
const videoDates = loadVideoDates();
|
|
54
|
+
rememberVideoDates(videos);
|
|
50
55
|
const pointerByType = {
|
|
51
56
|
longform: row.last_longform_id,
|
|
52
57
|
short: row.last_short_id,
|
|
@@ -74,10 +79,12 @@ for (const type of ['longform', 'short', 'live']) {
|
|
|
74
79
|
}
|
|
75
80
|
// normal: find pointer's publishedAt, take everything strictly newer
|
|
76
81
|
const pointerVideo = ofType.find(v => v.videoId === pointerId);
|
|
82
|
+
// Pointer not in the fetched window: older than the lookback, or deleted/unlisted. Its date
|
|
83
|
+
// from the sweep's cache keeps already-seen videos out; without one, take the whole window.
|
|
84
|
+
const cachedTs = Date.parse(videoDates[pointerId] ?? '');
|
|
77
85
|
const cutoffTs = pointerVideo
|
|
78
86
|
? new Date(pointerVideo.publishedAt).getTime()
|
|
79
|
-
:
|
|
80
|
-
-Infinity;
|
|
87
|
+
: Number.isFinite(cachedTs) ? cachedTs : -Infinity;
|
|
81
88
|
for (const v of ofType) {
|
|
82
89
|
if (new Date(v.publishedAt).getTime() <= cutoffTs)
|
|
83
90
|
continue;
|
package/dist/yt-rating.js
CHANGED
|
@@ -27,7 +27,8 @@ import { spawn } from 'node:child_process';
|
|
|
27
27
|
import { loadEnv } from "./lib/env.js";
|
|
28
28
|
import { loadConfig } from "./lib/config.js";
|
|
29
29
|
import { distillComment } from "./lib/distill.js";
|
|
30
|
-
import { parseChannels, appendSkipTitle, appendNote, bumpStatePointer } from "./lib/yt-lib.js";
|
|
30
|
+
import { parseChannels, parseState, appendSkipTitle, appendNote, bumpStatePointer, pointerFor, pointerWouldRegress } from "./lib/yt-lib.js";
|
|
31
|
+
import { loadVideoDates } from "./lib/video-dates.js";
|
|
31
32
|
import { CHANNELS_MD, STATE_MD, PENDING_FILE, QUEUE_FILE, profilePath } from "./lib/paths.js";
|
|
32
33
|
loadEnv();
|
|
33
34
|
function getArg(args, name) {
|
|
@@ -123,7 +124,14 @@ if (profileAfter !== profileBefore) {
|
|
|
123
124
|
let stateBumped = false;
|
|
124
125
|
if (!args.noState) {
|
|
125
126
|
const stateBefore = readFileSync(STATE_MD, 'utf8');
|
|
126
|
-
|
|
127
|
+
// An older video rated after a newer one of the same channel was title-skipped must not
|
|
128
|
+
// rewind the pointer; the date comes from the sweep's cache of listed videos.
|
|
129
|
+
const pointer = pointerFor(parseState(stateBefore).find(r => r.handle === args.channel), args.type);
|
|
130
|
+
const dates = loadVideoDates();
|
|
131
|
+
const publishedAt = dates[args.id] ?? (() => { const p = loadPending(); return p.videoId === args.id ? p.publishedAt : undefined; })();
|
|
132
|
+
// Never rewind: re-bump the current pointer instead, so the day's session still counts.
|
|
133
|
+
const regress = pointer !== null && pointerWouldRegress(pointer, args.id, publishedAt, dates);
|
|
134
|
+
const stateAfter = bumpStatePointer(stateBefore, args.channel, args.type, regress ? pointer : args.id, date);
|
|
127
135
|
if (stateAfter !== stateBefore) {
|
|
128
136
|
writeFileSync(STATE_MD, stateAfter, 'utf8');
|
|
129
137
|
stateBumped = true;
|
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
|
|
@@ -51,7 +53,8 @@
|
|
|
51
53
|
import { readFileSync, writeFileSync, existsSync, rmSync, mkdirSync, renameSync, appendFileSync } from 'node:fs';
|
|
52
54
|
import { spawn } from 'node:child_process';
|
|
53
55
|
import { loadEnv, missingEnv, missingEnvMessage, REQUIRED_YOUTUBE } from "./lib/env.js";
|
|
54
|
-
import { parseChannels, parseState, bumpStatePointer, isResolved } from "./lib/yt-lib.js";
|
|
56
|
+
import { parseChannels, parseState, bumpStatePointer, isResolved, pointerFor, pointerWouldRegress } from "./lib/yt-lib.js";
|
|
57
|
+
import { loadVideoDates, rememberVideoDates } from "./lib/video-dates.js";
|
|
55
58
|
import { chat, claudeMissing } from "./lib/llm.js";
|
|
56
59
|
import { outputLang } from "./lib/config.js";
|
|
57
60
|
import { PKG_ROOT, CHANNELS_MD, STATE_MD, CACHE_DIR, QUEUE_FILE, REST_FILE, PENDING_FILE, PREFETCH_FILE, LOG_FILE, profilePath, script, } from "./lib/paths.js";
|
|
@@ -67,6 +70,7 @@ const CONCURRENCY = 6;
|
|
|
67
70
|
const argv = process.argv.slice(2);
|
|
68
71
|
const reset = argv.includes('--reset');
|
|
69
72
|
const fillMode = argv.includes('--fill');
|
|
73
|
+
const pendingMode = argv.includes('--pending');
|
|
70
74
|
const pfIdx = argv.indexOf('--prefetch');
|
|
71
75
|
const prefetchTarget = pfIdx !== -1 ? argv[pfIdx + 1] : null;
|
|
72
76
|
const today = new Date().toISOString().slice(0, 10);
|
|
@@ -105,9 +109,14 @@ function run(cmd) {
|
|
|
105
109
|
});
|
|
106
110
|
}
|
|
107
111
|
// ---------- state mutation (inline, crash-safe per write) ----------
|
|
108
|
-
function bumpState(handle, type, videoId) {
|
|
112
|
+
function bumpState(handle, type, videoId, publishedAt) {
|
|
109
113
|
const before = readFileSync(STATE_MD, 'utf8');
|
|
110
|
-
const
|
|
114
|
+
const pointer = pointerFor(parseState(before).find(r => r.handle === handle), type);
|
|
115
|
+
// Never rewind: re-bump the current pointer instead, so the day's session still counts.
|
|
116
|
+
const regress = pointer !== null && pointerWouldRegress(pointer, videoId, publishedAt, loadVideoDates());
|
|
117
|
+
if (regress)
|
|
118
|
+
log(`pointer kept ${handle} ${type} ${pointer} (not rewound to older ${videoId})`);
|
|
119
|
+
const after = bumpStatePointer(before, handle, type, regress ? pointer : videoId, today);
|
|
111
120
|
if (after !== before)
|
|
112
121
|
writeFileSync(STATE_MD, after, 'utf8');
|
|
113
122
|
}
|
|
@@ -117,20 +126,23 @@ function bumpState(handle, type, videoId) {
|
|
|
117
126
|
*/
|
|
118
127
|
function persistSkip(item, status) {
|
|
119
128
|
if (status !== 'no_transcript')
|
|
120
|
-
bumpState(item.channel, item.type, item.videoId);
|
|
129
|
+
bumpState(item.channel, item.type, item.videoId, item.publishedAt);
|
|
121
130
|
}
|
|
122
131
|
/**
|
|
123
|
-
* Apply title-skips to state.md.
|
|
132
|
+
* Apply title-skips to state.md. Bumps the pointer (a title-skip is final) unless that would
|
|
133
|
+
* rewind it: a background fill's skips are listed against the pointer it saw when it started.
|
|
124
134
|
* Idempotent — re-applying the same skip is a no-op. ONLY the foreground calls this.
|
|
125
135
|
*/
|
|
126
136
|
function applyTitleSkips(skips) {
|
|
137
|
+
rememberVideoDates(skips);
|
|
127
138
|
for (const s of skips) {
|
|
128
|
-
bumpState(s.channel, s.type, s.videoId);
|
|
139
|
+
bumpState(s.channel, s.type, s.videoId, s.publishedAt);
|
|
129
140
|
recordSkip({ stage: 'title', channel: s.channel, videoId: s.videoId, title: s.title, reason: s.reason || 'title filter' });
|
|
130
141
|
}
|
|
131
142
|
}
|
|
132
143
|
/** Append items, skipping any videoId already queued OR already resolved (seen). */
|
|
133
144
|
function enqueue(queue, add) {
|
|
145
|
+
rememberVideoDates(add);
|
|
134
146
|
const have = new Set([...queue.items.map(i => i.videoId), ...queue.seen]);
|
|
135
147
|
for (const it of add)
|
|
136
148
|
if (!have.has(it.videoId)) {
|
|
@@ -341,7 +353,7 @@ async function expandChannel(ref) {
|
|
|
341
353
|
const skips = [];
|
|
342
354
|
for (const it of candidates) {
|
|
343
355
|
if (titleSkip.has(it.videoId))
|
|
344
|
-
skips.push({ channel: it.channel, type: it.type, videoId: it.videoId, title: it.title, reason: titleSkip.get(it.videoId) || 'title filter' });
|
|
356
|
+
skips.push({ channel: it.channel, type: it.type, videoId: it.videoId, title: it.title, reason: titleSkip.get(it.videoId) || 'title filter', publishedAt: it.publishedAt });
|
|
345
357
|
else
|
|
346
358
|
items.push(it);
|
|
347
359
|
}
|
|
@@ -421,11 +433,7 @@ function loadQueue() {
|
|
|
421
433
|
// ---------- advance (the lazy step) ----------
|
|
422
434
|
/** state.md pointer for this item's type, or null if the channel has no row yet. */
|
|
423
435
|
function statePointer(item) {
|
|
424
|
-
|
|
425
|
-
if (!row)
|
|
426
|
-
return null;
|
|
427
|
-
return item.type === 'longform' ? row.last_longform_id
|
|
428
|
-
: item.type === 'short' ? row.last_short_id : row.last_live_id;
|
|
436
|
+
return pointerFor(parseState(readFileSync(STATE_MD, 'utf8')).find(r => r.handle === item.channel), item.type);
|
|
429
437
|
}
|
|
430
438
|
/**
|
|
431
439
|
* Pure-ish pipeline for one queue item: fetch transcript → substance check + summary.
|
|
@@ -453,6 +461,13 @@ async function processItem(item) {
|
|
|
453
461
|
return { kind: 'skip', status: 'content_skip', reason: summary.slice(9).trim() };
|
|
454
462
|
return { kind: 'ratable', summary };
|
|
455
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
|
+
}
|
|
456
471
|
async function advance(queue) {
|
|
457
472
|
while (true) {
|
|
458
473
|
// Pull in the background fill (or expand a channel ourselves) until there's an
|
|
@@ -467,8 +482,8 @@ async function advance(queue) {
|
|
|
467
482
|
dropHead(queue);
|
|
468
483
|
continue;
|
|
469
484
|
}
|
|
470
|
-
// Already emitted (
|
|
471
|
-
//
|
|
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.
|
|
472
487
|
const emitted = loadCachedSummary(PENDING_FILE, item.videoId);
|
|
473
488
|
const cached = emitted ?? loadPrefetch(item.videoId);
|
|
474
489
|
if (cached)
|
|
@@ -492,10 +507,7 @@ async function advance(queue) {
|
|
|
492
507
|
}
|
|
493
508
|
// Ratable — hand off to the frontend. Leave item at queue head (rating bumps state,
|
|
494
509
|
// next call detects pointer === videoId and drops it).
|
|
495
|
-
const pending =
|
|
496
|
-
channel: item.channel, videoId: item.videoId, title: item.title,
|
|
497
|
-
type: item.type, publishedAt: item.publishedAt, is_baseline: item.is_baseline,
|
|
498
|
-
};
|
|
510
|
+
const pending = pendingOf(item);
|
|
499
511
|
writeAtomic(PENDING_FILE, { ...pending, summary: result.summary, built_at: today });
|
|
500
512
|
writeFileSync(QUEUE_FILE, JSON.stringify(queue));
|
|
501
513
|
// Warm the NEXT video in the background while the user rates this one. A repeat emit of the
|
|
@@ -511,6 +523,21 @@ async function advance(queue) {
|
|
|
511
523
|
clearPrefetch();
|
|
512
524
|
emit({ status: 'done' });
|
|
513
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
|
+
}
|
|
514
541
|
/**
|
|
515
542
|
* --prefetch mode: compute the summary for one specific queued video and cache it, with
|
|
516
543
|
* NO side effects on state.md / queue / pending. Runs detached while the user rates the
|
|
@@ -568,6 +595,9 @@ if (prefetchTarget) {
|
|
|
568
595
|
if (fillMode) {
|
|
569
596
|
await runFill(); // detached background queue builder — never returns
|
|
570
597
|
}
|
|
598
|
+
if (pendingMode) {
|
|
599
|
+
runPending(); // read-only answer for the rating dialog — never returns
|
|
600
|
+
}
|
|
571
601
|
if (reset) {
|
|
572
602
|
if (existsSync(QUEUE_FILE))
|
|
573
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) {
|