atris 2.6.3 → 3.0.0

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.
Files changed (54) hide show
  1. package/README.md +124 -34
  2. package/atris/CLAUDE.md +5 -1
  3. package/atris/atris.md +4 -0
  4. package/atris/features/README.md +24 -0
  5. package/atris/skills/autopilot/SKILL.md +74 -75
  6. package/atris/skills/endgame/SKILL.md +179 -0
  7. package/atris/skills/flow/SKILL.md +121 -0
  8. package/atris/skills/improve/SKILL.md +84 -0
  9. package/atris/skills/loop/SKILL.md +72 -0
  10. package/atris/skills/wiki/SKILL.md +61 -0
  11. package/atris/team/executor/MEMBER.md +10 -4
  12. package/atris/team/navigator/MEMBER.md +2 -0
  13. package/atris/team/validator/MEMBER.md +8 -5
  14. package/atris.md +33 -0
  15. package/bin/atris.js +210 -41
  16. package/commands/activate.js +28 -2
  17. package/commands/align.js +720 -0
  18. package/commands/auth.js +75 -2
  19. package/commands/autopilot.js +1213 -270
  20. package/commands/browse.js +100 -0
  21. package/commands/business.js +785 -12
  22. package/commands/clean.js +107 -2
  23. package/commands/computer.js +429 -0
  24. package/commands/context-sync.js +78 -8
  25. package/commands/experiments.js +351 -0
  26. package/commands/feedback.js +150 -0
  27. package/commands/fleet.js +395 -0
  28. package/commands/fork.js +127 -0
  29. package/commands/init.js +50 -1
  30. package/commands/learn.js +407 -0
  31. package/commands/lifecycle.js +94 -0
  32. package/commands/loop.js +114 -0
  33. package/commands/publish.js +129 -0
  34. package/commands/pull.js +369 -38
  35. package/commands/push.js +283 -246
  36. package/commands/review.js +149 -0
  37. package/commands/run.js +76 -43
  38. package/commands/serve.js +360 -0
  39. package/commands/setup.js +1 -1
  40. package/commands/soul.js +381 -0
  41. package/commands/status.js +119 -1
  42. package/commands/sync.js +147 -1
  43. package/commands/terminal.js +201 -0
  44. package/commands/wiki.js +376 -0
  45. package/commands/workflow.js +191 -74
  46. package/commands/workspace-clean.js +3 -3
  47. package/lib/endstate.js +259 -0
  48. package/lib/learnings.js +235 -0
  49. package/lib/manifest.js +1 -0
  50. package/lib/todo.js +9 -5
  51. package/lib/wiki.js +578 -0
  52. package/package.json +2 -2
  53. package/utils/api.js +40 -35
  54. package/utils/auth.js +1 -0
@@ -20,6 +20,18 @@ function saveBusinesses(data) {
20
20
  fs.writeFileSync(getBusinessConfigPath(), JSON.stringify(data, null, 2));
21
21
  }
22
22
 
23
+ function detectBusinessSlug(explicitSlug) {
24
+ if (explicitSlug) return explicitSlug;
25
+ const bizFile = path.join(process.cwd(), '.atris', 'business.json');
26
+ if (!fs.existsSync(bizFile)) return null;
27
+ try {
28
+ const biz = JSON.parse(fs.readFileSync(bizFile, 'utf8'));
29
+ return biz.slug || biz.name || null;
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
23
35
  async function addBusiness(slug) {
24
36
  if (!slug) {
25
37
  console.error('Usage: atris business add <slug>');
@@ -33,14 +45,14 @@ async function addBusiness(slug) {
33
45
  }
34
46
 
35
47
  // Resolve slug to business
36
- const result = await apiRequestJson(`/businesses/by-slug/${slug}`, {
48
+ const result = await apiRequestJson(`/business/by-slug/${slug}`, {
37
49
  method: 'GET',
38
50
  token: creds.token,
39
51
  });
40
52
 
41
53
  if (!result.ok) {
42
54
  // Try listing all and matching
43
- const listResult = await apiRequestJson('/businesses/', { method: 'GET', token: creds.token });
55
+ const listResult = await apiRequestJson('/business/', { method: 'GET', token: creds.token });
44
56
  if (listResult.ok && Array.isArray(listResult.data)) {
45
57
  const match = listResult.data.find(b => b.slug === slug || b.name.toLowerCase() === slug.toLowerCase());
46
58
  if (match) {
@@ -74,7 +86,14 @@ async function addBusiness(slug) {
74
86
  console.log(`\nAdded "${biz.name}" (${biz.slug})`);
75
87
  }
76
88
 
77
- async function listBusinesses() {
89
+ async function listBusinesses(opts = {}) {
90
+ // --local mode: walk ~/arena/atris-business/ and show fleet status table
91
+ // (no API calls, rate-limit safe). Different from API-mode below which lists
92
+ // businesses cached from the API.
93
+ if (opts.local) {
94
+ return listBusinessesLocal(opts);
95
+ }
96
+
78
97
  const businesses = loadBusinesses();
79
98
  const slugs = Object.keys(businesses);
80
99
 
@@ -93,6 +112,159 @@ async function listBusinesses() {
93
112
  }
94
113
  }
95
114
 
115
+ /**
116
+ * Walk ~/arena/atris-business/ and print a fleet status table for every
117
+ * customer workspace. Pure local — no API calls, no rate-limit risk.
118
+ *
119
+ * Classifies each dir as: canonical, flat, unbound, nested, bare, or superseded.
120
+ *
121
+ * Discovered the need for this during overnight loop tick #3 when we hand-wrote
122
+ * /tmp/customer_fleet.md. Now any team member can run `atris business list --local`
123
+ * (or `atris business fleet`) to see fleet state in one shot.
124
+ */
125
+ function listBusinessesLocal(opts = {}) {
126
+ const os = require('os');
127
+ const SKIP_DIRS = new Set(['deals', 'archive', 'archives', '_archive', 'templates', 'node_modules', '.git']);
128
+ const SKIP_FILES = new Set(['.DS_Store', 'Thumbs.db']);
129
+
130
+ const rootDir = opts.root || path.join(os.homedir(), 'arena', 'atris-business');
131
+ const jsonMode = opts.json === true;
132
+
133
+ if (!fs.existsSync(rootDir)) {
134
+ console.error(`Fleet root not found: ${rootDir}`);
135
+ process.exit(1);
136
+ }
137
+
138
+ function countFiles(dir) {
139
+ let total = 0;
140
+ let md = 0;
141
+ function walk(d) {
142
+ let entries;
143
+ try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
144
+ for (const e of entries) {
145
+ if (e.name.startsWith('.git')) continue;
146
+ if (e.name === 'node_modules') continue;
147
+ const full = path.join(d, e.name);
148
+ if (e.isDirectory()) {
149
+ walk(full);
150
+ } else if (e.isFile()) {
151
+ if (SKIP_FILES.has(e.name)) continue;
152
+ total++;
153
+ if (e.name.endsWith('.md')) md++;
154
+ }
155
+ }
156
+ }
157
+ walk(dir);
158
+ return { total, md };
159
+ }
160
+
161
+ function classifyCustomer(name) {
162
+ const customerDir = path.join(rootDir, name);
163
+ const businessJson = path.join(customerDir, '.atris', 'business.json');
164
+ const atrisDir = path.join(customerDir, 'atris');
165
+ const nestedDir = path.join(customerDir, name);
166
+
167
+ const hasBizJson = fs.existsSync(businessJson);
168
+ const hasAtris = fs.existsSync(atrisDir) && fs.statSync(atrisDir).isDirectory();
169
+ const hasNested = fs.existsSync(nestedDir) && fs.statSync(nestedDir).isDirectory();
170
+ const { total, md } = countFiles(customerDir);
171
+
172
+ let state, action, icon;
173
+ if (hasBizJson && hasAtris) {
174
+ state = 'canonical'; action = 'none'; icon = '🟢';
175
+ } else if (hasBizJson && !hasAtris) {
176
+ state = 'flat'; action = 'migrate to atris/ wrapper'; icon = '🟡';
177
+ } else if (!hasBizJson && hasAtris) {
178
+ state = 'unbound'; action = 'create .atris/business.json'; icon = '🟡';
179
+ } else if (hasNested) {
180
+ state = 'nested'; action = 'legacy nesting bug'; icon = '🔴';
181
+ } else if (total < 5) {
182
+ state = 'bare'; action = 'not yet onboarded'; icon = '⚪';
183
+ } else {
184
+ state = 'flat-unbound'; action = 'needs canonical init'; icon = '🟡';
185
+ }
186
+
187
+ let bizName = name;
188
+ if (hasBizJson) {
189
+ try {
190
+ const meta = JSON.parse(fs.readFileSync(businessJson, 'utf8'));
191
+ bizName = meta.name || name;
192
+ } catch {}
193
+ }
194
+
195
+ return { name, bizName, state, icon, files: total, md, hasBizJson, hasAtris, hasNested, action };
196
+ }
197
+
198
+ const entries = fs.readdirSync(rootDir, { withFileTypes: true })
199
+ .filter((e) => e.isDirectory())
200
+ .filter((e) => !e.name.startsWith('.'))
201
+ .filter((e) => !SKIP_DIRS.has(e.name))
202
+ .map((e) => e.name)
203
+ .sort();
204
+
205
+ const customers = entries.map(classifyCustomer);
206
+
207
+ // Mark superseded: any customer with a -canonical sibling is superseded
208
+ const canonicalNames = new Set(
209
+ customers.filter((c) => c.name.endsWith('-canonical')).map((c) => c.name.replace(/-canonical$/, ''))
210
+ );
211
+ for (const c of customers) {
212
+ if (canonicalNames.has(c.name)) {
213
+ c.state = 'superseded';
214
+ c.icon = '🔴';
215
+ c.action = `superseded by ${c.name}-canonical`;
216
+ }
217
+ }
218
+
219
+ if (jsonMode) {
220
+ console.log(JSON.stringify({ root: rootDir, customers }, null, 2));
221
+ return;
222
+ }
223
+
224
+ console.log('');
225
+ console.log(`Atris Fleet — ${rootDir}`);
226
+ console.log('═'.repeat(86));
227
+ console.log(' CUSTOMER STATE FILES BIZ.JSON ATRIS/ ACTION');
228
+ console.log(' ' + '─'.repeat(83));
229
+
230
+ const order = ['canonical', 'flat', 'unbound', 'flat-unbound', 'bare', 'nested', 'superseded'];
231
+ const grouped = {};
232
+ for (const c of customers) {
233
+ if (!grouped[c.state]) grouped[c.state] = [];
234
+ grouped[c.state].push(c);
235
+ }
236
+
237
+ for (const state of order) {
238
+ if (!grouped[state]) continue;
239
+ for (const c of grouped[state]) {
240
+ const name = c.name.padEnd(20).slice(0, 20);
241
+ const stateLabel = (c.icon + ' ' + state).padEnd(13).slice(0, 13);
242
+ const filesStr = String(c.files).padStart(5);
243
+ const bizStr = c.hasBizJson ? ' ✓ ' : ' ✗ ';
244
+ const atrisStr = c.hasAtris ? ' ✓ ' : ' ✗ ';
245
+ const action = c.action.length > 28 ? c.action.slice(0, 25) + '...' : c.action;
246
+ console.log(` ${name} ${stateLabel} ${filesStr} ${bizStr} ${atrisStr} ${action}`);
247
+ }
248
+ }
249
+
250
+ console.log(' ' + '─'.repeat(83));
251
+
252
+ const counts = {};
253
+ for (const c of customers) counts[c.state] = (counts[c.state] || 0) + 1;
254
+ const summary = order.filter((s) => counts[s]).map((s) => `${counts[s]} ${s}`).join(', ');
255
+ console.log(` ${customers.length} customers — ${summary}`);
256
+ console.log('');
257
+
258
+ const needsWork = customers.filter((c) => ['flat', 'unbound', 'flat-unbound', 'nested'].includes(c.state));
259
+ if (needsWork.length > 0) {
260
+ console.log(' Next actions:');
261
+ needsWork.slice(0, 5).forEach((c) => {
262
+ console.log(` ${c.icon} ${c.name}: ${c.action}`);
263
+ });
264
+ console.log('');
265
+ }
266
+ }
267
+
96
268
  async function removeBusiness(slug) {
97
269
  if (!slug) {
98
270
  console.error('Usage: atris business remove <slug>');
@@ -122,7 +294,7 @@ async function resolveSlug(slug, creds) {
122
294
  }
123
295
 
124
296
  // Try by-slug endpoint
125
- const result = await apiRequestJson(`/businesses/by-slug/${slug}/`, {
297
+ const result = await apiRequestJson(`/business/by-slug/${slug}/`, {
126
298
  method: 'GET',
127
299
  token: creds.token,
128
300
  });
@@ -131,7 +303,7 @@ async function resolveSlug(slug, creds) {
131
303
  }
132
304
 
133
305
  // Fallback: list all and match
134
- const listResult = await apiRequestJson('/businesses/', { method: 'GET', token: creds.token });
306
+ const listResult = await apiRequestJson('/business/', { method: 'GET', token: creds.token });
135
307
  if (listResult.ok && Array.isArray(listResult.data)) {
136
308
  const match = listResult.data.find(b => b.slug === slug || b.name.toLowerCase() === slug.toLowerCase());
137
309
  if (match) {
@@ -189,9 +361,9 @@ async function businessHealth(slug) {
189
361
  // Fetch dashboard and workspace snapshot in parallel
190
362
  const fetchOpts = { method: 'GET', token: creds.token, timeoutMs: 120000 };
191
363
  const [dashResult, wsResult] = await Promise.all([
192
- apiRequestJson(`/businesses/${bizId}/dashboard/`, fetchOpts),
364
+ apiRequestJson(`/business/${bizId}/dashboard/`, fetchOpts),
193
365
  wsId
194
- ? apiRequestJson(`/businesses/${bizId}/workspaces/${wsId}/snapshot?include_content=false`, fetchOpts)
366
+ ? apiRequestJson(`/business/${bizId}/workspaces/${wsId}/snapshot?include_content=false`, fetchOpts)
195
367
  : Promise.resolve({ ok: false }),
196
368
  ]);
197
369
 
@@ -303,7 +475,7 @@ async function businessAudit() {
303
475
  process.exit(1);
304
476
  }
305
477
 
306
- const listResult = await apiRequestJson('/businesses/', { method: 'GET', token: creds.token });
478
+ const listResult = await apiRequestJson('/business/', { method: 'GET', token: creds.token });
307
479
  if (!listResult.ok || !Array.isArray(listResult.data)) {
308
480
  console.error(`Failed to fetch businesses: ${listResult.error || 'unknown error'}`);
309
481
  process.exit(1);
@@ -347,15 +519,578 @@ async function businessAudit() {
347
519
  console.log('');
348
520
  }
349
521
 
522
+ async function createBusiness(name, ...flags) {
523
+ if (!name) {
524
+ console.error('Usage: atris business create <name> [--description "..."]');
525
+ process.exit(1);
526
+ }
527
+
528
+ const creds = loadCredentials();
529
+ if (!creds || !creds.token) {
530
+ console.error('Not logged in. Run: atris login');
531
+ process.exit(1);
532
+ }
533
+
534
+ // Parse flags
535
+ let description = '';
536
+ for (let i = 0; i < flags.length; i++) {
537
+ if ((flags[i] === '--description' || flags[i] === '-d') && flags[i + 1]) {
538
+ description = flags[i + 1];
539
+ i++;
540
+ }
541
+ }
542
+
543
+ console.log(`Creating business: ${name}...`);
544
+
545
+ const result = await apiRequestJson('/business/', {
546
+ method: 'POST',
547
+ token: creds.token,
548
+ body: { name, description: description || undefined },
549
+ });
550
+
551
+ if (!result.ok) {
552
+ console.error(`Failed: ${result.errorMessage || result.error || result.status}`);
553
+ process.exit(1);
554
+ }
555
+
556
+ const biz = result.data;
557
+
558
+ // Register locally
559
+ const businesses = loadBusinesses();
560
+ businesses[biz.slug] = {
561
+ business_id: biz.id,
562
+ workspace_id: biz.workspace_id,
563
+ name: biz.name,
564
+ slug: biz.slug,
565
+ agent_id: biz.agent_id,
566
+ added_at: new Date().toISOString(),
567
+ };
568
+ saveBusinesses(businesses);
569
+
570
+ // Scaffold local directory if in an atris project
571
+ const atrisDir = findAtrisDir();
572
+ if (atrisDir) {
573
+ const bizDir = path.join(atrisDir, 'business', biz.slug);
574
+ if (!fs.existsSync(bizDir)) {
575
+ fs.mkdirSync(path.join(bizDir, 'context'), { recursive: true });
576
+ fs.mkdirSync(path.join(bizDir, 'team'), { recursive: true });
577
+ fs.mkdirSync(path.join(bizDir, 'workspace'), { recursive: true });
578
+ fs.writeFileSync(path.join(bizDir, 'BUSINESS.md'), [
579
+ `# ${biz.name}`,
580
+ description ? `\n> ${description}\n` : '',
581
+ '\n## The Business\n\n[What problem does this solve?]\n',
582
+ '## Revenue Model\n\n[How does this make money?]\n',
583
+ `---\n*Created: ${new Date().toISOString().split('T')[0]}*\n`,
584
+ ].join(''));
585
+ console.log(` Local scaffold: ${bizDir}/`);
586
+ }
587
+ }
588
+
589
+ // Apply template if specified
590
+ let template = null;
591
+ for (let i = 0; i < flags.length; i++) {
592
+ if ((flags[i] === '--template' || flags[i] === '-t') && flags[i + 1]) {
593
+ template = flags[i + 1];
594
+ i++;
595
+ }
596
+ }
597
+
598
+ if (template) {
599
+ const templates = {
600
+ 'saas': { agents: ['growth-hacker', 'product-analyst', 'support-agent'], desc: 'SaaS Startup' },
601
+ 'agency': { agents: ['project-manager', 'researcher', 'outreach-agent'], desc: 'Agency / Consulting' },
602
+ 'ecommerce': { agents: ['inventory-analyst', 'marketing-agent', 'support-agent'], desc: 'E-Commerce' },
603
+ 'content': { agents: ['writer', 'researcher', 'social-media-agent'], desc: 'Content Creator' },
604
+ 'restaurant': { agents: ['review-responder', 'social-media-agent', 'booking-agent'], desc: 'Restaurant / Local' },
605
+ };
606
+ const tpl = templates[template.toLowerCase()];
607
+ if (tpl) {
608
+ console.log(` Template: ${tpl.desc} (${tpl.agents.length} agents)`);
609
+ for (const agentName of tpl.agents) {
610
+ console.log(` + ${agentName}`);
611
+ }
612
+ } else {
613
+ console.log(` Unknown template: ${template}`);
614
+ console.log(` Available: ${Object.keys(templates).join(', ')}`);
615
+ }
616
+ }
617
+
618
+ console.log(`\n Business created!`);
619
+ console.log(` ID: ${biz.id}`);
620
+ console.log(` Slug: ${biz.slug}`);
621
+ console.log(` Agent: ${biz.agent_id || '(none)'}`);
622
+ console.log(` Dashboard: https://atris.ai/dashboard/gm/${biz.id}`);
623
+ console.log('');
624
+ }
625
+
626
+
627
+ async function businessStatus(slug) {
628
+ const creds = loadCredentials();
629
+ if (!creds || !creds.token) {
630
+ console.error('Not logged in. Run: atris login');
631
+ process.exit(1);
632
+ }
633
+
634
+ const resolved = await resolveSlug(slug, creds);
635
+ if (!resolved) {
636
+ console.error('No business specified. Usage: atris business status <slug>');
637
+ process.exit(1);
638
+ }
639
+
640
+ const result = await apiRequestJson(`/business/${resolved.business_id}`, {
641
+ method: 'GET',
642
+ token: creds.token,
643
+ });
644
+
645
+ if (!result.ok) {
646
+ console.error(`Failed to fetch business: ${result.errorMessage || result.status}`);
647
+ return;
648
+ }
649
+
650
+ const biz = result.data;
651
+ const agents = biz.member_count || 0;
652
+ const apps = biz.app_count || 0;
653
+
654
+ // Quick status line
655
+ console.log(`\n ${biz.name} (${biz.slug})`);
656
+ console.log(` ${'─'.repeat(40)}`);
657
+ console.log(` Agents: ${agents}`);
658
+ console.log(` Apps: ${apps}`);
659
+ if (biz.workspace_id) console.log(` Workspace: ${biz.workspace_id.slice(0, 12)}...`);
660
+ console.log(` Created: ${biz.created_at ? biz.created_at.split('T')[0] : '?'}`);
661
+ console.log('');
662
+ }
663
+
664
+ function describeAccess(member) {
665
+ const role = (member.role || '').toLowerCase();
666
+ if (role === 'owner') return 'full control';
667
+ if (role === 'admin') return 'admin access';
668
+ if (role === 'member') return 'standard access';
669
+ if (role === 'agent') return 'agent';
670
+ return role || 'unknown';
671
+ }
672
+
673
+ async function businessTeam(slug) {
674
+ const requestedSlug = detectBusinessSlug(slug);
675
+ if (!requestedSlug) {
676
+ console.error('No business specified. Usage: atris business team <slug>');
677
+ process.exit(1);
678
+ }
679
+
680
+ const creds = loadCredentials();
681
+ if (!creds || !creds.token) {
682
+ console.error('Not logged in. Run: atris login');
683
+ process.exit(1);
684
+ }
685
+
686
+ const resolved = await resolveSlug(requestedSlug, creds);
687
+ if (!resolved) {
688
+ console.error(`Business "${requestedSlug}" not found.`);
689
+ process.exit(1);
690
+ }
691
+
692
+ const result = await apiRequestJson(`/business/${resolved.business_id}`, {
693
+ method: 'GET',
694
+ token: creds.token,
695
+ });
696
+
697
+ if (!result.ok) {
698
+ console.error(`Failed to fetch business team: ${result.errorMessage || result.status}`);
699
+ process.exit(1);
700
+ }
701
+
702
+ const biz = result.data || {};
703
+ const members = Array.isArray(biz.members) ? [...biz.members] : [];
704
+ const roleOrder = { owner: 0, admin: 1, member: 2, agent: 3 };
705
+ members.sort((a, b) => {
706
+ const roleDelta = (roleOrder[a.role] ?? 99) - (roleOrder[b.role] ?? 99);
707
+ if (roleDelta !== 0) return roleDelta;
708
+ const aName = (a.display_name || a.name || a.email || '').toLowerCase();
709
+ const bName = (b.display_name || b.name || b.email || '').toLowerCase();
710
+ return aName.localeCompare(bName);
711
+ });
712
+
713
+ const admins = members.filter(m => ['owner', 'admin'].includes((m.role || '').toLowerCase()));
714
+ const nonAdmins = members.filter(m => !['owner', 'admin'].includes((m.role || '').toLowerCase()));
715
+ const roleCounts = members.reduce((acc, member) => {
716
+ const role = member.role || 'unknown';
717
+ acc[role] = (acc[role] || 0) + 1;
718
+ return acc;
719
+ }, {});
720
+ const roleSummary = Object.entries(roleCounts)
721
+ .sort((a, b) => (roleOrder[a[0]] ?? 99) - (roleOrder[b[0]] ?? 99))
722
+ .map(([role, count]) => `${count} ${role}${count === 1 ? '' : 's'}`)
723
+ .join(', ');
724
+
725
+ console.log('');
726
+ console.log(`Business Team: ${biz.name || resolved.name || requestedSlug} (${biz.slug || resolved.slug || requestedSlug})`);
727
+ console.log('━'.repeat(32 + (biz.name || resolved.name || requestedSlug).length));
728
+ console.log('');
729
+ console.log(` Members: ${members.length}`);
730
+ console.log(` Roles: ${roleSummary || 'none'}`);
731
+ console.log(` Admins: ${admins.length}`);
732
+
733
+ if (admins.length > 0) {
734
+ console.log('');
735
+ console.log(' Admin Access:');
736
+ for (const member of admins) {
737
+ const name = member.display_name || member.name || member.email || 'Unknown';
738
+ const email = member.email || '(no email)';
739
+ const role = member.role || 'unknown';
740
+ console.log(` ${name.padEnd(24)} ${role.padEnd(8)} ${describeAccess(member).padEnd(14)} ${email}`);
741
+ }
742
+ }
743
+
744
+ if (nonAdmins.length > 0) {
745
+ console.log('');
746
+ console.log(' Standard Access:');
747
+ for (const member of nonAdmins) {
748
+ const name = member.display_name || member.name || member.email || 'Unknown';
749
+ const email = member.email || '(no email)';
750
+ const role = member.role || 'unknown';
751
+ console.log(` ${name.padEnd(24)} ${role.padEnd(8)} ${describeAccess(member).padEnd(14)} ${email}`);
752
+ }
753
+ }
754
+
755
+ console.log('');
756
+ }
757
+
758
+
759
+ async function connectService(connector, ...flags) {
760
+ if (!connector) {
761
+ console.log('Usage: atris business connect <service> [--business <slug>]');
762
+ console.log('');
763
+ console.log('Available connectors:');
764
+ // List skills that look like integrations
765
+ const skillDirs = [
766
+ path.join(__dirname, '..', '..', '.claude', 'skills'),
767
+ path.join(require('os').homedir(), '.claude', 'skills'),
768
+ ];
769
+ const seen = new Set();
770
+ for (const dir of skillDirs) {
771
+ if (!fs.existsSync(dir)) continue;
772
+ for (const name of fs.readdirSync(dir)) {
773
+ const skillFile = path.join(dir, name, 'SKILL.md');
774
+ if (fs.existsSync(skillFile) && !seen.has(name)) {
775
+ seen.add(name);
776
+ }
777
+ }
778
+ }
779
+ const integrations = [...seen].filter(s =>
780
+ ['slack', 'hubspot', 'linear', 'notion', 'google-drive', 'github',
781
+ 'calendar', 'email-agent', 'x-search', 'youtube', 'ramp'].includes(s)
782
+ ).sort();
783
+ for (const s of integrations) {
784
+ console.log(` ${s}`);
785
+ }
786
+ if (integrations.length === 0) console.log(' (none found — install skills first)');
787
+ return;
788
+ }
789
+
790
+ // Parse --business flag
791
+ let bizSlug = null;
792
+ for (let i = 0; i < flags.length; i++) {
793
+ if ((flags[i] === '--business' || flags[i] === '-b') && flags[i + 1]) {
794
+ bizSlug = flags[i + 1];
795
+ i++;
796
+ }
797
+ }
798
+
799
+ // Find the skill
800
+ const skillDirs = [
801
+ path.join(__dirname, '..', '..', '.claude', 'skills', connector),
802
+ path.join(require('os').homedir(), '.claude', 'skills', connector),
803
+ ];
804
+ let skillPath = null;
805
+ for (const dir of skillDirs) {
806
+ const p = path.join(dir, 'SKILL.md');
807
+ if (fs.existsSync(p)) { skillPath = p; break; }
808
+ }
809
+
810
+ if (!skillPath) {
811
+ console.error(`Skill "${connector}" not found.`);
812
+ console.error('Check: .claude/skills/ or ~/.claude/skills/');
813
+ process.exit(1);
814
+ }
815
+
816
+ console.log(`\n Connecting: ${connector}`);
817
+ console.log(` Skill: ${skillPath}`);
818
+ if (bizSlug) console.log(` Business: ${bizSlug}`);
819
+
820
+ // Read skill to check for required secrets
821
+ const skillContent = fs.readFileSync(skillPath, 'utf8');
822
+ const secretMatches = skillContent.match(/[A-Z][A-Z0-9_]*_(?:KEY|TOKEN|SECRET|PASSWORD|API_KEY)/g) || [];
823
+ const uniqueSecrets = [...new Set(secretMatches)];
824
+
825
+ if (uniqueSecrets.length > 0) {
826
+ console.log(`\n Required secrets:`);
827
+ for (const secret of uniqueSecrets) {
828
+ console.log(` ${secret}`);
829
+ }
830
+ console.log(`\n Store secrets with: atris computer run "echo $${uniqueSecrets[0]}"`);
831
+ console.log(` Or set in: ~/.atris/secrets/${connector}/`);
832
+ }
833
+
834
+ // Create local secrets directory
835
+ const secretsDir = path.join(require('os').homedir(), '.atris', 'secrets', connector);
836
+ if (!fs.existsSync(secretsDir)) {
837
+ fs.mkdirSync(secretsDir, { recursive: true });
838
+ console.log(`\n Created secrets dir: ${secretsDir}/`);
839
+ }
840
+
841
+ console.log(`\n Connected "${connector}" skill.`);
842
+ console.log(` Agent can now use ${connector} capabilities.`);
843
+ console.log('');
844
+ }
845
+
846
+
847
+ async function setNotificationMode(mode, ...flags) {
848
+ const validModes = ['digest', 'silent', 'push'];
849
+ if (!mode || !validModes.includes(mode)) {
850
+ console.log('Usage: atris business notify <digest|silent|push> [--business <slug>]');
851
+ console.log('');
852
+ console.log(' digest Batch all reports into morning briefing (1 email/day)');
853
+ console.log(' silent Log only, never notify (check with `atris business status`)');
854
+ console.log(' push Interrupt immediately on every action (default, noisy)');
855
+ return;
856
+ }
857
+
858
+ const creds = loadCredentials();
859
+ if (!creds || !creds.token) {
860
+ console.error('Not logged in. Run: atris login');
861
+ process.exit(1);
862
+ }
863
+
864
+ // Parse --business flag
865
+ let bizSlug = null;
866
+ for (let i = 0; i < flags.length; i++) {
867
+ if ((flags[i] === '--business' || flags[i] === '-b') && flags[i + 1]) {
868
+ bizSlug = flags[i + 1];
869
+ i++;
870
+ }
871
+ }
872
+
873
+ const resolved = await resolveSlug(bizSlug, creds);
874
+ if (!resolved) {
875
+ console.error('No business specified. Usage: atris business notify digest --business <slug>');
876
+ process.exit(1);
877
+ }
878
+
879
+ // Update business config with notification mode
880
+ const result = await apiRequestJson(`/business/${resolved.business_id}`, {
881
+ method: 'PUT',
882
+ token: creds.token,
883
+ body: {
884
+ config: { notification_mode: mode },
885
+ },
886
+ });
887
+
888
+ if (!result.ok) {
889
+ console.error(`Failed: ${result.errorMessage || result.status}`);
890
+ process.exit(1);
891
+ }
892
+
893
+ const icons = { digest: '📬', silent: '🔇', push: '🔔' };
894
+ const descriptions = {
895
+ digest: 'Agents report in morning briefing only (1 email/day)',
896
+ silent: 'Everything logged, nothing notified',
897
+ push: 'Every action sends a notification',
898
+ };
899
+
900
+ console.log(`\n ${icons[mode]} Notification mode: ${mode}`);
901
+ console.log(` ${descriptions[mode]}`);
902
+ console.log(` Business: ${resolved.name || resolved.slug}`);
903
+ console.log('');
904
+ }
905
+
906
+
907
+ async function deployBusiness(slug) {
908
+ if (!slug) {
909
+ console.error('Usage: atris business deploy <slug>');
910
+ console.error(' Pushes local atris/business/<slug>/ to the cloud business.');
911
+ process.exit(1);
912
+ }
913
+
914
+ const creds = loadCredentials();
915
+ if (!creds || !creds.token) {
916
+ console.error('Not logged in. Run: atris login');
917
+ process.exit(1);
918
+ }
919
+
920
+ // Find local business directory
921
+ const atrisDir = findAtrisDir();
922
+ if (!atrisDir) {
923
+ console.error('Not in an atris project. Run from a directory with atris/ folder.');
924
+ process.exit(1);
925
+ }
926
+
927
+ const bizDir = path.join(atrisDir, 'business', slug);
928
+ if (!fs.existsSync(bizDir)) {
929
+ console.error(`Local business not found: ${bizDir}`);
930
+ console.error(`Create with: atris business create "${slug}"`);
931
+ process.exit(1);
932
+ }
933
+
934
+ // Check if business exists in cloud
935
+ const businesses = loadBusinesses();
936
+ let bizConfig = businesses[slug];
937
+
938
+ if (!bizConfig) {
939
+ // Try to find by slug in cloud
940
+ const listResult = await apiRequestJson('/business/', { method: 'GET', token: creds.token });
941
+ if (listResult.ok && Array.isArray(listResult.data)) {
942
+ const match = listResult.data.find(b => b.slug === slug);
943
+ if (match) {
944
+ bizConfig = { business_id: match.id, workspace_id: match.workspace_id, name: match.name, slug: match.slug };
945
+ businesses[slug] = { ...bizConfig, added_at: new Date().toISOString() };
946
+ saveBusinesses(businesses);
947
+ }
948
+ }
949
+ }
950
+
951
+ if (!bizConfig || !bizConfig.business_id) {
952
+ console.log(` Business "${slug}" not in cloud. Creating...`);
953
+ const bizMd = path.join(bizDir, 'BUSINESS.md');
954
+ const name = slug.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
955
+ const createResult = await apiRequestJson('/business/', {
956
+ method: 'POST', token: creds.token,
957
+ body: { name },
958
+ });
959
+ if (!createResult.ok) {
960
+ console.error(`Failed to create: ${createResult.errorMessage || createResult.status}`);
961
+ process.exit(1);
962
+ }
963
+ bizConfig = {
964
+ business_id: createResult.data.id,
965
+ workspace_id: createResult.data.workspace_id,
966
+ name: createResult.data.name,
967
+ slug: createResult.data.slug,
968
+ };
969
+ businesses[slug] = { ...bizConfig, added_at: new Date().toISOString() };
970
+ saveBusinesses(businesses);
971
+ console.log(` Created: ${bizConfig.name} (${bizConfig.business_id.slice(0, 12)}...)`);
972
+ }
973
+
974
+ // Upload workspace files
975
+ const workspaceDir = path.join(bizDir, 'workspace');
976
+ let uploadCount = 0;
977
+ if (fs.existsSync(workspaceDir)) {
978
+ const files = walkDir(workspaceDir);
979
+ for (const filePath of files) {
980
+ const relativePath = path.relative(workspaceDir, filePath);
981
+ if (relativePath.startsWith('.')) continue;
982
+ try {
983
+ const content = fs.readFileSync(filePath, 'utf8');
984
+ const uploadResult = await apiRequestJson(
985
+ `/business/${bizConfig.business_id}/workspaces/${bizConfig.workspace_id}/file`,
986
+ { method: 'PUT', token: creds.token, body: { path: '/' + relativePath, content } }
987
+ );
988
+ if (uploadResult.ok) {
989
+ uploadCount++;
990
+ process.stdout.write(` Uploaded: ${relativePath}\n`);
991
+ }
992
+ } catch (e) {
993
+ // Skip binary files or errors
994
+ }
995
+ }
996
+ }
997
+
998
+ // Upload BUSINESS.md as context
999
+ const bizMd = path.join(bizDir, 'BUSINESS.md');
1000
+ if (fs.existsSync(bizMd)) {
1001
+ try {
1002
+ const content = fs.readFileSync(bizMd, 'utf8');
1003
+ await apiRequestJson(
1004
+ `/business/${bizConfig.business_id}/workspaces/${bizConfig.workspace_id}/file`,
1005
+ { method: 'PUT', token: creds.token, body: { path: '/BUSINESS.md', content } }
1006
+ );
1007
+ uploadCount++;
1008
+ console.log(' Uploaded: BUSINESS.md');
1009
+ } catch {}
1010
+ }
1011
+
1012
+ console.log(`\n Deployed ${uploadCount} files to ${bizConfig.name}`);
1013
+ console.log(` Dashboard: https://atris.ai/dashboard/gm/${bizConfig.business_id}`);
1014
+ console.log('');
1015
+ }
1016
+
1017
+
1018
+ function walkDir(dir) {
1019
+ let results = [];
1020
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
1021
+ const full = path.join(dir, entry.name);
1022
+ if (entry.isDirectory()) {
1023
+ results = results.concat(walkDir(full));
1024
+ } else {
1025
+ results.push(full);
1026
+ }
1027
+ }
1028
+ return results;
1029
+ }
1030
+
1031
+
1032
+ function findAtrisDir() {
1033
+ let dir = process.cwd();
1034
+ while (dir !== path.dirname(dir)) {
1035
+ if (fs.existsSync(path.join(dir, 'atris'))) return path.join(dir, 'atris');
1036
+ dir = path.dirname(dir);
1037
+ }
1038
+ return null;
1039
+ }
1040
+
1041
+
1042
+ async function quickstart() {
1043
+ console.log(`
1044
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1045
+ Start a Business in 3 Commands
1046
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1047
+
1048
+ 1. Create:
1049
+ atris business create "My Company" --template saas
1050
+
1051
+ 2. Connect integrations:
1052
+ atris business connect slack --business my-company
1053
+ atris business connect github --business my-company
1054
+
1055
+ 3. Deploy:
1056
+ atris business deploy my-company
1057
+
1058
+ That's it. Your agents are live.
1059
+
1060
+ Optional:
1061
+ atris business notify digest --business my-company
1062
+ (get 1 email/day instead of every notification)
1063
+
1064
+ Templates: saas, agency, ecommerce, content, restaurant
1065
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1066
+ `);
1067
+ }
1068
+
1069
+
350
1070
  async function businessCommand(subcommand, ...args) {
351
1071
  switch (subcommand) {
352
1072
  case 'add':
353
1073
  await addBusiness(args[0]);
354
1074
  break;
1075
+ case 'create':
1076
+ case 'new':
1077
+ await createBusiness(args[0], ...args.slice(1));
1078
+ break;
355
1079
  case 'list':
356
- case 'ls':
357
- await listBusinesses();
1080
+ case 'ls': {
1081
+ const opts = {};
1082
+ if (args.includes('--local')) opts.local = true;
1083
+ if (args.includes('--json')) opts.json = true;
1084
+ await listBusinesses(opts);
358
1085
  break;
1086
+ }
1087
+ case 'fleet': {
1088
+ // Shorthand for `business list --local`
1089
+ const opts = { local: true };
1090
+ if (args.includes('--json')) opts.json = true;
1091
+ await listBusinesses(opts);
1092
+ break;
1093
+ }
359
1094
  case 'remove':
360
1095
  case 'rm':
361
1096
  await removeBusiness(args[0]);
@@ -363,12 +1098,50 @@ async function businessCommand(subcommand, ...args) {
363
1098
  case 'health':
364
1099
  await businessHealth(args[0]);
365
1100
  break;
1101
+ case 'team':
1102
+ case 'members':
1103
+ case 'roster':
1104
+ await businessTeam(args[0]);
1105
+ break;
1106
+ case 'status':
1107
+ await businessStatus(args[0]);
1108
+ break;
366
1109
  case 'audit':
367
1110
  await businessAudit();
368
1111
  break;
1112
+ case 'connect':
1113
+ await connectService(args[0], ...args.slice(1));
1114
+ break;
1115
+ case 'notify':
1116
+ case 'notification':
1117
+ await setNotificationMode(args[0], ...args.slice(1));
1118
+ break;
1119
+ case 'deploy':
1120
+ case 'push':
1121
+ await deployBusiness(args[0]);
1122
+ break;
1123
+ case 'quickstart':
1124
+ case 'start':
1125
+ case 'guide':
1126
+ await quickstart();
1127
+ break;
369
1128
  default:
370
- console.log('Usage: atris business <add|list|remove|health|audit> [slug]');
1129
+ console.log('Usage: atris business <command> [args]');
1130
+ console.log('');
1131
+ console.log(' quickstart ← Start here! 3-command guide');
1132
+ console.log('');
1133
+ console.log(' create <name> Create a new business (cloud + local)');
1134
+ console.log(' add <slug> Register an existing cloud business');
1135
+ console.log(' list Show registered businesses');
1136
+ console.log(' team [slug] Show members, roles, and admin access');
1137
+ console.log(' status <slug> Quick status check');
1138
+ console.log(' health [slug] Full health dashboard');
1139
+ console.log(' audit Audit all businesses');
1140
+ console.log(' connect <service> Connect a skill/integration');
1141
+ console.log(' notify <mode> Set notification mode (digest/silent/push)');
1142
+ console.log(' deploy <slug> Push local business to cloud');
1143
+ console.log(' remove <slug> Unregister locally');
371
1144
  }
372
1145
  }
373
1146
 
374
- module.exports = { businessCommand, businessHealth, businessAudit, loadBusinesses, saveBusinesses, getBusinessConfigPath };
1147
+ module.exports = { businessCommand, businessHealth, businessAudit, businessTeam, loadBusinesses, saveBusinesses, getBusinessConfigPath };