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
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// `atris moves`: alive onboarding. Shows the 3 highest-leverage next moves and
|
|
4
|
+
// lets you approve (seed it into the loop), kill (stop suggesting it), or skip.
|
|
5
|
+
// This is the seed of proactiveness: the workspace proposes, you steer.
|
|
6
|
+
|
|
7
|
+
const readline = require('readline');
|
|
8
|
+
const {
|
|
9
|
+
nextMoves,
|
|
10
|
+
recordDecision,
|
|
11
|
+
seedInboxFromMove,
|
|
12
|
+
} = require('../lib/next-moves');
|
|
13
|
+
|
|
14
|
+
function currentMoves(root, limit) {
|
|
15
|
+
return nextMoves(root, limit);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function renderMoves(moves) {
|
|
19
|
+
if (!moves.length) {
|
|
20
|
+
return [
|
|
21
|
+
'',
|
|
22
|
+
'your next moves',
|
|
23
|
+
'',
|
|
24
|
+
' nothing queued. add an item to ROADMAP.md under "## Open loop items",',
|
|
25
|
+
' or jot an idea with `atris log`.',
|
|
26
|
+
'',
|
|
27
|
+
].join('\n');
|
|
28
|
+
}
|
|
29
|
+
const lines = ['', 'your next moves', ''];
|
|
30
|
+
moves.forEach((m, i) => {
|
|
31
|
+
lines.push(` ${i + 1}. ${m.title}`);
|
|
32
|
+
lines.push(` why: ${m.why} id: ${m.id}`);
|
|
33
|
+
lines.push('');
|
|
34
|
+
});
|
|
35
|
+
lines.push(' approve: atris moves --approve <id|N> seed it into the loop');
|
|
36
|
+
lines.push(' kill: atris moves --kill <id|N> stop suggesting it');
|
|
37
|
+
lines.push(' (use the id when you act later; the numbered order can shift)');
|
|
38
|
+
lines.push('');
|
|
39
|
+
return lines.join('\n');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseIndexes(value) {
|
|
43
|
+
return String(value || '')
|
|
44
|
+
.split(/[\s,]+/)
|
|
45
|
+
.map((s) => parseInt(s, 10))
|
|
46
|
+
.filter((n) => Number.isInteger(n) && n >= 1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Resolve --approve/--kill tokens to move objects. A token is either a stable
|
|
50
|
+
// move id (m_...) or a 1-based position. The id is preferred because the list
|
|
51
|
+
// can re-rank between viewing and acting, so a bare index can hit a different
|
|
52
|
+
// move than the one you saw.
|
|
53
|
+
function resolveSelection(moves, value) {
|
|
54
|
+
const tokens = String(value || '').split(/[\s,]+/).filter(Boolean);
|
|
55
|
+
const byId = new Map(moves.map((m) => [m.id, m]));
|
|
56
|
+
const seen = new Set();
|
|
57
|
+
const out = [];
|
|
58
|
+
for (const tok of tokens) {
|
|
59
|
+
let move = null;
|
|
60
|
+
if (byId.has(tok)) move = byId.get(tok);
|
|
61
|
+
else if (/^\d+$/.test(tok)) move = moves[parseInt(tok, 10) - 1];
|
|
62
|
+
if (move && !seen.has(move.id)) { seen.add(move.id); out.push(move); }
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function applyDecision(root, selectedMoves, decision, stamp) {
|
|
68
|
+
const { claimRoadmapItem } = require('../lib/next-moves');
|
|
69
|
+
const acted = [];
|
|
70
|
+
for (const move of selectedMoves) {
|
|
71
|
+
recordDecision(root, move, decision, stamp);
|
|
72
|
+
if (decision === 'approve') {
|
|
73
|
+
// Seed then claim, mirroring the loop. For a roadmap-sourced move, mark it
|
|
74
|
+
// claimed in ROADMAP so the loop and the moves list agree it is handled.
|
|
75
|
+
const seeded = seedInboxFromMove(root, move);
|
|
76
|
+
if (move.source === 'roadmap') claimRoadmapItem(root, move.title);
|
|
77
|
+
acted.push({ move, seeded });
|
|
78
|
+
} else {
|
|
79
|
+
acted.push({ move });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return acted;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readArgValue(args, name) {
|
|
86
|
+
const i = args.indexOf(name);
|
|
87
|
+
if (i === -1) return null;
|
|
88
|
+
return args[i + 1] || '';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function movesCommand(args = [], root = process.cwd()) {
|
|
92
|
+
if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
|
|
93
|
+
console.log('');
|
|
94
|
+
console.log('Usage: atris moves [--approve <id|N>] [--kill <id|N>] [--json] [--limit N]');
|
|
95
|
+
console.log('');
|
|
96
|
+
console.log('Show the next moves and steer them. Each move has a stable id; prefer it');
|
|
97
|
+
console.log('over the position number, which can shift between viewing and acting.');
|
|
98
|
+
console.log('');
|
|
99
|
+
console.log(' atris moves Show the 3 next moves (prompts on a terminal)');
|
|
100
|
+
console.log(' atris moves --approve <id|N> Seed that move into the loop (writes the inbox)');
|
|
101
|
+
console.log(' atris moves --kill <id|N> Stop suggesting that move');
|
|
102
|
+
console.log(' atris moves --json Print the moves (with ids) as JSON, no prompt');
|
|
103
|
+
console.log(' atris moves --limit N Show N moves (default 3)');
|
|
104
|
+
console.log('');
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const limitArg = readArgValue(args, '--limit');
|
|
109
|
+
const limit = limitArg && !Number.isNaN(parseInt(limitArg, 10)) ? Math.max(1, parseInt(limitArg, 10)) : 3;
|
|
110
|
+
const stamp = new Date().toISOString();
|
|
111
|
+
|
|
112
|
+
const approveVal = readArgValue(args, '--approve');
|
|
113
|
+
const killVal = readArgValue(args, '--kill');
|
|
114
|
+
|
|
115
|
+
if (approveVal !== null || killVal !== null) {
|
|
116
|
+
const moves = currentMoves(root, limit);
|
|
117
|
+
const killed = applyDecision(root, resolveSelection(moves, killVal), 'kill', stamp);
|
|
118
|
+
const approved = applyDecision(root, resolveSelection(moves, approveVal), 'approve', stamp);
|
|
119
|
+
for (const a of approved) {
|
|
120
|
+
const note = a.seeded && a.seeded.alreadyPresent ? 'already in the inbox' : 'seeded into the loop';
|
|
121
|
+
console.log(`approved: ${a.move.title} -> ${note}`);
|
|
122
|
+
}
|
|
123
|
+
for (const k of killed) console.log(`killed: ${k.move.title} -> will not suggest again`);
|
|
124
|
+
if (!approved.length && !killed.length) console.log('no matching move for that id or number.');
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const moves = currentMoves(root, limit);
|
|
129
|
+
|
|
130
|
+
if (args.includes('--json')) {
|
|
131
|
+
console.log(JSON.stringify({ moves }, null, 2));
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log(renderMoves(moves));
|
|
136
|
+
|
|
137
|
+
// Only prompt on a real terminal, so spawned/non-interactive runs never hang.
|
|
138
|
+
if (!process.stdin.isTTY || !moves.length) return 0;
|
|
139
|
+
|
|
140
|
+
const answer = await new Promise((resolve) => {
|
|
141
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
142
|
+
rl.question('approve N, kill kN, or enter to skip: ', (a) => { rl.close(); resolve(a.trim()); });
|
|
143
|
+
});
|
|
144
|
+
if (!answer) return 0;
|
|
145
|
+
|
|
146
|
+
const killTokens = (answer.match(/k\s*\d+/gi) || []).map((t) => t.replace(/k/i, '').trim());
|
|
147
|
+
const approveTokens = answer.replace(/k\s*\d+/gi, '').trim();
|
|
148
|
+
|
|
149
|
+
const killed = applyDecision(root, resolveSelection(moves, killTokens.join(' ')), 'kill', stamp);
|
|
150
|
+
const approved = applyDecision(root, resolveSelection(moves, approveTokens), 'approve', stamp);
|
|
151
|
+
for (const a of approved) console.log(`approved: ${a.move.title} -> seeded into the loop`);
|
|
152
|
+
for (const k of killed) console.log(`killed: ${k.move.title}`);
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = { movesCommand, renderMoves, currentMoves, parseIndexes, resolveSelection };
|
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 };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// atris signup <handle>
|
|
2
|
+
//
|
|
3
|
+
// One-call seedless agent signup. A brand-new agent with no account hits
|
|
4
|
+
// POST /api/auth/agent/signup (unauthenticated) and gets back a real, INERT
|
|
5
|
+
// Atris identity + token, then we write the active profile so `atris play`
|
|
6
|
+
// works on the very next command. This closes the install -> signup -> play
|
|
7
|
+
// seam: `npm i -g atris && atris signup x && atris play`.
|
|
8
|
+
//
|
|
9
|
+
// The account is born inert by design (0 credits, no executing agent, external
|
|
10
|
+
// mail paid-gated) — identity is free, capability is earned via AgentXP.
|
|
11
|
+
// Backend: backend/routers/agent_auth_router.py (POST /auth/agent/signup).
|
|
12
|
+
|
|
13
|
+
const crypto = require('crypto');
|
|
14
|
+
const { apiRequestJson, getApiBaseUrl } = require('../utils/api');
|
|
15
|
+
const { saveCredentials } = require('../utils/auth');
|
|
16
|
+
|
|
17
|
+
// Mirror the server rule exactly (^[a-z0-9]{3,30}$) so we fail fast and friendly
|
|
18
|
+
// before spending a network round trip / a rate-limit slot.
|
|
19
|
+
const HANDLE_RE = /^[a-z0-9]{3,30}$/;
|
|
20
|
+
|
|
21
|
+
// Proof-of-work — mirror the server (agent_auth_router.py). The signup endpoint
|
|
22
|
+
// requires a nonce whose sha256(prefix:handle:bucket:nonce) has DIFFICULTY_BITS
|
|
23
|
+
// leading zero bits. Bucket = current 5-min window. Solved locally (~1s) so
|
|
24
|
+
// signup stays a single call; this is the cost that makes mass-minting expensive.
|
|
25
|
+
const POW_PREFIX = 'atris-signup-v1';
|
|
26
|
+
const POW_WINDOW_S = 300;
|
|
27
|
+
const POW_DIFFICULTY_BITS = 20;
|
|
28
|
+
|
|
29
|
+
function solvePow(handle) {
|
|
30
|
+
const bucket = Math.floor(Date.now() / 1000 / POW_WINDOW_S);
|
|
31
|
+
const fullBytes = Math.floor(POW_DIFFICULTY_BITS / 8);
|
|
32
|
+
const remBits = POW_DIFFICULTY_BITS % 8;
|
|
33
|
+
for (let n = 0; ; n++) {
|
|
34
|
+
const d = crypto.createHash('sha256').update(`${POW_PREFIX}:${handle}:${bucket}:${n}`).digest();
|
|
35
|
+
let ok = true;
|
|
36
|
+
for (let i = 0; i < fullBytes; i++) { if (d[i] !== 0) { ok = false; break; } }
|
|
37
|
+
if (ok && remBits && (d[fullBytes] >> (8 - remBits)) !== 0) ok = false;
|
|
38
|
+
if (ok) return String(n);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseHandle(args = []) {
|
|
43
|
+
const positional = args.find((a) => a && !a.startsWith('-'));
|
|
44
|
+
return (positional || '').trim().toLowerCase();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function signupCommand(args = []) {
|
|
48
|
+
const handle = parseHandle(args);
|
|
49
|
+
|
|
50
|
+
if (!handle) {
|
|
51
|
+
console.error('Usage: atris signup <handle>');
|
|
52
|
+
console.error(' handle: 3–30 characters, lowercase letters and digits only.');
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
if (!HANDLE_RE.test(handle)) {
|
|
56
|
+
console.error(`✗ Invalid handle "${handle}".`);
|
|
57
|
+
console.error(' Must be 3–30 characters, lowercase letters and digits (a–z, 0–9). No spaces or symbols.');
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log(`Claiming @${handle} … (solving proof-of-work)`);
|
|
62
|
+
const pow = solvePow(handle);
|
|
63
|
+
|
|
64
|
+
const res = await apiRequestJson('/auth/agent/signup', {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
body: { handle, pow },
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (res.ok && res.data && res.data.token) {
|
|
70
|
+
const { token, email, user_id: userId } = res.data;
|
|
71
|
+
const identity = email || `${handle}@atrismail.com`;
|
|
72
|
+
saveCredentials(token, null, identity, userId || null, 'atrisos');
|
|
73
|
+
console.log(`\n✓ You're in — ${identity}`);
|
|
74
|
+
console.log(' Inert starter account (0 credits): identity is free, capability is earned.');
|
|
75
|
+
console.log(' Saved to your active profile.');
|
|
76
|
+
console.log('\nNext:');
|
|
77
|
+
console.log(' atris play # claim a starter mission and earn your first proof-backed rep');
|
|
78
|
+
console.log(' atris xp # see where you stand on the board');
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Friendly, actionable errors — no stack traces for expected cases.
|
|
83
|
+
if (res.status === 409) {
|
|
84
|
+
console.error(`✗ "${handle}" is already taken or reserved. Try a different handle.`);
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
if (res.status === 429) {
|
|
88
|
+
console.error('✗ Too many signups right now. Wait a minute and try again.');
|
|
89
|
+
return 1;
|
|
90
|
+
}
|
|
91
|
+
if (res.status === 404) {
|
|
92
|
+
console.error('✗ Seedless signup isn’t available on this backend yet.');
|
|
93
|
+
console.error(' Use `atris login` for now, or try again after the next deploy.');
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
console.error(`✗ Signup failed: ${res.error || 'unknown error'} (status ${res.status}).`);
|
|
97
|
+
console.error(` API: ${getApiBaseUrl()}`);
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = { signupCommand, parseHandle, HANDLE_RE };
|
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 };
|