atris 2.3.1 → 2.3.4
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/atris/team/{brainstormer.md → brainstormer/MEMBER.md} +17 -2
- package/atris/team/{executor.md → executor/MEMBER.md} +17 -2
- package/atris/team/{launcher.md → launcher/MEMBER.md} +18 -2
- package/atris/team/{navigator.md → navigator/MEMBER.md} +17 -2
- package/atris/team/{validator.md → validator/MEMBER.md} +18 -2
- package/bin/atris.js +22 -27
- package/commands/init.js +17 -30
- package/commands/skill.js +395 -1
- package/commands/sync.js +98 -65
- package/package.json +1 -1
|
@@ -1,6 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
name: brainstormer
|
|
3
|
+
role: Idea Shaper
|
|
4
|
+
description: Shape ideas, explore possibilities, adapt to user depth
|
|
5
|
+
version: 1.0.0
|
|
6
|
+
|
|
7
|
+
skills: []
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
can-read: true
|
|
11
|
+
can-plan: false
|
|
12
|
+
can-execute: false
|
|
13
|
+
can-approve: false
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Brainstormer — Idea & Reality Shaper
|
|
2
17
|
|
|
3
|
-
> **
|
|
18
|
+
> **Source:** Inbox items, raw ideas
|
|
4
19
|
> **Style:** Read `atris/PERSONA.md` for communication style.
|
|
5
20
|
|
|
6
21
|
---
|
|
@@ -1,6 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
name: executor
|
|
3
|
+
role: Builder
|
|
4
|
+
description: Execute from build specs, one step at a time
|
|
5
|
+
version: 1.0.0
|
|
6
|
+
|
|
7
|
+
skills: []
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
can-read: true
|
|
11
|
+
can-plan: false
|
|
12
|
+
can-execute: true
|
|
13
|
+
can-approve: false
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Executor — Builder
|
|
2
17
|
|
|
3
|
-
> **
|
|
18
|
+
> **Source:** build.md, MAP.md
|
|
4
19
|
> **Style:** Read `atris/PERSONA.md` for communication style.
|
|
5
20
|
|
|
6
21
|
---
|
|
@@ -1,6 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
name: launcher
|
|
3
|
+
role: Closer
|
|
4
|
+
description: Document, capture learnings, publish, celebrate
|
|
5
|
+
version: 1.0.0
|
|
6
|
+
|
|
7
|
+
skills: []
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
can-read: true
|
|
11
|
+
can-plan: false
|
|
12
|
+
can-execute: false
|
|
13
|
+
can-approve: false
|
|
14
|
+
can-ship: true
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Launcher — The Closer
|
|
2
18
|
|
|
3
|
-
> **
|
|
19
|
+
> **Source:** Completed tasks, validation results
|
|
4
20
|
> **Style:** Read `atris/PERSONA.md` for communication style.
|
|
5
21
|
|
|
6
22
|
---
|
|
@@ -1,6 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
name: navigator
|
|
3
|
+
role: Planner
|
|
4
|
+
description: Transform messy human intent into precise execution plans
|
|
5
|
+
version: 1.0.0
|
|
6
|
+
|
|
7
|
+
skills: []
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
can-read: true
|
|
11
|
+
can-plan: true
|
|
12
|
+
can-execute: false
|
|
13
|
+
can-approve: false
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Navigator — Planner
|
|
2
17
|
|
|
3
|
-
> **
|
|
18
|
+
> **Source:** idea.md, MAP.md
|
|
4
19
|
> **Style:** Read `atris/PERSONA.md` for communication style.
|
|
5
20
|
|
|
6
21
|
---
|
|
@@ -1,6 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
name: validator
|
|
3
|
+
role: Reviewer
|
|
4
|
+
description: Validate execution, run tests, ensure quality before shipping
|
|
5
|
+
version: 1.0.0
|
|
6
|
+
|
|
7
|
+
skills: []
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
can-read: true
|
|
11
|
+
can-plan: false
|
|
12
|
+
can-execute: false
|
|
13
|
+
can-approve: true
|
|
14
|
+
can-ship: true
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Validator — Reviewer
|
|
2
18
|
|
|
3
|
-
> **
|
|
19
|
+
> **Source:** build.md, MAP.md, code
|
|
4
20
|
> **Style:** Read `atris/PERSONA.md` for communication style.
|
|
5
21
|
|
|
6
22
|
---
|
package/bin/atris.js
CHANGED
|
@@ -219,6 +219,8 @@ function showHelp() {
|
|
|
219
219
|
console.log(' integrations - Show integration status');
|
|
220
220
|
console.log('');
|
|
221
221
|
console.log('Skills:');
|
|
222
|
+
console.log(' skill create <name> - Scaffold a new skill (--integration, --system)');
|
|
223
|
+
console.log(' skill link [--all] - Symlink skills to ~/.claude/skills/ (system-level)');
|
|
222
224
|
console.log(' skill list - Show all skills with compliance status');
|
|
223
225
|
console.log(' skill audit [name] - Validate skill against Anthropic guide');
|
|
224
226
|
console.log(' skill fix [name] - Auto-fix common compliance issues');
|
|
@@ -897,31 +899,22 @@ function initAtris() {
|
|
|
897
899
|
console.log('✓ Created TASK_CONTEXTS.md placeholder');
|
|
898
900
|
}
|
|
899
901
|
|
|
900
|
-
// Copy agent templates from package
|
|
901
|
-
const
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
902
|
+
// Copy agent templates from package (MEMBER.md directory format)
|
|
903
|
+
const members = ['navigator', 'executor', 'validator', 'launcher', 'brainstormer'];
|
|
904
|
+
members.forEach(name => {
|
|
905
|
+
const sourceFile = path.join(__dirname, '..', 'atris', 'team', name, 'MEMBER.md');
|
|
906
|
+
const memberDir = path.join(teamDir, name);
|
|
907
|
+
const targetFile = path.join(memberDir, 'MEMBER.md');
|
|
908
|
+
const legacyFile = path.join(teamDir, `${name}.md`);
|
|
905
909
|
|
|
906
|
-
|
|
907
|
-
fs.copyFileSync(navigatorSource, navigatorFile);
|
|
908
|
-
console.log('✓ Created team/navigator.md');
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
if (!fs.existsSync(executorFile) && fs.existsSync(executorSource)) {
|
|
912
|
-
fs.copyFileSync(executorSource, executorFile);
|
|
913
|
-
console.log('✓ Created team/executor.md');
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
if (!fs.existsSync(validatorFile) && fs.existsSync(validatorSource)) {
|
|
917
|
-
fs.copyFileSync(validatorSource, validatorFile);
|
|
918
|
-
console.log('✓ Created team/validator.md');
|
|
919
|
-
}
|
|
910
|
+
if (fs.existsSync(targetFile) || fs.existsSync(legacyFile)) return;
|
|
920
911
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
912
|
+
if (fs.existsSync(sourceFile)) {
|
|
913
|
+
fs.mkdirSync(memberDir, { recursive: true });
|
|
914
|
+
fs.copyFileSync(sourceFile, targetFile);
|
|
915
|
+
console.log(`✓ Created team/${name}/MEMBER.md`);
|
|
916
|
+
}
|
|
917
|
+
});
|
|
925
918
|
|
|
926
919
|
// Copy policies from package
|
|
927
920
|
const antislopSource = path.join(__dirname, '..', 'atris', 'policies', 'ANTISLOP.md');
|
|
@@ -987,10 +980,11 @@ function syncAtris() {
|
|
|
987
980
|
{ source: 'atrisDev.md', target: 'atrisDev.md' },
|
|
988
981
|
{ source: 'PERSONA.md', target: 'PERSONA.md' },
|
|
989
982
|
{ source: 'GETTING_STARTED.md', target: 'GETTING_STARTED.md' },
|
|
990
|
-
{ source: 'atris/team/navigator.md', target: 'team/navigator.md' },
|
|
991
|
-
{ source: 'atris/team/executor.md', target: 'team/executor.md' },
|
|
992
|
-
{ source: 'atris/team/validator.md', target: 'team/validator.md' },
|
|
993
|
-
{ source: 'atris/team/launcher.md', target: 'team/launcher.md' },
|
|
983
|
+
{ source: 'atris/team/navigator/MEMBER.md', target: 'team/navigator/MEMBER.md' },
|
|
984
|
+
{ source: 'atris/team/executor/MEMBER.md', target: 'team/executor/MEMBER.md' },
|
|
985
|
+
{ source: 'atris/team/validator/MEMBER.md', target: 'team/validator/MEMBER.md' },
|
|
986
|
+
{ source: 'atris/team/launcher/MEMBER.md', target: 'team/launcher/MEMBER.md' },
|
|
987
|
+
{ source: 'atris/team/brainstormer/MEMBER.md', target: 'team/brainstormer/MEMBER.md' },
|
|
994
988
|
{ source: 'atris/policies/ANTISLOP.md', target: 'policies/ANTISLOP.md' }
|
|
995
989
|
];
|
|
996
990
|
|
|
@@ -1014,6 +1008,7 @@ function syncAtris() {
|
|
|
1014
1008
|
return;
|
|
1015
1009
|
}
|
|
1016
1010
|
|
|
1011
|
+
fs.mkdirSync(path.dirname(targetFile), { recursive: true });
|
|
1017
1012
|
fs.copyFileSync(sourceFile, targetFile);
|
|
1018
1013
|
console.log(`✓ Updated ${target}`);
|
|
1019
1014
|
updated++;
|
package/commands/init.js
CHANGED
|
@@ -475,36 +475,23 @@ function initAtris() {
|
|
|
475
475
|
});
|
|
476
476
|
|
|
477
477
|
|
|
478
|
-
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
fs.copyFileSync(validatorSource, validatorFile);
|
|
496
|
-
console.log('✓ Created team/validator.md');
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
if (!fs.existsSync(launcherFile) && fs.existsSync(launcherSource)) {
|
|
500
|
-
fs.copyFileSync(launcherSource, launcherFile);
|
|
501
|
-
console.log('✓ Created team/launcher.md');
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
if (!fs.existsSync(brainstormerFile) && fs.existsSync(brainstormerSource)) {
|
|
505
|
-
fs.copyFileSync(brainstormerSource, brainstormerFile);
|
|
506
|
-
console.log('✓ Created team/brainstormer.md');
|
|
507
|
-
}
|
|
478
|
+
// Copy team members (MEMBER.md format — directory per member)
|
|
479
|
+
const members = ['navigator', 'executor', 'validator', 'launcher', 'brainstormer'];
|
|
480
|
+
members.forEach(name => {
|
|
481
|
+
const sourceFile = path.join(__dirname, '..', 'atris', 'team', name, 'MEMBER.md');
|
|
482
|
+
const targetDir = path.join(teamDir, name);
|
|
483
|
+
const targetFile = path.join(targetDir, 'MEMBER.md');
|
|
484
|
+
const legacyFile = path.join(teamDir, `${name}.md`);
|
|
485
|
+
|
|
486
|
+
// Skip if already exists (either format)
|
|
487
|
+
if (fs.existsSync(targetFile) || fs.existsSync(legacyFile)) return;
|
|
488
|
+
|
|
489
|
+
if (fs.existsSync(sourceFile)) {
|
|
490
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
491
|
+
fs.copyFileSync(sourceFile, targetFile);
|
|
492
|
+
console.log(`✓ Created team/${name}/MEMBER.md`);
|
|
493
|
+
}
|
|
494
|
+
});
|
|
508
495
|
|
|
509
496
|
// Detect project context and generate profile
|
|
510
497
|
const profile = detectProjectContext(process.cwd());
|
package/commands/skill.js
CHANGED
|
@@ -470,6 +470,377 @@ function skillFix(name) {
|
|
|
470
470
|
console.log('');
|
|
471
471
|
}
|
|
472
472
|
|
|
473
|
+
// --- Skill Scaffold Template ---
|
|
474
|
+
|
|
475
|
+
function generateSkillTemplate(name, description) {
|
|
476
|
+
return `---
|
|
477
|
+
name: ${name}
|
|
478
|
+
description: ${description || `Custom skill for ${name}. Use when user asks about ${name}-related tasks.`}
|
|
479
|
+
version: 1.0.0
|
|
480
|
+
tags:
|
|
481
|
+
- ${name}
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
# ${name}
|
|
485
|
+
|
|
486
|
+
## What This Skill Does
|
|
487
|
+
|
|
488
|
+
Describe what this skill does in 2-3 sentences.
|
|
489
|
+
|
|
490
|
+
## Workflows
|
|
491
|
+
|
|
492
|
+
### "Example trigger phrase"
|
|
493
|
+
|
|
494
|
+
1. Step one
|
|
495
|
+
2. Step two
|
|
496
|
+
3. Step three
|
|
497
|
+
|
|
498
|
+
## Rules
|
|
499
|
+
|
|
500
|
+
- Always confirm before taking destructive actions
|
|
501
|
+
- Never skip the approval gate on sends/deletes
|
|
502
|
+
`;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function generateIntegrationSkillTemplate(name, description) {
|
|
506
|
+
return `---
|
|
507
|
+
name: ${name}
|
|
508
|
+
description: ${description || `Integration skill for ${name}. Use when user asks about ${name}-related tasks.`}
|
|
509
|
+
version: 1.0.0
|
|
510
|
+
tags:
|
|
511
|
+
- ${name}
|
|
512
|
+
- integration
|
|
513
|
+
---
|
|
514
|
+
|
|
515
|
+
# ${name}
|
|
516
|
+
|
|
517
|
+
## Bootstrap (ALWAYS Run First)
|
|
518
|
+
|
|
519
|
+
Before any operation, run this bootstrap to ensure everything is set up:
|
|
520
|
+
|
|
521
|
+
\`\`\`bash
|
|
522
|
+
#!/bin/bash
|
|
523
|
+
set -e
|
|
524
|
+
|
|
525
|
+
# 1. Check if logged in to AtrisOS
|
|
526
|
+
if [ ! -f ~/.atris/credentials.json ]; then
|
|
527
|
+
echo "Not logged in to AtrisOS."
|
|
528
|
+
echo "Run: atris login"
|
|
529
|
+
exit 1
|
|
530
|
+
fi
|
|
531
|
+
|
|
532
|
+
# 2. Extract token
|
|
533
|
+
TOKEN=$(node -e "console.log(require('$HOME/.atris/credentials.json').token)")
|
|
534
|
+
|
|
535
|
+
# 3. Check connection status
|
|
536
|
+
STATUS=$(curl -s "https://api.atris.ai/api/integrations/YOUR_INTEGRATION/status" \\
|
|
537
|
+
-H "Authorization: Bearer $TOKEN")
|
|
538
|
+
|
|
539
|
+
echo "$STATUS"
|
|
540
|
+
export ATRIS_TOKEN="$TOKEN"
|
|
541
|
+
\`\`\`
|
|
542
|
+
|
|
543
|
+
## API Reference
|
|
544
|
+
|
|
545
|
+
Base: \`https://api.atris.ai/api/integrations/YOUR_INTEGRATION\`
|
|
546
|
+
|
|
547
|
+
All requests require: \`-H "Authorization: Bearer $TOKEN"\`
|
|
548
|
+
|
|
549
|
+
### Get Token (after bootstrap)
|
|
550
|
+
\`\`\`bash
|
|
551
|
+
TOKEN=$(node -e "console.log(require('$HOME/.atris/credentials.json').token)")
|
|
552
|
+
\`\`\`
|
|
553
|
+
|
|
554
|
+
### List Items
|
|
555
|
+
\`\`\`bash
|
|
556
|
+
curl -s "https://api.atris.ai/api/integrations/YOUR_INTEGRATION/items" \\
|
|
557
|
+
-H "Authorization: Bearer $TOKEN"
|
|
558
|
+
\`\`\`
|
|
559
|
+
|
|
560
|
+
## Workflows
|
|
561
|
+
|
|
562
|
+
### "Example trigger phrase"
|
|
563
|
+
|
|
564
|
+
1. Run bootstrap
|
|
565
|
+
2. Call the API
|
|
566
|
+
3. Display results
|
|
567
|
+
4. **Confirm with user before any write action**
|
|
568
|
+
|
|
569
|
+
## Error Handling
|
|
570
|
+
|
|
571
|
+
| Error | Meaning | Solution |
|
|
572
|
+
|-------|---------|----------|
|
|
573
|
+
| \`Token expired\` | AtrisOS session expired | Run \`atris login\` |
|
|
574
|
+
| \`401 Unauthorized\` | Invalid/expired token | Run \`atris login\` |
|
|
575
|
+
| \`429 Rate limited\` | Too many requests | Wait 60s, retry |
|
|
576
|
+
|
|
577
|
+
## Security Model
|
|
578
|
+
|
|
579
|
+
1. **Local token** (\`~/.atris/credentials.json\`): Stored locally with 600 permissions.
|
|
580
|
+
2. **Integration credentials**: Stored server-side in AtrisOS encrypted vault. Never local.
|
|
581
|
+
3. **HTTPS only**: All API communication encrypted in transit.
|
|
582
|
+
`;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// --- CREATE subcommand ---
|
|
586
|
+
|
|
587
|
+
function skillCreate(nameArg, ...flags) {
|
|
588
|
+
if (!nameArg) {
|
|
589
|
+
console.error('Usage: atris skill create <name> [--integration] [--description="..."] [--system]');
|
|
590
|
+
console.error('');
|
|
591
|
+
console.error('Examples:');
|
|
592
|
+
console.error(' atris skill create daily-standup');
|
|
593
|
+
console.error(' atris skill create email-outreach --integration');
|
|
594
|
+
console.error(' atris skill create pallet/bol-processor --integration');
|
|
595
|
+
console.error(' atris skill create my-skill --system');
|
|
596
|
+
process.exit(1);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const isIntegration = flags.includes('--integration');
|
|
600
|
+
const isLocal = flags.includes('--local');
|
|
601
|
+
const descFlag = flags.find(f => f.startsWith('--description='));
|
|
602
|
+
const description = descFlag ? descFlag.split('=').slice(1).join('=').replace(/^["']|["']$/g, '') : '';
|
|
603
|
+
|
|
604
|
+
// Parse name — supports "customer/skill-name" format
|
|
605
|
+
let skillDir, skillName, customerName;
|
|
606
|
+
if (nameArg.includes('/')) {
|
|
607
|
+
const parts = nameArg.split('/');
|
|
608
|
+
customerName = parts[0];
|
|
609
|
+
skillName = parts[1];
|
|
610
|
+
const customersDir = path.join(process.cwd(), 'atris', 'customers', customerName, 'skills');
|
|
611
|
+
skillDir = path.join(customersDir, skillName);
|
|
612
|
+
} else {
|
|
613
|
+
skillName = nameArg;
|
|
614
|
+
skillDir = path.join(process.cwd(), 'atris', 'skills', skillName);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Check if already exists
|
|
618
|
+
if (fs.existsSync(skillDir)) {
|
|
619
|
+
console.error(`✗ Skill "${nameArg}" already exists at ${skillDir}`);
|
|
620
|
+
process.exit(1);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Generate content
|
|
624
|
+
const content = isIntegration
|
|
625
|
+
? generateIntegrationSkillTemplate(skillName, description)
|
|
626
|
+
: generateSkillTemplate(skillName, description);
|
|
627
|
+
|
|
628
|
+
// Create skill directory and SKILL.md
|
|
629
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
630
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
|
|
631
|
+
|
|
632
|
+
console.log('');
|
|
633
|
+
if (customerName) {
|
|
634
|
+
console.log(`✓ Created atris/customers/${customerName}/skills/${skillName}/SKILL.md`);
|
|
635
|
+
} else {
|
|
636
|
+
console.log(`✓ Created atris/skills/${skillName}/SKILL.md`);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Symlink to project-level .claude/skills/
|
|
640
|
+
const projectClaudeSkills = path.join(process.cwd(), '.claude', 'skills');
|
|
641
|
+
if (fs.existsSync(path.join(process.cwd(), '.claude'))) {
|
|
642
|
+
fs.mkdirSync(projectClaudeSkills, { recursive: true });
|
|
643
|
+
const projectLink = path.join(projectClaudeSkills, skillName);
|
|
644
|
+
if (!fs.existsSync(projectLink)) {
|
|
645
|
+
try {
|
|
646
|
+
const relTarget = path.relative(projectClaudeSkills, skillDir);
|
|
647
|
+
fs.symlinkSync(relTarget, projectLink);
|
|
648
|
+
console.log(`✓ Linked .claude/skills/${skillName} (project-level)`);
|
|
649
|
+
} catch (e) {
|
|
650
|
+
// Copy fallback
|
|
651
|
+
fs.mkdirSync(projectLink, { recursive: true });
|
|
652
|
+
fs.copyFileSync(path.join(skillDir, 'SKILL.md'), path.join(projectLink, 'SKILL.md'));
|
|
653
|
+
console.log(`✓ Copied to .claude/skills/${skillName} (project-level)`);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Symlink to all AI tool skill directories (always, unless --local)
|
|
659
|
+
if (!isLocal) {
|
|
660
|
+
const home = require('os').homedir();
|
|
661
|
+
const toolDirs = [
|
|
662
|
+
{ dir: path.join(home, '.claude', 'skills'), label: 'Claude' },
|
|
663
|
+
{ dir: path.join(home, '.codex', 'skills'), label: 'Codex' },
|
|
664
|
+
{ dir: path.join(home, '.cursor', 'skills'), label: 'Cursor' },
|
|
665
|
+
];
|
|
666
|
+
|
|
667
|
+
const linked = [];
|
|
668
|
+
for (const { dir, label } of toolDirs) {
|
|
669
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
670
|
+
const linkPath = path.join(dir, skillName);
|
|
671
|
+
if (!fs.existsSync(linkPath)) {
|
|
672
|
+
try {
|
|
673
|
+
fs.symlinkSync(skillDir, linkPath);
|
|
674
|
+
linked.push(label);
|
|
675
|
+
} catch (e) {
|
|
676
|
+
// Copy fallback
|
|
677
|
+
fs.mkdirSync(linkPath, { recursive: true });
|
|
678
|
+
fs.copyFileSync(path.join(skillDir, 'SKILL.md'), path.join(linkPath, 'SKILL.md'));
|
|
679
|
+
linked.push(label);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
if (linked.length > 0) {
|
|
684
|
+
console.log(`✓ Linked to ${linked.join(', ')} (system-level — all tools)`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Summary
|
|
689
|
+
console.log('');
|
|
690
|
+
if (isIntegration) {
|
|
691
|
+
console.log(' Template: integration (bootstrap + API reference + error handling)');
|
|
692
|
+
} else {
|
|
693
|
+
console.log(' Template: standard (workflows + rules)');
|
|
694
|
+
}
|
|
695
|
+
console.log(` Edit: ${path.join(skillDir, 'SKILL.md')}`);
|
|
696
|
+
console.log('');
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// --- LINK subcommand (system-level symlink for existing skills) ---
|
|
700
|
+
|
|
701
|
+
function skillLink(name, ...flags) {
|
|
702
|
+
const isAll = name === '--all';
|
|
703
|
+
|
|
704
|
+
const skillsDir = path.join(process.cwd(), 'atris', 'skills');
|
|
705
|
+
const allSkills = findAllSkills(skillsDir);
|
|
706
|
+
|
|
707
|
+
if (allSkills.length === 0) {
|
|
708
|
+
console.error('No skills found in atris/skills/.');
|
|
709
|
+
process.exit(1);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const targets = isAll
|
|
713
|
+
? allSkills
|
|
714
|
+
: allSkills.filter(s => s.folder === name || s.leafFolder === name);
|
|
715
|
+
|
|
716
|
+
if (targets.length === 0) {
|
|
717
|
+
console.error(`Skill "${name}" not found. Run "atris skill list".`);
|
|
718
|
+
process.exit(1);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const home = require('os').homedir();
|
|
722
|
+
const toolDirs = [
|
|
723
|
+
{ dir: path.join(home, '.claude', 'skills'), label: 'Claude' },
|
|
724
|
+
{ dir: path.join(home, '.codex', 'skills'), label: 'Codex' },
|
|
725
|
+
{ dir: path.join(home, '.cursor', 'skills'), label: 'Cursor' },
|
|
726
|
+
];
|
|
727
|
+
|
|
728
|
+
// Ensure all tool directories exist
|
|
729
|
+
for (const { dir } of toolDirs) {
|
|
730
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
let linked = 0;
|
|
734
|
+
for (const skill of targets) {
|
|
735
|
+
const srcDir = path.dirname(skill.path);
|
|
736
|
+
const linkName = skill.leafFolder;
|
|
737
|
+
const toolsLinked = [];
|
|
738
|
+
|
|
739
|
+
for (const { dir, label } of toolDirs) {
|
|
740
|
+
const linkPath = path.join(dir, linkName);
|
|
741
|
+
|
|
742
|
+
if (fs.existsSync(linkPath)) {
|
|
743
|
+
try {
|
|
744
|
+
const existing = fs.readlinkSync(linkPath);
|
|
745
|
+
if (existing === srcDir || path.resolve(linkPath, '..', existing) === srcDir) {
|
|
746
|
+
continue; // Already linked correctly
|
|
747
|
+
}
|
|
748
|
+
} catch (e) {
|
|
749
|
+
continue; // Not a symlink, skip
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
try {
|
|
754
|
+
fs.symlinkSync(srcDir, linkPath);
|
|
755
|
+
toolsLinked.push(label);
|
|
756
|
+
} catch (e) {
|
|
757
|
+
// silent fail per tool
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (toolsLinked.length > 0) {
|
|
762
|
+
console.log(`✓ ${linkName} → ${toolsLinked.join(', ')}`);
|
|
763
|
+
linked++;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (linked === 0) {
|
|
768
|
+
console.log('All skills already linked at system level.');
|
|
769
|
+
} else {
|
|
770
|
+
console.log(`\n${linked} skill(s) linked to ~/.claude/skills/ (available in all tools).`);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// --- DELETE subcommand ---
|
|
775
|
+
|
|
776
|
+
function skillDelete(name) {
|
|
777
|
+
if (!name) {
|
|
778
|
+
console.error('Usage: atris skill delete <name>');
|
|
779
|
+
process.exit(1);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const home = require('os').homedir();
|
|
783
|
+
const removed = [];
|
|
784
|
+
|
|
785
|
+
// Remove from atris/skills/
|
|
786
|
+
const skillDir = path.join(process.cwd(), 'atris', 'skills', name);
|
|
787
|
+
if (fs.existsSync(skillDir)) {
|
|
788
|
+
fs.rmSync(skillDir, { recursive: true, force: true });
|
|
789
|
+
removed.push(`atris/skills/${name}`);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Remove from atris/customers/ (check all customers)
|
|
793
|
+
const customersDir = path.join(process.cwd(), 'atris', 'customers');
|
|
794
|
+
if (fs.existsSync(customersDir)) {
|
|
795
|
+
const customers = fs.readdirSync(customersDir);
|
|
796
|
+
for (const customer of customers) {
|
|
797
|
+
const custSkillDir = path.join(customersDir, customer, 'skills', name);
|
|
798
|
+
if (fs.existsSync(custSkillDir)) {
|
|
799
|
+
fs.rmSync(custSkillDir, { recursive: true, force: true });
|
|
800
|
+
removed.push(`atris/customers/${customer}/skills/${name}`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Remove symlinks — use unlinkSync for symlinks, rmSync for directories
|
|
806
|
+
function removeLink(linkPath, label) {
|
|
807
|
+
try {
|
|
808
|
+
const stat = fs.lstatSync(linkPath);
|
|
809
|
+
if (stat.isSymbolicLink()) {
|
|
810
|
+
fs.unlinkSync(linkPath);
|
|
811
|
+
} else {
|
|
812
|
+
fs.rmSync(linkPath, { recursive: true, force: true });
|
|
813
|
+
}
|
|
814
|
+
removed.push(label);
|
|
815
|
+
} catch (e) { /* doesn't exist */ }
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Project-level
|
|
819
|
+
removeLink(path.join(process.cwd(), '.claude', 'skills', name), `.claude/skills/${name}`);
|
|
820
|
+
|
|
821
|
+
// System-level — all tool directories
|
|
822
|
+
const toolDirs = [
|
|
823
|
+
{ dir: path.join(home, '.claude', 'skills', name), label: '~/.claude' },
|
|
824
|
+
{ dir: path.join(home, '.codex', 'skills', name), label: '~/.codex' },
|
|
825
|
+
{ dir: path.join(home, '.cursor', 'skills', name), label: '~/.cursor' },
|
|
826
|
+
];
|
|
827
|
+
|
|
828
|
+
for (const { dir, label } of toolDirs) {
|
|
829
|
+
removeLink(dir, `${label}/skills/${name}`);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (removed.length === 0) {
|
|
833
|
+
console.error(`✗ Skill "${name}" not found anywhere.`);
|
|
834
|
+
process.exit(1);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
console.log('');
|
|
838
|
+
for (const r of removed) {
|
|
839
|
+
console.log(`✓ Removed ${r}`);
|
|
840
|
+
}
|
|
841
|
+
console.log('');
|
|
842
|
+
}
|
|
843
|
+
|
|
473
844
|
// --- Main Dispatcher ---
|
|
474
845
|
|
|
475
846
|
function skillCommand(subcommand, ...args) {
|
|
@@ -481,15 +852,38 @@ function skillCommand(subcommand, ...args) {
|
|
|
481
852
|
return skillAudit(args[0] || '--all');
|
|
482
853
|
case 'fix':
|
|
483
854
|
return skillFix(args[0] || '--all');
|
|
855
|
+
case 'create':
|
|
856
|
+
case 'new':
|
|
857
|
+
return skillCreate(args[0], ...args.slice(1));
|
|
858
|
+
case 'link':
|
|
859
|
+
return skillLink(args[0] || '--all', ...args.slice(1));
|
|
860
|
+
case 'delete':
|
|
861
|
+
case 'rm':
|
|
862
|
+
case 'remove':
|
|
863
|
+
return skillDelete(args[0]);
|
|
484
864
|
default:
|
|
485
865
|
console.log('');
|
|
486
866
|
console.log('Usage: atris skill <subcommand> [name]');
|
|
487
867
|
console.log('');
|
|
488
868
|
console.log('Subcommands:');
|
|
489
|
-
console.log('
|
|
869
|
+
console.log(' create <name> Scaffold a new skill with SKILL.md template');
|
|
870
|
+
console.log(' delete <name> Remove a skill and all its symlinks');
|
|
871
|
+
console.log(' link [name|--all] Symlink skills to ~/.claude/skills/ (system-level)');
|
|
872
|
+
console.log(' list Show all skills with compliance status');
|
|
490
873
|
console.log(' audit [name|--all] Validate skill against Anthropic guide');
|
|
491
874
|
console.log(' fix [name|--all] Auto-fix common compliance issues');
|
|
492
875
|
console.log('');
|
|
876
|
+
console.log('Create flags:');
|
|
877
|
+
console.log(' --integration Use integration template (bootstrap + API)');
|
|
878
|
+
console.log(' --local Only link to this project (skip system-level)');
|
|
879
|
+
console.log(' --description="..." Set the skill description');
|
|
880
|
+
console.log('');
|
|
881
|
+
console.log('Examples:');
|
|
882
|
+
console.log(' atris skill create daily-standup');
|
|
883
|
+
console.log(' atris skill create email-outreach --integration');
|
|
884
|
+
console.log(' atris skill create pallet/bol-processor --integration');
|
|
885
|
+
console.log(' atris skill link --all');
|
|
886
|
+
console.log('');
|
|
493
887
|
}
|
|
494
888
|
}
|
|
495
889
|
|
package/commands/sync.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
3
4
|
|
|
4
5
|
function syncAtris() {
|
|
5
6
|
const targetDir = path.join(process.cwd(), 'atris');
|
|
@@ -59,11 +60,11 @@ function syncAtris() {
|
|
|
59
60
|
{ source: 'PERSONA.md', target: 'PERSONA.md' },
|
|
60
61
|
{ source: 'GETTING_STARTED.md', target: 'GETTING_STARTED.md' },
|
|
61
62
|
{ source: 'atris/CLAUDE.md', target: 'CLAUDE.md' },
|
|
62
|
-
{ source: 'atris/team/navigator.md', target: 'team/navigator.md' },
|
|
63
|
-
{ source: 'atris/team/executor.md', target: 'team/executor.md' },
|
|
64
|
-
{ source: 'atris/team/validator.md', target: 'team/validator.md' },
|
|
65
|
-
{ source: 'atris/team/launcher.md', target: 'team/launcher.md' },
|
|
66
|
-
{ source: 'atris/team/brainstormer.md', target: 'team/brainstormer.md' },
|
|
63
|
+
{ source: 'atris/team/navigator/MEMBER.md', target: 'team/navigator/MEMBER.md' },
|
|
64
|
+
{ source: 'atris/team/executor/MEMBER.md', target: 'team/executor/MEMBER.md' },
|
|
65
|
+
{ source: 'atris/team/validator/MEMBER.md', target: 'team/validator/MEMBER.md' },
|
|
66
|
+
{ source: 'atris/team/launcher/MEMBER.md', target: 'team/launcher/MEMBER.md' },
|
|
67
|
+
{ source: 'atris/team/brainstormer/MEMBER.md', target: 'team/brainstormer/MEMBER.md' },
|
|
67
68
|
{ source: 'atris/policies/ANTISLOP.md', target: 'policies/ANTISLOP.md' }
|
|
68
69
|
];
|
|
69
70
|
|
|
@@ -87,6 +88,7 @@ function syncAtris() {
|
|
|
87
88
|
return;
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
fs.mkdirSync(path.dirname(targetFile), { recursive: true });
|
|
90
92
|
fs.copyFileSync(sourceFile, targetFile);
|
|
91
93
|
console.log(`✓ Updated ${target}`);
|
|
92
94
|
updated++;
|
|
@@ -293,83 +295,114 @@ After displaying the boot output, respond to the user naturally.
|
|
|
293
295
|
}
|
|
294
296
|
|
|
295
297
|
/**
|
|
296
|
-
*
|
|
297
|
-
|
|
298
|
+
* Recursively sync files from src to dest. Returns count of files updated.
|
|
299
|
+
*/
|
|
300
|
+
function syncRecursiveCount(src, dest, label, silent) {
|
|
301
|
+
let count = 0;
|
|
302
|
+
if (!fs.existsSync(dest)) {
|
|
303
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
304
|
+
}
|
|
305
|
+
const entries = fs.readdirSync(src);
|
|
306
|
+
for (const entry of entries) {
|
|
307
|
+
const srcPath = path.join(src, entry);
|
|
308
|
+
const destPath = path.join(dest, entry);
|
|
309
|
+
|
|
310
|
+
if (fs.statSync(srcPath).isDirectory()) {
|
|
311
|
+
count += syncRecursiveCount(srcPath, destPath, `${label}/${entry}`, silent);
|
|
312
|
+
} else {
|
|
313
|
+
const srcContent = fs.readFileSync(srcPath, 'utf8');
|
|
314
|
+
const destContent = fs.existsSync(destPath) ? fs.readFileSync(destPath, 'utf8') : '';
|
|
315
|
+
if (srcContent !== destContent) {
|
|
316
|
+
fs.writeFileSync(destPath, srcContent);
|
|
317
|
+
if (entry.endsWith('.sh')) {
|
|
318
|
+
fs.chmodSync(destPath, 0o755);
|
|
319
|
+
}
|
|
320
|
+
if (!silent) {
|
|
321
|
+
console.log(`✓ Updated ${label}/${entry}`);
|
|
322
|
+
}
|
|
323
|
+
count++;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return count;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Lightweight skill-only sync. Syncs skills from the npm package to:
|
|
332
|
+
* 1. Global skill dirs (~/.claude/skills/, ~/.codex/skills/) — always, if they exist
|
|
333
|
+
* 2. Project-level (atris/skills/ + .claude/skills/ symlinks) — if in a project
|
|
334
|
+
*
|
|
335
|
+
* Global = baseline truth. Project = optional override.
|
|
298
336
|
* Returns number of files updated (0 = already current).
|
|
299
337
|
*/
|
|
300
338
|
function syncSkills({ silent = false } = {}) {
|
|
301
|
-
const targetDir = path.join(process.cwd(), 'atris');
|
|
302
339
|
const packageSkillsDir = path.join(__dirname, '..', 'atris', 'skills');
|
|
303
|
-
|
|
304
|
-
const claudeSkillsBaseDir = path.join(process.cwd(), '.claude', 'skills');
|
|
305
|
-
|
|
306
|
-
if (!fs.existsSync(targetDir) || !fs.existsSync(packageSkillsDir)) {
|
|
340
|
+
if (!fs.existsSync(packageSkillsDir)) {
|
|
307
341
|
return 0;
|
|
308
342
|
}
|
|
309
343
|
|
|
310
|
-
if (!fs.existsSync(userSkillsDir)) {
|
|
311
|
-
fs.mkdirSync(userSkillsDir, { recursive: true });
|
|
312
|
-
}
|
|
313
|
-
if (!fs.existsSync(claudeSkillsBaseDir)) {
|
|
314
|
-
fs.mkdirSync(claudeSkillsBaseDir, { recursive: true });
|
|
315
|
-
}
|
|
316
|
-
|
|
317
344
|
let updated = 0;
|
|
345
|
+
const homeDir = os.homedir();
|
|
318
346
|
|
|
319
347
|
const skillFolders = fs.readdirSync(packageSkillsDir).filter(f =>
|
|
320
348
|
fs.statSync(path.join(packageSkillsDir, f)).isDirectory()
|
|
321
349
|
);
|
|
322
350
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
351
|
+
// --- 1. Global skill directories (sync if they exist) ---
|
|
352
|
+
const globalSkillDirs = [
|
|
353
|
+
path.join(homeDir, '.claude', 'skills'),
|
|
354
|
+
path.join(homeDir, '.codex', 'skills'),
|
|
355
|
+
];
|
|
327
356
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
if (fs.statSync(srcPath).isDirectory()) {
|
|
339
|
-
syncRecursive(srcPath, destPath, skillName, relPath);
|
|
340
|
-
} else {
|
|
341
|
-
const srcContent = fs.readFileSync(srcPath, 'utf8');
|
|
342
|
-
const destContent = fs.existsSync(destPath) ? fs.readFileSync(destPath, 'utf8') : '';
|
|
343
|
-
if (srcContent !== destContent) {
|
|
344
|
-
fs.writeFileSync(destPath, srcContent);
|
|
345
|
-
if (entry.endsWith('.sh')) {
|
|
346
|
-
fs.chmodSync(destPath, 0o755);
|
|
347
|
-
}
|
|
348
|
-
if (!silent) {
|
|
349
|
-
console.log(`✓ Updated atris/skills/${skillName}/${relPath}`);
|
|
350
|
-
}
|
|
351
|
-
updated++;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
357
|
+
for (const globalDir of globalSkillDirs) {
|
|
358
|
+
if (!fs.existsSync(globalDir)) continue;
|
|
359
|
+
const dirName = path.basename(path.dirname(globalDir)); // .claude or .codex
|
|
360
|
+
|
|
361
|
+
for (const skill of skillFolders) {
|
|
362
|
+
const srcSkillDir = path.join(packageSkillsDir, skill);
|
|
363
|
+
const destSkillDir = path.join(globalDir, skill);
|
|
364
|
+
|
|
365
|
+
if (fs.existsSync(destSkillDir) || fs.existsSync(globalDir)) {
|
|
366
|
+
updated += syncRecursiveCount(srcSkillDir, destSkillDir, `~/${dirName}/skills/${skill}`, silent);
|
|
354
367
|
}
|
|
355
|
-
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
356
370
|
|
|
357
|
-
|
|
371
|
+
// --- 2. Project-level (only if inside an atris project) ---
|
|
372
|
+
const targetDir = path.join(process.cwd(), 'atris');
|
|
373
|
+
if (fs.existsSync(targetDir)) {
|
|
374
|
+
const userSkillsDir = path.join(targetDir, 'skills');
|
|
375
|
+
const claudeSkillsBaseDir = path.join(process.cwd(), '.claude', 'skills');
|
|
358
376
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
377
|
+
if (!fs.existsSync(userSkillsDir)) {
|
|
378
|
+
fs.mkdirSync(userSkillsDir, { recursive: true });
|
|
379
|
+
}
|
|
380
|
+
if (!fs.existsSync(claudeSkillsBaseDir)) {
|
|
381
|
+
fs.mkdirSync(claudeSkillsBaseDir, { recursive: true });
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
for (const skill of skillFolders) {
|
|
385
|
+
const srcSkillDir = path.join(packageSkillsDir, skill);
|
|
386
|
+
const destSkillDir = path.join(userSkillsDir, skill);
|
|
387
|
+
const symlinkPath = path.join(claudeSkillsBaseDir, skill);
|
|
388
|
+
|
|
389
|
+
updated += syncRecursiveCount(srcSkillDir, destSkillDir, `atris/skills/${skill}`, silent);
|
|
390
|
+
|
|
391
|
+
// Create symlink if doesn't exist
|
|
392
|
+
if (!fs.existsSync(symlinkPath)) {
|
|
393
|
+
const relativePath = path.join('..', '..', 'atris', 'skills', skill);
|
|
394
|
+
try {
|
|
395
|
+
fs.symlinkSync(relativePath, symlinkPath);
|
|
396
|
+
if (!silent) {
|
|
397
|
+
console.log(`✓ Linked .claude/skills/${skill}`);
|
|
398
|
+
}
|
|
399
|
+
} catch (e) {
|
|
400
|
+
// Fallback: copy instead of symlink
|
|
401
|
+
fs.mkdirSync(symlinkPath, { recursive: true });
|
|
402
|
+
const skillFile = path.join(destSkillDir, 'SKILL.md');
|
|
403
|
+
if (fs.existsSync(skillFile)) {
|
|
404
|
+
fs.copyFileSync(skillFile, path.join(symlinkPath, 'SKILL.md'));
|
|
405
|
+
}
|
|
373
406
|
}
|
|
374
407
|
}
|
|
375
408
|
}
|
package/package.json
CHANGED