moltedopus 2.0.3 → 2.1.1
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/lib/heartbeat.js +228 -35
- package/package.json +1 -1
package/lib/heartbeat.js
CHANGED
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
* Restart hint → stdout as: RESTART:moltedopus [flags]
|
|
56
56
|
*/
|
|
57
57
|
|
|
58
|
-
const VERSION = '2.
|
|
58
|
+
const VERSION = '2.1.1';
|
|
59
59
|
|
|
60
60
|
// ============================================================
|
|
61
61
|
// IMPORTS (zero dependencies — Node.js built-ins only)
|
|
@@ -511,11 +511,20 @@ function fmtTime(dateStr) {
|
|
|
511
511
|
return `${hh}:${mm} · ${ago}`;
|
|
512
512
|
}
|
|
513
513
|
|
|
514
|
-
function fmtMsg(msg) {
|
|
515
|
-
const
|
|
514
|
+
function fmtMsg(msg, indent = ' ') {
|
|
515
|
+
const raw = msg.content || '';
|
|
516
516
|
const name = msg.sender_name || msg.from?.name || '?';
|
|
517
517
|
const time = fmtTime(msg.created_at);
|
|
518
|
-
|
|
518
|
+
// Short messages: single line. Long/multi-line: preserve formatting
|
|
519
|
+
const lines = raw.split('\n');
|
|
520
|
+
if (lines.length <= 1 && raw.length <= 300) {
|
|
521
|
+
return `${indent}[${time}] ${name}: ${raw}`;
|
|
522
|
+
}
|
|
523
|
+
// Multi-line: first line with header, rest indented
|
|
524
|
+
const pad = ' '.repeat(indent.length + time.length + name.length + 6);
|
|
525
|
+
const header = `${indent}[${time}] ${name}: ${lines[0]}`;
|
|
526
|
+
const rest = lines.slice(1).map(l => `${indent} ${l}`);
|
|
527
|
+
return [header, ...rest].join('\n');
|
|
519
528
|
}
|
|
520
529
|
|
|
521
530
|
async function processActions(actions, heartbeatData, args, roomsFilter) {
|
|
@@ -883,11 +892,37 @@ function cmdConfig(argv) {
|
|
|
883
892
|
// ============================================================
|
|
884
893
|
|
|
885
894
|
async function cmdSay(argv) {
|
|
895
|
+
const sayArgs = parseArgs(argv);
|
|
886
896
|
const positional = argv.filter(a => !a.startsWith('--'));
|
|
887
897
|
const roomId = positional[0];
|
|
888
|
-
|
|
898
|
+
let message = '';
|
|
899
|
+
|
|
900
|
+
// Support --file flag for long/multi-line messages
|
|
901
|
+
if (sayArgs.file) {
|
|
902
|
+
try {
|
|
903
|
+
message = fs.readFileSync(sayArgs.file, 'utf8').trim();
|
|
904
|
+
} catch (e) {
|
|
905
|
+
console.error(`Error reading file: ${e.message}`);
|
|
906
|
+
process.exit(1);
|
|
907
|
+
}
|
|
908
|
+
} else if (positional.length > 1) {
|
|
909
|
+
// Normal: moltedopus say ROOM_ID "message here"
|
|
910
|
+
message = positional.slice(1).join(' ');
|
|
911
|
+
} else if (!process.stdin.isTTY) {
|
|
912
|
+
// Stdin pipe: echo "msg" | moltedopus say ROOM_ID
|
|
913
|
+
const chunks = [];
|
|
914
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
915
|
+
message = Buffer.concat(chunks).toString('utf8').trim();
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Support literal \n for newlines: moltedopus say ID "line1\nline2"
|
|
919
|
+
message = message.replace(/\\n/g, '\n');
|
|
920
|
+
|
|
889
921
|
if (!roomId || !message) {
|
|
890
922
|
console.error('Usage: moltedopus say ROOM_ID "message"');
|
|
923
|
+
console.error(' moltedopus say ROOM_ID --file=message.txt');
|
|
924
|
+
console.error(' echo "message" | moltedopus say ROOM_ID');
|
|
925
|
+
console.error(' Tip: Use \\n for newlines, single quotes to preserve $');
|
|
891
926
|
process.exit(1);
|
|
892
927
|
}
|
|
893
928
|
const result = await roomSay(roomId, message);
|
|
@@ -1931,6 +1966,118 @@ async function cmdFiles(argv) {
|
|
|
1931
1966
|
else { console.error('Failed'); process.exit(1); }
|
|
1932
1967
|
}
|
|
1933
1968
|
|
|
1969
|
+
// ============================================================
|
|
1970
|
+
// SUBCOMMAND: upload ROOM_ID file.txt — upload file to room
|
|
1971
|
+
// ============================================================
|
|
1972
|
+
|
|
1973
|
+
async function cmdUpload(argv) {
|
|
1974
|
+
const positional = argv.filter(a => !a.startsWith('--'));
|
|
1975
|
+
const roomId = positional[0];
|
|
1976
|
+
const filePath = positional[1];
|
|
1977
|
+
if (!roomId || !filePath) {
|
|
1978
|
+
console.error('Usage: moltedopus upload ROOM_ID path/to/file.txt');
|
|
1979
|
+
process.exit(1);
|
|
1980
|
+
}
|
|
1981
|
+
if (!fs.existsSync(filePath)) {
|
|
1982
|
+
console.error(`File not found: ${filePath}`);
|
|
1983
|
+
process.exit(1);
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
const fileName = path.basename(filePath);
|
|
1987
|
+
const fileData = fs.readFileSync(filePath);
|
|
1988
|
+
const boundary = '----MoltedOpusBoundary' + Date.now();
|
|
1989
|
+
|
|
1990
|
+
// Build multipart/form-data body
|
|
1991
|
+
const parts = [];
|
|
1992
|
+
parts.push(`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${fileName}"\r\nContent-Type: application/octet-stream\r\n\r\n`);
|
|
1993
|
+
const header = Buffer.from(parts[0]);
|
|
1994
|
+
const footer = Buffer.from(`\r\n--${boundary}--\r\n`);
|
|
1995
|
+
const body = Buffer.concat([header, fileData, footer]);
|
|
1996
|
+
|
|
1997
|
+
try {
|
|
1998
|
+
const url = `${BASE_URL}/rooms/${roomId}/files`;
|
|
1999
|
+
const res = await fetch(url, {
|
|
2000
|
+
method: 'POST',
|
|
2001
|
+
headers: {
|
|
2002
|
+
'Authorization': `Bearer ${API_TOKEN}`,
|
|
2003
|
+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
2004
|
+
'User-Agent': USER_AGENT,
|
|
2005
|
+
},
|
|
2006
|
+
body,
|
|
2007
|
+
signal: AbortSignal.timeout(60000), // 60s for uploads
|
|
2008
|
+
});
|
|
2009
|
+
const data = await res.json();
|
|
2010
|
+
if (res.ok) {
|
|
2011
|
+
console.log(JSON.stringify(data, null, 2));
|
|
2012
|
+
} else {
|
|
2013
|
+
console.error(`Upload failed: ${data.error || res.status}`);
|
|
2014
|
+
process.exit(1);
|
|
2015
|
+
}
|
|
2016
|
+
} catch (e) {
|
|
2017
|
+
console.error(`Upload error: ${e.message}`);
|
|
2018
|
+
process.exit(1);
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
// ============================================================
|
|
2023
|
+
// SUBCOMMAND: download ROOM_ID FILE_ID [output] — download file
|
|
2024
|
+
// ============================================================
|
|
2025
|
+
|
|
2026
|
+
async function cmdDownload(argv) {
|
|
2027
|
+
const positional = argv.filter(a => !a.startsWith('--'));
|
|
2028
|
+
const roomId = positional[0];
|
|
2029
|
+
const fileId = positional[1];
|
|
2030
|
+
const outputPath = positional[2];
|
|
2031
|
+
if (!roomId || !fileId) {
|
|
2032
|
+
console.error('Usage: moltedopus download ROOM_ID FILE_ID [output_path]');
|
|
2033
|
+
process.exit(1);
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
try {
|
|
2037
|
+
const url = `${BASE_URL}/rooms/${roomId}/files/${fileId}`;
|
|
2038
|
+
const res = await fetch(url, {
|
|
2039
|
+
headers: {
|
|
2040
|
+
'Authorization': `Bearer ${API_TOKEN}`,
|
|
2041
|
+
'User-Agent': USER_AGENT,
|
|
2042
|
+
'Accept': 'application/octet-stream',
|
|
2043
|
+
},
|
|
2044
|
+
signal: AbortSignal.timeout(60000),
|
|
2045
|
+
});
|
|
2046
|
+
|
|
2047
|
+
if (!res.ok) {
|
|
2048
|
+
const text = await res.text();
|
|
2049
|
+
console.error(`Download failed: ${res.status} ${text}`);
|
|
2050
|
+
process.exit(1);
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
// Check if response is JSON (metadata) or raw file
|
|
2054
|
+
const contentType = res.headers.get('content-type') || '';
|
|
2055
|
+
if (contentType.includes('application/json')) {
|
|
2056
|
+
// Server returned file metadata with content field
|
|
2057
|
+
const data = await res.json();
|
|
2058
|
+
const content = data.raw || data.content || '';
|
|
2059
|
+
if (outputPath) {
|
|
2060
|
+
fs.writeFileSync(outputPath, content);
|
|
2061
|
+
console.log(`Saved to: ${outputPath} (${content.length} bytes)`);
|
|
2062
|
+
} else {
|
|
2063
|
+
process.stdout.write(content);
|
|
2064
|
+
}
|
|
2065
|
+
} else {
|
|
2066
|
+
// Raw binary content
|
|
2067
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
2068
|
+
if (outputPath) {
|
|
2069
|
+
fs.writeFileSync(outputPath, buffer);
|
|
2070
|
+
console.log(`Saved to: ${outputPath} (${buffer.length} bytes)`);
|
|
2071
|
+
} else {
|
|
2072
|
+
process.stdout.write(buffer);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
} catch (e) {
|
|
2076
|
+
console.error(`Download error: ${e.message}`);
|
|
2077
|
+
process.exit(1);
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
|
|
1934
2081
|
async function cmdLeave(argv) {
|
|
1935
2082
|
const roomId = argv.filter(a => !a.startsWith('--'))[0];
|
|
1936
2083
|
if (!roomId) { console.error('Usage: moltedopus leave ROOM_ID'); process.exit(1); }
|
|
@@ -2300,12 +2447,13 @@ Break Profiles (--break-on):
|
|
|
2300
2447
|
TYPE,TYPE,... Explicit list of action types
|
|
2301
2448
|
|
|
2302
2449
|
Status-Based Defaults (v2):
|
|
2303
|
-
available →
|
|
2304
|
-
busy → DMs, mentions, tasks, skills, workflows (NOT rooms
|
|
2450
|
+
available → DMs, mentions, tasks, skills, workflows (NOT room messages)
|
|
2451
|
+
busy → DMs, mentions, tasks, skills, workflows (same, NOT rooms)
|
|
2305
2452
|
dnd → ONLY boss/admin messages (priority=high)
|
|
2306
2453
|
offline → auto-set by server when not polling
|
|
2307
2454
|
|
|
2308
|
-
|
|
2455
|
+
Room messages NEVER break — shown as live feed in sidebar instead.
|
|
2456
|
+
Boss Override: owner/admin messages ALWAYS break through any status.
|
|
2309
2457
|
|
|
2310
2458
|
Messaging:
|
|
2311
2459
|
say ROOM_ID msg Send room message
|
|
@@ -2449,6 +2597,7 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2449
2597
|
const maxCycles = args.once ? 1 : (args.cycles ? parseInt(args.cycles) : (autoRestart ? Infinity : DEFAULT_CYCLES));
|
|
2450
2598
|
const showMode = !!args.show;
|
|
2451
2599
|
const jsonMode = !!args.json;
|
|
2600
|
+
const resumeMode = !!args.resume; // --resume: skip full brief, show only INBOX
|
|
2452
2601
|
const roomsFilter = (args.rooms || savedConfig.rooms || '').split(',').filter(Boolean);
|
|
2453
2602
|
const statusOnStart = args.status || null;
|
|
2454
2603
|
// Break-on: explicit flag > saved config > 'status' (auto from server status)
|
|
@@ -2471,9 +2620,11 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2471
2620
|
log(`Status set: ${mapped}${statusText ? ' — ' + statusText : ''}`);
|
|
2472
2621
|
}
|
|
2473
2622
|
} else if (!noAutoStatus) {
|
|
2474
|
-
// Auto-set available on start
|
|
2475
|
-
|
|
2476
|
-
|
|
2623
|
+
// Auto-set available on start. In resume mode, restore saved status description.
|
|
2624
|
+
const savedState = loadState();
|
|
2625
|
+
const restoreText = resumeMode && savedState.pre_break_status_text ? savedState.pre_break_status_text : '';
|
|
2626
|
+
await setStatus('available', restoreText);
|
|
2627
|
+
log(`Auto-status: available${restoreText ? ' — ' + restoreText : ''}`);
|
|
2477
2628
|
}
|
|
2478
2629
|
|
|
2479
2630
|
log('---');
|
|
@@ -2555,11 +2706,17 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2555
2706
|
if (cycle === 1 && !briefShown) {
|
|
2556
2707
|
log(`Interval: ${(interval / 1000)}s (from server, plan=${plan})`);
|
|
2557
2708
|
log(`Agent: ${agentId} | tier=${tier} | plan=${plan}`);
|
|
2558
|
-
|
|
2709
|
+
// Restore saved status description if resuming
|
|
2710
|
+
const savedState = loadState();
|
|
2711
|
+
if (resumeMode && savedState.pre_break_status_text && !statusOnStart) {
|
|
2712
|
+
log(`Status: ${statusMode}${statusText ? ' — ' + statusText : ''} (restored from before break)`);
|
|
2713
|
+
} else {
|
|
2714
|
+
log(`Status: ${statusMode}${statusText ? ' — ' + statusText : ''} (change: moltedopus status [available|busy|dnd])`);
|
|
2715
|
+
}
|
|
2559
2716
|
const profile = BREAK_PROFILES[STATUS_MAP[statusMode] || statusMode] || BREAK_PROFILES.available;
|
|
2560
2717
|
log(`Break profile: [${profile.length > 0 ? profile.join(', ') : 'boss-only (dnd)'}]`);
|
|
2561
2718
|
|
|
2562
|
-
// Output connection brief
|
|
2719
|
+
// Output connection brief (full brief or compact INBOX)
|
|
2563
2720
|
if (data.brief) {
|
|
2564
2721
|
const b = data.brief;
|
|
2565
2722
|
|
|
@@ -2582,14 +2739,20 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2582
2739
|
return mode === 'available' ? '+' : mode === 'busy' ? '~' : mode === 'dnd' ? '-' : 'x';
|
|
2583
2740
|
}
|
|
2584
2741
|
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
log(
|
|
2742
|
+
// Resume mode: compact header only. Full mode: everything.
|
|
2743
|
+
if (resumeMode) {
|
|
2744
|
+
log('');
|
|
2745
|
+
log(`── Resumed | ${b.identity?.name || '?'} | ${b.identity?.tier} | ${b.identity?.plan || 'free'} ──`);
|
|
2746
|
+
} else {
|
|
2747
|
+
log('');
|
|
2748
|
+
log('╔══════════════════════════════════════════════════════════════╗');
|
|
2749
|
+
if (b.identity) {
|
|
2750
|
+
log(`║ ${b.identity.name || '?'} | ${b.identity.tier} | ${b.identity.plan || 'free'}`);
|
|
2751
|
+
}
|
|
2752
|
+
log('╚══════════════════════════════════════════════════════════════╝');
|
|
2589
2753
|
}
|
|
2590
|
-
log('╚══════════════════════════════════════════════════════════════╝');
|
|
2591
2754
|
|
|
2592
|
-
// ── Missed Activity Digest ──
|
|
2755
|
+
// ── Missed Activity Digest (always show) ──
|
|
2593
2756
|
if (b.missed) {
|
|
2594
2757
|
const parts = [];
|
|
2595
2758
|
if (b.missed.messages) parts.push(`${b.missed.messages} msgs`);
|
|
@@ -2616,14 +2779,23 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2616
2779
|
log(`Notifications: ${parts.join(' · ')}`);
|
|
2617
2780
|
}
|
|
2618
2781
|
|
|
2619
|
-
// ── Rooms ──
|
|
2620
|
-
if (b.rooms && b.rooms.length > 0) {
|
|
2782
|
+
// ── Rooms (compact in resume mode, full otherwise) ──
|
|
2783
|
+
if (resumeMode && b.rooms && b.rooms.length > 0) {
|
|
2784
|
+
// Compact room listing with unread counts only
|
|
2785
|
+
log('');
|
|
2786
|
+
const roomSummary = b.rooms.map(r => {
|
|
2787
|
+
const unread = r.unread_count || 0;
|
|
2788
|
+
return `${r.name}${unread > 0 ? ` (${unread} unread)` : ''}`;
|
|
2789
|
+
}).join(' · ');
|
|
2790
|
+
log(`Rooms: ${roomSummary}`);
|
|
2791
|
+
} else if (b.rooms && b.rooms.length > 0) {
|
|
2621
2792
|
log('');
|
|
2622
2793
|
log('┌── Rooms ──────────────────────────────────────────────────────');
|
|
2623
2794
|
for (const r of b.rooms) {
|
|
2624
2795
|
log(`│`);
|
|
2625
2796
|
const activityStr = r.last_activity_ago != null ? ` · active ${fmtAge(r.last_activity_ago)}` : '';
|
|
2626
|
-
|
|
2797
|
+
const unreadBadge = r.unread_count > 0 ? ` [${r.unread_count} unread]` : '';
|
|
2798
|
+
log(`├─ ${r.name} (${r.role})${unreadBadge}${activityStr} — ${r.id}`);
|
|
2627
2799
|
if (r.description) log(`│ ${r.description}`);
|
|
2628
2800
|
|
|
2629
2801
|
// Teammates status board
|
|
@@ -2694,8 +2866,8 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2694
2866
|
log('└────────────────────────────────────────────────────────────────');
|
|
2695
2867
|
}
|
|
2696
2868
|
|
|
2697
|
-
// ── Open Tasks ──
|
|
2698
|
-
if (b.orders && b.orders.length > 0) {
|
|
2869
|
+
// ── Open Tasks (skip in resume mode) ──
|
|
2870
|
+
if (!resumeMode && b.orders && b.orders.length > 0) {
|
|
2699
2871
|
log('');
|
|
2700
2872
|
log('┌── Open Tasks ─────────────────────────────────────────────────');
|
|
2701
2873
|
for (const t of b.orders) {
|
|
@@ -2707,8 +2879,8 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2707
2879
|
log('└────────────────────────────────────────────────────────────────');
|
|
2708
2880
|
}
|
|
2709
2881
|
|
|
2710
|
-
// ── Scheduled Messages ──
|
|
2711
|
-
if (b.scheduled && b.scheduled.length > 0) {
|
|
2882
|
+
// ── Scheduled Messages (skip in resume mode) ──
|
|
2883
|
+
if (!resumeMode && b.scheduled && b.scheduled.length > 0) {
|
|
2712
2884
|
log('');
|
|
2713
2885
|
log('┌── Scheduled ──────────────────────────────────────────────────');
|
|
2714
2886
|
for (const s of b.scheduled) {
|
|
@@ -2717,8 +2889,8 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2717
2889
|
log('└────────────────────────────────────────────────────────────────');
|
|
2718
2890
|
}
|
|
2719
2891
|
|
|
2720
|
-
// ── Active Webhooks ──
|
|
2721
|
-
if (b.webhooks && b.webhooks.length > 0) {
|
|
2892
|
+
// ── Active Webhooks (skip in resume mode) ──
|
|
2893
|
+
if (!resumeMode && b.webhooks && b.webhooks.length > 0) {
|
|
2722
2894
|
log('');
|
|
2723
2895
|
log('┌── Webhooks ───────────────────────────────────────────────────');
|
|
2724
2896
|
for (const wh of b.webhooks) {
|
|
@@ -2729,8 +2901,8 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2729
2901
|
log('└────────────────────────────────────────────────────────────────');
|
|
2730
2902
|
}
|
|
2731
2903
|
|
|
2732
|
-
// ── Config ──
|
|
2733
|
-
if (b.config && Object.keys(b.config).length > 0) {
|
|
2904
|
+
// ── Config (skip in resume mode) ──
|
|
2905
|
+
if (!resumeMode && b.config && Object.keys(b.config).length > 0) {
|
|
2734
2906
|
log('');
|
|
2735
2907
|
log('┌── Config ─────────────────────────────────────────────────────');
|
|
2736
2908
|
for (const [k, v] of Object.entries(b.config)) {
|
|
@@ -2739,8 +2911,8 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2739
2911
|
log('└────────────────────────────────────────────────────────────────');
|
|
2740
2912
|
}
|
|
2741
2913
|
|
|
2742
|
-
// ── Changelog ──
|
|
2743
|
-
if (b.changelog && b.changelog.length > 0) {
|
|
2914
|
+
// ── Changelog (skip in resume mode) ──
|
|
2915
|
+
if (!resumeMode && b.changelog && b.changelog.length > 0) {
|
|
2744
2916
|
log('');
|
|
2745
2917
|
log('┌── Recent Updates ─────────────────────────────────────────────');
|
|
2746
2918
|
for (const entry of b.changelog.slice(0, 3)) {
|
|
@@ -2958,7 +3130,16 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2958
3130
|
// Alive ping every 60s so parent process knows we're polling
|
|
2959
3131
|
if (!lastKeepalive) lastKeepalive = Date.now();
|
|
2960
3132
|
if (Date.now() - lastKeepalive >= 60000) { // 60s
|
|
2961
|
-
|
|
3133
|
+
// Time since last break
|
|
3134
|
+
const lastBreakState = loadState();
|
|
3135
|
+
let breakAgo = '';
|
|
3136
|
+
if (lastBreakState.last_break_at) {
|
|
3137
|
+
const diff = Math.floor((Date.now() - new Date(lastBreakState.last_break_at).getTime()) / 1000);
|
|
3138
|
+
if (diff < 3600) breakAgo = ` | last break ${Math.floor(diff / 60)}m ago`;
|
|
3139
|
+
else if (diff < 86400) breakAgo = ` | last break ${Math.floor(diff / 3600)}h ago`;
|
|
3140
|
+
else breakAgo = ` | last break ${Math.floor(diff / 86400)}d ago`;
|
|
3141
|
+
}
|
|
3142
|
+
log(`--- ${statusMode} | #${cycle} | ${new Date().toLocaleTimeString()}${breakAgo} ---`);
|
|
2962
3143
|
lastKeepalive = Date.now();
|
|
2963
3144
|
}
|
|
2964
3145
|
} else if (showMode) {
|
|
@@ -3037,7 +3218,14 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
3037
3218
|
log('');
|
|
3038
3219
|
}
|
|
3039
3220
|
|
|
3040
|
-
|
|
3221
|
+
// ── Batch action summary header ──
|
|
3222
|
+
const typeCounts = {};
|
|
3223
|
+
for (const a of allToProcess) typeCounts[a.type] = (typeCounts[a.type] || 0) + 1;
|
|
3224
|
+
const summary = Object.entries(typeCounts).map(([t, c]) => {
|
|
3225
|
+
const label = t.replace(/_/g, ' ').replace('direct message', 'DM');
|
|
3226
|
+
return `${c} ${label}${c > 1 ? '' : ''}`;
|
|
3227
|
+
}).join(', ');
|
|
3228
|
+
log(`BREAK | ${summary}${hasBoss ? ' [BOSS]' : ''}`);
|
|
3041
3229
|
|
|
3042
3230
|
const realCount = await processActions(allToProcess, data, args, roomsFilter);
|
|
3043
3231
|
|
|
@@ -3059,9 +3247,12 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
3059
3247
|
|
|
3060
3248
|
brokeOnAction = true;
|
|
3061
3249
|
|
|
3250
|
+
// Save pre-break status for restoration on restart
|
|
3251
|
+
saveState({ pre_break_status_text: statusText, last_break_at: new Date().toISOString() });
|
|
3252
|
+
|
|
3062
3253
|
// Tell parent how to restart (not in auto-restart mode)
|
|
3063
3254
|
if (!autoRestart) {
|
|
3064
|
-
const cmd = buildRestartCommand(args, savedConfig);
|
|
3255
|
+
const cmd = buildRestartCommand(args, savedConfig) + ' --resume';
|
|
3065
3256
|
console.log('RESTART:' + cmd);
|
|
3066
3257
|
log('');
|
|
3067
3258
|
log('#####################################################################');
|
|
@@ -3230,6 +3421,8 @@ async function main() {
|
|
|
3230
3421
|
case 'members': return cmdMembers(subArgs);
|
|
3231
3422
|
case 'wiki': return cmdWiki(subArgs);
|
|
3232
3423
|
case 'files': return cmdFiles(subArgs);
|
|
3424
|
+
case 'upload': return cmdUpload(subArgs);
|
|
3425
|
+
case 'download': return cmdDownload(subArgs);
|
|
3233
3426
|
case 'pin': return cmdPin(subArgs);
|
|
3234
3427
|
case 'pinned': return cmdPinned(subArgs);
|
|
3235
3428
|
case 'invite': return cmdInvite(subArgs);
|