atris 3.15.55 → 3.15.56

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.
@@ -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,8 +383,137 @@ 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());
387
+ }
388
+ }
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]);
363
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);
364
517
  }
365
518
 
366
519
  async function slackCommand(subcommand, ...args) {
@@ -369,9 +522,22 @@ async function slackCommand(subcommand, ...args) {
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
 
package/commands/xp.js CHANGED
@@ -1123,6 +1123,36 @@ function workspaceName(workspace) {
1123
1123
  return path.basename(workspace) || workspace;
1124
1124
  }
1125
1125
 
1126
+ function booleanSetting(value) {
1127
+ if (value === true || value === false) return value;
1128
+ const text = String(value || '').trim().toLowerCase();
1129
+ if (['1', 'true', 'yes', 'public', 'global'].includes(text)) return true;
1130
+ if (['0', 'false', 'no', 'private', 'internal', 'org'].includes(text)) return false;
1131
+ return null;
1132
+ }
1133
+
1134
+ function agentXpPublicBinding(binding = {}) {
1135
+ const nested = binding.agentxp && typeof binding.agentxp === 'object' ? binding.agentxp : {};
1136
+ const explicit = booleanSetting(
1137
+ binding.agentxp_public
1138
+ ?? binding.public_agentxp
1139
+ ?? binding.agentxp_public_leaderboard
1140
+ ?? nested.public
1141
+ ?? nested.public_agentxp
1142
+ );
1143
+ if (explicit !== null) return explicit;
1144
+
1145
+ const visibility = slugify(binding.agentxp_visibility || nested.visibility);
1146
+ if (visibility === 'public') return true;
1147
+ if (['internal', 'private'].includes(visibility)) return false;
1148
+
1149
+ const scope = slugify(binding.agentxp_scope || nested.scope);
1150
+ if (scope === 'global') return true;
1151
+ if (['org', 'internal', 'private'].includes(scope)) return false;
1152
+
1153
+ return null;
1154
+ }
1155
+
1126
1156
  function publicWorkspaceBinding(workspace) {
1127
1157
  const binding = readJsonFile(path.join(workspace, BUSINESS_BINDING_FILE), null);
1128
1158
  if (!binding || typeof binding !== 'object') return null;
@@ -1130,6 +1160,7 @@ function publicWorkspaceBinding(workspace) {
1130
1160
  const workspaceId = String(binding.workspace_id || '').trim();
1131
1161
  const businessSlug = slugify(binding.slug || binding.business_slug || binding.name);
1132
1162
  const workspaceTemplate = slugify(binding.workspace_template || binding.organization_type || binding.computer_type);
1163
+ const agentxpPublic = agentXpPublicBinding(binding);
1133
1164
  if (!businessId && !workspaceId && !businessSlug) return null;
1134
1165
  return {
1135
1166
  business_id: businessId || null,
@@ -1138,6 +1169,7 @@ function publicWorkspaceBinding(workspace) {
1138
1169
  workspace_template: workspaceTemplate || null,
1139
1170
  computer: businessSlug || workspaceName(workspace),
1140
1171
  computer_slug: businessSlug || slugify(workspaceName(workspace)),
1172
+ ...(agentxpPublic === null ? {} : { agentxp_public: agentxpPublic }),
1141
1173
  };
1142
1174
  }
1143
1175
 
@@ -1792,19 +1824,22 @@ function uniqueTruthy(values) {
1792
1824
  return Array.from(new Set(values.map(value => String(value || '').trim()).filter(Boolean))).sort();
1793
1825
  }
1794
1826
 
1795
- function syncAttribution(workspaces) {
1827
+ function syncAttribution(workspaces, options = {}) {
1796
1828
  const included = workspaces.filter(workspace => workspace.included !== false);
1797
1829
  const scoped = included.length ? included : workspaces;
1798
1830
  const businessIds = uniqueTruthy(scoped.map(workspace => workspace.business_id));
1799
1831
  const workspaceIds = uniqueTruthy(scoped.map(workspace => workspace.workspace_id));
1800
1832
  const businessSlugs = uniqueTruthy(scoped.map(workspace => workspace.business_slug || workspace.computer_slug));
1801
1833
  const templates = uniqueTruthy(scoped.map(workspace => workspace.workspace_template));
1834
+ const explicitPublic = typeof options.publicAgentXp === 'boolean' ? options.publicAgentXp : null;
1835
+ const workspacePublic = scoped.length === 1 && scoped[0]?.agentxp_public === true;
1836
+ const agentxpPublic = explicitPublic === null ? (workspacePublic ? true : null) : explicitPublic;
1802
1837
 
1803
1838
  if (businessIds.length > 1) {
1804
- return { attribution_scope: 'multi_business', computer: 'multiple-workspaces' };
1839
+ return { attribution_scope: 'multi_business', computer: 'multiple-workspaces', agentxp_public: explicitPublic === true };
1805
1840
  }
1806
1841
  if (businessIds.length === 0 && scoped.length > 1 && businessSlugs.length > 1) {
1807
- return { attribution_scope: 'multi_workspace', computer: 'multiple-workspaces' };
1842
+ return { attribution_scope: 'multi_workspace', computer: 'multiple-workspaces', agentxp_public: explicitPublic === true };
1808
1843
  }
1809
1844
 
1810
1845
  const businessId = businessIds[0] || null;
@@ -1818,6 +1853,7 @@ function syncAttribution(workspaces) {
1818
1853
  business_slug: businessSlug,
1819
1854
  workspace_template: workspaceTemplate,
1820
1855
  computer: businessSlug || scoped[0]?.computer || scoped[0]?.name || 'local',
1856
+ agentxp_public: agentxpPublic,
1821
1857
  };
1822
1858
  }
1823
1859
 
@@ -1825,15 +1861,24 @@ function syncScopeFields(attribution = {}) {
1825
1861
  const attributionScope = slugify(attribution.attribution_scope);
1826
1862
  const orgId = String(attribution.business_id || attribution.business_slug || '').trim() || null;
1827
1863
  const businessBound = Boolean(orgId) || attributionScope === 'business-bound' || attributionScope === 'multi-business';
1864
+ const publicAgentXp = typeof attribution.agentxp_public === 'boolean'
1865
+ ? attribution.agentxp_public
1866
+ : !businessBound;
1828
1867
  return {
1829
- scope: businessBound ? 'org' : 'global',
1868
+ scope: publicAgentXp ? 'global' : (businessBound ? 'org' : 'global'),
1830
1869
  org_id: orgId,
1831
1870
  computer_id: String(attribution.workspace_id || attribution.computer || 'local').trim() || 'local',
1832
- visibility: businessBound ? 'internal' : 'public',
1833
- public_agentxp: !businessBound,
1871
+ visibility: publicAgentXp ? 'public' : 'internal',
1872
+ public_agentxp: publicAgentXp,
1834
1873
  };
1835
1874
  }
1836
1875
 
1876
+ function publicAgentXpOverride(args = []) {
1877
+ if (hasFlag(args, '--public')) return true;
1878
+ if (hasFlag(args, '--private') || hasFlag(args, '--internal')) return false;
1879
+ return null;
1880
+ }
1881
+
1837
1882
  function credentialHandle(credentials) {
1838
1883
  return slugify(
1839
1884
  credentials?.username
@@ -1886,13 +1931,13 @@ function syncPlayer(args, projection) {
1886
1931
 
1887
1932
  function buildAgentXpSyncPacket(args = []) {
1888
1933
  const localMode = hasFlag(args, '--local') || hasFlag(args, '--workspace') || hasFlag(args, '--operator');
1889
- const projectionArgs = args.filter(arg => !['--dry-run', '--no-post', '--packet'].includes(arg));
1934
+ const projectionArgs = args.filter(arg => !['--dry-run', '--no-post', '--packet', '--public', '--private', '--internal'].includes(arg));
1890
1935
  const projection = hasFlag(args, '--all') || !localMode
1891
1936
  ? collectAllLocalXpProjection(projectionArgs)
1892
1937
  : collectLocalXpProjection(projectionArgs);
1893
1938
  const player = syncPlayer(args, projection);
1894
1939
  const workspaces = projectionWorkspaceSummaries(projection);
1895
- const attribution = syncAttribution(workspaces);
1940
+ const attribution = syncAttribution(workspaces, { publicAgentXp: publicAgentXpOverride(args) });
1896
1941
  const scopeFields = syncScopeFields(attribution);
1897
1942
  const totalXp = asNumber(projection.total_agent_xp ?? projection.agent_xp ?? projection.total_xp ?? projection.career_xp);
1898
1943
  const receiptsCount = asNumber(projection.receipts_count);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.15.55",
3
+ "version": "3.15.56",
4
4
  "main": "bin/atris.js",
5
5
  "bin": {
6
6
  "atris": "bin/atris.js",