atris 3.57.2 → 3.57.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/atris/skills/engines/SKILL.md +4 -3
- package/atris/skills/x-search/SKILL.md +11 -5
- package/atris/skills/youtube/SKILL.md +6 -4
- package/ax +4 -1
- package/bin/atris.js +1 -1
- package/commands/close.js +294 -12
- package/commands/integrations.js +11 -2
- package/commands/mission.js +7 -16
- package/commands/worktree.js +56 -0
- package/commands/x-search.js +231 -14
- package/commands/youtube.js +331 -33
- package/lib/apply-gate.js +13 -0
- package/lib/engine-ask.js +21 -2
- package/package.json +1 -1
package/commands/youtube.js
CHANGED
|
@@ -30,7 +30,7 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
|
|
|
30
30
|
output(`Usage: ${commandName} search "<query>" [--limit N] [--json]`);
|
|
31
31
|
output(` ${commandName} search --paid "<query>" [--limit N] [--json]`);
|
|
32
32
|
output(` ${commandName} notes <youtube-url> [youtube-url-or-playlist...] [engine] [--save]`);
|
|
33
|
-
output(` ${commandName} teach <youtube-url> [--section N] [--save]`);
|
|
33
|
+
output(` ${commandName} teach <youtube-url> [--section N] [--save] [--recap TEXT] [--skip]`);
|
|
34
34
|
output(` ${commandName} unsave <url-or-id>`);
|
|
35
35
|
output(` ${commandName} process <youtube-url> [options]`);
|
|
36
36
|
output(` ${commandName} digest [--days N]`);
|
|
@@ -44,6 +44,7 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
|
|
|
44
44
|
output('search --paid = 5 credits, watch permalinks + titles from Atris');
|
|
45
45
|
output('notes = free local notes to stdout; ephemeral unless --save');
|
|
46
46
|
output('teach = one chapter from local captions; ephemeral unless --save');
|
|
47
|
+
output('rich ephemeral notes/teach print one apply next-step (no files)');
|
|
47
48
|
output('process = 5 credits cloud knowledge (needs a filled Apply)');
|
|
48
49
|
output('digest = one decision page from this week\'s video briefs');
|
|
49
50
|
output('watch = subscribed channels turn into briefs without a human');
|
|
@@ -53,9 +54,11 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
|
|
|
53
54
|
output('Options:');
|
|
54
55
|
output(' --limit <n> Max search results (default: 5)');
|
|
55
56
|
output(' --paid Bill 5 credits for watch permalinks (search only)');
|
|
56
|
-
output(' --save File brief, journal, apply
|
|
57
|
+
output(' --save File brief, journal, apply; rich notes/teach mint a keep/revert experiment');
|
|
57
58
|
output(' --section <n> Chapter to teach, 1-based (teach only, default: 1)');
|
|
58
|
-
output(' --
|
|
59
|
+
output(' --recap <text> Unlock the next teach section with the unpaid check');
|
|
60
|
+
output(' --skip Unlock the next teach section without answering');
|
|
61
|
+
output(' --unsave Delete filed brief, apply stub, and matching notes/teach experiment packs (no paid calls)');
|
|
59
62
|
output(' --query, -q <text> Focus question for the analysis');
|
|
60
63
|
output(' --agent <id> Agent id to store knowledge against');
|
|
61
64
|
output(' --store Save as agent knowledge (requires --agent)');
|
|
@@ -645,16 +648,68 @@ function applySidecarRel(id) {
|
|
|
645
648
|
return applyGate.applySidecarRel('youtube', id);
|
|
646
649
|
}
|
|
647
650
|
|
|
648
|
-
function
|
|
651
|
+
function notesExperimentSlug(id) {
|
|
652
|
+
return `notes-${experimentIdToken(id)}`;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function notesExperimentRel(id) {
|
|
656
|
+
return `atris/experiments/${notesExperimentSlug(id)}`;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function notesLessonFromText(text) {
|
|
660
|
+
const body = String(text || '');
|
|
661
|
+
return {
|
|
662
|
+
numbers: extractTeachNumbers(body),
|
|
663
|
+
mechanisms: extractTeachMechanisms(body),
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function readNotesText({ url, workDir } = {}) {
|
|
668
|
+
const id = videoIdFromUrl(url);
|
|
669
|
+
if (!id || !workDir) return '';
|
|
670
|
+
const notesPath = path.join(workDir, `yt_${id}.md`);
|
|
671
|
+
try {
|
|
672
|
+
if (!fs.existsSync(notesPath)) return '';
|
|
673
|
+
return fs.readFileSync(notesPath, 'utf8');
|
|
674
|
+
} catch {
|
|
675
|
+
return '';
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function saveRichNotes(url, deps = {}) {
|
|
680
|
+
const workDir = deps.workDir || path.join(process.env.TMPDIR || '/tmp', 'ytnotes');
|
|
681
|
+
const lesson = notesLessonFromText(readNotesText({ url, workDir }));
|
|
682
|
+
if (isThinTeachLesson(lesson)) {
|
|
683
|
+
return { thin: true, brief: null, packRel: null };
|
|
684
|
+
}
|
|
685
|
+
const brief = fileNotesBrief(url, deps);
|
|
649
686
|
const id = videoIdFromUrl(url);
|
|
687
|
+
const packRel = fileTeachExperiment({
|
|
688
|
+
cwd: deps.cwd || process.cwd(),
|
|
689
|
+
url,
|
|
690
|
+
lesson,
|
|
691
|
+
slug: id ? notesExperimentSlug(id) : null,
|
|
692
|
+
applyRel: id ? applySidecarRel(id) : null,
|
|
693
|
+
});
|
|
694
|
+
return { thin: false, brief, packRel };
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function ensureNotesApply({ cwd, url, packRel, now, output } = {}) {
|
|
698
|
+
const id = videoIdFromUrl(url);
|
|
699
|
+
const pack = packRel || (id ? notesExperimentRel(id) : null);
|
|
650
700
|
return applyGate.ensureApply({
|
|
651
701
|
cwd,
|
|
652
702
|
source: url,
|
|
653
703
|
rel: id ? applySidecarRel(id) : null,
|
|
654
704
|
now,
|
|
655
705
|
output,
|
|
656
|
-
incompleteMessage:
|
|
706
|
+
incompleteMessage: pack
|
|
707
|
+
? `next: apply ${pack}. keep only if measure.py moves 0→1`
|
|
708
|
+
: APPLY_NEXT_MESSAGE,
|
|
657
709
|
required: false,
|
|
710
|
+
change: pack ? `apply ${pack}` : undefined,
|
|
711
|
+
receipt: pack ? TEACH_KEEP_RULE : undefined,
|
|
712
|
+
journalLine: pack ? `- [claimable] apply: ${pack}. ${TEACH_KEEP_RULE}` : undefined,
|
|
658
713
|
});
|
|
659
714
|
}
|
|
660
715
|
|
|
@@ -662,6 +717,50 @@ function youtubeBriefRel(id) {
|
|
|
662
717
|
return `atris/wiki/briefs/youtube-${id}.md`;
|
|
663
718
|
}
|
|
664
719
|
|
|
720
|
+
function removeUnsaveRel(cwd, rel, removed) {
|
|
721
|
+
const abs = path.join(cwd, rel);
|
|
722
|
+
try {
|
|
723
|
+
if (!fs.existsSync(abs)) return;
|
|
724
|
+
const st = fs.lstatSync(abs);
|
|
725
|
+
if (st.isDirectory()) fs.rmSync(abs, { recursive: true, force: true });
|
|
726
|
+
else fs.unlinkSync(abs);
|
|
727
|
+
removed.push(rel);
|
|
728
|
+
} catch {
|
|
729
|
+
// already gone or unreadable: do not error
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function listTeachSectionNumbers(cwd, id) {
|
|
734
|
+
const root = path.join(cwd, 'atris', 'experiments');
|
|
735
|
+
const sections = [];
|
|
736
|
+
try {
|
|
737
|
+
if (!fs.existsSync(root)) return sections;
|
|
738
|
+
for (const name of fs.readdirSync(root)) {
|
|
739
|
+
const match = String(name).match(/^teach-.+-s(\d+)$/);
|
|
740
|
+
if (!match) continue;
|
|
741
|
+
const section = Number(match[1]);
|
|
742
|
+
if (name === teachExperimentSlug(id, section)) sections.push(section);
|
|
743
|
+
}
|
|
744
|
+
} catch {
|
|
745
|
+
// missing experiments dir is fine
|
|
746
|
+
}
|
|
747
|
+
return sections.sort((a, b) => a - b);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function listTeachSidecarRels(cwd, id) {
|
|
751
|
+
const briefsDir = path.join(cwd, 'atris', 'wiki', 'briefs');
|
|
752
|
+
const prefix = `youtube-${id}-s`;
|
|
753
|
+
try {
|
|
754
|
+
if (!fs.existsSync(briefsDir)) return [];
|
|
755
|
+
return fs.readdirSync(briefsDir)
|
|
756
|
+
.filter((name) => name.startsWith(prefix) && name.endsWith('.md'))
|
|
757
|
+
.sort((a, b) => a.localeCompare(b, 'en'))
|
|
758
|
+
.map((name) => `atris/wiki/briefs/${name}`);
|
|
759
|
+
} catch {
|
|
760
|
+
return [];
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
665
764
|
function unsaveYoutubeNotes(target, deps = {}) {
|
|
666
765
|
const output = deps.output || ((line = '') => console.log(line));
|
|
667
766
|
const cwd = deps.cwd || process.cwd();
|
|
@@ -672,18 +771,26 @@ function unsaveYoutubeNotes(target, deps = {}) {
|
|
|
672
771
|
}
|
|
673
772
|
const briefRel = youtubeBriefRel(id);
|
|
674
773
|
const applyRel = applySidecarRel(id);
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
774
|
+
const sections = listTeachSectionNumbers(cwd, id);
|
|
775
|
+
const rels = [];
|
|
776
|
+
const seen = new Set();
|
|
777
|
+
const add = (rel) => {
|
|
778
|
+
if (!rel || seen.has(rel)) return;
|
|
779
|
+
seen.add(rel);
|
|
780
|
+
rels.push(rel);
|
|
781
|
+
};
|
|
782
|
+
add(briefRel);
|
|
783
|
+
add(applyRel);
|
|
784
|
+
for (const section of sections) {
|
|
785
|
+
add(teachBriefRel(id, section));
|
|
786
|
+
add(applySidecarRel(`${id}-s${section}`));
|
|
686
787
|
}
|
|
788
|
+
for (const rel of listTeachSidecarRels(cwd, id)) add(rel);
|
|
789
|
+
add(notesExperimentRel(id));
|
|
790
|
+
for (const section of sections) add(teachExperimentRel(id, section));
|
|
791
|
+
|
|
792
|
+
const removed = [];
|
|
793
|
+
for (const rel of rels) removeUnsaveRel(cwd, rel, removed);
|
|
687
794
|
if (!removed.length) {
|
|
688
795
|
output(`already gone: ${briefRel} and ${applyRel}`);
|
|
689
796
|
return 0;
|
|
@@ -1339,9 +1446,30 @@ function runOneNotesItem(item, engine, deps = {}) {
|
|
|
1339
1446
|
} catch {
|
|
1340
1447
|
status = 1;
|
|
1341
1448
|
}
|
|
1342
|
-
|
|
1449
|
+
let brief = null;
|
|
1450
|
+
let ok = status === 0;
|
|
1451
|
+
if (ok && deps.save) {
|
|
1452
|
+
const saved = saveRichNotes(item.url, deps);
|
|
1453
|
+
if (saved.thin) {
|
|
1454
|
+
output(TEACH_THIN_REFUSE);
|
|
1455
|
+
ok = false;
|
|
1456
|
+
} else {
|
|
1457
|
+
brief = saved.brief;
|
|
1458
|
+
const ensureApply = deps.ensureApply || ensureNotesApply;
|
|
1459
|
+
try {
|
|
1460
|
+
ensureApply({
|
|
1461
|
+
cwd: deps.cwd || process.cwd(),
|
|
1462
|
+
url: item.url,
|
|
1463
|
+
packRel: saved.packRel,
|
|
1464
|
+
now: deps.now,
|
|
1465
|
+
output,
|
|
1466
|
+
});
|
|
1467
|
+
} catch {
|
|
1468
|
+
// apply filing must never break the batch
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1343
1472
|
const seconds = Math.max(0, Math.round((readNowMs(deps) - started) / 1000));
|
|
1344
|
-
const ok = status === 0;
|
|
1345
1473
|
output(`${label} ${seconds}s ${ok ? (brief || 'ok') : 'FAILED'}`);
|
|
1346
1474
|
return { url: item.url, id: item.id, seconds, ok, brief };
|
|
1347
1475
|
}
|
|
@@ -1380,12 +1508,23 @@ function runSingleYoutubeNotes(url, engine, deps = {}) {
|
|
|
1380
1508
|
}
|
|
1381
1509
|
const status = readRunnerStatus(result);
|
|
1382
1510
|
if (status !== 0) return status;
|
|
1383
|
-
|
|
1384
|
-
|
|
1511
|
+
const output = deps.output || ((line = '') => console.error(line));
|
|
1512
|
+
if (!deps.save) {
|
|
1513
|
+
const workDir = deps.workDir || path.join(process.env.TMPDIR || '/tmp', 'ytnotes');
|
|
1514
|
+
const lesson = notesLessonFromText(readNotesText({ url, workDir }));
|
|
1515
|
+
if (!isThinTeachLesson(lesson)) applyGate.hintEphemeralApply(output, 'notes');
|
|
1516
|
+
return 0;
|
|
1517
|
+
}
|
|
1518
|
+
const saved = saveRichNotes(url, deps);
|
|
1519
|
+
if (saved.thin) {
|
|
1520
|
+
output(TEACH_THIN_REFUSE);
|
|
1521
|
+
return 2;
|
|
1522
|
+
}
|
|
1385
1523
|
const ensureApply = deps.ensureApply || ensureNotesApply;
|
|
1386
1524
|
return ensureApply({
|
|
1387
1525
|
cwd: deps.cwd || process.cwd(),
|
|
1388
1526
|
url,
|
|
1527
|
+
packRel: saved.packRel,
|
|
1389
1528
|
now: deps.now,
|
|
1390
1529
|
output: deps.output,
|
|
1391
1530
|
});
|
|
@@ -1936,9 +2075,11 @@ async function runYoutubeSearch(args = [], deps = {}) {
|
|
|
1936
2075
|
return 0;
|
|
1937
2076
|
}
|
|
1938
2077
|
|
|
1939
|
-
const YTTEACH_USAGE = 'usage: atris youtube teach <youtube-url> [--section N] [--save]';
|
|
2078
|
+
const YTTEACH_USAGE = 'usage: atris youtube teach <youtube-url> [--section N] [--save] [--recap TEXT] [--skip]';
|
|
1940
2079
|
const TEACH_PAID_REFUSE = 'teach is free local captions. drop --paid.';
|
|
1941
2080
|
const TEACH_THIN_REFUSE = 'thin: no number or named mechanism. no brief.';
|
|
2081
|
+
const TEACH_OWED_FILE = 'youtube-teach-owed.json';
|
|
2082
|
+
const TEACH_RECAP_MISSING = '--recap needs the unpaid check';
|
|
1942
2083
|
const TEACH_APPLY_NEXT_MESSAGE = APPLY_NEXT_MESSAGE;
|
|
1943
2084
|
const TEACH_KEEP_RULE = 'keep only if measure.py moves 0→1. scores 1 only when the fixture contains the check tokens.';
|
|
1944
2085
|
const MECHANISM_STOP = new Set([
|
|
@@ -1965,6 +2106,9 @@ function parseTeachArgs(argv = []) {
|
|
|
1965
2106
|
const options = {
|
|
1966
2107
|
help: false,
|
|
1967
2108
|
save: false,
|
|
2109
|
+
json: false,
|
|
2110
|
+
skip: false,
|
|
2111
|
+
recap: null,
|
|
1968
2112
|
url: null,
|
|
1969
2113
|
section: 1,
|
|
1970
2114
|
};
|
|
@@ -1980,8 +2124,23 @@ function parseTeachArgs(argv = []) {
|
|
|
1980
2124
|
options.help = true;
|
|
1981
2125
|
} else if (arg === '--save') {
|
|
1982
2126
|
options.save = true;
|
|
2127
|
+
} else if (arg === '--json') {
|
|
2128
|
+
options.json = true;
|
|
2129
|
+
} else if (arg === '--skip') {
|
|
2130
|
+
options.skip = true;
|
|
1983
2131
|
} else if (arg === '--paid') {
|
|
1984
2132
|
throw new Error(TEACH_PAID_REFUSE);
|
|
2133
|
+
} else if (arg === '--recap') {
|
|
2134
|
+
const raw = args[i + 1];
|
|
2135
|
+
if (raw == null || String(raw).startsWith('--')) {
|
|
2136
|
+
throw new Error(TEACH_RECAP_MISSING);
|
|
2137
|
+
}
|
|
2138
|
+
options.recap = String(raw);
|
|
2139
|
+
i += 1;
|
|
2140
|
+
} else if (arg.startsWith('--recap=')) {
|
|
2141
|
+
const value = arg.slice('--recap='.length);
|
|
2142
|
+
if (!value) throw new Error(TEACH_RECAP_MISSING);
|
|
2143
|
+
options.recap = value;
|
|
1985
2144
|
} else if (arg === '--section') {
|
|
1986
2145
|
const raw = args[i + 1];
|
|
1987
2146
|
const value = Number.parseInt(raw, 10);
|
|
@@ -1998,6 +2157,18 @@ function parseTeachArgs(argv = []) {
|
|
|
1998
2157
|
options.section = value;
|
|
1999
2158
|
} else if (arg.startsWith('-')) {
|
|
2000
2159
|
throw new Error(`Unknown option: ${arg}`);
|
|
2160
|
+
} else if (arg === 'recap' && options.recap == null && !options.url) {
|
|
2161
|
+
const parts = [];
|
|
2162
|
+
i += 1;
|
|
2163
|
+
while (i < args.length && !String(args[i]).startsWith('--')) {
|
|
2164
|
+
parts.push(String(args[i]));
|
|
2165
|
+
i += 1;
|
|
2166
|
+
}
|
|
2167
|
+
i -= 1;
|
|
2168
|
+
options.recap = parts.join(' ').trim();
|
|
2169
|
+
if (!options.recap) throw new Error(TEACH_RECAP_MISSING);
|
|
2170
|
+
} else if (arg === 'skip' && !options.url) {
|
|
2171
|
+
options.skip = true;
|
|
2001
2172
|
} else if (!options.url && looksLikeYoutubeUrl(arg)) {
|
|
2002
2173
|
options.url = arg;
|
|
2003
2174
|
} else {
|
|
@@ -2006,7 +2177,9 @@ function parseTeachArgs(argv = []) {
|
|
|
2006
2177
|
}
|
|
2007
2178
|
|
|
2008
2179
|
if (options.help) return options;
|
|
2009
|
-
if (!options.url
|
|
2180
|
+
if (!options.url && options.recap == null && !options.skip) {
|
|
2181
|
+
throw new Error('Missing YouTube URL. Run "atris youtube teach --help".');
|
|
2182
|
+
}
|
|
2010
2183
|
return options;
|
|
2011
2184
|
}
|
|
2012
2185
|
|
|
@@ -2185,14 +2358,17 @@ function teachBriefRel(id, section) {
|
|
|
2185
2358
|
return `atris/wiki/briefs/youtube-${id}-s${section}.md`;
|
|
2186
2359
|
}
|
|
2187
2360
|
|
|
2188
|
-
function
|
|
2189
|
-
|
|
2361
|
+
function experimentIdToken(id) {
|
|
2362
|
+
return String(id || 'video')
|
|
2190
2363
|
.toLowerCase()
|
|
2191
2364
|
.replace(/_/g, '-')
|
|
2192
2365
|
.replace(/[^a-z0-9-]+/g, '')
|
|
2193
2366
|
.replace(/-+/g, '-')
|
|
2194
2367
|
.replace(/^-+|-+$/g, '') || 'video';
|
|
2195
|
-
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
function teachExperimentSlug(id, section) {
|
|
2371
|
+
return `teach-${experimentIdToken(id)}-s${Number(section) || 1}`;
|
|
2196
2372
|
}
|
|
2197
2373
|
|
|
2198
2374
|
function teachExperimentRel(id, section) {
|
|
@@ -2207,22 +2383,118 @@ function teachCheckNeedles(lesson = {}) {
|
|
|
2207
2383
|
return [];
|
|
2208
2384
|
}
|
|
2209
2385
|
|
|
2386
|
+
function teachRecapTokens(lesson = {}) {
|
|
2387
|
+
const tokens = [];
|
|
2388
|
+
const seen = new Set();
|
|
2389
|
+
for (const value of [...(lesson.mechanisms || []), ...(lesson.numbers || [])]) {
|
|
2390
|
+
const token = String(value || '').trim().toLowerCase();
|
|
2391
|
+
if (!token || seen.has(token)) continue;
|
|
2392
|
+
seen.add(token);
|
|
2393
|
+
tokens.push(token);
|
|
2394
|
+
}
|
|
2395
|
+
return tokens;
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
function recapHitsTokens(text, tokens) {
|
|
2399
|
+
const hay = String(text || '').toLowerCase();
|
|
2400
|
+
return (tokens || []).some((token) => {
|
|
2401
|
+
const needle = String(token || '').trim().toLowerCase();
|
|
2402
|
+
return needle.length > 0 && hay.includes(needle);
|
|
2403
|
+
});
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
function teachOwedPath(deps = {}) {
|
|
2407
|
+
if (deps.teachOwedPath) return deps.teachOwedPath;
|
|
2408
|
+
const cwd = deps.cwd || process.cwd();
|
|
2409
|
+
return path.join(cwd, '.atris', TEACH_OWED_FILE);
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
function readTeachOwedStore(deps = {}) {
|
|
2413
|
+
try {
|
|
2414
|
+
const parsed = JSON.parse(fs.readFileSync(teachOwedPath(deps), 'utf8'));
|
|
2415
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {};
|
|
2416
|
+
return parsed;
|
|
2417
|
+
} catch {
|
|
2418
|
+
return {};
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
function writeTeachOwedStore(deps, store) {
|
|
2423
|
+
try {
|
|
2424
|
+
const filePath = teachOwedPath(deps);
|
|
2425
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
2426
|
+
fs.writeFileSync(filePath, `${JSON.stringify(store || {})}\n`);
|
|
2427
|
+
} catch {
|
|
2428
|
+
// owed recap is ephemeral; a failed write must not break teach
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
function rememberTeachOwed(deps, { url, section, lesson } = {}) {
|
|
2433
|
+
const id = videoIdFromUrl(url);
|
|
2434
|
+
if (!id) return;
|
|
2435
|
+
const store = readTeachOwedStore(deps);
|
|
2436
|
+
store[id] = {
|
|
2437
|
+
section: Number(section) || 1,
|
|
2438
|
+
check: teachCheckLine(lesson),
|
|
2439
|
+
tokens: teachRecapTokens(lesson),
|
|
2440
|
+
};
|
|
2441
|
+
writeTeachOwedStore(deps, store);
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
function previousTeachUnlocked(owed, section) {
|
|
2445
|
+
if (!owed) return true;
|
|
2446
|
+
return Number(owed.section) >= Number(section);
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
function applyTeachRecap(parsed, deps, output) {
|
|
2450
|
+
const store = readTeachOwedStore(deps);
|
|
2451
|
+
let id = parsed.url ? videoIdFromUrl(parsed.url) : null;
|
|
2452
|
+
let entry = id ? store[id] : null;
|
|
2453
|
+
if (!entry && !id) {
|
|
2454
|
+
const ids = Object.keys(store);
|
|
2455
|
+
if (parsed.recap) {
|
|
2456
|
+
id = ids.find((key) => recapHitsTokens(parsed.recap, store[key] && store[key].tokens));
|
|
2457
|
+
}
|
|
2458
|
+
if (!id && ids.length === 1) id = ids[0];
|
|
2459
|
+
entry = id ? store[id] : null;
|
|
2460
|
+
}
|
|
2461
|
+
if (!entry) {
|
|
2462
|
+
if (parsed.skip) return 0;
|
|
2463
|
+
if (!parsed.json) output('recap the previous section first');
|
|
2464
|
+
return 2;
|
|
2465
|
+
}
|
|
2466
|
+
if (parsed.skip) {
|
|
2467
|
+
delete store[id];
|
|
2468
|
+
writeTeachOwedStore(deps, store);
|
|
2469
|
+
return 0;
|
|
2470
|
+
}
|
|
2471
|
+
if (!recapHitsTokens(parsed.recap, entry.tokens)) {
|
|
2472
|
+
if (!parsed.json) output(entry.check);
|
|
2473
|
+
return 2;
|
|
2474
|
+
}
|
|
2475
|
+
delete store[id];
|
|
2476
|
+
writeTeachOwedStore(deps, store);
|
|
2477
|
+
return 0;
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2210
2480
|
function teachCheckLine(lesson = {}) {
|
|
2211
2481
|
return oneTeachCheck(lesson.mechanisms || [], lesson.numbers || [], '');
|
|
2212
2482
|
}
|
|
2213
2483
|
|
|
2214
|
-
function fileTeachExperiment({ cwd, url, section, lesson } = {}) {
|
|
2484
|
+
function fileTeachExperiment({ cwd, url, section, lesson, slug, applyRel } = {}) {
|
|
2215
2485
|
try {
|
|
2216
|
-
const id = videoIdFromUrl(url);
|
|
2217
|
-
if (!
|
|
2218
|
-
const
|
|
2219
|
-
|
|
2486
|
+
const id = url ? videoIdFromUrl(url) : null;
|
|
2487
|
+
if (!cwd) return null;
|
|
2488
|
+
const packSlug = slug || (id ? teachExperimentSlug(id, section) : null);
|
|
2489
|
+
if (!packSlug) return null;
|
|
2490
|
+
const rel = `atris/experiments/${packSlug}`;
|
|
2220
2491
|
const dir = path.join(cwd, rel);
|
|
2221
2492
|
fs.mkdirSync(dir, { recursive: true });
|
|
2222
2493
|
|
|
2223
2494
|
const check = teachCheckLine(lesson);
|
|
2224
2495
|
const needles = teachCheckNeedles(lesson);
|
|
2225
|
-
const
|
|
2496
|
+
const sidecarRel = applyRel || (id ? applySidecarRel(`${id}-s${section}`) : null);
|
|
2497
|
+
if (!sidecarRel) return null;
|
|
2226
2498
|
const program = [
|
|
2227
2499
|
'# Program',
|
|
2228
2500
|
'',
|
|
@@ -2243,7 +2515,7 @@ function fileTeachExperiment({ cwd, url, section, lesson } = {}) {
|
|
|
2243
2515
|
'EXPERIMENT_DIR = Path(__file__).resolve().parent',
|
|
2244
2516
|
`CHECK = ${JSON.stringify(check)}`,
|
|
2245
2517
|
`NEEDLES = ${JSON.stringify(needles)}`,
|
|
2246
|
-
`DEFAULT_TARGET = ${JSON.stringify(
|
|
2518
|
+
`DEFAULT_TARGET = ${JSON.stringify(sidecarRel)}`,
|
|
2247
2519
|
'',
|
|
2248
2520
|
'',
|
|
2249
2521
|
'def repo_root() -> Path:',
|
|
@@ -2559,6 +2831,26 @@ async function runYoutubeTeach(args = [], deps = {}) {
|
|
|
2559
2831
|
return 0;
|
|
2560
2832
|
}
|
|
2561
2833
|
|
|
2834
|
+
const owedDeps = { ...deps, cwd: deps.cwd || process.cwd() };
|
|
2835
|
+
if (parsed.recap != null || parsed.skip) {
|
|
2836
|
+
const recapCode = applyTeachRecap(parsed, owedDeps, output);
|
|
2837
|
+
if (recapCode !== 0) return recapCode;
|
|
2838
|
+
if (!parsed.url || parsed.section <= 1) return 0;
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
if (!parsed.url) {
|
|
2842
|
+
output('Missing YouTube URL. Run "atris youtube teach --help".');
|
|
2843
|
+
return 2;
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
if (parsed.section > 1) {
|
|
2847
|
+
const owed = readTeachOwedStore(owedDeps)[videoIdFromUrl(parsed.url) || ''];
|
|
2848
|
+
if (!previousTeachUnlocked(owed, parsed.section)) {
|
|
2849
|
+
if (!parsed.json && owed && owed.check) output(owed.check);
|
|
2850
|
+
return 2;
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2562
2854
|
const source = await (deps.extractTeachSource || extractTeachSource)(parsed.url, deps);
|
|
2563
2855
|
if (!source || !Array.isArray(source.cues) || !source.cues.length) {
|
|
2564
2856
|
output('no english captions for this url. teach stays local and will not call process.');
|
|
@@ -2582,8 +2874,12 @@ async function runYoutubeTeach(args = [], deps = {}) {
|
|
|
2582
2874
|
title: source.title,
|
|
2583
2875
|
});
|
|
2584
2876
|
output(lesson.text);
|
|
2877
|
+
rememberTeachOwed(owedDeps, { url: parsed.url, section: parsed.section, lesson });
|
|
2585
2878
|
|
|
2586
|
-
if (!parsed.save)
|
|
2879
|
+
if (!parsed.save) {
|
|
2880
|
+
if (!isThinTeachLesson(lesson)) applyGate.hintEphemeralApply(output, 'teach');
|
|
2881
|
+
return 0;
|
|
2882
|
+
}
|
|
2587
2883
|
if (isThinTeachLesson(lesson)) {
|
|
2588
2884
|
output(TEACH_THIN_REFUSE);
|
|
2589
2885
|
return 2;
|
|
@@ -2706,5 +3002,7 @@ module.exports = {
|
|
|
2706
3002
|
isThinTeachLesson,
|
|
2707
3003
|
TEACH_THIN_REFUSE,
|
|
2708
3004
|
teachExperimentSlug,
|
|
3005
|
+
notesExperimentSlug,
|
|
3006
|
+
fileTeachExperiment,
|
|
2709
3007
|
youtubeCommand,
|
|
2710
3008
|
};
|
package/lib/apply-gate.js
CHANGED
|
@@ -95,8 +95,21 @@ function ensureApply({
|
|
|
95
95
|
return required ? 2 : 0;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
function ephemeralApplyMessage(kind) {
|
|
99
|
+
const label = String(kind || '').trim() || 'result';
|
|
100
|
+
return `next: write one apply (change + receipt) for this ${label}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function hintEphemeralApply(output, kind) {
|
|
104
|
+
const print = typeof output === 'function' ? output : (line = '') => console.error(line);
|
|
105
|
+
print(ephemeralApplyMessage(kind));
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
98
109
|
module.exports = {
|
|
99
110
|
applySlug,
|
|
100
111
|
applySidecarRel,
|
|
101
112
|
ensureApply,
|
|
113
|
+
ephemeralApplyMessage,
|
|
114
|
+
hintEphemeralApply,
|
|
102
115
|
};
|
package/lib/engine-ask.js
CHANGED
|
@@ -222,7 +222,6 @@ function buildReadOnlyEngineInvocation(engineName, prompt, modelName = '') {
|
|
|
222
222
|
'--model', model || profile.model || DEFAULT_CLAUDE_RUNNER_MODEL,
|
|
223
223
|
'--tools', 'Read,Glob,Grep,WebSearch,WebFetch',
|
|
224
224
|
'--permission-mode', 'plan',
|
|
225
|
-
'--safe-mode',
|
|
226
225
|
'--no-session-persistence',
|
|
227
226
|
],
|
|
228
227
|
};
|
|
@@ -293,6 +292,25 @@ function cancelledAskResult(job, extra = {}) {
|
|
|
293
292
|
};
|
|
294
293
|
}
|
|
295
294
|
|
|
295
|
+
function osAccountName() {
|
|
296
|
+
try {
|
|
297
|
+
return String(os.userInfo().username || '').trim();
|
|
298
|
+
} catch {
|
|
299
|
+
return '';
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Claude Code looks up macOS Keychain with process.env.USER, not whoami.
|
|
304
|
+
// Host apps sometimes pass a display name, so pin USER and LOGNAME to the OS account.
|
|
305
|
+
function buildAskSpawnEnv(sourceEnv = process.env) {
|
|
306
|
+
const env = { ...sourceEnv };
|
|
307
|
+
const username = osAccountName();
|
|
308
|
+
if (!username) return env;
|
|
309
|
+
env.USER = username;
|
|
310
|
+
env.LOGNAME = username;
|
|
311
|
+
return env;
|
|
312
|
+
}
|
|
313
|
+
|
|
296
314
|
function runAskProcess(invocation, {
|
|
297
315
|
cwd = process.cwd(),
|
|
298
316
|
timeoutMs = DEFAULT_ASK_TIMEOUT_MS,
|
|
@@ -387,7 +405,7 @@ function runAskProcess(invocation, {
|
|
|
387
405
|
try {
|
|
388
406
|
child = spawnProcess(invocation.bin, invocation.args, {
|
|
389
407
|
cwd,
|
|
390
|
-
env:
|
|
408
|
+
env: buildAskSpawnEnv(),
|
|
391
409
|
detached: process.platform !== 'win32',
|
|
392
410
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
393
411
|
});
|
|
@@ -671,6 +689,7 @@ module.exports = {
|
|
|
671
689
|
MAX_ASK_PROMPT_BYTES,
|
|
672
690
|
parseEngineAskArgs,
|
|
673
691
|
buildReadOnlyEngineInvocation,
|
|
692
|
+
buildAskSpawnEnv,
|
|
674
693
|
runAskProcess,
|
|
675
694
|
runEngineAskJobs,
|
|
676
695
|
runEngineAskCommand,
|