atris 2.2.2 → 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.
@@ -0,0 +1,355 @@
1
+ ---
2
+ name: slides
3
+ description: Google Slides integration via AtrisOS API. List, create, read, and update presentations. Add slides, text, shapes, images. Export to PDF. Use when user asks about slides, presentations, decks, or pitch decks.
4
+ version: 1.0.0
5
+ tags:
6
+ - slides
7
+ - google
8
+ - productivity
9
+ ---
10
+
11
+ # Google Slides Agent
12
+
13
+ > Drop this in `~/.claude/skills/slides/SKILL.md` and Claude Code becomes your presentation assistant.
14
+
15
+ ## Prerequisites
16
+
17
+ Google Slides shares OAuth with Google Drive. If Drive is connected, Slides works automatically. If not:
18
+
19
+ ```bash
20
+ TOKEN=$(node -e "console.log(require('$HOME/.atris/credentials.json').token)")
21
+
22
+ # Check if Drive is connected (Slides piggybacks on Drive)
23
+ curl -s "https://api.atris.ai/api/integrations/google-drive/status" -H "Authorization: Bearer $TOKEN"
24
+
25
+ # If not connected, start Drive OAuth (includes Slides scope)
26
+ curl -s -X POST "https://api.atris.ai/api/integrations/google-drive/start" \
27
+ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
28
+ -d '{"next_url":"https://atris.ai/dashboard/settings"}'
29
+ ```
30
+
31
+ ---
32
+
33
+ ## API Reference
34
+
35
+ Base: `https://api.atris.ai/api/integrations/google-slides`
36
+
37
+ All requests require: `-H "Authorization: Bearer $TOKEN"`
38
+
39
+ ### Get Token
40
+ ```bash
41
+ TOKEN=$(node -e "console.log(require('$HOME/.atris/credentials.json').token)")
42
+ ```
43
+
44
+ ---
45
+
46
+ ## Presentations
47
+
48
+ ### List Presentations
49
+ ```bash
50
+ curl -s "https://api.atris.ai/api/integrations/google-slides/presentations?page_size=20" \
51
+ -H "Authorization: Bearer $TOKEN"
52
+ ```
53
+
54
+ ### Get a Presentation (with all slides)
55
+ ```bash
56
+ curl -s "https://api.atris.ai/api/integrations/google-slides/presentations/{presentation_id}" \
57
+ -H "Authorization: Bearer $TOKEN"
58
+ ```
59
+
60
+ ### Create a Presentation
61
+ ```bash
62
+ curl -s -X POST "https://api.atris.ai/api/integrations/google-slides/presentations" \
63
+ -H "Authorization: Bearer $TOKEN" \
64
+ -H "Content-Type: application/json" \
65
+ -d '{"title": "Q1 2026 Review"}'
66
+ ```
67
+
68
+ ---
69
+
70
+ ## Updating Slides (batch-update)
71
+
72
+ All slide mutations use the batch-update endpoint. This is the most powerful endpoint — it can add slides, insert text, add shapes, images, change formatting, and more.
73
+
74
+ ```bash
75
+ curl -s -X POST "https://api.atris.ai/api/integrations/google-slides/presentations/{id}/batch-update" \
76
+ -H "Authorization: Bearer $TOKEN" \
77
+ -H "Content-Type: application/json" \
78
+ -d '{"requests": [...]}'
79
+ ```
80
+
81
+ ### Add a Blank Slide
82
+ ```json
83
+ {
84
+ "requests": [
85
+ {
86
+ "createSlide": {
87
+ "insertionIndex": 1,
88
+ "slideLayoutReference": {
89
+ "predefinedLayout": "BLANK"
90
+ }
91
+ }
92
+ }
93
+ ]
94
+ }
95
+ ```
96
+
97
+ **Available layouts:** `BLANK`, `TITLE`, `TITLE_AND_BODY`, `TITLE_AND_TWO_COLUMNS`, `TITLE_ONLY`, `SECTION_HEADER`, `SECTION_TITLE_AND_DESCRIPTION`, `ONE_COLUMN_TEXT`, `MAIN_POINT`, `BIG_NUMBER`
98
+
99
+ ### Add a Title Slide
100
+ ```json
101
+ {
102
+ "requests": [
103
+ {
104
+ "createSlide": {
105
+ "insertionIndex": 0,
106
+ "slideLayoutReference": {
107
+ "predefinedLayout": "TITLE"
108
+ },
109
+ "placeholderIdMappings": [
110
+ {"layoutPlaceholder": {"type": "TITLE"}, "objectId": "titleId"},
111
+ {"layoutPlaceholder": {"type": "SUBTITLE"}, "objectId": "subtitleId"}
112
+ ]
113
+ }
114
+ },
115
+ {
116
+ "insertText": {
117
+ "objectId": "titleId",
118
+ "text": "Q1 2026 Review"
119
+ }
120
+ },
121
+ {
122
+ "insertText": {
123
+ "objectId": "subtitleId",
124
+ "text": "Atris Labs - Confidential"
125
+ }
126
+ }
127
+ ]
128
+ }
129
+ ```
130
+
131
+ ### Insert Text into an Element
132
+ ```json
133
+ {
134
+ "requests": [
135
+ {
136
+ "insertText": {
137
+ "objectId": "ELEMENT_ID",
138
+ "text": "Hello, world!"
139
+ }
140
+ }
141
+ ]
142
+ }
143
+ ```
144
+
145
+ ### Add a Text Box
146
+ ```json
147
+ {
148
+ "requests": [
149
+ {
150
+ "createShape": {
151
+ "objectId": "myTextBox1",
152
+ "shapeType": "TEXT_BOX",
153
+ "elementProperties": {
154
+ "pageObjectId": "SLIDE_ID",
155
+ "size": {
156
+ "width": {"magnitude": 400, "unit": "PT"},
157
+ "height": {"magnitude": 50, "unit": "PT"}
158
+ },
159
+ "transform": {
160
+ "scaleX": 1, "scaleY": 1,
161
+ "translateX": 100, "translateY": 200,
162
+ "unit": "PT"
163
+ }
164
+ }
165
+ }
166
+ },
167
+ {
168
+ "insertText": {
169
+ "objectId": "myTextBox1",
170
+ "text": "Custom text here"
171
+ }
172
+ }
173
+ ]
174
+ }
175
+ ```
176
+
177
+ ### Add an Image
178
+ ```json
179
+ {
180
+ "requests": [
181
+ {
182
+ "createImage": {
183
+ "objectId": "myImage1",
184
+ "url": "https://example.com/image.png",
185
+ "elementProperties": {
186
+ "pageObjectId": "SLIDE_ID",
187
+ "size": {
188
+ "width": {"magnitude": 300, "unit": "PT"},
189
+ "height": {"magnitude": 200, "unit": "PT"}
190
+ },
191
+ "transform": {
192
+ "scaleX": 1, "scaleY": 1,
193
+ "translateX": 150, "translateY": 100,
194
+ "unit": "PT"
195
+ }
196
+ }
197
+ }
198
+ }
199
+ ]
200
+ }
201
+ ```
202
+
203
+ ### Replace All Text (find & replace)
204
+ ```json
205
+ {
206
+ "requests": [
207
+ {
208
+ "replaceAllText": {
209
+ "containsText": {"text": "{{company_name}}"},
210
+ "replaceText": "Atris Labs"
211
+ }
212
+ }
213
+ ]
214
+ }
215
+ ```
216
+
217
+ ### Delete a Slide or Element
218
+ ```json
219
+ {
220
+ "requests": [
221
+ {
222
+ "deleteObject": {
223
+ "objectId": "SLIDE_OR_ELEMENT_ID"
224
+ }
225
+ }
226
+ ]
227
+ }
228
+ ```
229
+
230
+ ### Style Text
231
+ ```json
232
+ {
233
+ "requests": [
234
+ {
235
+ "updateTextStyle": {
236
+ "objectId": "ELEMENT_ID",
237
+ "style": {
238
+ "bold": true,
239
+ "fontSize": {"magnitude": 24, "unit": "PT"},
240
+ "foregroundColor": {
241
+ "opaqueColor": {"rgbColor": {"red": 0.2, "green": 0.2, "blue": 0.8}}
242
+ }
243
+ },
244
+ "fields": "bold,fontSize,foregroundColor"
245
+ }
246
+ }
247
+ ]
248
+ }
249
+ ```
250
+
251
+ ---
252
+
253
+ ## Pages (Individual Slides)
254
+
255
+ ### Get a Single Slide
256
+ ```bash
257
+ curl -s "https://api.atris.ai/api/integrations/google-slides/presentations/{id}/pages/{page_id}" \
258
+ -H "Authorization: Bearer $TOKEN"
259
+ ```
260
+
261
+ ### Get Slide Thumbnail
262
+ ```bash
263
+ curl -s "https://api.atris.ai/api/integrations/google-slides/presentations/{id}/pages/{page_id}/thumbnail" \
264
+ -H "Authorization: Bearer $TOKEN"
265
+ ```
266
+
267
+ ---
268
+
269
+ ## Export
270
+
271
+ ### Export as PDF
272
+ ```bash
273
+ curl -s "https://api.atris.ai/api/integrations/google-slides/presentations/{id}/export" \
274
+ -H "Authorization: Bearer $TOKEN"
275
+ ```
276
+ Returns `{"pdf_base64": "...", "content_type": "application/pdf"}`.
277
+
278
+ ---
279
+
280
+ ## Workflows
281
+
282
+ ### "Create a pitch deck"
283
+ 1. Create presentation: `POST /presentations` with title
284
+ 2. Get the presentation to find the first slide ID
285
+ 3. Batch update: add title slide, content slides, closing slide
286
+ 4. Each slide: createSlide + insertText for content
287
+ 5. Return the presentation URL: `https://docs.google.com/presentation/d/{id}`
288
+
289
+ ### "List my presentations"
290
+ 1. `GET /presentations`
291
+ 2. Display: name, last modified, link
292
+
293
+ ### "Add a slide to an existing deck"
294
+ 1. `GET /presentations/{id}` to see current slides
295
+ 2. Batch update: createSlide at the desired index
296
+ 3. Add content with insertText, createShape, createImage
297
+
298
+ ### "Export deck to PDF"
299
+ 1. `GET /presentations/{id}/export`
300
+ 2. Decode base64 and save to file
301
+
302
+ ### "Update text in a deck"
303
+ 1. `GET /presentations/{id}` to find element IDs
304
+ 2. Batch update with replaceAllText for template variables
305
+ 3. Or insertText/deleteText for specific elements
306
+
307
+ ---
308
+
309
+ ## Important Notes
310
+
311
+ - **Shares Drive OAuth** — no separate connection needed. If Drive is connected, Slides works
312
+ - **Batch update is everything** — all slide mutations (add, edit, delete, style) go through batch-update
313
+ - **Object IDs** — every slide, shape, text box, image has an ID. Get them from `GET /presentations/{id}`
314
+ - **Slide size** — default is 10x5.63 inches (720x406.5 PT). Position elements accordingly
315
+ - **Images** — must be publicly accessible URLs. For private images, upload to Drive first
316
+ - **Templates** — use replaceAllText with `{{placeholder}}` patterns for template-based decks
317
+
318
+ ---
319
+
320
+ ## Error Handling
321
+
322
+ | Error | Meaning | Solution |
323
+ |-------|---------|----------|
324
+ | `Drive not connected` | No Drive OAuth token | Connect Google Drive first |
325
+ | `403 insufficient_scope` | Token missing presentations scope | Reconnect Drive to get updated scopes |
326
+ | `404 not_found` | Presentation doesn't exist | Check presentation ID |
327
+ | `400 invalid_request` | Bad batch update request | Check request format against API docs |
328
+
329
+ ---
330
+
331
+ ## Quick Reference
332
+
333
+ ```bash
334
+ # Get token
335
+ TOKEN=$(node -e "console.log(require('$HOME/.atris/credentials.json').token)")
336
+
337
+ # List presentations
338
+ curl -s "https://api.atris.ai/api/integrations/google-slides/presentations" -H "Authorization: Bearer $TOKEN"
339
+
340
+ # Create presentation
341
+ curl -s -X POST "https://api.atris.ai/api/integrations/google-slides/presentations" \
342
+ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
343
+ -d '{"title":"My Deck"}'
344
+
345
+ # Get presentation (with all slides)
346
+ curl -s "https://api.atris.ai/api/integrations/google-slides/presentations/PRES_ID" -H "Authorization: Bearer $TOKEN"
347
+
348
+ # Add a slide with text
349
+ curl -s -X POST "https://api.atris.ai/api/integrations/google-slides/presentations/PRES_ID/batch-update" \
350
+ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
351
+ -d '{"requests":[{"createSlide":{"slideLayoutReference":{"predefinedLayout":"TITLE_AND_BODY"}}}]}'
352
+
353
+ # Export as PDF
354
+ curl -s "https://api.atris.ai/api/integrations/google-slides/presentations/PRES_ID/export" -H "Authorization: Bearer $TOKEN"
355
+ ```
package/bin/atris.js CHANGED
@@ -25,7 +25,7 @@ const DEFAULT_CLIENT_ID = `AtrisCLI/${CLI_VERSION}`;
25
25
  const DEFAULT_USER_AGENT = `${DEFAULT_CLIENT_ID} (node ${process.version}; ${os.platform()} ${os.release()} ${os.arch()})`;
26
26
 
27
27
  // Update check utility
28
- const { checkForUpdates, showUpdateNotification } = require('../utils/update-check');
28
+ const { checkForUpdates, showUpdateNotification, autoUpdate } = require('../utils/update-check');
29
29
 
30
30
  // State detection for smart default
31
31
  const { detectWorkspaceState, loadContext } = require('../lib/state-detection');
@@ -39,9 +39,11 @@ if (!skipUpdateCheck && (!process.argv[2] || (process.argv[2] && !['version', 'u
39
39
  .then((updateInfo) => {
40
40
  // Show notification if update available (after command completes)
41
41
  if (updateInfo) {
42
- // Wait a bit for command output to finish, then show notification
42
+ // Auto-update in background, fall back to notification if it fails
43
43
  setTimeout(() => {
44
- showUpdateNotification(updateInfo);
44
+ if (!autoUpdate(updateInfo)) {
45
+ showUpdateNotification(updateInfo);
46
+ }
45
47
  }, 100);
46
48
  }
47
49
  return updateInfo;
@@ -54,6 +56,17 @@ if (!skipUpdateCheck && (!process.argv[2] || (process.argv[2] && !['version', 'u
54
56
 
55
57
  const command = process.argv[2];
56
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
+
57
70
  const TOKEN_REFRESH_BUFFER_SECONDS = 300; // Refresh ~5 minutes before expiry
58
71
 
59
72
  function decodeJwtClaims(token) {
@@ -195,6 +208,8 @@ function showHelp() {
195
208
  console.log(' login - Authenticate (use --token <t> for non-interactive)');
196
209
  console.log(' logout - Remove credentials');
197
210
  console.log(' whoami - Show auth status');
211
+ console.log(' switch - Switch account (atris switch <name>)');
212
+ console.log(' accounts - List saved accounts');
198
213
  console.log('');
199
214
  console.log('Integrations:');
200
215
  console.log(' gmail - Email commands (inbox, read)');
@@ -208,6 +223,11 @@ function showHelp() {
208
223
  console.log(' skill audit [name] - Validate skill against Anthropic guide');
209
224
  console.log(' skill fix [name] - Auto-fix common compliance issues');
210
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('');
211
231
  console.log('Other:');
212
232
  console.log(' version - Show Atris version');
213
233
  console.log(' help - Show this help');
@@ -302,7 +322,7 @@ const { initAtris: initCmd } = require('../commands/init');
302
322
  const { syncAtris: syncCmd } = require('../commands/sync');
303
323
  const { logAtris: logCmd } = require('../commands/log');
304
324
  const { logSyncAtris: logSyncCmd } = require('../commands/log-sync');
305
- 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');
306
326
  const { showVersion: versionCmd } = require('../commands/version');
307
327
  const { planAtris: planCmd, doAtris: doCmd, reviewAtris: reviewCmd } = require('../commands/workflow');
308
328
  const { visualizeAtris: visualizeCmd } = require('../commands/visualize');
@@ -314,11 +334,12 @@ const { analyticsAtris: analyticsCmd } = require('../commands/analytics');
314
334
  const { cleanAtris: cleanCmd } = require('../commands/clean');
315
335
  const { verifyAtris: verifyCmd } = require('../commands/verify');
316
336
  const { skillCommand: skillCmd } = require('../commands/skill');
337
+ const { pluginCommand: pluginCmd } = require('../commands/plugin');
317
338
 
318
339
  // Check if this is a known command or natural language input
319
340
  const knownCommands = ['init', 'log', 'status', 'analytics', 'visualize', 'brainstorm', 'autopilot', 'plan', 'do', 'review',
320
- 'activate', 'agent', 'chat', 'login', 'logout', 'whoami', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
321
- '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',
322
343
  'gmail', 'calendar', 'twitter', 'slack', 'integrations'];
323
344
 
324
345
  // Check if command is an atris.md spec file - triggers welcome visualization
@@ -648,6 +669,10 @@ if (command === 'init') {
648
669
  logoutCmd();
649
670
  } else if (command === 'whoami') {
650
671
  whoamiCmd();
672
+ } else if (command === 'switch') {
673
+ switchCmd();
674
+ } else if (command === 'accounts') {
675
+ accountsCmd();
651
676
  } else if (command === 'visualize') {
652
677
  console.log('ℹ️ "atris visualize" is a legacy helper. Visualization is now built into "atris plan".');
653
678
  console.log(' Prefer: atris plan');
@@ -800,6 +825,10 @@ if (command === 'init') {
800
825
  const subcommand = process.argv[3];
801
826
  const args = process.argv.slice(4);
802
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);
803
832
  } else {
804
833
  console.log(`Unknown command: ${command}`);
805
834
  console.log('Run "atris help" to see available commands');
@@ -2631,109 +2660,6 @@ async function atrisDevEntry(userInput = null) {
2631
2660
  console.log('');
2632
2661
  }
2633
2662
 
2634
- function launchAtris() {
2635
- const targetDir = path.join(process.cwd(), 'atris');
2636
- const launcherFile = path.join(targetDir, 'team', 'launcher.md');
2637
-
2638
- if (!fs.existsSync(launcherFile)) {
2639
- console.log('✗ launcher.md not found. Run "atris init" first.');
2640
- process.exit(1);
2641
- }
2642
-
2643
- // Read launcher.md
2644
- const launcherSpec = fs.readFileSync(launcherFile, 'utf8');
2645
-
2646
- // Reference TODO.md (agents read on-demand, legacy TASK_CONTEXTS.md supported)
2647
- const todoFile = path.join(targetDir, 'TODO.md');
2648
- const legacyTaskContextsFile = path.join(targetDir, 'TASK_CONTEXTS.md');
2649
-
2650
- // Reference MAP.md (agents read on-demand)
2651
- const mapFile = path.join(targetDir, 'MAP.md');
2652
- const mapPath = fs.existsSync(mapFile) ? path.relative(process.cwd(), mapFile) : null;
2653
-
2654
- // Reference journal (agents read on-demand)
2655
- const { logFile, dateFormatted } = getLogPath();
2656
- let journalPath = '';
2657
- if (fs.existsSync(logFile)) {
2658
- journalPath = path.relative(process.cwd(), logFile);
2659
- }
2660
-
2661
- console.log('');
2662
- console.log('┌─────────────────────────────────────────────────────────────┐');
2663
- console.log('│ Atris Launch — Launcher Agent Activated │');
2664
- console.log('└─────────────────────────────────────────────────────────────┘');
2665
- console.log('');
2666
- console.log('📋 AGENT SPEC:');
2667
- console.log('─────────────────────────────────────────────────────────────');
2668
- console.log(launcherSpec);
2669
- console.log('');
2670
- console.log('─────────────────────────────────────────────────────────────');
2671
- console.log('');
2672
- const taskFilePath = fs.existsSync(todoFile)
2673
- ? todoFile
2674
- : (fs.existsSync(legacyTaskContextsFile) ? legacyTaskContextsFile : null);
2675
- const taskContextsPath = taskFilePath ? path.relative(process.cwd(), taskFilePath) : null;
2676
- console.log('📝 TODO.md: ' + (taskContextsPath || 'Not found'));
2677
- console.log(' Read for completed tasks context (usually small, or reference path if large).');
2678
- console.log('');
2679
- console.log('─────────────────────────────────────────────────────────────');
2680
- console.log('');
2681
- console.log('🗺️ MAP.md: ' + (mapPath || 'Not found'));
2682
- console.log(' Read this file for file:line references when navigating the codebase.');
2683
- console.log('');
2684
- console.log('─────────────────────────────────────────────────────────────');
2685
- console.log('');
2686
- console.log('📅 JOURNAL: ' + (journalPath || 'Not found'));
2687
- console.log(' Read for recent completions and context.');
2688
- console.log('');
2689
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2690
- console.log('📋 INSTRUCTION PROMPT FOR YOUR CODING AGENT:');
2691
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2692
- console.log('');
2693
- console.log('You are the Launcher. Ship it clean.');
2694
- console.log('');
2695
- console.log('⚠️ CRITICAL: Execute these steps NOW using terminal and file tools:');
2696
- console.log('');
2697
- // Detect if this is the atris package project or a user project
2698
- const isAtrisPackage = fs.existsSync(path.join(process.cwd(), 'package.json')) &&
2699
- fs.existsSync(path.join(process.cwd(), 'bin', 'atris.js')) &&
2700
- fs.existsSync(path.join(process.cwd(), 'atris.md'));
2701
-
2702
- console.log('Launch Workflow:');
2703
- console.log(' 1. Document what was shipped (add Launch entry to journal Notes section)');
2704
- console.log(' 2. Extract learnings (what worked? what would you do differently?)');
2705
- console.log(' 3. Update MAP.md with new patterns/file locations');
2706
- console.log(' 4. Update relevant docs (README, API docs, etc.)');
2707
- console.log(' 5. Clean up (remove temp files, unused code, etc.)');
2708
- if (isAtrisPackage) {
2709
- console.log(' 6. [EXECUTE] Test locally (package development):');
2710
- console.log(' - Run: npm link (link package for local testing)');
2711
- console.log(' - Test: Create test project, run atris init to verify changes');
2712
- console.log(' 7. [EXECUTE] Git commit + push:');
2713
- console.log(' - Run: git add -A');
2714
- console.log(' - Run: git commit -m "Descriptive message about what was shipped"');
2715
- console.log(' - Run: git push origin master');
2716
- console.log(' 8. [EXECUTE] Publish to npm (if ready for release):');
2717
- console.log(' - Run: npm publish');
2718
- console.log(' 9. Optional: Update changelog/blog (7 sentences max essay on what shipped)');
2719
- console.log(' 10. Run: atris log sync (to sync journal to backend)');
2720
- console.log(' 11. Celebrate! 🎉');
2721
- } else {
2722
- console.log(' 6. [EXECUTE] Git commit + push:');
2723
- console.log(' - Run: git add -A');
2724
- console.log(' - Run: git commit -m "Descriptive message about what was shipped"');
2725
- console.log(' - Run: git push origin <your-branch>');
2726
- console.log(' 7. Optional: Update changelog/blog (7 sentences max essay on what shipped)');
2727
- console.log(' 8. Run: atris log sync (to sync journal to backend)');
2728
- console.log(' 9. Celebrate! 🎉');
2729
- }
2730
- console.log('');
2731
- console.log('DO NOT just describe these steps - actually execute the git commands!');
2732
- console.log('');
2733
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2734
- console.log('');
2735
- }
2736
-
2737
2663
  function spawnClaudeCodeSession(url, token, body) {
2738
2664
  return new Promise((resolve, reject) => {
2739
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 };