atris 2.2.0 → 2.2.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.
@@ -0,0 +1,496 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ // --- YAML Frontmatter Parser (regex-based, no deps) ---
5
+
6
+ function parseFrontmatter(content) {
7
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
8
+ if (!match) return null;
9
+
10
+ const yaml = match[1];
11
+ const result = {};
12
+ let currentKey = null;
13
+
14
+ for (const line of yaml.split('\n')) {
15
+ // List item: " - value"
16
+ const listMatch = line.match(/^\s+-\s+(.+)$/);
17
+ if (listMatch && currentKey) {
18
+ if (!Array.isArray(result[currentKey])) result[currentKey] = [];
19
+ result[currentKey].push(listMatch[1].trim());
20
+ continue;
21
+ }
22
+
23
+ // Nested key (one level): " key: value"
24
+ const nestedMatch = line.match(/^\s+([a-z_-]+):\s*(.*)$/);
25
+ if (nestedMatch && currentKey && typeof result[currentKey] === 'object' && !Array.isArray(result[currentKey])) {
26
+ result[currentKey][nestedMatch[1]] = nestedMatch[2].trim() || true;
27
+ continue;
28
+ }
29
+
30
+ // Top-level key: "key: value"
31
+ const kvMatch = line.match(/^([a-z_-]+):\s*(.*)$/);
32
+ if (kvMatch) {
33
+ currentKey = kvMatch[1];
34
+ const val = kvMatch[2].trim();
35
+ if (val === '') {
36
+ // Could be start of a list or nested object — leave empty, next lines fill it
37
+ result[currentKey] = {};
38
+ } else if (val.startsWith('[') && val.endsWith(']')) {
39
+ // Inline array: [a, b, "c d"]
40
+ result[currentKey] = val.slice(1, -1).split(',').map(s => s.trim().replace(/^["']|["']$/g, ''));
41
+ } else {
42
+ result[currentKey] = val.replace(/^["']|["']$/g, '');
43
+ }
44
+ }
45
+ }
46
+
47
+ return result;
48
+ }
49
+
50
+ function getFrontmatterRaw(content) {
51
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
52
+ return match ? match[1] : null;
53
+ }
54
+
55
+ // --- Skill Discovery ---
56
+
57
+ function findAllSkills(skillsDir) {
58
+ if (!fs.existsSync(skillsDir)) return [];
59
+
60
+ const skills = [];
61
+
62
+ function walk(dir, prefix) {
63
+ const entries = fs.readdirSync(dir);
64
+ for (const entry of entries) {
65
+ const fullPath = path.join(dir, entry);
66
+ const stat = fs.statSync(fullPath);
67
+ if (!stat.isDirectory()) continue;
68
+
69
+ const skillFile = path.join(fullPath, 'SKILL.md');
70
+ if (fs.existsSync(skillFile)) {
71
+ const content = fs.readFileSync(skillFile, 'utf8');
72
+ const folderName = prefix ? `${prefix}/${entry}` : entry;
73
+ skills.push({
74
+ folder: folderName,
75
+ leafFolder: entry,
76
+ path: skillFile,
77
+ content,
78
+ frontmatter: parseFrontmatter(content)
79
+ });
80
+ } else if (stat.isDirectory()) {
81
+ // Check nested (e.g., clawhub/atris)
82
+ walk(fullPath, prefix ? `${prefix}/${entry}` : entry);
83
+ }
84
+ }
85
+ }
86
+
87
+ walk(skillsDir, '');
88
+ return skills;
89
+ }
90
+
91
+ // --- Audit Checks ---
92
+
93
+ function runAuditChecks(skill) {
94
+ const fm = skill.frontmatter || {};
95
+ const checks = [];
96
+
97
+ // 1. name exists
98
+ checks.push({
99
+ id: 'name-exists',
100
+ severity: 'ERROR',
101
+ pass: !!fm.name,
102
+ message: fm.name ? `name: ${fm.name}` : 'missing name field'
103
+ });
104
+
105
+ // 2. name is kebab-case
106
+ const isKebab = fm.name ? /^[a-z][a-z0-9-]*$/.test(fm.name) : false;
107
+ checks.push({
108
+ id: 'name-kebab',
109
+ severity: 'ERROR',
110
+ pass: isKebab,
111
+ message: isKebab ? 'valid kebab-case' : `"${fm.name || ''}" is not kebab-case`
112
+ });
113
+
114
+ // 3. name matches folder
115
+ const nameMatchesFolder = fm.name === skill.leafFolder;
116
+ checks.push({
117
+ id: 'name-matches-folder',
118
+ severity: 'ERROR',
119
+ pass: nameMatchesFolder,
120
+ message: nameMatchesFolder
121
+ ? `matches folder "${skill.leafFolder}"`
122
+ : `name "${fm.name}" != folder "${skill.leafFolder}"`
123
+ });
124
+
125
+ // 4. description exists
126
+ checks.push({
127
+ id: 'desc-exists',
128
+ severity: 'ERROR',
129
+ pass: !!fm.description,
130
+ message: fm.description ? `${fm.description.length} chars` : 'missing description'
131
+ });
132
+
133
+ // 5. description under 1024 chars
134
+ const descLen = (fm.description || '').length;
135
+ checks.push({
136
+ id: 'desc-length',
137
+ severity: 'WARN',
138
+ pass: descLen <= 1024,
139
+ message: `${descLen}/1024 chars`
140
+ });
141
+
142
+ // 6. description has trigger/WHEN guidance
143
+ const desc = (fm.description || '').toLowerCase();
144
+ const hasTriggers = /use when|triggers? on|use for|use if/.test(desc);
145
+ checks.push({
146
+ id: 'desc-has-when',
147
+ severity: 'WARN',
148
+ pass: hasTriggers,
149
+ message: hasTriggers ? 'has WHEN guidance' : 'no trigger/WHEN phrases in description'
150
+ });
151
+
152
+ // 7. no XML tags in content (skip placeholders like <name>, <keyword>, code blocks)
153
+ const xmlMatches = skill.content.match(/<[a-zA-Z][^>]*>/g) || [];
154
+ const placeholders = /^<(name|keyword|placeholder|value|type|path|file|dir|id|url|tag|description|your-|user-|project-|skill-)/i;
155
+ const realXml = xmlMatches.filter(t =>
156
+ !t.startsWith('<!--') && !t.startsWith('<!') && !placeholders.test(t)
157
+ );
158
+ checks.push({
159
+ id: 'no-xml-tags',
160
+ severity: 'ERROR',
161
+ pass: realXml.length === 0,
162
+ message: realXml.length === 0 ? 'clean' : `found XML: ${realXml.slice(0, 3).join(', ')}`
163
+ });
164
+
165
+ // 8. version field present
166
+ checks.push({
167
+ id: 'version-exists',
168
+ severity: 'WARN',
169
+ pass: !!fm.version,
170
+ message: fm.version ? `v${fm.version}` : 'missing version field'
171
+ });
172
+
173
+ // 9. tags field present
174
+ const hasTags = Array.isArray(fm.tags) && fm.tags.length > 0;
175
+ checks.push({
176
+ id: 'tags-exist',
177
+ severity: 'WARN',
178
+ pass: hasTags,
179
+ message: hasTags ? `${fm.tags.length} tags` : 'missing tags field'
180
+ });
181
+
182
+ // 10. no README.md in skill folder
183
+ const skillDir = path.dirname(skill.path);
184
+ const hasReadme = fs.existsSync(path.join(skillDir, 'README.md'));
185
+ checks.push({
186
+ id: 'no-readme',
187
+ severity: 'WARN',
188
+ pass: !hasReadme,
189
+ message: hasReadme ? 'README.md found (should not exist in skill folder)' : 'clean'
190
+ });
191
+
192
+ // 11. under 5000 words
193
+ const wordCount = skill.content.split(/\s+/).length;
194
+ checks.push({
195
+ id: 'word-count',
196
+ severity: 'INFO',
197
+ pass: wordCount <= 5000,
198
+ message: `${wordCount} words (limit: 5000)`
199
+ });
200
+
201
+ // 12. has numbered steps
202
+ const hasSteps = /^\d+\.\s/m.test(skill.content);
203
+ checks.push({
204
+ id: 'has-steps',
205
+ severity: 'INFO',
206
+ pass: hasSteps,
207
+ message: hasSteps ? 'has numbered instructions' : 'no numbered instructions found'
208
+ });
209
+
210
+ return checks;
211
+ }
212
+
213
+ function getStatus(checks) {
214
+ const hasError = checks.some(c => !c.pass && c.severity === 'ERROR');
215
+ const hasWarn = checks.some(c => !c.pass && c.severity === 'WARN');
216
+ if (hasError) return 'FAIL';
217
+ if (hasWarn) return 'WARN';
218
+ return 'PASS';
219
+ }
220
+
221
+ // --- Auto-Fix ---
222
+
223
+ function autoFix(skill, dryRun) {
224
+ const fixes = [];
225
+ let content = skill.content;
226
+ const fm = skill.frontmatter || {};
227
+ const rawFm = getFrontmatterRaw(content);
228
+ if (!rawFm) return fixes;
229
+
230
+ let newFmLines = rawFm.split('\n');
231
+
232
+ // Fix 1: name mismatch — replace name value with folder name
233
+ if (fm.name && fm.name !== skill.leafFolder) {
234
+ const idx = newFmLines.findIndex(l => l.match(/^name:\s/));
235
+ if (idx !== -1) {
236
+ newFmLines[idx] = `name: ${skill.leafFolder}`;
237
+ fixes.push(`Fixed name: "${fm.name}" -> "${skill.leafFolder}"`);
238
+ }
239
+ }
240
+
241
+ // Fix 2: merge triggers into description, remove triggers field
242
+ if (fm.triggers) {
243
+ const triggers = Array.isArray(fm.triggers) ? fm.triggers : [fm.triggers];
244
+ const triggerText = triggers.map(t => `"${t}"`).join(', ');
245
+
246
+ // Update description to include triggers
247
+ const descIdx = newFmLines.findIndex(l => l.match(/^description:\s/));
248
+ if (descIdx !== -1) {
249
+ let desc = fm.description || '';
250
+ if (!/triggers? on/i.test(desc)) {
251
+ desc = desc.replace(/\s*$/, '') + ` Triggers on ${triggerText}.`;
252
+ newFmLines[descIdx] = `description: ${desc}`;
253
+ }
254
+ }
255
+
256
+ // Remove triggers field (could be inline or block list)
257
+ const trigIdx = newFmLines.findIndex(l => l.match(/^triggers:\s*/));
258
+ if (trigIdx !== -1) {
259
+ // Remove the triggers line and any following list items
260
+ let endIdx = trigIdx + 1;
261
+ while (endIdx < newFmLines.length && newFmLines[endIdx].match(/^\s+-\s/)) {
262
+ endIdx++;
263
+ }
264
+ newFmLines.splice(trigIdx, endIdx - trigIdx);
265
+ fixes.push(`Merged ${triggers.length} triggers into description, removed triggers field`);
266
+ }
267
+ }
268
+
269
+ // Fix 3: add version if missing
270
+ if (!fm.version) {
271
+ // Insert after description line
272
+ const descIdx = newFmLines.findIndex(l => l.match(/^description:\s/));
273
+ const insertAt = descIdx !== -1 ? descIdx + 1 : newFmLines.length;
274
+ newFmLines.splice(insertAt, 0, 'version: 1.0.0');
275
+ fixes.push('Added version: 1.0.0');
276
+ }
277
+
278
+ // Fix 4: add tags if missing
279
+ const hasTags = Array.isArray(fm.tags) && fm.tags.length > 0;
280
+ if (!hasTags) {
281
+ const tags = generateTags(skill.leafFolder, fm.description || '');
282
+ const tagLines = ['tags:'].concat(tags.map(t => ` - ${t}`));
283
+ newFmLines = newFmLines.concat(tagLines);
284
+ fixes.push(`Added tags: [${tags.join(', ')}]`);
285
+ }
286
+
287
+ // Fix 5: remove XML tags from body
288
+ const fmEnd = content.indexOf('\n---', 4);
289
+ if (fmEnd !== -1) {
290
+ let body = content.substring(fmEnd);
291
+ const xmlPattern = /<([a-zA-Z][a-zA-Z0-9]*)[^>]*>([\s\S]*?)<\/\1>/g;
292
+ let xmlCount = 0;
293
+ const newBody = body.replace(xmlPattern, (match, tag, inner) => {
294
+ xmlCount++;
295
+ return `[${inner.trim()}]`;
296
+ });
297
+ if (xmlCount > 0) {
298
+ content = content.substring(0, fmEnd) + newBody;
299
+ fixes.push(`Replaced ${xmlCount} XML tag(s) with bracket notation`);
300
+ }
301
+ }
302
+
303
+ if (fixes.length > 0 && !dryRun) {
304
+ // Reconstruct file with new frontmatter
305
+ const fmEndIdx = content.indexOf('\n---', 4);
306
+ const bodyPart = content.substring(fmEndIdx + 4); // after closing ---
307
+ const newContent = '---\n' + newFmLines.join('\n') + '\n---' + bodyPart;
308
+
309
+ // Re-apply XML fixes to the full content
310
+ const xmlPattern = /<([a-zA-Z][a-zA-Z0-9]*)[^>]*>([\s\S]*?)<\/\1>/g;
311
+ const finalContent = newContent.replace(xmlPattern, (match, tag, inner) => {
312
+ return `[${inner.trim()}]`;
313
+ });
314
+
315
+ fs.writeFileSync(skill.path, finalContent, 'utf8');
316
+ }
317
+
318
+ return fixes;
319
+ }
320
+
321
+ function generateTags(folderName, description) {
322
+ const tags = [];
323
+
324
+ // Tag from folder name
325
+ tags.push(folderName);
326
+
327
+ // Extract keywords from description
328
+ const keywords = {
329
+ 'workflow': ['workflow', 'plan', 'process', 'loop'],
330
+ 'automation': ['automat', 'autonomous', 'loop', 'execute'],
331
+ 'writing': ['writing', 'essay', 'draft', 'edit', 'copy'],
332
+ 'frontend': ['frontend', 'ui', 'design', 'css', 'layout', 'component'],
333
+ 'backend': ['backend', 'api', 'server', 'database', 'endpoint'],
334
+ 'email': ['email', 'gmail', 'inbox', 'message'],
335
+ 'memory': ['memory', 'history', 'journal', 'search', 'past'],
336
+ 'metacognition': ['metacognition', 'think', 'stuck', 'self-check'],
337
+ 'navigation': ['navigation', 'map', 'codebase', 'file:line'],
338
+ 'anti-slop': ['slop', 'ai pattern', 'cleanup', 'humanize'],
339
+ 'quality': ['audit', 'review', 'validate', 'improve']
340
+ };
341
+
342
+ const descLower = description.toLowerCase();
343
+ for (const [tag, patterns] of Object.entries(keywords)) {
344
+ if (tag === folderName) continue; // Already added
345
+ if (patterns.some(p => descLower.includes(p))) {
346
+ tags.push(tag);
347
+ }
348
+ }
349
+
350
+ return tags.slice(0, 5); // Max 5 tags
351
+ }
352
+
353
+ // --- Subcommand Handlers ---
354
+
355
+ function skillList() {
356
+ const skillsDir = path.join(process.cwd(), 'atris', 'skills');
357
+ const skills = findAllSkills(skillsDir);
358
+
359
+ if (skills.length === 0) {
360
+ console.log('No skills found in atris/skills/. Run "atris init" first.');
361
+ return;
362
+ }
363
+
364
+ console.log('');
365
+ console.log(' Skill Version Checks Status');
366
+ console.log(' ───── ─────── ────── ──────');
367
+
368
+ let needsAttention = 0;
369
+ for (const skill of skills) {
370
+ const checks = runAuditChecks(skill);
371
+ const passing = checks.filter(c => c.pass).length;
372
+ const total = checks.length;
373
+ const status = getStatus(checks);
374
+ const version = (skill.frontmatter || {}).version || '-';
375
+ const statusIcon = status === 'PASS' ? '\x1b[32mPASS\x1b[0m'
376
+ : status === 'WARN' ? '\x1b[33mWARN\x1b[0m'
377
+ : '\x1b[31mFAIL\x1b[0m';
378
+
379
+ if (status !== 'PASS') needsAttention++;
380
+
381
+ const name = skill.folder.padEnd(18);
382
+ const ver = version.padEnd(10);
383
+ const score = `${passing}/${total}`.padEnd(8);
384
+ console.log(` ${name} ${ver} ${score} ${statusIcon}`);
385
+ }
386
+
387
+ console.log('');
388
+ if (needsAttention > 0) {
389
+ console.log(` ${needsAttention} skill(s) need attention. Run: atris skill audit --all`);
390
+ } else {
391
+ console.log(' All skills passing.');
392
+ }
393
+ console.log('');
394
+ }
395
+
396
+ function skillAudit(name) {
397
+ const skillsDir = path.join(process.cwd(), 'atris', 'skills');
398
+ const allSkills = findAllSkills(skillsDir);
399
+
400
+ const targets = name === '--all'
401
+ ? allSkills
402
+ : allSkills.filter(s => s.folder === name || s.leafFolder === name);
403
+
404
+ if (targets.length === 0) {
405
+ console.error(`Skill "${name}" not found. Run "atris skill list" to see available skills.`);
406
+ process.exit(1);
407
+ }
408
+
409
+ for (const skill of targets) {
410
+ const checks = runAuditChecks(skill);
411
+ const errors = checks.filter(c => !c.pass && c.severity === 'ERROR').length;
412
+ const warns = checks.filter(c => !c.pass && c.severity === 'WARN').length;
413
+ const passing = checks.filter(c => c.pass).length;
414
+
415
+ console.log('');
416
+ console.log(` Audit: ${skill.folder}`);
417
+ console.log(' ' + '─'.repeat(40));
418
+
419
+ for (const check of checks) {
420
+ const icon = check.pass ? '\x1b[32m\u2713\x1b[0m' : '\x1b[31m\u2717\x1b[0m';
421
+ const id = check.id.padEnd(22);
422
+ console.log(` ${icon} ${id} ${check.message}`);
423
+ }
424
+
425
+ console.log('');
426
+ console.log(` Score: ${passing}/${checks.length} (${errors} errors, ${warns} warnings)`);
427
+
428
+ if (errors > 0 || warns > 0) {
429
+ console.log(` Run: atris skill fix ${skill.folder}`);
430
+ }
431
+ console.log('');
432
+ }
433
+ }
434
+
435
+ function skillFix(name) {
436
+ const skillsDir = path.join(process.cwd(), 'atris', 'skills');
437
+ const allSkills = findAllSkills(skillsDir);
438
+
439
+ const targets = name === '--all'
440
+ ? allSkills
441
+ : allSkills.filter(s => s.folder === name || s.leafFolder === name);
442
+
443
+ if (targets.length === 0) {
444
+ console.error(`Skill "${name}" not found. Run "atris skill list" to see available skills.`);
445
+ process.exit(1);
446
+ }
447
+
448
+ let totalFixes = 0;
449
+
450
+ for (const skill of targets) {
451
+ const fixes = autoFix(skill, false);
452
+
453
+ if (fixes.length > 0) {
454
+ console.log('');
455
+ console.log(` Fixing: ${skill.folder}`);
456
+ for (const fix of fixes) {
457
+ console.log(` \x1b[32m\u2713\x1b[0m ${fix}`);
458
+ }
459
+ totalFixes += fixes.length;
460
+ }
461
+ }
462
+
463
+ if (totalFixes === 0) {
464
+ console.log('');
465
+ console.log(' Nothing to fix. All auto-fixable issues already resolved.');
466
+ } else {
467
+ console.log('');
468
+ console.log(` ${totalFixes} fix(es) applied. Run: atris skill audit ${name}`);
469
+ }
470
+ console.log('');
471
+ }
472
+
473
+ // --- Main Dispatcher ---
474
+
475
+ function skillCommand(subcommand, ...args) {
476
+ switch (subcommand) {
477
+ case 'list':
478
+ case 'ls':
479
+ return skillList();
480
+ case 'audit':
481
+ return skillAudit(args[0] || '--all');
482
+ case 'fix':
483
+ return skillFix(args[0] || '--all');
484
+ default:
485
+ console.log('');
486
+ console.log('Usage: atris skill <subcommand> [name]');
487
+ console.log('');
488
+ console.log('Subcommands:');
489
+ console.log(' list Show all skills with compliance status');
490
+ console.log(' audit [name|--all] Validate skill against Anthropic guide');
491
+ console.log(' fix [name|--all] Auto-fix common compliance issues');
492
+ console.log('');
493
+ }
494
+ }
495
+
496
+ module.exports = { skillCommand, parseFrontmatter, runAuditChecks, findAllSkills };
@@ -146,9 +146,17 @@ function statusAtris(isQuick = false) {
146
146
  }
147
147
  }
148
148
 
149
+ // Read lessons count
150
+ let lessonsCount = 0;
151
+ const lessonsFile = path.join(targetDir, 'lessons.md');
152
+ if (fs.existsSync(lessonsFile)) {
153
+ const lessonsContent = fs.readFileSync(lessonsFile, 'utf8');
154
+ lessonsCount = (lessonsContent.match(/^- \*\*/gm) || []).length;
155
+ }
156
+
149
157
  // Quick mode: one-line summary
150
158
  if (isQuick) {
151
- console.log(`📥 ${inboxItems.length} | 📋 ${backlogTasks.length} | 🔨 ${inProgressTasks.length} | ✅ ${completions.length}`);
159
+ console.log(`📥 ${inboxItems.length} | 📋 ${backlogTasks.length} | 🔨 ${inProgressTasks.length} | ✅ ${completions.length} | 📚 ${lessonsCount}`);
152
160
  return;
153
161
  }
154
162
 
@@ -116,6 +116,9 @@ async function planAtris(userInput = null) {
116
116
  console.log(`- MAP: ${mapDisplay}`);
117
117
  console.log(`- TODO: ${taskSourcePath || 'atris/TODO.md (missing)'}`);
118
118
  console.log(`- Features index: ${featuresReadmeRef || 'atris/features/README.md (missing)'}`);
119
+ const lessonsPath = path.join(targetDir, 'lessons.md');
120
+ const lessonsRef = fs.existsSync(lessonsPath) ? path.relative(process.cwd(), lessonsPath) : null;
121
+ console.log(`- Lessons: ${lessonsRef || 'atris/lessons.md (none yet)'}`);
119
122
  console.log(`- Journal (today): ${journalPath}`);
120
123
  console.log('');
121
124
  console.log(`📥 Inbox items: ${inboxCount}`);
@@ -148,6 +151,7 @@ async function planAtris(userInput = null) {
148
151
  if (mapFileRef) console.log(`- ${mapFileRef}`);
149
152
  if (taskSourcePath) console.log(`- ${taskSourcePath}`);
150
153
  if (featuresReadmeRef) console.log(`- ${featuresReadmeRef}`);
154
+ if (lessonsRef) console.log(`- ${lessonsRef}`);
151
155
  console.log(`- ${journalPath}`);
152
156
  console.log('');
153
157
  if (!mapFileRef || mapIsPlaceholder) {
@@ -847,6 +851,8 @@ async function reviewAtris() {
847
851
  console.log('1) Run the project test suite (follow TESTING_GUIDE if present).');
848
852
  console.log('2) Execute any `atris/features/*/validate.md` scripts; if a step fails, fix + rerun.');
849
853
  console.log('3) Update TODO.md + today\'s journal with results; propose tool upgrades if drift repeats.');
854
+ console.log('4) If anything surprised you (broke, worked unexpectedly), append to atris/lessons.md:');
855
+ console.log(' - **[YYYY-MM-DD] [feature-name]** — (pass|fail) — One-line lesson');
850
856
  console.log('');
851
857
  console.log('Done when: ✅ All good. Ready for human testing.');
852
858
  console.log('');
@@ -938,6 +944,7 @@ async function reviewAtris() {
938
944
  userPrompt += ` • Update docs if needed (MAP.md, TODO.md)\n`;
939
945
  userPrompt += ` • Clean TODO.md (move completed tasks to Completed section, then delete)\n`;
940
946
  userPrompt += ` • Extract learnings for journal\n`;
947
+ userPrompt += ` • If anything surprised you, append to atris/lessons.md\n`;
941
948
  userPrompt += ` • EVOLUTION: If you see drift in the logs, propose a tool upgrade.\n\n`;
942
949
  userPrompt += `The cycle: do → review → [issues] → do → review → ✅ Ready\n`;
943
950
  userPrompt += `Start validating now. Read files, run tests, verify implementation.`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "description": "atrisDev (atris dev) - CLI for AI coding agents. Works with Claude Code, Cursor, Windsurf. Make any codebase AI-navigable.",
5
5
  "main": "bin/atris.js",
6
6
  "bin": {