atris 3.24.0 → 3.25.1
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 +6 -6
- package/atris/atrisDev.md +717 -0
- package/atris/policies/outbound-artifact-gate.md +48 -0
- package/atris/skills/atris-feedback/SKILL.md +2 -3
- package/atris/wiki/sources/atris-labs-2026-05-10.txt +6 -9
- package/atris/wiki/sources/atris-labs-goals-2026-05-10.txt +4 -5
- package/atris.md +19 -43
- package/ax +1695 -101
- package/bin/atris.js +2 -38
- package/commands/aeo.js +5 -5
- package/commands/computer.js +0 -1
- package/commands/mission.js +2 -1
- package/commands/recap.js +0 -16
- package/commands/sync.js +2 -0
- package/commands/workflow.js +1 -2
- package/commands/youtube.js +183 -0
- package/lib/ax-chat-input.js +164 -0
- package/lib/ax-goal.js +307 -0
- package/lib/ax-prefs.js +70 -0
- package/lib/ax-shimmer.js +63 -0
- package/lib/context-gatherer.js +8 -26
- package/package.json +2 -1
- package/commands/card.js +0 -121
- package/commands/deck.js +0 -184
- package/commands/reel.js +0 -128
- package/commands/site.js +0 -48
- package/commands/slop.js +0 -307
- package/commands/theme.js +0 -217
- package/lib/card.js +0 -120
- package/lib/deck-from-md.js +0 -110
- package/lib/html-render.js +0 -257
- package/lib/memory-view.js +0 -95
- package/lib/reel.js +0 -52
- package/lib/site.js +0 -114
- package/lib/slides-deck.js +0 -237
- package/lib/theme.js +0 -264
package/lib/ax-goal.js
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
const GOAL_CLEAR_ALIASES = new Set(['clear', 'stop', 'off', 'reset', 'none', 'cancel']);
|
|
2
|
+
const GOAL_ACHIEVED_RE = /^\s*GOAL_ACHIEVED:\s*(.+)$/im;
|
|
3
|
+
const GOAL_JSON_RE = /\{[\s\S]*"achieved"\s*:\s*(true|false)[\s\S]*\}/i;
|
|
4
|
+
|
|
5
|
+
function parseTokenBudget(raw) {
|
|
6
|
+
const text = String(raw || '').trim().toUpperCase();
|
|
7
|
+
const match = text.match(/^(\d+(?:\.\d+)?)([KMB])?$/);
|
|
8
|
+
if (!match) return null;
|
|
9
|
+
let value = Number(match[1]);
|
|
10
|
+
if (match[2] === 'K') value *= 1000;
|
|
11
|
+
if (match[2] === 'M') value *= 1000000;
|
|
12
|
+
if (match[2] === 'B') value *= 1000000000;
|
|
13
|
+
return Math.round(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseGoalCommand(line) {
|
|
17
|
+
const raw = String(line || '').trim();
|
|
18
|
+
const lower = raw.toLowerCase();
|
|
19
|
+
if (!lower.startsWith('/goal')) return null;
|
|
20
|
+
|
|
21
|
+
const rest = raw.slice(5).trim();
|
|
22
|
+
if (!rest) return { action: 'status' };
|
|
23
|
+
|
|
24
|
+
const firstWord = rest.split(/\s+/)[0].toLowerCase();
|
|
25
|
+
if (GOAL_CLEAR_ALIASES.has(firstWord)) return { action: 'clear' };
|
|
26
|
+
|
|
27
|
+
let condition = rest;
|
|
28
|
+
let tokenBudget = null;
|
|
29
|
+
const tokensMatch = condition.match(/^--tokens\s+(\S+)\s+([\s\S]+)$/i);
|
|
30
|
+
if (tokensMatch) {
|
|
31
|
+
tokenBudget = parseTokenBudget(tokensMatch[1]);
|
|
32
|
+
condition = tokensMatch[2].trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const maxTurnsMatch = condition.match(/\b(?:max|stop after|within)\s+(\d+)\s+turns?\b/i);
|
|
36
|
+
const maxTurns = maxTurnsMatch ? Number(maxTurnsMatch[1]) : null;
|
|
37
|
+
|
|
38
|
+
if (!condition) return { action: 'status' };
|
|
39
|
+
return { action: 'set', condition, maxTurns, tokenBudget };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function createGoalState(condition, options = {}) {
|
|
43
|
+
return {
|
|
44
|
+
active: true,
|
|
45
|
+
condition: String(condition || '').trim(),
|
|
46
|
+
maxTurns: Number.isFinite(options.maxTurns) ? options.maxTurns : null,
|
|
47
|
+
tokenBudget: Number.isFinite(options.tokenBudget) ? options.tokenBudget : null,
|
|
48
|
+
turns: 0,
|
|
49
|
+
evalTurns: 0,
|
|
50
|
+
tokensUsed: 0,
|
|
51
|
+
creditsUsed: 0,
|
|
52
|
+
startedAt: Date.now(),
|
|
53
|
+
lastReason: 'Goal started — working toward the condition.',
|
|
54
|
+
achieved: false,
|
|
55
|
+
achievedAt: null,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function goalElapsedMs(goal) {
|
|
60
|
+
const end = goal.achievedAt || Date.now();
|
|
61
|
+
return Math.max(0, end - (goal.startedAt || end));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function truncateGoalText(text, limit = 72) {
|
|
65
|
+
const value = String(text || '').trim();
|
|
66
|
+
if (value.length <= limit) return value;
|
|
67
|
+
return `${value.slice(0, limit - 1)}…`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function compactGoalHistory(history = [], limit = 8) {
|
|
71
|
+
return history
|
|
72
|
+
.slice(-limit)
|
|
73
|
+
.map(turn => `${turn.role}: ${String(turn.content || '').slice(0, 900)}`)
|
|
74
|
+
.join('\n');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function buildGoalDirective(goal, options = {}) {
|
|
78
|
+
const condition = goal.condition;
|
|
79
|
+
const reason = goal.lastReason && goal.turns > 0
|
|
80
|
+
? `\nEvaluator guidance from last turn: ${goal.lastReason}`
|
|
81
|
+
: '';
|
|
82
|
+
return [
|
|
83
|
+
'Work autonomously toward this completion condition:',
|
|
84
|
+
condition,
|
|
85
|
+
'',
|
|
86
|
+
'Use local tools as needed. Do not ask the user for permission between steps.',
|
|
87
|
+
'Do not declare the goal achieved yourself — a separate evaluator decides that.',
|
|
88
|
+
reason,
|
|
89
|
+
options.continue ? '\nContinue from the recent conversation below.' : '',
|
|
90
|
+
].filter(Boolean).join('\n');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function buildGoalEvalPrompt(goal, history, lastOutput) {
|
|
94
|
+
return [
|
|
95
|
+
'You are a strict goal evaluator for a coding agent session.',
|
|
96
|
+
'Reply with JSON only: {"achieved": true|false, "reason": "short reason"}',
|
|
97
|
+
'Judge only from observable evidence in the transcript and latest output.',
|
|
98
|
+
'If the condition requires command output or file contents, require proof in the transcript.',
|
|
99
|
+
'',
|
|
100
|
+
`Goal condition: ${goal.condition}`,
|
|
101
|
+
'',
|
|
102
|
+
'Recent conversation:',
|
|
103
|
+
compactGoalHistory(history),
|
|
104
|
+
'',
|
|
105
|
+
'Latest assistant output:',
|
|
106
|
+
String(lastOutput || '').slice(0, 4000),
|
|
107
|
+
].join('\n');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function parseGoalEvalResponse(text) {
|
|
111
|
+
const raw = String(text || '').trim();
|
|
112
|
+
if (!raw) return null;
|
|
113
|
+
|
|
114
|
+
const marker = raw.match(GOAL_ACHIEVED_RE);
|
|
115
|
+
if (marker) {
|
|
116
|
+
return { achieved: true, reason: marker[1].trim() };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const jsonMatch = raw.match(GOAL_JSON_RE);
|
|
120
|
+
if (!jsonMatch) return null;
|
|
121
|
+
try {
|
|
122
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
123
|
+
return {
|
|
124
|
+
achieved: parsed.achieved === true,
|
|
125
|
+
reason: String(parsed.reason || '').trim() || (parsed.achieved ? 'Condition met.' : 'Condition not met yet.'),
|
|
126
|
+
};
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function parseGoalAchievedMarker(text) {
|
|
133
|
+
const marker = String(text || '').match(GOAL_ACHIEVED_RE);
|
|
134
|
+
if (!marker) return null;
|
|
135
|
+
return { achieved: true, reason: marker[1].trim() };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function accumulateGoalUsage(goal, result, creditsFromState) {
|
|
139
|
+
if (!goal || !result) return;
|
|
140
|
+
const credits = typeof creditsFromState === 'function' ? creditsFromState(result) : null;
|
|
141
|
+
if (Number.isFinite(credits) && credits > 0) {
|
|
142
|
+
goal.creditsUsed += credits;
|
|
143
|
+
}
|
|
144
|
+
const approxTokens = Math.max(0, Math.round(String(result.output || '').length / 4));
|
|
145
|
+
goal.tokensUsed += approxTokens;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function goalBudgetExceeded(goal) {
|
|
149
|
+
if (!goal || !Number.isFinite(goal.tokenBudget)) return false;
|
|
150
|
+
return goal.tokensUsed >= goal.tokenBudget;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function goalTurnLimitReached(goal) {
|
|
154
|
+
if (!goal || !Number.isFinite(goal.maxTurns)) return false;
|
|
155
|
+
return goal.turns >= goal.maxTurns;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function clearGoalState(goal) {
|
|
159
|
+
if (!goal) return null;
|
|
160
|
+
goal.active = false;
|
|
161
|
+
return goal;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function finishGoalAchieved(goal, reason) {
|
|
165
|
+
goal.active = false;
|
|
166
|
+
goal.achieved = true;
|
|
167
|
+
goal.achievedAt = Date.now();
|
|
168
|
+
goal.lastReason = reason || 'Condition met.';
|
|
169
|
+
return goal;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function formatGoalCounter(goal) {
|
|
173
|
+
const turns = `${goal.turns}${Number.isFinite(goal.maxTurns) ? `/${goal.maxTurns}` : ''}`;
|
|
174
|
+
const duration = `${Math.max(1, Math.round(goalElapsedMs(goal) / 1000))}s`;
|
|
175
|
+
const usage = [];
|
|
176
|
+
if (goal.creditsUsed > 0) usage.push(`${goal.creditsUsed} credits`);
|
|
177
|
+
if (goal.tokensUsed > 0) usage.push(`~${goal.tokensUsed} tokens`);
|
|
178
|
+
return { turns, duration, usage: usage.join(' · ') };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function formatGoalStatus(goal, options = {}) {
|
|
182
|
+
const paint = options.paint || ((text) => String(text));
|
|
183
|
+
if (!goal) {
|
|
184
|
+
return 'No active goal. Set one with /goal <condition>.';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const counter = formatGoalCounter(goal);
|
|
188
|
+
const lines = [];
|
|
189
|
+
|
|
190
|
+
if (goal.active) {
|
|
191
|
+
lines.push(paint('◎ /goal active', [options.bold, options.magenta]));
|
|
192
|
+
lines.push(paint(goal.condition, [options.bold]));
|
|
193
|
+
lines.push(paint(`turn ${counter.turns} · ${counter.duration}${counter.usage ? ` · ${counter.usage}` : ''}`, [options.muted]));
|
|
194
|
+
if (goal.lastReason) lines.push(paint(`reason: ${goal.lastReason}`, [options.muted]));
|
|
195
|
+
if (Number.isFinite(goal.tokenBudget)) {
|
|
196
|
+
lines.push(paint(`budget: ~${goal.tokensUsed}/${goal.tokenBudget} tokens`, [options.muted]));
|
|
197
|
+
}
|
|
198
|
+
return lines.join('\n');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (goal.achieved) {
|
|
202
|
+
lines.push(paint('✦ /goal achieved', [options.bold, options.ok || options.magenta]));
|
|
203
|
+
lines.push(paint(goal.condition, [options.bold]));
|
|
204
|
+
lines.push(paint(`${counter.turns} turns · ${counter.duration}${counter.usage ? ` · ${counter.usage}` : ''}`, [options.muted]));
|
|
205
|
+
if (goal.lastReason) lines.push(paint(`reason: ${goal.lastReason}`, [options.muted]));
|
|
206
|
+
return lines.join('\n');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
lines.push(paint('Goal stopped.', [options.muted]));
|
|
210
|
+
lines.push(paint(goal.condition, [options.bold]));
|
|
211
|
+
lines.push(paint(`turn ${counter.turns} · ${counter.duration}`, [options.muted]));
|
|
212
|
+
if (goal.lastReason) lines.push(paint(`reason: ${goal.lastReason}`, [options.muted]));
|
|
213
|
+
return lines.join('\n');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function formatGoalActiveBanner(goal, options = {}) {
|
|
217
|
+
if (!goal || !goal.active) return '';
|
|
218
|
+
const paint = options.paint || ((text) => String(text));
|
|
219
|
+
const counter = formatGoalCounter(goal);
|
|
220
|
+
return paint(
|
|
221
|
+
`◎ /goal active · ${truncateGoalText(goal.condition, 56)} · turn ${counter.turns}${Number.isFinite(goal.maxTurns) ? `/${goal.maxTurns}` : ''} · ${counter.duration}`,
|
|
222
|
+
[options.bold, options.magenta]
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function formatGoalAchieved(goal, options = {}) {
|
|
227
|
+
const paint = options.paint || ((text) => String(text));
|
|
228
|
+
const counter = formatGoalCounter(goal);
|
|
229
|
+
return [
|
|
230
|
+
paint('✦ Goal achieved', [options.bold, options.magenta]),
|
|
231
|
+
paint(goal.condition, [options.bold, options.accent]),
|
|
232
|
+
paint(`${counter.turns} turns · ${counter.duration}${counter.usage ? ` · ${counter.usage}` : ''}`, [options.muted]),
|
|
233
|
+
goal.lastReason ? paint(`reason: ${goal.lastReason}`, [options.muted]) : '',
|
|
234
|
+
].filter(Boolean).join('\n');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function formatGoalContinue(goal, options = {}) {
|
|
238
|
+
const paint = options.paint || ((text) => String(text));
|
|
239
|
+
return paint(`◎ continuing goal · turn ${goal.turns + 1}${Number.isFinite(goal.maxTurns) ? `/${goal.maxTurns}` : ''} · ${goal.lastReason}`, [options.magenta]);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function formatGoalStopped(goal, reason, options = {}) {
|
|
243
|
+
const paint = options.paint || ((text) => String(text));
|
|
244
|
+
const counter = formatGoalCounter(goal);
|
|
245
|
+
return [
|
|
246
|
+
paint('◎ Goal stopped', [options.bold, options.muted]),
|
|
247
|
+
paint(reason, [options.muted]),
|
|
248
|
+
paint(`${counter.turns} turns · ${counter.duration}`, [options.muted]),
|
|
249
|
+
].join('\n');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function evaluateGoalTurn(goal, ctx = {}, deps = {}) {
|
|
253
|
+
const marker = parseGoalAchievedMarker(ctx.lastOutput);
|
|
254
|
+
if (marker) return marker;
|
|
255
|
+
|
|
256
|
+
if (typeof deps.evaluateGoal === 'function') {
|
|
257
|
+
return deps.evaluateGoal(goal, ctx);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (typeof deps.postTurn !== 'function') {
|
|
261
|
+
return { achieved: false, reason: goal.lastReason || 'Condition not met yet.' };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const evalOutput = { isTTY: false, write() {} };
|
|
265
|
+
try {
|
|
266
|
+
const result = await deps.postTurn(buildGoalEvalPrompt(goal, ctx.history || [], ctx.lastOutput), {
|
|
267
|
+
...(ctx.turnOptions || {}),
|
|
268
|
+
mode: 'fast',
|
|
269
|
+
history: [],
|
|
270
|
+
output: evalOutput,
|
|
271
|
+
showProgress: false,
|
|
272
|
+
goalEval: true,
|
|
273
|
+
});
|
|
274
|
+
const parsed = parseGoalEvalResponse(result.output);
|
|
275
|
+
if (parsed) return parsed;
|
|
276
|
+
} catch (error) {
|
|
277
|
+
return { achieved: false, reason: `Evaluator unavailable: ${error.message}` };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return { achieved: false, reason: goal.lastReason || 'Condition not met yet.' };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
module.exports = {
|
|
284
|
+
GOAL_CLEAR_ALIASES,
|
|
285
|
+
accumulateGoalUsage,
|
|
286
|
+
buildGoalDirective,
|
|
287
|
+
buildGoalEvalPrompt,
|
|
288
|
+
clearGoalState,
|
|
289
|
+
compactGoalHistory,
|
|
290
|
+
createGoalState,
|
|
291
|
+
evaluateGoalTurn,
|
|
292
|
+
finishGoalAchieved,
|
|
293
|
+
formatGoalAchieved,
|
|
294
|
+
formatGoalActiveBanner,
|
|
295
|
+
formatGoalContinue,
|
|
296
|
+
formatGoalCounter,
|
|
297
|
+
formatGoalStatus,
|
|
298
|
+
formatGoalStopped,
|
|
299
|
+
goalBudgetExceeded,
|
|
300
|
+
goalElapsedMs,
|
|
301
|
+
goalTurnLimitReached,
|
|
302
|
+
parseGoalAchievedMarker,
|
|
303
|
+
parseGoalCommand,
|
|
304
|
+
parseGoalEvalResponse,
|
|
305
|
+
parseTokenBudget,
|
|
306
|
+
truncateGoalText,
|
|
307
|
+
};
|
package/lib/ax-prefs.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const MEMBER_SLUG = 'ax';
|
|
6
|
+
const TRUTHY = new Set(['1', 'true', 'yes', 'on']);
|
|
7
|
+
|
|
8
|
+
function prefsPath() {
|
|
9
|
+
return path.join(process.env.HOME || os.homedir(), '.atris', 'ax.json');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const PREFS_PATH = prefsPath();
|
|
13
|
+
|
|
14
|
+
function truthy(value) {
|
|
15
|
+
return TRUTHY.has(String(value || '').trim().toLowerCase());
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function loadAxPrefs() {
|
|
19
|
+
const filePath = prefsPath();
|
|
20
|
+
try {
|
|
21
|
+
if (!fs.existsSync(filePath)) {
|
|
22
|
+
return { member_slug: MEMBER_SLUG, bypass_permissions: false };
|
|
23
|
+
}
|
|
24
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
25
|
+
return {
|
|
26
|
+
member_slug: MEMBER_SLUG,
|
|
27
|
+
bypass_permissions: parsed.bypass_permissions === true,
|
|
28
|
+
};
|
|
29
|
+
} catch {
|
|
30
|
+
return { member_slug: MEMBER_SLUG, bypass_permissions: false };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function saveAxPrefs(prefs) {
|
|
35
|
+
const filePath = prefsPath();
|
|
36
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
37
|
+
fs.writeFileSync(filePath, `${JSON.stringify({
|
|
38
|
+
member_slug: MEMBER_SLUG,
|
|
39
|
+
bypass_permissions: prefs.bypass_permissions === true,
|
|
40
|
+
updated_at: new Date().toISOString(),
|
|
41
|
+
}, null, 2)}\n`);
|
|
42
|
+
return loadAxPrefs();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function setBypassPermissions(enabled, { persist = true } = {}) {
|
|
46
|
+
const next = { member_slug: MEMBER_SLUG, bypass_permissions: enabled === true };
|
|
47
|
+
return persist ? saveAxPrefs(next) : next;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveBypassPermissions(options = {}) {
|
|
51
|
+
if (options.bypassPermissions === true) return true;
|
|
52
|
+
if (options.bypassPermissions === false) return false;
|
|
53
|
+
if (truthy(process.env.AX_BYPASS_PERMISSIONS)) return true;
|
|
54
|
+
if (truthy(process.env.AX_SAFE_PERMISSIONS)) return false;
|
|
55
|
+
return loadAxPrefs().bypass_permissions === true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function permissionsLabel(options = {}) {
|
|
59
|
+
return resolveBypassPermissions(options) ? 'bypass' : 'safe';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = {
|
|
63
|
+
MEMBER_SLUG,
|
|
64
|
+
PREFS_PATH,
|
|
65
|
+
loadAxPrefs,
|
|
66
|
+
saveAxPrefs,
|
|
67
|
+
setBypassPermissions,
|
|
68
|
+
resolveBypassPermissions,
|
|
69
|
+
permissionsLabel,
|
|
70
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const ANSI = {
|
|
2
|
+
reset: '\x1b[0m',
|
|
3
|
+
dim: '\x1b[2m',
|
|
4
|
+
muted: '\x1b[90m',
|
|
5
|
+
white: '\x1b[37m',
|
|
6
|
+
bright: '\x1b[97m',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function useColor(options = {}) {
|
|
10
|
+
if (options.color === false) return false;
|
|
11
|
+
if (process.env.NO_COLOR) return false;
|
|
12
|
+
return Boolean(options.color || options.isTTY);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function paint(text, codes, options = {}) {
|
|
16
|
+
if (!useColor(options)) return String(text);
|
|
17
|
+
const styles = codes.filter(Boolean);
|
|
18
|
+
if (!styles.length) return String(text);
|
|
19
|
+
return `${styles.join('')}${text}${ANSI.reset}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const SHIMMER_TICK_STEP = 0.44;
|
|
23
|
+
|
|
24
|
+
function shimmerStyleForDistance(dist) {
|
|
25
|
+
// Obelisk .shimmer-text: pure grayscale luminance sweep — no bold, no hue.
|
|
26
|
+
const t = Math.min(Math.max(dist, 0) / 3.4, 1);
|
|
27
|
+
if (t <= 0.2) return [ANSI.bright];
|
|
28
|
+
if (t <= 0.48) return [ANSI.white];
|
|
29
|
+
if (t <= 0.72) return [ANSI.muted];
|
|
30
|
+
return [ANSI.dim, ANSI.muted];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function renderShimmerText(text, tick = 0, options = {}) {
|
|
34
|
+
const value = String(text || '');
|
|
35
|
+
if (!value) return '';
|
|
36
|
+
if (!useColor(options)) return value;
|
|
37
|
+
|
|
38
|
+
const len = Math.max(1, value.length);
|
|
39
|
+
const cycle = len + 12;
|
|
40
|
+
const head = (Number(tick) * SHIMMER_TICK_STEP) % cycle;
|
|
41
|
+
|
|
42
|
+
let out = '';
|
|
43
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
44
|
+
const char = value[i];
|
|
45
|
+
if (char === ' ') {
|
|
46
|
+
out += ' ';
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
let dist = Math.abs(i - head);
|
|
50
|
+
if (dist > cycle / 2) dist = cycle - dist;
|
|
51
|
+
out += paint(char, shimmerStyleForDistance(dist), options);
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = {
|
|
57
|
+
ANSI,
|
|
58
|
+
SHIMMER_TICK_STEP,
|
|
59
|
+
paint,
|
|
60
|
+
renderShimmerText,
|
|
61
|
+
shimmerStyleForDistance,
|
|
62
|
+
useColor,
|
|
63
|
+
};
|
package/lib/context-gatherer.js
CHANGED
|
@@ -25,6 +25,13 @@ function hasContextProfile(root = process.cwd()) {
|
|
|
25
25
|
return Boolean(profile && String(profile.first_answer || '').trim());
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
function isAtrisMetaQuestion(value) {
|
|
29
|
+
const text = String(value || '').trim().toLowerCase().replace(/[?.!]+$/g, '');
|
|
30
|
+
if (!text) return false;
|
|
31
|
+
return /^(what is|what's|whats|who is|who's|whos|explain|tell me about)\s+atris\b/.test(text)
|
|
32
|
+
|| /^atris\s+(help|overview|what is this|what are you)$/i.test(text);
|
|
33
|
+
}
|
|
34
|
+
|
|
28
35
|
function compactText(value, max = 160) {
|
|
29
36
|
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
30
37
|
if (!text) return '';
|
|
@@ -39,31 +46,6 @@ function inferDomain(answer) {
|
|
|
39
46
|
return 'general';
|
|
40
47
|
}
|
|
41
48
|
|
|
42
|
-
function normalizeQuestionText(value) {
|
|
43
|
-
return String(value || '')
|
|
44
|
-
.toLowerCase()
|
|
45
|
-
.replace(/[^\w\s']/g, ' ')
|
|
46
|
-
.replace(/\s+/g, ' ')
|
|
47
|
-
.trim();
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function isAtrisMetaQuestion(value) {
|
|
51
|
-
const text = normalizeQuestionText(value);
|
|
52
|
-
if (!text) return false;
|
|
53
|
-
|
|
54
|
-
const taskVerb = /\b(add|audit|build|change|create|debug|deploy|edit|fix|implement|make|patch|refactor|remove|review|run|ship|test|update|write)\b/;
|
|
55
|
-
if (taskVerb.test(text)) return false;
|
|
56
|
-
|
|
57
|
-
return [
|
|
58
|
-
/^(what'?s|what is|what are|who is|who are)\s+(atris|you|this)\b/,
|
|
59
|
-
/^what\s+atris\s+is\b/,
|
|
60
|
-
/^(what|how)\s+(does|do|can)\s+(atris|you|this)\b/,
|
|
61
|
-
/^(explain|describe|define)\s+(atris|this)\b/,
|
|
62
|
-
/^tell me\s+(about|what)\s+(atris|this)\b/,
|
|
63
|
-
/^why\s+atris\b/,
|
|
64
|
-
].some((pattern) => pattern.test(text));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
49
|
function starterTaskTitle(answer) {
|
|
68
50
|
const summary = compactText(answer, 80) || 'first useful path';
|
|
69
51
|
return `First useful step: ${summary}`;
|
|
@@ -160,10 +142,10 @@ module.exports = {
|
|
|
160
142
|
profilePath,
|
|
161
143
|
loadContextProfile,
|
|
162
144
|
hasContextProfile,
|
|
163
|
-
isAtrisMetaQuestion,
|
|
164
145
|
saveContextProfile,
|
|
165
146
|
createStarterTask,
|
|
166
147
|
shouldGatherContext,
|
|
148
|
+
isAtrisMetaQuestion,
|
|
167
149
|
renderPrompt,
|
|
168
150
|
starterTaskTitle,
|
|
169
151
|
inferDomain,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "atris",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.25.1",
|
|
4
4
|
"main": "bin/atris.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"atris": "bin/atris.js",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"atris.md",
|
|
20
20
|
"GETTING_STARTED.md",
|
|
21
21
|
"PERSONA.md",
|
|
22
|
+
"atris/atrisDev.md",
|
|
22
23
|
"atris/CLAUDE.md",
|
|
23
24
|
"atris/GEMINI.md",
|
|
24
25
|
"atris/GETTING_STARTED.md",
|
package/commands/card.js
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
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 };
|