atris 3.15.30 → 3.15.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/commands/aeo.js CHANGED
@@ -1,98 +1,24 @@
1
1
  /**
2
2
  * atris aeo — AI Engine Optimization commands
3
3
  *
4
- * Backend-routed (require EC2):
5
- * atris aeo init # create entity-graph skeleton
6
- * atris aeo draft "<topic>" [opts] # generate citation-optimized article
4
+ * atris aeo init # create entity-graph skeleton in workspace
5
+ * atris aeo draft "<topic>" [opts] # generate citation-optimized article (credit-metered)
7
6
  *
8
- * Local-read against ~/arena/atrisos-backend/atris/features/aeo/proof/:
9
- * atris aeo log [--engine X] [--limit N] # citation attempt log
10
- * atris aeo status # engine + proof + buyer summary
11
- * atris aeo packet <slug> # buyer packet for a surface
12
- * atris aeo proofs [--filter X] # list proof receipt categories
7
+ * Hits the backend endpoints registered under:
8
+ * POST /api/business/{id}/workspaces/{ws}/aeo/init
9
+ * POST /api/business/{id}/workspaces/{ws}/aeo/draft
13
10
  *
14
- * Shell out to atrisos-backend/scripts/aeo_*.py:
15
- * atris aeo discover <source> [...] # discovery audit
16
- * atris aeo audit <source> [...] # agent-usability audit
17
- *
18
- * Backend root resolution: $ATRIS_BACKEND_ROOT or ~/arena/atrisos-backend.
11
+ * Business resolution mirrors `atris terminal`: explicit --workspace slug,
12
+ * else cwd .atris/business.json. The endpoint itself takes care of running
13
+ * Claude Sonnet 4.6 with the 10 AEO rules and writing to /workspace/atris/aeo/drafts/.
19
14
  */
20
15
 
21
16
  const fs = require('fs');
22
- const os = require('os');
23
17
  const path = require('path');
24
- const { spawnSync } = require('child_process');
25
18
  const { loadCredentials } = require('../utils/auth');
26
19
  const { apiRequestJson } = require('../utils/api');
27
20
  const { loadBusinesses, saveBusinesses } = require('./business');
28
21
 
29
- function resolveBackendRoot() {
30
- const candidates = [
31
- process.env.ATRIS_BACKEND_ROOT,
32
- path.join(os.homedir(), 'arena', 'atrisos-backend'),
33
- ].filter(Boolean);
34
- for (const root of candidates) {
35
- if (fs.existsSync(path.join(root, 'atris', 'features', 'aeo'))) return root;
36
- }
37
- return null;
38
- }
39
-
40
- function requireBackendRoot() {
41
- const root = resolveBackendRoot();
42
- if (!root) {
43
- console.error('Cannot find atrisos-backend. Set $ATRIS_BACKEND_ROOT or clone to ~/arena/atrisos-backend.');
44
- process.exit(1);
45
- }
46
- return root;
47
- }
48
-
49
- function readJsonSafe(p) {
50
- if (!fs.existsSync(p)) return null;
51
- try {
52
- return JSON.parse(fs.readFileSync(p, 'utf8'));
53
- } catch (err) {
54
- console.error(` warning: malformed JSON at ${p} (${err.message})`);
55
- return null;
56
- }
57
- }
58
-
59
- function pad(s, n) { s = String(s); return s.length >= n ? s.slice(0, n) : s + ' '.repeat(n - s.length); }
60
-
61
- function assertNoExtras(sub, args, allowedFlags) {
62
- const allowed = new Set(allowedFlags);
63
- const unknownFlags = args.filter((a) => a.startsWith('--') && !allowed.has(a));
64
- const positional = args.filter((a) => !a.startsWith('--'));
65
- if (unknownFlags.length) {
66
- console.error(`Unknown flag for aeo ${sub}: ${unknownFlags.join(' ')}. Supported: ${[...allowed].join(' ') || '(none)'}`);
67
- process.exit(1);
68
- }
69
- if (positional.length) {
70
- console.error(`Unexpected argument for aeo ${sub}: ${positional.join(' ')}`);
71
- process.exit(1);
72
- }
73
- }
74
-
75
- function readArg(args, ...keys) {
76
- for (const k of keys) {
77
- const eqIdx = args.findIndex((a) => a.startsWith(`${k}=`));
78
- if (eqIdx !== -1) {
79
- const v = args[eqIdx].slice(k.length + 1);
80
- args.splice(eqIdx, 1);
81
- return v;
82
- }
83
- const i = args.findIndex((a) => a === k);
84
- if (i === -1) continue;
85
- const v = args[i + 1];
86
- if (v === undefined || v.startsWith('--')) {
87
- console.error(`Flag ${k} requires a value.`);
88
- process.exit(1);
89
- }
90
- args.splice(i, 2);
91
- return v;
92
- }
93
- return null;
94
- }
95
-
96
22
  function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
97
23
 
98
24
  async function ensureAwake(token, businessId, maxWaitSec = 90) {
@@ -157,25 +83,13 @@ function pickSlug(args) {
157
83
 
158
84
  function printHelp() {
159
85
  console.log('Usage:');
160
- console.log(' Backend / EC2:');
161
- console.log(' atris aeo init [--workspace <slug>]');
162
- console.log(' atris aeo draft "<topic>" [--workspace <slug>] [--queries q1,q2] [--slug X] [--url URL]');
163
- console.log('');
164
- console.log(' Local read (atris/features/aeo/proof/):');
165
- console.log(' atris aeo log [--engine X] [--limit N] [--json]');
166
- console.log(' atris aeo status [--json]');
167
- console.log(' atris aeo packet <slug> [--json]');
168
- console.log(' atris aeo proofs [--filter X]');
169
- console.log('');
170
- console.log(' Script wrappers (scripts/aeo_*.py):');
171
- console.log(' atris aeo discover <source> [--question Q ...] [--canonical-url URL] [--out-dir DIR]');
172
- console.log(' atris aeo audit <source> [--baseline B] [--canonical-url URL] [--out-dir DIR]');
86
+ console.log(' atris aeo init [--workspace <slug>]');
87
+ console.log(' atris aeo draft "<topic>" [--workspace <slug>] [--queries q1,q2] [--slug X] [--url URL]');
173
88
  console.log('');
174
89
  console.log('Examples:');
175
- console.log(' atris aeo log --engine perplexity --limit 5');
176
- console.log(' atris aeo packet pallet');
177
- console.log(' atris aeo status');
178
- console.log(' atris aeo discover https://atris.ai/aeo --canonical-url https://atris.ai/aeo');
90
+ console.log(' atris aeo init');
91
+ console.log(' atris aeo draft "what is example-co" --queries "what is example-co,best freight platform"');
92
+ console.log(' atris aeo draft "how does atris work" --workspace doordash --slug atris-overview');
179
93
  }
180
94
 
181
95
  async function aeoInit(args) {
@@ -269,290 +183,12 @@ async function aeoDraft(args) {
269
183
  if (data.hint) console.log(` hint: ${data.hint}`);
270
184
  }
271
185
 
272
- // ---------- LOCAL READ SUBCOMMANDS ----------
273
-
274
- function loadCitationAttempts(root) {
275
- const dir = path.join(root, 'atris', 'features', 'aeo', 'proof', 'live-citation-attempts');
276
- if (!fs.existsSync(dir)) return [];
277
- const rows = [];
278
- for (const file of fs.readdirSync(dir)) {
279
- if (!file.endsWith('.json')) continue;
280
- const data = readJsonSafe(path.join(dir, file));
281
- if (!data || !Array.isArray(data.attempts)) continue;
282
- for (const attempt of data.attempts) {
283
- if (!attempt || typeof attempt !== 'object') continue;
284
- const cited = attempt.answer_cites_target_url === true;
285
- const mentioned = attempt.answer_mentions_target_entity === true;
286
- let status;
287
- if (cited) status = 'verified';
288
- else if (mentioned) status = 'pending';
289
- else status = 'failed';
290
- const str = (v) => (typeof v === 'string' ? v : '');
291
- const arr = (v) => (Array.isArray(v) ? v : []);
292
- rows.push({
293
- file,
294
- attempted_at: str(data.attempted_at),
295
- engine: str(data.engine),
296
- prompt_id: str(attempt.prompt_id),
297
- prompt: str(attempt.exact_prompt),
298
- target_entity: str(data.target_entity),
299
- target_urls: arr(data.target_url_candidates),
300
- answer_evidence_uri: str(attempt.answer_evidence_uri),
301
- status,
302
- competitors: arr(attempt.observed_competitor_or_alternative_entities),
303
- });
304
- }
305
- }
306
- rows.sort((a, b) => (b.attempted_at || '').localeCompare(a.attempted_at || ''));
307
- return rows;
308
- }
309
-
310
- async function aeoLog(args) {
311
- const engine = readArg(args, '--engine', '-e');
312
- const limitRaw = readArg(args, '--limit', '-n');
313
- assertNoExtras('log', args, ['--json']);
314
- const wantJson = args.includes('--json');
315
- let limit = 20;
316
- if (limitRaw != null) {
317
- const trimmed = String(limitRaw).trim();
318
- const parsed = parseInt(trimmed, 10);
319
- if (!/^[+-]?\d+$/.test(trimmed) || !Number.isFinite(parsed) || parsed < 1) {
320
- console.error(`Invalid --limit value: "${limitRaw}". Expected a positive integer.`);
321
- process.exit(1);
322
- }
323
- limit = parsed;
324
- }
325
- const root = requireBackendRoot();
326
- let rows = loadCitationAttempts(root);
327
- if (engine) rows = rows.filter((r) => r.engine.toLowerCase() === engine.toLowerCase());
328
- rows = rows.slice(0, limit);
329
-
330
- if (wantJson) {
331
- console.log(JSON.stringify(rows, null, 2));
332
- return;
333
- }
334
-
335
- if (!rows.length) {
336
- console.log('No citation attempts found.');
337
- return;
338
- }
339
-
340
- const counts = rows.reduce((acc, r) => { acc[r.status] = (acc[r.status] || 0) + 1; return acc; }, {});
341
- console.log(`AEO citation log (${rows.length} attempt${rows.length === 1 ? '' : 's'})`);
342
- console.log(` ${Object.entries(counts).map(([k, v]) => `${k}=${v}`).join(' ')}`);
343
- console.log('');
344
- console.log(` ${pad('ts', 22)}${pad('engine', 12)}${pad('prompt_id', 26)}${pad('status', 10)}`);
345
- console.log(` ${'-'.repeat(70)}`);
346
- for (const r of rows) {
347
- console.log(` ${pad(r.attempted_at.slice(0, 19), 22)}${pad(r.engine, 12)}${pad(r.prompt_id, 26)}${pad(r.status, 10)}`);
348
- }
349
- }
350
-
351
- async function aeoStatus(args) {
352
- assertNoExtras('status', args, ['--json']);
353
- const wantJson = args.includes('--json');
354
- const root = requireBackendRoot();
355
- const proofRoot = path.join(root, 'atris', 'features', 'aeo', 'proof');
356
-
357
- const attempts = loadCitationAttempts(root);
358
- const enginesSeen = new Set(attempts.map((a) => a.engine).filter(Boolean));
359
- const verified = attempts.filter((a) => a.status === 'verified').length;
360
- const pending = attempts.filter((a) => a.status === 'pending').length;
361
- const failed = attempts.filter((a) => a.status === 'failed').length;
362
-
363
- const packets = [];
364
- if (fs.existsSync(proofRoot)) {
365
- for (const entry of fs.readdirSync(proofRoot)) {
366
- if (!entry.endsWith('-buyer-packet')) continue;
367
- const p = path.join(proofRoot, entry, 'packet.json');
368
- const data = readJsonSafe(p);
369
- if (!data) continue;
370
- packets.push({
371
- slug: entry.replace(/-buyer-packet$/, ''),
372
- surface: data.surface || entry,
373
- target_url: data.target_url || '',
374
- baseline: data?.agent_usability?.baseline_score ?? null,
375
- proposed: data?.agent_usability?.proposed_score ?? null,
376
- claim_status: data.claim_status || '',
377
- });
378
- }
379
- }
380
-
381
- const operator = readJsonSafe(path.join(proofRoot, 'live-citation-operator', 'live-citation-operator.json'));
382
-
383
- const proofDirs = fs.existsSync(proofRoot)
384
- ? fs.readdirSync(proofRoot).filter((e) => fs.statSync(path.join(proofRoot, e)).isDirectory()).length
385
- : 0;
386
-
387
- if (wantJson) {
388
- console.log(JSON.stringify({
389
- backend_root: root,
390
- proof_dirs: proofDirs,
391
- citation: { total: attempts.length, verified, pending, failed, engines: [...enginesSeen] },
392
- packets,
393
- operator_status: operator?.status || null,
394
- operator_blocker: operator?.current_blocker || null,
395
- }, null, 2));
396
- return;
397
- }
398
-
399
- console.log('Atris AEO status');
400
- console.log(` backend root: ${root}`);
401
- console.log(` proof receipts: ${proofDirs} categories`);
402
- console.log('');
403
- console.log('Live citations');
404
- console.log(` attempts: ${attempts.length}`);
405
- console.log(` verified: ${verified}`);
406
- console.log(` pending: ${pending}`);
407
- console.log(` failed: ${failed}`);
408
- console.log(` engines observed: ${[...enginesSeen].join(', ') || '(none)'}`);
409
- if (operator) {
410
- console.log(` operator state: ${operator.status || '?'} (blocker: ${operator.current_blocker || 'none'})`);
411
- }
412
- console.log('');
413
- console.log(`Buyer packets (${packets.length})`);
414
- for (const p of packets) {
415
- const delta = p.baseline != null && p.proposed != null ? `${p.baseline} → ${p.proposed}` : '?';
416
- console.log(` ${pad(p.slug, 16)} ${pad(p.target_url || p.surface, 36)} ${pad(delta, 12)} ${p.claim_status}`);
417
- }
418
- }
419
-
420
- async function aeoPacket(args) {
421
- const known = new Set(['--json']);
422
- const positional = [];
423
- for (const a of args) {
424
- if (a.startsWith('--')) {
425
- if (!known.has(a)) {
426
- console.error(`Unknown flag: ${a}. Supported: --json`);
427
- process.exit(1);
428
- }
429
- } else {
430
- positional.push(a);
431
- }
432
- }
433
- if (positional.length === 0) {
434
- console.error('Missing slug. Usage: atris aeo packet <slug>');
435
- process.exit(1);
436
- }
437
- if (positional.length > 1) {
438
- console.error(`Too many arguments: ${positional.join(' ')}. Expected one slug.`);
439
- process.exit(1);
440
- }
441
- const slug = positional[0];
442
- const wantJson = args.includes('--json');
443
- const root = requireBackendRoot();
444
- const file = path.join(root, 'atris', 'features', 'aeo', 'proof', `${slug}-buyer-packet`, 'packet.json');
445
- const data = readJsonSafe(file);
446
- if (!data) {
447
- console.error(`Packet not found: ${file}`);
448
- process.exit(1);
449
- }
450
-
451
- if (wantJson) {
452
- console.log(JSON.stringify(data, null, 2));
453
- return;
454
- }
455
-
456
- const u = (data && typeof data.agent_usability === 'object' && data.agent_usability) || {};
457
- const onlyObjects = (v) => (Array.isArray(v) ? v.filter((x) => x && typeof x === 'object') : []);
458
- const friction = onlyObjects(u.baseline_friction_points);
459
- const fixes = onlyObjects(u.fix_backlog);
460
- console.log(`AEO buyer packet — ${slug}`);
461
- console.log(` surface: ${data.surface || ''}`);
462
- console.log(` target url: ${data.target_url || ''}`);
463
- console.log(` claim status: ${data.claim_status || ''}`);
464
- console.log(` positioning: ${u.positioning || ''}`);
465
- console.log('');
466
- console.log('Agent usability scores');
467
- console.log(` baseline: ${u.baseline_score ?? '?'}`);
468
- console.log(` proposed: ${u.proposed_score ?? '?'}`);
469
- console.log(` delta: ${u.score_delta ?? '?'}`);
470
- console.log(` verified: ${u.movement_verified ? 'yes' : 'no'}`);
471
- console.log('');
472
- console.log(`Baseline friction (${friction.length})`);
473
- for (const f of friction) {
474
- console.log(` [${f.severity || '?'}] ${f.stage || '?'}: ${f.missing_artifact || f.id || ''}`);
475
- }
476
- console.log('');
477
- console.log(`Fix backlog (${fixes.length})`);
478
- for (const f of fixes) {
479
- console.log(` #${f.priority ?? '?'} ${f.stage || '?'}: ${f.action || ''}`);
480
- }
481
- }
482
-
483
- async function aeoProofs(args) {
484
- const filter = readArg(args, '--filter', '-f');
485
- assertNoExtras('proofs', args, []);
486
- const root = requireBackendRoot();
487
- const proofRoot = path.join(root, 'atris', 'features', 'aeo', 'proof');
488
- if (!fs.existsSync(proofRoot)) {
489
- console.error(`Proof root not found: ${proofRoot}`);
490
- process.exit(1);
491
- }
492
- const needle = filter ? filter.toLowerCase() : null;
493
- const entries = fs.readdirSync(proofRoot)
494
- .filter((e) => fs.statSync(path.join(proofRoot, e)).isDirectory())
495
- .filter((e) => !needle || e.toLowerCase().includes(needle))
496
- .sort();
497
- console.log(`AEO proof receipts at ${path.relative(process.cwd(), proofRoot)}`);
498
- console.log('');
499
- for (const e of entries) {
500
- const files = fs.readdirSync(path.join(proofRoot, e)).filter((f) => f.endsWith('.json'));
501
- console.log(` ${pad(e, 44)} ${files.length} file${files.length === 1 ? '' : 's'}`);
502
- }
503
- }
504
-
505
- // ---------- SCRIPT WRAPPERS ----------
506
-
507
- function runBackendScript(scriptName, args) {
508
- const root = requireBackendRoot();
509
- const script = path.join(root, 'scripts', scriptName);
510
- if (!fs.existsSync(script)) {
511
- console.error(`Script not found: ${script}`);
512
- process.exit(1);
513
- }
514
- const py = process.env.ATRIS_PYTHON || 'python3';
515
- const result = spawnSync(py, [script, ...args], { cwd: root, stdio: 'inherit' });
516
- if (result.error) {
517
- console.error(`Failed to spawn ${py}: ${result.error.message}`);
518
- process.exit(1);
519
- }
520
- if (result.signal) {
521
- const signum = os.constants?.signals?.[result.signal] ?? 0;
522
- console.error(`${scriptName} terminated by signal ${result.signal}`);
523
- process.exit(128 + signum);
524
- }
525
- process.exit(result.status ?? 1);
526
- }
527
-
528
- async function aeoDiscover(args) {
529
- if (!args.length || args[0] === '--help' || args[0] === '-h') {
530
- console.log('Usage: atris aeo discover <source> [--question Q]... [--canonical-url URL] [--out-dir DIR] [--json]');
531
- return;
532
- }
533
- runBackendScript('aeo_discovery_audit.py', args);
534
- }
535
-
536
- async function aeoAudit(args) {
537
- if (!args.length || args[0] === '--help' || args[0] === '-h') {
538
- console.log('Usage: atris aeo audit <source> [--baseline B] [--canonical-url URL] [--out-dir DIR] [--task T]');
539
- return;
540
- }
541
- runBackendScript('aeo_agent_usability_audit.py', args);
542
- }
543
-
544
186
  async function run(args = []) {
545
187
  const sub = args[0];
546
188
  if (!sub || sub === 'help' || sub === '--help' || sub === '-h') return printHelp();
547
189
  const rest = args.slice(1);
548
190
  if (sub === 'init') return aeoInit(rest);
549
191
  if (sub === 'draft') return aeoDraft(rest);
550
- if (sub === 'log') return aeoLog(rest);
551
- if (sub === 'status') return aeoStatus(rest);
552
- if (sub === 'packet') return aeoPacket(rest);
553
- if (sub === 'proofs') return aeoProofs(rest);
554
- if (sub === 'discover') return aeoDiscover(rest);
555
- if (sub === 'audit') return aeoAudit(rest);
556
192
  console.error(`Unknown aeo subcommand: ${sub}`);
557
193
  printHelp();
558
194
  process.exit(1);
@@ -3,6 +3,7 @@
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)
8
9
  * atris computer card — Show the local computer card
@@ -19,7 +20,7 @@ const path = require('path');
19
20
  const readline = require('readline');
20
21
  const { spawnSync } = require('child_process');
21
22
  const { loadCredentials, decodeJwtClaims } = require('../utils/auth');
22
- const { apiRequestJson, getApiBaseUrl } = require('../utils/api');
23
+ const { apiRequestJson, getApiBaseUrl, getAppBaseUrl } = require('../utils/api');
23
24
  const { loadBusinesses, saveBusinesses } = require('./business');
24
25
  const { consoleCommand, gatherAtrisContext, buildSystemPrompt } = require('./console');
25
26
  const { streamSession } = require('./serve');
@@ -544,6 +545,36 @@ function parseComputerOptions(argv) {
544
545
  };
545
546
  }
546
547
 
548
+ function parseComputerCreateArgs(argv = []) {
549
+ const nameParts = [];
550
+ let businessSlug = null;
551
+ let help = false;
552
+
553
+ for (let i = 0; i < argv.length; i++) {
554
+ const arg = argv[i];
555
+ if (arg === '--help' || arg === '-h' || arg === 'help') {
556
+ help = true;
557
+ continue;
558
+ }
559
+ if ((arg === '--business' || arg === '-b') && argv[i + 1]) {
560
+ businessSlug = argv[i + 1];
561
+ i++;
562
+ continue;
563
+ }
564
+ if (arg.startsWith('--business=')) {
565
+ businessSlug = arg.split('=', 2)[1] || null;
566
+ continue;
567
+ }
568
+ nameParts.push(arg);
569
+ }
570
+
571
+ return {
572
+ name: nameParts.join(' ').trim(),
573
+ businessSlug: businessSlug ? String(businessSlug).trim() : null,
574
+ help,
575
+ };
576
+ }
577
+
547
578
  function formatCloudSelection(options = {}) {
548
579
  const worker = activeWorker(options.worker);
549
580
  const parts = [`worker=${worker}`];
@@ -953,6 +984,55 @@ async function resolveBusinessContextBySlug(token, slug) {
953
984
  return null;
954
985
  }
955
986
 
987
+ async function resolveBusinessOwnerForCreate(token, businessSlug = null) {
988
+ const wantedSlug = businessSlug ? String(businessSlug).trim() : null;
989
+ if (wantedSlug) {
990
+ const fromApi = await resolveBusinessContextBySlug(token, wantedSlug);
991
+ if (fromApi) return fromApi;
992
+
993
+ const cached = loadBusinesses()[wantedSlug];
994
+ if (cached?.business_id) {
995
+ return {
996
+ slug: cached.slug || wantedSlug,
997
+ businessId: cached.business_id,
998
+ workspaceId: cached.workspace_id || null,
999
+ businessName: cached.name || cached.slug || wantedSlug,
1000
+ };
1001
+ }
1002
+ return null;
1003
+ }
1004
+
1005
+ const binding = readBusinessBinding();
1006
+ if (binding?.business_id) {
1007
+ return {
1008
+ slug: binding.slug || binding.canonical_slug || null,
1009
+ businessId: binding.business_id,
1010
+ workspaceId: binding.workspace_id || null,
1011
+ businessName: binding.name || binding.slug || 'business',
1012
+ };
1013
+ }
1014
+
1015
+ return resolveBusinessContext(token);
1016
+ }
1017
+
1018
+ function rememberCreatedComputer(ctx, workspace, endpoint = null) {
1019
+ const slug = ctx.slug || (ctx.businessName || '').toLowerCase().replace(/\s+/g, '-');
1020
+ if (!slug) return;
1021
+ const businesses = loadBusinesses();
1022
+ businesses[slug] = {
1023
+ ...(businesses[slug] || {}),
1024
+ business_id: ctx.businessId,
1025
+ workspace_id: workspace.id,
1026
+ name: ctx.businessName,
1027
+ slug,
1028
+ computer_name: workspace.name,
1029
+ endpoint: endpoint || undefined,
1030
+ added_at: businesses[slug]?.added_at || new Date().toISOString(),
1031
+ updated_at: new Date().toISOString(),
1032
+ };
1033
+ saveBusinesses(businesses);
1034
+ }
1035
+
956
1036
  function shellQuote(value) {
957
1037
  return `'${String(value).replace(/'/g, `'\\''`)}'`;
958
1038
  }
@@ -1224,6 +1304,90 @@ async function computerWake(token, ctx = null) {
1224
1304
  console.log(` Endpoint: ${result.data.endpoint}`);
1225
1305
  }
1226
1306
 
1307
+ async function computerCreate(token, args = []) {
1308
+ const options = parseComputerCreateArgs(args);
1309
+ if (options.help || !options.name) {
1310
+ console.log('Usage: atris computer create <name> --business <slug>');
1311
+ console.log('');
1312
+ console.log('Create a business computer, activate it, and wake it in one command.');
1313
+ console.log('');
1314
+ console.log('Examples:');
1315
+ console.log(' atris computer create "My Business Computer" --business atris-labs');
1316
+ console.log(' atris computer create "Recruiting Computer"');
1317
+ if (!options.name && !options.help) process.exitCode = 1;
1318
+ return;
1319
+ }
1320
+
1321
+ const ctx = await resolveBusinessOwnerForCreate(token, options.businessSlug);
1322
+ if (!ctx?.businessId) {
1323
+ console.error('No business found.');
1324
+ console.error('Run inside a bound business workspace or pass: --business <slug>');
1325
+ process.exitCode = 1;
1326
+ return;
1327
+ }
1328
+
1329
+ console.log(`Creating computer "${options.name}" for ${ctx.businessName}...`);
1330
+ const created = await apiRequestJson(`/business/${ctx.businessId}/workspaces`, {
1331
+ method: 'POST',
1332
+ token,
1333
+ body: { name: options.name, type: 'general' },
1334
+ });
1335
+ if (!created.ok) {
1336
+ console.error(`Failed to create workspace: ${created.errorMessage || created.error || created.status}`);
1337
+ process.exitCode = 1;
1338
+ return;
1339
+ }
1340
+
1341
+ const workspace = created.data || {};
1342
+ const workspaceId = workspace.id || workspace.workspace_id;
1343
+ if (!workspaceId) {
1344
+ console.error('Failed to create workspace: response did not include workspace id');
1345
+ process.exitCode = 1;
1346
+ return;
1347
+ }
1348
+
1349
+ const activate = await apiRequestJson(`/business/${ctx.businessId}/workspaces/${workspaceId}/activate`, {
1350
+ method: 'POST',
1351
+ token,
1352
+ body: {},
1353
+ });
1354
+ if (!activate.ok && activate.status !== 409) {
1355
+ console.error(`Failed to activate computer: ${activate.errorMessage || activate.error || activate.status}`);
1356
+ process.exitCode = 1;
1357
+ return;
1358
+ }
1359
+
1360
+ const wake = await apiRequestJson(`/business/${ctx.businessId}/ai-computer/wake`, {
1361
+ method: 'POST',
1362
+ token,
1363
+ body: {},
1364
+ });
1365
+ if (!wake.ok && !activate.ok) {
1366
+ console.error(`Failed to wake computer: ${wake.errorMessage || wake.error || wake.status}`);
1367
+ process.exitCode = 1;
1368
+ return;
1369
+ }
1370
+
1371
+ const endpoint = activate.data?.endpoint || wake.data?.endpoint || null;
1372
+ const status = endpoint
1373
+ ? 'running'
1374
+ : (wake.data?.status || (activate.ok ? 'activated' : 'warming_up'));
1375
+ rememberCreatedComputer(ctx, { ...workspace, id: workspaceId, name: workspace.name || options.name }, endpoint);
1376
+
1377
+ const appBase = getAppBaseUrl();
1378
+ console.log('');
1379
+ console.log(`Computer created: ${workspaceId}`);
1380
+ console.log(` Name: ${workspace.name || options.name}`);
1381
+ console.log(` Business: ${ctx.businessName}`);
1382
+ console.log(` Status: ${status}`);
1383
+ if (endpoint) console.log(` Endpoint: ${endpoint}`);
1384
+ console.log(` Dashboard: ${appBase}/dashboard/gm/${ctx.businessId}`);
1385
+ console.log('');
1386
+ console.log('Next:');
1387
+ console.log(` atris pull ${ctx.slug || ctx.businessId}`);
1388
+ console.log(' atris computer');
1389
+ }
1390
+
1227
1391
  async function computerSleep(token, ctx = null) {
1228
1392
  if (ctx) {
1229
1393
  console.log(`Sleeping computer for ${ctx.businessName}...`);
@@ -2562,6 +2726,14 @@ async function runComputer() {
2562
2726
  return;
2563
2727
  }
2564
2728
 
2729
+ if (sub === 'create') {
2730
+ const createOptions = parseComputerCreateArgs(args.slice(1));
2731
+ if (createOptions.help || !createOptions.name) {
2732
+ await computerCreate(null, args.slice(1));
2733
+ return;
2734
+ }
2735
+ }
2736
+
2565
2737
  if (sub === '--help') {
2566
2738
  console.log('Usage: atris computer [mode|command]');
2567
2739
  console.log('');
@@ -2593,6 +2765,7 @@ async function runComputer() {
2593
2765
  console.log(' claude|codex Legacy local console backends');
2594
2766
  console.log('');
2595
2767
  console.log('Cloud commands:');
2768
+ console.log(' create <name> Create and wake a business computer');
2596
2769
  console.log(' chat Interactive cloud workspace chat');
2597
2770
  console.log(' Ctrl-C during a cloud run interrupts it');
2598
2771
  console.log(' /start shows the beginner flow');
@@ -2614,6 +2787,7 @@ async function runComputer() {
2614
2787
  console.log('Examples:');
2615
2788
  console.log(' atris computer');
2616
2789
  console.log(' atris computer card --write');
2790
+ console.log(' atris computer create "My Business Computer" --business atris-labs');
2617
2791
  console.log(' atris business init "My Lab" # shared owner + first/default computer');
2618
2792
  console.log(' atris computer proof');
2619
2793
  console.log(' atris computer local');
@@ -2695,6 +2869,7 @@ async function runComputer() {
2695
2869
  switch (sub) {
2696
2870
  case 'chat': return computerChat(token, ctx, cloudOptions);
2697
2871
  case 'card': return computerCard(args.slice(1));
2872
+ case 'create': return computerCreate(token, args.slice(1));
2698
2873
  case 'proof': return computerProof(token, ctx, cloudOptions);
2699
2874
  case 'status': return computerStatus(token, ctx);
2700
2875
  case 'wake': return computerWake(token, ctx);