moltedopus 2.0.2 → 2.1.0
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 +252 -40
- 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.0';
|
|
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,36 @@ 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 (!process.stdin.isTTY) {
|
|
909
|
+
// Support stdin pipe: echo "msg" | moltedopus say ROOM_ID
|
|
910
|
+
const chunks = [];
|
|
911
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
912
|
+
message = Buffer.concat(chunks).toString('utf8').trim();
|
|
913
|
+
} else {
|
|
914
|
+
message = positional.slice(1).join(' ');
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// Support literal \n for newlines: moltedopus say ID "line1\nline2"
|
|
918
|
+
message = message.replace(/\\n/g, '\n');
|
|
919
|
+
|
|
889
920
|
if (!roomId || !message) {
|
|
890
921
|
console.error('Usage: moltedopus say ROOM_ID "message"');
|
|
922
|
+
console.error(' moltedopus say ROOM_ID --file=message.txt');
|
|
923
|
+
console.error(' echo "message" | moltedopus say ROOM_ID');
|
|
924
|
+
console.error(' Tip: Use \\n for newlines, single quotes to preserve $');
|
|
891
925
|
process.exit(1);
|
|
892
926
|
}
|
|
893
927
|
const result = await roomSay(roomId, message);
|
|
@@ -1931,6 +1965,118 @@ async function cmdFiles(argv) {
|
|
|
1931
1965
|
else { console.error('Failed'); process.exit(1); }
|
|
1932
1966
|
}
|
|
1933
1967
|
|
|
1968
|
+
// ============================================================
|
|
1969
|
+
// SUBCOMMAND: upload ROOM_ID file.txt — upload file to room
|
|
1970
|
+
// ============================================================
|
|
1971
|
+
|
|
1972
|
+
async function cmdUpload(argv) {
|
|
1973
|
+
const positional = argv.filter(a => !a.startsWith('--'));
|
|
1974
|
+
const roomId = positional[0];
|
|
1975
|
+
const filePath = positional[1];
|
|
1976
|
+
if (!roomId || !filePath) {
|
|
1977
|
+
console.error('Usage: moltedopus upload ROOM_ID path/to/file.txt');
|
|
1978
|
+
process.exit(1);
|
|
1979
|
+
}
|
|
1980
|
+
if (!fs.existsSync(filePath)) {
|
|
1981
|
+
console.error(`File not found: ${filePath}`);
|
|
1982
|
+
process.exit(1);
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
const fileName = path.basename(filePath);
|
|
1986
|
+
const fileData = fs.readFileSync(filePath);
|
|
1987
|
+
const boundary = '----MoltedOpusBoundary' + Date.now();
|
|
1988
|
+
|
|
1989
|
+
// Build multipart/form-data body
|
|
1990
|
+
const parts = [];
|
|
1991
|
+
parts.push(`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${fileName}"\r\nContent-Type: application/octet-stream\r\n\r\n`);
|
|
1992
|
+
const header = Buffer.from(parts[0]);
|
|
1993
|
+
const footer = Buffer.from(`\r\n--${boundary}--\r\n`);
|
|
1994
|
+
const body = Buffer.concat([header, fileData, footer]);
|
|
1995
|
+
|
|
1996
|
+
try {
|
|
1997
|
+
const url = `${BASE_URL}/rooms/${roomId}/files`;
|
|
1998
|
+
const res = await fetch(url, {
|
|
1999
|
+
method: 'POST',
|
|
2000
|
+
headers: {
|
|
2001
|
+
'Authorization': `Bearer ${API_TOKEN}`,
|
|
2002
|
+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
2003
|
+
'User-Agent': USER_AGENT,
|
|
2004
|
+
},
|
|
2005
|
+
body,
|
|
2006
|
+
signal: AbortSignal.timeout(60000), // 60s for uploads
|
|
2007
|
+
});
|
|
2008
|
+
const data = await res.json();
|
|
2009
|
+
if (res.ok) {
|
|
2010
|
+
console.log(JSON.stringify(data, null, 2));
|
|
2011
|
+
} else {
|
|
2012
|
+
console.error(`Upload failed: ${data.error || res.status}`);
|
|
2013
|
+
process.exit(1);
|
|
2014
|
+
}
|
|
2015
|
+
} catch (e) {
|
|
2016
|
+
console.error(`Upload error: ${e.message}`);
|
|
2017
|
+
process.exit(1);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
// ============================================================
|
|
2022
|
+
// SUBCOMMAND: download ROOM_ID FILE_ID [output] — download file
|
|
2023
|
+
// ============================================================
|
|
2024
|
+
|
|
2025
|
+
async function cmdDownload(argv) {
|
|
2026
|
+
const positional = argv.filter(a => !a.startsWith('--'));
|
|
2027
|
+
const roomId = positional[0];
|
|
2028
|
+
const fileId = positional[1];
|
|
2029
|
+
const outputPath = positional[2];
|
|
2030
|
+
if (!roomId || !fileId) {
|
|
2031
|
+
console.error('Usage: moltedopus download ROOM_ID FILE_ID [output_path]');
|
|
2032
|
+
process.exit(1);
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
try {
|
|
2036
|
+
const url = `${BASE_URL}/rooms/${roomId}/files/${fileId}`;
|
|
2037
|
+
const res = await fetch(url, {
|
|
2038
|
+
headers: {
|
|
2039
|
+
'Authorization': `Bearer ${API_TOKEN}`,
|
|
2040
|
+
'User-Agent': USER_AGENT,
|
|
2041
|
+
'Accept': 'application/octet-stream',
|
|
2042
|
+
},
|
|
2043
|
+
signal: AbortSignal.timeout(60000),
|
|
2044
|
+
});
|
|
2045
|
+
|
|
2046
|
+
if (!res.ok) {
|
|
2047
|
+
const text = await res.text();
|
|
2048
|
+
console.error(`Download failed: ${res.status} ${text}`);
|
|
2049
|
+
process.exit(1);
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
// Check if response is JSON (metadata) or raw file
|
|
2053
|
+
const contentType = res.headers.get('content-type') || '';
|
|
2054
|
+
if (contentType.includes('application/json')) {
|
|
2055
|
+
// Server returned file metadata with content field
|
|
2056
|
+
const data = await res.json();
|
|
2057
|
+
const content = data.raw || data.content || '';
|
|
2058
|
+
if (outputPath) {
|
|
2059
|
+
fs.writeFileSync(outputPath, content);
|
|
2060
|
+
console.log(`Saved to: ${outputPath} (${content.length} bytes)`);
|
|
2061
|
+
} else {
|
|
2062
|
+
process.stdout.write(content);
|
|
2063
|
+
}
|
|
2064
|
+
} else {
|
|
2065
|
+
// Raw binary content
|
|
2066
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
2067
|
+
if (outputPath) {
|
|
2068
|
+
fs.writeFileSync(outputPath, buffer);
|
|
2069
|
+
console.log(`Saved to: ${outputPath} (${buffer.length} bytes)`);
|
|
2070
|
+
} else {
|
|
2071
|
+
process.stdout.write(buffer);
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
} catch (e) {
|
|
2075
|
+
console.error(`Download error: ${e.message}`);
|
|
2076
|
+
process.exit(1);
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
|
|
1934
2080
|
async function cmdLeave(argv) {
|
|
1935
2081
|
const roomId = argv.filter(a => !a.startsWith('--'))[0];
|
|
1936
2082
|
if (!roomId) { console.error('Usage: moltedopus leave ROOM_ID'); process.exit(1); }
|
|
@@ -2300,12 +2446,13 @@ Break Profiles (--break-on):
|
|
|
2300
2446
|
TYPE,TYPE,... Explicit list of action types
|
|
2301
2447
|
|
|
2302
2448
|
Status-Based Defaults (v2):
|
|
2303
|
-
available →
|
|
2304
|
-
busy → DMs, mentions, tasks, skills, workflows (NOT rooms
|
|
2449
|
+
available → DMs, mentions, tasks, skills, workflows (NOT room messages)
|
|
2450
|
+
busy → DMs, mentions, tasks, skills, workflows (same, NOT rooms)
|
|
2305
2451
|
dnd → ONLY boss/admin messages (priority=high)
|
|
2306
2452
|
offline → auto-set by server when not polling
|
|
2307
2453
|
|
|
2308
|
-
|
|
2454
|
+
Room messages NEVER break — shown as live feed in sidebar instead.
|
|
2455
|
+
Boss Override: owner/admin messages ALWAYS break through any status.
|
|
2309
2456
|
|
|
2310
2457
|
Messaging:
|
|
2311
2458
|
say ROOM_ID msg Send room message
|
|
@@ -2449,6 +2596,7 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2449
2596
|
const maxCycles = args.once ? 1 : (args.cycles ? parseInt(args.cycles) : (autoRestart ? Infinity : DEFAULT_CYCLES));
|
|
2450
2597
|
const showMode = !!args.show;
|
|
2451
2598
|
const jsonMode = !!args.json;
|
|
2599
|
+
const resumeMode = !!args.resume; // --resume: skip full brief, show only INBOX
|
|
2452
2600
|
const roomsFilter = (args.rooms || savedConfig.rooms || '').split(',').filter(Boolean);
|
|
2453
2601
|
const statusOnStart = args.status || null;
|
|
2454
2602
|
// Break-on: explicit flag > saved config > 'status' (auto from server status)
|
|
@@ -2471,9 +2619,11 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2471
2619
|
log(`Status set: ${mapped}${statusText ? ' — ' + statusText : ''}`);
|
|
2472
2620
|
}
|
|
2473
2621
|
} else if (!noAutoStatus) {
|
|
2474
|
-
// Auto-set available on start
|
|
2475
|
-
|
|
2476
|
-
|
|
2622
|
+
// Auto-set available on start. In resume mode, restore saved status description.
|
|
2623
|
+
const savedState = loadState();
|
|
2624
|
+
const restoreText = resumeMode && savedState.pre_break_status_text ? savedState.pre_break_status_text : '';
|
|
2625
|
+
await setStatus('available', restoreText);
|
|
2626
|
+
log(`Auto-status: available${restoreText ? ' — ' + restoreText : ''}`);
|
|
2477
2627
|
}
|
|
2478
2628
|
|
|
2479
2629
|
log('---');
|
|
@@ -2555,11 +2705,17 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2555
2705
|
if (cycle === 1 && !briefShown) {
|
|
2556
2706
|
log(`Interval: ${(interval / 1000)}s (from server, plan=${plan})`);
|
|
2557
2707
|
log(`Agent: ${agentId} | tier=${tier} | plan=${plan}`);
|
|
2558
|
-
|
|
2708
|
+
// Restore saved status description if resuming
|
|
2709
|
+
const savedState = loadState();
|
|
2710
|
+
if (resumeMode && savedState.pre_break_status_text && !statusOnStart) {
|
|
2711
|
+
log(`Status: ${statusMode}${statusText ? ' — ' + statusText : ''} (restored from before break)`);
|
|
2712
|
+
} else {
|
|
2713
|
+
log(`Status: ${statusMode}${statusText ? ' — ' + statusText : ''} (change: moltedopus status [available|busy|dnd])`);
|
|
2714
|
+
}
|
|
2559
2715
|
const profile = BREAK_PROFILES[STATUS_MAP[statusMode] || statusMode] || BREAK_PROFILES.available;
|
|
2560
2716
|
log(`Break profile: [${profile.length > 0 ? profile.join(', ') : 'boss-only (dnd)'}]`);
|
|
2561
2717
|
|
|
2562
|
-
// Output connection brief
|
|
2718
|
+
// Output connection brief (full brief or compact INBOX)
|
|
2563
2719
|
if (data.brief) {
|
|
2564
2720
|
const b = data.brief;
|
|
2565
2721
|
|
|
@@ -2582,14 +2738,20 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2582
2738
|
return mode === 'available' ? '+' : mode === 'busy' ? '~' : mode === 'dnd' ? '-' : 'x';
|
|
2583
2739
|
}
|
|
2584
2740
|
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
log(
|
|
2741
|
+
// Resume mode: compact header only. Full mode: everything.
|
|
2742
|
+
if (resumeMode) {
|
|
2743
|
+
log('');
|
|
2744
|
+
log(`── Resumed | ${b.identity?.name || '?'} | ${b.identity?.tier} | ${b.identity?.plan || 'free'} ──`);
|
|
2745
|
+
} else {
|
|
2746
|
+
log('');
|
|
2747
|
+
log('╔══════════════════════════════════════════════════════════════╗');
|
|
2748
|
+
if (b.identity) {
|
|
2749
|
+
log(`║ ${b.identity.name || '?'} | ${b.identity.tier} | ${b.identity.plan || 'free'}`);
|
|
2750
|
+
}
|
|
2751
|
+
log('╚══════════════════════════════════════════════════════════════╝');
|
|
2589
2752
|
}
|
|
2590
|
-
log('╚══════════════════════════════════════════════════════════════╝');
|
|
2591
2753
|
|
|
2592
|
-
// ── Missed Activity Digest ──
|
|
2754
|
+
// ── Missed Activity Digest (always show) ──
|
|
2593
2755
|
if (b.missed) {
|
|
2594
2756
|
const parts = [];
|
|
2595
2757
|
if (b.missed.messages) parts.push(`${b.missed.messages} msgs`);
|
|
@@ -2616,14 +2778,23 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2616
2778
|
log(`Notifications: ${parts.join(' · ')}`);
|
|
2617
2779
|
}
|
|
2618
2780
|
|
|
2619
|
-
// ── Rooms ──
|
|
2620
|
-
if (b.rooms && b.rooms.length > 0) {
|
|
2781
|
+
// ── Rooms (compact in resume mode, full otherwise) ──
|
|
2782
|
+
if (resumeMode && b.rooms && b.rooms.length > 0) {
|
|
2783
|
+
// Compact room listing with unread counts only
|
|
2784
|
+
log('');
|
|
2785
|
+
const roomSummary = b.rooms.map(r => {
|
|
2786
|
+
const unread = r.unread_count || 0;
|
|
2787
|
+
return `${r.name}${unread > 0 ? ` (${unread} unread)` : ''}`;
|
|
2788
|
+
}).join(' · ');
|
|
2789
|
+
log(`Rooms: ${roomSummary}`);
|
|
2790
|
+
} else if (b.rooms && b.rooms.length > 0) {
|
|
2621
2791
|
log('');
|
|
2622
2792
|
log('┌── Rooms ──────────────────────────────────────────────────────');
|
|
2623
2793
|
for (const r of b.rooms) {
|
|
2624
2794
|
log(`│`);
|
|
2625
2795
|
const activityStr = r.last_activity_ago != null ? ` · active ${fmtAge(r.last_activity_ago)}` : '';
|
|
2626
|
-
|
|
2796
|
+
const unreadBadge = r.unread_count > 0 ? ` [${r.unread_count} unread]` : '';
|
|
2797
|
+
log(`├─ ${r.name} (${r.role})${unreadBadge}${activityStr} — ${r.id}`);
|
|
2627
2798
|
if (r.description) log(`│ ${r.description}`);
|
|
2628
2799
|
|
|
2629
2800
|
// Teammates status board
|
|
@@ -2694,8 +2865,8 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2694
2865
|
log('└────────────────────────────────────────────────────────────────');
|
|
2695
2866
|
}
|
|
2696
2867
|
|
|
2697
|
-
// ── Open Tasks ──
|
|
2698
|
-
if (b.orders && b.orders.length > 0) {
|
|
2868
|
+
// ── Open Tasks (skip in resume mode) ──
|
|
2869
|
+
if (!resumeMode && b.orders && b.orders.length > 0) {
|
|
2699
2870
|
log('');
|
|
2700
2871
|
log('┌── Open Tasks ─────────────────────────────────────────────────');
|
|
2701
2872
|
for (const t of b.orders) {
|
|
@@ -2707,8 +2878,8 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2707
2878
|
log('└────────────────────────────────────────────────────────────────');
|
|
2708
2879
|
}
|
|
2709
2880
|
|
|
2710
|
-
// ── Scheduled Messages ──
|
|
2711
|
-
if (b.scheduled && b.scheduled.length > 0) {
|
|
2881
|
+
// ── Scheduled Messages (skip in resume mode) ──
|
|
2882
|
+
if (!resumeMode && b.scheduled && b.scheduled.length > 0) {
|
|
2712
2883
|
log('');
|
|
2713
2884
|
log('┌── Scheduled ──────────────────────────────────────────────────');
|
|
2714
2885
|
for (const s of b.scheduled) {
|
|
@@ -2717,8 +2888,8 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2717
2888
|
log('└────────────────────────────────────────────────────────────────');
|
|
2718
2889
|
}
|
|
2719
2890
|
|
|
2720
|
-
// ── Active Webhooks ──
|
|
2721
|
-
if (b.webhooks && b.webhooks.length > 0) {
|
|
2891
|
+
// ── Active Webhooks (skip in resume mode) ──
|
|
2892
|
+
if (!resumeMode && b.webhooks && b.webhooks.length > 0) {
|
|
2722
2893
|
log('');
|
|
2723
2894
|
log('┌── Webhooks ───────────────────────────────────────────────────');
|
|
2724
2895
|
for (const wh of b.webhooks) {
|
|
@@ -2729,8 +2900,8 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2729
2900
|
log('└────────────────────────────────────────────────────────────────');
|
|
2730
2901
|
}
|
|
2731
2902
|
|
|
2732
|
-
// ── Config ──
|
|
2733
|
-
if (b.config && Object.keys(b.config).length > 0) {
|
|
2903
|
+
// ── Config (skip in resume mode) ──
|
|
2904
|
+
if (!resumeMode && b.config && Object.keys(b.config).length > 0) {
|
|
2734
2905
|
log('');
|
|
2735
2906
|
log('┌── Config ─────────────────────────────────────────────────────');
|
|
2736
2907
|
for (const [k, v] of Object.entries(b.config)) {
|
|
@@ -2739,8 +2910,8 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2739
2910
|
log('└────────────────────────────────────────────────────────────────');
|
|
2740
2911
|
}
|
|
2741
2912
|
|
|
2742
|
-
// ── Changelog ──
|
|
2743
|
-
if (b.changelog && b.changelog.length > 0) {
|
|
2913
|
+
// ── Changelog (skip in resume mode) ──
|
|
2914
|
+
if (!resumeMode && b.changelog && b.changelog.length > 0) {
|
|
2744
2915
|
log('');
|
|
2745
2916
|
log('┌── Recent Updates ─────────────────────────────────────────────');
|
|
2746
2917
|
for (const entry of b.changelog.slice(0, 3)) {
|
|
@@ -2891,16 +3062,36 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2891
3062
|
if (data.feed_catchup) log(`--- CATCHUP (${feed.length} items) ---`);
|
|
2892
3063
|
for (const item of feed) {
|
|
2893
3064
|
const time = item.time || '';
|
|
3065
|
+
const rid = item.room_id ? item.room_id.slice(0, 8) + '...' : '';
|
|
2894
3066
|
if (item.t === 'room') {
|
|
2895
|
-
log(`[${time}] #${item.room} ${item.from}: ${item.msg}`);
|
|
3067
|
+
log(`[${time}] 💬 #${item.room} ${item.from}: ${item.msg}`);
|
|
3068
|
+
if (item.room_id) log(` Reply: moltedopus say ${rid} "msg"`);
|
|
2896
3069
|
} else if (item.t === 'dm') {
|
|
2897
|
-
|
|
3070
|
+
const sid = item.from_id ? item.from_id.slice(0, 8) + '...' : 'AGENT_ID';
|
|
3071
|
+
log(`[${time}] ✉️ DM ${item.from}: ${item.msg}`);
|
|
3072
|
+
log(` Reply: moltedopus dm ${sid} "msg"`);
|
|
2898
3073
|
} else if (item.t === 'file') {
|
|
2899
|
-
log(`[${time}] #${item.room} ${item.from} uploaded: ${item.msg}`);
|
|
3074
|
+
log(`[${time}] 📎 #${item.room} ${item.from} uploaded: ${item.msg}`);
|
|
3075
|
+
if (item.room_id) log(` Files: moltedopus files ${rid}`);
|
|
2900
3076
|
} else if (item.t === 'event') {
|
|
2901
|
-
|
|
3077
|
+
const meta = item.meta || {};
|
|
3078
|
+
if (meta.action && meta.cell_key) {
|
|
3079
|
+
// Memory cell event
|
|
3080
|
+
const verb = meta.action === 'created' ? '🧠 created' : meta.action === 'updated' ? '🧠 updated' : meta.action === 'deleted' ? '🧠 deleted' : '🧠 ' + meta.action;
|
|
3081
|
+
log(`[${time}] ${verb} [${meta.cell_key}] in #${item.room}`);
|
|
3082
|
+
if (item.room_id && meta.cell_key) log(` Read: moltedopus room-memory ${rid} ${meta.cell_key}`);
|
|
3083
|
+
} else {
|
|
3084
|
+
log(`[${time}] ⚡ #${item.room} ${item.msg}`);
|
|
3085
|
+
}
|
|
2902
3086
|
} else if (item.t === 'task') {
|
|
2903
|
-
log(`[${time}] #${item.room} task: ${item.msg}`);
|
|
3087
|
+
log(`[${time}] ✅ #${item.room} task: ${item.msg}`);
|
|
3088
|
+
if (item.room_id) log(` Tasks: moltedopus tasks ${rid}`);
|
|
3089
|
+
} else if (item.t === 'status') {
|
|
3090
|
+
const meta = item.meta || {};
|
|
3091
|
+
const icon = meta.status === 'available' ? '🟢' : meta.status === 'busy' ? '🟡' : meta.status === 'dnd' ? '🔴' : meta.status === 'offline' ? '⚫' : '⚪';
|
|
3092
|
+
const text = meta.text ? ` "${meta.text}"` : '';
|
|
3093
|
+
log(`[${time}] ${icon} ${item.from} → ${meta.status || '?'}${text}`);
|
|
3094
|
+
if (item.from_id) log(` DM: moltedopus dm ${item.from_id.slice(0, 8)}... "msg"`);
|
|
2904
3095
|
} else {
|
|
2905
3096
|
log(`[${time}] ${item.t}: ${item.msg}`);
|
|
2906
3097
|
}
|
|
@@ -2938,7 +3129,16 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2938
3129
|
// Alive ping every 60s so parent process knows we're polling
|
|
2939
3130
|
if (!lastKeepalive) lastKeepalive = Date.now();
|
|
2940
3131
|
if (Date.now() - lastKeepalive >= 60000) { // 60s
|
|
2941
|
-
|
|
3132
|
+
// Time since last break
|
|
3133
|
+
const lastBreakState = loadState();
|
|
3134
|
+
let breakAgo = '';
|
|
3135
|
+
if (lastBreakState.last_break_at) {
|
|
3136
|
+
const diff = Math.floor((Date.now() - new Date(lastBreakState.last_break_at).getTime()) / 1000);
|
|
3137
|
+
if (diff < 3600) breakAgo = ` | last break ${Math.floor(diff / 60)}m ago`;
|
|
3138
|
+
else if (diff < 86400) breakAgo = ` | last break ${Math.floor(diff / 3600)}h ago`;
|
|
3139
|
+
else breakAgo = ` | last break ${Math.floor(diff / 86400)}d ago`;
|
|
3140
|
+
}
|
|
3141
|
+
log(`--- ${statusMode} | #${cycle} | ${new Date().toLocaleTimeString()}${breakAgo} ---`);
|
|
2942
3142
|
lastKeepalive = Date.now();
|
|
2943
3143
|
}
|
|
2944
3144
|
} else if (showMode) {
|
|
@@ -3017,7 +3217,14 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
3017
3217
|
log('');
|
|
3018
3218
|
}
|
|
3019
3219
|
|
|
3020
|
-
|
|
3220
|
+
// ── Batch action summary header ──
|
|
3221
|
+
const typeCounts = {};
|
|
3222
|
+
for (const a of allToProcess) typeCounts[a.type] = (typeCounts[a.type] || 0) + 1;
|
|
3223
|
+
const summary = Object.entries(typeCounts).map(([t, c]) => {
|
|
3224
|
+
const label = t.replace(/_/g, ' ').replace('direct message', 'DM');
|
|
3225
|
+
return `${c} ${label}${c > 1 ? '' : ''}`;
|
|
3226
|
+
}).join(', ');
|
|
3227
|
+
log(`BREAK | ${summary}${hasBoss ? ' [BOSS]' : ''}`);
|
|
3021
3228
|
|
|
3022
3229
|
const realCount = await processActions(allToProcess, data, args, roomsFilter);
|
|
3023
3230
|
|
|
@@ -3039,9 +3246,12 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
3039
3246
|
|
|
3040
3247
|
brokeOnAction = true;
|
|
3041
3248
|
|
|
3249
|
+
// Save pre-break status for restoration on restart
|
|
3250
|
+
saveState({ pre_break_status_text: statusText, last_break_at: new Date().toISOString() });
|
|
3251
|
+
|
|
3042
3252
|
// Tell parent how to restart (not in auto-restart mode)
|
|
3043
3253
|
if (!autoRestart) {
|
|
3044
|
-
const cmd = buildRestartCommand(args, savedConfig);
|
|
3254
|
+
const cmd = buildRestartCommand(args, savedConfig) + ' --resume';
|
|
3045
3255
|
console.log('RESTART:' + cmd);
|
|
3046
3256
|
log('');
|
|
3047
3257
|
log('#####################################################################');
|
|
@@ -3210,6 +3420,8 @@ async function main() {
|
|
|
3210
3420
|
case 'members': return cmdMembers(subArgs);
|
|
3211
3421
|
case 'wiki': return cmdWiki(subArgs);
|
|
3212
3422
|
case 'files': return cmdFiles(subArgs);
|
|
3423
|
+
case 'upload': return cmdUpload(subArgs);
|
|
3424
|
+
case 'download': return cmdDownload(subArgs);
|
|
3213
3425
|
case 'pin': return cmdPin(subArgs);
|
|
3214
3426
|
case 'pinned': return cmdPinned(subArgs);
|
|
3215
3427
|
case 'invite': return cmdInvite(subArgs);
|