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 +1 -1
- package/commands/computer.js +45 -23
- package/commands/integrations.js +242 -10
- package/commands/skill.js +37 -6
- package/commands/xp.js +88 -8
- package/package.json +1 -1
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.
|
|
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))
|
package/commands/computer.js
CHANGED
|
@@ -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
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
sawVisibleOutput
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
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
|
}
|
package/commands/integrations.js
CHANGED
|
@@ -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
|
|
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
|
|
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 =
|
|
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 ${
|
|
663
|
+
LIMIT ${limit};
|
|
473
664
|
`;
|
|
474
|
-
const
|
|
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
|
-
|
|
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
|
|
359
|
-
const skills = findAllSkills(skillsDir);
|
|
391
|
+
const skills = findReadableSkills();
|
|
360
392
|
|
|
361
393
|
if (skills.length === 0) {
|
|
362
|
-
console.log('No skills found in
|
|
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
|
|
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:
|
|
1833
|
-
public_agentxp:
|
|
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',
|