atris 2.3.3 → 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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: atris
3
- description: "Codebase intelligence — generates structured navigation maps with file:line references so agents stop re-scanning the same files every session. Use when exploring code, answering 'where is X?', or onboarding to a new codebase."
3
+ description: Codebase intelligence — generates structured navigation maps with file:line references so agents stop re-scanning the same files every session. Use when exploring code, answering "where is X?", or onboarding to a new codebase.
4
4
  version: 1.1.0
5
5
  requires:
6
6
  bins:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: drive
3
3
  description: Google Drive integration via AtrisOS API. Browse, search, read, upload files and work with Google Sheets. Use when user asks about Drive, files, docs, sheets, or spreadsheets.
4
- version: 1.2.0
4
+ version: 1.1.0
5
5
  tags:
6
6
  - drive
7
7
  - backend
@@ -199,29 +199,6 @@ curl -s -X POST "https://api.atris.ai/api/integrations/google-drive/files" \
199
199
  }'
200
200
  ```
201
201
 
202
- ### Update Existing File
203
- ```bash
204
- curl -s -X PUT "https://api.atris.ai/api/integrations/google-drive/files/{file_id}" \
205
- -H "Authorization: Bearer $TOKEN" \
206
- -H "Content-Type: application/json" \
207
- -d '{
208
- "content": "Updated file content here",
209
- "mime_type": "text/plain"
210
- }'
211
- ```
212
-
213
- **Update content and rename:**
214
- ```bash
215
- curl -s -X PUT "https://api.atris.ai/api/integrations/google-drive/files/{file_id}" \
216
- -H "Authorization: Bearer $TOKEN" \
217
- -H "Content-Type: application/json" \
218
- -d '{
219
- "content": "New content",
220
- "mime_type": "text/plain",
221
- "name": "renamed-file.txt"
222
- }'
223
- ```
224
-
225
202
  ---
226
203
 
227
204
  ## Google Sheets
@@ -322,14 +299,6 @@ curl -s -X POST "https://api.atris.ai/api/integrations/google-drive/sheets/{spre
322
299
  3. **Confirm with user**: "Upload {filename} to Drive?"
323
300
  4. Upload: `POST /google-drive/files` with `{name, content, mime_type}`
324
301
 
325
- ### "Edit a file on Drive"
326
- 1. Run bootstrap
327
- 2. Find the file: `GET /google-drive/search?q=FILENAME`
328
- 3. Read current content: `GET /google-drive/files/{id}/export?mime_type=text/plain`
329
- 4. Make edits
330
- 5. **Show user the changes for approval**
331
- 6. Update: `PUT /google-drive/files/{id}` with `{content, mime_type}`
332
-
333
302
  ---
334
303
 
335
304
  ## Error Handling
@@ -387,13 +356,8 @@ curl -s -X POST "https://api.atris.ai/api/integrations/google-drive/sheets/{id}/
387
356
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
388
357
  -d '{"range":"Sheet1","values":[["Alice",95]]}'
389
358
 
390
- # Upload a new file
359
+ # Upload a file
391
360
  curl -s -X POST "https://api.atris.ai/api/integrations/google-drive/files" \
392
361
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
393
362
  -d '{"name":"notes.txt","content":"Hello world","mime_type":"text/plain"}'
394
-
395
- # Update an existing file
396
- curl -s -X PUT "https://api.atris.ai/api/integrations/google-drive/files/{file_id}" \
397
- -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
398
- -d '{"content":"Updated content","mime_type":"text/plain"}'
399
363
  ```
@@ -250,17 +250,6 @@ curl -s -X DELETE "https://api.atris.ai/api/integrations/gmail/drafts/{draft_id}
250
250
  -H "Authorization: Bearer $TOKEN"
251
251
  ```
252
252
 
253
- ### Mark as Read / Unread
254
- ```bash
255
- # Mark as read
256
- curl -s -X POST "https://api.atris.ai/api/integrations/gmail/messages/{message_id}/read" \
257
- -H "Authorization: Bearer $TOKEN"
258
-
259
- # Mark as unread
260
- curl -s -X POST "https://api.atris.ai/api/integrations/gmail/messages/{message_id}/unread" \
261
- -H "Authorization: Bearer $TOKEN"
262
- ```
263
-
264
253
  ### Archive Email
265
254
  ```bash
266
255
  # Single message
@@ -274,19 +263,6 @@ curl -s -X POST "https://api.atris.ai/api/integrations/gmail/messages/batch-arch
274
263
  -d '{"message_ids": ["id1", "id2", "id3"]}'
275
264
  ```
276
265
 
277
- ### Trash Email
278
- ```bash
279
- # Single message
280
- curl -s -X POST "https://api.atris.ai/api/integrations/gmail/messages/{message_id}/trash" \
281
- -H "Authorization: Bearer $TOKEN"
282
-
283
- # Batch trash
284
- curl -s -X POST "https://api.atris.ai/api/integrations/gmail/messages/batch-trash" \
285
- -H "Authorization: Bearer $TOKEN" \
286
- -H "Content-Type: application/json" \
287
- -d '{"message_ids": ["id1", "id2", "id3"]}'
288
- ```
289
-
290
266
  ### Check Status
291
267
  ```bash
292
268
  curl -s "https://api.atris.ai/api/integrations/gmail/status" \
@@ -434,12 +410,6 @@ curl -s -X POST "https://api.atris.ai/api/integrations/gmail/drafts" \
434
410
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
435
411
  -d '{"to":"email@example.com","subject":"Hi","body":"Draft text"}'
436
412
 
437
- # Mark as read
438
- curl -s -X POST "https://api.atris.ai/api/integrations/gmail/messages/{message_id}/read" -H "Authorization: Bearer $TOKEN"
439
-
440
- # Trash an email
441
- curl -s -X POST "https://api.atris.ai/api/integrations/gmail/messages/{message_id}/trash" -H "Authorization: Bearer $TOKEN"
442
-
443
413
  # Send a draft
444
414
  curl -s -X POST "https://api.atris.ai/api/integrations/gmail/drafts/{draft_id}/send" \
445
415
  -H "Authorization: Bearer $TOKEN"
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: slack
3
3
  description: Slack integration via AtrisOS API. Read messages, send as yourself, search conversations, manage DMs. Use when user asks about Slack, messages, channels, or team communication.
4
- version: 1.1.0
4
+ version: 1.0.0
5
5
  tags:
6
6
  - slack
7
7
  - backend
@@ -187,14 +187,6 @@ curl -s "https://api.atris.ai/api/integrations/slack/users" \
187
187
  -H "Authorization: Bearer $TOKEN"
188
188
  ```
189
189
 
190
- ### Search Users
191
- ```bash
192
- curl -s "https://api.atris.ai/api/integrations/slack/users/search?q=justin" \
193
- -H "Authorization: Bearer $TOKEN"
194
- ```
195
-
196
- Searches by name, display name, or email. Much faster than pulling the full user list.
197
-
198
190
  ### Send as Bot
199
191
  ```bash
200
192
  curl -s -X POST "https://api.atris.ai/api/integrations/slack/test-send" \
@@ -231,9 +223,9 @@ curl -s -X DELETE "https://api.atris.ai/api/integrations/slack" \
231
223
 
232
224
  ### "Send a message to someone"
233
225
  1. Run bootstrap
234
- 2. Find the user: `GET /slack/users/search?q=NAME`
226
+ 2. Find the user: `GET /slack/users` (search by name/email)
235
227
  3. **Show user the draft for approval**
236
- 4. Send DM: `POST /slack/me/dm` with `{slack_user_id, text}`
228
+ 4. Send DM: `POST /slack/me/dm` with `{user_id, text}`
237
229
  5. Confirm: "Message sent!"
238
230
 
239
231
  ### "Reply in a channel"
@@ -251,7 +243,7 @@ curl -s -X DELETE "https://api.atris.ai/api/integrations/slack" \
251
243
 
252
244
  ### "What did [person] say to me?"
253
245
  1. Run bootstrap
254
- 2. Find user: `GET /slack/users/search?q=NAME` (get user ID)
246
+ 2. List users: `GET /slack/users` (find user ID)
255
247
  3. List DMs: `GET /slack/me/dms` (find DM channel with that user)
256
248
  4. Read messages: `GET /slack/me/messages/{channel_id}`
257
249
  5. Display conversation
@@ -325,7 +317,4 @@ curl -s "https://api.atris.ai/api/integrations/slack/me/search?q=project+update"
325
317
 
326
318
  # List workspace users
327
319
  curl -s "https://api.atris.ai/api/integrations/slack/users" -H "Authorization: Bearer $TOKEN"
328
-
329
- # Search users by name
330
- curl -s "https://api.atris.ai/api/integrations/slack/users/search?q=justin" -H "Authorization: Bearer $TOKEN"
331
320
  ```
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: writing
3
- description: "Essay writing skill. Triggers on: essay, draft, write, outline"
3
+ description: Essay writing skill. Triggers on: essay, draft, write, outline
4
4
  version: 1.0.0
5
5
  allowed-tools: Read, Write, Edit, Glob, Grep
6
6
  tags:
@@ -1,6 +1,21 @@
1
- # brainstormer.md — Idea & Reality Shaper
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
- > **Role:** Shape ideas, explore possibilities, adapt to user depth | **Source:** Inbox items, raw ideas
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
- # executor.md — Builder (The Trigger)
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
- > **Role:** Execute from build.md, one step at a time | **Source:** build.md, MAP.md
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
- # launcher.md — The Closer
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
- > **Role:** Document, capture learnings, publish, celebrate | **Source:** Completed tasks, validation results
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
- # navigator.md — Planner
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
- > **Role:** Transform messy human intent into precise execution plans | **Source:** idea.md, MAP.md
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
- # validator.md — Reviewer (The Safety)
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
- > **Role:** Validate execution, update docs, ensure quality | **Source:** build.md, MAP.md, code
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 navigatorSource = path.join(__dirname, '..', 'atris', 'team', 'navigator.md');
902
- const executorSource = path.join(__dirname, '..', 'atris', 'team', 'executor.md');
903
- const validatorSource = path.join(__dirname, '..', 'atris', 'team', 'validator.md');
904
- const launcherSource = path.join(__dirname, '..', 'atris', 'team', 'launcher.md');
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
- if (!fs.existsSync(navigatorFile) && fs.existsSync(navigatorSource)) {
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
- if (!fs.existsSync(launcherFile) && fs.existsSync(launcherSource)) {
922
- fs.copyFileSync(launcherSource, launcherFile);
923
- console.log('✓ Created team/launcher.md');
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
- const navigatorSource = path.join(__dirname, '..', 'atris', 'team', 'navigator.md');
479
- const executorSource = path.join(__dirname, '..', 'atris', 'team', 'executor.md');
480
- const validatorSource = path.join(__dirname, '..', 'atris', 'team', 'validator.md');
481
- const launcherSource = path.join(__dirname, '..', 'atris', 'team', 'launcher.md');
482
- const brainstormerSource = path.join(__dirname, '..', 'atris', 'team', 'brainstormer.md');
483
-
484
- if (!fs.existsSync(navigatorFile) && fs.existsSync(navigatorSource)) {
485
- fs.copyFileSync(navigatorSource, navigatorFile);
486
- console.log('✓ Created team/navigator.md');
487
- }
488
-
489
- if (!fs.existsSync(executorFile) && fs.existsSync(executorSource)) {
490
- fs.copyFileSync(executorSource, executorFile);
491
- console.log('✓ Created team/executor.md');
492
- }
493
-
494
- if (!fs.existsSync(validatorFile) && fs.existsSync(validatorSource)) {
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(' list Show all skills with compliance status');
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
@@ -60,11 +60,11 @@ function syncAtris() {
60
60
  { source: 'PERSONA.md', target: 'PERSONA.md' },
61
61
  { source: 'GETTING_STARTED.md', target: 'GETTING_STARTED.md' },
62
62
  { source: 'atris/CLAUDE.md', target: 'CLAUDE.md' },
63
- { source: 'atris/team/navigator.md', target: 'team/navigator.md' },
64
- { source: 'atris/team/executor.md', target: 'team/executor.md' },
65
- { source: 'atris/team/validator.md', target: 'team/validator.md' },
66
- { source: 'atris/team/launcher.md', target: 'team/launcher.md' },
67
- { 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' },
68
68
  { source: 'atris/policies/ANTISLOP.md', target: 'policies/ANTISLOP.md' }
69
69
  ];
70
70
 
@@ -88,6 +88,7 @@ function syncAtris() {
88
88
  return;
89
89
  }
90
90
 
91
+ fs.mkdirSync(path.dirname(targetFile), { recursive: true });
91
92
  fs.copyFileSync(sourceFile, targetFile);
92
93
  console.log(`✓ Updated ${target}`);
93
94
  updated++;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "2.3.3",
3
+ "version": "2.3.4",
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": {