yt-briefing 1.3.0 → 1.3.2
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/yt-sweep.js +28 -14
- package/package.json +1 -1
- package/plugin/hooks/register.tsx +16 -14
package/dist/yt-sweep.js
CHANGED
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
* rating) + `seen`. Foreground is the SOLE writer.
|
|
30
30
|
* Tagged with built_at; auto-rebuilt on a new day or via --reset.
|
|
31
31
|
* - <DATA_DIR>/.cache/queue-rest.json background-fill handoff written by the --fill child.
|
|
32
|
-
* - <DATA_DIR>/.cache/pending.json current ratable video's metadata for yt-rating.ts
|
|
32
|
+
* - <DATA_DIR>/.cache/pending.json current ratable video's metadata for yt-rating.ts, plus its
|
|
33
|
+
* emitted summary so a repeat sweep returns it unchanged.
|
|
33
34
|
* - <DATA_DIR>/.cache/prefetch.json background-computed summary for the NEXT video.
|
|
34
35
|
*
|
|
35
36
|
* All .cache/ files are throwaway (rebuilt each run; safe to delete / gitignore).
|
|
@@ -143,22 +144,30 @@ function dropHead(queue) {
|
|
|
143
144
|
if (v)
|
|
144
145
|
queue.seen.push(v.videoId);
|
|
145
146
|
}
|
|
146
|
-
|
|
147
|
-
|
|
147
|
+
/**
|
|
148
|
+
* A summary cached today for `videoId` in `file`: prefetch.json (the warmed next video) or
|
|
149
|
+
* pending.json (the head already emitted, so a repeat sweep returns it at once and unchanged).
|
|
150
|
+
*/
|
|
151
|
+
function loadCachedSummary(file, videoId) {
|
|
152
|
+
if (!existsSync(file))
|
|
148
153
|
return null;
|
|
149
154
|
try {
|
|
150
|
-
const p = JSON.parse(readFileSync(
|
|
151
|
-
if (p.videoId === videoId && p.built_at === today)
|
|
155
|
+
const p = JSON.parse(readFileSync(file, 'utf8'));
|
|
156
|
+
if (p.videoId === videoId && p.built_at === today && p.summary)
|
|
152
157
|
return p.summary;
|
|
153
158
|
}
|
|
154
159
|
catch { /* corrupt → ignore */ }
|
|
155
160
|
return null;
|
|
156
161
|
}
|
|
162
|
+
const loadPrefetch = (videoId) => loadCachedSummary(PREFETCH_FILE, videoId);
|
|
157
163
|
/** Atomic write (temp + rename) so a concurrent reader never sees a partial file. */
|
|
164
|
+
function writeAtomic(file, value) {
|
|
165
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
166
|
+
writeFileSync(tmp, JSON.stringify(value));
|
|
167
|
+
renameSync(tmp, file);
|
|
168
|
+
}
|
|
158
169
|
function writePrefetch(p) {
|
|
159
|
-
|
|
160
|
-
writeFileSync(tmp, JSON.stringify(p));
|
|
161
|
-
renameSync(tmp, PREFETCH_FILE);
|
|
170
|
+
writeAtomic(PREFETCH_FILE, p);
|
|
162
171
|
}
|
|
163
172
|
function clearPrefetch() {
|
|
164
173
|
if (existsSync(PREFETCH_FILE))
|
|
@@ -296,6 +305,7 @@ Steps:
|
|
|
296
305
|
- 2-5 numbered thematic sections × 2-5 sentences each
|
|
297
306
|
- At most 5-8 short quotes from the transcript
|
|
298
307
|
- No timestamps
|
|
308
|
+
- 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
309
|
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
310
|
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
311
|
5. Output: ONLY the briefing itself OR 'OFFTOPIC: ...'. No preamble, and no meta-commentary about these instructions.`;
|
|
@@ -457,10 +467,12 @@ async function advance(queue) {
|
|
|
457
467
|
dropHead(queue);
|
|
458
468
|
continue;
|
|
459
469
|
}
|
|
460
|
-
//
|
|
461
|
-
|
|
470
|
+
// Already emitted (the plugin re-runs the sweep after the briefing turn to open the rating
|
|
471
|
+
// dialog) or warmed by the background child? Use it and skip the live fetch + content filter.
|
|
472
|
+
const emitted = loadCachedSummary(PENDING_FILE, item.videoId);
|
|
473
|
+
const cached = emitted ?? loadPrefetch(item.videoId);
|
|
462
474
|
if (cached)
|
|
463
|
-
log(
|
|
475
|
+
log(`${emitted ? 'pending' : 'prefetch'} hit ${item.videoId}`);
|
|
464
476
|
const result = cached
|
|
465
477
|
? { kind: 'ratable', summary: cached }
|
|
466
478
|
: await processItem(item);
|
|
@@ -484,10 +496,12 @@ async function advance(queue) {
|
|
|
484
496
|
channel: item.channel, videoId: item.videoId, title: item.title,
|
|
485
497
|
type: item.type, publishedAt: item.publishedAt, is_baseline: item.is_baseline,
|
|
486
498
|
};
|
|
487
|
-
|
|
499
|
+
writeAtomic(PENDING_FILE, { ...pending, summary: result.summary, built_at: today });
|
|
488
500
|
writeFileSync(QUEUE_FILE, JSON.stringify(queue));
|
|
489
|
-
// Warm the NEXT video in the background while the user rates this one.
|
|
490
|
-
|
|
501
|
+
// Warm the NEXT video in the background while the user rates this one. A repeat emit of the
|
|
502
|
+
// same head already spawned that child; a second one would summarize the next video twice.
|
|
503
|
+
if (!emitted)
|
|
504
|
+
spawnPrefetch(queue.items[1]);
|
|
491
505
|
emit({ status: 'rating_needed', summary: result.summary, pending, lang: LANG });
|
|
492
506
|
}
|
|
493
507
|
// All processed.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yt-briefing",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
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": {
|
|
@@ -79,26 +79,28 @@ const STRAY_TAP_MS = 1500
|
|
|
79
79
|
|
|
80
80
|
const CHOICES = ['OK', 'Weak', 'Research', 'Stop'] as const
|
|
81
81
|
|
|
82
|
-
/** What
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
/** What the chat shows of each briefing prompt; the instructions ride along as unseen context. */
|
|
83
|
+
const NEXT_TEXT = 'yt: next video'
|
|
84
|
+
/** The first prompt of a /yt run: the only one that rebuilds the queue. */
|
|
85
|
+
const FIRST_TEXT = 'yt: first video'
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* What a briefing turn must do, read from the system prompt while the loop runs. The text is the
|
|
89
|
+
* same for every turn: a session-scoped section may be kept as first composed, so a per-turn
|
|
90
|
+
* `--reset` here once stuck to every turn and threw away the warmed next video each time.
|
|
91
|
+
*/
|
|
92
|
+
function loopSection(): string {
|
|
93
|
+
const sweep = engine('yt-sweep').join(' ')
|
|
85
94
|
return [
|
|
86
|
-
`The yt-briefing plugin is running the person's YouTube briefing. A user message reading exactly "${NEXT_TEXT}" is its request for the next video:`,
|
|
87
|
-
`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).`,
|
|
95
|
+
`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).`,
|
|
88
97
|
'Otherwise answer with one short line saying why there is nothing to rate. Write in the language of the summaries.',
|
|
89
98
|
].join('\n')
|
|
90
99
|
}
|
|
91
100
|
|
|
92
|
-
/** What the chat shows of each briefing prompt; the instructions ride along as unseen context. */
|
|
93
|
-
const NEXT_TEXT = 'yt: next video'
|
|
94
|
-
|
|
95
|
-
/** Whether the next briefing prompt starts a fresh sweep. */
|
|
96
|
-
let resetNext = false
|
|
97
|
-
|
|
98
101
|
/** Ask for a briefing once the hook that wants it has answered: one submitted from inside would wait on it. */
|
|
99
102
|
function submitLater($: EngineInterface, reset: boolean) {
|
|
100
|
-
|
|
101
|
-
$.clock.after(0, () => void $.prompt.submit({ text: NEXT_TEXT, asUser: true }).catch(err => {
|
|
103
|
+
$.clock.after(0, () => void $.prompt.submit({ text: reset ? FIRST_TEXT : NEXT_TEXT, asUser: true }).catch(err => {
|
|
102
104
|
looping = false
|
|
103
105
|
$.ui.toast(`Could not continue the briefing: ${String(err)}`)
|
|
104
106
|
}))
|
|
@@ -176,7 +178,7 @@ export const register: Register = on => {
|
|
|
176
178
|
on('prompt.compose', async ($, e, next) => {
|
|
177
179
|
const composed = await next(e)
|
|
178
180
|
if (!looping) return composed
|
|
179
|
-
return { sections: [...composed.sections, { id: 'yt-briefing:loop', text: loopSection(
|
|
181
|
+
return { sections: [...composed.sections, { id: 'yt-briefing:loop', text: loopSection(), scope: 'session' as const }] }
|
|
180
182
|
})
|
|
181
183
|
|
|
182
184
|
on('turn.complete', async ($, e, next) => {
|