atris 3.15.55 → 3.15.57

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/bin/atris.js CHANGED
@@ -1567,7 +1567,7 @@ if (command === 'init') {
1567
1567
  searchJournal(keyword);
1568
1568
  } else if (command === 'xp') {
1569
1569
  require('../commands/xp').xpCommand(...process.argv.slice(3))
1570
- .then(() => process.exit(0))
1570
+ .then(() => { process.exitCode = 0; })
1571
1571
  .catch((err) => { console.error(`✗ Error: ${err.message || err}`); process.exit(1); });
1572
1572
  } else if (command === 'play') {
1573
1573
  require('../commands/play').playCommand(...process.argv.slice(3))
@@ -2550,34 +2550,56 @@ async function streamBusinessChatResult(token, ctx, executionId, rl = null, opti
2550
2550
 
2551
2551
  errors = 0;
2552
2552
  let done = false;
2553
- for (const event of (events.data?.events || [])) {
2554
- fromIndex++;
2555
- if (event.type === 'assistant_text' && event.content) {
2556
- sawVisibleOutput = true;
2557
- process.stdout.write(event.content);
2558
- } else if (event.type === 'result' && event.result && !sawVisibleOutput) {
2559
- sawVisibleOutput = true;
2560
- process.stdout.write(String(event.result));
2561
- } else if (!options.quiet && event.type === 'tool_use' && event.tool) {
2562
- const arg = event.input?.file_path || event.input?.path || event.input?.pattern || event.input?.command || '';
2563
- if (arg) {
2564
- console.log(`\n [${event.tool}] ${String(arg).slice(0, 120)}`);
2565
- } else {
2566
- console.log(`\n [${event.tool}]`);
2553
+ const emitEvents = (items, { showTools = true } = {}) => {
2554
+ let batchDone = false;
2555
+ for (const event of (items || [])) {
2556
+ if ((event.type === 'assistant_text' || event.type === 'text') && event.content) {
2557
+ sawVisibleOutput = true;
2558
+ process.stdout.write(event.content);
2559
+ } else if (event.type === 'result' && event.result && !sawVisibleOutput) {
2560
+ sawVisibleOutput = true;
2561
+ process.stdout.write(String(event.result));
2562
+ } else if (showTools && !options.quiet && event.type === 'tool_use' && event.tool) {
2563
+ const arg = event.input?.file_path || event.input?.path || event.input?.pattern || event.input?.command || '';
2564
+ if (arg) {
2565
+ console.log(`\n [${event.tool}] ${String(arg).slice(0, 120)}`);
2566
+ } else {
2567
+ console.log(`\n [${event.tool}]`);
2568
+ }
2569
+ } else if (event.type === 'error') {
2570
+ if (event.error) console.error(`\n${event.error}`);
2571
+ terminalStatus = 'error';
2572
+ batchDone = true;
2573
+ break;
2574
+ } else if (event.type === 'complete') {
2575
+ terminalStatus = 'completed';
2576
+ batchDone = true;
2577
+ break;
2567
2578
  }
2568
- } else if (event.type === 'error') {
2569
- if (event.error) console.error(`\n${event.error}`);
2570
- terminalStatus = 'error';
2571
- done = true;
2572
- break;
2573
- } else if (event.type === 'complete') {
2574
- terminalStatus = 'completed';
2575
- done = true;
2576
- break;
2577
2579
  }
2580
+ return batchDone;
2581
+ };
2582
+
2583
+ const batch = events.data?.events || [];
2584
+ done = emitEvents(batch);
2585
+ const nextIndex = events.data?.next_index;
2586
+ if (Number.isInteger(nextIndex) && nextIndex >= fromIndex) {
2587
+ fromIndex = nextIndex;
2588
+ } else {
2589
+ fromIndex += batch.length;
2578
2590
  }
2579
2591
 
2580
2592
  if (done || ['completed', 'error', 'failed', 'cancelled'].includes(events.data?.status)) {
2593
+ if (!sawVisibleOutput && events.data?.status === 'completed') {
2594
+ const fullEvents = await apiRequestJson(
2595
+ `/business/${ctx.businessId}/chat/events?execution_id=${executionId}&workspace_id=${ctx.workspaceId}&from_index=0`,
2596
+ { method: 'GET', token, timeoutMs: 60000 }
2597
+ );
2598
+ if (fullEvents.ok) {
2599
+ emitEvents(fullEvents.data?.events || [], { showTools: false });
2600
+ }
2601
+ }
2602
+
2581
2603
  if (!process.stdout.write('\n')) {
2582
2604
  // no-op: keep line handling stable
2583
2605
  }
@@ -10,6 +10,8 @@
10
10
  * atris calendar date YYYY-MM-DD - Show events on a date
11
11
  * atris twitter post - Post a tweet (interactive)
12
12
  * atris slack channels - List Slack channels
13
+ * atris slack messages <channel> [--limit 20] - Read recent messages
14
+ * atris slack search <query> [--limit 20] - Search Slack messages
13
15
  */
14
16
 
15
17
  const { loadCredentials, ensureValidCredentials } = require('../utils/auth');
@@ -163,6 +165,29 @@ function localDateBounds(dateText) {
163
165
  return { start, end };
164
166
  }
165
167
 
168
+ function calendarEventTimeValue(value) {
169
+ if (!value) return '';
170
+ if (typeof value === 'string') return value;
171
+ return value.dateTime || value.date || '';
172
+ }
173
+
174
+ function isDateOnly(value) {
175
+ return /^\d{4}-\d{2}-\d{2}$/.test(String(value || ''));
176
+ }
177
+
178
+ function formatCalendarTimeRange(event) {
179
+ const start = calendarEventTimeValue(event.start || event.start_time || event.startTime);
180
+ const end = calendarEventTimeValue(event.end || event.end_time || event.endTime);
181
+ if (!start || isDateOnly(start)) return 'All day';
182
+ const startDate = new Date(start);
183
+ if (Number.isNaN(startDate.getTime())) return 'Time unavailable';
184
+ const startText = startDate.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
185
+ if (!end || isDateOnly(end)) return startText;
186
+ const endDate = new Date(end);
187
+ if (Number.isNaN(endDate.getTime())) return startText;
188
+ return `${startText}-${endDate.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}`;
189
+ }
190
+
166
191
  function formatCalendarEvents(label, events) {
167
192
  if (events.length === 0) {
168
193
  console.log(`No events ${label}. 🎉`);
@@ -172,8 +197,7 @@ function formatCalendarEvents(label, events) {
172
197
  console.log('─'.repeat(50));
173
198
 
174
199
  for (const event of events) {
175
- const start = event.start?.dateTime || event.start?.date || '';
176
- const time = start ? new Date(start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) : 'All day';
200
+ const time = formatCalendarTimeRange(event);
177
201
  const title = event.summary || '(no title)';
178
202
 
179
203
  console.log(`${time} ${title}`);
@@ -331,7 +355,7 @@ async function slackChannels() {
331
355
 
332
356
  console.log('💬 Fetching Slack channels...\n');
333
357
 
334
- const result = await apiRequestJson('/integrations/slack/channels', {
358
+ const result = await apiRequestJson('/integrations/slack/me/channels', {
335
359
  method: 'GET',
336
360
  token,
337
361
  });
@@ -359,19 +383,161 @@ async function slackChannels() {
359
383
  for (const ch of channels) {
360
384
  const name = ch.name || ch.id;
361
385
  const priv = ch.is_private ? '🔒' : '#';
362
- console.log(` ${priv} ${name}`);
386
+ console.log(` ${priv} ${name} ${ch.id || ''}`.trimEnd());
363
387
  }
364
388
  }
365
389
 
390
+ async function slackDms() {
391
+ const token = await getAuthToken();
392
+
393
+ console.log('💬 Fetching Slack DMs...\n');
394
+
395
+ const result = await apiRequestJson('/integrations/slack/me/dms', {
396
+ method: 'GET',
397
+ token,
398
+ });
399
+
400
+ if (!result.ok) {
401
+ console.error(`Error: ${result.error || 'Failed to fetch DMs'}`);
402
+ process.exit(1);
403
+ }
404
+
405
+ const dms = result.data?.dms || result.data?.channels || result.data || [];
406
+ if (!dms.length) {
407
+ console.log('No DMs found.');
408
+ return;
409
+ }
410
+ for (const dm of dms) {
411
+ const name = dm.name || dm.user_name || dm.user || dm.id;
412
+ console.log(` ${name} ${dm.id || ''}`.trimEnd());
413
+ }
414
+ }
415
+
416
+ function parseLimit(args, fallback = 20) {
417
+ const idx = args.findIndex((arg) => arg === '--limit' || arg === '-n');
418
+ const raw = idx >= 0 ? args[idx + 1] : '';
419
+ const parsed = Number.parseInt(raw, 10);
420
+ return Number.isFinite(parsed) && parsed > 0 ? Math.min(parsed, 100) : fallback;
421
+ }
422
+
423
+ function argsWithoutLimit(args = []) {
424
+ const out = [];
425
+ for (let i = 0; i < args.length; i += 1) {
426
+ if (args[i] === '--limit' || args[i] === '-n') {
427
+ i += 1;
428
+ continue;
429
+ }
430
+ out.push(args[i]);
431
+ }
432
+ return out;
433
+ }
434
+
435
+ async function slackPersonalChannels(token) {
436
+ const result = await apiRequestJson('/integrations/slack/me/channels', {
437
+ method: 'GET',
438
+ token,
439
+ });
440
+ if (!result.ok) return [];
441
+ return result.data?.channels || result.data || [];
442
+ }
443
+
444
+ async function resolveSlackChannel(token, channel) {
445
+ const text = String(channel || '').trim();
446
+ if (!text) return '';
447
+ if (/^[A-Z][A-Z0-9]{5,}$/.test(text)) return text;
448
+ const wanted = text.replace(/^#/, '').toLowerCase();
449
+ const channels = await slackPersonalChannels(token);
450
+ const match = channels.find((ch) => String(ch.name || '').toLowerCase() === wanted || String(ch.id || '').toLowerCase() === wanted);
451
+ return match?.id || text;
452
+ }
453
+
454
+ function formatSlackMessages(messages) {
455
+ if (!messages.length) {
456
+ console.log('No Slack messages found.');
457
+ return;
458
+ }
459
+ console.log('─'.repeat(60));
460
+ for (const message of messages) {
461
+ const author = message.user_name || message.username || message.user || message.sender || 'unknown';
462
+ const ts = message.datetime || message.time || message.ts || message.created_at || '';
463
+ const text = String(message.text || message.content || message.message || '').replace(/\s+/g, ' ').trim();
464
+ console.log(`${ts} ${author}: ${text || '(no text)'}`);
465
+ }
466
+ console.log('─'.repeat(60));
467
+ }
468
+
469
+ async function slackMessages(channel, args = []) {
470
+ if (!channel) {
471
+ console.error('Usage: atris slack messages <channel-or-id> [--limit 20]');
472
+ process.exit(1);
473
+ }
474
+ const token = await getAuthToken();
475
+ const limit = parseLimit(args);
476
+ const channelId = await resolveSlackChannel(token, channel);
477
+
478
+ console.log(`💬 Reading Slack messages from ${channel}...\n`);
479
+
480
+ const result = await apiRequestJson(`/integrations/slack/me/messages/${encodeURIComponent(channelId)}?limit=${limit}`, {
481
+ method: 'GET',
482
+ token,
483
+ });
484
+
485
+ if (!result.ok) {
486
+ console.error(`Error: ${result.error || 'Failed to fetch Slack messages'}`);
487
+ process.exit(1);
488
+ }
489
+
490
+ const messages = result.data?.messages || result.data || [];
491
+ formatSlackMessages(messages);
492
+ }
493
+
494
+ async function slackSearch(query, args = []) {
495
+ if (!query) {
496
+ console.error('Usage: atris slack search <query> [--limit 20]');
497
+ process.exit(1);
498
+ }
499
+ const token = await getAuthToken();
500
+ const limit = parseLimit(args);
501
+ const q = encodeURIComponent(query);
502
+
503
+ console.log(`💬 Searching Slack for "${query}"...\n`);
504
+
505
+ const result = await apiRequestJson(`/integrations/slack/me/search?q=${q}&count=${limit}`, {
506
+ method: 'GET',
507
+ token,
508
+ });
509
+
510
+ if (!result.ok) {
511
+ console.error(`Error: ${result.error || 'Failed to search Slack messages'}`);
512
+ process.exit(1);
513
+ }
514
+
515
+ const messages = result.data?.messages || result.data?.results || result.data || [];
516
+ formatSlackMessages(messages);
517
+ }
518
+
366
519
  async function slackCommand(subcommand, ...args) {
367
520
  switch (subcommand) {
368
521
  case 'channels':
369
522
  case 'list':
370
523
  await slackChannels();
371
524
  break;
525
+ case 'dms':
526
+ await slackDms();
527
+ break;
528
+ case 'messages':
529
+ case 'read':
530
+ await slackMessages(args[0], args.slice(1));
531
+ break;
532
+ case 'search':
533
+ await slackSearch(argsWithoutLimit(args).join(' '), args);
534
+ break;
372
535
  default:
373
536
  console.log('Slack commands:');
374
- console.log(' atris slack channels - List Slack channels');
537
+ console.log(' atris slack channels - List Slack channels');
538
+ console.log(' atris slack dms - List Slack DMs');
539
+ console.log(' atris slack messages <channel> - Read recent messages');
540
+ console.log(' atris slack search <query> - Search Slack messages');
375
541
  }
376
542
  }
377
543
 
@@ -448,6 +614,31 @@ function printImessageDoctor(result, json = false) {
448
614
  }
449
615
  }
450
616
 
617
+ function boundedImessageLimit(value) {
618
+ const parsed = Number(value || 20);
619
+ if (!Number.isFinite(parsed)) return 20;
620
+ return Math.max(1, Math.min(100, Math.trunc(parsed)));
621
+ }
622
+
623
+ function printImessageRows(result, options = {}) {
624
+ if (options.json) {
625
+ const rows = String(result.stdout || '').trim();
626
+ let messages = [];
627
+ try {
628
+ messages = rows ? JSON.parse(rows) : [];
629
+ } catch {
630
+ messages = [];
631
+ }
632
+ console.log(JSON.stringify({
633
+ ok: result.status === 0,
634
+ count: Array.isArray(messages) ? messages.length : 0,
635
+ messages: Array.isArray(messages) ? messages : [],
636
+ }, null, 2));
637
+ return;
638
+ }
639
+ console.log(String(result.stdout || '').trim() || 'No recent messages found.');
640
+ }
641
+
451
642
  function imessageRecent(handle, options = {}) {
452
643
  if (!handle) {
453
644
  console.error('Usage: atris imessage recent <phone-or-email> [--limit 20]');
@@ -459,7 +650,7 @@ function imessageRecent(handle, options = {}) {
459
650
  process.exit(1);
460
651
  }
461
652
 
462
- const limit = Number(options.limit || 20);
653
+ const limit = boundedImessageLimit(options.limit);
463
654
  const chatDb = path.join(os.homedir(), 'Library', 'Messages', 'chat.db');
464
655
  const sql = `
465
656
  SELECT datetime(m.date/1000000000 + 978307200, 'unixepoch', 'localtime') AS ts,
@@ -469,14 +660,44 @@ function imessageRecent(handle, options = {}) {
469
660
  JOIN handle h ON h.rowid = m.handle_id
470
661
  WHERE h.id = '${String(handle).replace(/'/g, "''")}'
471
662
  ORDER BY m.date DESC
472
- LIMIT ${Math.max(1, Math.min(100, limit))};
663
+ LIMIT ${limit};
473
664
  `;
474
- const result = spawnSync('sqlite3', ['-readonly', chatDb, sql], { encoding: 'utf8' });
665
+ const sqliteArgs = options.json ? ['-json', '-readonly', chatDb, sql] : ['-readonly', chatDb, sql];
666
+ const result = spawnSync('sqlite3', sqliteArgs, { encoding: 'utf8' });
667
+ if (result.status !== 0) {
668
+ console.error(result.stderr || 'Failed to read Messages database.');
669
+ process.exit(1);
670
+ }
671
+ printImessageRows(result, options);
672
+ }
673
+
674
+ function imessageLatest(options = {}) {
675
+ const doctor = imessageDoctor();
676
+ if (!doctor.connected) {
677
+ printImessageDoctor(doctor, Boolean(options.json));
678
+ process.exit(1);
679
+ }
680
+
681
+ const limit = boundedImessageLimit(options.limit);
682
+ const chatDb = path.join(os.homedir(), 'Library', 'Messages', 'chat.db');
683
+ const sql = `
684
+ SELECT datetime(m.date/1000000000 + 978307200, 'unixepoch', 'localtime') AS ts,
685
+ CASE m.is_from_me WHEN 1 THEN 'me' ELSE COALESCE(h.id, 'unknown') END AS sender,
686
+ COALESCE(h.id, 'unknown') AS handle,
687
+ replace(replace(COALESCE(m.text,''), char(10), ' '), char(13), ' ') AS text
688
+ FROM message m
689
+ LEFT JOIN handle h ON h.rowid = m.handle_id
690
+ WHERE length(COALESCE(m.text,'')) > 0
691
+ ORDER BY m.date DESC
692
+ LIMIT ${limit};
693
+ `;
694
+ const sqliteArgs = options.json ? ['-json', '-readonly', chatDb, sql] : ['-readonly', chatDb, sql];
695
+ const result = spawnSync('sqlite3', sqliteArgs, { encoding: 'utf8' });
475
696
  if (result.status !== 0) {
476
697
  console.error(result.stderr || 'Failed to read Messages database.');
477
698
  process.exit(1);
478
699
  }
479
- console.log(result.stdout.trim() || 'No recent messages found.');
700
+ printImessageRows(result, options);
480
701
  }
481
702
 
482
703
  function escapeSqlString(value) {
@@ -1003,12 +1224,22 @@ async function imessageCommand(subcommand, ...args) {
1003
1224
  break;
1004
1225
  }
1005
1226
  case 'recent': {
1006
- const handle = args[0];
1007
1227
  const limitFlag = args.findIndex((x) => x === '--limit');
1008
1228
  const limit = limitFlag >= 0 ? args[limitFlag + 1] : 20;
1229
+ const handle = args.find((arg) => arg && !arg.startsWith('--') && arg !== String(limit));
1230
+ if (!handle) {
1231
+ imessageLatest({ limit, json: args.includes('--json') });
1232
+ break;
1233
+ }
1009
1234
  imessageRecent(handle, { limit, json: args.includes('--json') });
1010
1235
  break;
1011
1236
  }
1237
+ case 'latest': {
1238
+ const limitFlag = args.findIndex((x) => x === '--limit');
1239
+ const limit = limitFlag >= 0 ? args[limitFlag + 1] : 20;
1240
+ imessageLatest({ limit, json: args.includes('--json') });
1241
+ break;
1242
+ }
1012
1243
  case 'lookup': {
1013
1244
  imessageLookup(args);
1014
1245
  break;
@@ -1022,6 +1253,7 @@ async function imessageCommand(subcommand, ...args) {
1022
1253
  console.log(' atris imessage doctor [--json] - Check local Messages access');
1023
1254
  console.log(' atris imessage lookup --name <name> [--json] [--refresh]');
1024
1255
  console.log(' atris imessage recent <handle> - Read recent local messages');
1256
+ console.log(' atris imessage latest [--limit 20] [--json] - Read latest local messages');
1025
1257
  console.log(' atris imessage send --to <handle> --text <text> --approved [--json] [--receipt]');
1026
1258
  }
1027
1259
  }
package/commands/skill.js CHANGED
@@ -88,6 +88,39 @@ function findAllSkills(skillsDir) {
88
88
  return skills;
89
89
  }
90
90
 
91
+ function localSkillsDir() {
92
+ return path.join(process.cwd(), 'atris', 'skills');
93
+ }
94
+
95
+ function bundledSkillsDir() {
96
+ return path.join(__dirname, '..', 'atris', 'skills');
97
+ }
98
+
99
+ function readableSkillRoots() {
100
+ const roots = [localSkillsDir(), bundledSkillsDir()];
101
+ const seen = new Set();
102
+ return roots.filter((root) => {
103
+ if (!root || !fs.existsSync(root)) return false;
104
+ const real = fs.realpathSync(root);
105
+ if (seen.has(real)) return false;
106
+ seen.add(real);
107
+ return true;
108
+ });
109
+ }
110
+
111
+ function findReadableSkills() {
112
+ const seen = new Set();
113
+ const skills = [];
114
+ for (const root of readableSkillRoots()) {
115
+ for (const skill of findAllSkills(root)) {
116
+ if (seen.has(skill.folder)) continue;
117
+ seen.add(skill.folder);
118
+ skills.push(skill);
119
+ }
120
+ }
121
+ return skills;
122
+ }
123
+
91
124
  // --- Audit Checks ---
92
125
 
93
126
  function runAuditChecks(skill) {
@@ -355,11 +388,10 @@ function generateTags(folderName, description) {
355
388
  // --- Subcommand Handlers ---
356
389
 
357
390
  function skillList() {
358
- const skillsDir = path.join(process.cwd(), 'atris', 'skills');
359
- const skills = findAllSkills(skillsDir);
391
+ const skills = findReadableSkills();
360
392
 
361
393
  if (skills.length === 0) {
362
- console.log('No skills found in atris/skills/. Run "atris init" first.');
394
+ console.log('No skills found in local or bundled Atris skill roots.');
363
395
  return;
364
396
  }
365
397
 
@@ -396,15 +428,14 @@ function skillList() {
396
428
  }
397
429
 
398
430
  function skillAudit(name) {
399
- const skillsDir = path.join(process.cwd(), 'atris', 'skills');
400
- const allSkills = findAllSkills(skillsDir);
431
+ const allSkills = findReadableSkills();
401
432
 
402
433
  const targets = name === '--all'
403
434
  ? allSkills
404
435
  : allSkills.filter(s => s.folder === name || s.leafFolder === name);
405
436
 
406
437
  if (targets.length === 0) {
407
- console.error(`Skill "${name}" not found. Run "atris skill list" to see available skills.`);
438
+ console.error(`Skill "${name}" not found. Run "atris skill list" to see available local and bundled skills.`);
408
439
  process.exit(1);
409
440
  }
410
441
 
package/commands/xp.js CHANGED
@@ -7,6 +7,7 @@ const crypto = require('crypto');
7
7
  const { spawnSync } = require('child_process');
8
8
 
9
9
  const DEFAULT_GRAPH_DAYS = 365;
10
+ const MAX_SYNC_GRAPH_DAYS = 370;
10
11
  const INTENSITY_CHARS = [' ', '.', ':', '*', '#'];
11
12
  const ROW_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
12
13
  const TASK_EPISODES_FILE = path.join('.atris', 'state', 'task_episodes.jsonl');
@@ -210,6 +211,38 @@ function combineAgentXpContributionGraphs(graphs, windowDays = DEFAULT_GRAPH_DAY
210
211
  return graphFromDailyTotals(totals, windowDays);
211
212
  }
212
213
 
214
+ function sanitizeContributionGraphForSync(graph) {
215
+ if (!graph || typeof graph !== 'object') return null;
216
+ const rawDays = Array.isArray(graph.days) ? graph.days : [];
217
+ const days = rawDays
218
+ .slice(-MAX_SYNC_GRAPH_DAYS)
219
+ .map((day) => {
220
+ const date = typeof day?.date === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(day.date)
221
+ ? day.date
222
+ : null;
223
+ if (!date) return null;
224
+ const xp = Math.max(0, asNumber(day.xp ?? day.total_xp));
225
+ return {
226
+ date,
227
+ xp,
228
+ total_xp: Math.max(0, asNumber(day.total_xp, xp)),
229
+ intensity: Math.max(0, Math.min(4, asNumber(day.intensity))),
230
+ };
231
+ })
232
+ .filter(Boolean);
233
+ if (days.length === 0) return null;
234
+ return {
235
+ schema: 'atris.agent_xp_contribution_graph.v1',
236
+ metric_label: AGENT_XP_LABEL,
237
+ window_days: days.length,
238
+ total_xp: days.reduce((sum, day) => sum + day.xp, 0),
239
+ active_days: days.filter(day => day.xp > 0).length,
240
+ first_date: days[0]?.date || null,
241
+ last_date: days[days.length - 1]?.date || null,
242
+ days,
243
+ };
244
+ }
245
+
213
246
  function currentForm(payload) {
214
247
  const arenas = payload.current_form_by_arena || {};
215
248
  const local = arenas.local_workspace || {};
@@ -1123,6 +1156,36 @@ function workspaceName(workspace) {
1123
1156
  return path.basename(workspace) || workspace;
1124
1157
  }
1125
1158
 
1159
+ function booleanSetting(value) {
1160
+ if (value === true || value === false) return value;
1161
+ const text = String(value || '').trim().toLowerCase();
1162
+ if (['1', 'true', 'yes', 'public', 'global'].includes(text)) return true;
1163
+ if (['0', 'false', 'no', 'private', 'internal', 'org'].includes(text)) return false;
1164
+ return null;
1165
+ }
1166
+
1167
+ function agentXpPublicBinding(binding = {}) {
1168
+ const nested = binding.agentxp && typeof binding.agentxp === 'object' ? binding.agentxp : {};
1169
+ const explicit = booleanSetting(
1170
+ binding.agentxp_public
1171
+ ?? binding.public_agentxp
1172
+ ?? binding.agentxp_public_leaderboard
1173
+ ?? nested.public
1174
+ ?? nested.public_agentxp
1175
+ );
1176
+ if (explicit !== null) return explicit;
1177
+
1178
+ const visibility = slugify(binding.agentxp_visibility || nested.visibility);
1179
+ if (visibility === 'public') return true;
1180
+ if (['internal', 'private'].includes(visibility)) return false;
1181
+
1182
+ const scope = slugify(binding.agentxp_scope || nested.scope);
1183
+ if (scope === 'global') return true;
1184
+ if (['org', 'internal', 'private'].includes(scope)) return false;
1185
+
1186
+ return null;
1187
+ }
1188
+
1126
1189
  function publicWorkspaceBinding(workspace) {
1127
1190
  const binding = readJsonFile(path.join(workspace, BUSINESS_BINDING_FILE), null);
1128
1191
  if (!binding || typeof binding !== 'object') return null;
@@ -1130,6 +1193,7 @@ function publicWorkspaceBinding(workspace) {
1130
1193
  const workspaceId = String(binding.workspace_id || '').trim();
1131
1194
  const businessSlug = slugify(binding.slug || binding.business_slug || binding.name);
1132
1195
  const workspaceTemplate = slugify(binding.workspace_template || binding.organization_type || binding.computer_type);
1196
+ const agentxpPublic = agentXpPublicBinding(binding);
1133
1197
  if (!businessId && !workspaceId && !businessSlug) return null;
1134
1198
  return {
1135
1199
  business_id: businessId || null,
@@ -1138,6 +1202,7 @@ function publicWorkspaceBinding(workspace) {
1138
1202
  workspace_template: workspaceTemplate || null,
1139
1203
  computer: businessSlug || workspaceName(workspace),
1140
1204
  computer_slug: businessSlug || slugify(workspaceName(workspace)),
1205
+ ...(agentxpPublic === null ? {} : { agentxp_public: agentxpPublic }),
1141
1206
  };
1142
1207
  }
1143
1208
 
@@ -1792,19 +1857,22 @@ function uniqueTruthy(values) {
1792
1857
  return Array.from(new Set(values.map(value => String(value || '').trim()).filter(Boolean))).sort();
1793
1858
  }
1794
1859
 
1795
- function syncAttribution(workspaces) {
1860
+ function syncAttribution(workspaces, options = {}) {
1796
1861
  const included = workspaces.filter(workspace => workspace.included !== false);
1797
1862
  const scoped = included.length ? included : workspaces;
1798
1863
  const businessIds = uniqueTruthy(scoped.map(workspace => workspace.business_id));
1799
1864
  const workspaceIds = uniqueTruthy(scoped.map(workspace => workspace.workspace_id));
1800
1865
  const businessSlugs = uniqueTruthy(scoped.map(workspace => workspace.business_slug || workspace.computer_slug));
1801
1866
  const templates = uniqueTruthy(scoped.map(workspace => workspace.workspace_template));
1867
+ const explicitPublic = typeof options.publicAgentXp === 'boolean' ? options.publicAgentXp : null;
1868
+ const workspacePublic = scoped.length === 1 && scoped[0]?.agentxp_public === true;
1869
+ const agentxpPublic = explicitPublic === null ? (workspacePublic ? true : null) : explicitPublic;
1802
1870
 
1803
1871
  if (businessIds.length > 1) {
1804
- return { attribution_scope: 'multi_business', computer: 'multiple-workspaces' };
1872
+ return { attribution_scope: 'multi_business', computer: 'multiple-workspaces', agentxp_public: explicitPublic === true };
1805
1873
  }
1806
1874
  if (businessIds.length === 0 && scoped.length > 1 && businessSlugs.length > 1) {
1807
- return { attribution_scope: 'multi_workspace', computer: 'multiple-workspaces' };
1875
+ return { attribution_scope: 'multi_workspace', computer: 'multiple-workspaces', agentxp_public: explicitPublic === true };
1808
1876
  }
1809
1877
 
1810
1878
  const businessId = businessIds[0] || null;
@@ -1818,6 +1886,7 @@ function syncAttribution(workspaces) {
1818
1886
  business_slug: businessSlug,
1819
1887
  workspace_template: workspaceTemplate,
1820
1888
  computer: businessSlug || scoped[0]?.computer || scoped[0]?.name || 'local',
1889
+ agentxp_public: agentxpPublic,
1821
1890
  };
1822
1891
  }
1823
1892
 
@@ -1825,15 +1894,24 @@ function syncScopeFields(attribution = {}) {
1825
1894
  const attributionScope = slugify(attribution.attribution_scope);
1826
1895
  const orgId = String(attribution.business_id || attribution.business_slug || '').trim() || null;
1827
1896
  const businessBound = Boolean(orgId) || attributionScope === 'business-bound' || attributionScope === 'multi-business';
1897
+ const publicAgentXp = typeof attribution.agentxp_public === 'boolean'
1898
+ ? attribution.agentxp_public
1899
+ : !businessBound;
1828
1900
  return {
1829
- scope: businessBound ? 'org' : 'global',
1901
+ scope: publicAgentXp ? 'global' : (businessBound ? 'org' : 'global'),
1830
1902
  org_id: orgId,
1831
1903
  computer_id: String(attribution.workspace_id || attribution.computer || 'local').trim() || 'local',
1832
- visibility: businessBound ? 'internal' : 'public',
1833
- public_agentxp: !businessBound,
1904
+ visibility: publicAgentXp ? 'public' : 'internal',
1905
+ public_agentxp: publicAgentXp,
1834
1906
  };
1835
1907
  }
1836
1908
 
1909
+ function publicAgentXpOverride(args = []) {
1910
+ if (hasFlag(args, '--public')) return true;
1911
+ if (hasFlag(args, '--private') || hasFlag(args, '--internal')) return false;
1912
+ return null;
1913
+ }
1914
+
1837
1915
  function credentialHandle(credentials) {
1838
1916
  return slugify(
1839
1917
  credentials?.username
@@ -1886,13 +1964,13 @@ function syncPlayer(args, projection) {
1886
1964
 
1887
1965
  function buildAgentXpSyncPacket(args = []) {
1888
1966
  const localMode = hasFlag(args, '--local') || hasFlag(args, '--workspace') || hasFlag(args, '--operator');
1889
- const projectionArgs = args.filter(arg => !['--dry-run', '--no-post', '--packet'].includes(arg));
1967
+ const projectionArgs = args.filter(arg => !['--dry-run', '--no-post', '--packet', '--public', '--private', '--internal'].includes(arg));
1890
1968
  const projection = hasFlag(args, '--all') || !localMode
1891
1969
  ? collectAllLocalXpProjection(projectionArgs)
1892
1970
  : collectLocalXpProjection(projectionArgs);
1893
1971
  const player = syncPlayer(args, projection);
1894
1972
  const workspaces = projectionWorkspaceSummaries(projection);
1895
- const attribution = syncAttribution(workspaces);
1973
+ const attribution = syncAttribution(workspaces, { publicAgentXp: publicAgentXpOverride(args) });
1896
1974
  const scopeFields = syncScopeFields(attribution);
1897
1975
  const totalXp = asNumber(projection.total_agent_xp ?? projection.agent_xp ?? projection.total_xp ?? projection.career_xp);
1898
1976
  const receiptsCount = asNumber(projection.receipts_count);
@@ -1900,6 +1978,7 @@ function buildAgentXpSyncPacket(args = []) {
1900
1978
  const publicXp = earnedAgentXp(totalXp);
1901
1979
  const currentForm = currentFormScore(totalXp);
1902
1980
  const levelProgress = agentXpLevelProgress(publicXp);
1981
+ const contributionGraph = sanitizeContributionGraphForSync(projection.contribution_graph);
1903
1982
  const workspaceRootHash = sha256(workspaces.map(item => item.workspace_root_hash || item.name).sort().join(':'));
1904
1983
  const entry = {
1905
1984
  user_id: player,
@@ -1919,6 +1998,7 @@ function buildAgentXpSyncPacket(args = []) {
1919
1998
  lock_reason: eligible ? null : 'not_enough_trusted_proof',
1920
1999
  public_adjustment: null,
1921
2000
  next_move: eligible ? 'Play the next proof-backed AgentXP mission.' : 'Complete one proof-backed AgentXP rep.',
2001
+ contribution_graph: contributionGraph,
1922
2002
  };
1923
2003
  const packet = {
1924
2004
  schema: 'atris.agentxp_sync_packet.v1',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.15.55",
3
+ "version": "3.15.57",
4
4
  "main": "bin/atris.js",
5
5
  "bin": {
6
6
  "atris": "bin/atris.js",