atris 3.25.2 → 3.27.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/README.md +2 -0
- package/bin/atris.js +56 -13
- package/commands/activate.js +24 -0
- package/commands/brain.js +4 -2
- package/commands/card.js +121 -0
- package/commands/clarity.js +125 -0
- package/commands/deck.js +184 -0
- package/commands/moves.js +156 -0
- package/commands/reel.js +128 -0
- package/commands/run.js +34 -1
- package/commands/signup.js +101 -0
- 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/clarity.js +97 -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/next-moves.js +362 -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/utils/update-check.js +77 -24
package/README.md
CHANGED
|
@@ -319,6 +319,8 @@ atris upgrade # Install latest from npm
|
|
|
319
319
|
atris update # Sync local workspace files to new version
|
|
320
320
|
```
|
|
321
321
|
|
|
322
|
+
Packaged npm installs check for new versions during normal commands and start a background update automatically. Git checkout installs stay manual so linked development copies are not overwritten unexpectedly.
|
|
323
|
+
|
|
322
324
|
---
|
|
323
325
|
|
|
324
326
|
**License:** MIT | **Repo:** [github.com/atrislabs/atris](https://github.com/atrislabs/atris.git)
|
package/bin/atris.js
CHANGED
|
@@ -92,14 +92,10 @@ if (!skipUpdateCheck && (!updateCommand || (updateCommand && !['version', 'updat
|
|
|
92
92
|
.then((updateInfo) => {
|
|
93
93
|
// Show notification if update available (after command completes)
|
|
94
94
|
if (updateInfo) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
showUpdateNotification(updateInfo);
|
|
100
|
-
}
|
|
101
|
-
}, 100);
|
|
102
|
-
} else {
|
|
95
|
+
const autoUpdateStarted = autoUpdate(updateInfo, {
|
|
96
|
+
packageRoot: path.join(__dirname, '..'),
|
|
97
|
+
});
|
|
98
|
+
if (!autoUpdateStarted) {
|
|
103
99
|
showUpdateNotification(updateInfo);
|
|
104
100
|
}
|
|
105
101
|
}
|
|
@@ -644,6 +640,7 @@ function showUpgradeHelp() {
|
|
|
644
640
|
console.log('');
|
|
645
641
|
console.log('Description:');
|
|
646
642
|
console.log(' Check npm for the latest Atris CLI and install it globally if newer.');
|
|
643
|
+
console.log(' Normal packaged installs also auto-update in the background.');
|
|
647
644
|
console.log('');
|
|
648
645
|
console.log('Options:');
|
|
649
646
|
console.log(' --help, -h Show this help.');
|
|
@@ -826,7 +823,7 @@ if (command === '2' && ['fast', 'pro'].includes(String(firstCommandArg || '').to
|
|
|
826
823
|
const knownCommands = ['init', 'log', 'now', 'radar', 'ctop', 'status', 'analytics', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', 'plan', 'do', 'review', 'release',
|
|
827
824
|
'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
825
|
'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',
|
|
826
|
+
'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'probe', 'worktree', 'aeo', 'slop', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'xp', 'play', 'gm', 'x', 'recap', 'signup', 'clarity', 'moves',
|
|
830
827
|
'gmail', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
|
|
831
828
|
'fork', 'browse', 'publish', 'sleep', 'wake', 'feedback', 'errors', 'wiki', 'code-review', 'cr', 'soul', 'fleet', 'compile', 'spaceship'];
|
|
832
829
|
|
|
@@ -2059,6 +2056,52 @@ if (command === 'init') {
|
|
|
2059
2056
|
Promise.resolve(require('../commands/compile').compileCommand(subcommand, ...args))
|
|
2060
2057
|
.then(() => process.exit(process.exitCode || 0))
|
|
2061
2058
|
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
2059
|
+
} else if (command === 'slop') {
|
|
2060
|
+
// Slop: deterministic frontend-slop detector (no LLM). Exit 1 = slop found, for CI + the autopilot gate.
|
|
2061
|
+
Promise.resolve(require('../commands/slop').slopCommand(process.argv.slice(3)))
|
|
2062
|
+
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
2063
|
+
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
2064
|
+
} else if (command === 'deck') {
|
|
2065
|
+
// Deck: premium Google Slides from a plain content spec, via the Atris deck engine (anti-slop design system).
|
|
2066
|
+
Promise.resolve(require('../commands/deck').run(process.argv.slice(3)))
|
|
2067
|
+
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
2068
|
+
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
2069
|
+
} else if (command === 'site') {
|
|
2070
|
+
// Site: beautiful static site from a folder of markdown, in the anti-slop design system.
|
|
2071
|
+
Promise.resolve(require('../commands/site').run(process.argv.slice(3)))
|
|
2072
|
+
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
2073
|
+
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
2074
|
+
} else if (command === 'theme') {
|
|
2075
|
+
// Theme: brand themes (.atris/theme.json) for the whole design system (deck/html/site).
|
|
2076
|
+
Promise.resolve(require('../commands/theme').run(process.argv.slice(3)))
|
|
2077
|
+
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
2078
|
+
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
2079
|
+
} else if (command === 'card') {
|
|
2080
|
+
// Card: one line of text into an on-brand image (uses your theme + the design system).
|
|
2081
|
+
Promise.resolve(require('../commands/card').run(process.argv.slice(3)))
|
|
2082
|
+
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
2083
|
+
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
2084
|
+
} else if (command === 'reel') {
|
|
2085
|
+
// Reel: one line of text into a short on-brand video (an animated card; frames via Chrome + ffmpeg).
|
|
2086
|
+
Promise.resolve(require('../commands/reel').run(process.argv.slice(3)))
|
|
2087
|
+
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
2088
|
+
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
2089
|
+
} else if (command === 'signup') {
|
|
2090
|
+
// Signup: one-call seedless agent signup (POST /auth/agent/signup) → writes the
|
|
2091
|
+
// active profile so `atris play` works next. The install→signup→play seam.
|
|
2092
|
+
Promise.resolve(require('../commands/signup').signupCommand(process.argv.slice(3)))
|
|
2093
|
+
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
2094
|
+
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
2095
|
+
} else if (command === 'moves') {
|
|
2096
|
+
// Moves: your 3 next moves — approve one into the loop, kill, or skip.
|
|
2097
|
+
Promise.resolve(require('../commands/moves').movesCommand(process.argv.slice(3)))
|
|
2098
|
+
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
2099
|
+
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
2100
|
+
} else if (command === 'clarity') {
|
|
2101
|
+
// Clarity: interview yourself once; agents read how you work so you stop repeating it.
|
|
2102
|
+
Promise.resolve(require('../commands/clarity').clarityCommand(process.argv.slice(3)))
|
|
2103
|
+
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
2104
|
+
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
2062
2105
|
} else if (command === 'receipt' || command === 'proof' || command === 'openclaw') {
|
|
2063
2106
|
const subcommand = process.argv[3];
|
|
2064
2107
|
const args = process.argv.slice(4);
|
|
@@ -2137,8 +2180,8 @@ async function upgradeAtris() {
|
|
|
2137
2180
|
console.log('Installing update...');
|
|
2138
2181
|
console.log('');
|
|
2139
2182
|
|
|
2140
|
-
// Run npm
|
|
2141
|
-
const result = spawnSync('npm', ['
|
|
2183
|
+
// Run npm install -g atris@latest
|
|
2184
|
+
const result = spawnSync('npm', ['install', '-g', 'atris@latest'], {
|
|
2142
2185
|
stdio: 'inherit',
|
|
2143
2186
|
shell: true
|
|
2144
2187
|
});
|
|
@@ -2152,10 +2195,10 @@ async function upgradeAtris() {
|
|
|
2152
2195
|
} else {
|
|
2153
2196
|
console.log('');
|
|
2154
2197
|
console.log('✗ Upgrade failed. Try running manually:');
|
|
2155
|
-
console.log(' npm
|
|
2198
|
+
console.log(' npm install -g atris@latest');
|
|
2156
2199
|
console.log('');
|
|
2157
2200
|
console.log('If you see permission errors, try:');
|
|
2158
|
-
console.log(' sudo npm
|
|
2201
|
+
console.log(' sudo npm install -g atris@latest');
|
|
2159
2202
|
console.log('');
|
|
2160
2203
|
}
|
|
2161
2204
|
}
|
package/commands/activate.js
CHANGED
|
@@ -158,6 +158,30 @@ function activateAtris() {
|
|
|
158
158
|
wikiStatus.bullets.forEach((line) => console.log(`- ${line.replace(/^- /, '')}`));
|
|
159
159
|
}
|
|
160
160
|
console.log('');
|
|
161
|
+
try {
|
|
162
|
+
const { nextMoves } = require('../lib/next-moves');
|
|
163
|
+
const { readProfile, isEmptyProfile } = require('../lib/clarity');
|
|
164
|
+
const root = process.cwd();
|
|
165
|
+
const moves = nextMoves(root, 3);
|
|
166
|
+
console.log('Your next moves:');
|
|
167
|
+
if (moves.length) {
|
|
168
|
+
moves.forEach((m, i) => console.log(` ${i + 1}. ${m.title}`));
|
|
169
|
+
console.log(' steer them: atris moves');
|
|
170
|
+
} else {
|
|
171
|
+
console.log(' none queued. add to ROADMAP.md under "## Open loop items", or jot one with atris log');
|
|
172
|
+
}
|
|
173
|
+
const profile = readProfile(root);
|
|
174
|
+
console.log('');
|
|
175
|
+
if (isEmptyProfile(profile)) {
|
|
176
|
+
console.log('Tip: run atris clarity once so agents learn how you work.');
|
|
177
|
+
} else {
|
|
178
|
+
console.log('How you work (atris clarity):');
|
|
179
|
+
['focus', 'voice', 'cadence', 'done', 'leash']
|
|
180
|
+
.filter((k) => profile[k])
|
|
181
|
+
.forEach((k) => console.log(` ${k}: ${profile[k]}`));
|
|
182
|
+
}
|
|
183
|
+
console.log('');
|
|
184
|
+
} catch { /* alive onboarding is best-effort; never block activate */ }
|
|
161
185
|
console.log('Next: atris plan → do → review (or atris log)');
|
|
162
186
|
console.log('');
|
|
163
187
|
}
|
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 };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// `atris clarity`: interview the operator for how they work, write it down once.
|
|
4
|
+
// The interview moves one question at a time so you stay in flow. The result is
|
|
5
|
+
// a durable profile (.atris/clarity.json + atris/CLARITY.md) that agents read.
|
|
6
|
+
|
|
7
|
+
const readline = require('readline');
|
|
8
|
+
const {
|
|
9
|
+
QUESTIONS,
|
|
10
|
+
KEYS,
|
|
11
|
+
mergeProfile,
|
|
12
|
+
isEmptyProfile,
|
|
13
|
+
renderClarityMd,
|
|
14
|
+
readProfile,
|
|
15
|
+
writeProfile,
|
|
16
|
+
profilePaths,
|
|
17
|
+
} = require('../lib/clarity');
|
|
18
|
+
|
|
19
|
+
function parseSets(args) {
|
|
20
|
+
const answers = {};
|
|
21
|
+
for (let i = 0; i < args.length; i++) {
|
|
22
|
+
if (args[i] === '--set' && args[i + 1]) {
|
|
23
|
+
const eq = args[i + 1].indexOf('=');
|
|
24
|
+
if (eq > 0) {
|
|
25
|
+
const key = args[i + 1].slice(0, eq).trim();
|
|
26
|
+
const val = args[i + 1].slice(eq + 1).trim();
|
|
27
|
+
if (KEYS.includes(key)) answers[key] = val;
|
|
28
|
+
}
|
|
29
|
+
i++;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return answers;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function ask(rl, question) {
|
|
36
|
+
return new Promise((resolve) => rl.question(question, (a) => resolve(a)));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function clarityCommand(args = [], root = process.cwd()) {
|
|
40
|
+
const sub = args[0];
|
|
41
|
+
const stamp = new Date().toISOString();
|
|
42
|
+
|
|
43
|
+
if (args.includes('--help') || args.includes('-h') || sub === 'help') {
|
|
44
|
+
console.log('');
|
|
45
|
+
console.log('Usage: atris clarity [show|--json|--set key=value|--reset]');
|
|
46
|
+
console.log('');
|
|
47
|
+
console.log('Interview the operator for how they work, once. Agents read the result.');
|
|
48
|
+
console.log('');
|
|
49
|
+
console.log(' atris clarity Run the interview (one question at a time)');
|
|
50
|
+
console.log(' atris clarity show Show the current profile');
|
|
51
|
+
console.log(' atris clarity --json Print the profile as JSON');
|
|
52
|
+
console.log(' atris clarity --set voice=plain Set one field without prompting');
|
|
53
|
+
console.log(' atris clarity --reset Clear the profile');
|
|
54
|
+
console.log('');
|
|
55
|
+
console.log(`Fields: ${KEYS.join(', ')}`);
|
|
56
|
+
console.log('');
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (args.includes('--reset')) {
|
|
61
|
+
writeProfile(root, { updated_at: stamp });
|
|
62
|
+
console.log('clarity profile reset.');
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const existing = readProfile(root);
|
|
67
|
+
|
|
68
|
+
if (args.includes('--json')) {
|
|
69
|
+
console.log(JSON.stringify(existing, null, 2));
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (sub === 'show') {
|
|
74
|
+
console.log(renderClarityMd(existing));
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Non-interactive set: write fields and exit.
|
|
79
|
+
const sets = parseSets(args);
|
|
80
|
+
if (Object.keys(sets).length) {
|
|
81
|
+
const merged = mergeProfile(existing, sets, stamp);
|
|
82
|
+
// Only the keys whose values actually landed (mergeProfile drops empties).
|
|
83
|
+
const changed = Object.keys(sets).filter((k) => sets[k].trim() && merged[k] === sets[k].trim());
|
|
84
|
+
if (!changed.length) {
|
|
85
|
+
console.log('nothing to set (empty value ignored). use --reset to clear all.');
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
const { md } = writeProfile(root, merged);
|
|
89
|
+
console.log(`saved ${changed.join(', ')} to ${md.replace(`${root}/`, '')}`);
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Interactive interview. Only on a terminal, so spawns never hang.
|
|
94
|
+
if (!process.stdin.isTTY) {
|
|
95
|
+
console.log(renderClarityMd(existing));
|
|
96
|
+
if (isEmptyProfile(existing)) {
|
|
97
|
+
console.log('Run `atris clarity` in a terminal to fill this in, or use --set key=value.');
|
|
98
|
+
}
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log('');
|
|
103
|
+
console.log('clarity interview. plain answers, one at a time. enter to keep the current value.');
|
|
104
|
+
console.log('');
|
|
105
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
106
|
+
const answers = {};
|
|
107
|
+
for (const { key, q } of QUESTIONS) {
|
|
108
|
+
const current = existing[key] ? ` [${existing[key]}]` : '';
|
|
109
|
+
// eslint-disable-next-line no-await-in-loop
|
|
110
|
+
const a = (await ask(rl, `${q}${current}\n> `)).trim();
|
|
111
|
+
if (a) answers[key] = a;
|
|
112
|
+
console.log('');
|
|
113
|
+
}
|
|
114
|
+
rl.close();
|
|
115
|
+
|
|
116
|
+
const merged = mergeProfile(existing, answers, stamp);
|
|
117
|
+
const { md } = writeProfile(root, merged);
|
|
118
|
+
console.log('clarity saved. agents will read it from:');
|
|
119
|
+
console.log(` ${md.replace(`${root}/`, '')}`);
|
|
120
|
+
console.log('');
|
|
121
|
+
console.log(renderClarityMd(merged));
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = { clarityCommand, parseSets };
|
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 };
|