atris 3.58.5 → 3.58.7
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 +8 -0
- package/atris/policies/engineering-principles.md +129 -0
- package/atris/policies/genesis.md +112 -0
- package/atris/policies/product-design-principles.md +100 -0
- package/atris/skills/design/SKILL.md +3 -1
- package/atris/skills/engines/SKILL.md +3 -3
- package/atris/skills/x-search/SKILL.md +2 -2
- package/atris/skills/youtube/SKILL.md +44 -28
- package/bin/atris.js +56 -3
- package/commands/auth.js +58 -24
- package/commands/brain.js +1 -0
- package/commands/design.js +362 -0
- package/commands/doc-health.js +329 -0
- package/commands/drive.js +32 -0
- package/commands/improve.js +67 -1
- package/commands/land.js +144 -4
- package/commands/learn.js +211 -40
- package/commands/member.js +65 -11
- package/commands/mission.js +37 -7
- package/commands/pulse.js +38 -0
- package/commands/rsi.js +156 -0
- package/commands/task.js +41 -1
- package/commands/workflow.js +15 -14
- package/commands/x-search.js +9 -10
- package/commands/youtube.js +518 -107
- package/lib/apply-gate.js +22 -4
- package/lib/daily-log.js +88 -0
- package/lib/design-api.js +130 -0
- package/lib/engine-ask.js +1 -1
- package/lib/first-minute.js +1 -6
- package/lib/known-commands.js +3 -3
- package/lib/member-context.js +42 -0
- package/lib/rsi-record.js +335 -0
- package/lib/state-detection.js +8 -8
- package/lib/task-db.js +71 -51
- package/lib/task-list-keeper.js +192 -0
- package/lib/todo-fallback.js +9 -3
- package/lib/todo.js +22 -10
- package/mcp/atris-mcp/index.mjs +174 -0
- package/package.json +8 -3
- package/scripts/det/ytnotes +122 -10
- package/utils/auth.js +109 -13
package/commands/youtube.js
CHANGED
|
@@ -42,12 +42,12 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
|
|
|
42
42
|
output(` ${commandName} watch tick`);
|
|
43
43
|
output(` ${commandName} <youtube-url> [options]`);
|
|
44
44
|
output('');
|
|
45
|
-
output('search = free local discovery (ytsearch / yt-dlp), returns youtu.be links; rich free search
|
|
46
|
-
output('search --paid = 5 credits, watch permalinks + titles from Atris; rich paid search
|
|
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
47
|
output('notes = free local notes to stdout; ephemeral unless --save; hands off to teach');
|
|
48
48
|
output('teach = one chapter from local captions; bare teach resumes unpaid checks, then the next chapter after recap or skip');
|
|
49
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
|
|
50
|
+
output('process = 5 credits cloud knowledge (needs a filled Apply); rich process writes one apply and a failing keep/revert pack');
|
|
51
51
|
output('digest = one decision page from this week\'s video briefs; rich digest writes one apply and a failing keep/revert pack');
|
|
52
52
|
output('watch = subscribed channels turn into briefs without a human; add hands off to tick; tick hands off to teach when it briefed');
|
|
53
53
|
output('Process a YouTube video through Atris using timestamped transcript-first analysis.');
|
|
@@ -211,8 +211,16 @@ function youtubeFailureError(result) {
|
|
|
211
211
|
? ' Run "atris login --force".'
|
|
212
212
|
: result.status === 402
|
|
213
213
|
? ' Check Atris credits.'
|
|
214
|
-
:
|
|
215
|
-
|
|
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'));
|
|
216
224
|
}
|
|
217
225
|
|
|
218
226
|
function captionHostAllowed(urlString) {
|
|
@@ -275,6 +283,51 @@ function parseVttTimestampMs(value) {
|
|
|
275
283
|
return ((hours * 3600) + (minutes * 60) + seconds) * 1000 + millis;
|
|
276
284
|
}
|
|
277
285
|
|
|
286
|
+
function parseCleanTimestampMs(value) {
|
|
287
|
+
const match = String(value || '').trim().match(/^(?:(\d{1,2}):)?(\d{1,2}):(\d{2})$/);
|
|
288
|
+
if (!match) return null;
|
|
289
|
+
const hours = Number(match[1] || 0);
|
|
290
|
+
const minutes = Number(match[2] || 0);
|
|
291
|
+
const seconds = Number(match[3] || 0);
|
|
292
|
+
if (![hours, minutes, seconds].every(Number.isFinite)) return null;
|
|
293
|
+
if (minutes > 59 || seconds > 59) return null;
|
|
294
|
+
return ((hours * 3600) + (minutes * 60) + seconds) * 1000;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function parseCleanTranscriptCues(raw) {
|
|
298
|
+
const cues = [];
|
|
299
|
+
let startMs = 0;
|
|
300
|
+
let sawStamp = false;
|
|
301
|
+
const pending = [];
|
|
302
|
+
const flush = () => {
|
|
303
|
+
const text = pending.join(' ').replace(/\s+/g, ' ').trim();
|
|
304
|
+
pending.length = 0;
|
|
305
|
+
if (!text) return;
|
|
306
|
+
const cue = { startMs, text };
|
|
307
|
+
if (cues.length && cues[cues.length - 1].text === cue.text && cues[cues.length - 1].startMs === cue.startMs) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
cues.push(cue);
|
|
311
|
+
};
|
|
312
|
+
for (const line of String(raw).split(/\r?\n/)) {
|
|
313
|
+
const stripped = line.trim();
|
|
314
|
+
if (!stripped) continue;
|
|
315
|
+
const stamp = stripped.match(/^\[(\d{1,2}:\d{2}(?::\d{2})?)\]$/);
|
|
316
|
+
if (stamp) {
|
|
317
|
+
flush();
|
|
318
|
+
const parsed = parseCleanTimestampMs(stamp[1]);
|
|
319
|
+
if (parsed != null) {
|
|
320
|
+
startMs = parsed;
|
|
321
|
+
sawStamp = true;
|
|
322
|
+
}
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
pending.push(stripped.replace(/<[^>]+>/g, ''));
|
|
326
|
+
}
|
|
327
|
+
flush();
|
|
328
|
+
return sawStamp && cues.length ? cues : [];
|
|
329
|
+
}
|
|
330
|
+
|
|
278
331
|
function fetchCaptionText(urlString, redirects = 0) {
|
|
279
332
|
if (!captionHostAllowed(urlString)) {
|
|
280
333
|
return Promise.resolve(null);
|
|
@@ -372,7 +425,7 @@ function parseCaptionCues(raw) {
|
|
|
372
425
|
return cues;
|
|
373
426
|
}
|
|
374
427
|
|
|
375
|
-
return
|
|
428
|
+
return parseCleanTranscriptCues(raw);
|
|
376
429
|
}
|
|
377
430
|
|
|
378
431
|
function parseCaptionText(raw) {
|
|
@@ -406,33 +459,117 @@ function parseCaptionText(raw) {
|
|
|
406
459
|
return segments.join(' ');
|
|
407
460
|
}
|
|
408
461
|
|
|
462
|
+
function parseYtDlpInfoJson(result) {
|
|
463
|
+
const kept = [];
|
|
464
|
+
for (const line of String((result && result.stdout) || '').split(/\r?\n/)) {
|
|
465
|
+
const trimmed = line.trim();
|
|
466
|
+
if (!trimmed) continue;
|
|
467
|
+
if (/^(WARNING|ERROR|INFO)\b/i.test(trimmed)) continue;
|
|
468
|
+
kept.push(trimmed);
|
|
469
|
+
}
|
|
470
|
+
const raw = kept.join('\n').trim();
|
|
471
|
+
if (!raw) return null;
|
|
472
|
+
try {
|
|
473
|
+
const info = JSON.parse(raw);
|
|
474
|
+
return info && typeof info === 'object' && !Array.isArray(info) ? info : null;
|
|
475
|
+
} catch {
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function localCaptionNames(id) {
|
|
481
|
+
// scripts/det/ytnotes keeps these VTT names plus leftover yt_<id>.clean.txt.
|
|
482
|
+
return [
|
|
483
|
+
`yt_${id}.en.vtt`,
|
|
484
|
+
`yt_${id}.en-orig.vtt`,
|
|
485
|
+
`yt_${id}.en-US.vtt`,
|
|
486
|
+
`yt_${id}.en-GB.vtt`,
|
|
487
|
+
`yt_${id}.clean.txt`,
|
|
488
|
+
];
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function captionLookupIds({ url, id } = {}) {
|
|
492
|
+
const ids = [];
|
|
493
|
+
const seen = new Set();
|
|
494
|
+
const add = (value) => {
|
|
495
|
+
const text = String(value || '').trim();
|
|
496
|
+
if (!text || seen.has(text)) return;
|
|
497
|
+
seen.add(text);
|
|
498
|
+
ids.push(text);
|
|
499
|
+
};
|
|
500
|
+
add(id);
|
|
501
|
+
add(videoIdFromUrl(url));
|
|
502
|
+
return ids;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function readLocalCaptionText({ url, id, workDir } = {}) {
|
|
506
|
+
if (!workDir) return '';
|
|
507
|
+
for (const videoId of captionLookupIds({ url, id })) {
|
|
508
|
+
for (const name of localCaptionNames(videoId)) {
|
|
509
|
+
const abs = path.join(workDir, name);
|
|
510
|
+
try {
|
|
511
|
+
if (!fs.existsSync(abs)) continue;
|
|
512
|
+
const text = fs.readFileSync(abs, 'utf8');
|
|
513
|
+
if (String(text).trim()) return text;
|
|
514
|
+
} catch {
|
|
515
|
+
// try the next written caption file
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return '';
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async function loadCaptionRaw(info, youtubeUrl, deps = {}) {
|
|
523
|
+
const payload = info && typeof info === 'object' ? info : {};
|
|
524
|
+
const selected = chooseCaptionTrack(payload);
|
|
525
|
+
let raw = '';
|
|
526
|
+
if (selected?.track?.url) {
|
|
527
|
+
raw = await (deps.fetchCaptionText || fetchCaptionText)(selected.track.url);
|
|
528
|
+
}
|
|
529
|
+
if (String(raw || '').trim()) {
|
|
530
|
+
return { raw, language: selected?.language || 'unknown' };
|
|
531
|
+
}
|
|
532
|
+
const local = readLocalCaptionText({
|
|
533
|
+
url: youtubeUrl,
|
|
534
|
+
id: payload.id,
|
|
535
|
+
workDir: deps.workDir,
|
|
536
|
+
});
|
|
537
|
+
if (!String(local || '').trim()) return null;
|
|
538
|
+
return { raw: local, language: selected?.language || 'en' };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function ytDlpInfoArgs(youtubeUrl) {
|
|
542
|
+
return ['-J', '--skip-download', '--no-warnings', '--no-playlist', youtubeUrl];
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function teachSourceVideoId(info, youtubeUrl) {
|
|
546
|
+
const fromUrl = videoIdFromUrl(youtubeUrl);
|
|
547
|
+
const fromInfo = info && info.id;
|
|
548
|
+
const entries = info && Array.isArray(info.entries) ? info.entries : [];
|
|
549
|
+
const fromEntry = entries[0] && entries[0].id;
|
|
550
|
+
if (info && (info._type === 'playlist' || entries.length)) {
|
|
551
|
+
return fromUrl || fromEntry || fromInfo;
|
|
552
|
+
}
|
|
553
|
+
return fromInfo || fromUrl;
|
|
554
|
+
}
|
|
555
|
+
|
|
409
556
|
async function extractLocalTranscript(youtubeUrl, deps = {}) {
|
|
410
557
|
if (process.env.ATRIS_YOUTUBE_LOCAL_TRANSCRIPT === '0') return null;
|
|
411
558
|
const runner = deps.spawnSync || spawnSync;
|
|
412
|
-
const result = runner('yt-dlp',
|
|
559
|
+
const result = runner('yt-dlp', ytDlpInfoArgs(youtubeUrl), {
|
|
413
560
|
encoding: 'utf8',
|
|
414
561
|
timeout: 20000,
|
|
415
562
|
maxBuffer: 10 * 1024 * 1024,
|
|
416
563
|
});
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
try {
|
|
421
|
-
info = JSON.parse(result.stdout);
|
|
422
|
-
} catch {
|
|
423
|
-
return null;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
const selected = chooseCaptionTrack(info);
|
|
427
|
-
if (!selected?.track?.url) return null;
|
|
428
|
-
const rawCaption = await (deps.fetchCaptionText || fetchCaptionText)(selected.track.url);
|
|
429
|
-
const transcript = parseCaptionText(rawCaption);
|
|
564
|
+
const info = parseYtDlpInfoJson(result);
|
|
565
|
+
const loaded = await loadCaptionRaw(info, youtubeUrl, deps);
|
|
566
|
+
const transcript = parseCaptionText(loaded && loaded.raw);
|
|
430
567
|
if (!transcript) return null;
|
|
431
568
|
|
|
432
569
|
return {
|
|
433
570
|
transcriptText: transcript.slice(0, LOCAL_TRANSCRIPT_MAX_CHARS),
|
|
434
|
-
language:
|
|
435
|
-
durationSeconds: Number(info.duration || 0) || undefined,
|
|
571
|
+
language: loaded.language || 'unknown',
|
|
572
|
+
durationSeconds: Number((info && info.duration) || 0) || undefined,
|
|
436
573
|
};
|
|
437
574
|
}
|
|
438
575
|
|
|
@@ -468,6 +605,12 @@ async function processYoutube(options, deps = {}) {
|
|
|
468
605
|
if (!result.ok && result.status === 401 && !auth.minted) {
|
|
469
606
|
const remint = await ensureBilled('youtube', { ...deps, forceMint: true });
|
|
470
607
|
if (remint?.ok && remint.token) {
|
|
608
|
+
if (!options.json) {
|
|
609
|
+
const print = typeof deps.output === 'function' ? deps.output : () => {};
|
|
610
|
+
for (const line of formatCreditsLines(paidSearchCredits(result.data))) {
|
|
611
|
+
print(line);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
471
614
|
auth = remint;
|
|
472
615
|
result = await apiFn('/agent/process_youtube', {
|
|
473
616
|
method: 'POST',
|
|
@@ -484,7 +627,10 @@ async function processYoutube(options, deps = {}) {
|
|
|
484
627
|
const localExtractor = deps.extractLocalTranscript || extractLocalTranscript;
|
|
485
628
|
let localTranscript = null;
|
|
486
629
|
try {
|
|
487
|
-
localTranscript = await localExtractor(options.youtubeUrl,
|
|
630
|
+
localTranscript = await localExtractor(options.youtubeUrl, {
|
|
631
|
+
...deps,
|
|
632
|
+
workDir: deps.workDir || notesWorkDir(deps),
|
|
633
|
+
});
|
|
488
634
|
} catch {
|
|
489
635
|
localTranscript = null;
|
|
490
636
|
}
|
|
@@ -501,6 +647,12 @@ async function processYoutube(options, deps = {}) {
|
|
|
501
647
|
if (transcriptResult.status === 401 || transcriptResult.status === 402 || transcriptResult.status === 400) {
|
|
502
648
|
throw youtubeFailureError(transcriptResult);
|
|
503
649
|
}
|
|
650
|
+
if (!options.json) {
|
|
651
|
+
const print = typeof deps.output === 'function' ? deps.output : () => {};
|
|
652
|
+
for (const line of formatCreditsLines(paidSearchCredits(transcriptResult.data))) {
|
|
653
|
+
print(line);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
504
656
|
}
|
|
505
657
|
|
|
506
658
|
const result = await requestYoutube(buildYoutubePayload(options));
|
|
@@ -537,11 +689,8 @@ function formatYoutubeResult(data) {
|
|
|
537
689
|
: '';
|
|
538
690
|
lines.push(`Processing: ${method}${source}`);
|
|
539
691
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
const remaining = data.credits_remaining !== undefined ? data.credits_remaining : '?';
|
|
543
|
-
lines.push(`Credits: ${used} used, ${remaining} remaining`);
|
|
544
|
-
}
|
|
692
|
+
const creditLines = formatCreditsLines(paidSearchCredits(data));
|
|
693
|
+
if (creditLines.length) lines.push(...creditLines);
|
|
545
694
|
const analysis = processAnalysisText(data);
|
|
546
695
|
if (analysis) {
|
|
547
696
|
lines.push('');
|
|
@@ -551,11 +700,13 @@ function formatYoutubeResult(data) {
|
|
|
551
700
|
}
|
|
552
701
|
|
|
553
702
|
function videoIdFromUrl(url) {
|
|
554
|
-
const text = String(url || '');
|
|
703
|
+
const text = String(url || '').split('#')[0];
|
|
555
704
|
const watch = text.match(/[?&]v=([^&]+)/);
|
|
556
705
|
if (watch) return watch[1];
|
|
557
706
|
const short = text.match(/youtu\.be\/([^?&/]+)/);
|
|
558
|
-
|
|
707
|
+
if (short) return short[1];
|
|
708
|
+
const pathId = text.match(/\/(?:shorts|embed|live|v|e)\/([^?&/]+)/i);
|
|
709
|
+
return pathId ? pathId[1] : null;
|
|
559
710
|
}
|
|
560
711
|
|
|
561
712
|
function videoIdFromArg(arg) {
|
|
@@ -569,7 +720,7 @@ function videoIdFromArg(arg) {
|
|
|
569
720
|
function looksLikeYoutubeUrl(arg) {
|
|
570
721
|
const text = String(arg || '').trim();
|
|
571
722
|
if (!text || text.startsWith('-')) return false;
|
|
572
|
-
return /youtube\.com|youtu\.be/i.test(text);
|
|
723
|
+
return /youtube\.com|youtu\.be|youtube-nocookie\.com/i.test(text);
|
|
573
724
|
}
|
|
574
725
|
|
|
575
726
|
function isPlaylistUrl(url) {
|
|
@@ -666,6 +817,10 @@ function applySidecarRel(id) {
|
|
|
666
817
|
return applyGate.applySidecarRel('youtube', id);
|
|
667
818
|
}
|
|
668
819
|
|
|
820
|
+
function notesApplyRel(id) {
|
|
821
|
+
return applyGate.applySidecarRel('notes', experimentIdToken(id));
|
|
822
|
+
}
|
|
823
|
+
|
|
669
824
|
function notesExperimentSlug(id) {
|
|
670
825
|
return `notes-${experimentIdToken(id)}`;
|
|
671
826
|
}
|
|
@@ -694,6 +849,14 @@ function readNotesText({ url, workDir } = {}) {
|
|
|
694
849
|
}
|
|
695
850
|
}
|
|
696
851
|
|
|
852
|
+
function notesWorkDir(deps = {}) {
|
|
853
|
+
return deps.workDir || path.join(process.env.TMPDIR || '/tmp', 'ytnotes');
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function keptPrintedNotes({ url, workDir } = {}) {
|
|
857
|
+
return Boolean(String(readNotesText({ url, workDir }) || '').trim());
|
|
858
|
+
}
|
|
859
|
+
|
|
697
860
|
function saveRichNotes(url, deps = {}) {
|
|
698
861
|
const workDir = deps.workDir || path.join(process.env.TMPDIR || '/tmp', 'ytnotes');
|
|
699
862
|
const lesson = notesLessonFromText(readNotesText({ url, workDir }));
|
|
@@ -707,7 +870,7 @@ function saveRichNotes(url, deps = {}) {
|
|
|
707
870
|
url,
|
|
708
871
|
lesson,
|
|
709
872
|
slug: id ? notesExperimentSlug(id) : null,
|
|
710
|
-
applyRel: id ?
|
|
873
|
+
applyRel: id ? notesApplyRel(id) : null,
|
|
711
874
|
});
|
|
712
875
|
return { thin: false, brief, packRel, lesson };
|
|
713
876
|
}
|
|
@@ -719,7 +882,7 @@ function ensureNotesApply({ cwd, url, packRel, now, output } = {}) {
|
|
|
719
882
|
return applyGate.ensureApply({
|
|
720
883
|
cwd,
|
|
721
884
|
source: url,
|
|
722
|
-
rel: id ?
|
|
885
|
+
rel: id ? notesApplyRel(id) : null,
|
|
723
886
|
now,
|
|
724
887
|
output,
|
|
725
888
|
incompleteMessage: slug
|
|
@@ -800,12 +963,15 @@ function unsaveYoutubeNotes(target, deps = {}) {
|
|
|
800
963
|
};
|
|
801
964
|
add(briefRel);
|
|
802
965
|
add(applyRel);
|
|
966
|
+
add(notesApplyRel(id));
|
|
803
967
|
for (const section of sections) {
|
|
804
968
|
add(teachBriefRel(id, section));
|
|
805
969
|
add(applySidecarRel(`${id}-s${section}`));
|
|
806
970
|
}
|
|
807
971
|
for (const rel of listTeachSidecarRels(cwd, id)) add(rel);
|
|
808
972
|
add(notesExperimentRel(id));
|
|
973
|
+
add(processApplyRel(id));
|
|
974
|
+
add(processExperimentRel(id));
|
|
809
975
|
for (const section of sections) add(teachExperimentRel(id, section));
|
|
810
976
|
|
|
811
977
|
const removed = [];
|
|
@@ -850,9 +1016,89 @@ function ensureProcessApply({ cwd, url, now, output } = {}) {
|
|
|
850
1016
|
output,
|
|
851
1017
|
incompleteMessage: PROCESS_APPLY_MESSAGE,
|
|
852
1018
|
required: true,
|
|
1019
|
+
human: true,
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function processExperimentSlug(id) {
|
|
1024
|
+
return `process-${experimentIdToken(id)}`;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function processExperimentRel(id) {
|
|
1028
|
+
return `atris/experiments/${processExperimentSlug(id)}`;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
function processApplyRel(id) {
|
|
1032
|
+
return applyGate.applySidecarRel('process', experimentIdToken(id));
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function saveRichProcess({ cwd, url, lesson } = {}) {
|
|
1036
|
+
if (isThinTeachLesson(lesson)) {
|
|
1037
|
+
return { thin: true, packRel: null, lesson };
|
|
1038
|
+
}
|
|
1039
|
+
if (cwd) fs.mkdirSync(path.join(cwd, 'atris', 'wiki'), { recursive: true });
|
|
1040
|
+
const id = videoIdFromUrl(url);
|
|
1041
|
+
const packRel = fileTeachExperiment({
|
|
1042
|
+
cwd,
|
|
1043
|
+
url,
|
|
1044
|
+
lesson,
|
|
1045
|
+
slug: id ? processExperimentSlug(id) : null,
|
|
1046
|
+
applyRel: id ? processApplyRel(id) : null,
|
|
1047
|
+
});
|
|
1048
|
+
return { thin: false, packRel, lesson, source: url };
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function ensureProcessLearnerApply({ cwd, url, packRel, now, output, source } = {}) {
|
|
1052
|
+
const id = videoIdFromUrl(url);
|
|
1053
|
+
const pack = packRel || (id ? processExperimentRel(id) : null);
|
|
1054
|
+
const slug = pack ? path.basename(pack) : null;
|
|
1055
|
+
if (cwd) fs.mkdirSync(path.join(cwd, 'atris', 'wiki'), { recursive: true });
|
|
1056
|
+
return applyGate.ensureApply({
|
|
1057
|
+
cwd,
|
|
1058
|
+
source: source || url || (id ? `process:${id}` : 'process'),
|
|
1059
|
+
rel: id ? processApplyRel(id) : null,
|
|
1060
|
+
now,
|
|
1061
|
+
output,
|
|
1062
|
+
incompleteMessage: slug
|
|
1063
|
+
? `next: atris experiments keep ${slug}`
|
|
1064
|
+
: applyGate.ephemeralApplyMessage('process'),
|
|
1065
|
+
required: false,
|
|
1066
|
+
change: pack ? `apply ${pack}` : undefined,
|
|
1067
|
+
receipt: pack ? TEACH_KEEP_RULE : undefined,
|
|
1068
|
+
journalLine: pack ? `- [claimable] apply: ${pack}. ${TEACH_KEEP_RULE}` : undefined,
|
|
853
1069
|
});
|
|
854
1070
|
}
|
|
855
1071
|
|
|
1072
|
+
function mintRichProcess({ cwd, url, data, now, output, ensureApply, json } = {}) {
|
|
1073
|
+
const print = typeof output === 'function' ? output : (line = '') => console.log(line);
|
|
1074
|
+
const lesson = notesLessonFromText(processAnalysisText(data));
|
|
1075
|
+
if (isThinTeachLesson(lesson)) {
|
|
1076
|
+
printProcessLearnerGate(data, { json }, print);
|
|
1077
|
+
return 0;
|
|
1078
|
+
}
|
|
1079
|
+
const saved = saveRichProcess({ cwd, url, lesson });
|
|
1080
|
+
const applyFn = ensureApply || ensureProcessLearnerApply;
|
|
1081
|
+
const applyCode = applyFn({
|
|
1082
|
+
cwd,
|
|
1083
|
+
url,
|
|
1084
|
+
packRel: saved.packRel,
|
|
1085
|
+
now,
|
|
1086
|
+
output: print,
|
|
1087
|
+
source: url,
|
|
1088
|
+
});
|
|
1089
|
+
if (ensureApply) return applyCode;
|
|
1090
|
+
const id = videoIdFromUrl(url);
|
|
1091
|
+
const baseline = proveSavedLearnerBaseline({
|
|
1092
|
+
cwd,
|
|
1093
|
+
applyRel: id ? processApplyRel(id) : null,
|
|
1094
|
+
lesson: saved.lesson,
|
|
1095
|
+
output: print,
|
|
1096
|
+
json,
|
|
1097
|
+
});
|
|
1098
|
+
if (baseline !== 0) return baseline;
|
|
1099
|
+
return applyCode;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
856
1102
|
const DIGEST_ENGINE_TIMEOUT_MS = 240000;
|
|
857
1103
|
const DEFAULT_DIGEST_DAYS = 7;
|
|
858
1104
|
|
|
@@ -1082,6 +1328,15 @@ function ensureWatchApply({ cwd, url, packRel, now, output, source } = {}) {
|
|
|
1082
1328
|
});
|
|
1083
1329
|
}
|
|
1084
1330
|
|
|
1331
|
+
function firstRichWatchLesson(urls, workDir) {
|
|
1332
|
+
for (const url of Array.isArray(urls) ? urls : []) {
|
|
1333
|
+
if (!url) continue;
|
|
1334
|
+
const lesson = notesLessonFromText(readNotesText({ url, workDir }));
|
|
1335
|
+
if (!isThinTeachLesson(lesson)) return { url, lesson };
|
|
1336
|
+
}
|
|
1337
|
+
return null;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1085
1340
|
function saveRichWatch({ cwd, url, lesson } = {}) {
|
|
1086
1341
|
if (isThinTeachLesson(lesson)) {
|
|
1087
1342
|
return { thin: true, packRel: null, lesson };
|
|
@@ -1258,15 +1513,22 @@ function channelVideosUrl(channel) {
|
|
|
1258
1513
|
return `${base}/videos`;
|
|
1259
1514
|
}
|
|
1260
1515
|
|
|
1516
|
+
function looksLikeFlatVideoId(id) {
|
|
1517
|
+
const text = String(id || '');
|
|
1518
|
+
if (/^(NA|None)$/i.test(text)) return false;
|
|
1519
|
+
return /^[A-Za-z0-9_-]+$/.test(text);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1261
1522
|
function parseFlatPlaylist(stdout) {
|
|
1262
1523
|
const videos = [];
|
|
1263
1524
|
for (const line of String(stdout || '').split(/\r?\n/)) {
|
|
1264
1525
|
const trimmed = line.trim();
|
|
1265
1526
|
if (!trimmed || !trimmed.includes('|')) continue;
|
|
1527
|
+
if (/^(WARNING|ERROR|INFO)\b/i.test(trimmed)) continue;
|
|
1266
1528
|
const idx = trimmed.indexOf('|');
|
|
1267
1529
|
const id = trimmed.slice(0, idx).trim();
|
|
1268
1530
|
const title = trimmed.slice(idx + 1).trim();
|
|
1269
|
-
if (id
|
|
1531
|
+
if (looksLikeFlatVideoId(id)) videos.push({ id, title });
|
|
1270
1532
|
}
|
|
1271
1533
|
return videos;
|
|
1272
1534
|
}
|
|
@@ -1276,6 +1538,7 @@ function defaultChannelFetcher(videosUrl, deps = {}) {
|
|
|
1276
1538
|
const result = spawn('yt-dlp', [
|
|
1277
1539
|
'--no-update',
|
|
1278
1540
|
'--flat-playlist',
|
|
1541
|
+
'--no-warnings',
|
|
1279
1542
|
'--playlist-end',
|
|
1280
1543
|
'3',
|
|
1281
1544
|
'--print',
|
|
@@ -1286,11 +1549,13 @@ function defaultChannelFetcher(videosUrl, deps = {}) {
|
|
|
1286
1549
|
timeout: 60000,
|
|
1287
1550
|
maxBuffer: 2 * 1024 * 1024,
|
|
1288
1551
|
});
|
|
1552
|
+
const videos = parseFlatPlaylist(result && result.stdout);
|
|
1553
|
+
if (videos.length) return videos;
|
|
1289
1554
|
if (result.error || result.status !== 0) {
|
|
1290
1555
|
const detail = String(result.stderr || result.error?.message || 'fetch failed').trim();
|
|
1291
1556
|
throw new Error(detail || 'fetch failed');
|
|
1292
1557
|
}
|
|
1293
|
-
return
|
|
1558
|
+
return videos;
|
|
1294
1559
|
}
|
|
1295
1560
|
|
|
1296
1561
|
function defaultNotesRunner(url, deps = {}) {
|
|
@@ -1420,6 +1685,7 @@ async function tickWatch(deps = {}) {
|
|
|
1420
1685
|
let totalNew = 0;
|
|
1421
1686
|
let totalBriefed = 0;
|
|
1422
1687
|
let firstBriefedUrl = null;
|
|
1688
|
+
const briefedUrls = [];
|
|
1423
1689
|
|
|
1424
1690
|
for (const row of state.channels) {
|
|
1425
1691
|
const videosUrl = channelVideosUrl(row.channel);
|
|
@@ -1466,6 +1732,7 @@ async function tickWatch(deps = {}) {
|
|
|
1466
1732
|
}
|
|
1467
1733
|
markSeen(state, row.channel, video.id, timestamp);
|
|
1468
1734
|
briefed += 1;
|
|
1735
|
+
briefedUrls.push(url);
|
|
1469
1736
|
if (!firstBriefedUrl) firstBriefedUrl = url;
|
|
1470
1737
|
}
|
|
1471
1738
|
|
|
@@ -1485,34 +1752,35 @@ async function tickWatch(deps = {}) {
|
|
|
1485
1752
|
output(`total: ${totalNew} new, ${totalBriefed} briefed`);
|
|
1486
1753
|
saveWatchState(statePath, state);
|
|
1487
1754
|
if (totalBriefed > 0) {
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
printLearnerCheckGate(output, lesson, { includeCheck: true });
|
|
1492
|
-
printYoutubeTeachNext(firstBriefedUrl, {}, output);
|
|
1493
|
-
return 0;
|
|
1494
|
-
}
|
|
1495
|
-
const saved = saveRichWatch({ cwd, url: firstBriefedUrl, lesson });
|
|
1755
|
+
const rich = firstRichWatchLesson(briefedUrls, workDir);
|
|
1756
|
+
if (rich) {
|
|
1757
|
+
const saved = saveRichWatch({ cwd, url: rich.url, lesson: rich.lesson });
|
|
1496
1758
|
const ensureApply = deps.ensureApply || ensureWatchApply;
|
|
1497
1759
|
const applyCode = ensureApply({
|
|
1498
1760
|
cwd,
|
|
1499
|
-
url:
|
|
1761
|
+
url: rich.url,
|
|
1500
1762
|
packRel: saved.packRel,
|
|
1501
1763
|
now,
|
|
1502
1764
|
output,
|
|
1503
|
-
source:
|
|
1765
|
+
source: rich.url,
|
|
1504
1766
|
});
|
|
1505
1767
|
if (deps.ensureApply) return applyCode;
|
|
1506
|
-
const id = videoIdFromUrl(
|
|
1768
|
+
const id = videoIdFromUrl(rich.url);
|
|
1507
1769
|
const baseline = proveSavedLearnerBaseline({
|
|
1508
1770
|
cwd,
|
|
1509
1771
|
applyRel: id ? watchApplyRel(id) : null,
|
|
1510
|
-
lesson,
|
|
1772
|
+
lesson: rich.lesson,
|
|
1511
1773
|
output,
|
|
1512
1774
|
});
|
|
1513
1775
|
if (baseline !== 0) return baseline;
|
|
1514
1776
|
return applyCode;
|
|
1515
1777
|
}
|
|
1778
|
+
if (firstBriefedUrl) {
|
|
1779
|
+
const lesson = notesLessonFromText(readNotesText({ url: firstBriefedUrl, workDir }));
|
|
1780
|
+
printLearnerCheckGate(output, lesson, { includeCheck: true });
|
|
1781
|
+
printYoutubeTeachNext(firstBriefedUrl, {}, output);
|
|
1782
|
+
return 0;
|
|
1783
|
+
}
|
|
1516
1784
|
printYoutubeTeachNext(firstBriefedUrl, {}, output);
|
|
1517
1785
|
return 0;
|
|
1518
1786
|
}
|
|
@@ -1541,6 +1809,7 @@ function defaultPlaylistExpander(playlistUrl, deps = {}) {
|
|
|
1541
1809
|
const result = spawn('yt-dlp', [
|
|
1542
1810
|
'--no-update',
|
|
1543
1811
|
'--flat-playlist',
|
|
1812
|
+
'--no-warnings',
|
|
1544
1813
|
'--print',
|
|
1545
1814
|
'%(id)s|%(title)s',
|
|
1546
1815
|
playlistUrl,
|
|
@@ -1549,11 +1818,13 @@ function defaultPlaylistExpander(playlistUrl, deps = {}) {
|
|
|
1549
1818
|
timeout: 60000,
|
|
1550
1819
|
maxBuffer: 2 * 1024 * 1024,
|
|
1551
1820
|
});
|
|
1821
|
+
const videos = parseFlatPlaylist(result && result.stdout);
|
|
1822
|
+
if (videos.length) return videos;
|
|
1552
1823
|
if (result.error || (result.status != null && result.status !== 0)) {
|
|
1553
1824
|
const detail = String(result.stderr || result.error?.message || 'playlist expand failed').trim();
|
|
1554
1825
|
throw new Error(detail || 'playlist expand failed');
|
|
1555
1826
|
}
|
|
1556
|
-
return
|
|
1827
|
+
return videos;
|
|
1557
1828
|
}
|
|
1558
1829
|
|
|
1559
1830
|
function defaultNotesItemRunner(url, engine, deps = {}) {
|
|
@@ -1647,15 +1918,20 @@ function runOneNotesItem(item, engine, deps = {}) {
|
|
|
1647
1918
|
}
|
|
1648
1919
|
|
|
1649
1920
|
const started = readNowMs(deps);
|
|
1650
|
-
let
|
|
1921
|
+
let result = { status: 1 };
|
|
1651
1922
|
try {
|
|
1652
|
-
|
|
1653
|
-
} catch {
|
|
1654
|
-
|
|
1923
|
+
result = invokeNotesRunner(item.url, engine, deps);
|
|
1924
|
+
} catch (err) {
|
|
1925
|
+
result = { status: 1, stderr: String((err && err.message) || err || '') };
|
|
1655
1926
|
}
|
|
1927
|
+
const status = readRunnerStatus(result);
|
|
1656
1928
|
let brief = null;
|
|
1657
1929
|
let lesson = null;
|
|
1658
|
-
let ok = status === 0
|
|
1930
|
+
let ok = status === 0 || keptPrintedNotes({
|
|
1931
|
+
url: item.url,
|
|
1932
|
+
workDir: notesWorkDir(deps),
|
|
1933
|
+
result,
|
|
1934
|
+
});
|
|
1659
1935
|
if (ok && deps.save) {
|
|
1660
1936
|
const saved = saveRichNotes(item.url, deps);
|
|
1661
1937
|
if (saved.thin) {
|
|
@@ -1728,7 +2004,7 @@ function runYoutubeNotesBatch({ urls, engine, save, json } = {}, deps = {}) {
|
|
|
1728
2004
|
}));
|
|
1729
2005
|
const baseline = proveSavedLearnerBaseline({
|
|
1730
2006
|
cwd: deps.cwd || process.cwd(),
|
|
1731
|
-
applyRel: id ?
|
|
2007
|
+
applyRel: id ? notesApplyRel(id) : null,
|
|
1732
2008
|
lesson,
|
|
1733
2009
|
output,
|
|
1734
2010
|
json: asJson,
|
|
@@ -1743,10 +2019,16 @@ function runSingleYoutubeNotes(url, engine, deps = {}) {
|
|
|
1743
2019
|
try {
|
|
1744
2020
|
result = invokeNotesRunner(url, engine, deps);
|
|
1745
2021
|
} catch {
|
|
1746
|
-
|
|
2022
|
+
result = { status: 1 };
|
|
1747
2023
|
}
|
|
1748
2024
|
const status = readRunnerStatus(result);
|
|
1749
|
-
if (status !== 0
|
|
2025
|
+
if (status !== 0 && !keptPrintedNotes({
|
|
2026
|
+
url,
|
|
2027
|
+
workDir: notesWorkDir(deps),
|
|
2028
|
+
result,
|
|
2029
|
+
})) {
|
|
2030
|
+
return status == null ? 1 : status;
|
|
2031
|
+
}
|
|
1750
2032
|
const output = deps.output || ((line = '') => console.error(line));
|
|
1751
2033
|
if (!deps.save) {
|
|
1752
2034
|
const json = deps.json === true;
|
|
@@ -1773,7 +2055,7 @@ function runSingleYoutubeNotes(url, engine, deps = {}) {
|
|
|
1773
2055
|
const id = videoIdFromUrl(url);
|
|
1774
2056
|
const baseline = proveSavedLearnerBaseline({
|
|
1775
2057
|
cwd,
|
|
1776
|
-
applyRel: id ?
|
|
2058
|
+
applyRel: id ? notesApplyRel(id) : null,
|
|
1777
2059
|
lesson: saved.lesson,
|
|
1778
2060
|
output,
|
|
1779
2061
|
json: deps.json === true,
|
|
@@ -1827,14 +2109,14 @@ function showYoutubeSearchHelp(output = console.log, commandName = 'atris youtub
|
|
|
1827
2109
|
output('');
|
|
1828
2110
|
output('Free local discovery. Uses ytsearch on PATH when present, else the');
|
|
1829
2111
|
output('bundled scripts/det/ytsearch, else yt-dlp ytsearchN with the same print contract.');
|
|
1830
|
-
output('Does not bill credits. A hit prints one next: atris youtube teach <first-url>.');
|
|
1831
|
-
output('A rich hit
|
|
2112
|
+
output('Does not bill credits. A thin hit prints check: fill this and one next: atris youtube teach <first-url>.');
|
|
2113
|
+
output('A rich hit writes one apply and a failing keep/revert pack (score 0).');
|
|
1832
2114
|
output('');
|
|
1833
2115
|
output(`--paid buys watch permalinks from Atris (${PAID_SEARCH_COST_HINT}).`);
|
|
1834
2116
|
output('Requires login. Same auth path as atris youtube process.');
|
|
1835
|
-
output('A hit
|
|
1836
|
-
output('A
|
|
1837
|
-
output('Empty or failed paid search
|
|
2117
|
+
output('A rich hit writes one apply and a failing keep/revert pack (score 0).');
|
|
2118
|
+
output('A thin hit prints check: fill this and one next: atris youtube teach <first-url>.');
|
|
2119
|
+
output('Empty or failed paid search prints credits refunded only when the server marks a refund.');
|
|
1838
2120
|
output('');
|
|
1839
2121
|
output('Options:');
|
|
1840
2122
|
output(` --limit <n> Max results (default: ${DEFAULT_SEARCH_LIMIT})`);
|
|
@@ -1913,10 +2195,12 @@ function parseSearchStdout(stdout = '') {
|
|
|
1913
2195
|
for (const line of String(stdout || '').split(/\r?\n/)) {
|
|
1914
2196
|
const trimmed = line.trim();
|
|
1915
2197
|
if (!trimmed || !trimmed.includes('|')) continue;
|
|
2198
|
+
if (/^(WARNING|ERROR|INFO)\b/i.test(trimmed)) continue;
|
|
1916
2199
|
const parts = trimmed.split(/\s*\|\s*/).map((part) => part.trim());
|
|
1917
2200
|
if (parts.length < 5) continue;
|
|
1918
2201
|
const url = parts[parts.length - 1];
|
|
1919
2202
|
if (!/^https?:\/\/(?:www\.)?(?:youtube\.com\/|youtu\.be\/)/i.test(url)) continue;
|
|
2203
|
+
if (!looksLikeFlatVideoId(videoIdFromUrl(url))) continue;
|
|
1920
2204
|
const row = {
|
|
1921
2205
|
title: parts[0] || '',
|
|
1922
2206
|
channel: parts[1] || '',
|
|
@@ -2079,9 +2363,8 @@ function paidSearchCredits(data) {
|
|
|
2079
2363
|
return { used, remaining, refunded };
|
|
2080
2364
|
}
|
|
2081
2365
|
|
|
2082
|
-
function
|
|
2366
|
+
function creditsRefundedExplicitly(credits) {
|
|
2083
2367
|
if (!credits) return false;
|
|
2084
|
-
if (credits.used === 0) return true;
|
|
2085
2368
|
if (credits.refunded === true) return true;
|
|
2086
2369
|
return typeof credits.refunded === 'number' && credits.refunded > 0;
|
|
2087
2370
|
}
|
|
@@ -2091,7 +2374,7 @@ function formatCreditsLines(credits) {
|
|
|
2091
2374
|
if (credits.used !== undefined || credits.remaining !== undefined) {
|
|
2092
2375
|
lines.push(`Credits: ${credits.used !== undefined ? credits.used : '?'} used, ${credits.remaining !== undefined ? credits.remaining : '?'} remaining`);
|
|
2093
2376
|
}
|
|
2094
|
-
if (
|
|
2377
|
+
if (creditsRefundedExplicitly(credits)) {
|
|
2095
2378
|
lines.push('credits refunded');
|
|
2096
2379
|
}
|
|
2097
2380
|
return lines;
|
|
@@ -2118,7 +2401,7 @@ function youtubeSearchFailureError(result) {
|
|
|
2118
2401
|
? ' YouTube search is unavailable; retry in a few seconds.'
|
|
2119
2402
|
: '';
|
|
2120
2403
|
const credits = paidSearchCredits(result.data);
|
|
2121
|
-
const refundHint = result.status === 502 &&
|
|
2404
|
+
const refundHint = result.status === 502 && creditsRefundedExplicitly(credits)
|
|
2122
2405
|
? ' credits refunded.'
|
|
2123
2406
|
: '';
|
|
2124
2407
|
const lines = [`YouTube search failed (${result.status}): ${resultErrorText(result)}.${hint}${refundHint}`];
|
|
@@ -2147,6 +2430,12 @@ async function requestPaidYoutubeSearch(options, deps = {}) {
|
|
|
2147
2430
|
if (!result.ok && result.status === 401 && !auth.minted) {
|
|
2148
2431
|
const remint = await ensureBilled('youtube', { ...deps, forceMint: true });
|
|
2149
2432
|
if (remint?.ok && remint.token) {
|
|
2433
|
+
if (!options.json) {
|
|
2434
|
+
const print = typeof deps.output === 'function' ? deps.output : () => {};
|
|
2435
|
+
for (const line of formatCreditsLines(paidSearchCredits(result.data))) {
|
|
2436
|
+
print(line);
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2150
2439
|
auth = remint;
|
|
2151
2440
|
result = await call(auth.token);
|
|
2152
2441
|
}
|
|
@@ -2178,9 +2467,7 @@ async function runPaidYoutubeSearch(options, deps = {}) {
|
|
|
2178
2467
|
return 2;
|
|
2179
2468
|
}
|
|
2180
2469
|
output(rendered);
|
|
2181
|
-
|
|
2182
|
-
printSearchTeachNext(videos, options, output);
|
|
2183
|
-
return 0;
|
|
2470
|
+
return gateSearchLearner(videos, options, output, deps);
|
|
2184
2471
|
}
|
|
2185
2472
|
|
|
2186
2473
|
function searchLessonText(rows) {
|
|
@@ -2190,6 +2477,93 @@ function searchLessonText(rows) {
|
|
|
2190
2477
|
.join('\n');
|
|
2191
2478
|
}
|
|
2192
2479
|
|
|
2480
|
+
function searchExperimentSlug(query) {
|
|
2481
|
+
return `search-${applyGate.applySlug(query)}`;
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
function searchExperimentRel(query) {
|
|
2485
|
+
return `atris/experiments/${searchExperimentSlug(query)}`;
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
function searchApplyRel(query) {
|
|
2489
|
+
return applyGate.applySidecarRel('search', applyGate.applySlug(query));
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
function searchApplyNow(now) {
|
|
2493
|
+
if (typeof now === 'function') {
|
|
2494
|
+
const value = now();
|
|
2495
|
+
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}/.test(value)) return value;
|
|
2496
|
+
const ms = Number(value);
|
|
2497
|
+
if (Number.isFinite(ms) && ms > 0) return new Date(ms).toISOString().slice(0, 10);
|
|
2498
|
+
return undefined;
|
|
2499
|
+
}
|
|
2500
|
+
return now;
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
function saveRichSearch({ cwd, query, lesson } = {}) {
|
|
2504
|
+
if (isThinTeachLesson(lesson)) {
|
|
2505
|
+
return { thin: true, packRel: null, lesson };
|
|
2506
|
+
}
|
|
2507
|
+
if (cwd) fs.mkdirSync(path.join(cwd, 'atris', 'wiki'), { recursive: true });
|
|
2508
|
+
const packRel = fileTeachExperiment({
|
|
2509
|
+
cwd,
|
|
2510
|
+
lesson,
|
|
2511
|
+
slug: query ? searchExperimentSlug(query) : null,
|
|
2512
|
+
applyRel: query ? searchApplyRel(query) : null,
|
|
2513
|
+
});
|
|
2514
|
+
return { thin: false, packRel, lesson, source: query };
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
function ensureSearchApply({ cwd, query, packRel, now, output, source } = {}) {
|
|
2518
|
+
const pack = packRel || (query ? searchExperimentRel(query) : null);
|
|
2519
|
+
const slug = pack ? path.basename(pack) : null;
|
|
2520
|
+
if (cwd) fs.mkdirSync(path.join(cwd, 'atris', 'wiki'), { recursive: true });
|
|
2521
|
+
return applyGate.ensureApply({
|
|
2522
|
+
cwd,
|
|
2523
|
+
source: source || query || 'search',
|
|
2524
|
+
rel: query ? searchApplyRel(query) : null,
|
|
2525
|
+
now,
|
|
2526
|
+
output,
|
|
2527
|
+
incompleteMessage: slug
|
|
2528
|
+
? `next: atris experiments keep ${slug}`
|
|
2529
|
+
: applyGate.ephemeralApplyMessage('search'),
|
|
2530
|
+
required: false,
|
|
2531
|
+
change: pack ? `apply ${pack}` : undefined,
|
|
2532
|
+
receipt: pack ? TEACH_KEEP_RULE : undefined,
|
|
2533
|
+
journalLine: pack ? `- [claimable] apply: ${pack}. ${TEACH_KEEP_RULE}` : undefined,
|
|
2534
|
+
});
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
function mintRichSearch({ cwd, query, rows, now, output, ensureApply, json } = {}) {
|
|
2538
|
+
const print = typeof output === 'function' ? output : (line = '') => console.log(line);
|
|
2539
|
+
const lesson = notesLessonFromText(searchLessonText(rows));
|
|
2540
|
+
if (json) return { thin: false, code: 0, lesson };
|
|
2541
|
+
if (isThinTeachLesson(lesson)) {
|
|
2542
|
+
printSearchLearnerGate(rows, { json: false }, print);
|
|
2543
|
+
return { thin: true, code: 0, lesson };
|
|
2544
|
+
}
|
|
2545
|
+
const saved = saveRichSearch({ cwd, query, lesson });
|
|
2546
|
+
const applyFn = ensureApply || ensureSearchApply;
|
|
2547
|
+
const applyCode = applyFn({
|
|
2548
|
+
cwd,
|
|
2549
|
+
query,
|
|
2550
|
+
packRel: saved.packRel,
|
|
2551
|
+
now: searchApplyNow(now),
|
|
2552
|
+
output: print,
|
|
2553
|
+
source: query,
|
|
2554
|
+
});
|
|
2555
|
+
if (ensureApply) return { thin: false, code: applyCode, lesson: saved.lesson };
|
|
2556
|
+
const baseline = proveSavedLearnerBaseline({
|
|
2557
|
+
cwd,
|
|
2558
|
+
applyRel: query ? searchApplyRel(query) : null,
|
|
2559
|
+
lesson: saved.lesson,
|
|
2560
|
+
output: print,
|
|
2561
|
+
json,
|
|
2562
|
+
});
|
|
2563
|
+
if (baseline !== 0) return { thin: false, code: baseline, lesson: saved.lesson };
|
|
2564
|
+
return { thin: false, code: applyCode, lesson: saved.lesson };
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2193
2567
|
function printSearchLearnerGate(rows, options, output) {
|
|
2194
2568
|
printLearnerCheckGate(output, notesLessonFromText(searchLessonText(rows)), {
|
|
2195
2569
|
includeCheck: true,
|
|
@@ -2197,6 +2571,40 @@ function printSearchLearnerGate(rows, options, output) {
|
|
|
2197
2571
|
});
|
|
2198
2572
|
}
|
|
2199
2573
|
|
|
2574
|
+
function gateSearchLearner(rows, options, output, deps = {}) {
|
|
2575
|
+
if (options && options.json) return 0;
|
|
2576
|
+
const minted = mintRichSearch({
|
|
2577
|
+
cwd: deps.cwd || process.cwd(),
|
|
2578
|
+
query: options && options.query,
|
|
2579
|
+
rows,
|
|
2580
|
+
now: deps.now,
|
|
2581
|
+
output,
|
|
2582
|
+
ensureApply: deps.ensureApply,
|
|
2583
|
+
json: false,
|
|
2584
|
+
});
|
|
2585
|
+
if (minted.thin) printSearchTeachNext(rows, options, output);
|
|
2586
|
+
return minted.code;
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
function printSearchOutcome(rows, options, output, deps = {}) {
|
|
2590
|
+
printSearchRows(rows, options, output);
|
|
2591
|
+
return gateSearchLearner(rows, options, output, deps);
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
function resultStdout(result) {
|
|
2595
|
+
if (typeof result === 'string') return result;
|
|
2596
|
+
return String((result && result.stdout) || '');
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
function searchRowsFromResult(result) {
|
|
2600
|
+
return parseSearchStdout(resultStdout(result));
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
function finishSuccessfulSearch(rows, options, output, deps) {
|
|
2604
|
+
writeLocalSearchCache(options.query, rows, deps);
|
|
2605
|
+
return printSearchOutcome(rows, options, output, deps);
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2200
2608
|
function commandOnPath(name, deps = {}) {
|
|
2201
2609
|
const spawn = deps.spawnSync || spawnSync;
|
|
2202
2610
|
const result = spawn('sh', ['-c', `command -v ${shellSingleQuote(name)}`], {
|
|
@@ -2313,6 +2721,9 @@ async function runYoutubeSearch(args = [], deps = {}) {
|
|
|
2313
2721
|
return 2;
|
|
2314
2722
|
}
|
|
2315
2723
|
|
|
2724
|
+
let rows = searchRowsFromResult(result);
|
|
2725
|
+
if (rows.length) return finishSuccessfulSearch(rows, options, output, deps);
|
|
2726
|
+
|
|
2316
2727
|
let status = searchRunnerStatus(result);
|
|
2317
2728
|
if (status != null && status !== 0 && isLocalSearchRateLimited(result)) {
|
|
2318
2729
|
await waitLocalSearchBackoff(deps);
|
|
@@ -2326,15 +2737,16 @@ async function runYoutubeSearch(args = [], deps = {}) {
|
|
|
2326
2737
|
output('ytsearch and yt-dlp not found. Install yt-dlp or put ytsearch on PATH.');
|
|
2327
2738
|
return 2;
|
|
2328
2739
|
}
|
|
2740
|
+
rows = searchRowsFromResult(result);
|
|
2741
|
+
if (rows.length) return finishSuccessfulSearch(rows, options, output, deps);
|
|
2329
2742
|
status = searchRunnerStatus(result);
|
|
2330
2743
|
if (status != null && status !== 0 && isLocalSearchRateLimited(result)) {
|
|
2331
2744
|
const cached = readFreshLocalSearchCache(options.query, deps);
|
|
2332
2745
|
if (cached) {
|
|
2333
|
-
const
|
|
2334
|
-
|
|
2335
|
-
printSearchTeachNext(rows, options, output);
|
|
2746
|
+
const cachedRows = cached.rows.slice(0, options.limit);
|
|
2747
|
+
const code = printSearchOutcome(cachedRows, options, output, deps);
|
|
2336
2748
|
output(LOCAL_SEARCH_CACHE_NOTE);
|
|
2337
|
-
return
|
|
2749
|
+
return code;
|
|
2338
2750
|
}
|
|
2339
2751
|
output(LOCAL_SEARCH_RATE_LIMIT_MESSAGE);
|
|
2340
2752
|
return status == null ? 1 : status;
|
|
@@ -2347,19 +2759,9 @@ async function runYoutubeSearch(args = [], deps = {}) {
|
|
|
2347
2759
|
return status == null ? 1 : status;
|
|
2348
2760
|
}
|
|
2349
2761
|
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
output('no videos found');
|
|
2354
|
-
if (!options.json) printWatchTickNext(output);
|
|
2355
|
-
return 2;
|
|
2356
|
-
}
|
|
2357
|
-
|
|
2358
|
-
writeLocalSearchCache(options.query, rows, deps);
|
|
2359
|
-
printSearchRows(rows, options, output);
|
|
2360
|
-
printSearchLearnerGate(rows, options, output);
|
|
2361
|
-
printSearchTeachNext(rows, options, output);
|
|
2362
|
-
return 0;
|
|
2762
|
+
output('no videos found');
|
|
2763
|
+
if (!options.json) printWatchTickNext(output);
|
|
2764
|
+
return 2;
|
|
2363
2765
|
}
|
|
2364
2766
|
|
|
2365
2767
|
const YTTEACH_USAGE = 'usage: atris youtube teach <youtube-url> [--section N] [--save] [--recap TEXT] [--skip] | owed | next';
|
|
@@ -3292,33 +3694,23 @@ async function extractTeachSource(youtubeUrl, deps = {}) {
|
|
|
3292
3694
|
return deps.extractTeachSource(youtubeUrl, deps);
|
|
3293
3695
|
}
|
|
3294
3696
|
const runner = deps.spawnSync || spawnSync;
|
|
3295
|
-
const result = runner('yt-dlp',
|
|
3697
|
+
const result = runner('yt-dlp', ytDlpInfoArgs(youtubeUrl), {
|
|
3296
3698
|
encoding: 'utf8',
|
|
3297
3699
|
timeout: 20000,
|
|
3298
3700
|
maxBuffer: 10 * 1024 * 1024,
|
|
3299
3701
|
});
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
try {
|
|
3304
|
-
info = JSON.parse(result.stdout);
|
|
3305
|
-
} catch {
|
|
3306
|
-
return null;
|
|
3307
|
-
}
|
|
3308
|
-
|
|
3309
|
-
const selected = chooseCaptionTrack(info);
|
|
3310
|
-
if (!selected?.track?.url) return null;
|
|
3311
|
-
const rawCaption = await (deps.fetchCaptionText || fetchCaptionText)(selected.track.url);
|
|
3312
|
-
const cues = parseCaptionCues(rawCaption);
|
|
3702
|
+
const info = parseYtDlpInfoJson(result);
|
|
3703
|
+
const loaded = await loadCaptionRaw(info, youtubeUrl, deps);
|
|
3704
|
+
const cues = parseCaptionCues(loaded && loaded.raw);
|
|
3313
3705
|
if (!cues.length) return null;
|
|
3314
3706
|
|
|
3315
3707
|
return {
|
|
3316
|
-
id: info
|
|
3317
|
-
title: info.title || '',
|
|
3708
|
+
id: teachSourceVideoId(info, youtubeUrl),
|
|
3709
|
+
title: (info && info.title) || '',
|
|
3318
3710
|
url: youtubeUrl,
|
|
3319
|
-
durationSeconds: Number(info.duration || 0) || undefined,
|
|
3320
|
-
language:
|
|
3321
|
-
chapters: normalizeChapters(info.chapters, info.duration),
|
|
3711
|
+
durationSeconds: Number((info && info.duration) || 0) || undefined,
|
|
3712
|
+
language: loaded.language || 'unknown',
|
|
3713
|
+
chapters: normalizeChapters(info && info.chapters, info && info.duration),
|
|
3322
3714
|
cues,
|
|
3323
3715
|
};
|
|
3324
3716
|
}
|
|
@@ -3368,7 +3760,10 @@ async function runYoutubeTeach(args = [], deps = {}) {
|
|
|
3368
3760
|
}
|
|
3369
3761
|
}
|
|
3370
3762
|
|
|
3371
|
-
const source = await (deps.extractTeachSource || extractTeachSource)(parsed.url,
|
|
3763
|
+
const source = await (deps.extractTeachSource || extractTeachSource)(parsed.url, {
|
|
3764
|
+
...deps,
|
|
3765
|
+
workDir: deps.workDir || notesWorkDir(deps),
|
|
3766
|
+
});
|
|
3372
3767
|
if (!source || !Array.isArray(source.cues) || !source.cues.length) {
|
|
3373
3768
|
output('no english captions for this url. teach stays local and will not call process.');
|
|
3374
3769
|
return 2;
|
|
@@ -3492,7 +3887,15 @@ async function youtubeCommand(argv = process.argv.slice(3), deps = {}) {
|
|
|
3492
3887
|
output(JSON.stringify(data, null, 2));
|
|
3493
3888
|
} else {
|
|
3494
3889
|
output(formatYoutubeResult(data));
|
|
3495
|
-
|
|
3890
|
+
const mintCode = mintRichProcess({
|
|
3891
|
+
cwd: deps.cwd || process.cwd(),
|
|
3892
|
+
url: options.youtubeUrl,
|
|
3893
|
+
data,
|
|
3894
|
+
now: deps.now,
|
|
3895
|
+
output,
|
|
3896
|
+
ensureApply: deps.ensureApply,
|
|
3897
|
+
});
|
|
3898
|
+
if (mintCode !== 0) status = mintCode;
|
|
3496
3899
|
}
|
|
3497
3900
|
} catch (err) {
|
|
3498
3901
|
if (!err.applyRequired) output(err.message);
|
|
@@ -3510,10 +3913,12 @@ module.exports = {
|
|
|
3510
3913
|
parseYoutubeArgs,
|
|
3511
3914
|
buildYoutubePayload,
|
|
3512
3915
|
extractLocalTranscript,
|
|
3916
|
+
parseYtDlpInfoJson,
|
|
3513
3917
|
processYoutube,
|
|
3514
3918
|
shouldRetryWithLocalTranscript,
|
|
3515
3919
|
formatYoutubeResult,
|
|
3516
3920
|
fileBriefFromNotes,
|
|
3921
|
+
keptPrintedNotes,
|
|
3517
3922
|
ensureNotesApply,
|
|
3518
3923
|
unsaveYoutubeNotes,
|
|
3519
3924
|
APPLY_NEXT_MESSAGE,
|
|
@@ -3541,6 +3946,7 @@ module.exports = {
|
|
|
3541
3946
|
extractTeachNumbers,
|
|
3542
3947
|
extractTeachMechanisms,
|
|
3543
3948
|
extractTeachSource,
|
|
3949
|
+
readLocalCaptionText,
|
|
3544
3950
|
oneTeachCheck,
|
|
3545
3951
|
learnerCheckFromLesson,
|
|
3546
3952
|
scoreLearnerNeedles,
|
|
@@ -3556,6 +3962,11 @@ module.exports = {
|
|
|
3556
3962
|
notesExperimentSlug,
|
|
3557
3963
|
digestExperimentSlug,
|
|
3558
3964
|
watchExperimentSlug,
|
|
3965
|
+
processExperimentSlug,
|
|
3966
|
+
processApplyRel,
|
|
3967
|
+
searchExperimentSlug,
|
|
3968
|
+
searchApplyRel,
|
|
3969
|
+
firstRichWatchLesson,
|
|
3559
3970
|
fileTeachExperiment,
|
|
3560
3971
|
youtubeCommand,
|
|
3561
3972
|
};
|