atris 3.57.4 → 3.58.6
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 +12 -1
- package/atris/skills/clawhub/member-runtime/SKILL.md +9 -9
- package/atris/skills/create-member/SKILL.md +16 -13
- package/atris/skills/design/SKILL.md +27 -4
- package/atris/skills/engines/SKILL.md +55 -12
- package/atris/skills/x-search/SKILL.md +8 -6
- package/atris/skills/youtube/SKILL.md +66 -21
- package/atris/team/brainstormer/MEMBER.md +2 -2
- package/atris/team/executor/MEMBER.md +3 -3
- package/atris/team/launcher/MEMBER.md +4 -4
- package/atris/team/navigator/MEMBER.md +3 -3
- package/atris/team/researcher/MEMBER.md +2 -2
- package/atris/team/validator/MEMBER.md +1 -1
- package/atris.md +14 -0
- package/ax +153 -23
- package/bin/atris.js +78 -30
- package/commands/aeo.js +4 -4
- package/commands/agents.js +1 -1
- package/commands/align.js +12 -12
- package/commands/analytics.js +3 -3
- package/commands/app.js +5 -5
- package/commands/ask.js +175 -0
- package/commands/auth.js +59 -31
- package/commands/autoland.js +29 -5
- package/commands/autopilot-front.js +6 -6
- package/commands/autopilot.js +90 -90
- package/commands/avail.js +5 -5
- package/commands/bench.js +63 -1
- package/commands/browse.js +2 -2
- package/commands/business.js +12 -12
- package/commands/card.js +2 -2
- package/commands/clean.js +13 -13
- package/commands/close.js +3 -3
- package/commands/compile.js +13 -13
- package/commands/computer.js +94 -27
- package/commands/console.js +5 -5
- package/commands/decide.js +1 -1
- package/commands/deck.js +9 -9
- package/commands/drive.js +4 -4
- package/commands/engine.js +173 -19
- package/commands/errors.js +3 -3
- package/commands/experiments.js +18 -1
- package/commands/feed.js +2 -2
- package/commands/feedback.js +4 -4
- package/commands/fleet.js +12 -12
- package/commands/game.js +1 -1
- package/commands/guide.js +49 -0
- package/commands/improve.js +19 -17
- package/commands/init.js +67 -19
- package/commands/interview.js +7 -7
- package/commands/land.js +40 -15
- package/commands/learn.js +216 -50
- package/commands/lesson.js +2 -2
- package/commands/log.js +1 -1
- package/commands/loops.js +4 -4
- package/commands/member.js +75 -50
- package/commands/mission.js +90 -82
- package/commands/one-lap.js +1 -1
- package/commands/pack.js +74 -8
- package/commands/plugin.js +6 -6
- package/commands/probe.js +9 -9
- package/commands/pull.js +35 -35
- package/commands/pulse.js +1 -1
- package/commands/push.js +21 -21
- package/commands/rainmaker.js +1 -1
- package/commands/recap.js +7 -7
- package/commands/reel.js +2 -2
- package/commands/release.js +3 -3
- package/commands/report.js +1 -1
- package/commands/review.js +5 -5
- package/commands/revisions.js +1 -1
- package/commands/run.js +1 -1
- package/commands/scout.js +3 -3
- package/commands/security-review.js +7 -7
- package/commands/serve.js +9 -9
- package/commands/setup.js +1 -1
- package/commands/sign.js +4 -4
- package/commands/signup.js +4 -4
- package/commands/site.js +8 -3
- package/commands/skill.js +6 -6
- package/commands/slop.js +18 -18
- package/commands/social.js +3 -3
- package/commands/soul.js +14 -14
- package/commands/spaceship.js +1 -1
- package/commands/status.js +2 -2
- package/commands/strings.js +13 -13
- package/commands/study.js +4 -4
- package/commands/sync.js +8 -8
- package/commands/task.js +50 -25
- package/commands/teach.js +2 -2
- package/commands/team.js +1 -1
- package/commands/theme.js +2 -2
- package/commands/tree.js +34 -0
- package/commands/truth.js +7 -7
- package/commands/verify.js +12 -12
- package/commands/visualize.js +2 -2
- package/commands/workflow.js +101 -48
- package/commands/worktree.js +1 -1
- package/commands/write.js +9 -9
- package/commands/x-search.js +93 -22
- package/commands/xp.js +6 -3
- package/commands/youtube.js +1019 -123
- package/lib/accept-verify-gate.js +3 -3
- package/lib/activity-stream.js +1 -1
- package/lib/auto-accept-certified.js +28 -12
- package/lib/autoland.js +6 -6
- package/lib/bench/engines.js +59 -8
- package/lib/bench/report.js +1 -1
- package/lib/bench/runner.js +204 -5
- package/lib/bench/tree-render.js +76 -0
- package/lib/card.js +1 -1
- package/lib/chat-log-scan.js +1 -1
- package/lib/claude-boot-block.js +2 -0
- package/lib/deck-compose.js +1 -1
- package/lib/deck-history.js +2 -2
- package/lib/deck-layout.js +1 -1
- package/lib/deck-review.js +5 -5
- package/lib/deck-schema.js +1 -1
- package/lib/default-verifier.js +1 -1
- package/lib/developer-api.js +3 -0
- package/lib/engine-ask.js +45 -7
- package/lib/engine-registry.js +15 -2
- package/lib/file-ops.js +1 -1
- package/lib/fleet.js +71 -35
- package/lib/html-render.js +1 -1
- package/lib/intents.js +172 -0
- package/lib/journal.js +1 -1
- package/lib/known-commands.js +2 -2
- package/lib/land-green.js +364 -0
- package/lib/lesson-contradiction.js +2 -2
- package/lib/lesson-preflight.js +1 -1
- package/lib/manifest.js +5 -5
- package/lib/member-alive.js +9 -5
- package/lib/memory-view.js +3 -3
- package/lib/mission-protected-lane.js +1 -1
- package/lib/mission-root.js +2 -2
- package/lib/orb-context.js +10 -10
- package/lib/pack-capabilities.js +861 -3
- package/lib/permission-grants.js +2 -2
- package/lib/policy-lessons.js +6 -6
- package/lib/pulse.js +8 -8
- package/lib/receipt-evidence.js +1 -1
- package/lib/reel.js +1 -1
- package/lib/review-integrity.js +1 -1
- package/lib/runner-command.js +35 -5
- package/lib/scorecard.js +4 -4
- package/lib/security-scan.js +5 -5
- package/lib/self-drive.js +1 -1
- package/lib/site-publish.js +372 -0
- package/lib/site.js +1 -1
- package/lib/slides-deck.js +2 -2
- package/lib/state-detection.js +2 -2
- package/lib/sync-telemetry.js +2 -2
- package/lib/task-db.js +17 -10
- package/lib/task-explanation.js +1 -1
- package/lib/task-proof.js +2 -2
- package/lib/task-receipt.js +3 -3
- package/lib/theme.js +12 -12
- package/lib/todo-fallback.js +3 -3
- package/lib/todo-sections.js +1 -1
- package/lib/tree-hash.js +79 -0
- package/lib/usage.js +1 -1
- package/lib/wish-delegate.js +1 -1
- package/lib/workspace-safety.js +4 -4
- package/lib/workspace-scaffold.js +3 -3
- package/package.json +1 -1
- package/scripts/det/concierge-bakeoff.sh +26 -0
- package/scripts/det/ytnotes +82 -5
- package/scripts/det/ytquote-repair.js +0 -1
- package/scripts/det/ytrail-race.js +0 -5
- package/scripts/member-operate.mjs +52 -14
- package/scripts/outbound-artifact-gate.js +0 -3
- package/utils/auth.js +47 -9
- package/utils/config.js +0 -1
- package/scripts/det/checklist-score.js +0 -191
package/commands/youtube.js
CHANGED
|
@@ -31,6 +31,8 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
|
|
|
31
31
|
output(` ${commandName} search --paid "<query>" [--limit N] [--json]`);
|
|
32
32
|
output(` ${commandName} notes <youtube-url> [youtube-url-or-playlist...] [engine] [--save]`);
|
|
33
33
|
output(` ${commandName} teach <youtube-url> [--section N] [--save] [--recap TEXT] [--skip]`);
|
|
34
|
+
output(` ${commandName} teach owed [--json]`);
|
|
35
|
+
output(` ${commandName} teach next [--json]`);
|
|
34
36
|
output(` ${commandName} unsave <url-or-id>`);
|
|
35
37
|
output(` ${commandName} process <youtube-url> [options]`);
|
|
36
38
|
output(` ${commandName} digest [--days N]`);
|
|
@@ -40,14 +42,14 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
|
|
|
40
42
|
output(` ${commandName} watch tick`);
|
|
41
43
|
output(` ${commandName} <youtube-url> [options]`);
|
|
42
44
|
output('');
|
|
43
|
-
output('search = free local discovery (ytsearch / yt-dlp), returns youtu.be links');
|
|
44
|
-
output('search --paid = 5 credits, watch permalinks + titles from Atris');
|
|
45
|
-
output('notes = free local notes to stdout; ephemeral unless --save');
|
|
46
|
-
output('teach = one chapter from local captions;
|
|
47
|
-
output('rich ephemeral notes/teach print one apply next-step (no files)');
|
|
48
|
-
output('process = 5 credits cloud knowledge (needs a filled Apply)');
|
|
49
|
-
output('digest = one decision page from this week\'s video briefs');
|
|
50
|
-
output('watch = subscribed channels turn into briefs without a human');
|
|
45
|
+
output('search = free local discovery (ytsearch / yt-dlp), returns youtu.be links; rich free search writes one apply and a failing keep/revert pack; thin hands off to teach');
|
|
46
|
+
output('search --paid = 5 credits, watch permalinks + titles from Atris; rich paid search writes one apply and a failing keep/revert pack; thin hands off to teach');
|
|
47
|
+
output('notes = free local notes to stdout; ephemeral unless --save; hands off to teach');
|
|
48
|
+
output('teach = one chapter from local captions; bare teach resumes unpaid checks, then the next chapter after recap or skip');
|
|
49
|
+
output('rich ephemeral notes/teach print one apply next-step and one failing check (no files)');
|
|
50
|
+
output('process = 5 credits cloud knowledge (needs a filled Apply); rich process writes one apply and a failing keep/revert pack');
|
|
51
|
+
output('digest = one decision page from this week\'s video briefs; rich digest writes one apply and a failing keep/revert pack');
|
|
52
|
+
output('watch = subscribed channels turn into briefs without a human; add hands off to tick; tick hands off to teach when it briefed');
|
|
51
53
|
output('Process a YouTube video through Atris using timestamped transcript-first analysis.');
|
|
52
54
|
output('Falls back to cloud video processing when local captions are unavailable.');
|
|
53
55
|
output('');
|
|
@@ -58,6 +60,8 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
|
|
|
58
60
|
output(' --section <n> Chapter to teach, 1-based (teach only, default: 1)');
|
|
59
61
|
output(' --recap <text> Unlock the next teach section with the unpaid check');
|
|
60
62
|
output(' --skip Unlock the next teach section without answering');
|
|
63
|
+
output(' owed Print the unpaid teach check (no network)');
|
|
64
|
+
output(' next Next unlocked chapter (no url, no billing)');
|
|
61
65
|
output(' --unsave Delete filed brief, apply stub, and matching notes/teach experiment packs (no paid calls)');
|
|
62
66
|
output(' --query, -q <text> Focus question for the analysis');
|
|
63
67
|
output(' --agent <id> Agent id to store knowledge against');
|
|
@@ -77,6 +81,7 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
|
|
|
77
81
|
output(` ${commandName} notes https://www.youtube.com/watch?v=VIDEO_ID --save`);
|
|
78
82
|
output(` ${commandName} teach "https://www.youtube.com/watch?v=VIDEO_ID"`);
|
|
79
83
|
output(` ${commandName} teach "https://www.youtube.com/watch?v=VIDEO_ID" --section 2`);
|
|
84
|
+
output(` ${commandName} teach next`);
|
|
80
85
|
output(` ${commandName} notes --unsave VIDEO_ID`);
|
|
81
86
|
output(` ${commandName} unsave VIDEO_ID`);
|
|
82
87
|
output(` ${commandName} notes https://www.youtube.com/watch?v=VIDEO_ID https://youtu.be/OTHER_ID`);
|
|
@@ -206,8 +211,16 @@ function youtubeFailureError(result) {
|
|
|
206
211
|
? ' Run "atris login --force".'
|
|
207
212
|
: result.status === 402
|
|
208
213
|
? ' Check Atris credits.'
|
|
209
|
-
:
|
|
210
|
-
|
|
214
|
+
: result.status === 502
|
|
215
|
+
? ' YouTube processing is unavailable; retry in a few seconds.'
|
|
216
|
+
: '';
|
|
217
|
+
const credits = paidSearchCredits(result.data);
|
|
218
|
+
const refundHint = result.status === 502 && creditsRefundedExplicitly(credits)
|
|
219
|
+
? ' credits refunded.'
|
|
220
|
+
: '';
|
|
221
|
+
const lines = [`YouTube processing failed (${result.status}): ${resultErrorText(result)}.${hint}${refundHint}`];
|
|
222
|
+
lines.push(...formatCreditsLines(credits));
|
|
223
|
+
return new Error(lines.join('\n'));
|
|
211
224
|
}
|
|
212
225
|
|
|
213
226
|
function captionHostAllowed(urlString) {
|
|
@@ -401,33 +414,110 @@ function parseCaptionText(raw) {
|
|
|
401
414
|
return segments.join(' ');
|
|
402
415
|
}
|
|
403
416
|
|
|
417
|
+
function parseYtDlpInfoJson(result) {
|
|
418
|
+
const raw = String((result && result.stdout) || '').trim();
|
|
419
|
+
if (!raw) return null;
|
|
420
|
+
try {
|
|
421
|
+
const info = JSON.parse(raw);
|
|
422
|
+
return info && typeof info === 'object' && !Array.isArray(info) ? info : null;
|
|
423
|
+
} catch {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function localCaptionNames(id) {
|
|
429
|
+
// scripts/det/ytnotes keeps the same VTT names (not clean.txt).
|
|
430
|
+
return [
|
|
431
|
+
`yt_${id}.en.vtt`,
|
|
432
|
+
`yt_${id}.en-orig.vtt`,
|
|
433
|
+
`yt_${id}.en-US.vtt`,
|
|
434
|
+
`yt_${id}.en-GB.vtt`,
|
|
435
|
+
`yt_${id}.clean.txt`,
|
|
436
|
+
];
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function captionLookupIds({ url, id } = {}) {
|
|
440
|
+
const ids = [];
|
|
441
|
+
const seen = new Set();
|
|
442
|
+
const add = (value) => {
|
|
443
|
+
const text = String(value || '').trim();
|
|
444
|
+
if (!text || seen.has(text)) return;
|
|
445
|
+
seen.add(text);
|
|
446
|
+
ids.push(text);
|
|
447
|
+
};
|
|
448
|
+
add(id);
|
|
449
|
+
add(videoIdFromUrl(url));
|
|
450
|
+
return ids;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function readLocalCaptionText({ url, id, workDir } = {}) {
|
|
454
|
+
if (!workDir) return '';
|
|
455
|
+
for (const videoId of captionLookupIds({ url, id })) {
|
|
456
|
+
for (const name of localCaptionNames(videoId)) {
|
|
457
|
+
const abs = path.join(workDir, name);
|
|
458
|
+
try {
|
|
459
|
+
if (!fs.existsSync(abs)) continue;
|
|
460
|
+
const text = fs.readFileSync(abs, 'utf8');
|
|
461
|
+
if (String(text).trim()) return text;
|
|
462
|
+
} catch {
|
|
463
|
+
// try the next written caption file
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return '';
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function loadCaptionRaw(info, youtubeUrl, deps = {}) {
|
|
471
|
+
const payload = info && typeof info === 'object' ? info : {};
|
|
472
|
+
const selected = chooseCaptionTrack(payload);
|
|
473
|
+
let raw = '';
|
|
474
|
+
if (selected?.track?.url) {
|
|
475
|
+
raw = await (deps.fetchCaptionText || fetchCaptionText)(selected.track.url);
|
|
476
|
+
}
|
|
477
|
+
if (String(raw || '').trim()) {
|
|
478
|
+
return { raw, language: selected?.language || 'unknown' };
|
|
479
|
+
}
|
|
480
|
+
const local = readLocalCaptionText({
|
|
481
|
+
url: youtubeUrl,
|
|
482
|
+
id: payload.id,
|
|
483
|
+
workDir: deps.workDir,
|
|
484
|
+
});
|
|
485
|
+
if (!String(local || '').trim()) return null;
|
|
486
|
+
return { raw: local, language: selected?.language || 'en' };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function ytDlpInfoArgs(youtubeUrl) {
|
|
490
|
+
return ['-J', '--skip-download', '--no-warnings', '--no-playlist', youtubeUrl];
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function teachSourceVideoId(info, youtubeUrl) {
|
|
494
|
+
const fromUrl = videoIdFromUrl(youtubeUrl);
|
|
495
|
+
const fromInfo = info && info.id;
|
|
496
|
+
const entries = info && Array.isArray(info.entries) ? info.entries : [];
|
|
497
|
+
const fromEntry = entries[0] && entries[0].id;
|
|
498
|
+
if (info && (info._type === 'playlist' || entries.length)) {
|
|
499
|
+
return fromUrl || fromEntry || fromInfo;
|
|
500
|
+
}
|
|
501
|
+
return fromInfo || fromUrl;
|
|
502
|
+
}
|
|
503
|
+
|
|
404
504
|
async function extractLocalTranscript(youtubeUrl, deps = {}) {
|
|
405
505
|
if (process.env.ATRIS_YOUTUBE_LOCAL_TRANSCRIPT === '0') return null;
|
|
406
506
|
const runner = deps.spawnSync || spawnSync;
|
|
407
|
-
const result = runner('yt-dlp',
|
|
507
|
+
const result = runner('yt-dlp', ytDlpInfoArgs(youtubeUrl), {
|
|
408
508
|
encoding: 'utf8',
|
|
409
509
|
timeout: 20000,
|
|
410
510
|
maxBuffer: 10 * 1024 * 1024,
|
|
411
511
|
});
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
try {
|
|
416
|
-
info = JSON.parse(result.stdout);
|
|
417
|
-
} catch {
|
|
418
|
-
return null;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
const selected = chooseCaptionTrack(info);
|
|
422
|
-
if (!selected?.track?.url) return null;
|
|
423
|
-
const rawCaption = await (deps.fetchCaptionText || fetchCaptionText)(selected.track.url);
|
|
424
|
-
const transcript = parseCaptionText(rawCaption);
|
|
512
|
+
const info = parseYtDlpInfoJson(result);
|
|
513
|
+
const loaded = await loadCaptionRaw(info, youtubeUrl, deps);
|
|
514
|
+
const transcript = parseCaptionText(loaded && loaded.raw);
|
|
425
515
|
if (!transcript) return null;
|
|
426
516
|
|
|
427
517
|
return {
|
|
428
518
|
transcriptText: transcript.slice(0, LOCAL_TRANSCRIPT_MAX_CHARS),
|
|
429
|
-
language:
|
|
430
|
-
durationSeconds: Number(info.duration || 0) || undefined,
|
|
519
|
+
language: loaded.language || 'unknown',
|
|
520
|
+
durationSeconds: Number((info && info.duration) || 0) || undefined,
|
|
431
521
|
};
|
|
432
522
|
}
|
|
433
523
|
|
|
@@ -463,6 +553,12 @@ async function processYoutube(options, deps = {}) {
|
|
|
463
553
|
if (!result.ok && result.status === 401 && !auth.minted) {
|
|
464
554
|
const remint = await ensureBilled('youtube', { ...deps, forceMint: true });
|
|
465
555
|
if (remint?.ok && remint.token) {
|
|
556
|
+
if (!options.json) {
|
|
557
|
+
const print = typeof deps.output === 'function' ? deps.output : () => {};
|
|
558
|
+
for (const line of formatCreditsLines(paidSearchCredits(result.data))) {
|
|
559
|
+
print(line);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
466
562
|
auth = remint;
|
|
467
563
|
result = await apiFn('/agent/process_youtube', {
|
|
468
564
|
method: 'POST',
|
|
@@ -479,7 +575,10 @@ async function processYoutube(options, deps = {}) {
|
|
|
479
575
|
const localExtractor = deps.extractLocalTranscript || extractLocalTranscript;
|
|
480
576
|
let localTranscript = null;
|
|
481
577
|
try {
|
|
482
|
-
localTranscript = await localExtractor(options.youtubeUrl,
|
|
578
|
+
localTranscript = await localExtractor(options.youtubeUrl, {
|
|
579
|
+
...deps,
|
|
580
|
+
workDir: deps.workDir || notesWorkDir(deps),
|
|
581
|
+
});
|
|
483
582
|
} catch {
|
|
484
583
|
localTranscript = null;
|
|
485
584
|
}
|
|
@@ -496,6 +595,12 @@ async function processYoutube(options, deps = {}) {
|
|
|
496
595
|
if (transcriptResult.status === 401 || transcriptResult.status === 402 || transcriptResult.status === 400) {
|
|
497
596
|
throw youtubeFailureError(transcriptResult);
|
|
498
597
|
}
|
|
598
|
+
if (!options.json) {
|
|
599
|
+
const print = typeof deps.output === 'function' ? deps.output : () => {};
|
|
600
|
+
for (const line of formatCreditsLines(paidSearchCredits(transcriptResult.data))) {
|
|
601
|
+
print(line);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
499
604
|
}
|
|
500
605
|
|
|
501
606
|
const result = await requestYoutube(buildYoutubePayload(options));
|
|
@@ -507,6 +612,17 @@ async function processYoutube(options, deps = {}) {
|
|
|
507
612
|
return result.data;
|
|
508
613
|
}
|
|
509
614
|
|
|
615
|
+
function processAnalysisText(data) {
|
|
616
|
+
return data?.video_analysis || data?.analysis || data?.result || '';
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function printProcessLearnerGate(data, { json = false } = {}, output) {
|
|
620
|
+
printLearnerCheckGate(output, notesLessonFromText(processAnalysisText(data)), {
|
|
621
|
+
includeCheck: true,
|
|
622
|
+
json,
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
|
|
510
626
|
function formatYoutubeResult(data) {
|
|
511
627
|
const lines = [];
|
|
512
628
|
const metadata = data?.metadata || {};
|
|
@@ -521,12 +637,9 @@ function formatYoutubeResult(data) {
|
|
|
521
637
|
: '';
|
|
522
638
|
lines.push(`Processing: ${method}${source}`);
|
|
523
639
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
lines.push(`Credits: ${used} used, ${remaining} remaining`);
|
|
528
|
-
}
|
|
529
|
-
const analysis = data?.video_analysis || data?.analysis || data?.result;
|
|
640
|
+
const creditLines = formatCreditsLines(paidSearchCredits(data));
|
|
641
|
+
if (creditLines.length) lines.push(...creditLines);
|
|
642
|
+
const analysis = processAnalysisText(data);
|
|
530
643
|
if (analysis) {
|
|
531
644
|
lines.push('');
|
|
532
645
|
lines.push(String(analysis).trim());
|
|
@@ -535,11 +648,13 @@ function formatYoutubeResult(data) {
|
|
|
535
648
|
}
|
|
536
649
|
|
|
537
650
|
function videoIdFromUrl(url) {
|
|
538
|
-
const text = String(url || '');
|
|
651
|
+
const text = String(url || '').split('#')[0];
|
|
539
652
|
const watch = text.match(/[?&]v=([^&]+)/);
|
|
540
653
|
if (watch) return watch[1];
|
|
541
654
|
const short = text.match(/youtu\.be\/([^?&/]+)/);
|
|
542
|
-
|
|
655
|
+
if (short) return short[1];
|
|
656
|
+
const pathId = text.match(/\/(?:shorts|embed|live|v|e)\/([^?&/]+)/i);
|
|
657
|
+
return pathId ? pathId[1] : null;
|
|
543
658
|
}
|
|
544
659
|
|
|
545
660
|
function videoIdFromArg(arg) {
|
|
@@ -553,7 +668,7 @@ function videoIdFromArg(arg) {
|
|
|
553
668
|
function looksLikeYoutubeUrl(arg) {
|
|
554
669
|
const text = String(arg || '').trim();
|
|
555
670
|
if (!text || text.startsWith('-')) return false;
|
|
556
|
-
return /youtube\.com|youtu\.be/i.test(text);
|
|
671
|
+
return /youtube\.com|youtu\.be|youtube-nocookie\.com/i.test(text);
|
|
557
672
|
}
|
|
558
673
|
|
|
559
674
|
function isPlaylistUrl(url) {
|
|
@@ -567,22 +682,24 @@ function parseNotesArgs(argv = []) {
|
|
|
567
682
|
let help = false;
|
|
568
683
|
let save = false;
|
|
569
684
|
let unsave = false;
|
|
685
|
+
let json = false;
|
|
570
686
|
for (const raw of argv) {
|
|
571
687
|
const arg = String(raw);
|
|
572
688
|
if (arg === '--help' || arg === '-h' || arg === 'help') help = true;
|
|
573
689
|
else if (arg === '--save') save = true;
|
|
574
690
|
else if (arg === '--unsave') unsave = true;
|
|
691
|
+
else if (arg === '--json') json = true;
|
|
575
692
|
}
|
|
576
693
|
for (const raw of argv) {
|
|
577
694
|
const arg = String(raw);
|
|
578
695
|
if (arg === '--help' || arg === '-h' || arg === 'help') continue;
|
|
579
|
-
if (arg === '--save' || arg === '--unsave') continue;
|
|
696
|
+
if (arg === '--save' || arg === '--unsave' || arg === '--json') continue;
|
|
580
697
|
if (arg.startsWith('-')) continue;
|
|
581
698
|
if (looksLikeYoutubeUrl(arg)) urls.push(arg);
|
|
582
699
|
else if (unsave && videoIdFromArg(arg)) urls.push(arg);
|
|
583
700
|
else engine = arg;
|
|
584
701
|
}
|
|
585
|
-
return { urls, engine, help, save, unsave };
|
|
702
|
+
return { urls, engine, help, save, unsave, json };
|
|
586
703
|
}
|
|
587
704
|
|
|
588
705
|
function dateStamp(now) {
|
|
@@ -676,11 +793,19 @@ function readNotesText({ url, workDir } = {}) {
|
|
|
676
793
|
}
|
|
677
794
|
}
|
|
678
795
|
|
|
796
|
+
function notesWorkDir(deps = {}) {
|
|
797
|
+
return deps.workDir || path.join(process.env.TMPDIR || '/tmp', 'ytnotes');
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function keptPrintedNotes({ url, workDir } = {}) {
|
|
801
|
+
return Boolean(String(readNotesText({ url, workDir }) || '').trim());
|
|
802
|
+
}
|
|
803
|
+
|
|
679
804
|
function saveRichNotes(url, deps = {}) {
|
|
680
805
|
const workDir = deps.workDir || path.join(process.env.TMPDIR || '/tmp', 'ytnotes');
|
|
681
806
|
const lesson = notesLessonFromText(readNotesText({ url, workDir }));
|
|
682
807
|
if (isThinTeachLesson(lesson)) {
|
|
683
|
-
return { thin: true, brief: null, packRel: null };
|
|
808
|
+
return { thin: true, brief: null, packRel: null, lesson };
|
|
684
809
|
}
|
|
685
810
|
const brief = fileNotesBrief(url, deps);
|
|
686
811
|
const id = videoIdFromUrl(url);
|
|
@@ -691,20 +816,21 @@ function saveRichNotes(url, deps = {}) {
|
|
|
691
816
|
slug: id ? notesExperimentSlug(id) : null,
|
|
692
817
|
applyRel: id ? applySidecarRel(id) : null,
|
|
693
818
|
});
|
|
694
|
-
return { thin: false, brief, packRel };
|
|
819
|
+
return { thin: false, brief, packRel, lesson };
|
|
695
820
|
}
|
|
696
821
|
|
|
697
822
|
function ensureNotesApply({ cwd, url, packRel, now, output } = {}) {
|
|
698
823
|
const id = videoIdFromUrl(url);
|
|
699
824
|
const pack = packRel || (id ? notesExperimentRel(id) : null);
|
|
825
|
+
const slug = pack ? path.basename(pack) : null;
|
|
700
826
|
return applyGate.ensureApply({
|
|
701
827
|
cwd,
|
|
702
828
|
source: url,
|
|
703
829
|
rel: id ? applySidecarRel(id) : null,
|
|
704
830
|
now,
|
|
705
831
|
output,
|
|
706
|
-
incompleteMessage:
|
|
707
|
-
? `next:
|
|
832
|
+
incompleteMessage: slug
|
|
833
|
+
? `next: atris experiments keep ${slug}`
|
|
708
834
|
: APPLY_NEXT_MESSAGE,
|
|
709
835
|
required: false,
|
|
710
836
|
change: pack ? `apply ${pack}` : undefined,
|
|
@@ -787,6 +913,8 @@ function unsaveYoutubeNotes(target, deps = {}) {
|
|
|
787
913
|
}
|
|
788
914
|
for (const rel of listTeachSidecarRels(cwd, id)) add(rel);
|
|
789
915
|
add(notesExperimentRel(id));
|
|
916
|
+
add(processApplyRel(id));
|
|
917
|
+
add(processExperimentRel(id));
|
|
790
918
|
for (const section of sections) add(teachExperimentRel(id, section));
|
|
791
919
|
|
|
792
920
|
const removed = [];
|
|
@@ -815,6 +943,9 @@ function runYoutubeUnsave(args = [], deps = {}) {
|
|
|
815
943
|
const status = unsaveYoutubeNotes(target, deps);
|
|
816
944
|
if (status !== 0) code = status;
|
|
817
945
|
}
|
|
946
|
+
if (code === 0 && parsed.json !== true && deps.json !== true) {
|
|
947
|
+
printWatchSearchNext(output);
|
|
948
|
+
}
|
|
818
949
|
return code;
|
|
819
950
|
}
|
|
820
951
|
|
|
@@ -831,6 +962,85 @@ function ensureProcessApply({ cwd, url, now, output } = {}) {
|
|
|
831
962
|
});
|
|
832
963
|
}
|
|
833
964
|
|
|
965
|
+
function processExperimentSlug(id) {
|
|
966
|
+
return `process-${experimentIdToken(id)}`;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function processExperimentRel(id) {
|
|
970
|
+
return `atris/experiments/${processExperimentSlug(id)}`;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function processApplyRel(id) {
|
|
974
|
+
return applyGate.applySidecarRel('process', experimentIdToken(id));
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function saveRichProcess({ cwd, url, lesson } = {}) {
|
|
978
|
+
if (isThinTeachLesson(lesson)) {
|
|
979
|
+
return { thin: true, packRel: null, lesson };
|
|
980
|
+
}
|
|
981
|
+
if (cwd) fs.mkdirSync(path.join(cwd, 'atris', 'wiki'), { recursive: true });
|
|
982
|
+
const id = videoIdFromUrl(url);
|
|
983
|
+
const packRel = fileTeachExperiment({
|
|
984
|
+
cwd,
|
|
985
|
+
url,
|
|
986
|
+
lesson,
|
|
987
|
+
slug: id ? processExperimentSlug(id) : null,
|
|
988
|
+
applyRel: id ? processApplyRel(id) : null,
|
|
989
|
+
});
|
|
990
|
+
return { thin: false, packRel, lesson, source: url };
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function ensureProcessLearnerApply({ cwd, url, packRel, now, output, source } = {}) {
|
|
994
|
+
const id = videoIdFromUrl(url);
|
|
995
|
+
const pack = packRel || (id ? processExperimentRel(id) : null);
|
|
996
|
+
const slug = pack ? path.basename(pack) : null;
|
|
997
|
+
if (cwd) fs.mkdirSync(path.join(cwd, 'atris', 'wiki'), { recursive: true });
|
|
998
|
+
return applyGate.ensureApply({
|
|
999
|
+
cwd,
|
|
1000
|
+
source: source || url || (id ? `process:${id}` : 'process'),
|
|
1001
|
+
rel: id ? processApplyRel(id) : null,
|
|
1002
|
+
now,
|
|
1003
|
+
output,
|
|
1004
|
+
incompleteMessage: slug
|
|
1005
|
+
? `next: atris experiments keep ${slug}`
|
|
1006
|
+
: applyGate.ephemeralApplyMessage('process'),
|
|
1007
|
+
required: false,
|
|
1008
|
+
change: pack ? `apply ${pack}` : undefined,
|
|
1009
|
+
receipt: pack ? TEACH_KEEP_RULE : undefined,
|
|
1010
|
+
journalLine: pack ? `- [claimable] apply: ${pack}. ${TEACH_KEEP_RULE}` : undefined,
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function mintRichProcess({ cwd, url, data, now, output, ensureApply, json } = {}) {
|
|
1015
|
+
const print = typeof output === 'function' ? output : (line = '') => console.log(line);
|
|
1016
|
+
const lesson = notesLessonFromText(processAnalysisText(data));
|
|
1017
|
+
if (isThinTeachLesson(lesson)) {
|
|
1018
|
+
printProcessLearnerGate(data, { json }, print);
|
|
1019
|
+
return 0;
|
|
1020
|
+
}
|
|
1021
|
+
const saved = saveRichProcess({ cwd, url, lesson });
|
|
1022
|
+
const applyFn = ensureApply || ensureProcessLearnerApply;
|
|
1023
|
+
const applyCode = applyFn({
|
|
1024
|
+
cwd,
|
|
1025
|
+
url,
|
|
1026
|
+
packRel: saved.packRel,
|
|
1027
|
+
now,
|
|
1028
|
+
output: print,
|
|
1029
|
+
source: url,
|
|
1030
|
+
});
|
|
1031
|
+
if (ensureApply) return applyCode;
|
|
1032
|
+
const id = videoIdFromUrl(url);
|
|
1033
|
+
const baseline = proveSavedLearnerBaseline({
|
|
1034
|
+
cwd,
|
|
1035
|
+
applyRel: id ? processApplyRel(id) : null,
|
|
1036
|
+
lesson: saved.lesson,
|
|
1037
|
+
output: print,
|
|
1038
|
+
json,
|
|
1039
|
+
});
|
|
1040
|
+
if (baseline !== 0) return baseline;
|
|
1041
|
+
return applyCode;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
834
1044
|
const DIGEST_ENGINE_TIMEOUT_MS = 240000;
|
|
835
1045
|
const DEFAULT_DIGEST_DAYS = 7;
|
|
836
1046
|
|
|
@@ -977,6 +1187,113 @@ function appendDigestJournal({ cwd, date, relDigest }) {
|
|
|
977
1187
|
fs.writeFileSync(journalPath, `${existing}${prefix}${line}\n`);
|
|
978
1188
|
}
|
|
979
1189
|
|
|
1190
|
+
function digestExperimentSlug(date) {
|
|
1191
|
+
return `digest-${String(date || '').slice(0, 10)}`;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function digestExperimentRel(date) {
|
|
1195
|
+
return `atris/experiments/${digestExperimentSlug(date)}`;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function digestApplyRel(date) {
|
|
1199
|
+
return applyGate.applySidecarRel('digest', String(date || '').slice(0, 10));
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function ensureDigestApply({ cwd, date, packRel, now, output, source } = {}) {
|
|
1203
|
+
const stamp = String(date || '').slice(0, 10);
|
|
1204
|
+
const pack = packRel || (stamp ? digestExperimentRel(stamp) : null);
|
|
1205
|
+
const slug = pack ? path.basename(pack) : null;
|
|
1206
|
+
return applyGate.ensureApply({
|
|
1207
|
+
cwd,
|
|
1208
|
+
source: source || (stamp ? `digest:${stamp}` : 'digest'),
|
|
1209
|
+
rel: stamp ? digestApplyRel(stamp) : null,
|
|
1210
|
+
now,
|
|
1211
|
+
output,
|
|
1212
|
+
incompleteMessage: slug
|
|
1213
|
+
? `next: atris experiments keep ${slug}`
|
|
1214
|
+
: applyGate.ephemeralApplyMessage('digest'),
|
|
1215
|
+
required: false,
|
|
1216
|
+
change: pack ? `apply ${pack}` : undefined,
|
|
1217
|
+
receipt: pack ? TEACH_KEEP_RULE : undefined,
|
|
1218
|
+
journalLine: pack ? `- [claimable] apply: ${pack}. ${TEACH_KEEP_RULE}` : undefined,
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
function saveRichDigest({ cwd, date, lesson, source } = {}) {
|
|
1223
|
+
if (isThinTeachLesson(lesson)) {
|
|
1224
|
+
return { thin: true, packRel: null, lesson };
|
|
1225
|
+
}
|
|
1226
|
+
const packRel = fileTeachExperiment({
|
|
1227
|
+
cwd,
|
|
1228
|
+
lesson,
|
|
1229
|
+
slug: date ? digestExperimentSlug(date) : null,
|
|
1230
|
+
applyRel: date ? digestApplyRel(date) : null,
|
|
1231
|
+
});
|
|
1232
|
+
return { thin: false, packRel, lesson, source };
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
function watchExperimentSlug(id) {
|
|
1236
|
+
return `watch-${experimentIdToken(id)}`;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
function watchExperimentRel(id) {
|
|
1240
|
+
return `atris/experiments/${watchExperimentSlug(id)}`;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
function watchApplyRel(id) {
|
|
1244
|
+
return applyGate.applySidecarRel('watch', experimentIdToken(id));
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function ensureWatchWiki(cwd) {
|
|
1248
|
+
if (!cwd) return;
|
|
1249
|
+
fs.mkdirSync(path.join(cwd, 'atris', 'wiki'), { recursive: true });
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function ensureWatchApply({ cwd, url, packRel, now, output, source } = {}) {
|
|
1253
|
+
const id = videoIdFromUrl(url);
|
|
1254
|
+
const pack = packRel || (id ? watchExperimentRel(id) : null);
|
|
1255
|
+
const slug = pack ? path.basename(pack) : null;
|
|
1256
|
+
ensureWatchWiki(cwd);
|
|
1257
|
+
return applyGate.ensureApply({
|
|
1258
|
+
cwd,
|
|
1259
|
+
source: source || url || (id ? `watch:${id}` : 'watch'),
|
|
1260
|
+
rel: id ? watchApplyRel(id) : null,
|
|
1261
|
+
now,
|
|
1262
|
+
output,
|
|
1263
|
+
incompleteMessage: slug
|
|
1264
|
+
? `next: atris experiments keep ${slug}`
|
|
1265
|
+
: applyGate.ephemeralApplyMessage('watch'),
|
|
1266
|
+
required: false,
|
|
1267
|
+
change: pack ? `apply ${pack}` : undefined,
|
|
1268
|
+
receipt: pack ? TEACH_KEEP_RULE : undefined,
|
|
1269
|
+
journalLine: pack ? `- [claimable] apply: ${pack}. ${TEACH_KEEP_RULE}` : undefined,
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
function firstRichWatchLesson(urls, workDir) {
|
|
1274
|
+
for (const url of Array.isArray(urls) ? urls : []) {
|
|
1275
|
+
if (!url) continue;
|
|
1276
|
+
const lesson = notesLessonFromText(readNotesText({ url, workDir }));
|
|
1277
|
+
if (!isThinTeachLesson(lesson)) return { url, lesson };
|
|
1278
|
+
}
|
|
1279
|
+
return null;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
function saveRichWatch({ cwd, url, lesson } = {}) {
|
|
1283
|
+
if (isThinTeachLesson(lesson)) {
|
|
1284
|
+
return { thin: true, packRel: null, lesson };
|
|
1285
|
+
}
|
|
1286
|
+
const id = videoIdFromUrl(url);
|
|
1287
|
+
const packRel = fileTeachExperiment({
|
|
1288
|
+
cwd,
|
|
1289
|
+
url,
|
|
1290
|
+
lesson,
|
|
1291
|
+
slug: id ? watchExperimentSlug(id) : null,
|
|
1292
|
+
applyRel: id ? watchApplyRel(id) : null,
|
|
1293
|
+
});
|
|
1294
|
+
return { thin: false, packRel, lesson, source: url };
|
|
1295
|
+
}
|
|
1296
|
+
|
|
980
1297
|
function runYoutubeDigest(args = [], deps = {}) {
|
|
981
1298
|
const output = deps.output || ((line = '') => console.log(line));
|
|
982
1299
|
let options;
|
|
@@ -996,6 +1313,7 @@ function runYoutubeDigest(args = [], deps = {}) {
|
|
|
996
1313
|
const briefs = collectVideoBriefs({ cwd, now, days: options.days });
|
|
997
1314
|
if (!briefs.length) {
|
|
998
1315
|
output(`no video briefs in the last ${options.days} days`);
|
|
1316
|
+
printWatchSearchNext(output);
|
|
999
1317
|
return 0;
|
|
1000
1318
|
}
|
|
1001
1319
|
|
|
@@ -1023,7 +1341,32 @@ function runYoutubeDigest(args = [], deps = {}) {
|
|
|
1023
1341
|
fs.writeFileSync(path.join(cwd, relDigest), `${header}\n\n${text}\n`);
|
|
1024
1342
|
appendDigestJournal({ cwd, date, relDigest });
|
|
1025
1343
|
output(`digest filed: ${relDigest} (${briefs.length} briefs)`);
|
|
1026
|
-
|
|
1344
|
+
const lesson = notesLessonFromText(text);
|
|
1345
|
+
if (isThinTeachLesson(lesson)) {
|
|
1346
|
+
printLearnerCheckGate(output, lesson, { includeCheck: true });
|
|
1347
|
+
printWatchTickNext(output);
|
|
1348
|
+
return 0;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
const saved = saveRichDigest({ cwd, date, lesson, source: relDigest });
|
|
1352
|
+
const ensureApply = deps.ensureApply || ensureDigestApply;
|
|
1353
|
+
const applyCode = ensureApply({
|
|
1354
|
+
cwd,
|
|
1355
|
+
date,
|
|
1356
|
+
packRel: saved.packRel,
|
|
1357
|
+
now,
|
|
1358
|
+
output,
|
|
1359
|
+
source: relDigest,
|
|
1360
|
+
});
|
|
1361
|
+
if (deps.ensureApply) return applyCode;
|
|
1362
|
+
const baseline = proveSavedLearnerBaseline({
|
|
1363
|
+
cwd,
|
|
1364
|
+
applyRel: digestApplyRel(date),
|
|
1365
|
+
lesson,
|
|
1366
|
+
output,
|
|
1367
|
+
});
|
|
1368
|
+
if (baseline !== 0) return baseline;
|
|
1369
|
+
return applyCode;
|
|
1027
1370
|
}
|
|
1028
1371
|
|
|
1029
1372
|
function watchStatePath(cwd = process.cwd()) {
|
|
@@ -1130,6 +1473,7 @@ function defaultChannelFetcher(videosUrl, deps = {}) {
|
|
|
1130
1473
|
const result = spawn('yt-dlp', [
|
|
1131
1474
|
'--no-update',
|
|
1132
1475
|
'--flat-playlist',
|
|
1476
|
+
'--no-warnings',
|
|
1133
1477
|
'--playlist-end',
|
|
1134
1478
|
'3',
|
|
1135
1479
|
'--print',
|
|
@@ -1140,11 +1484,13 @@ function defaultChannelFetcher(videosUrl, deps = {}) {
|
|
|
1140
1484
|
timeout: 60000,
|
|
1141
1485
|
maxBuffer: 2 * 1024 * 1024,
|
|
1142
1486
|
});
|
|
1487
|
+
const videos = parseFlatPlaylist(result && result.stdout);
|
|
1488
|
+
if (videos.length) return videos;
|
|
1143
1489
|
if (result.error || result.status !== 0) {
|
|
1144
1490
|
const detail = String(result.stderr || result.error?.message || 'fetch failed').trim();
|
|
1145
1491
|
throw new Error(detail || 'fetch failed');
|
|
1146
1492
|
}
|
|
1147
|
-
return
|
|
1493
|
+
return videos;
|
|
1148
1494
|
}
|
|
1149
1495
|
|
|
1150
1496
|
function defaultNotesRunner(url, deps = {}) {
|
|
@@ -1172,6 +1518,22 @@ function markSeen(state, channel, id, timestamp) {
|
|
|
1172
1518
|
state.seenByChannel[channel][id] = timestamp;
|
|
1173
1519
|
}
|
|
1174
1520
|
|
|
1521
|
+
const WATCH_TICK_NEXT = 'next: atris youtube watch tick';
|
|
1522
|
+
const WATCH_ADD_NEXT = 'next: atris youtube watch add <channel-url-or-@handle>';
|
|
1523
|
+
const WATCH_SEARCH_NEXT = 'next: atris youtube search " "';
|
|
1524
|
+
|
|
1525
|
+
function printWatchTickNext(output) {
|
|
1526
|
+
output(WATCH_TICK_NEXT);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
function printWatchAddNext(output) {
|
|
1530
|
+
output(WATCH_ADD_NEXT);
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
function printWatchSearchNext(output) {
|
|
1534
|
+
output(WATCH_SEARCH_NEXT);
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1175
1537
|
function addWatchChannel(channelInput, deps = {}) {
|
|
1176
1538
|
const output = deps.output || ((line = '') => console.log(line));
|
|
1177
1539
|
if (!channelInput) {
|
|
@@ -1191,6 +1553,7 @@ function addWatchChannel(channelInput, deps = {}) {
|
|
|
1191
1553
|
const state = loadWatchState(statePath);
|
|
1192
1554
|
if (state.channels.some((row) => row.channel === channel)) {
|
|
1193
1555
|
output(`already watching ${channel}`);
|
|
1556
|
+
printWatchTickNext(output);
|
|
1194
1557
|
return 0;
|
|
1195
1558
|
}
|
|
1196
1559
|
|
|
@@ -1200,6 +1563,7 @@ function addWatchChannel(channelInput, deps = {}) {
|
|
|
1200
1563
|
});
|
|
1201
1564
|
saveWatchState(statePath, state);
|
|
1202
1565
|
output(`watching ${channel}`);
|
|
1566
|
+
printWatchTickNext(output);
|
|
1203
1567
|
return 0;
|
|
1204
1568
|
}
|
|
1205
1569
|
|
|
@@ -1208,6 +1572,7 @@ function listWatchChannels(deps = {}) {
|
|
|
1208
1572
|
const state = loadWatchState(resolveWatchStatePath(deps));
|
|
1209
1573
|
if (!state.channels.length) {
|
|
1210
1574
|
output('no channels watched');
|
|
1575
|
+
printWatchAddNext(output);
|
|
1211
1576
|
return 0;
|
|
1212
1577
|
}
|
|
1213
1578
|
|
|
@@ -1215,6 +1580,7 @@ function listWatchChannels(deps = {}) {
|
|
|
1215
1580
|
const count = Object.keys(channelSeenMap(state, row.channel)).length;
|
|
1216
1581
|
output(`${index + 1}. ${row.channel} (${count} seen)`);
|
|
1217
1582
|
});
|
|
1583
|
+
printWatchTickNext(output);
|
|
1218
1584
|
return 0;
|
|
1219
1585
|
}
|
|
1220
1586
|
|
|
@@ -1231,6 +1597,11 @@ function removeWatchChannel(rawNumber, deps = {}) {
|
|
|
1231
1597
|
const [removed] = state.channels.splice(index - 1, 1);
|
|
1232
1598
|
saveWatchState(statePath, state);
|
|
1233
1599
|
output(`removed ${removed.channel}`);
|
|
1600
|
+
if (!state.channels.length) {
|
|
1601
|
+
printWatchAddNext(output);
|
|
1602
|
+
} else {
|
|
1603
|
+
printWatchTickNext(output);
|
|
1604
|
+
}
|
|
1234
1605
|
return 0;
|
|
1235
1606
|
}
|
|
1236
1607
|
|
|
@@ -1248,6 +1619,8 @@ async function tickWatch(deps = {}) {
|
|
|
1248
1619
|
const state = loadWatchState(statePath);
|
|
1249
1620
|
let totalNew = 0;
|
|
1250
1621
|
let totalBriefed = 0;
|
|
1622
|
+
let firstBriefedUrl = null;
|
|
1623
|
+
const briefedUrls = [];
|
|
1251
1624
|
|
|
1252
1625
|
for (const row of state.channels) {
|
|
1253
1626
|
const videosUrl = channelVideosUrl(row.channel);
|
|
@@ -1294,6 +1667,8 @@ async function tickWatch(deps = {}) {
|
|
|
1294
1667
|
}
|
|
1295
1668
|
markSeen(state, row.channel, video.id, timestamp);
|
|
1296
1669
|
briefed += 1;
|
|
1670
|
+
briefedUrls.push(url);
|
|
1671
|
+
if (!firstBriefedUrl) firstBriefedUrl = url;
|
|
1297
1672
|
}
|
|
1298
1673
|
|
|
1299
1674
|
if (!isFresh) {
|
|
@@ -1311,6 +1686,41 @@ async function tickWatch(deps = {}) {
|
|
|
1311
1686
|
|
|
1312
1687
|
output(`total: ${totalNew} new, ${totalBriefed} briefed`);
|
|
1313
1688
|
saveWatchState(statePath, state);
|
|
1689
|
+
if (totalBriefed > 0) {
|
|
1690
|
+
const rich = firstRichWatchLesson(briefedUrls, workDir);
|
|
1691
|
+
if (rich) {
|
|
1692
|
+
const saved = saveRichWatch({ cwd, url: rich.url, lesson: rich.lesson });
|
|
1693
|
+
const ensureApply = deps.ensureApply || ensureWatchApply;
|
|
1694
|
+
const applyCode = ensureApply({
|
|
1695
|
+
cwd,
|
|
1696
|
+
url: rich.url,
|
|
1697
|
+
packRel: saved.packRel,
|
|
1698
|
+
now,
|
|
1699
|
+
output,
|
|
1700
|
+
source: rich.url,
|
|
1701
|
+
});
|
|
1702
|
+
if (deps.ensureApply) return applyCode;
|
|
1703
|
+
const id = videoIdFromUrl(rich.url);
|
|
1704
|
+
const baseline = proveSavedLearnerBaseline({
|
|
1705
|
+
cwd,
|
|
1706
|
+
applyRel: id ? watchApplyRel(id) : null,
|
|
1707
|
+
lesson: rich.lesson,
|
|
1708
|
+
output,
|
|
1709
|
+
});
|
|
1710
|
+
if (baseline !== 0) return baseline;
|
|
1711
|
+
return applyCode;
|
|
1712
|
+
}
|
|
1713
|
+
if (firstBriefedUrl) {
|
|
1714
|
+
const lesson = notesLessonFromText(readNotesText({ url: firstBriefedUrl, workDir }));
|
|
1715
|
+
printLearnerCheckGate(output, lesson, { includeCheck: true });
|
|
1716
|
+
printYoutubeTeachNext(firstBriefedUrl, {}, output);
|
|
1717
|
+
return 0;
|
|
1718
|
+
}
|
|
1719
|
+
printYoutubeTeachNext(firstBriefedUrl, {}, output);
|
|
1720
|
+
return 0;
|
|
1721
|
+
}
|
|
1722
|
+
if (!state.channels.length) printWatchAddNext(output);
|
|
1723
|
+
else printWatchSearchNext(output);
|
|
1314
1724
|
return 0;
|
|
1315
1725
|
}
|
|
1316
1726
|
|
|
@@ -1342,11 +1752,13 @@ function defaultPlaylistExpander(playlistUrl, deps = {}) {
|
|
|
1342
1752
|
timeout: 60000,
|
|
1343
1753
|
maxBuffer: 2 * 1024 * 1024,
|
|
1344
1754
|
});
|
|
1755
|
+
const videos = parseFlatPlaylist(result && result.stdout);
|
|
1756
|
+
if (videos.length) return videos;
|
|
1345
1757
|
if (result.error || (result.status != null && result.status !== 0)) {
|
|
1346
1758
|
const detail = String(result.stderr || result.error?.message || 'playlist expand failed').trim();
|
|
1347
1759
|
throw new Error(detail || 'playlist expand failed');
|
|
1348
1760
|
}
|
|
1349
|
-
return
|
|
1761
|
+
return videos;
|
|
1350
1762
|
}
|
|
1351
1763
|
|
|
1352
1764
|
function defaultNotesItemRunner(url, engine, deps = {}) {
|
|
@@ -1440,14 +1852,20 @@ function runOneNotesItem(item, engine, deps = {}) {
|
|
|
1440
1852
|
}
|
|
1441
1853
|
|
|
1442
1854
|
const started = readNowMs(deps);
|
|
1443
|
-
let
|
|
1855
|
+
let result = { status: 1 };
|
|
1444
1856
|
try {
|
|
1445
|
-
|
|
1446
|
-
} catch {
|
|
1447
|
-
|
|
1857
|
+
result = invokeNotesRunner(item.url, engine, deps);
|
|
1858
|
+
} catch (err) {
|
|
1859
|
+
result = { status: 1, stderr: String((err && err.message) || err || '') };
|
|
1448
1860
|
}
|
|
1861
|
+
const status = readRunnerStatus(result);
|
|
1449
1862
|
let brief = null;
|
|
1450
|
-
let
|
|
1863
|
+
let lesson = null;
|
|
1864
|
+
let ok = status === 0 || keptPrintedNotes({
|
|
1865
|
+
url: item.url,
|
|
1866
|
+
workDir: notesWorkDir(deps),
|
|
1867
|
+
result,
|
|
1868
|
+
});
|
|
1451
1869
|
if (ok && deps.save) {
|
|
1452
1870
|
const saved = saveRichNotes(item.url, deps);
|
|
1453
1871
|
if (saved.thin) {
|
|
@@ -1455,6 +1873,7 @@ function runOneNotesItem(item, engine, deps = {}) {
|
|
|
1455
1873
|
ok = false;
|
|
1456
1874
|
} else {
|
|
1457
1875
|
brief = saved.brief;
|
|
1876
|
+
lesson = saved.lesson;
|
|
1458
1877
|
const ensureApply = deps.ensureApply || ensureNotesApply;
|
|
1459
1878
|
try {
|
|
1460
1879
|
ensureApply({
|
|
@@ -1471,7 +1890,7 @@ function runOneNotesItem(item, engine, deps = {}) {
|
|
|
1471
1890
|
}
|
|
1472
1891
|
const seconds = Math.max(0, Math.round((readNowMs(deps) - started) / 1000));
|
|
1473
1892
|
output(`${label} ${seconds}s ${ok ? (brief || 'ok') : 'FAILED'}`);
|
|
1474
|
-
return { url: item.url, id: item.id, seconds, ok, brief };
|
|
1893
|
+
return { url: item.url, id: item.id, seconds, ok, brief, lesson };
|
|
1475
1894
|
}
|
|
1476
1895
|
|
|
1477
1896
|
function formatNotesSummary(rows = []) {
|
|
@@ -1483,7 +1902,14 @@ function formatNotesSummary(rows = []) {
|
|
|
1483
1902
|
return lines.join('\n');
|
|
1484
1903
|
}
|
|
1485
1904
|
|
|
1486
|
-
function
|
|
1905
|
+
function printEphemeralNotesLearnerGate(url, { json = false, workDir } = {}, output) {
|
|
1906
|
+
const dir = workDir || path.join(process.env.TMPDIR || '/tmp', 'ytnotes');
|
|
1907
|
+
const lesson = notesLessonFromText(readNotesText({ url, workDir: dir }));
|
|
1908
|
+
if (!isThinTeachLesson(lesson)) applyGate.hintEphemeralApply(output, 'notes');
|
|
1909
|
+
printLearnerCheckGate(output, lesson, { includeCheck: true, json });
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
function runYoutubeNotesBatch({ urls, engine, save, json } = {}, deps = {}) {
|
|
1487
1913
|
deps = { ...deps, save: save === true || deps.save === true };
|
|
1488
1914
|
const output = deps.output || ((line = '') => console.error(line));
|
|
1489
1915
|
const items = expandNotesTargets(urls || [], deps);
|
|
@@ -1496,7 +1922,30 @@ function runYoutubeNotesBatch({ urls, engine, save } = {}, deps = {}) {
|
|
|
1496
1922
|
output(formatNotesSummary(rows));
|
|
1497
1923
|
}
|
|
1498
1924
|
if (!rows.length) return 2;
|
|
1499
|
-
|
|
1925
|
+
const firstOk = rows.find((row) => row.ok);
|
|
1926
|
+
const asJson = json === true || deps.json === true;
|
|
1927
|
+
if (firstOk && !deps.save) {
|
|
1928
|
+
if (!asJson) printEphemeralNotesLearnerGate(firstOk.url, { workDir: deps.workDir }, output);
|
|
1929
|
+
printYoutubeTeachNext(firstOk.url, { json: asJson }, output);
|
|
1930
|
+
}
|
|
1931
|
+
if (firstOk && deps.save) {
|
|
1932
|
+
if (deps.ensureApply) return 0;
|
|
1933
|
+
const id = firstOk.id || videoIdFromUrl(firstOk.url);
|
|
1934
|
+
const workDir = deps.workDir || path.join(process.env.TMPDIR || '/tmp', 'ytnotes');
|
|
1935
|
+
const lesson = firstOk.lesson || notesLessonFromText(readNotesText({
|
|
1936
|
+
url: firstOk.url,
|
|
1937
|
+
workDir,
|
|
1938
|
+
}));
|
|
1939
|
+
const baseline = proveSavedLearnerBaseline({
|
|
1940
|
+
cwd: deps.cwd || process.cwd(),
|
|
1941
|
+
applyRel: id ? applySidecarRel(id) : null,
|
|
1942
|
+
lesson,
|
|
1943
|
+
output,
|
|
1944
|
+
json: asJson,
|
|
1945
|
+
});
|
|
1946
|
+
if (baseline !== 0) return baseline;
|
|
1947
|
+
}
|
|
1948
|
+
return firstOk ? 0 : 2;
|
|
1500
1949
|
}
|
|
1501
1950
|
|
|
1502
1951
|
function runSingleYoutubeNotes(url, engine, deps = {}) {
|
|
@@ -1504,30 +1953,49 @@ function runSingleYoutubeNotes(url, engine, deps = {}) {
|
|
|
1504
1953
|
try {
|
|
1505
1954
|
result = invokeNotesRunner(url, engine, deps);
|
|
1506
1955
|
} catch {
|
|
1507
|
-
|
|
1956
|
+
result = { status: 1 };
|
|
1508
1957
|
}
|
|
1509
1958
|
const status = readRunnerStatus(result);
|
|
1510
|
-
if (status !== 0
|
|
1959
|
+
if (status !== 0 && !keptPrintedNotes({
|
|
1960
|
+
url,
|
|
1961
|
+
workDir: notesWorkDir(deps),
|
|
1962
|
+
result,
|
|
1963
|
+
})) {
|
|
1964
|
+
return status == null ? 1 : status;
|
|
1965
|
+
}
|
|
1511
1966
|
const output = deps.output || ((line = '') => console.error(line));
|
|
1512
1967
|
if (!deps.save) {
|
|
1513
|
-
const
|
|
1514
|
-
|
|
1515
|
-
|
|
1968
|
+
const json = deps.json === true;
|
|
1969
|
+
printEphemeralNotesLearnerGate(url, { json, workDir: deps.workDir }, output);
|
|
1970
|
+
printYoutubeTeachNext(url, { json }, output);
|
|
1516
1971
|
return 0;
|
|
1517
1972
|
}
|
|
1518
1973
|
const saved = saveRichNotes(url, deps);
|
|
1519
1974
|
if (saved.thin) {
|
|
1520
1975
|
output(TEACH_THIN_REFUSE);
|
|
1976
|
+
printYoutubeTeachNext(url, { json: deps.json }, output);
|
|
1521
1977
|
return 2;
|
|
1522
1978
|
}
|
|
1523
1979
|
const ensureApply = deps.ensureApply || ensureNotesApply;
|
|
1524
|
-
|
|
1525
|
-
|
|
1980
|
+
const cwd = deps.cwd || process.cwd();
|
|
1981
|
+
const applyCode = ensureApply({
|
|
1982
|
+
cwd,
|
|
1526
1983
|
url,
|
|
1527
1984
|
packRel: saved.packRel,
|
|
1528
1985
|
now: deps.now,
|
|
1529
1986
|
output: deps.output,
|
|
1530
1987
|
});
|
|
1988
|
+
if (deps.ensureApply) return applyCode;
|
|
1989
|
+
const id = videoIdFromUrl(url);
|
|
1990
|
+
const baseline = proveSavedLearnerBaseline({
|
|
1991
|
+
cwd,
|
|
1992
|
+
applyRel: id ? applySidecarRel(id) : null,
|
|
1993
|
+
lesson: saved.lesson,
|
|
1994
|
+
output,
|
|
1995
|
+
json: deps.json === true,
|
|
1996
|
+
});
|
|
1997
|
+
if (baseline !== 0) return baseline;
|
|
1998
|
+
return applyCode;
|
|
1531
1999
|
}
|
|
1532
2000
|
|
|
1533
2001
|
function runYoutubeNotes(args = [], deps = {}) {
|
|
@@ -1545,7 +2013,7 @@ function runYoutubeNotes(args = [], deps = {}) {
|
|
|
1545
2013
|
output(YTNOTES_HINT);
|
|
1546
2014
|
return 2;
|
|
1547
2015
|
}
|
|
1548
|
-
const nextDeps = { ...deps, save: parsed.save };
|
|
2016
|
+
const nextDeps = { ...deps, save: parsed.save, json: parsed.json };
|
|
1549
2017
|
if (parsed.urls.length === 1 && !isPlaylistUrl(parsed.urls[0])) {
|
|
1550
2018
|
return runSingleYoutubeNotes(parsed.urls[0], parsed.engine, nextDeps);
|
|
1551
2019
|
}
|
|
@@ -1575,11 +2043,14 @@ function showYoutubeSearchHelp(output = console.log, commandName = 'atris youtub
|
|
|
1575
2043
|
output('');
|
|
1576
2044
|
output('Free local discovery. Uses ytsearch on PATH when present, else the');
|
|
1577
2045
|
output('bundled scripts/det/ytsearch, else yt-dlp ytsearchN with the same print contract.');
|
|
1578
|
-
output('Does not bill credits.
|
|
2046
|
+
output('Does not bill credits. A thin hit prints check: fill this and one next: atris youtube teach <first-url>.');
|
|
2047
|
+
output('A rich hit writes one apply and a failing keep/revert pack (score 0).');
|
|
1579
2048
|
output('');
|
|
1580
2049
|
output(`--paid buys watch permalinks from Atris (${PAID_SEARCH_COST_HINT}).`);
|
|
1581
2050
|
output('Requires login. Same auth path as atris youtube process.');
|
|
1582
|
-
output('
|
|
2051
|
+
output('A rich hit writes one apply and a failing keep/revert pack (score 0).');
|
|
2052
|
+
output('A thin hit prints check: fill this and one next: atris youtube teach <first-url>.');
|
|
2053
|
+
output('Empty or failed paid search prints credits refunded only when the server marks a refund.');
|
|
1583
2054
|
output('');
|
|
1584
2055
|
output('Options:');
|
|
1585
2056
|
output(` --limit <n> Max results (default: ${DEFAULT_SEARCH_LIMIT})`);
|
|
@@ -1709,6 +2180,17 @@ function resolveSearchCachePath(deps = {}) {
|
|
|
1709
2180
|
return path.join(homeDir, '.atris', LOCAL_SEARCH_CACHE_FILE);
|
|
1710
2181
|
}
|
|
1711
2182
|
|
|
2183
|
+
function printYoutubeTeachNext(url, options, output) {
|
|
2184
|
+
if (options && options.json) return;
|
|
2185
|
+
if (!url) return;
|
|
2186
|
+
output(`next: atris youtube teach ${quoteYoutubeUrl(url)}`);
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
function printSearchTeachNext(rows, options, output) {
|
|
2190
|
+
const firstUrl = Array.isArray(rows) && rows[0] && rows[0].url;
|
|
2191
|
+
printYoutubeTeachNext(firstUrl, options, output);
|
|
2192
|
+
}
|
|
2193
|
+
|
|
1712
2194
|
function printSearchRows(rows, options, output) {
|
|
1713
2195
|
if (options.json) {
|
|
1714
2196
|
output(JSON.stringify(rows, null, 2));
|
|
@@ -1813,9 +2295,8 @@ function paidSearchCredits(data) {
|
|
|
1813
2295
|
return { used, remaining, refunded };
|
|
1814
2296
|
}
|
|
1815
2297
|
|
|
1816
|
-
function
|
|
2298
|
+
function creditsRefundedExplicitly(credits) {
|
|
1817
2299
|
if (!credits) return false;
|
|
1818
|
-
if (credits.used === 0) return true;
|
|
1819
2300
|
if (credits.refunded === true) return true;
|
|
1820
2301
|
return typeof credits.refunded === 'number' && credits.refunded > 0;
|
|
1821
2302
|
}
|
|
@@ -1825,7 +2306,7 @@ function formatCreditsLines(credits) {
|
|
|
1825
2306
|
if (credits.used !== undefined || credits.remaining !== undefined) {
|
|
1826
2307
|
lines.push(`Credits: ${credits.used !== undefined ? credits.used : '?'} used, ${credits.remaining !== undefined ? credits.remaining : '?'} remaining`);
|
|
1827
2308
|
}
|
|
1828
|
-
if (
|
|
2309
|
+
if (creditsRefundedExplicitly(credits)) {
|
|
1829
2310
|
lines.push('credits refunded');
|
|
1830
2311
|
}
|
|
1831
2312
|
return lines;
|
|
@@ -1852,7 +2333,7 @@ function youtubeSearchFailureError(result) {
|
|
|
1852
2333
|
? ' YouTube search is unavailable; retry in a few seconds.'
|
|
1853
2334
|
: '';
|
|
1854
2335
|
const credits = paidSearchCredits(result.data);
|
|
1855
|
-
const refundHint = result.status === 502 &&
|
|
2336
|
+
const refundHint = result.status === 502 && creditsRefundedExplicitly(credits)
|
|
1856
2337
|
? ' credits refunded.'
|
|
1857
2338
|
: '';
|
|
1858
2339
|
const lines = [`YouTube search failed (${result.status}): ${resultErrorText(result)}.${hint}${refundHint}`];
|
|
@@ -1881,6 +2362,12 @@ async function requestPaidYoutubeSearch(options, deps = {}) {
|
|
|
1881
2362
|
if (!result.ok && result.status === 401 && !auth.minted) {
|
|
1882
2363
|
const remint = await ensureBilled('youtube', { ...deps, forceMint: true });
|
|
1883
2364
|
if (remint?.ok && remint.token) {
|
|
2365
|
+
if (!options.json) {
|
|
2366
|
+
const print = typeof deps.output === 'function' ? deps.output : () => {};
|
|
2367
|
+
for (const line of formatCreditsLines(paidSearchCredits(result.data))) {
|
|
2368
|
+
print(line);
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
1884
2371
|
auth = remint;
|
|
1885
2372
|
result = await call(auth.token);
|
|
1886
2373
|
}
|
|
@@ -1908,10 +2395,146 @@ async function runPaidYoutubeSearch(options, deps = {}) {
|
|
|
1908
2395
|
const rendered = formatPaidSearchResults(data);
|
|
1909
2396
|
if (!videos.length) {
|
|
1910
2397
|
output(rendered ? `no videos found\n${rendered}` : 'no videos found');
|
|
2398
|
+
printWatchTickNext(output);
|
|
1911
2399
|
return 2;
|
|
1912
2400
|
}
|
|
1913
2401
|
output(rendered);
|
|
1914
|
-
return
|
|
2402
|
+
return gateSearchLearner(videos, options, output, deps);
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
function searchLessonText(rows) {
|
|
2406
|
+
return (Array.isArray(rows) ? rows : [])
|
|
2407
|
+
.map((row) => String(row && row.title ? row.title : '').trim())
|
|
2408
|
+
.filter(Boolean)
|
|
2409
|
+
.join('\n');
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
function searchExperimentSlug(query) {
|
|
2413
|
+
return `search-${applyGate.applySlug(query)}`;
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
function searchExperimentRel(query) {
|
|
2417
|
+
return `atris/experiments/${searchExperimentSlug(query)}`;
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
function searchApplyRel(query) {
|
|
2421
|
+
return applyGate.applySidecarRel('search', applyGate.applySlug(query));
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
function searchApplyNow(now) {
|
|
2425
|
+
if (typeof now === 'function') {
|
|
2426
|
+
const value = now();
|
|
2427
|
+
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}/.test(value)) return value;
|
|
2428
|
+
const ms = Number(value);
|
|
2429
|
+
if (Number.isFinite(ms) && ms > 0) return new Date(ms).toISOString().slice(0, 10);
|
|
2430
|
+
return undefined;
|
|
2431
|
+
}
|
|
2432
|
+
return now;
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
function saveRichSearch({ cwd, query, lesson } = {}) {
|
|
2436
|
+
if (isThinTeachLesson(lesson)) {
|
|
2437
|
+
return { thin: true, packRel: null, lesson };
|
|
2438
|
+
}
|
|
2439
|
+
if (cwd) fs.mkdirSync(path.join(cwd, 'atris', 'wiki'), { recursive: true });
|
|
2440
|
+
const packRel = fileTeachExperiment({
|
|
2441
|
+
cwd,
|
|
2442
|
+
lesson,
|
|
2443
|
+
slug: query ? searchExperimentSlug(query) : null,
|
|
2444
|
+
applyRel: query ? searchApplyRel(query) : null,
|
|
2445
|
+
});
|
|
2446
|
+
return { thin: false, packRel, lesson, source: query };
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
function ensureSearchApply({ cwd, query, packRel, now, output, source } = {}) {
|
|
2450
|
+
const pack = packRel || (query ? searchExperimentRel(query) : null);
|
|
2451
|
+
const slug = pack ? path.basename(pack) : null;
|
|
2452
|
+
if (cwd) fs.mkdirSync(path.join(cwd, 'atris', 'wiki'), { recursive: true });
|
|
2453
|
+
return applyGate.ensureApply({
|
|
2454
|
+
cwd,
|
|
2455
|
+
source: source || query || 'search',
|
|
2456
|
+
rel: query ? searchApplyRel(query) : null,
|
|
2457
|
+
now,
|
|
2458
|
+
output,
|
|
2459
|
+
incompleteMessage: slug
|
|
2460
|
+
? `next: atris experiments keep ${slug}`
|
|
2461
|
+
: applyGate.ephemeralApplyMessage('search'),
|
|
2462
|
+
required: false,
|
|
2463
|
+
change: pack ? `apply ${pack}` : undefined,
|
|
2464
|
+
receipt: pack ? TEACH_KEEP_RULE : undefined,
|
|
2465
|
+
journalLine: pack ? `- [claimable] apply: ${pack}. ${TEACH_KEEP_RULE}` : undefined,
|
|
2466
|
+
});
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
function mintRichSearch({ cwd, query, rows, now, output, ensureApply, json } = {}) {
|
|
2470
|
+
const print = typeof output === 'function' ? output : (line = '') => console.log(line);
|
|
2471
|
+
const lesson = notesLessonFromText(searchLessonText(rows));
|
|
2472
|
+
if (json) return { thin: false, code: 0, lesson };
|
|
2473
|
+
if (isThinTeachLesson(lesson)) {
|
|
2474
|
+
printSearchLearnerGate(rows, { json: false }, print);
|
|
2475
|
+
return { thin: true, code: 0, lesson };
|
|
2476
|
+
}
|
|
2477
|
+
const saved = saveRichSearch({ cwd, query, lesson });
|
|
2478
|
+
const applyFn = ensureApply || ensureSearchApply;
|
|
2479
|
+
const applyCode = applyFn({
|
|
2480
|
+
cwd,
|
|
2481
|
+
query,
|
|
2482
|
+
packRel: saved.packRel,
|
|
2483
|
+
now: searchApplyNow(now),
|
|
2484
|
+
output: print,
|
|
2485
|
+
source: query,
|
|
2486
|
+
});
|
|
2487
|
+
if (ensureApply) return { thin: false, code: applyCode, lesson: saved.lesson };
|
|
2488
|
+
const baseline = proveSavedLearnerBaseline({
|
|
2489
|
+
cwd,
|
|
2490
|
+
applyRel: query ? searchApplyRel(query) : null,
|
|
2491
|
+
lesson: saved.lesson,
|
|
2492
|
+
output: print,
|
|
2493
|
+
json,
|
|
2494
|
+
});
|
|
2495
|
+
if (baseline !== 0) return { thin: false, code: baseline, lesson: saved.lesson };
|
|
2496
|
+
return { thin: false, code: applyCode, lesson: saved.lesson };
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
function printSearchLearnerGate(rows, options, output) {
|
|
2500
|
+
printLearnerCheckGate(output, notesLessonFromText(searchLessonText(rows)), {
|
|
2501
|
+
includeCheck: true,
|
|
2502
|
+
json: Boolean(options && options.json),
|
|
2503
|
+
});
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
function gateSearchLearner(rows, options, output, deps = {}) {
|
|
2507
|
+
if (options && options.json) return 0;
|
|
2508
|
+
const minted = mintRichSearch({
|
|
2509
|
+
cwd: deps.cwd || process.cwd(),
|
|
2510
|
+
query: options && options.query,
|
|
2511
|
+
rows,
|
|
2512
|
+
now: deps.now,
|
|
2513
|
+
output,
|
|
2514
|
+
ensureApply: deps.ensureApply,
|
|
2515
|
+
json: false,
|
|
2516
|
+
});
|
|
2517
|
+
if (minted.thin) printSearchTeachNext(rows, options, output);
|
|
2518
|
+
return minted.code;
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
function printSearchOutcome(rows, options, output, deps = {}) {
|
|
2522
|
+
printSearchRows(rows, options, output);
|
|
2523
|
+
return gateSearchLearner(rows, options, output, deps);
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
function resultStdout(result) {
|
|
2527
|
+
if (typeof result === 'string') return result;
|
|
2528
|
+
return String((result && result.stdout) || '');
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
function searchRowsFromResult(result) {
|
|
2532
|
+
return parseSearchStdout(resultStdout(result));
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
function finishSuccessfulSearch(rows, options, output, deps) {
|
|
2536
|
+
writeLocalSearchCache(options.query, rows, deps);
|
|
2537
|
+
return printSearchOutcome(rows, options, output, deps);
|
|
1915
2538
|
}
|
|
1916
2539
|
|
|
1917
2540
|
function commandOnPath(name, deps = {}) {
|
|
@@ -2030,6 +2653,9 @@ async function runYoutubeSearch(args = [], deps = {}) {
|
|
|
2030
2653
|
return 2;
|
|
2031
2654
|
}
|
|
2032
2655
|
|
|
2656
|
+
let rows = searchRowsFromResult(result);
|
|
2657
|
+
if (rows.length) return finishSuccessfulSearch(rows, options, output, deps);
|
|
2658
|
+
|
|
2033
2659
|
let status = searchRunnerStatus(result);
|
|
2034
2660
|
if (status != null && status !== 0 && isLocalSearchRateLimited(result)) {
|
|
2035
2661
|
await waitLocalSearchBackoff(deps);
|
|
@@ -2043,14 +2669,16 @@ async function runYoutubeSearch(args = [], deps = {}) {
|
|
|
2043
2669
|
output('ytsearch and yt-dlp not found. Install yt-dlp or put ytsearch on PATH.');
|
|
2044
2670
|
return 2;
|
|
2045
2671
|
}
|
|
2672
|
+
rows = searchRowsFromResult(result);
|
|
2673
|
+
if (rows.length) return finishSuccessfulSearch(rows, options, output, deps);
|
|
2046
2674
|
status = searchRunnerStatus(result);
|
|
2047
2675
|
if (status != null && status !== 0 && isLocalSearchRateLimited(result)) {
|
|
2048
2676
|
const cached = readFreshLocalSearchCache(options.query, deps);
|
|
2049
2677
|
if (cached) {
|
|
2050
|
-
const
|
|
2051
|
-
|
|
2678
|
+
const cachedRows = cached.rows.slice(0, options.limit);
|
|
2679
|
+
const code = printSearchOutcome(cachedRows, options, output, deps);
|
|
2052
2680
|
output(LOCAL_SEARCH_CACHE_NOTE);
|
|
2053
|
-
return
|
|
2681
|
+
return code;
|
|
2054
2682
|
}
|
|
2055
2683
|
output(LOCAL_SEARCH_RATE_LIMIT_MESSAGE);
|
|
2056
2684
|
return status == null ? 1 : status;
|
|
@@ -2063,23 +2691,20 @@ async function runYoutubeSearch(args = [], deps = {}) {
|
|
|
2063
2691
|
return status == null ? 1 : status;
|
|
2064
2692
|
}
|
|
2065
2693
|
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
output('no videos found');
|
|
2070
|
-
return 2;
|
|
2071
|
-
}
|
|
2072
|
-
|
|
2073
|
-
writeLocalSearchCache(options.query, rows, deps);
|
|
2074
|
-
printSearchRows(rows, options, output);
|
|
2075
|
-
return 0;
|
|
2694
|
+
output('no videos found');
|
|
2695
|
+
if (!options.json) printWatchTickNext(output);
|
|
2696
|
+
return 2;
|
|
2076
2697
|
}
|
|
2077
2698
|
|
|
2078
|
-
const YTTEACH_USAGE = 'usage: atris youtube teach <youtube-url> [--section N] [--save] [--recap TEXT] [--skip]';
|
|
2699
|
+
const YTTEACH_USAGE = 'usage: atris youtube teach <youtube-url> [--section N] [--save] [--recap TEXT] [--skip] | owed | next';
|
|
2079
2700
|
const TEACH_PAID_REFUSE = 'teach is free local captions. drop --paid.';
|
|
2080
2701
|
const TEACH_THIN_REFUSE = 'thin: no number or named mechanism. no brief.';
|
|
2081
2702
|
const TEACH_OWED_FILE = 'youtube-teach-owed.json';
|
|
2082
2703
|
const TEACH_RECAP_MISSING = '--recap needs the unpaid check';
|
|
2704
|
+
const TEACH_RESUME_START = 'atris youtube teach <url>';
|
|
2705
|
+
const TEACH_RESUME_NEXT = 'next: atris youtube teach recap TEXT or atris youtube teach skip';
|
|
2706
|
+
const TEACH_WATCH_TICK_NEXT = WATCH_TICK_NEXT;
|
|
2707
|
+
const TEACH_CONTINUE_NEXT = 'next: atris youtube teach next';
|
|
2083
2708
|
const TEACH_APPLY_NEXT_MESSAGE = APPLY_NEXT_MESSAGE;
|
|
2084
2709
|
const TEACH_KEEP_RULE = 'keep only if measure.py moves 0→1. scores 1 only when the fixture contains the check tokens.';
|
|
2085
2710
|
const MECHANISM_STOP = new Set([
|
|
@@ -2108,12 +2733,16 @@ function parseTeachArgs(argv = []) {
|
|
|
2108
2733
|
save: false,
|
|
2109
2734
|
json: false,
|
|
2110
2735
|
skip: false,
|
|
2736
|
+
owed: false,
|
|
2737
|
+
resume: false,
|
|
2738
|
+
next: false,
|
|
2111
2739
|
recap: null,
|
|
2112
2740
|
url: null,
|
|
2113
2741
|
section: 1,
|
|
2114
2742
|
};
|
|
2743
|
+
let sectionSet = false;
|
|
2115
2744
|
|
|
2116
|
-
if (args.length
|
|
2745
|
+
if (args.length > 0 && ['help', '--help', '-h'].includes(args[0])) {
|
|
2117
2746
|
options.help = true;
|
|
2118
2747
|
return options;
|
|
2119
2748
|
}
|
|
@@ -2128,6 +2757,8 @@ function parseTeachArgs(argv = []) {
|
|
|
2128
2757
|
options.json = true;
|
|
2129
2758
|
} else if (arg === '--skip') {
|
|
2130
2759
|
options.skip = true;
|
|
2760
|
+
} else if (arg === '--owed') {
|
|
2761
|
+
options.owed = true;
|
|
2131
2762
|
} else if (arg === '--paid') {
|
|
2132
2763
|
throw new Error(TEACH_PAID_REFUSE);
|
|
2133
2764
|
} else if (arg === '--recap') {
|
|
@@ -2148,6 +2779,7 @@ function parseTeachArgs(argv = []) {
|
|
|
2148
2779
|
throw new Error('--section must be a positive integer');
|
|
2149
2780
|
}
|
|
2150
2781
|
options.section = value;
|
|
2782
|
+
sectionSet = true;
|
|
2151
2783
|
i += 1;
|
|
2152
2784
|
} else if (arg.startsWith('--section=')) {
|
|
2153
2785
|
const value = Number.parseInt(arg.slice('--section='.length), 10);
|
|
@@ -2155,6 +2787,7 @@ function parseTeachArgs(argv = []) {
|
|
|
2155
2787
|
throw new Error('--section must be a positive integer');
|
|
2156
2788
|
}
|
|
2157
2789
|
options.section = value;
|
|
2790
|
+
sectionSet = true;
|
|
2158
2791
|
} else if (arg.startsWith('-')) {
|
|
2159
2792
|
throw new Error(`Unknown option: ${arg}`);
|
|
2160
2793
|
} else if (arg === 'recap' && options.recap == null && !options.url) {
|
|
@@ -2169,6 +2802,10 @@ function parseTeachArgs(argv = []) {
|
|
|
2169
2802
|
if (!options.recap) throw new Error(TEACH_RECAP_MISSING);
|
|
2170
2803
|
} else if (arg === 'skip' && !options.url) {
|
|
2171
2804
|
options.skip = true;
|
|
2805
|
+
} else if (arg === 'owed' && !options.url) {
|
|
2806
|
+
options.owed = true;
|
|
2807
|
+
} else if (arg === 'next' && !options.url) {
|
|
2808
|
+
options.next = true;
|
|
2172
2809
|
} else if (!options.url && looksLikeYoutubeUrl(arg)) {
|
|
2173
2810
|
options.url = arg;
|
|
2174
2811
|
} else {
|
|
@@ -2177,7 +2814,12 @@ function parseTeachArgs(argv = []) {
|
|
|
2177
2814
|
}
|
|
2178
2815
|
|
|
2179
2816
|
if (options.help) return options;
|
|
2180
|
-
if (!options.url && options.recap == null && !options.skip) {
|
|
2817
|
+
if (!options.url && options.recap == null && !options.skip && !options.owed && !options.next) {
|
|
2818
|
+
if (!sectionSet && !options.save) {
|
|
2819
|
+
options.owed = true;
|
|
2820
|
+
options.resume = true;
|
|
2821
|
+
return options;
|
|
2822
|
+
}
|
|
2181
2823
|
throw new Error('Missing YouTube URL. Run "atris youtube teach --help".');
|
|
2182
2824
|
}
|
|
2183
2825
|
return options;
|
|
@@ -2297,7 +2939,13 @@ function extractTeachMechanisms(text) {
|
|
|
2297
2939
|
return found.slice(0, 8);
|
|
2298
2940
|
}
|
|
2299
2941
|
|
|
2300
|
-
|
|
2942
|
+
const LEARNER_CHECK_FILL = 'fill this';
|
|
2943
|
+
const LEARNER_SCORE_ZERO = 'score: 0';
|
|
2944
|
+
const LEARNER_CHECK_INCOMPLETE = 'incomplete: no measurable check. check: fill this';
|
|
2945
|
+
const LEARNER_BASELINE_INCOMPLETE = 'incomplete: check already passes. refuse invented success.';
|
|
2946
|
+
const LEARNER_APPLY_MISSING = 'incomplete: apply missing. check cannot score.';
|
|
2947
|
+
|
|
2948
|
+
function oneTeachCheck(mechanisms, numbers) {
|
|
2301
2949
|
if (mechanisms[0]) {
|
|
2302
2950
|
const name = mechanisms[0];
|
|
2303
2951
|
const named = new RegExp(`\\b(?:${MECHANISM_HEADS})\\b`, 'i').test(name);
|
|
@@ -2305,7 +2953,63 @@ function oneTeachCheck(mechanisms, numbers, title) {
|
|
|
2305
2953
|
return `what is ${name}?`;
|
|
2306
2954
|
}
|
|
2307
2955
|
if (numbers[0]) return `what does ${numbers[0]} measure in this chapter?`;
|
|
2308
|
-
return
|
|
2956
|
+
return LEARNER_CHECK_FILL;
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
function learnerCheckFromLesson(lesson = {}) {
|
|
2960
|
+
const mechanisms = Array.isArray(lesson.mechanisms) ? lesson.mechanisms : [];
|
|
2961
|
+
const numbers = Array.isArray(lesson.numbers) ? lesson.numbers : [];
|
|
2962
|
+
const inferred = mechanisms.length > 0 || numbers.length > 0;
|
|
2963
|
+
return {
|
|
2964
|
+
inferred,
|
|
2965
|
+
line: inferred ? oneTeachCheck(mechanisms, numbers) : LEARNER_CHECK_FILL,
|
|
2966
|
+
needles: teachCheckNeedles({ mechanisms, numbers }),
|
|
2967
|
+
};
|
|
2968
|
+
}
|
|
2969
|
+
|
|
2970
|
+
function scoreLearnerNeedles(text, needles) {
|
|
2971
|
+
const list = Array.isArray(needles) ? needles : [];
|
|
2972
|
+
if (!list.length) return 0;
|
|
2973
|
+
const blob = String(text || '').toLowerCase();
|
|
2974
|
+
return list.every((needle) => blob.includes(String(needle || '').toLowerCase())) ? 1 : 0;
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2977
|
+
function printLearnerCheckGate(output, lesson, { includeCheck = false, json = false } = {}) {
|
|
2978
|
+
if (json || typeof output !== 'function') return;
|
|
2979
|
+
const check = learnerCheckFromLesson(lesson);
|
|
2980
|
+
if (includeCheck) output(`check: ${check.line}`);
|
|
2981
|
+
if (check.inferred) output(LEARNER_SCORE_ZERO);
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
function proveSavedLearnerBaseline({ cwd, applyRel, lesson, output, json } = {}) {
|
|
2985
|
+
const print = typeof output === 'function' ? output : () => {};
|
|
2986
|
+
const check = learnerCheckFromLesson(lesson);
|
|
2987
|
+
if (!check.inferred) {
|
|
2988
|
+
if (!json) print(LEARNER_CHECK_INCOMPLETE);
|
|
2989
|
+
return 2;
|
|
2990
|
+
}
|
|
2991
|
+
if (!cwd || !applyRel) {
|
|
2992
|
+
if (!json) print(LEARNER_APPLY_MISSING);
|
|
2993
|
+
return 2;
|
|
2994
|
+
}
|
|
2995
|
+
const abs = path.join(cwd, applyRel);
|
|
2996
|
+
let text = '';
|
|
2997
|
+
try {
|
|
2998
|
+
if (!fs.existsSync(abs)) {
|
|
2999
|
+
if (!json) print(LEARNER_APPLY_MISSING);
|
|
3000
|
+
return 2;
|
|
3001
|
+
}
|
|
3002
|
+
text = fs.readFileSync(abs, 'utf8');
|
|
3003
|
+
} catch {
|
|
3004
|
+
if (!json) print(LEARNER_APPLY_MISSING);
|
|
3005
|
+
return 2;
|
|
3006
|
+
}
|
|
3007
|
+
if (scoreLearnerNeedles(text, check.needles) !== 0) {
|
|
3008
|
+
if (!json) print(LEARNER_BASELINE_INCOMPLETE);
|
|
3009
|
+
return 2;
|
|
3010
|
+
}
|
|
3011
|
+
if (!json) print(LEARNER_SCORE_ZERO);
|
|
3012
|
+
return 0;
|
|
2309
3013
|
}
|
|
2310
3014
|
|
|
2311
3015
|
function quoteYoutubeUrl(url) {
|
|
@@ -2336,10 +3040,7 @@ function teachLessonFromCues({ url, section, chapters, chapter, cues, title } =
|
|
|
2336
3040
|
lines.push(oneTeachCheck(mechanisms, numbers, heading));
|
|
2337
3041
|
if (section < total) {
|
|
2338
3042
|
lines.push('');
|
|
2339
|
-
lines.push(
|
|
2340
|
-
} else {
|
|
2341
|
-
lines.push('');
|
|
2342
|
-
lines.push('next: last section');
|
|
3043
|
+
lines.push(TEACH_RESUME_NEXT);
|
|
2343
3044
|
}
|
|
2344
3045
|
return { text: lines.join('\n'), numbers, mechanisms };
|
|
2345
3046
|
}
|
|
@@ -2429,32 +3130,136 @@ function writeTeachOwedStore(deps, store) {
|
|
|
2429
3130
|
}
|
|
2430
3131
|
}
|
|
2431
3132
|
|
|
2432
|
-
function rememberTeachOwed(deps, { url, section, lesson } = {}) {
|
|
3133
|
+
function rememberTeachOwed(deps, { url, section, lesson, total } = {}) {
|
|
2433
3134
|
const id = videoIdFromUrl(url);
|
|
2434
3135
|
if (!id) return;
|
|
2435
3136
|
const store = readTeachOwedStore(deps);
|
|
2436
|
-
|
|
3137
|
+
const row = {
|
|
2437
3138
|
section: Number(section) || 1,
|
|
2438
3139
|
check: teachCheckLine(lesson),
|
|
2439
3140
|
tokens: teachRecapTokens(lesson),
|
|
3141
|
+
url: url || undefined,
|
|
2440
3142
|
};
|
|
3143
|
+
const totalChapters = Number(total);
|
|
3144
|
+
if (Number.isInteger(totalChapters) && totalChapters > 0) row.total = totalChapters;
|
|
3145
|
+
store[id] = row;
|
|
2441
3146
|
writeTeachOwedStore(deps, store);
|
|
2442
3147
|
}
|
|
2443
3148
|
|
|
3149
|
+
function writeTeachContinue(store, id, entry) {
|
|
3150
|
+
const nextSection = (Number(entry && entry.section) || 1) + 1;
|
|
3151
|
+
const total = Number(entry && entry.total);
|
|
3152
|
+
if (Number.isInteger(total) && total > 0 && nextSection > total) {
|
|
3153
|
+
delete store[id];
|
|
3154
|
+
return null;
|
|
3155
|
+
}
|
|
3156
|
+
const cursor = {
|
|
3157
|
+
section: nextSection,
|
|
3158
|
+
url: (entry && entry.url) || undefined,
|
|
3159
|
+
};
|
|
3160
|
+
if (Number.isInteger(total) && total > 0) cursor.total = total;
|
|
3161
|
+
store[id] = cursor;
|
|
3162
|
+
return cursor;
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
function lastTeachContinueEntry(store) {
|
|
3166
|
+
const ids = Object.keys(store || {});
|
|
3167
|
+
for (let i = ids.length - 1; i >= 0; i -= 1) {
|
|
3168
|
+
const entry = store[ids[i]];
|
|
3169
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
3170
|
+
if (String(entry.check || '').trim()) continue;
|
|
3171
|
+
const url = String(entry.url || '').trim();
|
|
3172
|
+
if (!url) continue;
|
|
3173
|
+
return {
|
|
3174
|
+
id: ids[i],
|
|
3175
|
+
url,
|
|
3176
|
+
section: Number(entry.section) || 1,
|
|
3177
|
+
};
|
|
3178
|
+
}
|
|
3179
|
+
return null;
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
function lastTeachUnpaidEntry(store) {
|
|
3183
|
+
const ids = Object.keys(store || {});
|
|
3184
|
+
for (let i = ids.length - 1; i >= 0; i -= 1) {
|
|
3185
|
+
const entry = store[ids[i]];
|
|
3186
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
3187
|
+
const check = String(entry.check || '').trim();
|
|
3188
|
+
if (!check) continue;
|
|
3189
|
+
return {
|
|
3190
|
+
id: ids[i],
|
|
3191
|
+
check,
|
|
3192
|
+
url: entry.url || null,
|
|
3193
|
+
section: Number(entry.section) || 1,
|
|
3194
|
+
};
|
|
3195
|
+
}
|
|
3196
|
+
return null;
|
|
3197
|
+
}
|
|
3198
|
+
|
|
2444
3199
|
function previousTeachUnlocked(owed, section) {
|
|
2445
3200
|
if (!owed) return true;
|
|
2446
3201
|
return Number(owed.section) >= Number(section);
|
|
2447
3202
|
}
|
|
2448
3203
|
|
|
3204
|
+
function printTeachUnlockNext(parsed, entry, output) {
|
|
3205
|
+
if (!parsed || parsed.json) return;
|
|
3206
|
+
const url = parsed.url || (entry && entry.url) || '';
|
|
3207
|
+
if (!url) return;
|
|
3208
|
+
output(TEACH_CONTINUE_NEXT);
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3211
|
+
function printTeachWatchTickNext(parsed, lesson, total, output) {
|
|
3212
|
+
if (!parsed || parsed.json) return;
|
|
3213
|
+
if (parsed.save && !isThinTeachLesson(lesson)) return;
|
|
3214
|
+
const section = Number(parsed.section) || 0;
|
|
3215
|
+
if (section < Number(total)) return;
|
|
3216
|
+
printWatchTickNext(output);
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
function listTeachOwedEntries(store) {
|
|
3220
|
+
return Object.keys(store || {}).map((id) => {
|
|
3221
|
+
const entry = store[id] && typeof store[id] === 'object' ? store[id] : {};
|
|
3222
|
+
return {
|
|
3223
|
+
id,
|
|
3224
|
+
url: entry.url || null,
|
|
3225
|
+
section: Number(entry.section) || 1,
|
|
3226
|
+
check: String(entry.check || '').trim(),
|
|
3227
|
+
};
|
|
3228
|
+
}).filter((row) => row.check);
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3231
|
+
function printTeachOwed(parsed, deps, output) {
|
|
3232
|
+
const rows = listTeachOwedEntries(readTeachOwedStore(deps));
|
|
3233
|
+
if (parsed.json) {
|
|
3234
|
+
output(JSON.stringify(rows));
|
|
3235
|
+
return 0;
|
|
3236
|
+
}
|
|
3237
|
+
if (!rows.length) {
|
|
3238
|
+
output('nothing owed');
|
|
3239
|
+
if (parsed.resume) output(TEACH_RESUME_START);
|
|
3240
|
+
return 0;
|
|
3241
|
+
}
|
|
3242
|
+
rows.forEach((row, index) => {
|
|
3243
|
+
if (index) output('');
|
|
3244
|
+
output(row.url || row.id);
|
|
3245
|
+
output(`section ${row.section}`);
|
|
3246
|
+
if (row.check) output(row.check);
|
|
3247
|
+
});
|
|
3248
|
+
if (parsed.resume) output(TEACH_RESUME_NEXT);
|
|
3249
|
+
return 0;
|
|
3250
|
+
}
|
|
3251
|
+
|
|
2449
3252
|
function applyTeachRecap(parsed, deps, output) {
|
|
2450
3253
|
const store = readTeachOwedStore(deps);
|
|
2451
3254
|
let id = parsed.url ? videoIdFromUrl(parsed.url) : null;
|
|
2452
3255
|
let entry = id ? store[id] : null;
|
|
2453
3256
|
if (!entry && !id) {
|
|
2454
3257
|
const ids = Object.keys(store);
|
|
3258
|
+
const unpaidIds = ids.filter((key) => store[key] && String(store[key].check || '').trim());
|
|
2455
3259
|
if (parsed.recap) {
|
|
2456
|
-
id =
|
|
3260
|
+
id = unpaidIds.find((key) => recapHitsTokens(parsed.recap, store[key] && store[key].tokens));
|
|
2457
3261
|
}
|
|
3262
|
+
if (!id && unpaidIds.length === 1) id = unpaidIds[0];
|
|
2458
3263
|
if (!id && ids.length === 1) id = ids[0];
|
|
2459
3264
|
entry = id ? store[id] : null;
|
|
2460
3265
|
}
|
|
@@ -2463,8 +3268,13 @@ function applyTeachRecap(parsed, deps, output) {
|
|
|
2463
3268
|
if (!parsed.json) output('recap the previous section first');
|
|
2464
3269
|
return 2;
|
|
2465
3270
|
}
|
|
3271
|
+
if (!String(entry.check || '').trim()) {
|
|
3272
|
+
printTeachUnlockNext(parsed, entry, output);
|
|
3273
|
+
return 0;
|
|
3274
|
+
}
|
|
2466
3275
|
if (parsed.skip) {
|
|
2467
|
-
|
|
3276
|
+
printTeachUnlockNext(parsed, entry, output);
|
|
3277
|
+
writeTeachContinue(store, id, entry);
|
|
2468
3278
|
writeTeachOwedStore(deps, store);
|
|
2469
3279
|
return 0;
|
|
2470
3280
|
}
|
|
@@ -2472,11 +3282,40 @@ function applyTeachRecap(parsed, deps, output) {
|
|
|
2472
3282
|
if (!parsed.json) output(entry.check);
|
|
2473
3283
|
return 2;
|
|
2474
3284
|
}
|
|
2475
|
-
|
|
3285
|
+
printTeachUnlockNext(parsed, entry, output);
|
|
3286
|
+
writeTeachContinue(store, id, entry);
|
|
2476
3287
|
writeTeachOwedStore(deps, store);
|
|
2477
3288
|
return 0;
|
|
2478
3289
|
}
|
|
2479
3290
|
|
|
3291
|
+
function applyTeachNext(parsed, deps, output) {
|
|
3292
|
+
const store = readTeachOwedStore(deps);
|
|
3293
|
+
const cont = lastTeachContinueEntry(store);
|
|
3294
|
+
if (cont) {
|
|
3295
|
+
parsed.url = cont.url;
|
|
3296
|
+
parsed.section = cont.section;
|
|
3297
|
+
return 0;
|
|
3298
|
+
}
|
|
3299
|
+
const unpaid = lastTeachUnpaidEntry(store);
|
|
3300
|
+
if (unpaid) {
|
|
3301
|
+
if (!parsed.json) output(unpaid.check);
|
|
3302
|
+
return 2;
|
|
3303
|
+
}
|
|
3304
|
+
if (parsed.json) {
|
|
3305
|
+
output('[]');
|
|
3306
|
+
return 0;
|
|
3307
|
+
}
|
|
3308
|
+
output('nothing owed');
|
|
3309
|
+
output(TEACH_RESUME_START);
|
|
3310
|
+
return 0;
|
|
3311
|
+
}
|
|
3312
|
+
|
|
3313
|
+
function applyTeachResume(parsed, deps, output) {
|
|
3314
|
+
const store = readTeachOwedStore(deps);
|
|
3315
|
+
if (lastTeachUnpaidEntry(store)) return printTeachOwed(parsed, deps, output);
|
|
3316
|
+
return applyTeachNext(parsed, deps, output);
|
|
3317
|
+
}
|
|
3318
|
+
|
|
2480
3319
|
function teachCheckLine(lesson = {}) {
|
|
2481
3320
|
return oneTeachCheck(lesson.mechanisms || [], lesson.numbers || [], '');
|
|
2482
3321
|
}
|
|
@@ -2765,14 +3604,15 @@ function fileTeachBrief({ cwd, url, section, lesson, now } = {}) {
|
|
|
2765
3604
|
function ensureTeachApply({ cwd, url, section, packRel, now, output } = {}) {
|
|
2766
3605
|
const id = videoIdFromUrl(url);
|
|
2767
3606
|
const pack = packRel || (id ? teachExperimentRel(id, section) : null);
|
|
3607
|
+
const slug = pack ? path.basename(pack) : null;
|
|
2768
3608
|
return applyGate.ensureApply({
|
|
2769
3609
|
cwd,
|
|
2770
3610
|
source: url,
|
|
2771
3611
|
rel: id ? applySidecarRel(`${id}-s${section}`) : null,
|
|
2772
3612
|
now,
|
|
2773
3613
|
output,
|
|
2774
|
-
incompleteMessage:
|
|
2775
|
-
? `next:
|
|
3614
|
+
incompleteMessage: slug
|
|
3615
|
+
? `next: atris experiments keep ${slug}`
|
|
2776
3616
|
: TEACH_APPLY_NEXT_MESSAGE,
|
|
2777
3617
|
required: false,
|
|
2778
3618
|
change: pack ? `apply ${pack}` : undefined,
|
|
@@ -2786,33 +3626,23 @@ async function extractTeachSource(youtubeUrl, deps = {}) {
|
|
|
2786
3626
|
return deps.extractTeachSource(youtubeUrl, deps);
|
|
2787
3627
|
}
|
|
2788
3628
|
const runner = deps.spawnSync || spawnSync;
|
|
2789
|
-
const result = runner('yt-dlp',
|
|
3629
|
+
const result = runner('yt-dlp', ytDlpInfoArgs(youtubeUrl), {
|
|
2790
3630
|
encoding: 'utf8',
|
|
2791
3631
|
timeout: 20000,
|
|
2792
3632
|
maxBuffer: 10 * 1024 * 1024,
|
|
2793
3633
|
});
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
try {
|
|
2798
|
-
info = JSON.parse(result.stdout);
|
|
2799
|
-
} catch {
|
|
2800
|
-
return null;
|
|
2801
|
-
}
|
|
2802
|
-
|
|
2803
|
-
const selected = chooseCaptionTrack(info);
|
|
2804
|
-
if (!selected?.track?.url) return null;
|
|
2805
|
-
const rawCaption = await (deps.fetchCaptionText || fetchCaptionText)(selected.track.url);
|
|
2806
|
-
const cues = parseCaptionCues(rawCaption);
|
|
3634
|
+
const info = parseYtDlpInfoJson(result);
|
|
3635
|
+
const loaded = await loadCaptionRaw(info, youtubeUrl, deps);
|
|
3636
|
+
const cues = parseCaptionCues(loaded && loaded.raw);
|
|
2807
3637
|
if (!cues.length) return null;
|
|
2808
3638
|
|
|
2809
3639
|
return {
|
|
2810
|
-
id: info
|
|
2811
|
-
title: info.title || '',
|
|
3640
|
+
id: teachSourceVideoId(info, youtubeUrl),
|
|
3641
|
+
title: (info && info.title) || '',
|
|
2812
3642
|
url: youtubeUrl,
|
|
2813
|
-
durationSeconds: Number(info.duration || 0) || undefined,
|
|
2814
|
-
language:
|
|
2815
|
-
chapters: normalizeChapters(info.chapters, info.duration),
|
|
3643
|
+
durationSeconds: Number((info && info.duration) || 0) || undefined,
|
|
3644
|
+
language: loaded.language || 'unknown',
|
|
3645
|
+
chapters: normalizeChapters(info && info.chapters, info && info.duration),
|
|
2816
3646
|
cues,
|
|
2817
3647
|
};
|
|
2818
3648
|
}
|
|
@@ -2832,11 +3662,22 @@ async function runYoutubeTeach(args = [], deps = {}) {
|
|
|
2832
3662
|
}
|
|
2833
3663
|
|
|
2834
3664
|
const owedDeps = { ...deps, cwd: deps.cwd || process.cwd() };
|
|
3665
|
+
if (parsed.owed) {
|
|
3666
|
+
if (!parsed.resume) return printTeachOwed(parsed, owedDeps, output);
|
|
3667
|
+
const resumeCode = applyTeachResume(parsed, owedDeps, output);
|
|
3668
|
+
if (resumeCode !== 0) return resumeCode;
|
|
3669
|
+
if (!parsed.url) return 0;
|
|
3670
|
+
}
|
|
2835
3671
|
if (parsed.recap != null || parsed.skip) {
|
|
2836
3672
|
const recapCode = applyTeachRecap(parsed, owedDeps, output);
|
|
2837
3673
|
if (recapCode !== 0) return recapCode;
|
|
2838
3674
|
if (!parsed.url || parsed.section <= 1) return 0;
|
|
2839
3675
|
}
|
|
3676
|
+
if (parsed.next) {
|
|
3677
|
+
const nextCode = applyTeachNext(parsed, owedDeps, output);
|
|
3678
|
+
if (nextCode !== 0) return nextCode;
|
|
3679
|
+
if (!parsed.url) return 0;
|
|
3680
|
+
}
|
|
2840
3681
|
|
|
2841
3682
|
if (!parsed.url) {
|
|
2842
3683
|
output('Missing YouTube URL. Run "atris youtube teach --help".');
|
|
@@ -2851,7 +3692,10 @@ async function runYoutubeTeach(args = [], deps = {}) {
|
|
|
2851
3692
|
}
|
|
2852
3693
|
}
|
|
2853
3694
|
|
|
2854
|
-
const source = await (deps.extractTeachSource || extractTeachSource)(parsed.url,
|
|
3695
|
+
const source = await (deps.extractTeachSource || extractTeachSource)(parsed.url, {
|
|
3696
|
+
...deps,
|
|
3697
|
+
workDir: deps.workDir || notesWorkDir(deps),
|
|
3698
|
+
});
|
|
2855
3699
|
if (!source || !Array.isArray(source.cues) || !source.cues.length) {
|
|
2856
3700
|
output('no english captions for this url. teach stays local and will not call process.');
|
|
2857
3701
|
return 2;
|
|
@@ -2874,39 +3718,59 @@ async function runYoutubeTeach(args = [], deps = {}) {
|
|
|
2874
3718
|
title: source.title,
|
|
2875
3719
|
});
|
|
2876
3720
|
output(lesson.text);
|
|
2877
|
-
rememberTeachOwed(owedDeps, {
|
|
3721
|
+
rememberTeachOwed(owedDeps, {
|
|
3722
|
+
url: parsed.url,
|
|
3723
|
+
section: parsed.section,
|
|
3724
|
+
lesson,
|
|
3725
|
+
total: chapters.length,
|
|
3726
|
+
});
|
|
2878
3727
|
|
|
2879
3728
|
if (!parsed.save) {
|
|
2880
3729
|
if (!isThinTeachLesson(lesson)) applyGate.hintEphemeralApply(output, 'teach');
|
|
3730
|
+
printLearnerCheckGate(output, lesson, { includeCheck: true, json: parsed.json === true });
|
|
3731
|
+
printTeachWatchTickNext(parsed, lesson, chapters.length, output);
|
|
2881
3732
|
return 0;
|
|
2882
3733
|
}
|
|
2883
3734
|
if (isThinTeachLesson(lesson)) {
|
|
2884
3735
|
output(TEACH_THIN_REFUSE);
|
|
3736
|
+
printTeachWatchTickNext(parsed, lesson, chapters.length, output);
|
|
2885
3737
|
return 2;
|
|
2886
3738
|
}
|
|
2887
3739
|
|
|
3740
|
+
const cwd = deps.cwd || process.cwd();
|
|
2888
3741
|
fileTeachBrief({
|
|
2889
|
-
cwd
|
|
3742
|
+
cwd,
|
|
2890
3743
|
url: parsed.url,
|
|
2891
3744
|
section: parsed.section,
|
|
2892
3745
|
lesson: lesson.text,
|
|
2893
3746
|
now: deps.now,
|
|
2894
3747
|
});
|
|
2895
3748
|
const packRel = fileTeachExperiment({
|
|
2896
|
-
cwd
|
|
3749
|
+
cwd,
|
|
2897
3750
|
url: parsed.url,
|
|
2898
3751
|
section: parsed.section,
|
|
2899
3752
|
lesson,
|
|
2900
3753
|
});
|
|
2901
3754
|
const ensureApply = deps.ensureApply || ensureTeachApply;
|
|
2902
|
-
|
|
2903
|
-
cwd
|
|
3755
|
+
const applyCode = ensureApply({
|
|
3756
|
+
cwd,
|
|
2904
3757
|
url: parsed.url,
|
|
2905
3758
|
section: parsed.section,
|
|
2906
3759
|
packRel,
|
|
2907
3760
|
now: deps.now,
|
|
2908
3761
|
output,
|
|
2909
3762
|
});
|
|
3763
|
+
if (deps.ensureApply) return applyCode;
|
|
3764
|
+
const id = videoIdFromUrl(parsed.url);
|
|
3765
|
+
const baseline = proveSavedLearnerBaseline({
|
|
3766
|
+
cwd,
|
|
3767
|
+
applyRel: id ? applySidecarRel(`${id}-s${parsed.section}`) : null,
|
|
3768
|
+
lesson,
|
|
3769
|
+
output,
|
|
3770
|
+
json: parsed.json === true,
|
|
3771
|
+
});
|
|
3772
|
+
if (baseline !== 0) return baseline;
|
|
3773
|
+
return applyCode;
|
|
2910
3774
|
}
|
|
2911
3775
|
|
|
2912
3776
|
async function youtubeCommand(argv = process.argv.slice(3), deps = {}) {
|
|
@@ -2951,7 +3815,20 @@ async function youtubeCommand(argv = process.argv.slice(3), deps = {}) {
|
|
|
2951
3815
|
let status = 0;
|
|
2952
3816
|
try {
|
|
2953
3817
|
const data = await processYoutube(options, deps);
|
|
2954
|
-
|
|
3818
|
+
if (options.json) {
|
|
3819
|
+
output(JSON.stringify(data, null, 2));
|
|
3820
|
+
} else {
|
|
3821
|
+
output(formatYoutubeResult(data));
|
|
3822
|
+
const mintCode = mintRichProcess({
|
|
3823
|
+
cwd: deps.cwd || process.cwd(),
|
|
3824
|
+
url: options.youtubeUrl,
|
|
3825
|
+
data,
|
|
3826
|
+
now: deps.now,
|
|
3827
|
+
output,
|
|
3828
|
+
ensureApply: deps.ensureApply,
|
|
3829
|
+
});
|
|
3830
|
+
if (mintCode !== 0) status = mintCode;
|
|
3831
|
+
}
|
|
2955
3832
|
} catch (err) {
|
|
2956
3833
|
if (!err.applyRequired) output(err.message);
|
|
2957
3834
|
status = Number.isInteger(err.exitCode) ? err.exitCode : 1;
|
|
@@ -2968,10 +3845,12 @@ module.exports = {
|
|
|
2968
3845
|
parseYoutubeArgs,
|
|
2969
3846
|
buildYoutubePayload,
|
|
2970
3847
|
extractLocalTranscript,
|
|
3848
|
+
parseYtDlpInfoJson,
|
|
2971
3849
|
processYoutube,
|
|
2972
3850
|
shouldRetryWithLocalTranscript,
|
|
2973
3851
|
formatYoutubeResult,
|
|
2974
3852
|
fileBriefFromNotes,
|
|
3853
|
+
keptPrintedNotes,
|
|
2975
3854
|
ensureNotesApply,
|
|
2976
3855
|
unsaveYoutubeNotes,
|
|
2977
3856
|
APPLY_NEXT_MESSAGE,
|
|
@@ -2999,10 +3878,27 @@ module.exports = {
|
|
|
2999
3878
|
extractTeachNumbers,
|
|
3000
3879
|
extractTeachMechanisms,
|
|
3001
3880
|
extractTeachSource,
|
|
3881
|
+
readLocalCaptionText,
|
|
3882
|
+
oneTeachCheck,
|
|
3883
|
+
learnerCheckFromLesson,
|
|
3884
|
+
scoreLearnerNeedles,
|
|
3885
|
+
LEARNER_CHECK_FILL,
|
|
3886
|
+
LEARNER_SCORE_ZERO,
|
|
3887
|
+
printLearnerCheckGate,
|
|
3888
|
+
proveSavedLearnerBaseline,
|
|
3002
3889
|
isThinTeachLesson,
|
|
3003
3890
|
TEACH_THIN_REFUSE,
|
|
3891
|
+
TEACH_RESUME_NEXT,
|
|
3892
|
+
TEACH_WATCH_TICK_NEXT,
|
|
3004
3893
|
teachExperimentSlug,
|
|
3005
3894
|
notesExperimentSlug,
|
|
3895
|
+
digestExperimentSlug,
|
|
3896
|
+
watchExperimentSlug,
|
|
3897
|
+
processExperimentSlug,
|
|
3898
|
+
processApplyRel,
|
|
3899
|
+
searchExperimentSlug,
|
|
3900
|
+
searchApplyRel,
|
|
3901
|
+
firstRichWatchLesson,
|
|
3006
3902
|
fileTeachExperiment,
|
|
3007
3903
|
youtubeCommand,
|
|
3008
3904
|
};
|