atris 3.30.12 → 3.32.0
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/AGENTS.md +16 -3
- package/FOR_AGENTS.md +81 -0
- package/README.md +6 -4
- package/atris/CLAUDE.md +8 -0
- package/atris/atris.md +51 -19
- package/atris/skills/README.md +1 -0
- package/atris/skills/blocks/SKILL.md +134 -0
- package/atris/skills/clawhub/philosophy-of-work/SKILL.md +56 -0
- package/atris/skills/youtube/SKILL.md +31 -11
- package/atris.md +15 -0
- package/ax +189 -7
- package/bin/atris.js +273 -225
- package/commands/autoland.js +379 -0
- package/commands/autopilot-front.js +273 -0
- package/commands/autopilot.js +94 -4
- package/commands/business.js +1 -1
- package/commands/clean.js +72 -9
- package/commands/codex-goal.js +72 -22
- package/commands/computer.js +48 -3
- package/commands/gm.js +1 -1
- package/commands/harvest.js +179 -0
- package/commands/init.js +22 -1
- package/commands/land.js +442 -0
- package/commands/loop-front.js +122 -4
- package/commands/member.js +551 -19
- package/commands/mission.js +3674 -278
- package/commands/play.js +1 -1
- package/commands/pulse.js +65 -7
- package/commands/run-front.js +144 -0
- package/commands/run.js +10 -7
- package/commands/sign.js +90 -0
- package/commands/strings.js +301 -0
- package/commands/task.js +782 -108
- package/commands/truth.js +171 -0
- package/commands/xp.js +32 -8
- package/commands/youtube.js +72 -5
- package/decks/README.md +6 -12
- package/lib/auto-accept-certified.js +10 -0
- package/lib/autoland.js +391 -0
- package/lib/context-gatherer.js +0 -8
- package/lib/mission-artifact.js +504 -0
- package/lib/mission-room.js +846 -0
- package/lib/next-moves.js +212 -6
- package/lib/operator-next.js +7 -0
- package/lib/pulse.js +78 -4
- package/lib/runner-command.js +51 -20
- package/lib/runs-prune.js +242 -0
- package/lib/task-db.js +19 -2
- package/lib/task-proof.js +1 -1
- package/package.json +4 -4
- package/decks/atris-seed-pitch-v3.json +0 -118
- package/decks/atris-seed-pitch-v4-skeleton.json +0 -106
- package/decks/atris-seed-pitch-v5.json +0 -109
- package/decks/atris-seed-pitch-v6.json +0 -137
- package/decks/atris-seed-pitch-v7.json +0 -133
- package/decks/mark-pincus-narrative.json +0 -102
- package/decks/mark-pincus-sourcery.json +0 -94
- package/decks/yash-applied-compute-detailed.json +0 -150
- package/decks/yash-applied-compute-generalist.json +0 -82
- package/decks/yash-applied-compute-narrative.json +0 -54
- package/lib/ax-chat-input.js +0 -164
- package/lib/ax-goal.js +0 -307
- package/lib/ax-prefs.js +0 -63
- package/lib/ax-shimmer.js +0 -63
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// atris truth — one table of what is actually proven, blocked, or stale.
|
|
2
|
+
// Rolls up sources that already exist; writes nothing. SQL/state is truth, this is the render.
|
|
3
|
+
// 1. .atris/state/missions.jsonl — mission states (dedupe by id, latest wins)
|
|
4
|
+
// 2. ~/.atris/tasks.db — task counts by status
|
|
5
|
+
// 3. atris/features/*/ — validate.md frontmatter + newest proof/ receipt age
|
|
6
|
+
// 4. ~/.atris/heartbeat/{registry,state}.json — declared loops vs last_run / fails
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
|
|
12
|
+
const STALE_DAYS = 7;
|
|
13
|
+
|
|
14
|
+
function readJson(file) {
|
|
15
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function daysAgo(ms) {
|
|
19
|
+
if (!ms) return null;
|
|
20
|
+
return (Date.now() - ms) / 86400000;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function fmtAge(days) {
|
|
24
|
+
if (days == null) return 'never';
|
|
25
|
+
if (days < 1) return `${Math.round(days * 24)}h`;
|
|
26
|
+
return `${Math.round(days)}d`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function loadMissions(cwd) {
|
|
30
|
+
const file = path.join(cwd, '.atris', 'state', 'missions.jsonl');
|
|
31
|
+
if (!fs.existsSync(file)) return [];
|
|
32
|
+
const seen = new Map();
|
|
33
|
+
for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
|
|
34
|
+
if (!line.trim()) continue;
|
|
35
|
+
try {
|
|
36
|
+
const rec = JSON.parse(line);
|
|
37
|
+
const id = rec.id || rec.mission_id;
|
|
38
|
+
if (id) seen.set(id, rec);
|
|
39
|
+
} catch { /* skip bad lines */ }
|
|
40
|
+
}
|
|
41
|
+
// stopped = killed-on-purpose = a closed loop; only genuinely open states count
|
|
42
|
+
return [...seen.values()].filter((m) => m.status && !['complete', 'archived', 'stopped'].includes(m.status));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function loadTaskCounts() {
|
|
46
|
+
try {
|
|
47
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
48
|
+
const db = new DatabaseSync(path.join(os.homedir(), '.atris', 'tasks.db'), { readOnly: true });
|
|
49
|
+
const rows = db.prepare('SELECT status, COUNT(*) n FROM tasks GROUP BY status').all();
|
|
50
|
+
db.close();
|
|
51
|
+
const counts = {};
|
|
52
|
+
for (const r of rows) counts[r.status] = r.n;
|
|
53
|
+
return counts;
|
|
54
|
+
} catch { return null; }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function loadFeatures(cwd) {
|
|
58
|
+
const dir = path.join(cwd, 'atris', 'features');
|
|
59
|
+
if (!fs.existsSync(dir)) return [];
|
|
60
|
+
const out = [];
|
|
61
|
+
for (const name of fs.readdirSync(dir)) {
|
|
62
|
+
if (name.startsWith('_')) continue;
|
|
63
|
+
const fdir = path.join(dir, name);
|
|
64
|
+
const validate = path.join(fdir, 'validate.md');
|
|
65
|
+
if (!fs.existsSync(validate)) continue;
|
|
66
|
+
let status = null;
|
|
67
|
+
const head = fs.readFileSync(validate, 'utf8').slice(0, 500);
|
|
68
|
+
const m = head.match(/^status:\s*(.+)$/m);
|
|
69
|
+
if (m) status = m[1].trim();
|
|
70
|
+
// newest receipt in proof/ (if any)
|
|
71
|
+
let proofAgeDays = null;
|
|
72
|
+
const proofDir = path.join(fdir, 'proof');
|
|
73
|
+
if (fs.existsSync(proofDir)) {
|
|
74
|
+
let newest = 0;
|
|
75
|
+
for (const p of fs.readdirSync(proofDir)) {
|
|
76
|
+
try { newest = Math.max(newest, fs.statSync(path.join(proofDir, p)).mtimeMs); } catch { /* skip */ }
|
|
77
|
+
}
|
|
78
|
+
if (newest) proofAgeDays = daysAgo(newest);
|
|
79
|
+
}
|
|
80
|
+
let verdict;
|
|
81
|
+
if (/blocked/i.test(status || '')) verdict = 'blocked';
|
|
82
|
+
else if (proofAgeDays != null && proofAgeDays <= STALE_DAYS) verdict = 'proven';
|
|
83
|
+
else if (proofAgeDays != null) verdict = 'stale';
|
|
84
|
+
else verdict = 'unproven';
|
|
85
|
+
out.push({ lane: name, status: status || '-', verdict, proofAgeDays });
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function loadHeartbeats() {
|
|
91
|
+
const hbDir = path.join(os.homedir(), '.atris', 'heartbeat');
|
|
92
|
+
const registry = readJson(path.join(hbDir, 'registry.json'));
|
|
93
|
+
const state = readJson(path.join(hbDir, 'state.json')) || {};
|
|
94
|
+
if (!registry || !Array.isArray(registry.jobs)) return [];
|
|
95
|
+
return registry.jobs.filter((j) => !j.disabled).map((j) => {
|
|
96
|
+
const s = state[j.id] || {};
|
|
97
|
+
const lastRun = s.last_run ? Date.parse(s.last_run) : null;
|
|
98
|
+
const ageDays = daysAgo(lastRun);
|
|
99
|
+
const expectedDays = ((j.cadence_minutes || 60) / 1440) * 3; // 3 missed cadences = stale
|
|
100
|
+
let verdict;
|
|
101
|
+
if (s.disabled) verdict = 'disabled';
|
|
102
|
+
else if ((s.consecutive_fails || 0) > 0) verdict = 'blocked';
|
|
103
|
+
else if (lastRun == null) verdict = 'never-ran';
|
|
104
|
+
else if (ageDays > Math.max(expectedDays, 0.05)) verdict = 'stale';
|
|
105
|
+
else verdict = 'proven';
|
|
106
|
+
return { id: j.id, verdict, lastRunAgeDays: ageDays, fails: s.consecutive_fails || 0 };
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function truthCommand(args = []) {
|
|
111
|
+
const cwd = process.cwd();
|
|
112
|
+
const json = args.includes('--json');
|
|
113
|
+
const summary = args.includes('--summary');
|
|
114
|
+
|
|
115
|
+
const missions = loadMissions(cwd);
|
|
116
|
+
const tasks = loadTaskCounts();
|
|
117
|
+
const features = loadFeatures(cwd);
|
|
118
|
+
const heartbeats = loadHeartbeats();
|
|
119
|
+
|
|
120
|
+
const featureTally = {};
|
|
121
|
+
for (const f of features) featureTally[f.verdict] = (featureTally[f.verdict] || 0) + 1;
|
|
122
|
+
const loopTally = {};
|
|
123
|
+
for (const h of heartbeats) loopTally[h.verdict] = (loopTally[h.verdict] || 0) + 1;
|
|
124
|
+
|
|
125
|
+
const result = {
|
|
126
|
+
ok: true,
|
|
127
|
+
generated_at: new Date().toISOString(),
|
|
128
|
+
missions_active: missions.map((m) => ({ id: m.id || m.mission_id, status: m.status, owner: m.owner })),
|
|
129
|
+
tasks,
|
|
130
|
+
features: featureTally,
|
|
131
|
+
loops: loopTally,
|
|
132
|
+
feature_rows: features,
|
|
133
|
+
loop_rows: heartbeats,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
if (json) {
|
|
137
|
+
console.log(JSON.stringify(result, null, 2));
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const line = (s) => console.log(s);
|
|
142
|
+
if (summary) {
|
|
143
|
+
line(`truth: features ${featureTally.proven || 0} proven / ${featureTally.stale || 0} stale / ${featureTally.blocked || 0} blocked / ${featureTally.unproven || 0} unproven · loops ${loopTally.proven || 0} live / ${(loopTally.stale || 0) + (loopTally['never-ran'] || 0)} stale / ${loopTally.blocked || 0} failing · missions ${missions.length} active`);
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
line('ATRIS TRUTH — live state, not belief\n');
|
|
148
|
+
|
|
149
|
+
line(`Missions active: ${missions.length}`);
|
|
150
|
+
for (const m of missions) line(` ${m.status.padEnd(8)} ${m.owner || '?'} ${m.id || m.mission_id}`);
|
|
151
|
+
|
|
152
|
+
if (tasks) {
|
|
153
|
+
line(`\nTasks: ${Object.entries(tasks).map(([k, v]) => `${v} ${k}`).join(', ')}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
line(`\nFeature lanes (${features.length}):`);
|
|
157
|
+
const order = { blocked: 0, stale: 1, unproven: 2, proven: 3 };
|
|
158
|
+
for (const f of features.sort((a, b) => (order[a.verdict] ?? 9) - (order[b.verdict] ?? 9))) {
|
|
159
|
+
line(` ${f.verdict.padEnd(9)} ${fmtAge(f.proofAgeDays).padStart(6)} ${f.lane}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
line(`\nLoops (${heartbeats.length}):`);
|
|
163
|
+
for (const h of heartbeats) {
|
|
164
|
+
line(` ${h.verdict.padEnd(9)} ${fmtAge(h.lastRunAgeDays).padStart(6)} ${h.id}${h.fails ? ` fails=${h.fails}` : ''}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
line(`\nVerdict key: proven = receipt within ${STALE_DAYS}d · stale = receipt older · unproven = no receipt ever · blocked = failing now`);
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = { truthCommand };
|
package/commands/xp.js
CHANGED
|
@@ -18,6 +18,8 @@ const CAREER_XP_CURSOR_FILE = path.join('.atris', 'state', 'career_xp.cursor.jso
|
|
|
18
18
|
const CAREER_XP_SESSIONS_DIR = path.join('.atris', 'state', 'career_xp_sessions');
|
|
19
19
|
const TASK_PROJECTION_FILE = path.join('.atris', 'state', 'tasks.projection.json');
|
|
20
20
|
const CODEX_STATE_FILE = path.join(os.homedir(), '.codex', 'state_5.sqlite');
|
|
21
|
+
// Codex moved native goals to goals_1.sqlite; thread metadata (cwd/title) stayed in state_5.sqlite.
|
|
22
|
+
const CODEX_GOALS_FILE = path.join(os.homedir(), '.codex', 'goals_1.sqlite');
|
|
21
23
|
const AGENT_XP_LABEL = 'AgentXP';
|
|
22
24
|
const AGENTXP_LEADERBOARD_URL = 'https://api.atris.ai/api/agentxp/leaderboard';
|
|
23
25
|
const LEVEL_XP = 1000;
|
|
@@ -1549,20 +1551,42 @@ function workspacePathCandidates(workspace) {
|
|
|
1549
1551
|
return [...new Set(candidates.filter(Boolean))];
|
|
1550
1552
|
}
|
|
1551
1553
|
|
|
1554
|
+
function codexHasThreadsTable(dbPath) {
|
|
1555
|
+
if (!dbPath || !fs.existsSync(dbPath)) return false;
|
|
1556
|
+
return runSqliteJsonOptional(dbPath, `SELECT name FROM sqlite_master WHERE type='table' AND name='threads' LIMIT 1;`).length > 0;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1552
1559
|
function readCodexGoalsForSession(args, workspace, sinceMs, untilMs) {
|
|
1553
|
-
const
|
|
1554
|
-
|
|
1555
|
-
|
|
1560
|
+
const explicitDb = readFlag(args, '--codex-state', readFlag(args, '--state', process.env.CODEX_STATE_DB || ''));
|
|
1561
|
+
const dbPath = explicitDb
|
|
1562
|
+
? path.resolve(expandHome(explicitDb))
|
|
1563
|
+
: path.resolve(fs.existsSync(CODEX_GOALS_FILE) ? CODEX_GOALS_FILE : CODEX_STATE_FILE);
|
|
1564
|
+
|
|
1565
|
+
// Goals live in goals_1.sqlite (no `threads` table); cwd/title metadata lives in state_5.sqlite.
|
|
1566
|
+
// Legacy single-file layouts (and test fixtures) keep both in one DB.
|
|
1567
|
+
let threadsTable = codexHasThreadsTable(dbPath) ? 'threads' : null;
|
|
1568
|
+
let attachPrefix = '';
|
|
1569
|
+
if (!threadsTable) {
|
|
1570
|
+
const threadsDb = CODEX_STATE_FILE !== dbPath ? CODEX_STATE_FILE : '';
|
|
1571
|
+
if (codexHasThreadsTable(threadsDb)) {
|
|
1572
|
+
threadsTable = 'tdb.threads';
|
|
1573
|
+
attachPrefix = `ATTACH ${sqlString(threadsDb)} AS tdb;\n`;
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1556
1577
|
const threadId = readFlag(args, '--thread', process.env.CODEX_THREAD_ID || '');
|
|
1557
1578
|
const clauses = [];
|
|
1558
1579
|
if (threadId) clauses.push(`tg.thread_id = ${sqlString(threadId)}`);
|
|
1559
1580
|
const workspaceCandidates = workspacePathCandidates(workspace);
|
|
1560
|
-
if (workspaceCandidates.length) {
|
|
1581
|
+
if (workspaceCandidates.length && threadsTable) {
|
|
1561
1582
|
clauses.push(`(t.cwd IN (${workspaceCandidates.map(sqlString).join(', ')}) AND tg.created_at_ms <= ${Number(untilMs)} AND (tg.updated_at_ms >= ${Number(sinceMs)} OR tg.status = 'active'))`);
|
|
1562
1583
|
}
|
|
1563
1584
|
if (!clauses.length) return [];
|
|
1564
1585
|
|
|
1565
|
-
const
|
|
1586
|
+
const join = threadsTable ? `LEFT JOIN ${threadsTable} t ON t.id = tg.thread_id` : '';
|
|
1587
|
+
const cwdCol = threadsTable ? 't.cwd' : 'NULL';
|
|
1588
|
+
const titleCol = threadsTable ? 't.title' : 'NULL';
|
|
1589
|
+
const rows = runSqliteJsonOptional(dbPath, `${attachPrefix}
|
|
1566
1590
|
SELECT
|
|
1567
1591
|
'codex' AS provider,
|
|
1568
1592
|
tg.thread_id,
|
|
@@ -1574,10 +1598,10 @@ SELECT
|
|
|
1574
1598
|
tg.time_used_seconds,
|
|
1575
1599
|
tg.created_at_ms,
|
|
1576
1600
|
tg.updated_at_ms,
|
|
1577
|
-
|
|
1578
|
-
|
|
1601
|
+
${cwdCol} AS thread_cwd,
|
|
1602
|
+
${titleCol} AS thread_title
|
|
1579
1603
|
FROM thread_goals tg
|
|
1580
|
-
|
|
1604
|
+
${join}
|
|
1581
1605
|
WHERE ${clauses.join(' OR ')}
|
|
1582
1606
|
ORDER BY tg.updated_at_ms DESC
|
|
1583
1607
|
LIMIT 25
|
package/commands/youtube.js
CHANGED
|
@@ -3,7 +3,11 @@ const { ensureValidCredentials } = require('../utils/auth');
|
|
|
3
3
|
const { spawnSync } = require('child_process');
|
|
4
4
|
const https = require('https');
|
|
5
5
|
|
|
6
|
-
const DEFAULT_QUERY =
|
|
6
|
+
const DEFAULT_QUERY = [
|
|
7
|
+
'Create a timestamped YouTube brief for Atris.',
|
|
8
|
+
'Include: metadata, timestamped outline, core claims with confidence, memorable examples, actionable takeaways, Atris/product implications, and next actions.',
|
|
9
|
+
'Use transcript timestamps whenever possible.',
|
|
10
|
+
].join(' ');
|
|
7
11
|
const DEFAULT_TIMEOUT_MS = 300000;
|
|
8
12
|
const LOCAL_TRANSCRIPT_MAX_BYTES = 5 * 1024 * 1024;
|
|
9
13
|
const LOCAL_TRANSCRIPT_MAX_CHARS = 250000;
|
|
@@ -18,7 +22,8 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
|
|
|
18
22
|
output(`Usage: ${commandName} process <youtube-url> [options]`);
|
|
19
23
|
output(` ${commandName} <youtube-url> [options]`);
|
|
20
24
|
output('');
|
|
21
|
-
output('Process a YouTube video through Atris using transcript-first analysis.');
|
|
25
|
+
output('Process a YouTube video through Atris using timestamped transcript-first analysis.');
|
|
26
|
+
output('Falls back to cloud video processing when local captions are unavailable.');
|
|
22
27
|
output('');
|
|
23
28
|
output('Options:');
|
|
24
29
|
output(' --query, -q <text> Focus question for the analysis');
|
|
@@ -28,6 +33,9 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
|
|
|
28
33
|
output(' --json Print the raw JSON response');
|
|
29
34
|
output(' -h, --help This help');
|
|
30
35
|
output('');
|
|
36
|
+
output('Default output contract:');
|
|
37
|
+
output(' metadata -> timestamped outline -> claims -> examples -> takeaways -> Atris implications -> next actions');
|
|
38
|
+
output('');
|
|
31
39
|
output('Examples:');
|
|
32
40
|
output(` ${commandName} https://www.youtube.com/watch?v=VIDEO_ID`);
|
|
33
41
|
output(` ${commandName} process https://youtu.be/VIDEO_ID --query "Key takeaways"`);
|
|
@@ -186,6 +194,34 @@ function chooseCaptionTrack(info = {}) {
|
|
|
186
194
|
return chooseFrom(info.subtitles) || chooseFrom(info.automatic_captions);
|
|
187
195
|
}
|
|
188
196
|
|
|
197
|
+
function formatTimestampFromMs(ms) {
|
|
198
|
+
const totalSeconds = Math.max(0, Math.floor(Number(ms || 0) / 1000));
|
|
199
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
200
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
201
|
+
const seconds = totalSeconds % 60;
|
|
202
|
+
const two = (value) => String(value).padStart(2, '0');
|
|
203
|
+
return hours > 0
|
|
204
|
+
? `${two(hours)}:${two(minutes)}:${two(seconds)}`
|
|
205
|
+
: `${two(minutes)}:${two(seconds)}`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function timestampedCaptionLine(text, startMs) {
|
|
209
|
+
const clean = String(text || '').replace(/\s+/g, ' ').trim();
|
|
210
|
+
if (!clean) return '';
|
|
211
|
+
if (!Number.isFinite(Number(startMs))) return clean;
|
|
212
|
+
return `[${formatTimestampFromMs(startMs)}] ${clean}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function parseVttTimestampMs(value) {
|
|
216
|
+
const match = String(value || '').match(/(?:(\d{2}):)?(\d{2}):(\d{2})\.(\d{3})/);
|
|
217
|
+
if (!match) return null;
|
|
218
|
+
const hours = Number(match[1] || 0);
|
|
219
|
+
const minutes = Number(match[2] || 0);
|
|
220
|
+
const seconds = Number(match[3] || 0);
|
|
221
|
+
const millis = Number(match[4] || 0);
|
|
222
|
+
return ((hours * 3600) + (minutes * 60) + seconds) * 1000 + millis;
|
|
223
|
+
}
|
|
224
|
+
|
|
189
225
|
function fetchCaptionText(urlString, redirects = 0) {
|
|
190
226
|
if (!captionHostAllowed(urlString)) {
|
|
191
227
|
return Promise.resolve(null);
|
|
@@ -246,15 +282,37 @@ function parseCaptionText(raw) {
|
|
|
246
282
|
.replace(/\s+/g, ' ')
|
|
247
283
|
.trim();
|
|
248
284
|
if (!text) continue;
|
|
249
|
-
|
|
250
|
-
segments.
|
|
285
|
+
const line = timestampedCaptionLine(text, Number(event.tStartMs));
|
|
286
|
+
if (segments[segments.length - 1] === line) continue;
|
|
287
|
+
segments.push(line);
|
|
251
288
|
}
|
|
252
|
-
return segments.join('
|
|
289
|
+
return segments.join('\n');
|
|
253
290
|
} catch {
|
|
254
291
|
return '';
|
|
255
292
|
}
|
|
256
293
|
}
|
|
257
294
|
|
|
295
|
+
if (/^WEBVTT/i.test(trimmed) || trimmed.includes('-->')) {
|
|
296
|
+
const segments = [];
|
|
297
|
+
for (const block of String(raw).split(/\r?\n\r?\n+/)) {
|
|
298
|
+
const lines = block.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
299
|
+
const timeLine = lines.find((line) => line.includes('-->'));
|
|
300
|
+
if (!timeLine) continue;
|
|
301
|
+
const startMs = parseVttTimestampMs(timeLine.split('-->')[0]);
|
|
302
|
+
const text = lines.slice(lines.indexOf(timeLine) + 1)
|
|
303
|
+
.filter((line) => !/^(NOTE|STYLE|REGION|Kind:|Language:)/.test(line))
|
|
304
|
+
.map((line) => line.replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim())
|
|
305
|
+
.filter(Boolean)
|
|
306
|
+
.filter((line, index, all) => index === 0 || line !== all[index - 1])
|
|
307
|
+
.join(' ')
|
|
308
|
+
.trim();
|
|
309
|
+
const captionLine = timestampedCaptionLine(text, startMs);
|
|
310
|
+
if (!captionLine || segments[segments.length - 1] === captionLine) continue;
|
|
311
|
+
segments.push(captionLine);
|
|
312
|
+
}
|
|
313
|
+
if (segments.length) return segments.join('\n');
|
|
314
|
+
}
|
|
315
|
+
|
|
258
316
|
const segments = [];
|
|
259
317
|
for (const line of String(raw).split(/\r?\n/)) {
|
|
260
318
|
const stripped = line.trim();
|
|
@@ -358,6 +416,14 @@ function formatYoutubeResult(data) {
|
|
|
358
416
|
lines.push(data?.message || 'YouTube video processed successfully');
|
|
359
417
|
if (metadata.title) lines.push(`Title: ${metadata.title}`);
|
|
360
418
|
if (metadata.channel) lines.push(`Channel: ${metadata.channel}`);
|
|
419
|
+
if (metadata.duration_seconds) lines.push(`Duration: ${formatTimestampFromMs(Number(metadata.duration_seconds) * 1000)}`);
|
|
420
|
+
if (metadata.processing_method || metadata.transcript_source) {
|
|
421
|
+
const method = metadata.processing_method || metadata.transcript_source;
|
|
422
|
+
const source = metadata.transcript_source && metadata.transcript_source !== method
|
|
423
|
+
? ` via ${metadata.transcript_source}`
|
|
424
|
+
: '';
|
|
425
|
+
lines.push(`Processing: ${method}${source}`);
|
|
426
|
+
}
|
|
361
427
|
if (data?.credits_used !== undefined || data?.credits_remaining !== undefined) {
|
|
362
428
|
const used = data.credits_used !== undefined ? data.credits_used : '?';
|
|
363
429
|
const remaining = data.credits_remaining !== undefined ? data.credits_remaining : '?';
|
|
@@ -393,5 +459,6 @@ module.exports = {
|
|
|
393
459
|
processYoutube,
|
|
394
460
|
shouldRetryWithLocalTranscript,
|
|
395
461
|
formatYoutubeResult,
|
|
462
|
+
formatTimestampFromMs,
|
|
396
463
|
youtubeCommand,
|
|
397
464
|
};
|
package/decks/README.md
CHANGED
|
@@ -23,28 +23,28 @@ atris deck compose --md notes.md --out decks/my.json --style narrative
|
|
|
23
23
|
atris deck compose --url https://youtu.be/VIDEO --out decks/my.json --theme ink
|
|
24
24
|
|
|
25
25
|
# Pre-build lint (schema shape + taste/clip)
|
|
26
|
-
atris deck lint decks/atris-
|
|
26
|
+
atris deck lint decks/atris-one-loop-pitch.json
|
|
27
27
|
|
|
28
28
|
# Build + open visual review packet (--review-auto also flags blank thumbnails)
|
|
29
|
-
atris deck build decks/atris-
|
|
29
|
+
atris deck build decks/atris-one-loop-pitch.json --title "Atris One Loop" --review
|
|
30
30
|
|
|
31
31
|
# Rebuild an existing deck in place (URL/id stay stable)
|
|
32
|
-
atris deck build decks/atris-
|
|
32
|
+
atris deck build decks/atris-one-loop-pitch.json --update 139zTe9cPOGttzYbW04adRU26VJz0SqWRwaZmkoVibP8
|
|
33
33
|
|
|
34
34
|
# Batch-build several specs
|
|
35
|
-
atris deck build decks/atris-
|
|
35
|
+
atris deck build decks/atris-one-loop-pitch.json decks/atris-antislop-pitch.json
|
|
36
36
|
|
|
37
37
|
# One command: video -> reviewed deck
|
|
38
38
|
atris deck video https://youtu.be/VIDEO --theme noir --review
|
|
39
39
|
|
|
40
40
|
# Re-fetch thumbnails for an existing deck
|
|
41
|
-
atris deck review 139zTe9cPOGttzYbW04adRU26VJz0SqWRwaZmkoVibP8 --spec decks/atris-
|
|
41
|
+
atris deck review 139zTe9cPOGttzYbW04adRU26VJz0SqWRwaZmkoVibP8 --spec decks/atris-one-loop-pitch.json
|
|
42
42
|
|
|
43
43
|
# After thumbnails pass review (emits a receipt for the action queue)
|
|
44
44
|
atris deck review 139zTe9cPOGttzYbW04adRU26VJz0SqWRwaZmkoVibP8 --confirm "slides 5+8 spacing fixed"
|
|
45
45
|
|
|
46
46
|
# Builds recorded for a spec
|
|
47
|
-
atris deck history decks/atris-
|
|
47
|
+
atris deck history decks/atris-one-loop-pitch.json
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
## Themes
|
|
@@ -76,13 +76,7 @@ List all: `atris deck themes`
|
|
|
76
76
|
|
|
77
77
|
| File | Purpose |
|
|
78
78
|
|------|---------|
|
|
79
|
-
| `atris-seed-pitch-v3.json` | Main investor pitch (10 slides, paper theme) |
|
|
80
79
|
| `atris-one-loop-pitch.json` | Outcome wedge deck (7 slides, terminal theme) |
|
|
81
|
-
| `yash-applied-compute-generalist.json` | Yash Patil / Own or Be Owned (ink, boxed template) |
|
|
82
|
-
| `yash-applied-compute-narrative.json` | Same story, typography-first (ink, no widgets) |
|
|
83
|
-
| `yash-applied-compute-detailed.json` | Full episode notes, 18 slides, bullets + cases |
|
|
84
|
-
| `mark-pincus-sourcery.json` | Mark Pincus / Life at the Speed of Play (noir, boxed) |
|
|
85
|
-
| `mark-pincus-narrative.json` | Same story, box-free narrative (12 slides, noir) |
|
|
86
80
|
| `atris-single-shot-proof.json` | Review loop proof deck (6 slides, single-pass quality bar) |
|
|
87
81
|
| `atris-archetypes-v2.json` | New archetypes showcase (8 slides, paper theme) |
|
|
88
82
|
| `atris-antislop-pitch.json` | 3-slide antislop product pitch |
|
|
@@ -92,6 +92,15 @@ function isAllowedGitDiffCheck(argv) {
|
|
|
92
92
|
return false;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
function isAllowedAtrisCleanDryRun(argv) {
|
|
96
|
+
return argv.length === 5
|
|
97
|
+
&& argv[0] === 'node'
|
|
98
|
+
&& argv[1] === 'bin/atris.js'
|
|
99
|
+
&& argv[2] === 'clean'
|
|
100
|
+
&& argv[3] === '--dry-run'
|
|
101
|
+
&& argv[4] === '--json';
|
|
102
|
+
}
|
|
103
|
+
|
|
95
104
|
function isInsidePath(candidate, root) {
|
|
96
105
|
const relative = path.relative(root, candidate);
|
|
97
106
|
return relative === '' || (relative && !relative.startsWith('..') && !path.isAbsolute(relative));
|
|
@@ -209,6 +218,7 @@ function parseVerifyCommand(verify) {
|
|
|
209
218
|
|| (/^scripts\/[a-zA-Z0-9_./-]+$/.test(first || '') && safeNodePathArgs(argv.slice(1)))
|
|
210
219
|
))
|
|
211
220
|
|| (bin === 'tsc' && argv.length === 1)
|
|
221
|
+
|| isAllowedAtrisCleanDryRun(argv)
|
|
212
222
|
|| isAllowedGitDiffCheck(argv);
|
|
213
223
|
if (!allowed) return { ok: false, reason: 'verify_command_not_allowed' };
|
|
214
224
|
return { ok: true, argv };
|