atris 3.16.1 → 3.22.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 +32 -7
- package/atris/skills/atris/SKILL.md +15 -2
- package/atris/skills/atris-feedback/SKILL.md +7 -0
- package/atris/skills/design/SKILL.md +29 -2
- package/atris/skills/engines/SKILL.md +44 -0
- package/atris/skills/flow/SKILL.md +1 -1
- package/atris/skills/wake/SKILL.md +37 -0
- package/atris/skills/youtube/SKILL.md +13 -39
- package/atris/team/validator/MEMBER.md +1 -0
- package/atris/wiki/concepts/agent-activation-contract.md +3 -3
- package/atris/wiki/concepts/workspace-initialization-contract.md +3 -3
- package/atris/wiki/index.md +1 -0
- package/atris.md +43 -19
- package/bin/atris.js +413 -31
- package/commands/agent-spawn.js +480 -0
- package/commands/analytics.js +6 -3
- package/commands/apps.js +11 -0
- package/commands/autopilot.js +42 -18
- package/commands/brain.js +74 -7
- package/commands/brainstorm.js +9 -58
- package/commands/clean.js +1 -4
- package/commands/compile.js +9 -4
- package/commands/console.js +8 -3
- package/commands/deck.js +184 -0
- package/commands/init.js +22 -11
- package/commands/lesson.js +76 -0
- package/commands/member.js +252 -48
- package/commands/mission.js +405 -13
- package/commands/now.js +4 -2
- package/commands/probe.js +105 -27
- package/commands/pulse.js +504 -0
- package/commands/radar.js +1 -0
- package/commands/recap.js +71 -25
- package/commands/run.js +615 -22
- package/commands/site.js +48 -0
- package/commands/slop.js +307 -0
- package/commands/spaceship.js +39 -0
- package/commands/sync.js +0 -2
- package/commands/task.js +429 -37
- package/commands/theme.js +217 -0
- package/commands/verify.js +7 -3
- package/lib/activity-stream.js +166 -0
- package/lib/auto-accept-certified.js +23 -1
- package/lib/context-gatherer.js +170 -0
- package/lib/deck-from-md.js +110 -0
- package/lib/escape-regexp.js +13 -0
- package/lib/file-ops.js +6 -3
- package/lib/html-render.js +257 -0
- package/lib/journal.js +1 -1
- package/lib/lesson-contradiction.js +113 -0
- package/lib/memory-view.js +95 -0
- package/lib/policy-lessons.js +3 -2
- package/lib/pulse.js +401 -0
- package/lib/runner-command.js +156 -0
- package/lib/site.js +114 -0
- package/lib/slides-deck.js +237 -0
- package/lib/state-detection.js +1 -4
- package/lib/task-db.js +101 -4
- package/lib/task-proof.js +1 -1
- package/lib/theme.js +264 -0
- package/lib/todo-fallback.js +2 -1
- package/lib/todo-sections.js +33 -0
- package/package.json +1 -2
- package/utils/api.js +14 -2
- package/atris/atrisDev.md +0 -717
package/lib/theme.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// Brand themes — a project defines its own theme in .atris/theme.json and every
|
|
2
|
+
// renderer (deck, HTML, site, memory view) uses it. Compounds like slop rules:
|
|
3
|
+
// describe your brand once, every deck and page is on-brand.
|
|
4
|
+
//
|
|
5
|
+
// theme.json shapes (both accepted):
|
|
6
|
+
// { "color": { "accent": "#00A88F", "bg": "#0B1410" }, "fonts": { "display": "Fraunces" } }
|
|
7
|
+
// { "themes": { "acme": { "color": {...} }, "acme-light": { "color": {...} } } }
|
|
8
|
+
// Partial themes inherit every missing token from the base (a full warm-dark theme).
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
const PROJECT_THEME_FILE = path.join('.atris', 'theme.json');
|
|
14
|
+
|
|
15
|
+
// full base so a project can define just an accent + bg and inherit the rest
|
|
16
|
+
const BASE = {
|
|
17
|
+
fonts: { display: 'Fraunces', body: 'Outfit', mono: 'ui-monospace, SFMono-Regular, monospace' },
|
|
18
|
+
color: {
|
|
19
|
+
bg: '#141110', panel: '#1E1915', panelAlt: '#2C2520', line: '#3D332D',
|
|
20
|
+
ink: '#EAE3D9', soft: '#A39B92', faint: '#7C736B',
|
|
21
|
+
accent: '#F59E0B', accent2: '#FBBF24', onAccent: '#141110',
|
|
22
|
+
sev: ['#F59E0B', '#FBBF24', '#7F97A4'],
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const COLOR_ROLES = ['bg', 'panel', 'panelAlt', 'line', 'ink', 'soft', 'faint', 'accent', 'accent2', 'onAccent'];
|
|
27
|
+
|
|
28
|
+
function normalizeTheme(t, base = BASE) {
|
|
29
|
+
const src = t && typeof t === 'object' ? t : {};
|
|
30
|
+
return {
|
|
31
|
+
fonts: { ...base.fonts, ...(src.fonts || {}) },
|
|
32
|
+
color: { ...base.color, ...(src.color || {}), sev: (src.color && src.color.sev) || base.color.sev },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isValidThemeShape(t) {
|
|
37
|
+
if (!t || typeof t !== 'object') return false;
|
|
38
|
+
const hasColor = t.color && typeof t.color === 'object';
|
|
39
|
+
const hasFonts = t.fonts && typeof t.fonts === 'object';
|
|
40
|
+
return Boolean(hasColor || hasFonts);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// returns { name: normalizedTheme } from .atris/theme.json (or {} if none/bad)
|
|
44
|
+
function loadProjectThemes(root = process.cwd()) {
|
|
45
|
+
let raw;
|
|
46
|
+
try { raw = JSON.parse(fs.readFileSync(path.join(root, PROJECT_THEME_FILE), 'utf8')); }
|
|
47
|
+
catch { return {}; }
|
|
48
|
+
const out = {};
|
|
49
|
+
if (raw && raw.themes && typeof raw.themes === 'object') {
|
|
50
|
+
for (const [name, t] of Object.entries(raw.themes)) if (isValidThemeShape(t)) out[name] = normalizeTheme(t);
|
|
51
|
+
} else if (isValidThemeShape(raw)) {
|
|
52
|
+
out[raw.name || 'brand'] = normalizeTheme(raw);
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// merge built-in themes with project themes (project wins on name collision)
|
|
58
|
+
function mergedThemes(builtin, root = process.cwd()) {
|
|
59
|
+
return { ...builtin, ...loadProjectThemes(root) };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function writeStarterTheme(root = process.cwd()) {
|
|
63
|
+
const file = path.join(root, PROJECT_THEME_FILE);
|
|
64
|
+
if (fs.existsSync(file)) return { file, already: true };
|
|
65
|
+
const starter = {
|
|
66
|
+
name: 'brand',
|
|
67
|
+
color: { accent: '#00A88F', accent2: '#3FD0B6', bg: '#0B1410', ink: '#E8F0EC' },
|
|
68
|
+
fonts: { display: 'Fraunces', body: 'Outfit' },
|
|
69
|
+
};
|
|
70
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
71
|
+
fs.writeFileSync(file, JSON.stringify(starter, null, 2) + '\n');
|
|
72
|
+
return { file, already: false };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ---------- color math (WCAG contrast — the tasteful guardrail) ----------
|
|
76
|
+
function hexToRgb(hex) {
|
|
77
|
+
const h = String(hex == null ? '' : hex).trim().replace(/^#/, '');
|
|
78
|
+
const full = h.length === 3 ? h.split('').map((c) => c + c).join('') : h;
|
|
79
|
+
if (!/^[0-9a-fA-F]{6}$/.test(full)) return null;
|
|
80
|
+
return { r: parseInt(full.slice(0, 2), 16), g: parseInt(full.slice(2, 4), 16), b: parseInt(full.slice(4, 6), 16) };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isHex(hex) { return hexToRgb(hex) != null; }
|
|
84
|
+
|
|
85
|
+
function normHex(hex) {
|
|
86
|
+
const c = hexToRgb(hex);
|
|
87
|
+
if (!c) return null;
|
|
88
|
+
return '#' + [c.r, c.g, c.b].map((v) => v.toString(16).padStart(2, '0')).join('').toUpperCase();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function relLuminance(hex) {
|
|
92
|
+
const c = hexToRgb(hex);
|
|
93
|
+
if (!c) return 0;
|
|
94
|
+
const lin = [c.r, c.g, c.b].map((v) => {
|
|
95
|
+
const s = v / 255;
|
|
96
|
+
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
97
|
+
});
|
|
98
|
+
return 0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function contrastRatio(a, b) {
|
|
102
|
+
const hi = Math.max(relLuminance(a), relLuminance(b));
|
|
103
|
+
const lo = Math.min(relLuminance(a), relLuminance(b));
|
|
104
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// the readable ink to drop on top of a solid color (near-black vs near-white)
|
|
108
|
+
function bestOnColor(hex, dark = '#141110', light = '#FFFFFF') {
|
|
109
|
+
return contrastRatio(hex, dark) >= contrastRatio(hex, light) ? dark : light;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// blend two hex colors, t in [0,1] toward `b`
|
|
113
|
+
function mix(a, b, t) {
|
|
114
|
+
const ca = hexToRgb(a), cb = hexToRgb(b);
|
|
115
|
+
if (!ca || !cb) return normHex(a) || '#000000';
|
|
116
|
+
const m = (x, y) => Math.round(x + (y - x) * t);
|
|
117
|
+
return '#' + [m(ca.r, cb.r), m(ca.g, cb.g), m(ca.b, cb.b)].map((v) => v.toString(16).padStart(2, '0')).join('').toUpperCase();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---------- the tasteful vocabulary (moods, fonts) used by `atris theme create` ----------
|
|
121
|
+
const FONT_PERSONALITIES = {
|
|
122
|
+
'editorial-serif': { display: 'Fraunces', body: 'Outfit', mono: 'IBM Plex Mono' },
|
|
123
|
+
'clean-sans': { display: 'Space Grotesk', body: 'Outfit', mono: 'IBM Plex Mono' },
|
|
124
|
+
'mono-forward': { display: 'Space Grotesk', body: 'IBM Plex Mono', mono: 'IBM Plex Mono' },
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// each mood is hand-tuned neutral scaffolding + a font pairing + curated accents.
|
|
128
|
+
// the user picks a feeling; we keep the surface tasteful and let their accent lead.
|
|
129
|
+
const MOODS = {
|
|
130
|
+
editorial: {
|
|
131
|
+
blurb: 'warm, literary, considered — Fraunces headlines on calm paper',
|
|
132
|
+
defaultMode: 'dark', fonts: 'editorial-serif',
|
|
133
|
+
swatches: ['#B5572E', '#D98E5C', '#9E3B2E', '#C08552'],
|
|
134
|
+
modes: {
|
|
135
|
+
dark: { bg: '#1E1A16', panel: '#261F19', panelAlt: '#322820', line: '#443629', ink: '#EDE4D6', soft: '#B0A595', faint: '#837868' },
|
|
136
|
+
light: { bg: '#FBF8F2', panel: '#F2ECE0', panelAlt: '#E9E1D2', line: '#DCD2C0', ink: '#2A241D', soft: '#6B6256', faint: '#938A7C' },
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
technical: {
|
|
140
|
+
blurb: 'cool, precise, engineered — Space Grotesk on slate, mono accents',
|
|
141
|
+
defaultMode: 'dark', fonts: 'clean-sans',
|
|
142
|
+
swatches: ['#3B82F6', '#06B6D4', '#6366F1', '#14B8A6'],
|
|
143
|
+
modes: {
|
|
144
|
+
dark: { bg: '#0E1116', panel: '#161B22', panelAlt: '#1C2330', line: '#2A3340', ink: '#E6EDF3', soft: '#9DA7B3', faint: '#6B7682' },
|
|
145
|
+
light: { bg: '#F7F9FB', panel: '#EEF2F6', panelAlt: '#E3E9F0', line: '#D2DAE3', ink: '#1A2230', soft: '#5A6675', faint: '#8A95A3' },
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
noir: {
|
|
149
|
+
blurb: 'high-contrast, near-black, one electric accent — confident and stark',
|
|
150
|
+
defaultMode: 'dark', forceMode: 'dark', fonts: 'clean-sans',
|
|
151
|
+
swatches: ['#C8FF00', '#FF2D78', '#FFB000', '#7DD3FC'],
|
|
152
|
+
modes: {
|
|
153
|
+
dark: { bg: '#0A0A0B', panel: '#131316', panelAlt: '#1B1B20', line: '#2A2A30', ink: '#F4F4F5', soft: '#A1A1AA', faint: '#6E6E76' },
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
paper: {
|
|
157
|
+
blurb: 'calm, light, low-key — ink on cream, nothing shouting',
|
|
158
|
+
defaultMode: 'light', forceMode: 'light', fonts: 'editorial-serif',
|
|
159
|
+
swatches: ['#2F4858', '#5B7553', '#B5572E', '#6B4E71'],
|
|
160
|
+
modes: {
|
|
161
|
+
light: { bg: '#FBF8F2', panel: '#F2ECE0', panelAlt: '#E9E1D2', line: '#DCD2C0', ink: '#2A241D', soft: '#6B6256', faint: '#938A7C' },
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
vivid: {
|
|
165
|
+
blurb: 'saturated, playful, energetic — a bold accent that carries the page',
|
|
166
|
+
defaultMode: 'dark', fonts: 'clean-sans',
|
|
167
|
+
swatches: ['#FF5C5C', '#8B5CF6', '#10B981', '#F59E0B'],
|
|
168
|
+
modes: {
|
|
169
|
+
dark: { bg: '#121016', panel: '#1B1822', panelAlt: '#251F30', line: '#352C42', ink: '#F2EEF7', soft: '#ADA3BC', faint: '#7D7388' },
|
|
170
|
+
light: { bg: '#FCFAFF', panel: '#F3EFF9', panelAlt: '#E9E2F2', line: '#DAD0E6', ink: '#221B2C', soft: '#645A72', faint: '#938A9F' },
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const MOOD_NAMES = Object.keys(MOODS);
|
|
176
|
+
|
|
177
|
+
function resolveFonts(spec, mood) {
|
|
178
|
+
const m = MOODS[mood] || MOODS.editorial;
|
|
179
|
+
if (spec && typeof spec === 'object') return { ...FONT_PERSONALITIES[m.fonts], ...spec };
|
|
180
|
+
const key = spec && FONT_PERSONALITIES[spec] ? spec : m.fonts;
|
|
181
|
+
return { ...FONT_PERSONALITIES[key] };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function resolveMode(mood, mode) {
|
|
185
|
+
const m = MOODS[mood] || MOODS.editorial;
|
|
186
|
+
if (m.forceMode) return m.forceMode;
|
|
187
|
+
if ((mode === 'light' || mode === 'dark') && m.modes[mode]) return mode;
|
|
188
|
+
return m.defaultMode;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// pure: tasteful answers -> a full, normalized theme. The heart of `atris theme create`.
|
|
192
|
+
// answers = { mood, mode?, accent?, accent2?, fonts? }
|
|
193
|
+
function buildTheme(answers = {}) {
|
|
194
|
+
const moodKey = MOODS[answers.mood] ? answers.mood : 'editorial';
|
|
195
|
+
const mood = MOODS[moodKey];
|
|
196
|
+
const mode = resolveMode(moodKey, answers.mode);
|
|
197
|
+
const surface = mood.modes[mode];
|
|
198
|
+
|
|
199
|
+
const accent = normHex(answers.accent) || mood.swatches[0];
|
|
200
|
+
const accent2 = normHex(answers.accent2) || mix(accent, surface.ink, 0.28);
|
|
201
|
+
const onAccent = bestOnColor(accent);
|
|
202
|
+
const fonts = resolveFonts(answers.fonts, moodKey);
|
|
203
|
+
|
|
204
|
+
return normalizeTheme({
|
|
205
|
+
color: { ...surface, accent, accent2, onAccent, sev: [accent, accent2, surface.faint] },
|
|
206
|
+
fonts,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// readability guardrail — human messages for low-contrast choices (empty array = clean)
|
|
211
|
+
function themeWarnings(theme) {
|
|
212
|
+
const c = normalizeTheme(theme).color;
|
|
213
|
+
const warn = [];
|
|
214
|
+
const body = contrastRatio(c.ink, c.bg);
|
|
215
|
+
if (body < 4.5) warn.push(`body text (ink on bg) is ${body.toFixed(1)}:1 — below the 4.5:1 readable floor`);
|
|
216
|
+
const onAcc = contrastRatio(c.onAccent, c.accent);
|
|
217
|
+
if (onAcc < 4.5) warn.push(`text on the accent is ${onAcc.toFixed(1)}:1 — below 4.5:1`);
|
|
218
|
+
const accentBg = contrastRatio(c.accent, c.bg);
|
|
219
|
+
if (accentBg < 2.0) warn.push(`the accent barely separates from the background (${accentBg.toFixed(1)}:1) — pick a bolder color`);
|
|
220
|
+
return warn;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// write/replace a named theme in .atris/theme.json (always the themes-map form),
|
|
224
|
+
// preserving every other theme. Throws (code EBADTHEME) if an existing file is present
|
|
225
|
+
// but unparseable, so we never clobber a hand-edited brand file.
|
|
226
|
+
function upsertProjectTheme(name, theme, root = process.cwd(), recipe = null) {
|
|
227
|
+
const themeName = (String(name == null ? '' : name).trim()) || 'brand';
|
|
228
|
+
const file = path.join(root, PROJECT_THEME_FILE);
|
|
229
|
+
let themes = {};
|
|
230
|
+
if (fs.existsSync(file)) {
|
|
231
|
+
let raw;
|
|
232
|
+
try { raw = JSON.parse(fs.readFileSync(file, 'utf8')); }
|
|
233
|
+
catch { const e = new Error(`${PROJECT_THEME_FILE} is not valid JSON — fix or remove it before saving`); e.code = 'EBADTHEME'; throw e; }
|
|
234
|
+
if (raw && raw.themes && typeof raw.themes === 'object') themes = { ...raw.themes };
|
|
235
|
+
else if (isValidThemeShape(raw)) themes[raw.name || 'brand'] = { name: raw.name, color: raw.color, fonts: raw.fonts };
|
|
236
|
+
}
|
|
237
|
+
const existed = Object.prototype.hasOwnProperty.call(themes, themeName);
|
|
238
|
+
const t = normalizeTheme(theme);
|
|
239
|
+
// recipe = the high-level answers (mood/mode/accent); inert to renderers, lets `theme edit` re-seed
|
|
240
|
+
const record = { name: themeName };
|
|
241
|
+
if (recipe && typeof recipe === 'object') record.recipe = recipe;
|
|
242
|
+
record.color = t.color; record.fonts = t.fonts;
|
|
243
|
+
themes[themeName] = record;
|
|
244
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
245
|
+
fs.writeFileSync(file, JSON.stringify({ themes }, null, 2) + '\n');
|
|
246
|
+
return { file, name: themeName, action: existed ? 'updated' : 'created', count: Object.keys(themes).length };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// the saved high-level answers for a theme, if present (for `theme edit` pre-fill)
|
|
250
|
+
function loadThemeRecipe(name, root = process.cwd()) {
|
|
251
|
+
let raw;
|
|
252
|
+
try { raw = JSON.parse(fs.readFileSync(path.join(root, PROJECT_THEME_FILE), 'utf8')); }
|
|
253
|
+
catch { return null; }
|
|
254
|
+
const rec = raw && raw.themes && raw.themes[name];
|
|
255
|
+
return rec && rec.recipe && typeof rec.recipe === 'object' ? rec.recipe : null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
module.exports = {
|
|
259
|
+
BASE, COLOR_ROLES, normalizeTheme, isValidThemeShape, loadProjectThemes, mergedThemes,
|
|
260
|
+
writeStarterTheme, PROJECT_THEME_FILE,
|
|
261
|
+
hexToRgb, isHex, normHex, relLuminance, contrastRatio, bestOnColor, mix,
|
|
262
|
+
FONT_PERSONALITIES, MOODS, MOOD_NAMES, resolveFonts, resolveMode,
|
|
263
|
+
buildTheme, themeWarnings, upsertProjectTheme, loadThemeRecipe,
|
|
264
|
+
};
|
package/lib/todo-fallback.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
const fs = require('fs');
|
|
10
10
|
const path = require('path');
|
|
11
|
+
const escapeRegExp = require('./escape-regexp');
|
|
11
12
|
|
|
12
13
|
function parseTodoFile(todoPath) {
|
|
13
14
|
if (!fs.existsSync(todoPath)) return { backlog: [], inProgress: [], review: [], completed: [] };
|
|
@@ -37,7 +38,7 @@ function cleanTaskTitle(text) {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
function parseSection(content, sectionName) {
|
|
40
|
-
const escaped = sectionName
|
|
41
|
+
const escaped = escapeRegExp(sectionName);
|
|
41
42
|
const match = content.match(new RegExp(`(?:^|\\n)##\\s+${escaped}[^\\n]*\\n([\\s\\S]*?)(?=\\n##(?!#)\\s+|$)`, 'i'));
|
|
42
43
|
if (!match) return [];
|
|
43
44
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Canonical TODO/journal section classification. The same emoji-brittle logic
|
|
4
|
+
// existed (and was bug-fixed) independently in commands/now.js (CLI-287) and
|
|
5
|
+
// commands/brain.js (CLI-288); centralizing it here so a third copy can't drift.
|
|
6
|
+
//
|
|
7
|
+
// Headings may carry trailing decoration ("## In Progress 🔄", "## Completed ✅"),
|
|
8
|
+
// so detection uses \b (not \s*$) and section names match by prefix — "In Progress 🔄"
|
|
9
|
+
// counts as "In Progress", while "Backlogged Notes" does NOT count as "Backlog".
|
|
10
|
+
|
|
11
|
+
const RENDERED_SECTION_RE = /^##\s+(Backlog|In Progress|Blocked|Completed)\b/m;
|
|
12
|
+
|
|
13
|
+
// True when the document uses rendered task sections (vs a flat/legacy bullet list).
|
|
14
|
+
function hasRenderedSections(text) {
|
|
15
|
+
return RENDERED_SECTION_RE.test(String(text || ''));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// A parsed heading name matches a canonical section if it equals it or is that
|
|
19
|
+
// name followed by decoration (a space then emoji/text).
|
|
20
|
+
function sectionMatches(name, target) {
|
|
21
|
+
const n = String(name || '');
|
|
22
|
+
return n === target || n.startsWith(`${target} `);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isOpenSection(name) {
|
|
26
|
+
return sectionMatches(name, 'Backlog') || sectionMatches(name, 'In Progress');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isDoneSection(name) {
|
|
30
|
+
return sectionMatches(name, 'Completed');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { hasRenderedSections, sectionMatches, isOpenSection, isDoneSection, RENDERED_SECTION_RE };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "atris",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.22.0",
|
|
4
4
|
"main": "bin/atris.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"atris": "bin/atris.js",
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
"atris.md",
|
|
20
20
|
"GETTING_STARTED.md",
|
|
21
21
|
"PERSONA.md",
|
|
22
|
-
"atris/atrisDev.md",
|
|
23
22
|
"atris/CLAUDE.md",
|
|
24
23
|
"atris/GEMINI.md",
|
|
25
24
|
"atris/GETTING_STARTED.md",
|
package/utils/api.js
CHANGED
|
@@ -216,6 +216,7 @@ function streamProChat(url, token, body, showTools = false) {
|
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
let buffer = '';
|
|
219
|
+
let emittedText = false;
|
|
219
220
|
|
|
220
221
|
res.on('data', (chunk) => {
|
|
221
222
|
buffer += chunk.toString();
|
|
@@ -232,10 +233,16 @@ function streamProChat(url, token, body, showTools = false) {
|
|
|
232
233
|
|
|
233
234
|
if (msg.type === 'system_init' && showTools) {
|
|
234
235
|
console.log(`[System] Tools available: ${msg.tools?.join(', ') || 'none'}`);
|
|
236
|
+
} else if (msg.type === 'text_delta') {
|
|
237
|
+
if (msg.content) {
|
|
238
|
+
emittedText = true;
|
|
239
|
+
process.stdout.write(msg.content);
|
|
240
|
+
}
|
|
235
241
|
} else if (msg.type === 'assistant') {
|
|
236
242
|
if (msg.content && Array.isArray(msg.content)) {
|
|
237
243
|
for (const block of msg.content) {
|
|
238
244
|
if (block.type === 'text') {
|
|
245
|
+
emittedText = true;
|
|
239
246
|
process.stdout.write(block.text);
|
|
240
247
|
}
|
|
241
248
|
}
|
|
@@ -245,11 +252,14 @@ function streamProChat(url, token, body, showTools = false) {
|
|
|
245
252
|
} else if (msg.type === 'tool_result' && showTools) {
|
|
246
253
|
const preview = msg.content?.substring(0, 100) || '';
|
|
247
254
|
console.log(`[✓ Result]: ${preview}${msg.content?.length > 100 ? '...' : ''}`);
|
|
248
|
-
} else if (msg.type === 'result') {
|
|
255
|
+
} else if (msg.type === 'result' && !emittedText) {
|
|
249
256
|
if (msg.result) {
|
|
250
257
|
process.stdout.write(msg.result);
|
|
251
258
|
}
|
|
259
|
+
} else if (msg.type === 'error') {
|
|
260
|
+
reject(new Error(msg.error || 'Atris stream error'));
|
|
252
261
|
} else if (msg.chunk) {
|
|
262
|
+
emittedText = true;
|
|
253
263
|
process.stdout.write(msg.chunk);
|
|
254
264
|
}
|
|
255
265
|
} catch (e) {
|
|
@@ -270,7 +280,9 @@ function streamProChat(url, token, body, showTools = false) {
|
|
|
270
280
|
const msg = JSON.parse(data);
|
|
271
281
|
if (msg.chunk) {
|
|
272
282
|
process.stdout.write(msg.chunk);
|
|
273
|
-
} else if (msg.type === '
|
|
283
|
+
} else if (msg.type === 'text_delta' && msg.content) {
|
|
284
|
+
process.stdout.write(msg.content);
|
|
285
|
+
} else if (msg.type === 'result' && msg.result && !emittedText) {
|
|
274
286
|
process.stdout.write(msg.result);
|
|
275
287
|
}
|
|
276
288
|
} catch (e) {
|