atris 3.15.30 → 3.15.36

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.
@@ -3,8 +3,10 @@
3
3
  *
4
4
  * atris computer — Open SMART mode (cloud in business workspace, local elsewhere)
5
5
  * atris computer --cloud — Open CLOUD workspace mode
6
+ * atris computer create <name> — Create and wake a business computer
6
7
  * atris computer wake — Start the computer
7
8
  * atris computer sleep — Stop (files persist)
9
+ * atris computer delete — Sleep, confirm, and delete a business computer
8
10
  * atris computer card — Show the local computer card
9
11
  * atris computer run <command> — Run bash on EC2 (no LLM)
10
12
  * atris computer grep <pattern> — Search files on EC2
@@ -19,10 +21,11 @@ const path = require('path');
19
21
  const readline = require('readline');
20
22
  const { spawnSync } = require('child_process');
21
23
  const { loadCredentials, decodeJwtClaims } = require('../utils/auth');
22
- const { apiRequestJson, getApiBaseUrl } = require('../utils/api');
24
+ const { apiRequestJson, getApiBaseUrl, getAppBaseUrl } = require('../utils/api');
23
25
  const { loadBusinesses, saveBusinesses } = require('./business');
24
26
  const { consoleCommand, gatherAtrisContext, buildSystemPrompt } = require('./console');
25
27
  const { streamSession } = require('./serve');
28
+ const { buildRemoteAtrisBootstrapCommand } = require('../lib/runtime-bootstrap');
26
29
 
27
30
  function getToken() {
28
31
  const creds = loadCredentials();
@@ -149,6 +152,38 @@ function formatBillingMode(worker) {
149
152
  : 'Claude subscription lane';
150
153
  }
151
154
 
155
+ function extractAttachedWorkspaceMismatch(...values) {
156
+ const text = values
157
+ .filter((value) => value !== null && value !== undefined)
158
+ .map((value) => {
159
+ if (typeof value === 'string') return value;
160
+ try {
161
+ return JSON.stringify(value);
162
+ } catch {
163
+ return String(value);
164
+ }
165
+ })
166
+ .join('\n');
167
+ const match = text.match(/attached to workspace\s+([a-z0-9-]+)\.\s*Activate workspace\s+([a-z0-9-]+)\s+to switch/i);
168
+ if (!match) return null;
169
+ return {
170
+ attachedWorkspaceId: match[1],
171
+ requestedWorkspaceId: match[2],
172
+ };
173
+ }
174
+
175
+ function contextForAttachedWorkspaceMismatch(ctx, failure) {
176
+ const mismatch = extractAttachedWorkspaceMismatch(
177
+ failure?.result?.error,
178
+ failure?.result?.errorMessage,
179
+ failure?.result?.data,
180
+ failure?.fallback?.error,
181
+ failure?.fallback?.payload
182
+ );
183
+ if (!mismatch?.attachedWorkspaceId || mismatch.attachedWorkspaceId === ctx?.workspaceId) return null;
184
+ return { ...ctx, workspaceId: mismatch.attachedWorkspaceId };
185
+ }
186
+
152
187
  async function describeClaudeAuth(token, ctx) {
153
188
  try {
154
189
  const status = await fetchBusinessClaudeLoginStatus(token, ctx);
@@ -505,9 +540,33 @@ function parseComputerOptions(argv) {
505
540
  const positional = [];
506
541
  let worker = process.env.ATRIS_CLOUD_WORKER || null;
507
542
  let model = process.env.ATRIS_CLOUD_MODEL || null;
543
+ let businessSlug = null;
544
+ let workspaceId = null;
508
545
 
509
546
  for (let i = 0; i < argv.length; i++) {
510
547
  const arg = argv[i];
548
+ if ((arg === '--business' || arg === '-b') && argv[i + 1]) {
549
+ businessSlug = argv[i + 1];
550
+ i++;
551
+ continue;
552
+ }
553
+ if (arg.startsWith('--business=')) {
554
+ businessSlug = arg.split('=', 2)[1] || null;
555
+ continue;
556
+ }
557
+ if ((arg === '--workspace' || arg === '--workspace-id') && argv[i + 1]) {
558
+ workspaceId = argv[i + 1];
559
+ i++;
560
+ continue;
561
+ }
562
+ if (arg.startsWith('--workspace=')) {
563
+ workspaceId = arg.split('=', 2)[1] || null;
564
+ continue;
565
+ }
566
+ if (arg.startsWith('--workspace-id=')) {
567
+ workspaceId = arg.split('=', 2)[1] || null;
568
+ continue;
569
+ }
511
570
  if (arg === '--worker' && argv[i + 1]) {
512
571
  worker = argv[i + 1];
513
572
  i++;
@@ -540,10 +599,65 @@ function parseComputerOptions(argv) {
540
599
  options: {
541
600
  worker: worker || null,
542
601
  model: model || null,
602
+ businessSlug: businessSlug ? String(businessSlug).trim() : null,
603
+ workspaceId: workspaceId ? String(workspaceId).trim() : null,
543
604
  },
544
605
  };
545
606
  }
546
607
 
608
+ function parseComputerCreateArgs(argv = []) {
609
+ const nameParts = [];
610
+ let businessSlug = null;
611
+ let help = false;
612
+
613
+ for (let i = 0; i < argv.length; i++) {
614
+ const arg = argv[i];
615
+ if (arg === '--help' || arg === '-h' || arg === 'help') {
616
+ help = true;
617
+ continue;
618
+ }
619
+ if ((arg === '--business' || arg === '-b') && argv[i + 1]) {
620
+ businessSlug = argv[i + 1];
621
+ i++;
622
+ continue;
623
+ }
624
+ if (arg.startsWith('--business=')) {
625
+ businessSlug = arg.split('=', 2)[1] || null;
626
+ continue;
627
+ }
628
+ nameParts.push(arg);
629
+ }
630
+
631
+ return {
632
+ name: nameParts.join(' ').trim(),
633
+ businessSlug: businessSlug ? String(businessSlug).trim() : null,
634
+ help,
635
+ };
636
+ }
637
+
638
+ function parseComputerDeleteArgs(argv = []) {
639
+ const options = { help: false, confirm: null };
640
+
641
+ for (let i = 0; i < argv.length; i++) {
642
+ const arg = argv[i];
643
+ if (arg === '--help' || arg === '-h' || arg === 'help') {
644
+ options.help = true;
645
+ continue;
646
+ }
647
+ if (arg === '--confirm' && argv[i + 1]) {
648
+ options.confirm = argv[i + 1];
649
+ i++;
650
+ continue;
651
+ }
652
+ if (arg.startsWith('--confirm=')) {
653
+ options.confirm = arg.slice('--confirm='.length);
654
+ continue;
655
+ }
656
+ }
657
+
658
+ return options;
659
+ }
660
+
547
661
  function formatCloudSelection(options = {}) {
548
662
  const worker = activeWorker(options.worker);
549
663
  const parts = [`worker=${worker}`];
@@ -923,8 +1037,32 @@ async function resolveBusinessContext(token) {
923
1037
  return null;
924
1038
  }
925
1039
 
926
- async function resolveBusinessContextBySlug(token, slug) {
1040
+ function cachedBusinessContext(slug) {
927
1041
  if (!slug) return null;
1042
+ const wanted = String(slug).toLowerCase();
1043
+ const businesses = loadBusinesses();
1044
+ const cached = businesses[slug] || Object.values(businesses).find((entry) => {
1045
+ if (!entry) return false;
1046
+ return String(entry.slug || '').toLowerCase() === wanted
1047
+ || String(entry.canonical_slug || '').toLowerCase() === wanted
1048
+ || String(entry.name || '').toLowerCase() === wanted;
1049
+ });
1050
+ if (!cached?.business_id) return null;
1051
+ return {
1052
+ slug: cached.slug || slug,
1053
+ businessId: cached.business_id,
1054
+ workspaceId: cached.workspace_id || null,
1055
+ businessName: cached.name || cached.slug || slug,
1056
+ };
1057
+ }
1058
+
1059
+ async function resolveBusinessContextBySlug(token, slug, options = {}) {
1060
+ if (!slug) return null;
1061
+
1062
+ if (options.preferCache) {
1063
+ const cached = cachedBusinessContext(slug);
1064
+ if (cached?.workspaceId) return cached;
1065
+ }
928
1066
 
929
1067
  const businesses = loadBusinesses();
930
1068
  const list = await apiRequestJson('/business/', { method: 'GET', token });
@@ -953,6 +1091,70 @@ async function resolveBusinessContextBySlug(token, slug) {
953
1091
  return null;
954
1092
  }
955
1093
 
1094
+ async function resolveComputerCommandContext(token, options = {}) {
1095
+ if (options.businessSlug || options.workspaceId) {
1096
+ const ctx = options.businessSlug
1097
+ ? await resolveBusinessContextBySlug(token, options.businessSlug, { preferCache: true })
1098
+ : await resolveBusinessContext(token);
1099
+ if (!ctx?.businessId) return null;
1100
+ return {
1101
+ ...ctx,
1102
+ workspaceId: options.workspaceId || ctx.workspaceId,
1103
+ };
1104
+ }
1105
+
1106
+ return resolveBusinessContext(token);
1107
+ }
1108
+
1109
+ async function resolveBusinessOwnerForCreate(token, businessSlug = null) {
1110
+ const wantedSlug = businessSlug ? String(businessSlug).trim() : null;
1111
+ if (wantedSlug) {
1112
+ const fromApi = await resolveBusinessContextBySlug(token, wantedSlug);
1113
+ if (fromApi) return fromApi;
1114
+
1115
+ const cached = loadBusinesses()[wantedSlug];
1116
+ if (cached?.business_id) {
1117
+ return {
1118
+ slug: cached.slug || wantedSlug,
1119
+ businessId: cached.business_id,
1120
+ workspaceId: cached.workspace_id || null,
1121
+ businessName: cached.name || cached.slug || wantedSlug,
1122
+ };
1123
+ }
1124
+ return null;
1125
+ }
1126
+
1127
+ const binding = readBusinessBinding();
1128
+ if (binding?.business_id) {
1129
+ return {
1130
+ slug: binding.slug || binding.canonical_slug || null,
1131
+ businessId: binding.business_id,
1132
+ workspaceId: binding.workspace_id || null,
1133
+ businessName: binding.name || binding.slug || 'business',
1134
+ };
1135
+ }
1136
+
1137
+ return resolveBusinessContext(token);
1138
+ }
1139
+
1140
+ function rememberCreatedComputer(ctx, workspace, endpoint = null) {
1141
+ const slug = ctx.slug || (ctx.businessName || '').toLowerCase().replace(/\s+/g, '-');
1142
+ if (!slug) return;
1143
+ const businesses = loadBusinesses();
1144
+ businesses[slug] = {
1145
+ ...(businesses[slug] || {}),
1146
+ business_id: ctx.businessId,
1147
+ workspace_id: workspace.id,
1148
+ name: ctx.businessName,
1149
+ slug,
1150
+ computer_name: workspace.name,
1151
+ endpoint: endpoint || undefined,
1152
+ added_at: businesses[slug]?.added_at || new Date().toISOString(),
1153
+ updated_at: new Date().toISOString(),
1154
+ };
1155
+ saveBusinesses(businesses);
1156
+ }
1157
+
956
1158
  function shellQuote(value) {
957
1159
  return `'${String(value).replace(/'/g, `'\\''`)}'`;
958
1160
  }
@@ -974,6 +1176,42 @@ async function runBusinessTerminalCommand(token, ctx, command, timeout = 30) {
974
1176
  );
975
1177
  }
976
1178
 
1179
+ async function bootstrapBusinessComputerRuntime(token, ctx, boundary = 'computer-wake') {
1180
+ if (!ctx?.businessId || !ctx?.workspaceId) {
1181
+ return { ok: false, skipped: true, reason: 'missing_workspace' };
1182
+ }
1183
+ if (process.env.ATRIS_SKIP_RUNTIME_BOOTSTRAP === '1') {
1184
+ return { ok: true, skipped: true, reason: 'env' };
1185
+ }
1186
+
1187
+ const command = buildRemoteAtrisBootstrapCommand({
1188
+ boundary,
1189
+ businessSlug: ctx.slug || '',
1190
+ businessId: ctx.businessId,
1191
+ workspaceId: ctx.workspaceId,
1192
+ });
1193
+ const result = await runBusinessTerminalCommand(token, ctx, command, 120);
1194
+ if (!result.ok) {
1195
+ console.log(' Runtime: bootstrap could not run.');
1196
+ console.log(` Recovery: atris computer run "npm install -g atris@latest" --business ${ctx.slug || ctx.businessId} --workspace ${ctx.workspaceId}`);
1197
+ return { ok: false, result };
1198
+ }
1199
+
1200
+ const data = result.data || {};
1201
+ const output = String(data.stdout || data.output || data.result || '').trim();
1202
+ const line = output.split('\n').find((entry) => entry.includes('atris_runtime_bootstrap'));
1203
+ const recovery = output.split('\n').find((entry) => entry.startsWith('recovery='));
1204
+ if (line) {
1205
+ console.log(` Runtime: ${line.replace(/^atris_runtime_bootstrap\s*/, '')}`);
1206
+ } else {
1207
+ console.log(' Runtime: Atris bootstrap receipt written.');
1208
+ }
1209
+ if (recovery) {
1210
+ console.log(` Recovery: atris computer run "${recovery.slice('recovery='.length)}" --business ${ctx.slug || ctx.businessId} --workspace ${ctx.workspaceId}`);
1211
+ }
1212
+ return { ok: true, output };
1213
+ }
1214
+
977
1215
  async function readBusinessWorkspaceFile(token, ctx, remotePath, timeoutMs = 15000) {
978
1216
  return apiRequestJson(
979
1217
  `/business/${ctx.businessId}/workspaces/${ctx.workspaceId}/file?path=${encodeURIComponent(remotePath)}`,
@@ -1137,6 +1375,7 @@ async function ensureBusinessAwake(token, ctx, maxWaitSec = 90) {
1137
1375
  if (next.ok && next.data && next.data.status === 'running' && next.data.endpoint) {
1138
1376
  const elapsed = Math.floor((Date.now() - start) / 1000);
1139
1377
  console.log(`awake (${elapsed}s)`);
1378
+ await bootstrapBusinessComputerRuntime(token, ctx, 'computer-auto-wake');
1140
1379
  return true;
1141
1380
  }
1142
1381
  }
@@ -1207,6 +1446,8 @@ async function computerWake(token, ctx = null) {
1207
1446
  }
1208
1447
  console.log(` Status: ${result.data.status}`);
1209
1448
  if (result.data.endpoint) console.log(` Endpoint: ${result.data.endpoint}`);
1449
+ await bootstrapBusinessComputerRuntime(token, ctx, 'computer-wake');
1450
+ console.log(' Computer is awake.');
1210
1451
  return;
1211
1452
  }
1212
1453
 
@@ -1222,6 +1463,106 @@ async function computerWake(token, ctx = null) {
1222
1463
  }
1223
1464
  console.log(` Status: ${result.data.status}`);
1224
1465
  console.log(` Endpoint: ${result.data.endpoint}`);
1466
+ console.log(' Computer is awake.');
1467
+ }
1468
+
1469
+ async function computerCreate(token, args = [], defaults = {}) {
1470
+ const options = parseComputerCreateArgs(args);
1471
+ if (!options.businessSlug && defaults.businessSlug) {
1472
+ options.businessSlug = defaults.businessSlug;
1473
+ }
1474
+ if (options.help || !options.name) {
1475
+ console.log('Usage: atris computer create <name> --business <slug>');
1476
+ console.log('');
1477
+ console.log('Create a business computer, activate it, and wake it in one command.');
1478
+ console.log('');
1479
+ console.log('Examples:');
1480
+ console.log(' atris computer create "My Business Computer" --business atris-labs');
1481
+ console.log(' atris computer create "Recruiting Computer"');
1482
+ if (!options.name && !options.help) process.exitCode = 1;
1483
+ return;
1484
+ }
1485
+
1486
+ const ctx = await resolveBusinessOwnerForCreate(token, options.businessSlug);
1487
+ if (!ctx?.businessId) {
1488
+ console.error('No business found.');
1489
+ console.error('Run inside a bound business workspace or pass: --business <slug>');
1490
+ process.exitCode = 1;
1491
+ return;
1492
+ }
1493
+
1494
+ console.log(`Creating computer "${options.name}" for ${ctx.businessName}...`);
1495
+ const created = await apiRequestJson(`/business/${ctx.businessId}/workspaces`, {
1496
+ method: 'POST',
1497
+ token,
1498
+ body: { name: options.name, type: 'general' },
1499
+ });
1500
+ if (!created.ok) {
1501
+ console.error(`Failed to create workspace: ${created.errorMessage || created.error || created.status}`);
1502
+ process.exitCode = 1;
1503
+ return;
1504
+ }
1505
+
1506
+ const workspace = created.data || {};
1507
+ const workspaceId = workspace.id || workspace.workspace_id;
1508
+ if (!workspaceId) {
1509
+ console.error('Failed to create workspace: response did not include workspace id');
1510
+ process.exitCode = 1;
1511
+ return;
1512
+ }
1513
+
1514
+ const activate = await apiRequestJson(`/business/${ctx.businessId}/workspaces/${workspaceId}/activate`, {
1515
+ method: 'POST',
1516
+ token,
1517
+ body: {},
1518
+ });
1519
+ if (!activate.ok && activate.status !== 409) {
1520
+ console.error(`Failed to activate computer: ${activate.errorMessage || activate.error || activate.status}`);
1521
+ process.exitCode = 1;
1522
+ return;
1523
+ }
1524
+
1525
+ const wake = await apiRequestJson(`/business/${ctx.businessId}/ai-computer/wake`, {
1526
+ method: 'POST',
1527
+ token,
1528
+ body: {},
1529
+ });
1530
+ if (!wake.ok && !activate.ok) {
1531
+ console.error(`Failed to wake computer: ${wake.errorMessage || wake.error || wake.status}`);
1532
+ process.exitCode = 1;
1533
+ return;
1534
+ }
1535
+
1536
+ const endpoint = activate.data?.endpoint || wake.data?.endpoint || null;
1537
+ const status = endpoint
1538
+ ? 'running'
1539
+ : (wake.data?.status || (activate.ok ? 'activated' : 'warming_up'));
1540
+ rememberCreatedComputer(ctx, { ...workspace, id: workspaceId, name: workspace.name || options.name }, endpoint);
1541
+ await bootstrapBusinessComputerRuntime(token, { ...ctx, workspaceId }, 'computer-create');
1542
+
1543
+ const appBase = getAppBaseUrl();
1544
+ console.log('');
1545
+ console.log(`Computer created: ${workspaceId}`);
1546
+ console.log(` Name: ${workspace.name || options.name}`);
1547
+ console.log(` Business: ${ctx.businessName}`);
1548
+ console.log(` Status: ${status}`);
1549
+ if (endpoint) console.log(` Endpoint: ${endpoint}`);
1550
+ console.log(` Dashboard: ${appBase}/dashboard/gm/${ctx.businessId}`);
1551
+ console.log('');
1552
+ const owner = ctx.slug || ctx.businessId;
1553
+ console.log('Start here:');
1554
+ console.log(` atris computer --business ${owner} --workspace ${workspaceId}`);
1555
+ console.log('');
1556
+ console.log('Org workspace:');
1557
+ console.log(` cd ~/arena/atris-business/${owner}`);
1558
+ console.log(' atris member activate operator');
1559
+ console.log(' atris member activate validator');
1560
+ console.log('');
1561
+ console.log('If the org workspace does not exist yet:');
1562
+ console.log(` atris business init "${ctx.businessName}"`);
1563
+ console.log('');
1564
+ console.log('Cost control:');
1565
+ console.log(` atris computer sleep --business ${owner} --workspace ${workspaceId}`);
1225
1566
  }
1226
1567
 
1227
1568
  async function computerSleep(token, ctx = null) {
@@ -1237,6 +1578,7 @@ async function computerSleep(token, ctx = null) {
1237
1578
  return;
1238
1579
  }
1239
1580
  console.log(' Computer is sleeping. Files persist.');
1581
+ console.log(' No compute cost while sleeping.');
1240
1582
  return;
1241
1583
  }
1242
1584
 
@@ -1251,6 +1593,113 @@ async function computerSleep(token, ctx = null) {
1251
1593
  return;
1252
1594
  }
1253
1595
  console.log(' Computer is sleeping. Files persist.');
1596
+ console.log(' No compute cost while sleeping.');
1597
+ }
1598
+
1599
+ function rememberDeletedComputer(ctx) {
1600
+ const businesses = loadBusinesses();
1601
+ let changed = false;
1602
+ for (const [slug, entry] of Object.entries(businesses)) {
1603
+ if (!entry) continue;
1604
+ const sameBusiness = entry.business_id === ctx.businessId || slug === ctx.slug;
1605
+ const sameWorkspace = entry.workspace_id === ctx.workspaceId;
1606
+ if (sameBusiness && sameWorkspace) {
1607
+ delete entry.workspace_id;
1608
+ delete entry.computer_name;
1609
+ delete entry.endpoint;
1610
+ entry.deleted_workspace_id = ctx.workspaceId;
1611
+ entry.updated_at = new Date().toISOString();
1612
+ changed = true;
1613
+ }
1614
+ }
1615
+ if (changed) saveBusinesses(businesses);
1616
+ }
1617
+
1618
+ async function confirmComputerDelete(ctx, options) {
1619
+ const expected = `delete ${ctx.workspaceId}`;
1620
+ if (String(options.confirm || '').trim() === expected) return true;
1621
+
1622
+ console.log('');
1623
+ console.log('This will sleep the computer first, then delete the workspace record.');
1624
+ console.log(`Business: ${ctx.businessName}`);
1625
+ console.log(`Workspace: ${ctx.workspaceId}`);
1626
+ console.log(`Type "${expected}" to continue.`);
1627
+
1628
+ if (!useInteractiveTerminalUi()) {
1629
+ console.error(`Confirmation required. Re-run with: --confirm "${expected}"`);
1630
+ return false;
1631
+ }
1632
+
1633
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1634
+ const answer = String(await questionAsync(rl, 'Confirm: ') || '').trim();
1635
+ rl.close();
1636
+ if (answer === expected) return true;
1637
+
1638
+ console.error('Delete cancelled.');
1639
+ return false;
1640
+ }
1641
+
1642
+ async function computerDelete(token, ctx, options = {}, args = []) {
1643
+ const deleteOptions = parseComputerDeleteArgs(args);
1644
+ if (deleteOptions.help) {
1645
+ console.log('Usage: atris computer delete --business <slug> --workspace <workspace-id>');
1646
+ console.log('');
1647
+ console.log('Sleeps the computer first, then deletes the non-default workspace after confirmation.');
1648
+ console.log('');
1649
+ console.log('Examples:');
1650
+ console.log(' atris computer delete --business atris-labs --workspace ws_123');
1651
+ console.log(' atris computer delete --business atris-labs --workspace ws_123 --confirm "delete ws_123"');
1652
+ return;
1653
+ }
1654
+
1655
+ if (!ctx?.businessId) {
1656
+ console.error('No business found.');
1657
+ console.error('Pass: --business <slug> --workspace <workspace-id>');
1658
+ process.exitCode = 1;
1659
+ return;
1660
+ }
1661
+
1662
+ if (!options.workspaceId || !ctx.workspaceId) {
1663
+ console.error('Refusing to delete without an explicit workspace id.');
1664
+ console.error('Pass: --workspace <workspace-id>');
1665
+ process.exitCode = 1;
1666
+ return;
1667
+ }
1668
+
1669
+ const confirmed = await confirmComputerDelete(ctx, deleteOptions);
1670
+ if (!confirmed) {
1671
+ process.exitCode = 1;
1672
+ return;
1673
+ }
1674
+
1675
+ console.log(`Sleeping computer for ${ctx.businessName}...`);
1676
+ const slept = await apiRequestJson(`/business/${ctx.businessId}/ai-computer/sleep`, {
1677
+ method: 'POST',
1678
+ token,
1679
+ body: {},
1680
+ });
1681
+ if (!slept.ok) {
1682
+ console.error(`Failed to sleep computer: ${slept.errorMessage || slept.error || slept.status}`);
1683
+ process.exitCode = 1;
1684
+ return;
1685
+ }
1686
+
1687
+ console.log(' Computer is sleeping. Files persist.');
1688
+ console.log(`Deleting workspace ${ctx.workspaceId}...`);
1689
+ const deleted = await apiRequestJson(`/business/${ctx.businessId}/workspaces/${ctx.workspaceId}`, {
1690
+ method: 'DELETE',
1691
+ token,
1692
+ });
1693
+ if (!deleted.ok) {
1694
+ console.error(`Failed to delete computer: ${deleted.errorMessage || deleted.error || deleted.status}`);
1695
+ if (deleted.status === 400) console.error('Default workspaces cannot be deleted.');
1696
+ process.exitCode = 1;
1697
+ return;
1698
+ }
1699
+
1700
+ rememberDeletedComputer(ctx);
1701
+ console.log(' Computer deleted.');
1702
+ console.log(' Cost gate: sleeping before delete completed.');
1254
1703
  }
1255
1704
 
1256
1705
  async function computerRun(token, command, ctx = null) {
@@ -1941,6 +2390,9 @@ async function sendBusinessChat(token, ctx, message, sessionId, resetContext = f
1941
2390
  maxTurns: 25,
1942
2391
  });
1943
2392
  if (!fallback.ok) {
2393
+ if (typeof options.onFailure === 'function') {
2394
+ options.onFailure({ result, fallback });
2395
+ }
1944
2396
  console.error(`Failed: ${result.error || result.status}`);
1945
2397
  if (fallback.error) {
1946
2398
  console.error(`Fallback failed: ${fallback.error}`);
@@ -2452,12 +2904,33 @@ async function computerProof(token, ctx, initialOptions = {}) {
2452
2904
 
2453
2905
  console.log(ui.bold('Run'));
2454
2906
  console.log(` prompt: ${prompt}`);
2455
- const nextSessionId = await sendBusinessChat(token, ctx, prompt, sessionId, true, null, {
2907
+ let activeCtx = ctx;
2908
+ let chatFailure = null;
2909
+ let nextSessionId = await sendBusinessChat(token, activeCtx, prompt, sessionId, true, null, {
2456
2910
  worker,
2457
2911
  model,
2458
2912
  systemPrompt,
2459
2913
  localCliSessionId: bridge.sessionId,
2914
+ onFailure: (failure) => {
2915
+ chatFailure = failure;
2916
+ },
2460
2917
  });
2918
+ const retryCtx = contextForAttachedWorkspaceMismatch(activeCtx, chatFailure);
2919
+ if (retryCtx) {
2920
+ console.log('');
2921
+ console.log(`Retrying proof against attached workspace ${retryCtx.workspaceId}...`);
2922
+ activeCtx = retryCtx;
2923
+ chatFailure = null;
2924
+ nextSessionId = await sendBusinessChat(token, activeCtx, prompt, `${sessionId}-attached`, true, null, {
2925
+ worker,
2926
+ model,
2927
+ systemPrompt,
2928
+ localCliSessionId: bridge.sessionId,
2929
+ onFailure: (failure) => {
2930
+ chatFailure = failure;
2931
+ },
2932
+ });
2933
+ }
2461
2934
 
2462
2935
  const localPath = path.join(bridge.workingDir, fileName);
2463
2936
  let localContent = '';
@@ -2468,10 +2941,10 @@ async function computerProof(token, ctx, initialOptions = {}) {
2468
2941
  }
2469
2942
  const localOk = localContent === expected;
2470
2943
 
2471
- const cloudFile = await readBusinessWorkspaceFile(token, ctx, fileName, 15000);
2944
+ const cloudFile = await readBusinessWorkspaceFile(token, activeCtx, fileName, 15000);
2472
2945
  const cloudClear = !cloudFile.ok && cloudFile.status === 404;
2473
2946
 
2474
- const audit = await fetchBusinessChatAudit(token, ctx, 5);
2947
+ const audit = await fetchBusinessChatAudit(token, activeCtx, 5);
2475
2948
  const rows = audit.ok ? (audit.data?.rows || []) : [];
2476
2949
  const auditRow = rows.find((row) => row.session_id === nextSessionId || row.preview?.includes(fileName)) || rows[0] || {};
2477
2950
  const auditOk = audit.ok && auditRow.status === 'completed' && String(auditRow.result_preview || '').includes('ATRIS COMPUTER PROOF OK');
@@ -2505,6 +2978,13 @@ async function runComputer() {
2505
2978
  const sub = args[0];
2506
2979
 
2507
2980
  if (!sub) {
2981
+ if (cloudOptions.businessSlug || cloudOptions.workspaceId) {
2982
+ const token = getToken();
2983
+ const ctx = await resolveComputerCommandContext(token, cloudOptions);
2984
+ await computerChat(token, ctx, cloudOptions);
2985
+ return;
2986
+ }
2987
+
2508
2988
  const hasBusinessBinding = Boolean(readBusinessBinding());
2509
2989
  const hasLocalHarness = Boolean(findAtrisCodeTerminal());
2510
2990
  const surface = await chooseComputerSurface(hasBusinessBinding, hasLocalHarness);
@@ -2531,7 +3011,7 @@ async function runComputer() {
2531
3011
 
2532
3012
  if (sub === '--local' || sub === 'local') {
2533
3013
  const token = getToken();
2534
- const ctx = await resolveBusinessContext(token);
3014
+ const ctx = await resolveComputerCommandContext(token, cloudOptions);
2535
3015
  if (ctx) {
2536
3016
  await computerLocalAtris(token, ctx, cloudOptions);
2537
3017
  return;
@@ -2542,7 +3022,7 @@ async function runComputer() {
2542
3022
 
2543
3023
  if (sub === 'local-atris') {
2544
3024
  const token = getToken();
2545
- const ctx = await resolveBusinessContext(token);
3025
+ const ctx = await resolveComputerCommandContext(token, cloudOptions);
2546
3026
  await computerLocalAtris(token, ctx, cloudOptions);
2547
3027
  return;
2548
3028
  }
@@ -2562,6 +3042,14 @@ async function runComputer() {
2562
3042
  return;
2563
3043
  }
2564
3044
 
3045
+ if (sub === 'create') {
3046
+ const createOptions = parseComputerCreateArgs(args.slice(1));
3047
+ if (createOptions.help || !createOptions.name) {
3048
+ await computerCreate(null, args.slice(1));
3049
+ return;
3050
+ }
3051
+ }
3052
+
2565
3053
  if (sub === '--help') {
2566
3054
  console.log('Usage: atris computer [mode|command]');
2567
3055
  console.log('');
@@ -2588,19 +3076,23 @@ async function runComputer() {
2588
3076
  console.log(' --cloud Open CLOUD workspace mode in the bound business workspace');
2589
3077
  console.log(' cloud Open CLOUD workspace mode in the bound business workspace');
2590
3078
  console.log(' codeops Open Atris CodeOps workflow computer if your account has access');
3079
+ console.log(' --business Select a business by slug');
3080
+ console.log(' --workspace Select a specific workspace/computer id');
2591
3081
  console.log(' --worker Cloud worker override: claude | openai');
2592
3082
  console.log(' --model Cloud model override');
2593
3083
  console.log(' claude|codex Legacy local console backends');
2594
3084
  console.log('');
2595
3085
  console.log('Cloud commands:');
3086
+ console.log(' create <name> Create and wake an extra business computer');
2596
3087
  console.log(' chat Interactive cloud workspace chat');
2597
3088
  console.log(' Ctrl-C during a cloud run interrupts it');
2598
3089
  console.log(' /start shows the beginner flow');
2599
3090
  console.log(' /status shows lane, Claude auth, and billing');
2600
3091
  console.log(' /audit [n] shows recent cloud runs inside chat');
2601
3092
  console.log(' status Show computer status');
2602
- console.log(' wake Start the computer');
3093
+ console.log(' up|wake Start the computer');
2603
3094
  console.log(' sleep Stop the computer (files persist)');
3095
+ console.log(' delete Sleep, confirm, and delete a business computer');
2604
3096
  console.log(' run <cmd> Run bash on EC2 (no LLM cost)');
2605
3097
  console.log(' grep <pattern> Search files on EC2');
2606
3098
  console.log(' ls [path] List files');
@@ -2614,7 +3106,11 @@ async function runComputer() {
2614
3106
  console.log('Examples:');
2615
3107
  console.log(' atris computer');
2616
3108
  console.log(' atris computer card --write');
2617
- console.log(' atris business init "My Lab" # shared owner + first/default computer');
3109
+ console.log(' atris business init "My Lab" # first/default computer with Atris + operator');
3110
+ console.log(' atris computer create "Recruiting Computer" --business atris-labs');
3111
+ console.log(' atris computer --business atris-labs --workspace <workspace-id>');
3112
+ console.log(' atris computer sleep --business atris-labs --workspace <workspace-id>');
3113
+ console.log(' atris computer delete --business atris-labs --workspace <workspace-id>');
2618
3114
  console.log(' atris computer proof');
2619
3115
  console.log(' atris computer local');
2620
3116
  console.log(' atris computer codex');
@@ -2635,7 +3131,11 @@ async function runComputer() {
2635
3131
  }
2636
3132
 
2637
3133
  const token = getToken();
2638
- const ctx = await resolveBusinessContext(token);
3134
+ if (sub === 'create') {
3135
+ return computerCreate(token, args.slice(1), cloudOptions);
3136
+ }
3137
+
3138
+ const ctx = await resolveComputerCommandContext(token, cloudOptions);
2639
3139
 
2640
3140
  if (sub === 'codeops') {
2641
3141
  const codeopsCtx = await resolveBusinessContextBySlug(token, 'atris-codeops');
@@ -2697,8 +3197,11 @@ async function runComputer() {
2697
3197
  case 'card': return computerCard(args.slice(1));
2698
3198
  case 'proof': return computerProof(token, ctx, cloudOptions);
2699
3199
  case 'status': return computerStatus(token, ctx);
3200
+ case 'up':
2700
3201
  case 'wake': return computerWake(token, ctx);
2701
3202
  case 'sleep': return computerSleep(token, ctx);
3203
+ case 'delete':
3204
+ case 'rm': return computerDelete(token, ctx, cloudOptions, args.slice(1));
2702
3205
  case 'run': return computerRun(token, rest, ctx);
2703
3206
  case 'grep': return computerGrep(token, rest, ctx);
2704
3207
  case 'ls': return computerLs(token, rest || undefined, ctx);
@@ -2722,4 +3225,6 @@ module.exports = {
2722
3225
  buildComputerCard,
2723
3226
  renderComputerCard,
2724
3227
  renderComputerCardMarkdown,
3228
+ extractAttachedWorkspaceMismatch,
3229
+ contextForAttachedWorkspaceMismatch,
2725
3230
  };