yt-briefing 0.14.0 → 0.14.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/README.md +1 -1
- package/dist/lib/skill-install.js +9 -2
- package/dist/lib/summary-gate.js +28 -0
- package/dist/yt-summary-gate.js +15 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -148,7 +148,7 @@ valid JSON the installer leaves it alone and says so — add the entry yourself:
|
|
|
148
148
|
"hooks": {
|
|
149
149
|
"PreToolUse": [
|
|
150
150
|
{ "matcher": "AskUserQuestion",
|
|
151
|
-
"hooks": [ { "type": "command", "command": "node \"node_modules/yt-briefing/dist/yt-summary-gate.js\"" } ] }
|
|
151
|
+
"hooks": [ { "type": "command", "command": "node \"${CLAUDE_PROJECT_DIR}/node_modules/yt-briefing/dist/yt-summary-gate.js\"" } ] }
|
|
152
152
|
]
|
|
153
153
|
}
|
|
154
154
|
}
|
|
@@ -107,9 +107,16 @@ export function installSkills(root, dist = false) {
|
|
|
107
107
|
export const CLAUDE_CODE = '1';
|
|
108
108
|
/** Substring identifying our gate inside a settings.json hook command (used to update in place). */
|
|
109
109
|
const GATE_ID = 'yt-summary-gate';
|
|
110
|
-
/**
|
|
110
|
+
/**
|
|
111
|
+
* How settings.json must invoke the gate. Like the skill's engine commands it bakes nothing
|
|
112
|
+
* machine-absolute, but a hook may NOT assume its cwd — Claude Code's hooks reference states the
|
|
113
|
+
* working directory can vary, so a bare relative path would fail open, silently and invisibly
|
|
114
|
+
* (the popup appears ungated, which is exactly the bug the gate exists to catch). Hence the
|
|
115
|
+
* `$CLAUDE_PROJECT_DIR` prefix: still just a placeholder string in the committed JSON, resolved
|
|
116
|
+
* to the project root at hook time.
|
|
117
|
+
*/
|
|
111
118
|
export const gateCommand = (dist = false) => dist
|
|
112
|
-
? `${isBun ? 'bun' : 'node'} "
|
|
119
|
+
? `${isBun ? 'bun' : 'node'} "\${CLAUDE_PROJECT_DIR}/${toProjectRel(join(DIST_DIR, GATE_ID + '.js'))}"`
|
|
113
120
|
: `bun run src/${GATE_ID}.ts`;
|
|
114
121
|
/**
|
|
115
122
|
* Put the gate into a settings object: a PreToolUse hook on AskUserQuestion. Merges — every other
|
package/dist/lib/summary-gate.js
CHANGED
|
@@ -38,3 +38,31 @@ export function summaryWasPasted(transcript, videoId) {
|
|
|
38
38
|
}
|
|
39
39
|
return false;
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Same question as `summaryWasPasted`, but tolerant of the harness writing the transcript late.
|
|
43
|
+
*
|
|
44
|
+
* The agent pastes the summary and calls the popup in one turn, so the text block and the tool
|
|
45
|
+
* call are separate JSONL entries written by the harness asynchronously — the hook can run
|
|
46
|
+
* while only the tool call has reached disk. A single read then reports "not pasted" for a turn
|
|
47
|
+
* that did paste, and the block fires on every single video (observed 2026-08-30: the same
|
|
48
|
+
* transcript and pending file that blocked, replayed seconds later, allowed).
|
|
49
|
+
*
|
|
50
|
+
* So poll instead of guessing: re-read until the id shows up or the window closes. A turn that
|
|
51
|
+
* genuinely skipped the paste never produces the line, so the gate still blocks — just later.
|
|
52
|
+
* `read` returning null (a torn read mid-write) counts as "not yet", not as proof of absence.
|
|
53
|
+
*/
|
|
54
|
+
export async function waitForSummary(read, videoId, opts = {}) {
|
|
55
|
+
const timeoutMs = opts.timeoutMs ?? 5000;
|
|
56
|
+
const intervalMs = opts.intervalMs ?? 200;
|
|
57
|
+
const now = opts.now ?? (() => Date.now());
|
|
58
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
59
|
+
const deadline = now() + timeoutMs;
|
|
60
|
+
for (;;) {
|
|
61
|
+
const transcript = read();
|
|
62
|
+
if (transcript !== null && summaryWasPasted(transcript, videoId))
|
|
63
|
+
return true;
|
|
64
|
+
if (now() >= deadline)
|
|
65
|
+
return false;
|
|
66
|
+
await sleep(intervalMs);
|
|
67
|
+
}
|
|
68
|
+
}
|
package/dist/yt-summary-gate.js
CHANGED
|
@@ -14,12 +14,16 @@
|
|
|
14
14
|
* `pending.json`, and neither can see the conversation. The transcript is the one place the
|
|
15
15
|
* evidence exists and only the agent harness exposes it, so the check has to run as a hook.
|
|
16
16
|
*
|
|
17
|
+
* The transcript is polled for a few seconds rather than read once: the paste and the popup are
|
|
18
|
+
* a single turn and the harness flushes the JSONL asynchronously, so a single read can miss a
|
|
19
|
+
* text block that is merely still in flight.
|
|
20
|
+
*
|
|
17
21
|
* Reads the PreToolUse payload on stdin; exit 0 allows, exit 2 blocks and feeds stderr back to
|
|
18
22
|
* the agent. Anything unexpected — foreign tool, unreadable transcript, no pending video —
|
|
19
23
|
* allows: a gate that misfires on unrelated work would be worse than the bug it guards.
|
|
20
24
|
*/
|
|
21
25
|
import { readFileSync, existsSync } from 'node:fs';
|
|
22
|
-
import { isRatingPopup,
|
|
26
|
+
import { isRatingPopup, waitForSummary } from "./lib/summary-gate.js";
|
|
23
27
|
const ALLOW = 0;
|
|
24
28
|
const BLOCK = 2;
|
|
25
29
|
const allow = () => process.exit(ALLOW);
|
|
@@ -49,13 +53,16 @@ if (!pending.videoId)
|
|
|
49
53
|
const transcript = payload.transcript_path;
|
|
50
54
|
if (!transcript || !existsSync(transcript))
|
|
51
55
|
allow();
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
// Poll rather than read once: the paste and this popup are one turn, and the harness writes the
|
|
57
|
+
// transcript asynchronously, so the text block can still be in flight while the hook runs.
|
|
58
|
+
const seen = await waitForSummary(() => {
|
|
59
|
+
try {
|
|
60
|
+
return readFileSync(transcript, 'utf8');
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null; // transient (file being written) — treat as "not yet", keep waiting
|
|
64
|
+
}
|
|
65
|
+
}, pending.videoId);
|
|
59
66
|
if (seen)
|
|
60
67
|
allow();
|
|
61
68
|
process.stderr.write(`/yt step B not done: the summary for ${pending.videoId} ("${pending.title ?? '?'}") is not in your chat text.\n` +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yt-briefing",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.2",
|
|
4
4
|
"description": "A self-learning YouTube briefing engine: it sweeps the channels you follow, filters noise in two stages (title, then transcript), summarizes the rest in your language, and adapts to your ratings — one video at a time.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|