atris 3.15.31 → 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.
package/commands/aeo.js CHANGED
@@ -1,24 +1,98 @@
1
1
  /**
2
2
  * atris aeo — AI Engine Optimization commands
3
3
  *
4
- * atris aeo init # create entity-graph skeleton in workspace
5
- * atris aeo draft "<topic>" [opts] # generate citation-optimized article (credit-metered)
4
+ * Backend-routed (require EC2):
5
+ * atris aeo init # create entity-graph skeleton
6
+ * atris aeo draft "<topic>" [opts] # generate citation-optimized article
6
7
  *
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
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
10
13
  *
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/.
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.
14
19
  */
15
20
 
16
21
  const fs = require('fs');
22
+ const os = require('os');
17
23
  const path = require('path');
24
+ const { spawnSync } = require('child_process');
18
25
  const { loadCredentials } = require('../utils/auth');
19
26
  const { apiRequestJson } = require('../utils/api');
20
27
  const { loadBusinesses, saveBusinesses } = require('./business');
21
28
 
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
+
22
96
  function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
23
97
 
24
98
  async function ensureAwake(token, businessId, maxWaitSec = 90) {
@@ -83,13 +157,25 @@ function pickSlug(args) {
83
157
 
84
158
  function printHelp() {
85
159
  console.log('Usage:');
86
- console.log(' atris aeo init [--workspace <slug>]');
87
- console.log(' atris aeo draft "<topic>" [--workspace <slug>] [--queries q1,q2] [--slug X] [--url URL]');
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]');
88
173
  console.log('');
89
174
  console.log('Examples:');
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');
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');
93
179
  }
94
180
 
95
181
  async function aeoInit(args) {
@@ -183,12 +269,290 @@ async function aeoDraft(args) {
183
269
  if (data.hint) console.log(` hint: ${data.hint}`);
184
270
  }
185
271
 
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
+
186
544
  async function run(args = []) {
187
545
  const sub = args[0];
188
546
  if (!sub || sub === 'help' || sub === '--help' || sub === '-h') return printHelp();
189
547
  const rest = args.slice(1);
190
548
  if (sub === 'init') return aeoInit(rest);
191
549
  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);
192
556
  console.error(`Unknown aeo subcommand: ${sub}`);
193
557
  printHelp();
194
558
  process.exit(1);
@@ -5,6 +5,7 @@ const { loadCredentials } = require('../utils/auth');
5
5
  const { apiRequestJson } = require('../utils/api');
6
6
  const { syncBusinessCanonical, ensureWorkspaceStateFiles } = require('./sync');
7
7
  const { ensureContextScaffold, writeWikiStatus, appendWikiLog } = require('../lib/wiki');
8
+ const { writeRuntimeReceipt } = require('../lib/runtime-bootstrap');
8
9
 
9
10
  function getBusinessConfigPath() {
10
11
  const home = require('os').homedir();
@@ -398,6 +399,17 @@ function createCanonicalBusinessWorkspace(targetRoot, bizMeta, options = {}) {
398
399
  }, null, 2));
399
400
 
400
401
  syncBusinessCanonical(targetRoot, bizMeta, { force: false, dryRun: false, templateName: workspaceTemplate });
402
+ writeRuntimeReceipt(targetRoot, {
403
+ scope: 'local-business-computer',
404
+ boundary: 'business-workspace-scaffold',
405
+ business_id: bizMeta.business_id,
406
+ workspace_id: bizMeta.workspace_id,
407
+ business_slug: bizMeta.slug,
408
+ business_name: bizMeta.name,
409
+ workspace_template: workspaceTemplate,
410
+ install_status: 'local_cli_present',
411
+ sync_status: 'templates_seeded',
412
+ });
401
413
  return { targetRoot, businessJsonPath, workspaceTemplate };
402
414
  }
403
415
 
@@ -1827,8 +1839,15 @@ async function createBusinessInternal(name, flags = [], mode = 'auto') {
1827
1839
  console.log(` Dashboard: https://atris.ai/dashboard/gm/${biz.id}`);
1828
1840
  if (shouldCreateCanonicalWorkspace) {
1829
1841
  const workspaceRoot = resolveWorkspaceRoot(biz.slug, options);
1830
- console.log(` Next: cd ${workspaceRoot}`);
1831
- console.log(' atris align --fix');
1842
+ console.log(' Atris: seeded local computer + operator + validator');
1843
+ console.log('');
1844
+ console.log(' Start here:');
1845
+ console.log(` cd ${workspaceRoot}`);
1846
+ console.log(' atris member activate operator');
1847
+ console.log(' atris business onboard --website <url> --contact "Name" --note "what they do"');
1848
+ console.log('');
1849
+ console.log(' Sync when ready:');
1850
+ console.log(` atris align ${biz.slug} --fix`);
1832
1851
  }
1833
1852
  console.log('');
1834
1853
  }