atris 3.25.2 → 3.26.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/bin/atris.js +31 -1
- package/commands/brain.js +4 -2
- package/commands/card.js +121 -0
- package/commands/deck.js +184 -0
- package/commands/reel.js +128 -0
- package/commands/run.js +34 -1
- package/commands/site.js +48 -0
- package/commands/slop.js +307 -0
- package/commands/task.js +23 -5
- package/commands/theme.js +217 -0
- package/lib/card.js +120 -0
- package/lib/deck-from-md.js +110 -0
- package/lib/html-render.js +257 -0
- package/lib/memory-view.js +95 -0
- package/lib/reel.js +52 -0
- package/lib/site.js +114 -0
- package/lib/slides-deck.js +237 -0
- package/lib/task-proof.js +35 -0
- package/lib/theme.js +264 -0
- package/package.json +1 -1
package/bin/atris.js
CHANGED
|
@@ -826,7 +826,7 @@ if (command === '2' && ['fast', 'pro'].includes(String(firstCommandArg || '').to
|
|
|
826
826
|
const knownCommands = ['init', 'log', 'now', 'radar', 'ctop', 'status', 'analytics', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', 'plan', 'do', 'review', 'release',
|
|
827
827
|
'activate', '_activate', 'agent', 'chat', 'fast', 'ax', 'console', 'serve', 'login', 'logout', 'whoami', 'switch', 'use', 'accounts', '_resolve', '_profile-email', '_switch-session', 'shell-init', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
|
|
828
828
|
'clean', 'verify', 'search', 'skill', 'member', 'codex-goal', 'app', 'apps', 'learn', 'lesson', 'plugin', 'experiments', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'live', 'align', 'terminal', 'computer', 'diff', 'business', 'sync', 'youtube',
|
|
829
|
-
'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'probe', 'worktree', 'aeo', 'improve', 'xp', 'play', 'gm', 'x', 'recap',
|
|
829
|
+
'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'probe', 'worktree', 'aeo', 'slop', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'xp', 'play', 'gm', 'x', 'recap',
|
|
830
830
|
'gmail', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
|
|
831
831
|
'fork', 'browse', 'publish', 'sleep', 'wake', 'feedback', 'errors', 'wiki', 'code-review', 'cr', 'soul', 'fleet', 'compile', 'spaceship'];
|
|
832
832
|
|
|
@@ -2059,6 +2059,36 @@ if (command === 'init') {
|
|
|
2059
2059
|
Promise.resolve(require('../commands/compile').compileCommand(subcommand, ...args))
|
|
2060
2060
|
.then(() => process.exit(process.exitCode || 0))
|
|
2061
2061
|
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
2062
|
+
} else if (command === 'slop') {
|
|
2063
|
+
// Slop: deterministic frontend-slop detector (no LLM). Exit 1 = slop found, for CI + the autopilot gate.
|
|
2064
|
+
Promise.resolve(require('../commands/slop').slopCommand(process.argv.slice(3)))
|
|
2065
|
+
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
2066
|
+
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
2067
|
+
} else if (command === 'deck') {
|
|
2068
|
+
// Deck: premium Google Slides from a plain content spec, via the Atris deck engine (anti-slop design system).
|
|
2069
|
+
Promise.resolve(require('../commands/deck').run(process.argv.slice(3)))
|
|
2070
|
+
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
2071
|
+
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
2072
|
+
} else if (command === 'site') {
|
|
2073
|
+
// Site: beautiful static site from a folder of markdown, in the anti-slop design system.
|
|
2074
|
+
Promise.resolve(require('../commands/site').run(process.argv.slice(3)))
|
|
2075
|
+
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
2076
|
+
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
2077
|
+
} else if (command === 'theme') {
|
|
2078
|
+
// Theme: brand themes (.atris/theme.json) for the whole design system (deck/html/site).
|
|
2079
|
+
Promise.resolve(require('../commands/theme').run(process.argv.slice(3)))
|
|
2080
|
+
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
2081
|
+
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
2082
|
+
} else if (command === 'card') {
|
|
2083
|
+
// Card: one line of text into an on-brand image (uses your theme + the design system).
|
|
2084
|
+
Promise.resolve(require('../commands/card').run(process.argv.slice(3)))
|
|
2085
|
+
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
2086
|
+
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
2087
|
+
} else if (command === 'reel') {
|
|
2088
|
+
// Reel: one line of text into a short on-brand video (an animated card; frames via Chrome + ffmpeg).
|
|
2089
|
+
Promise.resolve(require('../commands/reel').run(process.argv.slice(3)))
|
|
2090
|
+
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
2091
|
+
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
2062
2092
|
} else if (command === 'receipt' || command === 'proof' || command === 'openclaw') {
|
|
2063
2093
|
const subcommand = process.argv[3];
|
|
2064
2094
|
const args = process.argv.slice(4);
|
package/commands/brain.js
CHANGED
|
@@ -32,13 +32,15 @@ const CORE_STATE_FILES = [
|
|
|
32
32
|
'agents.jsonl',
|
|
33
33
|
'approvals.jsonl',
|
|
34
34
|
];
|
|
35
|
+
// Loop health measures loops that actually emit receipts. Channels with no writer
|
|
36
|
+
// anywhere in the codebase were dropped so the dashboard reports real failures, not
|
|
37
|
+
// never-built aspirations: "Overnight RL" duplicated the active Pulse AGI loop, and
|
|
38
|
+
// "Company YC" had no data source. "Master loop" is now emitted by `atris run`.
|
|
35
39
|
const LOOP_HEALTH_CHANNELS = [
|
|
36
40
|
{ label: 'Task plane', files: ['task_events.jsonl', 'tasks.projection.json'] },
|
|
37
|
-
{ label: 'Overnight RL', files: ['overnight_rl_self_heal.jsonl'] },
|
|
38
41
|
{ label: 'Career XP', files: ['career_xp_receipts.jsonl', 'career_xp.projection.json', 'gm_xp.projection.json'] },
|
|
39
42
|
{ label: 'Master loop', files: ['master_loop_events.jsonl'] },
|
|
40
43
|
{ label: 'Missions', files: ['mission_events.jsonl', 'missions.jsonl'] },
|
|
41
|
-
{ label: 'Company YC', files: ['company_yc_wow_events.jsonl', 'company_yc_wow_latest.json'] },
|
|
42
44
|
{ label: 'Codex goal', files: ['codex_goal.json'] },
|
|
43
45
|
{ label: 'Pulse AGI', files: ['pulse_agi_loop_receipts.jsonl'] },
|
|
44
46
|
];
|
package/commands/card.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// atris card — one line of text -> a beautiful, on-brand image (your theme).
|
|
2
|
+
//
|
|
3
|
+
// atris card "Ship faster" --kind statement --theme brand --size og
|
|
4
|
+
// atris card "It just works" --kind quote --by "a happy user"
|
|
5
|
+
// atris card --kind stat --number "10x" --label "faster reviews"
|
|
6
|
+
//
|
|
7
|
+
// Writes an .html (always) and a .png (when headless Chrome is available).
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const { execFileSync, spawnSync } = require('child_process');
|
|
13
|
+
const { buildCard, SIZES, KINDS } = require('../lib/card');
|
|
14
|
+
|
|
15
|
+
function parseFlags(argv) {
|
|
16
|
+
const flags = {}; const pos = [];
|
|
17
|
+
for (let i = 0; i < argv.length; i++) {
|
|
18
|
+
const a = argv[i];
|
|
19
|
+
if (a === '--html-only') { flags.htmlOnly = true; continue; }
|
|
20
|
+
if (a.startsWith('--')) {
|
|
21
|
+
const key = a.slice(2);
|
|
22
|
+
const next = argv[i + 1];
|
|
23
|
+
if (next != null && !next.startsWith('--')) { flags[key] = next; i++; } else flags[key] = true;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
pos.push(a);
|
|
27
|
+
}
|
|
28
|
+
return { flags, pos };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// find an installed Chrome/Chromium without adding a dependency
|
|
32
|
+
function findChrome() {
|
|
33
|
+
if (process.env.CHROME_PATH && fs.existsSync(process.env.CHROME_PATH)) return process.env.CHROME_PATH;
|
|
34
|
+
const macApps = [
|
|
35
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
36
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
37
|
+
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
38
|
+
];
|
|
39
|
+
for (const p of macApps) if (fs.existsSync(p)) return p;
|
|
40
|
+
for (const name of ['google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser', 'chrome']) {
|
|
41
|
+
const r = spawnSync('command', ['-v', name], { shell: true, encoding: 'utf8' });
|
|
42
|
+
if (r.status === 0 && r.stdout.trim()) return r.stdout.trim();
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function renderPng(chrome, html, width, height, outPng) {
|
|
48
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-card-'));
|
|
49
|
+
const htmlFile = path.join(tmp, 'card.html');
|
|
50
|
+
fs.writeFileSync(htmlFile, html);
|
|
51
|
+
const args = [
|
|
52
|
+
'--headless=new', '--disable-gpu', '--hide-scrollbars',
|
|
53
|
+
'--force-device-scale-factor=2',
|
|
54
|
+
`--window-size=${width},${height}`,
|
|
55
|
+
'--virtual-time-budget=4000',
|
|
56
|
+
`--screenshot=${outPng}`,
|
|
57
|
+
`file://${htmlFile}`,
|
|
58
|
+
];
|
|
59
|
+
execFileSync(chrome, args, { stdio: 'ignore', timeout: 60000 });
|
|
60
|
+
return fs.existsSync(outPng);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function run(argv) {
|
|
64
|
+
const { flags, pos } = parseFlags(argv);
|
|
65
|
+
|
|
66
|
+
if (pos[0] === 'help' || flags.help) {
|
|
67
|
+
console.log(`\n atris card — one line of text into an on-brand image\n
|
|
68
|
+
atris card "Your headline" [--kind statement|quote|stat] [--theme <name>] [--size og|wide|square|story]
|
|
69
|
+
flags: --sub --kicker --by --number --label --brand --version --out <file.png> --html-only\n
|
|
70
|
+
examples:
|
|
71
|
+
atris card "Design that builds itself" --kicker "Atris v3.23.0" --theme brand
|
|
72
|
+
atris card "It just works." --kind quote --by "a founder" --size square
|
|
73
|
+
atris card --kind stat --number "1260" --label "tests, all green"\n`);
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const text = pos.join(' ').trim();
|
|
78
|
+
const kind = flags.kind || 'statement';
|
|
79
|
+
if (!KINDS.includes(kind)) { console.error(` unknown kind "${kind}". try: ${KINDS.join(', ')}`); return 2; }
|
|
80
|
+
if (flags.size && !SIZES[flags.size]) { console.error(` unknown size "${flags.size}". try: ${Object.keys(SIZES).join(', ')}`); return 2; }
|
|
81
|
+
if (kind === 'stat' && !flags.number && !text) { console.error(' stat cards need --number (e.g. --number "10x")'); return 2; }
|
|
82
|
+
if (kind !== 'stat' && !text) { console.error(' give the card some text: atris card "Your headline"'); return 2; }
|
|
83
|
+
|
|
84
|
+
const spec = {
|
|
85
|
+
kind, text, headline: text,
|
|
86
|
+
theme: flags.theme, size: flags.size,
|
|
87
|
+
sub: flags.sub, kicker: flags.kicker, by: flags.by,
|
|
88
|
+
number: flags.number, label: flags.label,
|
|
89
|
+
brand: flags.brand, version: flags.version,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
let card;
|
|
93
|
+
try { card = buildCard(spec); }
|
|
94
|
+
catch (e) { console.error(` could not build card: ${e.message}`); return 1; }
|
|
95
|
+
|
|
96
|
+
const base = (flags.out ? String(flags.out).replace(/\.png$/i, '') : `card-${kind}-${card.theme}-${card.size}`);
|
|
97
|
+
const outPng = path.resolve(`${base}.png`);
|
|
98
|
+
const outHtml = path.resolve(`${base}.html`);
|
|
99
|
+
fs.writeFileSync(outHtml, card.html);
|
|
100
|
+
|
|
101
|
+
if (flags.htmlOnly) {
|
|
102
|
+
console.log(`\n ✓ ${card.kind} card (${card.width}x${card.height}, theme ${card.theme})\n html: ${outHtml}\n open it, or render a png with Chrome installed.\n`);
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const chrome = findChrome();
|
|
107
|
+
if (!chrome) {
|
|
108
|
+
console.log(`\n ✓ wrote html: ${outHtml}\n no Chrome found for png. open the html, or set CHROME_PATH and re-run.\n`);
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
renderPng(chrome, card.html, card.width, card.height, outPng);
|
|
113
|
+
console.log(`\n ✓ ${card.kind} card, ${card.width}x${card.height}, theme ${card.theme}\n ${outPng}\n`);
|
|
114
|
+
return 0;
|
|
115
|
+
} catch (e) {
|
|
116
|
+
console.log(`\n ! png render failed (${e.message.split('\n')[0]})\n html is ready: ${outHtml}\n`);
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = { run };
|
package/commands/deck.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// atris deck — generate a premium, on-brand Google Slides deck from a plain
|
|
2
|
+
// content spec, using the Atris deck engine (lib/slides-deck.js). The pitch:
|
|
3
|
+
// describe the deck, get the design system for free. No Arial-on-white slop.
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// atris deck themes list design themes
|
|
7
|
+
// atris deck build <spec.json> [--title T] [--theme terminal|paper] [--update ID]
|
|
8
|
+
// atris deck sample [--theme paper] print a starter spec to stdout
|
|
9
|
+
//
|
|
10
|
+
// A spec is JSON: { theme, brand:{name,accent}, slides:[ {type,...} ] }.
|
|
11
|
+
// Slide types: title, statement, columns, panel, chips, bignumber, close.
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const https = require('https');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
const { buildDeck, THEMES } = require('../lib/slides-deck');
|
|
17
|
+
const { parseMarkdownToSpec } = require('../lib/deck-from-md');
|
|
18
|
+
const { mergedThemes } = require('../lib/theme');
|
|
19
|
+
|
|
20
|
+
const BASE = 'api.atris.ai';
|
|
21
|
+
const PFX = '/api/integrations/google-slides';
|
|
22
|
+
|
|
23
|
+
function token() {
|
|
24
|
+
try { return require(os.homedir() + '/.atris/credentials.json').token; }
|
|
25
|
+
catch { return null; }
|
|
26
|
+
}
|
|
27
|
+
function api(method, path, body, tok) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const data = body ? JSON.stringify(body) : null;
|
|
30
|
+
const req = https.request({ host: BASE, path: PFX + path, method,
|
|
31
|
+
headers: { Authorization: 'Bearer ' + tok, 'Content-Type': 'application/json',
|
|
32
|
+
...(data ? { 'Content-Length': Buffer.byteLength(data) } : {}) } },
|
|
33
|
+
(res) => { let b = ''; res.on('data', (c) => (b += c)); res.on('end', () => {
|
|
34
|
+
let j; try { j = JSON.parse(b); } catch { j = b; }
|
|
35
|
+
if (res.statusCode >= 300) reject(new Error('HTTP ' + res.statusCode + ': ' + (typeof j === 'string' ? j : JSON.stringify(j)).slice(0, 600)));
|
|
36
|
+
else resolve(j); }); });
|
|
37
|
+
req.on('error', reject); if (data) req.write(data); req.end();
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const SAMPLE = {
|
|
42
|
+
theme: 'terminal',
|
|
43
|
+
brand: { name: 'Sentinel', accent: '.' },
|
|
44
|
+
slides: [
|
|
45
|
+
{ type: 'title', headline: 'Read your incidents in the **dark.**',
|
|
46
|
+
sub: "On-call shouldn't mean panic. Every alert ranked by blast radius, in one calm view.",
|
|
47
|
+
panel: { header: { title: 'Active incidents', meta: 'updated 12s ago' },
|
|
48
|
+
rows: [
|
|
49
|
+
{ title: 'Checkout latency spike', sub: 'api-gateway · us-east-1', value: '42%', valueSub: 'of traffic', sev: 0, active: true },
|
|
50
|
+
{ title: 'Stale read replica', sub: 'orders-db · eu-west-2', value: '8%', valueSub: 'of traffic', sev: 1 },
|
|
51
|
+
{ title: 'Elevated 4xx on search', sub: 'search-svc · global', value: '1.2%', valueSub: 'of traffic', sev: 2 },
|
|
52
|
+
], footer: { left: '3 active, 1 worth a page', right: 'View all' } } },
|
|
53
|
+
{ type: 'statement', text: "On-call shouldn't mean **panic.**",
|
|
54
|
+
sub: 'So the console is calm by default. One screen, ranked by real impact.' },
|
|
55
|
+
{ type: 'columns', heading: 'What makes it calm', columns: [
|
|
56
|
+
{ h: 'Ranked by impact', b: 'Severity comes from real blast radius, so the top of the list is the thing to fix.' },
|
|
57
|
+
{ h: 'Quiet by default', b: 'One page-worthy signal per incident. The rest stays in the log until you ask.' },
|
|
58
|
+
{ h: 'Built for 3am', b: 'High contrast, keyboard-first, and readable before you are fully awake.' } ] },
|
|
59
|
+
{ type: 'bignumber', number: '11 min', label: 'median time to first action', sub: 'down from 47 minutes before Sentinel.' },
|
|
60
|
+
{ type: 'close', tagline: 'Read your incidents in the dark.',
|
|
61
|
+
buttons: [{ label: 'Open the console', primary: true }, { label: 'Read the docs' }], footer: 'sentinel.sh · 2026' },
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// shared: spec -> live deck. Returns the URL.
|
|
66
|
+
async function publishDeck(spec, { title, updateId, tok }) {
|
|
67
|
+
const { requests } = buildDeck(spec, { themes: mergedThemes(THEMES) });
|
|
68
|
+
let id, firstSlide;
|
|
69
|
+
if (updateId) {
|
|
70
|
+
id = updateId;
|
|
71
|
+
const got = await api('GET', `/presentations/${id}`, null, tok);
|
|
72
|
+
const slides = got.slides || (got.presentation && got.presentation.slides) || [];
|
|
73
|
+
firstSlide = slides[0] && slides[0].objectId;
|
|
74
|
+
} else {
|
|
75
|
+
const pres = await api('POST', '/presentations', { title }, tok);
|
|
76
|
+
id = pres.presentationId || pres.id || (pres.presentation && pres.presentation.presentationId);
|
|
77
|
+
const slides = pres.slides || (pres.presentation && pres.presentation.slides) || [];
|
|
78
|
+
firstSlide = slides[0] && slides[0].objectId;
|
|
79
|
+
}
|
|
80
|
+
const reqs = firstSlide ? [...requests, { deleteObject: { objectId: firstSlide } }] : requests;
|
|
81
|
+
console.log(` building ${spec.slides.length} slides (${spec.theme}) · ${reqs.length} ops...`);
|
|
82
|
+
await api('POST', `/presentations/${id}/batch-update`, { requests: reqs }, tok);
|
|
83
|
+
return `https://docs.google.com/presentation/d/${id}/edit`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// beautiful HTML output (page or AppBlock JSON) from a content spec
|
|
87
|
+
function outputHtml(spec, argv, srcLabel) {
|
|
88
|
+
const { renderHtml, renderBlock, THEMES: HTML_THEMES } = require('../lib/html-render');
|
|
89
|
+
const themes = mergedThemes(HTML_THEMES);
|
|
90
|
+
if (!themes[spec.theme]) spec.theme = 'atris';
|
|
91
|
+
const title = flag(argv, '--title');
|
|
92
|
+
if (hasFlag(argv, '--block')) {
|
|
93
|
+
console.log(JSON.stringify(renderBlock(spec, { title, themes }), null, 2));
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
const html = renderHtml(spec, { title, themes });
|
|
97
|
+
const out = flag(argv, '--out');
|
|
98
|
+
if (out) { fs.writeFileSync(out, html); console.log(`\n ✓ html written: ${out}${srcLabel ? ` (from ${srcLabel})` : ''}\n`); }
|
|
99
|
+
else process.stdout.write(html + '\n');
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function run(argv) {
|
|
104
|
+
const sub = argv[0];
|
|
105
|
+
|
|
106
|
+
if (sub === 'from') {
|
|
107
|
+
const docPath = argv.slice(1).find((a) => !a.startsWith('-'));
|
|
108
|
+
if (!docPath) { console.error(' usage: atris deck from <doc.md> [--theme x] [--brand Name] [--build] [--title T]'); return 2; }
|
|
109
|
+
let md;
|
|
110
|
+
try { md = fs.readFileSync(docPath, 'utf8'); }
|
|
111
|
+
catch (e) { console.error(` cannot read doc: ${e.message}`); return 2; }
|
|
112
|
+
const spec = parseMarkdownToSpec(md, { theme: flag(argv, '--theme'), brandName: flag(argv, '--brand') });
|
|
113
|
+
if (hasFlag(argv, '--html') || hasFlag(argv, '--block')) return outputHtml(spec, argv, docPath);
|
|
114
|
+
{ const dt = mergedThemes(THEMES); if (!dt[spec.theme]) { console.error(` unknown theme "${spec.theme}". try: ${Object.keys(dt).join(', ')}`); return 2; } }
|
|
115
|
+
if (!hasFlag(argv, '--build')) {
|
|
116
|
+
// default: print the spec so the PM can tweak before building
|
|
117
|
+
console.log(JSON.stringify(spec, null, 2));
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
const tok = token();
|
|
121
|
+
if (!tok) { console.error(' no credentials at ~/.atris/credentials.json — run `atris login` and connect Google Drive.'); return 1; }
|
|
122
|
+
const title = flag(argv, '--title') || `${(spec.brand && spec.brand.name) || 'Atris'} deck`;
|
|
123
|
+
const url = await publishDeck(spec, { title, updateId: flag(argv, '--update'), tok });
|
|
124
|
+
console.log(`\n ✓ deck from ${docPath} ready: ${url}\n`);
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (sub === 'themes') {
|
|
129
|
+
console.log('\n atris deck themes:\n');
|
|
130
|
+
for (const [name, t] of Object.entries(THEMES)) {
|
|
131
|
+
console.log(` ${name.padEnd(10)} ${t.fonts.display} + ${t.fonts.body} · accent ${t.color.accent} bg ${t.color.bg}`);
|
|
132
|
+
}
|
|
133
|
+
console.log('\n slide types: title, statement, columns, panel, chips, bignumber, close\n');
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (sub === 'sample') {
|
|
138
|
+
const theme = flag(argv, '--theme') || 'terminal';
|
|
139
|
+
console.log(JSON.stringify({ ...SAMPLE, theme }, null, 2));
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (sub === 'build') {
|
|
144
|
+
const specPath = argv.slice(1).find((a) => !a.startsWith('-'));
|
|
145
|
+
if (!specPath) { console.error(' usage: atris deck build <spec.json> [--title T] [--theme x] [--update ID]'); return 2; }
|
|
146
|
+
let spec;
|
|
147
|
+
try { spec = JSON.parse(fs.readFileSync(specPath, 'utf8')); }
|
|
148
|
+
catch (e) { console.error(` cannot read spec: ${e.message}`); return 2; }
|
|
149
|
+
const themeOverride = flag(argv, '--theme'); if (themeOverride) spec.theme = themeOverride;
|
|
150
|
+
if (hasFlag(argv, '--html') || hasFlag(argv, '--block')) return outputHtml(spec, argv, specPath);
|
|
151
|
+
{ const dt = mergedThemes(THEMES); if (!dt[spec.theme]) { console.error(` unknown theme "${spec.theme}". try: ${Object.keys(dt).join(', ')}`); return 2; } }
|
|
152
|
+
const title = flag(argv, '--title') || `${(spec.brand && spec.brand.name) || 'Atris'} deck`;
|
|
153
|
+
|
|
154
|
+
const tok = token();
|
|
155
|
+
if (!tok) { console.error(' no credentials at ~/.atris/credentials.json — run `atris login` and connect Google Drive.'); return 1; }
|
|
156
|
+
|
|
157
|
+
const url = await publishDeck(spec, { title, updateId: flag(argv, '--update'), tok });
|
|
158
|
+
console.log(`\n ✓ deck ready: ${url}\n`);
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log(`
|
|
163
|
+
atris deck — premium Google Slides from a plain content spec or a markdown doc
|
|
164
|
+
|
|
165
|
+
atris deck from doc.md [--build] [--title T] turn a markdown doc into a deck
|
|
166
|
+
atris deck from doc.md --html --out page.html beautiful HTML page (theme: atris|terminal|paper)
|
|
167
|
+
atris deck from doc.md --block emit the AppBlock JSON for a web app
|
|
168
|
+
atris deck sample [--theme paper] > my.json start from a sample spec
|
|
169
|
+
atris deck build my.json [--title "Q3 review"] create the deck, print the URL
|
|
170
|
+
atris deck build my.json --html --out p.html render the spec as HTML instead of slides
|
|
171
|
+
atris deck themes list design themes
|
|
172
|
+
|
|
173
|
+
'from' maps headings to slides (## with bullets -> columns, "**X** label" -> a
|
|
174
|
+
big number, Close -> a closing slide). Without --build it prints the spec to tweak.
|
|
175
|
+
Design system is baked in: distinctive fonts, one accent, real data panels, and
|
|
176
|
+
no AI tells (em dashes sanitized, sentence-case labels, no gradient text).
|
|
177
|
+
`);
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function flag(argv, name) { const i = argv.indexOf(name); return i !== -1 ? argv[i + 1] : null; }
|
|
182
|
+
function hasFlag(argv, name) { return argv.includes(name); }
|
|
183
|
+
|
|
184
|
+
module.exports = { run, SAMPLE, publishDeck };
|
package/commands/reel.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// atris reel — one line of text into a short, on-brand video (an animated card).
|
|
2
|
+
//
|
|
3
|
+
// atris reel "Ship faster" --theme brand --size square
|
|
4
|
+
// atris reel "It just works." --kind quote --by "a founder" --seconds 3
|
|
5
|
+
//
|
|
6
|
+
// Renders frames with headless Chrome (the same one `atris card` uses) and encodes
|
|
7
|
+
// with ffmpeg. No new dependency. Falls back to a card image if either is missing.
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const { execFile, spawnSync } = require('child_process');
|
|
13
|
+
const { promisify } = require('util');
|
|
14
|
+
const pexec = promisify(execFile);
|
|
15
|
+
const { buildReelFrame, reelFrames } = require('../lib/reel');
|
|
16
|
+
const { SIZES, KINDS } = require('../lib/card');
|
|
17
|
+
|
|
18
|
+
function parseFlags(argv) {
|
|
19
|
+
const flags = {}; const pos = [];
|
|
20
|
+
for (let i = 0; i < argv.length; i++) {
|
|
21
|
+
const a = argv[i];
|
|
22
|
+
if (a.startsWith('--')) {
|
|
23
|
+
const key = a.slice(2); const next = argv[i + 1];
|
|
24
|
+
if (next != null && !next.startsWith('--')) { flags[key] = next; i++; } else flags[key] = true;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
pos.push(a);
|
|
28
|
+
}
|
|
29
|
+
return { flags, pos };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function findBin(macApps, names) {
|
|
33
|
+
for (const p of macApps) if (fs.existsSync(p)) return p;
|
|
34
|
+
for (const name of names) {
|
|
35
|
+
const r = spawnSync('command', ['-v', name], { shell: true, encoding: 'utf8' });
|
|
36
|
+
if (r.status === 0 && r.stdout.trim()) return r.stdout.trim();
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const findChrome = () => (process.env.CHROME_PATH && fs.existsSync(process.env.CHROME_PATH))
|
|
41
|
+
? process.env.CHROME_PATH
|
|
42
|
+
: findBin([
|
|
43
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
44
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
45
|
+
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
46
|
+
], ['google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser', 'chrome']);
|
|
47
|
+
const findFfmpeg = () => findBin(['/opt/homebrew/bin/ffmpeg', '/usr/local/bin/ffmpeg', '/usr/bin/ffmpeg'], ['ffmpeg']);
|
|
48
|
+
|
|
49
|
+
async function renderFrames(chrome, ts, spec, dir, w, h, onDone) {
|
|
50
|
+
const pad = (i) => String(i).padStart(4, '0');
|
|
51
|
+
let idx = 0; let done = 0;
|
|
52
|
+
async function worker() {
|
|
53
|
+
while (idx < ts.length) {
|
|
54
|
+
const i = idx++;
|
|
55
|
+
const { html } = buildReelFrame(spec, ts[i]);
|
|
56
|
+
const hf = path.join(dir, `f-${pad(i)}.html`);
|
|
57
|
+
const pf = path.join(dir, `f-${pad(i)}.png`);
|
|
58
|
+
fs.writeFileSync(hf, html);
|
|
59
|
+
await pexec(chrome, [
|
|
60
|
+
'--headless=new', '--disable-gpu', '--hide-scrollbars', '--force-device-scale-factor=1',
|
|
61
|
+
`--window-size=${w},${h}`, '--virtual-time-budget=2200',
|
|
62
|
+
`--user-data-dir=${path.join(dir, 'ud-' + i)}`,
|
|
63
|
+
`--screenshot=${pf}`, `file://${hf}`,
|
|
64
|
+
], { timeout: 60000 });
|
|
65
|
+
onDone && onDone(++done, ts.length);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const conc = Math.min(5, ts.length);
|
|
69
|
+
await Promise.all(Array.from({ length: conc }, worker));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function run(argv) {
|
|
73
|
+
const { flags, pos } = parseFlags(argv);
|
|
74
|
+
if (pos[0] === 'help' || flags.help) {
|
|
75
|
+
console.log(`\n atris reel — one line of text into a short on-brand video\n
|
|
76
|
+
atris reel "Your headline" [--kind statement|quote|stat] [--theme <name>] [--size square|og|wide|story] [--seconds 2.6]
|
|
77
|
+
flags: --sub --kicker --by --number --label --brand --version --out <file.mp4>\n`);
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const text = pos.join(' ').trim();
|
|
82
|
+
const kind = flags.kind || 'statement';
|
|
83
|
+
if (!KINDS.includes(kind)) { console.error(` unknown kind "${kind}". try: ${KINDS.join(', ')}`); return 2; }
|
|
84
|
+
const size = flags.size || 'square';
|
|
85
|
+
if (!SIZES[size]) { console.error(` unknown size "${size}". try: ${Object.keys(SIZES).join(', ')}`); return 2; }
|
|
86
|
+
if (kind === 'stat' && !flags.number && !text) { console.error(' stat reels need --number'); return 2; }
|
|
87
|
+
if (kind !== 'stat' && !text) { console.error(' give the reel some text: atris reel "Your headline"'); return 2; }
|
|
88
|
+
|
|
89
|
+
const seconds = Math.max(1, Math.min(8, parseFloat(flags.seconds) || 2.6));
|
|
90
|
+
const fps = 20;
|
|
91
|
+
const spec = {
|
|
92
|
+
kind, text, headline: text, theme: flags.theme, size,
|
|
93
|
+
sub: flags.sub, kicker: flags.kicker, by: flags.by,
|
|
94
|
+
number: flags.number, label: flags.label, brand: flags.brand, version: flags.version,
|
|
95
|
+
};
|
|
96
|
+
const { w, h } = SIZES[size];
|
|
97
|
+
const base = flags.out ? String(flags.out).replace(/\.mp4$/i, '') : `reel-${kind}-${flags.theme || 'atris'}-${size}`;
|
|
98
|
+
const outMp4 = path.resolve(`${base}.mp4`);
|
|
99
|
+
|
|
100
|
+
const chrome = findChrome();
|
|
101
|
+
if (!chrome) { console.error('\n no Chrome found to render frames. install Chrome or set CHROME_PATH.\n'); return 1; }
|
|
102
|
+
const ffmpeg = findFfmpeg();
|
|
103
|
+
if (!ffmpeg) { console.error('\n no ffmpeg found to encode the video. install ffmpeg (brew install ffmpeg).\n'); return 1; }
|
|
104
|
+
|
|
105
|
+
const ts = reelFrames(seconds, fps);
|
|
106
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-reel-'));
|
|
107
|
+
console.log(`\n rendering ${ts.length} frames (${seconds}s, ${size})...`);
|
|
108
|
+
try {
|
|
109
|
+
await renderFrames(chrome, ts, spec, dir, w, h, (d, n) => {
|
|
110
|
+
if (d === n || d % 10 === 0) process.stdout.write(`\r frames ${d}/${n} `);
|
|
111
|
+
});
|
|
112
|
+
process.stdout.write('\n encoding...\n');
|
|
113
|
+
await pexec(ffmpeg, [
|
|
114
|
+
'-y', '-framerate', String(fps), '-i', path.join(dir, 'f-%04d.png'),
|
|
115
|
+
'-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-movflags', '+faststart', outMp4,
|
|
116
|
+
]);
|
|
117
|
+
} catch (e) {
|
|
118
|
+
console.error(`\n render failed: ${String(e.message).split('\n')[0]}`); return 1;
|
|
119
|
+
} finally {
|
|
120
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!fs.existsSync(outMp4)) { console.error(' no video produced'); return 1; }
|
|
124
|
+
console.log(`\n ✓ ${kind} reel, ${w}x${h}, ${seconds}s, theme ${flags.theme || 'atris'}\n ${outMp4}\n`);
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = { run, findChrome, findFfmpeg };
|
package/commands/run.js
CHANGED
|
@@ -280,6 +280,35 @@ function hasWork(atrisDir) {
|
|
|
280
280
|
return false;
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Append a durable Master-loop receipt for one cycle's review verdict.
|
|
285
|
+
*
|
|
286
|
+
* Closes the loop: until now the validator's pass/fail lived only in RAM
|
|
287
|
+
* (carried to the next cycle's plan), so a crash lost it and `atris brain`
|
|
288
|
+
* could never see that the review step ran. This persists the verdict to
|
|
289
|
+
* .atris/state/master_loop_events.jsonl, the channel the brain reads for
|
|
290
|
+
* loop health. Best-effort: telemetry must never fail the run.
|
|
291
|
+
*/
|
|
292
|
+
function appendMasterLoopReceipt(receipt, stateDir = path.join(process.cwd(), '.atris', 'state')) {
|
|
293
|
+
try {
|
|
294
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
295
|
+
const row = {
|
|
296
|
+
ts: new Date().toISOString(),
|
|
297
|
+
run_stamp: receipt.runStamp || null,
|
|
298
|
+
cycle: receipt.cycle,
|
|
299
|
+
verdict: receipt.verdict,
|
|
300
|
+
plan_ms: receipt.timing ? receipt.timing.plan : null,
|
|
301
|
+
do_ms: receipt.timing ? receipt.timing.do : null,
|
|
302
|
+
review_ms: receipt.timing ? receipt.timing.review : null,
|
|
303
|
+
review_summary: String(receipt.reviewOutput || '').replace(/\s+/g, ' ').trim().slice(0, 280),
|
|
304
|
+
};
|
|
305
|
+
fs.appendFileSync(path.join(stateDir, 'master_loop_events.jsonl'), JSON.stringify(row) + '\n');
|
|
306
|
+
return row;
|
|
307
|
+
} catch (_) {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
283
312
|
/**
|
|
284
313
|
* Log completion to journal
|
|
285
314
|
*/
|
|
@@ -453,6 +482,10 @@ async function runAtris(options = {}) {
|
|
|
453
482
|
// Carry the review output into the next cycle's plan — closes the loop
|
|
454
483
|
lastReviewOutput = reviewOutput;
|
|
455
484
|
|
|
485
|
+
// Persist the verdict durably so the brain can see the review actually ran.
|
|
486
|
+
const verdict = reviewOutput.includes('[REVIEW_FAILED]') ? 'fail' : 'pass';
|
|
487
|
+
appendMasterLoopReceipt({ runStamp, cycle, verdict, timing, reviewOutput });
|
|
488
|
+
|
|
456
489
|
if (reviewOutput.includes('[REVIEW_FAILED]')) {
|
|
457
490
|
console.log(verbose
|
|
458
491
|
? '⚠ Review found issues. Stopping for manual check.'
|
|
@@ -989,4 +1022,4 @@ function diffRunLogs(args = []) {
|
|
|
989
1022
|
}
|
|
990
1023
|
}
|
|
991
1024
|
|
|
992
|
-
module.exports = { runAtris, getRunLogDir, getRunLogPath, writePhaseToRunLog, listRunLogs, pruneRunLogs, searchRunLogs, statsRunLogs, exportRunLogs, diffRunLogs, buildRunPrompt };
|
|
1025
|
+
module.exports = { runAtris, getRunLogDir, getRunLogPath, writePhaseToRunLog, appendMasterLoopReceipt, listRunLogs, pruneRunLogs, searchRunLogs, statsRunLogs, exportRunLogs, diffRunLogs, buildRunPrompt };
|
package/commands/site.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// atris site — turn a folder of markdown (docs, your wiki, memory) into a
|
|
2
|
+
// beautiful, navigable static site in the design system. Built on lib/site.js.
|
|
3
|
+
//
|
|
4
|
+
// atris site <dir|doc.md> [--out dist] [--theme atris|terminal|paper] [--title T] [--serve]
|
|
5
|
+
|
|
6
|
+
const { buildSite, serveSite } = require('../lib/site');
|
|
7
|
+
|
|
8
|
+
function flag(argv, name) { const i = argv.indexOf(name); return i !== -1 ? argv[i + 1] : null; }
|
|
9
|
+
function hasFlag(argv, name) { return argv.includes(name); }
|
|
10
|
+
|
|
11
|
+
async function run(argv) {
|
|
12
|
+
const input = argv.find((a) => !a.startsWith('-'));
|
|
13
|
+
if (!input || input === 'help' || hasFlag(argv, '--help')) {
|
|
14
|
+
console.log(`
|
|
15
|
+
atris site — a beautiful static site from a folder of markdown
|
|
16
|
+
|
|
17
|
+
atris site <dir|doc.md> [--out dist] [--theme atris|terminal|paper] [--title T]
|
|
18
|
+
atris site atris/wiki --title "Atris Wiki" --serve
|
|
19
|
+
|
|
20
|
+
Each .md becomes a page; an index links them all. Same anti-slop design system,
|
|
21
|
+
semantic data-atris-block sections, ready for the web app. --serve previews it.
|
|
22
|
+
`);
|
|
23
|
+
return input === 'help' || hasFlag(argv, '--help') ? 0 : 2;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let res;
|
|
27
|
+
try {
|
|
28
|
+
res = buildSite(input, {
|
|
29
|
+
out: flag(argv, '--out') || 'dist',
|
|
30
|
+
theme: flag(argv, '--theme'),
|
|
31
|
+
title: flag(argv, '--title'),
|
|
32
|
+
brand: flag(argv, '--brand'),
|
|
33
|
+
});
|
|
34
|
+
} catch (e) { console.error(` ${e.message}`); return 2; }
|
|
35
|
+
|
|
36
|
+
console.log(`\n ✓ site built: ${res.pages.length} page${res.pages.length === 1 ? '' : 's'} + index -> ${res.outDir}/`);
|
|
37
|
+
console.log(` open ${res.indexPath}`);
|
|
38
|
+
|
|
39
|
+
if (hasFlag(argv, '--serve')) {
|
|
40
|
+
const port = Number(flag(argv, '--port')) || 4321;
|
|
41
|
+
const { url } = await serveSite(res.outDir, port);
|
|
42
|
+
console.log(`\n serving at ${url} (ctrl-c to stop)\n`);
|
|
43
|
+
await new Promise(() => {}); // keep alive until killed
|
|
44
|
+
}
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { run };
|