atris 3.30.1 → 3.30.2
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 +6 -0
- package/atris/AGENTS.md +11 -0
- package/atris/CLAUDE.md +5 -0
- package/atris/atris.md +19 -4
- package/atris/policies/atris-design.md +71 -0
- package/atris/policies/design-seed.md +187 -0
- package/atris/skills/atris/SKILL.md +26 -3
- package/atris/skills/design/SKILL.md +3 -2
- package/atris/skills/loop/SKILL.md +5 -3
- package/atris/team/_template/MEMBER.md +19 -0
- package/atris.md +63 -23
- package/ax +1434 -1698
- package/bin/atris.js +5 -1
- package/commands/agent-spawn.js +2 -2
- package/commands/brain.js +92 -7
- package/commands/brainstorm.js +62 -22
- package/commands/business-sync.js +92 -8
- package/commands/business.js +13 -7
- package/commands/chat-scan.js +102 -0
- package/commands/computer.js +758 -15
- package/commands/deck.js +505 -105
- package/commands/init.js +6 -1
- package/commands/launchpad.js +638 -0
- package/commands/log-sync.js +44 -13
- package/commands/loop-front.js +290 -0
- package/commands/member.js +124 -3
- package/commands/mission.js +653 -30
- package/commands/pull.js +37 -28
- package/commands/pulse.js +11 -8
- package/commands/run.js +79 -39
- package/commands/sync.js +36 -17
- package/commands/task.js +342 -66
- package/commands/xp.js +3 -0
- package/commands/youtube.js +221 -7
- package/decks/README.md +89 -0
- package/decks/archetype-catalog.json +180 -0
- package/decks/atris-antislop-pitch.json +48 -0
- package/decks/atris-archetypes-v2.json +83 -0
- package/decks/atris-one-loop-pitch.json +80 -0
- package/decks/atris-seed-pitch-v3.json +118 -0
- package/decks/atris-seed-pitch-v4-skeleton.json +106 -0
- package/decks/atris-seed-pitch-v5.json +109 -0
- package/decks/atris-seed-pitch-v6.json +137 -0
- package/decks/atris-seed-pitch-v7.json +133 -0
- package/decks/atris-single-shot-proof.json +74 -0
- package/decks/mark-pincus-narrative.json +102 -0
- package/decks/mark-pincus-sourcery.json +94 -0
- package/decks/yash-applied-compute-detailed.json +150 -0
- package/decks/yash-applied-compute-generalist.json +82 -0
- package/decks/yash-applied-compute-narrative.json +54 -0
- package/lib/ax-prefs.js +5 -12
- package/lib/chat-log-scan.js +377 -0
- package/lib/context-gatherer.js +35 -1
- package/lib/deck-compose.js +145 -0
- package/lib/deck-history.js +64 -0
- package/lib/deck-layout.js +169 -0
- package/lib/deck-review.js +431 -0
- package/lib/deck-schema.js +154 -0
- package/lib/file-ops.js +2 -2
- package/lib/functional-owner.js +189 -0
- package/lib/slides-deck.js +512 -58
- package/lib/task-db.js +109 -2
- package/package.json +2 -1
- package/templates/business-starter/team/START_HERE.md +12 -8
- package/utils/auth.js +4 -0
- package/utils/config.js +4 -0
- package/atris/atrisDev.md +0 -717
package/commands/deck.js
CHANGED
|
@@ -4,133 +4,302 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Usage:
|
|
6
6
|
// atris deck themes list design themes
|
|
7
|
-
// atris deck build <spec.json> [--title T] [--theme terminal|paper] [--
|
|
8
|
-
// atris deck
|
|
7
|
+
// atris deck build <spec.json> [--title T] [--theme terminal|paper|ink|noir] [--review]
|
|
8
|
+
// atris deck lint <spec.json> pre-build layout warnings
|
|
9
|
+
// atris deck review <id|url> [--spec path] [--confirm NOTE] [--json]
|
|
10
|
+
// atris deck sample [--theme paper] print a starter spec
|
|
9
11
|
//
|
|
10
12
|
// A spec is JSON: { theme, brand:{name,accent}, slides:[ {type,...} ] }.
|
|
11
|
-
// Slide types: title, statement, columns, panel, chips, bignumber, close
|
|
13
|
+
// Slide types: title, statement, columns, panel, chips, bignumber, close,
|
|
14
|
+
// timeline, versus, metricgrid, receipt, stack, quote, hero.
|
|
12
15
|
|
|
13
16
|
const fs = require('fs');
|
|
14
17
|
const https = require('https');
|
|
15
18
|
const os = require('os');
|
|
16
|
-
const
|
|
17
|
-
const {
|
|
18
|
-
const {
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const { buildDeck, THEMES, notesRequests } = require('../lib/slides-deck');
|
|
21
|
+
const {
|
|
22
|
+
lintSpec,
|
|
23
|
+
reviewDeck,
|
|
24
|
+
confirmReview,
|
|
25
|
+
extractPresentationId,
|
|
26
|
+
DEFAULT_OUT_ROOT,
|
|
27
|
+
} = require('../lib/deck-review');
|
|
28
|
+
const { validateSpec, checkSpec, hasErrors } = require('../lib/deck-schema');
|
|
29
|
+
const { recordBuild, historyFor } = require('../lib/deck-history');
|
|
30
|
+
const { composeSpec } = require('../lib/deck-compose');
|
|
19
31
|
|
|
20
32
|
const BASE = 'api.atris.ai';
|
|
21
33
|
const PFX = '/api/integrations/google-slides';
|
|
22
34
|
|
|
23
35
|
function token() {
|
|
24
|
-
try { return require(os.homedir()
|
|
36
|
+
try { return require(path.join(os.homedir(), '.atris/credentials.json')).token; }
|
|
25
37
|
catch { return null; }
|
|
26
38
|
}
|
|
27
|
-
|
|
39
|
+
|
|
40
|
+
function api(method, apiPath, body, tok) {
|
|
28
41
|
return new Promise((resolve, reject) => {
|
|
29
42
|
const data = body ? JSON.stringify(body) : null;
|
|
30
|
-
const req = https.request({
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
const req = https.request({
|
|
44
|
+
host: BASE,
|
|
45
|
+
path: PFX + apiPath,
|
|
46
|
+
method,
|
|
47
|
+
headers: {
|
|
48
|
+
Authorization: `Bearer ${tok}`,
|
|
49
|
+
'Content-Type': 'application/json',
|
|
50
|
+
...(data ? { 'Content-Length': Buffer.byteLength(data) } : {}),
|
|
51
|
+
},
|
|
52
|
+
}, (res) => {
|
|
53
|
+
let b = '';
|
|
54
|
+
res.on('data', (c) => { b += c; });
|
|
55
|
+
res.on('end', () => {
|
|
56
|
+
let j;
|
|
57
|
+
try { j = JSON.parse(b); } catch { j = b; }
|
|
58
|
+
if (res.statusCode >= 300) {
|
|
59
|
+
reject(new Error(`HTTP ${res.statusCode}: ${(typeof j === 'string' ? j : JSON.stringify(j)).slice(0, 600)}`));
|
|
60
|
+
} else {
|
|
61
|
+
resolve(j);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
req.on('error', reject);
|
|
66
|
+
if (data) req.write(data);
|
|
67
|
+
req.end();
|
|
38
68
|
});
|
|
39
69
|
}
|
|
40
70
|
|
|
71
|
+
const THEME_HINTS = {
|
|
72
|
+
terminal: 'warm dark · investor / product wedge',
|
|
73
|
+
paper: 'warm light · editorial default',
|
|
74
|
+
ink: 'stark light · magazine / press',
|
|
75
|
+
noir: 'cool dark · podcast / interview',
|
|
76
|
+
};
|
|
77
|
+
|
|
41
78
|
const SAMPLE = {
|
|
42
79
|
theme: 'terminal',
|
|
43
80
|
brand: { name: 'Sentinel', accent: '.' },
|
|
44
81
|
slides: [
|
|
45
|
-
{
|
|
82
|
+
{
|
|
83
|
+
type: 'title',
|
|
84
|
+
headline: 'Read your incidents in the **dark.**',
|
|
46
85
|
sub: "On-call shouldn't mean panic. Every alert ranked by blast radius, in one calm view.",
|
|
47
|
-
panel: {
|
|
86
|
+
panel: {
|
|
87
|
+
header: { title: 'Active incidents', meta: 'updated 12s ago' },
|
|
48
88
|
rows: [
|
|
49
89
|
{ title: 'Checkout latency spike', sub: 'api-gateway · us-east-1', value: '42%', valueSub: 'of traffic', sev: 0, active: true },
|
|
50
90
|
{ title: 'Stale read replica', sub: 'orders-db · eu-west-2', value: '8%', valueSub: 'of traffic', sev: 1 },
|
|
51
91
|
{ title: 'Elevated 4xx on search', sub: 'search-svc · global', value: '1.2%', valueSub: 'of traffic', sev: 2 },
|
|
52
|
-
],
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
92
|
+
],
|
|
93
|
+
footer: { left: '3 active, 1 worth a page', right: 'View all' },
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
{ type: 'statement', text: "On-call shouldn't mean **panic.**", sub: 'So the console is calm by default. One screen, ranked by real impact.' },
|
|
97
|
+
{
|
|
98
|
+
type: 'columns',
|
|
99
|
+
heading: 'What makes it calm',
|
|
100
|
+
columns: [
|
|
101
|
+
{ h: 'Ranked by impact', b: 'Severity comes from real blast radius, so the top of the list is the thing to fix.' },
|
|
102
|
+
{ h: 'Quiet by default', b: 'One page-worthy signal per incident. The rest stays in the log until you ask.' },
|
|
103
|
+
{ h: 'Built for 3am', b: 'High contrast, keyboard-first, and readable before you are fully awake.' },
|
|
104
|
+
],
|
|
105
|
+
},
|
|
59
106
|
{ type: 'bignumber', number: '11 min', label: 'median time to first action', sub: 'down from 47 minutes before Sentinel.' },
|
|
60
|
-
{
|
|
61
|
-
|
|
107
|
+
{
|
|
108
|
+
type: 'close',
|
|
109
|
+
tagline: 'Read your incidents in the dark.',
|
|
110
|
+
buttons: [{ label: 'Open the console', primary: true }, { label: 'Read the docs' }],
|
|
111
|
+
footer: 'sentinel.sh · 2026',
|
|
112
|
+
},
|
|
62
113
|
],
|
|
63
114
|
};
|
|
64
115
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
116
|
+
function flag(argv, name) {
|
|
117
|
+
const i = argv.indexOf(name);
|
|
118
|
+
return i !== -1 ? argv[i + 1] : null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function hasFlag(argv, name) {
|
|
122
|
+
return argv.includes(name);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Flags that take a value, so their following token is consumed (not a path).
|
|
126
|
+
const VALUE_FLAGS = new Set(['--theme', '--title', '--update', '--out', '--style', '--url', '--md']);
|
|
127
|
+
|
|
128
|
+
// Positional args, skipping boolean flags and value-flag values. Lets batch
|
|
129
|
+
// build collect real spec paths even when flags like `--theme noir` precede them.
|
|
130
|
+
function positionals(argv) {
|
|
131
|
+
const out = [];
|
|
132
|
+
for (let i = 0; i < argv.length; i++) {
|
|
133
|
+
const a = argv[i];
|
|
134
|
+
if (a.startsWith('-')) { if (VALUE_FLAGS.has(a)) i += 1; continue; }
|
|
135
|
+
out.push(a);
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function readSpec(specPath) {
|
|
141
|
+
return JSON.parse(fs.readFileSync(specPath, 'utf8'));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function printLint(findings) {
|
|
145
|
+
if (!findings.length) {
|
|
146
|
+
console.log('\n ✓ spec lint clean\n');
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
console.log('');
|
|
150
|
+
for (const f of findings) {
|
|
151
|
+
const icon = f.severity === 'error' ? '✗' : f.severity === 'warn' ? '⚠' : '·';
|
|
152
|
+
console.log(` ${icon} slide ${f.slide} ${f.rule.padEnd(22)} ${f.message}`);
|
|
153
|
+
}
|
|
154
|
+
console.log('');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function runReviewFlow({ presentationId, spec, specPath, json, auto = false }) {
|
|
158
|
+
const tok = token();
|
|
159
|
+
if (!tok) {
|
|
160
|
+
console.error(' no credentials at ~/.atris/credentials.json — run `atris login` and connect Google Drive.');
|
|
161
|
+
return 1;
|
|
162
|
+
}
|
|
163
|
+
const { packet, manifestPath, outDir } = await reviewDeck({
|
|
164
|
+
presentationId,
|
|
165
|
+
spec,
|
|
166
|
+
specPath,
|
|
167
|
+
api,
|
|
168
|
+
token: tok,
|
|
169
|
+
auto,
|
|
170
|
+
});
|
|
171
|
+
if (json) {
|
|
172
|
+
console.log(JSON.stringify({ ...packet, manifestPath, outDir }, null, 2));
|
|
173
|
+
} else {
|
|
174
|
+
console.log(`\n review packet: ${manifestPath}`);
|
|
175
|
+
console.log(` thumbnails: ${outDir}`);
|
|
176
|
+
console.log(` status: ${packet.status}`);
|
|
177
|
+
console.log(` slides: ${packet.slides.length}`);
|
|
178
|
+
if (packet.autoReview) {
|
|
179
|
+
const ar = packet.autoReview;
|
|
180
|
+
console.log(` auto review: ${ar.passed}/${ar.results.length} thumbnails ok${ar.failed ? ` · ${ar.failed} FLAGGED` : ''}`);
|
|
181
|
+
for (const r of ar.results.filter((x) => !x.ok)) {
|
|
182
|
+
console.log(` ✗ slide ${r.index}${r.type ? ` (${r.type})` : ''}: ${r.reason}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (packet.specLint.length) {
|
|
186
|
+
printLint(packet.specLint);
|
|
187
|
+
} else {
|
|
188
|
+
console.log(' spec lint: clean');
|
|
189
|
+
}
|
|
190
|
+
console.log(' next: open PNGs, fix spec if needed, then:');
|
|
191
|
+
console.log(` atris deck review ${packet.presentationId} --confirm "looks good"\n`);
|
|
192
|
+
}
|
|
193
|
+
return 0;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Pure: given the engine's build requests and what the API reports, produce the
|
|
197
|
+
// final batch-update request list.
|
|
198
|
+
// - update: delete every existing slide first, then add the fresh ones. The
|
|
199
|
+
// engine reuses fixed objectIds (deck_slide_N), so a prior build's slides
|
|
200
|
+
// must be removed before recreating them or the API rejects duplicate IDs.
|
|
201
|
+
// - create: append a delete of the blank default slide a new deck ships with.
|
|
202
|
+
function planBatch({ requests, updateId = null, existingSlides = [], newFirstSlideId = null }) {
|
|
203
|
+
if (updateId) {
|
|
204
|
+
const deletions = (existingSlides || [])
|
|
205
|
+
.filter((s) => s && s.objectId)
|
|
206
|
+
.map((s) => ({ deleteObject: { objectId: s.objectId } }));
|
|
207
|
+
return [...deletions, ...requests];
|
|
208
|
+
}
|
|
209
|
+
return newFirstSlideId ? [...requests, { deleteObject: { objectId: newFirstSlideId } }] : requests;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Publish a deck via the Slides API (create or in-place update). api/token are
|
|
213
|
+
// injected so this is unit-testable with a mock transport.
|
|
214
|
+
async function publishDeck({ spec, title, updateId, api: apiFn, token: tok }) {
|
|
215
|
+
const { requests } = buildDeck(spec);
|
|
216
|
+
let id;
|
|
217
|
+
let batch;
|
|
69
218
|
if (updateId) {
|
|
70
219
|
id = updateId;
|
|
71
|
-
const got = await
|
|
72
|
-
const
|
|
73
|
-
|
|
220
|
+
const got = await apiFn('GET', `/presentations/${id}`, null, tok);
|
|
221
|
+
const existing = got.slides || (got.presentation && got.presentation.slides) || [];
|
|
222
|
+
batch = planBatch({ requests, updateId, existingSlides: existing });
|
|
74
223
|
} else {
|
|
75
|
-
const pres = await
|
|
224
|
+
const pres = await apiFn('POST', '/presentations', { title }, tok);
|
|
76
225
|
id = pres.presentationId || pres.id || (pres.presentation && pres.presentation.presentationId);
|
|
77
226
|
const slides = pres.slides || (pres.presentation && pres.presentation.slides) || [];
|
|
78
|
-
|
|
227
|
+
batch = planBatch({ requests, newFirstSlideId: slides[0] && slides[0].objectId });
|
|
79
228
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
229
|
+
await apiFn('POST', `/presentations/${id}/batch-update`, { requests: batch }, tok);
|
|
230
|
+
|
|
231
|
+
// Second pass for speaker notes: the notes body objectIds only exist once the
|
|
232
|
+
// slides do, so re-read the deck and insert notes for slides that declare them.
|
|
233
|
+
let notes = 0;
|
|
234
|
+
if ((spec.slides || []).some((s) => s.notes)) {
|
|
235
|
+
const got = await apiFn('GET', `/presentations/${id}`, null, tok);
|
|
236
|
+
const apiSlides = got.slides || (got.presentation && got.presentation.slides) || [];
|
|
237
|
+
const nreqs = notesRequests(apiSlides, spec.slides);
|
|
238
|
+
if (nreqs.length) {
|
|
239
|
+
await apiFn('POST', `/presentations/${id}/batch-update`, { requests: nreqs }, tok);
|
|
240
|
+
notes = nreqs.length;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return { id, url: `https://docs.google.com/presentation/d/${id}/edit`, ops: batch.length, notes };
|
|
84
244
|
}
|
|
85
245
|
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
|
|
246
|
+
// One-shot: an analysis text -> composed spec -> lint gate -> published deck.
|
|
247
|
+
// api/token injected so the orchestration is testable without the network.
|
|
248
|
+
// Returns { ok:false, findings } if the composed spec fails the lint gate.
|
|
249
|
+
async function videoToDeck({ analysisText, title, theme, style, url, api: apiFn, token: tok }) {
|
|
250
|
+
const spec = composeSpec(analysisText, { theme, style, title, url });
|
|
251
|
+
const findings = checkSpec(spec, lintSpec);
|
|
252
|
+
if (hasErrors(findings)) return { ok: false, spec, findings };
|
|
253
|
+
const published = await publishDeck({
|
|
254
|
+
spec,
|
|
255
|
+
title: title || (spec.brand && spec.brand.name) || 'Atris deck',
|
|
256
|
+
updateId: null,
|
|
257
|
+
api: apiFn,
|
|
258
|
+
token: tok,
|
|
259
|
+
});
|
|
260
|
+
return { ok: true, spec, findings, ...published };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Build several specs in sequence (avoids rate-limit bursts). Plain build only.
|
|
264
|
+
async function runBatch(specPaths, { themeOverride }) {
|
|
265
|
+
const tok = token();
|
|
266
|
+
if (!tok) {
|
|
267
|
+
console.error(' no credentials at ~/.atris/credentials.json — run `atris login` and connect Google Drive.');
|
|
268
|
+
return 1;
|
|
95
269
|
}
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
270
|
+
let failures = 0;
|
|
271
|
+
for (const specPath of specPaths) {
|
|
272
|
+
let spec;
|
|
273
|
+
try { spec = readSpec(specPath); }
|
|
274
|
+
catch (e) { console.error(` ✗ ${specPath}: ${e.message}`); failures += 1; continue; }
|
|
275
|
+
if (themeOverride) spec.theme = themeOverride;
|
|
276
|
+
if (!THEMES[spec.theme]) { console.error(` ✗ ${specPath}: unknown theme "${spec.theme}"`); failures += 1; continue; }
|
|
277
|
+
const findings = validateSpec(spec);
|
|
278
|
+
if (hasErrors(findings)) { console.error(` ✗ ${specPath}: invalid spec`); printLint(findings); failures += 1; continue; }
|
|
279
|
+
try {
|
|
280
|
+
const { id, url, ops } = await publishDeck({ spec, title: (spec.brand && spec.brand.name) || 'Atris deck', updateId: null, api, token: tok });
|
|
281
|
+
try { recordBuild({ spec, specPath, presentationId: id, url, mode: 'create' }); } catch { /* ignore */ }
|
|
282
|
+
console.log(` ✓ ${specPath} -> ${url} (${ops} ops)`);
|
|
283
|
+
} catch (e) { console.error(` ✗ ${specPath}: ${e.message}`); failures += 1; }
|
|
284
|
+
}
|
|
285
|
+
console.log(`\n built ${specPaths.length - failures}/${specPaths.length} decks\n`);
|
|
286
|
+
return failures ? 1 : 0;
|
|
101
287
|
}
|
|
102
288
|
|
|
103
289
|
async function run(argv) {
|
|
104
290
|
const sub = argv[0];
|
|
105
291
|
|
|
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
292
|
if (sub === 'themes') {
|
|
129
293
|
console.log('\n atris deck themes:\n');
|
|
130
294
|
for (const [name, t] of Object.entries(THEMES)) {
|
|
131
|
-
|
|
295
|
+
const hint = THEME_HINTS[name] || '';
|
|
296
|
+
console.log(` ${name.padEnd(10)} ${hint}`);
|
|
297
|
+
console.log(` ${t.fonts.display} + ${t.fonts.body} · accent ${t.color.accent} bg ${t.color.bg}`);
|
|
132
298
|
}
|
|
133
|
-
console.log('\n
|
|
299
|
+
console.log('\n pick one: spec.json "theme" field OR atris deck build spec.json --theme noir');
|
|
300
|
+
console.log('\n slide types: title, statement, columns, panel, chips, bignumber, close,');
|
|
301
|
+
console.log(' timeline, versus, metricgrid, receipt, stack, quote, hero,');
|
|
302
|
+
console.log(' interstitial, lede, prose, split, bullets (narrative / box-free)\n');
|
|
134
303
|
return 0;
|
|
135
304
|
}
|
|
136
305
|
|
|
@@ -140,45 +309,276 @@ async function run(argv) {
|
|
|
140
309
|
return 0;
|
|
141
310
|
}
|
|
142
311
|
|
|
143
|
-
if (sub === '
|
|
312
|
+
if (sub === 'history') {
|
|
313
|
+
const query = argv.slice(1).find((a) => !a.startsWith('-'));
|
|
314
|
+
const entries = historyFor(query);
|
|
315
|
+
if (hasFlag(argv, '--json')) {
|
|
316
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
317
|
+
return 0;
|
|
318
|
+
}
|
|
319
|
+
if (!entries.length) {
|
|
320
|
+
console.log(`\n no deck builds recorded${query ? ` for "${query}"` : ''} yet\n`);
|
|
321
|
+
return 0;
|
|
322
|
+
}
|
|
323
|
+
console.log('');
|
|
324
|
+
for (const e of entries) {
|
|
325
|
+
const when = (e.at || '').replace('T', ' ').slice(0, 16);
|
|
326
|
+
console.log(` ${when} ${String(e.mode).padEnd(6)} ${String(e.slideCount).padStart(2)}sl ${String(e.theme || '?').padEnd(8)} ${e.specHash}`);
|
|
327
|
+
console.log(` ${e.specPath || '(inline)'} -> ${e.url || e.presentationId}`);
|
|
328
|
+
}
|
|
329
|
+
console.log('');
|
|
330
|
+
return 0;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (sub === 'compose') {
|
|
334
|
+
const out = flag(argv, '--out');
|
|
335
|
+
const url = flag(argv, '--url');
|
|
336
|
+
const mdPath = flag(argv, '--md');
|
|
337
|
+
const composeOpts = {
|
|
338
|
+
theme: flag(argv, '--theme'),
|
|
339
|
+
style: flag(argv, '--style'),
|
|
340
|
+
title: flag(argv, '--title'),
|
|
341
|
+
url,
|
|
342
|
+
};
|
|
343
|
+
if (!url && !mdPath) {
|
|
344
|
+
console.error(' usage: atris deck compose (--url <youtube> | --md <file.md>) [--out spec.json] [--theme ink] [--style narrative|dense|pitch]');
|
|
345
|
+
return 2;
|
|
346
|
+
}
|
|
347
|
+
let text;
|
|
348
|
+
if (mdPath) {
|
|
349
|
+
try { text = fs.readFileSync(mdPath, 'utf8'); }
|
|
350
|
+
catch (e) { console.error(` cannot read ${mdPath}: ${e.message}`); return 2; }
|
|
351
|
+
} else {
|
|
352
|
+
// Network path: fetch a transcript analysis from the cloud, then compose.
|
|
353
|
+
try {
|
|
354
|
+
const { processYoutube } = require('./youtube');
|
|
355
|
+
console.error(` fetching analysis for ${url} (this can take a minute)...`);
|
|
356
|
+
const data = await processYoutube({
|
|
357
|
+
youtubeUrl: url,
|
|
358
|
+
query: 'Summarize this video into a slide outline. Use markdown: one H1 title, then ## sections. Within a section use a bullet list for parallel points, a blockquote for a memorable line (with attribution), and short paragraphs for narrative. End with a ## Takeaway.',
|
|
359
|
+
timeoutMs: 300000,
|
|
360
|
+
});
|
|
361
|
+
text = data.video_analysis || data.analysis || data.result || (typeof data === 'string' ? data : '');
|
|
362
|
+
} catch (e) {
|
|
363
|
+
console.error(` compose --url needs Atris cloud access (run \`atris login\`): ${e.message}`);
|
|
364
|
+
return 1;
|
|
365
|
+
}
|
|
366
|
+
if (!text) { console.error(' no analysis returned for that URL'); return 1; }
|
|
367
|
+
}
|
|
368
|
+
const spec = composeSpec(text, composeOpts);
|
|
369
|
+
const findings = checkSpec(spec, lintSpec);
|
|
370
|
+
if (out) {
|
|
371
|
+
fs.writeFileSync(out, `${JSON.stringify(spec, null, 2)}\n`);
|
|
372
|
+
console.error(`\n ✓ composed ${spec.slides.length} slides (${spec.theme}) -> ${out}`);
|
|
373
|
+
printLint(findings);
|
|
374
|
+
console.error(` next: atris deck build ${out} --review\n`);
|
|
375
|
+
} else {
|
|
376
|
+
// stdout stays clean JSON for piping; lint goes to stderr
|
|
377
|
+
console.log(JSON.stringify(spec, null, 2));
|
|
378
|
+
const errs = findings.filter((f) => f.severity === 'error').length;
|
|
379
|
+
const warns = findings.filter((f) => f.severity === 'warn').length;
|
|
380
|
+
console.error(` composed ${spec.slides.length} slides (${spec.theme}) · lint: ${errs} errors, ${warns} warnings`);
|
|
381
|
+
}
|
|
382
|
+
return hasErrors(findings) ? 1 : 0;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (sub === 'lint') {
|
|
144
386
|
const specPath = argv.slice(1).find((a) => !a.startsWith('-'));
|
|
145
|
-
if (!specPath) {
|
|
387
|
+
if (!specPath) {
|
|
388
|
+
console.error(' usage: atris deck lint <spec.json>');
|
|
389
|
+
return 2;
|
|
390
|
+
}
|
|
146
391
|
let spec;
|
|
147
|
-
try { spec =
|
|
392
|
+
try { spec = readSpec(specPath); }
|
|
148
393
|
catch (e) { console.error(` cannot read spec: ${e.message}`); return 2; }
|
|
149
|
-
const
|
|
150
|
-
if (hasFlag(argv, '--
|
|
151
|
-
|
|
394
|
+
const findings = checkSpec(spec, lintSpec);
|
|
395
|
+
if (hasFlag(argv, '--json')) {
|
|
396
|
+
console.log(JSON.stringify({ ok: !hasErrors(findings), findings }, null, 2));
|
|
397
|
+
} else {
|
|
398
|
+
printLint(findings);
|
|
399
|
+
}
|
|
400
|
+
return hasErrors(findings) ? 1 : 0;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (sub === 'review') {
|
|
404
|
+
const idArg = argv.slice(1).find((a) => !a.startsWith('-'));
|
|
405
|
+
if (!idArg) {
|
|
406
|
+
console.error(' usage: atris deck review <presentation-id|url> [--spec spec.json] [--confirm NOTE] [--json]');
|
|
407
|
+
return 2;
|
|
408
|
+
}
|
|
409
|
+
const confirmNote = flag(argv, '--confirm');
|
|
410
|
+
if (confirmNote) {
|
|
411
|
+
const { manifestPath, packet, alreadyConfirmed, receiptPath } = confirmReview(extractPresentationId(idArg), confirmNote);
|
|
412
|
+
if (hasFlag(argv, '--json')) {
|
|
413
|
+
console.log(JSON.stringify({ manifestPath, packet, alreadyConfirmed, receiptPath }, null, 2));
|
|
414
|
+
} else {
|
|
415
|
+
console.log(`\n ✓ review confirmed: ${manifestPath}`);
|
|
416
|
+
if (receiptPath) console.log(` receipt: ${receiptPath}`);
|
|
417
|
+
console.log(` deck: ${packet.url}\n`);
|
|
418
|
+
}
|
|
419
|
+
return 0;
|
|
420
|
+
}
|
|
421
|
+
const specPath = flag(argv, '--spec');
|
|
422
|
+
let spec = null;
|
|
423
|
+
if (specPath) {
|
|
424
|
+
try { spec = readSpec(specPath); }
|
|
425
|
+
catch (e) { console.error(` cannot read spec: ${e.message}`); return 2; }
|
|
426
|
+
}
|
|
427
|
+
return runReviewFlow({
|
|
428
|
+
presentationId: idArg,
|
|
429
|
+
spec,
|
|
430
|
+
specPath,
|
|
431
|
+
json: hasFlag(argv, '--json'),
|
|
432
|
+
auto: hasFlag(argv, '--auto') || hasFlag(argv, '--review-auto'),
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (sub === 'video') {
|
|
437
|
+
const url = positionals(argv.slice(1))[0] || flag(argv, '--url');
|
|
438
|
+
if (!url) {
|
|
439
|
+
console.error(' usage: atris deck video <youtube-url> [--title T] [--theme noir] [--style narrative] [--review]');
|
|
440
|
+
return 2;
|
|
441
|
+
}
|
|
442
|
+
const tok = token();
|
|
443
|
+
if (!tok) {
|
|
444
|
+
console.error(' no credentials at ~/.atris/credentials.json — run `atris login` and connect Google Drive.');
|
|
445
|
+
return 1;
|
|
446
|
+
}
|
|
447
|
+
let analysisText;
|
|
448
|
+
try {
|
|
449
|
+
const { processYoutube } = require('./youtube');
|
|
450
|
+
console.error(` fetching analysis for ${url} (this can take a minute)...`);
|
|
451
|
+
const data = await processYoutube({
|
|
452
|
+
youtubeUrl: url,
|
|
453
|
+
query: 'Summarize this video into a slide outline. Use markdown: one H1 title, then ## sections. Within a section use a bullet list for parallel points, a blockquote for a memorable line (with attribution), and short paragraphs for narrative. End with a ## Takeaway.',
|
|
454
|
+
timeoutMs: 300000,
|
|
455
|
+
});
|
|
456
|
+
analysisText = data.video_analysis || data.analysis || data.result || (typeof data === 'string' ? data : '');
|
|
457
|
+
} catch (e) {
|
|
458
|
+
console.error(` atris deck video needs Atris cloud access (run \`atris login\`): ${e.message}`);
|
|
459
|
+
return 1;
|
|
460
|
+
}
|
|
461
|
+
if (!analysisText) { console.error(' no analysis returned for that URL'); return 1; }
|
|
462
|
+
const result = await videoToDeck({
|
|
463
|
+
analysisText,
|
|
464
|
+
title: flag(argv, '--title'),
|
|
465
|
+
theme: flag(argv, '--theme'),
|
|
466
|
+
style: flag(argv, '--style'),
|
|
467
|
+
url,
|
|
468
|
+
api,
|
|
469
|
+
token: tok,
|
|
470
|
+
});
|
|
471
|
+
if (!result.ok) {
|
|
472
|
+
console.error('\n ✗ composed spec failed lint — not building:');
|
|
473
|
+
printLint(result.findings);
|
|
474
|
+
return 1;
|
|
475
|
+
}
|
|
476
|
+
try { recordBuild({ spec: result.spec, presentationId: result.id, url: result.url, mode: 'create' }); } catch { /* ignore */ }
|
|
477
|
+
console.log(`\n ✓ deck ready: ${result.url} (${result.spec.slides.length} slides)\n`);
|
|
478
|
+
if (hasFlag(argv, '--review') || hasFlag(argv, '--review-auto')) {
|
|
479
|
+
return runReviewFlow({ presentationId: result.id, spec: result.spec, specPath: null, json: hasFlag(argv, '--json'), auto: hasFlag(argv, '--review-auto') });
|
|
480
|
+
}
|
|
481
|
+
return 0;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (sub === 'build') {
|
|
485
|
+
const specPaths = positionals(argv.slice(1));
|
|
486
|
+
if (specPaths.length > 1) {
|
|
487
|
+
return runBatch(specPaths, { themeOverride: flag(argv, '--theme') });
|
|
488
|
+
}
|
|
489
|
+
const specPath = specPaths[0];
|
|
490
|
+
if (!specPath) {
|
|
491
|
+
console.error(' usage: atris deck build <spec.json> [<spec2.json> ...] [--title T] [--theme x] [--review]');
|
|
492
|
+
return 2;
|
|
493
|
+
}
|
|
494
|
+
let spec;
|
|
495
|
+
try { spec = readSpec(specPath); }
|
|
496
|
+
catch (e) { console.error(` cannot read spec: ${e.message}`); return 2; }
|
|
497
|
+
const themeOverride = flag(argv, '--theme');
|
|
498
|
+
if (themeOverride) spec.theme = themeOverride;
|
|
499
|
+
if (!THEMES[spec.theme]) {
|
|
500
|
+
console.error(` unknown theme "${spec.theme}". try: ${Object.keys(THEMES).join(', ')}`);
|
|
501
|
+
return 2;
|
|
502
|
+
}
|
|
503
|
+
// Hard schema gate: fail a malformed spec locally with a slide index,
|
|
504
|
+
// before spending a network round trip on a doomed batch-update.
|
|
505
|
+
const schemaFindings = validateSpec(spec);
|
|
506
|
+
if (hasErrors(schemaFindings)) {
|
|
507
|
+
printLint(schemaFindings);
|
|
508
|
+
console.error('\n ✗ spec invalid — fix the errors above before building\n');
|
|
509
|
+
return 2;
|
|
510
|
+
}
|
|
152
511
|
const title = flag(argv, '--title') || `${(spec.brand && spec.brand.name) || 'Atris'} deck`;
|
|
512
|
+
const reviewAuto = hasFlag(argv, '--review-auto');
|
|
513
|
+
const doReview = hasFlag(argv, '--review') || reviewAuto;
|
|
514
|
+
const updateId = flag(argv, '--update');
|
|
515
|
+
if (doReview) {
|
|
516
|
+
const findings = lintSpec(spec);
|
|
517
|
+
printLint(findings);
|
|
518
|
+
if (findings.some((f) => f.severity === 'error')) {
|
|
519
|
+
console.error('\n ✗ lint failed — fix spec before building\n');
|
|
520
|
+
return 1;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
153
523
|
|
|
154
524
|
const tok = token();
|
|
155
|
-
if (!tok) {
|
|
525
|
+
if (!tok) {
|
|
526
|
+
console.error(' no credentials at ~/.atris/credentials.json — run `atris login` and connect Google Drive.');
|
|
527
|
+
return 1;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
console.log(` ${updateId ? 'updating' : 'building'} ${spec.slides.length} slides (${spec.theme})...`);
|
|
531
|
+
const { id, url, ops } = await publishDeck({ spec, title, updateId, api, token: tok });
|
|
532
|
+
// Ledger the build; never let a history write failure break a build.
|
|
533
|
+
try { recordBuild({ spec, specPath, presentationId: id, url, mode: updateId ? 'update' : 'create' }); } catch { /* ignore */ }
|
|
534
|
+
console.log(`\n ✓ deck ${updateId ? 'updated' : 'ready'}: ${url} (${ops} ops)\n`);
|
|
156
535
|
|
|
157
|
-
|
|
158
|
-
|
|
536
|
+
if (doReview) {
|
|
537
|
+
return runReviewFlow({ presentationId: id, spec, specPath, json: hasFlag(argv, '--json'), auto: reviewAuto });
|
|
538
|
+
}
|
|
159
539
|
return 0;
|
|
160
540
|
}
|
|
161
541
|
|
|
162
542
|
console.log(`
|
|
163
|
-
atris deck — premium Google Slides from a plain content spec
|
|
164
|
-
|
|
165
|
-
atris deck
|
|
166
|
-
atris deck
|
|
167
|
-
atris deck
|
|
168
|
-
atris deck
|
|
169
|
-
atris deck
|
|
170
|
-
atris deck build my.json
|
|
171
|
-
atris deck
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
543
|
+
atris deck — premium Google Slides from a plain content spec
|
|
544
|
+
|
|
545
|
+
atris deck sample [--theme paper] > my.json
|
|
546
|
+
atris deck compose --md notes.md --out my.json [--style narrative]
|
|
547
|
+
atris deck compose --url <youtube> --out my.json [--theme ink]
|
|
548
|
+
atris deck video <youtube> [--theme noir] [--review] compose+build in one shot
|
|
549
|
+
atris deck lint my.json
|
|
550
|
+
atris deck build my.json [more.json ...] [--title "Q3 review"] [--update ID] [--review]
|
|
551
|
+
atris deck review <id|url> [--spec my.json]
|
|
552
|
+
atris deck review <id> --confirm "looks good"
|
|
553
|
+
atris deck history [my.json] builds recorded for a spec
|
|
554
|
+
atris deck themes
|
|
555
|
+
|
|
556
|
+
--update ID replaces every slide in an existing deck in place, so the
|
|
557
|
+
presentation URL and id stay stable across rebuilds.
|
|
558
|
+
|
|
559
|
+
Review loop:
|
|
560
|
+
build --review -> downloads slide PNGs to ~/.atris/deck-review/<id>/
|
|
561
|
+
build --review-auto -> also auto-flags blank/failed thumbnails
|
|
562
|
+
agent inspects thumbnails -> fix spec/engine -> rebuild
|
|
563
|
+
review --confirm -> marks deck ready
|
|
564
|
+
|
|
565
|
+
Design system is baked in: distinctive fonts, one accent, real data panels,
|
|
566
|
+
and no AI tells (em dashes sanitized, sentence-case labels, no gradient text).
|
|
177
567
|
`);
|
|
178
568
|
return 0;
|
|
179
569
|
}
|
|
180
570
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
571
|
+
module.exports = {
|
|
572
|
+
run,
|
|
573
|
+
SAMPLE,
|
|
574
|
+
api,
|
|
575
|
+
token,
|
|
576
|
+
lintSpec,
|
|
577
|
+
validateSpec,
|
|
578
|
+
checkSpec,
|
|
579
|
+
planBatch,
|
|
580
|
+
publishDeck,
|
|
581
|
+
videoToDeck,
|
|
582
|
+
positionals,
|
|
583
|
+
DEFAULT_OUT_ROOT,
|
|
584
|
+
};
|