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.
@@ -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 Sushanth",
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 AI transformation roadmap",
150
+ "description": "Discuss project roadmap",
151
151
  "location": "Zoom",
152
- "attendees": ["sushanth@pallet.com"],
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.1.0
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
- module.exports = { loginAtris, logoutAtris, whoamiAtris };
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 };
@@ -13,13 +13,17 @@
13
13
  const { loadCredentials } = require('../utils/auth');
14
14
  const { apiRequestJson } = require('../utils/api');
15
15
 
16
- async function getAuthToken() {
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 = await getAuthToken();
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('Gmail not connected. Connect at: https://atris.ai/dashboard/settings');
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 = await getAuthToken();
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('Calendar not connected. Connect at: https://atris.ai/dashboard/settings');
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
- console.error('Twitter not connected. Connect at: https://atris.ai/dashboard/settings');
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
- console.error('Slack not connected. Connect at: https://atris.ai/dashboard/settings');
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
- module.exports = { syncAtris };
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
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": {
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 getCredentialsPath() {
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
- return path.join(atrisDir, 'credentials.json');
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
  };