atris 2.3.0 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/atris/skills/calendar/SKILL.md +3 -3
- package/atris/skills/email-agent/SKILL.md +42 -2
- package/bin/atris.js +30 -106
- package/commands/auth.js +97 -2
- package/commands/integrations.js +26 -12
- package/commands/plugin.js +450 -0
- package/commands/sync.js +87 -1
- package/package.json +1 -1
- package/utils/auth.js +72 -3
|
@@ -144,12 +144,12 @@ curl -s -X POST "https://api.atris.ai/api/integrations/google-calendar/events" \
|
|
|
144
144
|
-H "Authorization: Bearer $TOKEN" \
|
|
145
145
|
-H "Content-Type: application/json" \
|
|
146
146
|
-d '{
|
|
147
|
-
"summary": "Meeting with
|
|
147
|
+
"summary": "Meeting with Hugo",
|
|
148
148
|
"start": "2026-02-15T14:00:00-08:00",
|
|
149
149
|
"end": "2026-02-15T15:00:00-08:00",
|
|
150
|
-
"description": "Discuss
|
|
150
|
+
"description": "Discuss project roadmap",
|
|
151
151
|
"location": "Zoom",
|
|
152
|
-
"attendees": ["
|
|
152
|
+
"attendees": ["hugo@atrismail.com"],
|
|
153
153
|
"timezone": "America/Los_Angeles"
|
|
154
154
|
}'
|
|
155
155
|
```
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: email-agent
|
|
3
3
|
description: Gmail integration via AtrisOS API. Read, send, archive emails. Use when user asks about email, inbox, or wants to send/check messages.
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
tags:
|
|
6
6
|
- email-agent
|
|
7
7
|
- backend
|
|
@@ -158,6 +158,33 @@ curl -s -X POST "https://api.atris.ai/api/integrations/gmail/send" \
|
|
|
158
158
|
}'
|
|
159
159
|
```
|
|
160
160
|
|
|
161
|
+
**Reply in thread (IMPORTANT — use this for all replies):**
|
|
162
|
+
|
|
163
|
+
To reply within an existing email thread, you MUST pass `thread_id` and `reply_to_message_id`. Without these, Gmail creates a new thread.
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
# 1. First, get the message you're replying to (extract thread_id and id)
|
|
167
|
+
curl -s "https://api.atris.ai/api/integrations/gmail/messages/{message_id}" \
|
|
168
|
+
-H "Authorization: Bearer $TOKEN"
|
|
169
|
+
# Response includes: id, thread_id, subject, from, etc.
|
|
170
|
+
|
|
171
|
+
# 2. Send reply in the same thread
|
|
172
|
+
curl -s -X POST "https://api.atris.ai/api/integrations/gmail/send" \
|
|
173
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
174
|
+
-H "Content-Type: application/json" \
|
|
175
|
+
-d '{
|
|
176
|
+
"to": "original-sender@example.com",
|
|
177
|
+
"subject": "Re: Original Subject",
|
|
178
|
+
"body": "Your reply text here",
|
|
179
|
+
"thread_id": "THREAD_ID_FROM_STEP_1",
|
|
180
|
+
"reply_to_message_id": "MESSAGE_ID_FROM_STEP_1"
|
|
181
|
+
}'
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
- `thread_id` — The thread ID from the original message. Tells Gmail which thread to add this to.
|
|
185
|
+
- `reply_to_message_id` — The message ID you're replying to. The backend uses this to set `In-Reply-To` and `References` headers so Gmail threads it correctly.
|
|
186
|
+
- `subject` — Must match the original subject with "Re: " prefix.
|
|
187
|
+
|
|
161
188
|
**With attachments:**
|
|
162
189
|
```bash
|
|
163
190
|
curl -s -X POST "https://api.atris.ai/api/integrations/gmail/send" \
|
|
@@ -264,6 +291,14 @@ curl -s -X DELETE "https://api.atris.ai/api/integrations/gmail" \
|
|
|
264
291
|
4. On approval: `POST /send` with `{to, subject, body}`
|
|
265
292
|
5. Confirm: "Email sent!"
|
|
266
293
|
|
|
294
|
+
### "Reply to this email"
|
|
295
|
+
1. Run bootstrap
|
|
296
|
+
2. Read the message: `GET /messages/{message_id}` — extract `id`, `thread_id`, `from`, `subject`
|
|
297
|
+
3. Draft reply content
|
|
298
|
+
4. **Show user the reply for approval**
|
|
299
|
+
5. On approval: `POST /send` with `{to, subject: "Re: ...", body, thread_id, reply_to_message_id}`
|
|
300
|
+
6. Verify: response `thread_id` matches original thread_id (if it doesn't, something went wrong)
|
|
301
|
+
|
|
267
302
|
### "Clean up my inbox"
|
|
268
303
|
1. Run bootstrap
|
|
269
304
|
2. List: `GET /messages?query=in:inbox&max_results=50`
|
|
@@ -357,11 +392,16 @@ curl -s "https://api.atris.ai/api/integrations/gmail/status" -H "Authorization:
|
|
|
357
392
|
# List inbox
|
|
358
393
|
curl -s "https://api.atris.ai/api/integrations/gmail/messages?query=in:inbox&max_results=10" -H "Authorization: Bearer $TOKEN"
|
|
359
394
|
|
|
360
|
-
# Send email
|
|
395
|
+
# Send new email
|
|
361
396
|
curl -s -X POST "https://api.atris.ai/api/integrations/gmail/send" \
|
|
362
397
|
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
|
363
398
|
-d '{"to":"email@example.com","subject":"Hi","body":"Hello!"}'
|
|
364
399
|
|
|
400
|
+
# Reply in thread (pass thread_id + reply_to_message_id)
|
|
401
|
+
curl -s -X POST "https://api.atris.ai/api/integrations/gmail/send" \
|
|
402
|
+
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
|
403
|
+
-d '{"to":"sender@example.com","subject":"Re: Original","body":"Reply text","thread_id":"THREAD_ID","reply_to_message_id":"MSG_ID"}'
|
|
404
|
+
|
|
365
405
|
# List drafts
|
|
366
406
|
curl -s "https://api.atris.ai/api/integrations/gmail/drafts" -H "Authorization: Bearer $TOKEN"
|
|
367
407
|
|
package/bin/atris.js
CHANGED
|
@@ -56,6 +56,17 @@ if (!skipUpdateCheck && (!process.argv[2] || (process.argv[2] && !['version', 'u
|
|
|
56
56
|
|
|
57
57
|
const command = process.argv[2];
|
|
58
58
|
|
|
59
|
+
// Auto-sync skills on every command (fast — just file diffs, no network)
|
|
60
|
+
try {
|
|
61
|
+
const { syncSkills } = require('../commands/sync');
|
|
62
|
+
const skillsUpdated = syncSkills({ silent: true });
|
|
63
|
+
if (skillsUpdated > 0) {
|
|
64
|
+
console.log(`⬆️ ${skillsUpdated} skill${skillsUpdated > 1 ? 's' : ''} updated`);
|
|
65
|
+
}
|
|
66
|
+
} catch (e) {
|
|
67
|
+
// Non-critical
|
|
68
|
+
}
|
|
69
|
+
|
|
59
70
|
const TOKEN_REFRESH_BUFFER_SECONDS = 300; // Refresh ~5 minutes before expiry
|
|
60
71
|
|
|
61
72
|
function decodeJwtClaims(token) {
|
|
@@ -197,6 +208,8 @@ function showHelp() {
|
|
|
197
208
|
console.log(' login - Authenticate (use --token <t> for non-interactive)');
|
|
198
209
|
console.log(' logout - Remove credentials');
|
|
199
210
|
console.log(' whoami - Show auth status');
|
|
211
|
+
console.log(' switch - Switch account (atris switch <name>)');
|
|
212
|
+
console.log(' accounts - List saved accounts');
|
|
200
213
|
console.log('');
|
|
201
214
|
console.log('Integrations:');
|
|
202
215
|
console.log(' gmail - Email commands (inbox, read)');
|
|
@@ -210,6 +223,11 @@ function showHelp() {
|
|
|
210
223
|
console.log(' skill audit [name] - Validate skill against Anthropic guide');
|
|
211
224
|
console.log(' skill fix [name] - Auto-fix common compliance issues');
|
|
212
225
|
console.log('');
|
|
226
|
+
console.log('Plugin:');
|
|
227
|
+
console.log(' plugin build - Package skills as .plugin for Cowork');
|
|
228
|
+
console.log(' plugin publish - Sync skills to marketplace repo and push');
|
|
229
|
+
console.log(' plugin info - Preview what will be included');
|
|
230
|
+
console.log('');
|
|
213
231
|
console.log('Other:');
|
|
214
232
|
console.log(' version - Show Atris version');
|
|
215
233
|
console.log(' help - Show this help');
|
|
@@ -304,7 +322,7 @@ const { initAtris: initCmd } = require('../commands/init');
|
|
|
304
322
|
const { syncAtris: syncCmd } = require('../commands/sync');
|
|
305
323
|
const { logAtris: logCmd } = require('../commands/log');
|
|
306
324
|
const { logSyncAtris: logSyncCmd } = require('../commands/log-sync');
|
|
307
|
-
const { loginAtris: loginCmd, logoutAtris: logoutCmd, whoamiAtris: whoamiCmd } = require('../commands/auth');
|
|
325
|
+
const { loginAtris: loginCmd, logoutAtris: logoutCmd, whoamiAtris: whoamiCmd, switchAccount: switchCmd, listAccountsCmd: accountsCmd } = require('../commands/auth');
|
|
308
326
|
const { showVersion: versionCmd } = require('../commands/version');
|
|
309
327
|
const { planAtris: planCmd, doAtris: doCmd, reviewAtris: reviewCmd } = require('../commands/workflow');
|
|
310
328
|
const { visualizeAtris: visualizeCmd } = require('../commands/visualize');
|
|
@@ -316,11 +334,12 @@ const { analyticsAtris: analyticsCmd } = require('../commands/analytics');
|
|
|
316
334
|
const { cleanAtris: cleanCmd } = require('../commands/clean');
|
|
317
335
|
const { verifyAtris: verifyCmd } = require('../commands/verify');
|
|
318
336
|
const { skillCommand: skillCmd } = require('../commands/skill');
|
|
337
|
+
const { pluginCommand: pluginCmd } = require('../commands/plugin');
|
|
319
338
|
|
|
320
339
|
// Check if this is a known command or natural language input
|
|
321
340
|
const knownCommands = ['init', 'log', 'status', 'analytics', 'visualize', 'brainstorm', 'autopilot', 'plan', 'do', 'review',
|
|
322
|
-
'activate', 'agent', 'chat', 'login', 'logout', 'whoami', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
|
|
323
|
-
'clean', 'verify', 'search', 'skill',
|
|
341
|
+
'activate', 'agent', 'chat', 'login', 'logout', 'whoami', 'switch', 'accounts', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
|
|
342
|
+
'clean', 'verify', 'search', 'skill', 'plugin',
|
|
324
343
|
'gmail', 'calendar', 'twitter', 'slack', 'integrations'];
|
|
325
344
|
|
|
326
345
|
// Check if command is an atris.md spec file - triggers welcome visualization
|
|
@@ -650,6 +669,10 @@ if (command === 'init') {
|
|
|
650
669
|
logoutCmd();
|
|
651
670
|
} else if (command === 'whoami') {
|
|
652
671
|
whoamiCmd();
|
|
672
|
+
} else if (command === 'switch') {
|
|
673
|
+
switchCmd();
|
|
674
|
+
} else if (command === 'accounts') {
|
|
675
|
+
accountsCmd();
|
|
653
676
|
} else if (command === 'visualize') {
|
|
654
677
|
console.log('ℹ️ "atris visualize" is a legacy helper. Visualization is now built into "atris plan".');
|
|
655
678
|
console.log(' Prefer: atris plan');
|
|
@@ -802,6 +825,10 @@ if (command === 'init') {
|
|
|
802
825
|
const subcommand = process.argv[3];
|
|
803
826
|
const args = process.argv.slice(4);
|
|
804
827
|
skillCmd(subcommand, ...args);
|
|
828
|
+
} else if (command === 'plugin') {
|
|
829
|
+
const subcommand = process.argv[3] || 'build';
|
|
830
|
+
const args = process.argv.slice(4);
|
|
831
|
+
pluginCmd(subcommand, ...args);
|
|
805
832
|
} else {
|
|
806
833
|
console.log(`Unknown command: ${command}`);
|
|
807
834
|
console.log('Run "atris help" to see available commands');
|
|
@@ -2633,109 +2660,6 @@ async function atrisDevEntry(userInput = null) {
|
|
|
2633
2660
|
console.log('');
|
|
2634
2661
|
}
|
|
2635
2662
|
|
|
2636
|
-
function launchAtris() {
|
|
2637
|
-
const targetDir = path.join(process.cwd(), 'atris');
|
|
2638
|
-
const launcherFile = path.join(targetDir, 'team', 'launcher.md');
|
|
2639
|
-
|
|
2640
|
-
if (!fs.existsSync(launcherFile)) {
|
|
2641
|
-
console.log('✗ launcher.md not found. Run "atris init" first.');
|
|
2642
|
-
process.exit(1);
|
|
2643
|
-
}
|
|
2644
|
-
|
|
2645
|
-
// Read launcher.md
|
|
2646
|
-
const launcherSpec = fs.readFileSync(launcherFile, 'utf8');
|
|
2647
|
-
|
|
2648
|
-
// Reference TODO.md (agents read on-demand, legacy TASK_CONTEXTS.md supported)
|
|
2649
|
-
const todoFile = path.join(targetDir, 'TODO.md');
|
|
2650
|
-
const legacyTaskContextsFile = path.join(targetDir, 'TASK_CONTEXTS.md');
|
|
2651
|
-
|
|
2652
|
-
// Reference MAP.md (agents read on-demand)
|
|
2653
|
-
const mapFile = path.join(targetDir, 'MAP.md');
|
|
2654
|
-
const mapPath = fs.existsSync(mapFile) ? path.relative(process.cwd(), mapFile) : null;
|
|
2655
|
-
|
|
2656
|
-
// Reference journal (agents read on-demand)
|
|
2657
|
-
const { logFile, dateFormatted } = getLogPath();
|
|
2658
|
-
let journalPath = '';
|
|
2659
|
-
if (fs.existsSync(logFile)) {
|
|
2660
|
-
journalPath = path.relative(process.cwd(), logFile);
|
|
2661
|
-
}
|
|
2662
|
-
|
|
2663
|
-
console.log('');
|
|
2664
|
-
console.log('┌─────────────────────────────────────────────────────────────┐');
|
|
2665
|
-
console.log('│ Atris Launch — Launcher Agent Activated │');
|
|
2666
|
-
console.log('└─────────────────────────────────────────────────────────────┘');
|
|
2667
|
-
console.log('');
|
|
2668
|
-
console.log('📋 AGENT SPEC:');
|
|
2669
|
-
console.log('─────────────────────────────────────────────────────────────');
|
|
2670
|
-
console.log(launcherSpec);
|
|
2671
|
-
console.log('');
|
|
2672
|
-
console.log('─────────────────────────────────────────────────────────────');
|
|
2673
|
-
console.log('');
|
|
2674
|
-
const taskFilePath = fs.existsSync(todoFile)
|
|
2675
|
-
? todoFile
|
|
2676
|
-
: (fs.existsSync(legacyTaskContextsFile) ? legacyTaskContextsFile : null);
|
|
2677
|
-
const taskContextsPath = taskFilePath ? path.relative(process.cwd(), taskFilePath) : null;
|
|
2678
|
-
console.log('📝 TODO.md: ' + (taskContextsPath || 'Not found'));
|
|
2679
|
-
console.log(' Read for completed tasks context (usually small, or reference path if large).');
|
|
2680
|
-
console.log('');
|
|
2681
|
-
console.log('─────────────────────────────────────────────────────────────');
|
|
2682
|
-
console.log('');
|
|
2683
|
-
console.log('🗺️ MAP.md: ' + (mapPath || 'Not found'));
|
|
2684
|
-
console.log(' Read this file for file:line references when navigating the codebase.');
|
|
2685
|
-
console.log('');
|
|
2686
|
-
console.log('─────────────────────────────────────────────────────────────');
|
|
2687
|
-
console.log('');
|
|
2688
|
-
console.log('📅 JOURNAL: ' + (journalPath || 'Not found'));
|
|
2689
|
-
console.log(' Read for recent completions and context.');
|
|
2690
|
-
console.log('');
|
|
2691
|
-
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
2692
|
-
console.log('📋 INSTRUCTION PROMPT FOR YOUR CODING AGENT:');
|
|
2693
|
-
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
2694
|
-
console.log('');
|
|
2695
|
-
console.log('You are the Launcher. Ship it clean.');
|
|
2696
|
-
console.log('');
|
|
2697
|
-
console.log('⚠️ CRITICAL: Execute these steps NOW using terminal and file tools:');
|
|
2698
|
-
console.log('');
|
|
2699
|
-
// Detect if this is the atris package project or a user project
|
|
2700
|
-
const isAtrisPackage = fs.existsSync(path.join(process.cwd(), 'package.json')) &&
|
|
2701
|
-
fs.existsSync(path.join(process.cwd(), 'bin', 'atris.js')) &&
|
|
2702
|
-
fs.existsSync(path.join(process.cwd(), 'atris.md'));
|
|
2703
|
-
|
|
2704
|
-
console.log('Launch Workflow:');
|
|
2705
|
-
console.log(' 1. Document what was shipped (add Launch entry to journal Notes section)');
|
|
2706
|
-
console.log(' 2. Extract learnings (what worked? what would you do differently?)');
|
|
2707
|
-
console.log(' 3. Update MAP.md with new patterns/file locations');
|
|
2708
|
-
console.log(' 4. Update relevant docs (README, API docs, etc.)');
|
|
2709
|
-
console.log(' 5. Clean up (remove temp files, unused code, etc.)');
|
|
2710
|
-
if (isAtrisPackage) {
|
|
2711
|
-
console.log(' 6. [EXECUTE] Test locally (package development):');
|
|
2712
|
-
console.log(' - Run: npm link (link package for local testing)');
|
|
2713
|
-
console.log(' - Test: Create test project, run atris init to verify changes');
|
|
2714
|
-
console.log(' 7. [EXECUTE] Git commit + push:');
|
|
2715
|
-
console.log(' - Run: git add -A');
|
|
2716
|
-
console.log(' - Run: git commit -m "Descriptive message about what was shipped"');
|
|
2717
|
-
console.log(' - Run: git push origin master');
|
|
2718
|
-
console.log(' 8. [EXECUTE] Publish to npm (if ready for release):');
|
|
2719
|
-
console.log(' - Run: npm publish');
|
|
2720
|
-
console.log(' 9. Optional: Update changelog/blog (7 sentences max essay on what shipped)');
|
|
2721
|
-
console.log(' 10. Run: atris log sync (to sync journal to backend)');
|
|
2722
|
-
console.log(' 11. Celebrate! 🎉');
|
|
2723
|
-
} else {
|
|
2724
|
-
console.log(' 6. [EXECUTE] Git commit + push:');
|
|
2725
|
-
console.log(' - Run: git add -A');
|
|
2726
|
-
console.log(' - Run: git commit -m "Descriptive message about what was shipped"');
|
|
2727
|
-
console.log(' - Run: git push origin <your-branch>');
|
|
2728
|
-
console.log(' 7. Optional: Update changelog/blog (7 sentences max essay on what shipped)');
|
|
2729
|
-
console.log(' 8. Run: atris log sync (to sync journal to backend)');
|
|
2730
|
-
console.log(' 9. Celebrate! 🎉');
|
|
2731
|
-
}
|
|
2732
|
-
console.log('');
|
|
2733
|
-
console.log('DO NOT just describe these steps - actually execute the git commands!');
|
|
2734
|
-
console.log('');
|
|
2735
|
-
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
2736
|
-
console.log('');
|
|
2737
|
-
}
|
|
2738
|
-
|
|
2739
2663
|
function spawnClaudeCodeSession(url, token, body) {
|
|
2740
2664
|
return new Promise((resolve, reject) => {
|
|
2741
2665
|
const parsed = new URL(url);
|
package/commands/auth.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
const { loadCredentials, saveCredentials, deleteCredentials, getCredentialsPath, openBrowser, promptUser, displayAccountSummary } = require('../utils/auth');
|
|
1
|
+
const { loadCredentials, saveCredentials, deleteCredentials, getCredentialsPath, openBrowser, promptUser, displayAccountSummary, loadProfile, listProfiles, profileNameFromEmail } = require('../utils/auth');
|
|
2
2
|
const { getAppBaseUrl, apiRequestJson } = require('../utils/api');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
3
5
|
|
|
4
6
|
async function loginAtris(options = {}) {
|
|
5
7
|
// Support: atris login --token <token> --force
|
|
@@ -151,4 +153,97 @@ async function whoamiAtris() {
|
|
|
151
153
|
}
|
|
152
154
|
}
|
|
153
155
|
|
|
154
|
-
|
|
156
|
+
async function switchAccount() {
|
|
157
|
+
const args = process.argv.slice(3);
|
|
158
|
+
const targetName = args.filter(a => !a.startsWith('-'))[0];
|
|
159
|
+
|
|
160
|
+
const profiles = listProfiles();
|
|
161
|
+
if (profiles.length === 0) {
|
|
162
|
+
console.log('No saved profiles. Log in with different accounts to create profiles.');
|
|
163
|
+
console.log('Profiles are auto-saved on login.');
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const current = loadCredentials();
|
|
168
|
+
const currentName = profileNameFromEmail(current?.email);
|
|
169
|
+
|
|
170
|
+
if (!targetName) {
|
|
171
|
+
// Interactive: show list and let user pick
|
|
172
|
+
console.log('Switch account:\n');
|
|
173
|
+
profiles.forEach((name, i) => {
|
|
174
|
+
const profile = loadProfile(name);
|
|
175
|
+
const email = profile?.email || 'unknown';
|
|
176
|
+
const marker = name === currentName ? ' (active)' : '';
|
|
177
|
+
console.log(` ${i + 1}. ${name} — ${email}${marker}`);
|
|
178
|
+
});
|
|
179
|
+
console.log(` ${profiles.length + 1}. Cancel`);
|
|
180
|
+
|
|
181
|
+
const choice = await promptUser(`\nEnter choice (1-${profiles.length + 1}): `);
|
|
182
|
+
const idx = parseInt(choice, 10) - 1;
|
|
183
|
+
|
|
184
|
+
if (isNaN(idx) || idx < 0 || idx >= profiles.length) {
|
|
185
|
+
console.log('Cancelled.');
|
|
186
|
+
process.exit(0);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const chosen = profiles[idx];
|
|
190
|
+
return activateProfile(chosen, currentName);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Direct: atris switch <name>
|
|
194
|
+
// Fuzzy match: allow partial names
|
|
195
|
+
const exact = profiles.find(p => p === targetName);
|
|
196
|
+
const partial = !exact ? profiles.find(p => p.startsWith(targetName)) : null;
|
|
197
|
+
const match = exact || partial;
|
|
198
|
+
|
|
199
|
+
if (!match) {
|
|
200
|
+
console.error(`Profile "${targetName}" not found.`);
|
|
201
|
+
console.log(`Available: ${profiles.join(', ')}`);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return activateProfile(match, currentName);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function activateProfile(name, currentName) {
|
|
209
|
+
if (name === currentName) {
|
|
210
|
+
console.log(`Already on "${name}".`);
|
|
211
|
+
process.exit(0);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const profile = loadProfile(name);
|
|
215
|
+
if (!profile || !profile.token) {
|
|
216
|
+
console.error(`Profile "${name}" is corrupted. Login again to fix it.`);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Copy profile to credentials.json
|
|
221
|
+
const credentialsPath = getCredentialsPath();
|
|
222
|
+
fs.writeFileSync(credentialsPath, JSON.stringify(profile, null, 2));
|
|
223
|
+
try { fs.chmodSync(credentialsPath, 0o600); } catch {}
|
|
224
|
+
|
|
225
|
+
console.log(`Switched to "${name}" (${profile.email || 'unknown'})`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function listAccountsCmd() {
|
|
229
|
+
const profiles = listProfiles();
|
|
230
|
+
if (profiles.length === 0) {
|
|
231
|
+
console.log('No saved profiles. Profiles are auto-saved on login.');
|
|
232
|
+
process.exit(0);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const current = loadCredentials();
|
|
236
|
+
const currentName = profileNameFromEmail(current?.email);
|
|
237
|
+
|
|
238
|
+
console.log('Accounts:\n');
|
|
239
|
+
profiles.forEach(name => {
|
|
240
|
+
const profile = loadProfile(name);
|
|
241
|
+
const email = profile?.email || 'unknown';
|
|
242
|
+
const marker = name === currentName ? ' *' : '';
|
|
243
|
+
console.log(` ${name} — ${email}${marker}`);
|
|
244
|
+
});
|
|
245
|
+
console.log('\n* = active');
|
|
246
|
+
console.log('\nSwitch: atris switch <name>');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
module.exports = { loginAtris, logoutAtris, whoamiAtris, switchAccount, listAccountsCmd };
|
package/commands/integrations.js
CHANGED
|
@@ -13,13 +13,17 @@
|
|
|
13
13
|
const { loadCredentials } = require('../utils/auth');
|
|
14
14
|
const { apiRequestJson } = require('../utils/api');
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
function getAuth() {
|
|
17
17
|
const creds = loadCredentials();
|
|
18
18
|
if (!creds || !creds.token) {
|
|
19
19
|
console.error('Not logged in. Run: atris login');
|
|
20
20
|
process.exit(1);
|
|
21
21
|
}
|
|
22
|
-
return creds.token;
|
|
22
|
+
return { token: creds.token, email: creds.email || 'unknown' };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function getAuthToken() {
|
|
26
|
+
return getAuth().token;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
// ============================================================================
|
|
@@ -27,7 +31,7 @@ async function getAuthToken() {
|
|
|
27
31
|
// ============================================================================
|
|
28
32
|
|
|
29
33
|
async function gmailInbox(options = {}) {
|
|
30
|
-
const token =
|
|
34
|
+
const { token, email } = getAuth();
|
|
31
35
|
const limit = options.limit || 10;
|
|
32
36
|
|
|
33
37
|
console.log('📬 Fetching inbox...\n');
|
|
@@ -38,8 +42,10 @@ async function gmailInbox(options = {}) {
|
|
|
38
42
|
});
|
|
39
43
|
|
|
40
44
|
if (!result.ok) {
|
|
41
|
-
if (result.status === 401) {
|
|
42
|
-
console.error(
|
|
45
|
+
if (result.status === 400 || result.status === 401) {
|
|
46
|
+
console.error(`Gmail not connected for ${email}.`);
|
|
47
|
+
console.error('Connect at: https://atris.ai/dashboard/settings');
|
|
48
|
+
console.error(`Make sure you're signed in as ${email} on the web.`);
|
|
43
49
|
} else {
|
|
44
50
|
console.error(`Error: ${result.error || 'Failed to fetch inbox'}`);
|
|
45
51
|
}
|
|
@@ -123,7 +129,7 @@ async function gmailCommand(subcommand, ...args) {
|
|
|
123
129
|
// ============================================================================
|
|
124
130
|
|
|
125
131
|
async function calendarToday() {
|
|
126
|
-
const token =
|
|
132
|
+
const { token, email } = getAuth();
|
|
127
133
|
|
|
128
134
|
console.log('📅 Today\'s events:\n');
|
|
129
135
|
|
|
@@ -133,8 +139,10 @@ async function calendarToday() {
|
|
|
133
139
|
});
|
|
134
140
|
|
|
135
141
|
if (!result.ok) {
|
|
136
|
-
if (result.status === 401) {
|
|
137
|
-
console.error(
|
|
142
|
+
if (result.status === 400 || result.status === 401) {
|
|
143
|
+
console.error(`Calendar not connected for ${email}.`);
|
|
144
|
+
console.error('Connect at: https://atris.ai/dashboard/settings');
|
|
145
|
+
console.error(`Make sure you're signed in as ${email} on the web.`);
|
|
138
146
|
} else {
|
|
139
147
|
console.error(`Error: ${result.error || 'Failed to fetch events'}`);
|
|
140
148
|
}
|
|
@@ -215,8 +223,11 @@ async function twitterPost(text) {
|
|
|
215
223
|
});
|
|
216
224
|
|
|
217
225
|
if (!result.ok) {
|
|
218
|
-
if (result.status === 401) {
|
|
219
|
-
|
|
226
|
+
if (result.status === 400 || result.status === 401) {
|
|
227
|
+
const { email } = getAuth();
|
|
228
|
+
console.error(`Twitter not connected for ${email}.`);
|
|
229
|
+
console.error('Connect at: https://atris.ai/dashboard/settings');
|
|
230
|
+
console.error(`Make sure you're signed in as ${email} on the web.`);
|
|
220
231
|
} else {
|
|
221
232
|
console.error(`Error: ${result.error || 'Failed to post tweet'}`);
|
|
222
233
|
}
|
|
@@ -256,8 +267,11 @@ async function slackChannels() {
|
|
|
256
267
|
});
|
|
257
268
|
|
|
258
269
|
if (!result.ok) {
|
|
259
|
-
if (result.status === 401) {
|
|
260
|
-
|
|
270
|
+
if (result.status === 400 || result.status === 401) {
|
|
271
|
+
const { email } = getAuth();
|
|
272
|
+
console.error(`Slack not connected for ${email}.`);
|
|
273
|
+
console.error('Connect at: https://atris.ai/dashboard/settings');
|
|
274
|
+
console.error(`Make sure you're signed in as ${email} on the web.`);
|
|
261
275
|
} else {
|
|
262
276
|
console.error(`Error: ${result.error || 'Failed to fetch channels'}`);
|
|
263
277
|
}
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
const { findAllSkills, parseFrontmatter } = require('./skill');
|
|
6
|
+
|
|
7
|
+
// --- Recursive Copy Helper ---
|
|
8
|
+
|
|
9
|
+
function copyRecursive(src, dest) {
|
|
10
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
11
|
+
const entries = fs.readdirSync(src);
|
|
12
|
+
for (const entry of entries) {
|
|
13
|
+
if (entry === '.DS_Store' || entry === '.git') continue;
|
|
14
|
+
const srcPath = path.join(src, entry);
|
|
15
|
+
const destPath = path.join(dest, entry);
|
|
16
|
+
if (fs.statSync(srcPath).isDirectory()) {
|
|
17
|
+
copyRecursive(srcPath, destPath);
|
|
18
|
+
} else {
|
|
19
|
+
fs.copyFileSync(srcPath, destPath);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// --- Generate plugin.json manifest ---
|
|
25
|
+
|
|
26
|
+
function generateManifest(skills, projectDir) {
|
|
27
|
+
let pkg = {};
|
|
28
|
+
const pkgPath = path.join(projectDir, 'package.json');
|
|
29
|
+
if (fs.existsSync(pkgPath)) {
|
|
30
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
name: 'atris-workspace',
|
|
35
|
+
version: pkg.version || '1.0.0',
|
|
36
|
+
description: `Atris workspace skills for Cowork — ${skills.length} integrations (email, calendar, slack, drive, notion, and more)`,
|
|
37
|
+
author: {
|
|
38
|
+
name: typeof pkg.author === 'string' ? pkg.author : (pkg.author?.name || 'Atris Labs')
|
|
39
|
+
},
|
|
40
|
+
homepage: pkg.homepage || 'https://github.com/atrislabs/atris',
|
|
41
|
+
keywords: ['atris', 'workspace', 'integrations', 'email', 'calendar', 'slack', 'notion', 'drive']
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// --- Generate /atris-setup command ---
|
|
46
|
+
|
|
47
|
+
function generateSetupCommand() {
|
|
48
|
+
return `---
|
|
49
|
+
description: Set up Atris authentication and connect integrations (Gmail, Calendar, Slack, Notion, Drive)
|
|
50
|
+
allowed-tools: Bash, Read
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
# Atris Setup
|
|
54
|
+
|
|
55
|
+
Run the following steps to bootstrap Atris for this workspace.
|
|
56
|
+
|
|
57
|
+
## Step 1: Check if Atris CLI is installed
|
|
58
|
+
|
|
59
|
+
\`\`\`bash
|
|
60
|
+
command -v atris || npm install -g atris
|
|
61
|
+
\`\`\`
|
|
62
|
+
|
|
63
|
+
If the install fails, tell the user to run \`npm install -g atris\` manually.
|
|
64
|
+
|
|
65
|
+
## Step 2: Authenticate with AtrisOS
|
|
66
|
+
|
|
67
|
+
\`\`\`bash
|
|
68
|
+
atris whoami
|
|
69
|
+
\`\`\`
|
|
70
|
+
|
|
71
|
+
If not logged in, run:
|
|
72
|
+
|
|
73
|
+
\`\`\`bash
|
|
74
|
+
atris login
|
|
75
|
+
\`\`\`
|
|
76
|
+
|
|
77
|
+
This is interactive — the user will need to complete OAuth in their browser and paste the CLI code. Guide them through it.
|
|
78
|
+
|
|
79
|
+
## Step 3: Check integration status
|
|
80
|
+
|
|
81
|
+
After login, check which integrations are connected:
|
|
82
|
+
|
|
83
|
+
\`\`\`bash
|
|
84
|
+
TOKEN=$(node -e "console.log(require('$HOME/.atris/credentials.json').token)")
|
|
85
|
+
|
|
86
|
+
echo "=== Gmail ==="
|
|
87
|
+
curl -s "https://api.atris.ai/api/integrations/gmail/status" -H "Authorization: Bearer $TOKEN"
|
|
88
|
+
|
|
89
|
+
echo "\\n=== Google Calendar ==="
|
|
90
|
+
curl -s "https://api.atris.ai/api/integrations/google-calendar/status" -H "Authorization: Bearer $TOKEN"
|
|
91
|
+
|
|
92
|
+
echo "\\n=== Slack ==="
|
|
93
|
+
curl -s "https://api.atris.ai/api/integrations/slack/status" -H "Authorization: Bearer $TOKEN"
|
|
94
|
+
|
|
95
|
+
echo "\\n=== Notion ==="
|
|
96
|
+
curl -s "https://api.atris.ai/api/integrations/notion/status" -H "Authorization: Bearer $TOKEN"
|
|
97
|
+
|
|
98
|
+
echo "\\n=== Google Drive ==="
|
|
99
|
+
curl -s "https://api.atris.ai/api/integrations/google-drive/status" -H "Authorization: Bearer $TOKEN"
|
|
100
|
+
\`\`\`
|
|
101
|
+
|
|
102
|
+
## Step 4: Connect missing integrations
|
|
103
|
+
|
|
104
|
+
For any integration that shows \`"connected": false\`, guide the user through the OAuth flow:
|
|
105
|
+
|
|
106
|
+
1. Call the integration's \`/start\` endpoint to get an auth URL
|
|
107
|
+
2. Have the user open the URL in their browser
|
|
108
|
+
3. After authorizing, confirm the connection by re-checking status
|
|
109
|
+
|
|
110
|
+
Tell the user which integrations are connected and which still need setup. They can connect more later by running \`/atris-setup\` again.
|
|
111
|
+
`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// --- Generate README.md ---
|
|
115
|
+
|
|
116
|
+
function generateREADME(skills) {
|
|
117
|
+
const skillList = skills.map(s => {
|
|
118
|
+
const fm = s.frontmatter || {};
|
|
119
|
+
const name = fm.name || s.folder || 'unknown';
|
|
120
|
+
const desc = (fm.description || '').split('.')[0] || 'No description';
|
|
121
|
+
return `- **${name}**: ${desc}`;
|
|
122
|
+
}).join('\n');
|
|
123
|
+
|
|
124
|
+
return `# Atris Workspace Plugin
|
|
125
|
+
|
|
126
|
+
Brings Atris CLI skills into the Cowork desktop app.
|
|
127
|
+
|
|
128
|
+
## Setup
|
|
129
|
+
|
|
130
|
+
1. Install this plugin in Cowork
|
|
131
|
+
2. Run \`/atris-setup\` to authenticate and connect integrations
|
|
132
|
+
3. Start using skills — they trigger automatically based on your requests
|
|
133
|
+
|
|
134
|
+
## Included Skills (${skills.length})
|
|
135
|
+
|
|
136
|
+
${skillList}
|
|
137
|
+
|
|
138
|
+
## Requirements
|
|
139
|
+
|
|
140
|
+
- [Atris CLI](https://www.npmjs.com/package/atris) (\`npm install -g atris\`)
|
|
141
|
+
- AtrisOS account (free at https://atris.ai)
|
|
142
|
+
|
|
143
|
+
## Learn More
|
|
144
|
+
|
|
145
|
+
- Docs: https://github.com/atrislabs/atris
|
|
146
|
+
- CLI help: \`atris help\`
|
|
147
|
+
`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// --- BUILD subcommand ---
|
|
151
|
+
|
|
152
|
+
function buildPlugin(...args) {
|
|
153
|
+
const projectDir = process.cwd();
|
|
154
|
+
const atrisDir = path.join(projectDir, 'atris');
|
|
155
|
+
const skillsDir = path.join(atrisDir, 'skills');
|
|
156
|
+
|
|
157
|
+
// 1. Validate
|
|
158
|
+
if (!fs.existsSync(atrisDir)) {
|
|
159
|
+
console.error('✗ Error: atris/ folder not found. Run "atris init" first.');
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
if (!fs.existsSync(skillsDir)) {
|
|
163
|
+
console.error('✗ Error: atris/skills/ folder not found.');
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 2. Discover skills
|
|
168
|
+
const allSkills = findAllSkills(skillsDir);
|
|
169
|
+
if (allSkills.length === 0) {
|
|
170
|
+
console.warn('⚠ No skills found in atris/skills/. Plugin will be empty.');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
console.log(`\n📦 Building Atris plugin (${allSkills.length} skills)...\n`);
|
|
174
|
+
|
|
175
|
+
// 3. Create temp dir
|
|
176
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-plugin-'));
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
// 4. Create .claude-plugin/plugin.json
|
|
180
|
+
const pluginMetaDir = path.join(tempDir, '.claude-plugin');
|
|
181
|
+
fs.mkdirSync(pluginMetaDir, { recursive: true });
|
|
182
|
+
const manifest = generateManifest(allSkills, projectDir);
|
|
183
|
+
fs.writeFileSync(
|
|
184
|
+
path.join(pluginMetaDir, 'plugin.json'),
|
|
185
|
+
JSON.stringify(manifest, null, 2)
|
|
186
|
+
);
|
|
187
|
+
console.log(' ✓ Created plugin.json manifest');
|
|
188
|
+
|
|
189
|
+
// 5. Copy all skills
|
|
190
|
+
const pluginSkillsDir = path.join(tempDir, 'skills');
|
|
191
|
+
fs.mkdirSync(pluginSkillsDir, { recursive: true });
|
|
192
|
+
|
|
193
|
+
for (const skill of allSkills) {
|
|
194
|
+
// skill object from findAllSkills has .dir (full path) and we need folder name
|
|
195
|
+
const skillFolder = skill.folder || path.basename(skill.dir || skill.path || '');
|
|
196
|
+
if (!skillFolder) continue;
|
|
197
|
+
|
|
198
|
+
const srcPath = path.join(skillsDir, skillFolder);
|
|
199
|
+
const destPath = path.join(pluginSkillsDir, skillFolder);
|
|
200
|
+
|
|
201
|
+
if (fs.existsSync(srcPath) && fs.statSync(srcPath).isDirectory()) {
|
|
202
|
+
copyRecursive(srcPath, destPath);
|
|
203
|
+
const fm = skill.frontmatter || {};
|
|
204
|
+
console.log(` ✓ ${fm.name || skillFolder}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 6. Create commands/atris-setup.md
|
|
209
|
+
const commandsDir = path.join(tempDir, 'commands');
|
|
210
|
+
fs.mkdirSync(commandsDir, { recursive: true });
|
|
211
|
+
fs.writeFileSync(path.join(commandsDir, 'atris-setup.md'), generateSetupCommand());
|
|
212
|
+
console.log(' ✓ Created /atris-setup command');
|
|
213
|
+
|
|
214
|
+
// 7. Create README.md
|
|
215
|
+
fs.writeFileSync(path.join(tempDir, 'README.md'), generateREADME(allSkills));
|
|
216
|
+
console.log(' ✓ Created README.md');
|
|
217
|
+
|
|
218
|
+
// 8. Parse --output flag
|
|
219
|
+
let outputFile = path.join(projectDir, 'atris-workspace.plugin');
|
|
220
|
+
for (const arg of args) {
|
|
221
|
+
if (typeof arg === 'string' && arg.startsWith('--output=')) {
|
|
222
|
+
outputFile = path.resolve(arg.split('=')[1]);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 9. ZIP it
|
|
227
|
+
try {
|
|
228
|
+
// Create zip in /tmp first (Cowork best practice)
|
|
229
|
+
const tmpZip = path.join(os.tmpdir(), 'atris-workspace.plugin');
|
|
230
|
+
if (fs.existsSync(tmpZip)) fs.unlinkSync(tmpZip);
|
|
231
|
+
|
|
232
|
+
execSync(`cd "${tempDir}" && zip -r "${tmpZip}" . -x "*.DS_Store" "*.git*"`, {
|
|
233
|
+
stdio: 'pipe'
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Copy to final destination
|
|
237
|
+
fs.copyFileSync(tmpZip, outputFile);
|
|
238
|
+
fs.unlinkSync(tmpZip);
|
|
239
|
+
} catch (e) {
|
|
240
|
+
console.error('✗ ZIP failed. Ensure "zip" command is available.');
|
|
241
|
+
console.error(` Temp dir preserved at: ${tempDir}`);
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 10. Summary
|
|
246
|
+
const stats = fs.statSync(outputFile);
|
|
247
|
+
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
248
|
+
console.log('');
|
|
249
|
+
console.log(`✓ Plugin built: ${path.basename(outputFile)} (${allSkills.length} skills, ${sizeKB}KB)`);
|
|
250
|
+
console.log(` Location: ${outputFile}`);
|
|
251
|
+
console.log('');
|
|
252
|
+
console.log('Install in Cowork:');
|
|
253
|
+
console.log(' Open the .plugin file or drag it into the Cowork app.');
|
|
254
|
+
console.log(' Then run /atris-setup to authenticate.');
|
|
255
|
+
console.log('');
|
|
256
|
+
|
|
257
|
+
} finally {
|
|
258
|
+
// Clean up temp dir (only if ZIP succeeded — we skip cleanup on error above)
|
|
259
|
+
if (fs.existsSync(tempDir)) {
|
|
260
|
+
try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch (e) { /* ignore */ }
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// --- INFO subcommand ---
|
|
266
|
+
|
|
267
|
+
function showPluginInfo() {
|
|
268
|
+
const skillsDir = path.join(process.cwd(), 'atris', 'skills');
|
|
269
|
+
|
|
270
|
+
if (!fs.existsSync(skillsDir)) {
|
|
271
|
+
console.error('✗ atris/skills/ not found. Run "atris init" first.');
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const allSkills = findAllSkills(skillsDir);
|
|
276
|
+
|
|
277
|
+
let pkg = {};
|
|
278
|
+
const pkgPath = path.join(process.cwd(), 'package.json');
|
|
279
|
+
if (fs.existsSync(pkgPath)) {
|
|
280
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
console.log('');
|
|
284
|
+
console.log('Atris Plugin Preview');
|
|
285
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
286
|
+
console.log(` Name: atris-workspace`);
|
|
287
|
+
console.log(` Version: ${pkg.version || '1.0.0'}`);
|
|
288
|
+
console.log(` Skills: ${allSkills.length}`);
|
|
289
|
+
console.log('');
|
|
290
|
+
|
|
291
|
+
if (allSkills.length > 0) {
|
|
292
|
+
console.log('Included skills:');
|
|
293
|
+
for (const skill of allSkills) {
|
|
294
|
+
const fm = skill.frontmatter || {};
|
|
295
|
+
const name = fm.name || skill.folder || 'unknown';
|
|
296
|
+
const desc = (fm.description || 'No description').substring(0, 65);
|
|
297
|
+
console.log(` • ${name.padEnd(16)} ${desc}`);
|
|
298
|
+
}
|
|
299
|
+
console.log('');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
console.log('Components:');
|
|
303
|
+
console.log(` Skills: ${allSkills.length}`);
|
|
304
|
+
console.log(' Commands: 1 (/atris-setup)');
|
|
305
|
+
console.log('');
|
|
306
|
+
console.log('Run "atris plugin build" to package.');
|
|
307
|
+
console.log('');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// --- PUBLISH subcommand ---
|
|
311
|
+
|
|
312
|
+
function publishPlugin(...args) {
|
|
313
|
+
const projectDir = process.cwd();
|
|
314
|
+
const skillsDir = path.join(projectDir, 'atris', 'skills');
|
|
315
|
+
|
|
316
|
+
if (!fs.existsSync(skillsDir)) {
|
|
317
|
+
console.error('✗ atris/skills/ not found. Run "atris init" first.');
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Find the marketplace repo — check sibling dir or --repo flag
|
|
322
|
+
let marketplaceDir = null;
|
|
323
|
+
for (const arg of args) {
|
|
324
|
+
if (typeof arg === 'string' && arg.startsWith('--repo=')) {
|
|
325
|
+
marketplaceDir = path.resolve(arg.split('=')[1]);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (!marketplaceDir) {
|
|
329
|
+
// Default: look for atris-plugins as a sibling directory
|
|
330
|
+
const siblingDir = path.join(path.dirname(projectDir), 'atris-plugins');
|
|
331
|
+
if (fs.existsSync(siblingDir)) {
|
|
332
|
+
marketplaceDir = siblingDir;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (!marketplaceDir || !fs.existsSync(marketplaceDir)) {
|
|
336
|
+
console.error('✗ Marketplace repo not found.');
|
|
337
|
+
console.error(' Expected at: ../atris-plugins/');
|
|
338
|
+
console.error(' Or specify: atris plugin publish --repo=/path/to/atris-plugins');
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const pluginDir = path.join(marketplaceDir, 'plugins', 'atris-workspace');
|
|
343
|
+
const destSkillsDir = path.join(pluginDir, 'skills');
|
|
344
|
+
const destCommandsDir = path.join(pluginDir, 'commands');
|
|
345
|
+
|
|
346
|
+
console.log(`\n📡 Publishing to ${path.basename(marketplaceDir)}...\n`);
|
|
347
|
+
|
|
348
|
+
// 1. Clear old skills and re-copy
|
|
349
|
+
if (fs.existsSync(destSkillsDir)) {
|
|
350
|
+
fs.rmSync(destSkillsDir, { recursive: true, force: true });
|
|
351
|
+
}
|
|
352
|
+
fs.mkdirSync(destSkillsDir, { recursive: true });
|
|
353
|
+
|
|
354
|
+
const allSkills = findAllSkills(skillsDir);
|
|
355
|
+
for (const skill of allSkills) {
|
|
356
|
+
const skillFolder = skill.folder || path.basename(skill.dir || skill.path || '');
|
|
357
|
+
if (!skillFolder) continue;
|
|
358
|
+
const srcPath = path.join(skillsDir, skillFolder);
|
|
359
|
+
const destPath = path.join(destSkillsDir, skillFolder);
|
|
360
|
+
if (fs.existsSync(srcPath) && fs.statSync(srcPath).isDirectory()) {
|
|
361
|
+
copyRecursive(srcPath, destPath);
|
|
362
|
+
const fm = skill.frontmatter || {};
|
|
363
|
+
console.log(` ✓ ${fm.name || skillFolder}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// 2. Update commands
|
|
368
|
+
fs.mkdirSync(destCommandsDir, { recursive: true });
|
|
369
|
+
fs.writeFileSync(path.join(destCommandsDir, 'atris-setup.md'), generateSetupCommand());
|
|
370
|
+
console.log(' ✓ /atris-setup command');
|
|
371
|
+
|
|
372
|
+
// 3. Update plugin.json version
|
|
373
|
+
let pkg = {};
|
|
374
|
+
const pkgPath = path.join(projectDir, 'package.json');
|
|
375
|
+
if (fs.existsSync(pkgPath)) {
|
|
376
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
377
|
+
}
|
|
378
|
+
const pluginJsonPath = path.join(pluginDir, '.claude-plugin', 'plugin.json');
|
|
379
|
+
if (fs.existsSync(pluginJsonPath)) {
|
|
380
|
+
const pluginJson = JSON.parse(fs.readFileSync(pluginJsonPath, 'utf8'));
|
|
381
|
+
pluginJson.version = pkg.version || pluginJson.version;
|
|
382
|
+
fs.writeFileSync(pluginJsonPath, JSON.stringify(pluginJson, null, 2) + '\n');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// 4. Update marketplace.json version
|
|
386
|
+
const marketplaceJsonPath = path.join(marketplaceDir, '.claude-plugin', 'marketplace.json');
|
|
387
|
+
if (fs.existsSync(marketplaceJsonPath)) {
|
|
388
|
+
const mkt = JSON.parse(fs.readFileSync(marketplaceJsonPath, 'utf8'));
|
|
389
|
+
const entry = (mkt.plugins || []).find(p => p.name === 'atris-workspace');
|
|
390
|
+
if (entry) {
|
|
391
|
+
entry.version = pkg.version || entry.version;
|
|
392
|
+
}
|
|
393
|
+
fs.writeFileSync(marketplaceJsonPath, JSON.stringify(mkt, null, 2) + '\n');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// 5. Update README
|
|
397
|
+
fs.writeFileSync(path.join(pluginDir, 'README.md'), generateREADME(allSkills));
|
|
398
|
+
console.log(' ✓ README.md');
|
|
399
|
+
|
|
400
|
+
// 6. Git commit + push
|
|
401
|
+
console.log('');
|
|
402
|
+
const version = pkg.version || 'latest';
|
|
403
|
+
try {
|
|
404
|
+
execSync('git add -A', { cwd: marketplaceDir, stdio: 'pipe' });
|
|
405
|
+
const status = execSync('git status --porcelain', { cwd: marketplaceDir, encoding: 'utf8' }).trim();
|
|
406
|
+
if (!status) {
|
|
407
|
+
console.log('✓ No changes — marketplace already up to date.');
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
execSync(`git commit -m "chore: Update atris-workspace plugin to v${version}"`, {
|
|
411
|
+
cwd: marketplaceDir, stdio: 'pipe'
|
|
412
|
+
});
|
|
413
|
+
execSync('git push', { cwd: marketplaceDir, stdio: 'pipe' });
|
|
414
|
+
console.log(`✓ Published v${version} (${allSkills.length} skills) → pushed to remote`);
|
|
415
|
+
} catch (e) {
|
|
416
|
+
console.error('⚠ Skills synced but git push failed. Commit manually:');
|
|
417
|
+
console.error(` cd ${marketplaceDir} && git add -A && git commit -m "update" && git push`);
|
|
418
|
+
}
|
|
419
|
+
console.log('');
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// --- Main Dispatcher ---
|
|
423
|
+
|
|
424
|
+
function pluginCommand(subcommand, ...args) {
|
|
425
|
+
switch (subcommand) {
|
|
426
|
+
case 'build':
|
|
427
|
+
buildPlugin(...args);
|
|
428
|
+
break;
|
|
429
|
+
case 'info':
|
|
430
|
+
case 'preview':
|
|
431
|
+
showPluginInfo();
|
|
432
|
+
break;
|
|
433
|
+
case 'publish':
|
|
434
|
+
case 'sync':
|
|
435
|
+
publishPlugin(...args);
|
|
436
|
+
break;
|
|
437
|
+
default:
|
|
438
|
+
console.log('');
|
|
439
|
+
console.log('Usage: atris plugin <subcommand>');
|
|
440
|
+
console.log('');
|
|
441
|
+
console.log('Subcommands:');
|
|
442
|
+
console.log(' build [--output=path] - Package skills into .plugin for Cowork');
|
|
443
|
+
console.log(' publish [--repo=path] - Sync skills to marketplace repo and push');
|
|
444
|
+
console.log(' info - Preview what will be included');
|
|
445
|
+
console.log('');
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
module.exports = { pluginCommand };
|
package/commands/sync.js
CHANGED
|
@@ -292,4 +292,90 @@ After displaying the boot output, respond to the user naturally.
|
|
|
292
292
|
}
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
|
|
295
|
+
/**
|
|
296
|
+
* Lightweight skill-only sync. Compares package skills against project skills
|
|
297
|
+
* and updates any that differ. Called automatically from `atris activate`.
|
|
298
|
+
* Returns number of files updated (0 = already current).
|
|
299
|
+
*/
|
|
300
|
+
function syncSkills({ silent = false } = {}) {
|
|
301
|
+
const targetDir = path.join(process.cwd(), 'atris');
|
|
302
|
+
const packageSkillsDir = path.join(__dirname, '..', 'atris', 'skills');
|
|
303
|
+
const userSkillsDir = path.join(targetDir, 'skills');
|
|
304
|
+
const claudeSkillsBaseDir = path.join(process.cwd(), '.claude', 'skills');
|
|
305
|
+
|
|
306
|
+
if (!fs.existsSync(targetDir) || !fs.existsSync(packageSkillsDir)) {
|
|
307
|
+
return 0;
|
|
308
|
+
}
|
|
309
|
+
|
|
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
|
+
let updated = 0;
|
|
318
|
+
|
|
319
|
+
const skillFolders = fs.readdirSync(packageSkillsDir).filter(f =>
|
|
320
|
+
fs.statSync(path.join(packageSkillsDir, f)).isDirectory()
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
for (const skill of skillFolders) {
|
|
324
|
+
const srcSkillDir = path.join(packageSkillsDir, skill);
|
|
325
|
+
const destSkillDir = path.join(userSkillsDir, skill);
|
|
326
|
+
const symlinkPath = path.join(claudeSkillsBaseDir, skill);
|
|
327
|
+
|
|
328
|
+
const syncRecursive = (src, dest, skillName, basePath = '') => {
|
|
329
|
+
if (!fs.existsSync(dest)) {
|
|
330
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
331
|
+
}
|
|
332
|
+
const entries = fs.readdirSync(src);
|
|
333
|
+
for (const entry of entries) {
|
|
334
|
+
const srcPath = path.join(src, entry);
|
|
335
|
+
const destPath = path.join(dest, entry);
|
|
336
|
+
const relPath = basePath ? `${basePath}/${entry}` : entry;
|
|
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
|
+
}
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
syncRecursive(srcSkillDir, destSkillDir, skill);
|
|
358
|
+
|
|
359
|
+
// Create symlink if doesn't exist
|
|
360
|
+
if (!fs.existsSync(symlinkPath)) {
|
|
361
|
+
const relativePath = path.join('..', '..', 'atris', 'skills', skill);
|
|
362
|
+
try {
|
|
363
|
+
fs.symlinkSync(relativePath, symlinkPath);
|
|
364
|
+
if (!silent) {
|
|
365
|
+
console.log(`✓ Linked .claude/skills/${skill}`);
|
|
366
|
+
}
|
|
367
|
+
} catch (e) {
|
|
368
|
+
// Fallback: copy instead of symlink
|
|
369
|
+
fs.mkdirSync(symlinkPath, { recursive: true });
|
|
370
|
+
const skillFile = path.join(destSkillDir, 'SKILL.md');
|
|
371
|
+
if (fs.existsSync(skillFile)) {
|
|
372
|
+
fs.copyFileSync(skillFile, path.join(symlinkPath, 'SKILL.md'));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return updated;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
module.exports = { syncAtris, syncSkills };
|
package/package.json
CHANGED
package/utils/auth.js
CHANGED
|
@@ -109,15 +109,74 @@ function shouldRefreshToken(token, bufferSeconds = TOKEN_REFRESH_BUFFER_SECONDS)
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
// Credentials management
|
|
112
|
-
function
|
|
112
|
+
function getAtrisDir() {
|
|
113
113
|
const homeDir = os.homedir();
|
|
114
114
|
const atrisDir = path.join(homeDir, '.atris');
|
|
115
|
-
|
|
116
115
|
if (!fs.existsSync(atrisDir)) {
|
|
117
116
|
fs.mkdirSync(atrisDir, { recursive: true });
|
|
118
117
|
}
|
|
118
|
+
return atrisDir;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function getCredentialsPath() {
|
|
122
|
+
return path.join(getAtrisDir(), 'credentials.json');
|
|
123
|
+
}
|
|
119
124
|
|
|
120
|
-
|
|
125
|
+
function getProfilesDir() {
|
|
126
|
+
const dir = path.join(getAtrisDir(), 'profiles');
|
|
127
|
+
if (!fs.existsSync(dir)) {
|
|
128
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
129
|
+
}
|
|
130
|
+
return dir;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function profileNameFromEmail(email) {
|
|
134
|
+
if (!email) return null;
|
|
135
|
+
// "keshav@atrislabs.com" → "keshav"
|
|
136
|
+
return email.split('@')[0].toLowerCase().replace(/[^a-z0-9_-]/g, '-');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function saveProfile(name, credentials) {
|
|
140
|
+
const profilePath = path.join(getProfilesDir(), `${name}.json`);
|
|
141
|
+
fs.writeFileSync(profilePath, JSON.stringify(credentials, null, 2));
|
|
142
|
+
try { fs.chmodSync(profilePath, 0o600); } catch {}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function loadProfile(name) {
|
|
146
|
+
const profilePath = path.join(getProfilesDir(), `${name}.json`);
|
|
147
|
+
if (!fs.existsSync(profilePath)) return null;
|
|
148
|
+
try {
|
|
149
|
+
return JSON.parse(fs.readFileSync(profilePath, 'utf8'));
|
|
150
|
+
} catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function listProfiles() {
|
|
156
|
+
const dir = getProfilesDir();
|
|
157
|
+
try {
|
|
158
|
+
return fs.readdirSync(dir)
|
|
159
|
+
.filter(f => f.endsWith('.json'))
|
|
160
|
+
.map(f => f.replace('.json', ''));
|
|
161
|
+
} catch {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function deleteProfile(name) {
|
|
167
|
+
const profilePath = path.join(getProfilesDir(), `${name}.json`);
|
|
168
|
+
if (fs.existsSync(profilePath)) {
|
|
169
|
+
fs.unlinkSync(profilePath);
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function autoSaveProfile(credentials) {
|
|
176
|
+
const name = profileNameFromEmail(credentials?.email);
|
|
177
|
+
if (name) {
|
|
178
|
+
saveProfile(name, credentials);
|
|
179
|
+
}
|
|
121
180
|
}
|
|
122
181
|
|
|
123
182
|
function saveCredentials(token, refreshToken, email, userId, provider) {
|
|
@@ -137,6 +196,9 @@ function saveCredentials(token, refreshToken, email, userId, provider) {
|
|
|
137
196
|
} catch {
|
|
138
197
|
// Best effort: permissions may be unsupported on this platform.
|
|
139
198
|
}
|
|
199
|
+
|
|
200
|
+
// Auto-save as named profile
|
|
201
|
+
autoSaveProfile(credentials);
|
|
140
202
|
}
|
|
141
203
|
|
|
142
204
|
function loadCredentials() {
|
|
@@ -384,4 +446,11 @@ module.exports = {
|
|
|
384
446
|
ensureValidCredentials,
|
|
385
447
|
fetchMyAgents,
|
|
386
448
|
displayAccountSummary,
|
|
449
|
+
// Profile switching
|
|
450
|
+
saveProfile,
|
|
451
|
+
loadProfile,
|
|
452
|
+
listProfiles,
|
|
453
|
+
deleteProfile,
|
|
454
|
+
profileNameFromEmail,
|
|
455
|
+
autoSaveProfile,
|
|
387
456
|
};
|