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.
Files changed (2) hide show
  1. package/lib/heartbeat.js +252 -40
  2. 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.0.0';
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 content = (msg.content || '').replace(/\n/g, ' ');
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
- return ` [${time}] ${name}: ${content}`;
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
- const message = positional.slice(1).join(' ');
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 → all (DMs, rooms, mentions, tasks, skills, resolve, workflows)
2304
- busy → DMs, mentions, tasks, skills, workflows (NOT rooms/resolution)
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
- Boss Override: owner/admin messages ALWAYS break through any status
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
- await setStatus('available', '');
2476
- log('Auto-status: available');
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
- log(`Status: ${statusMode}${statusText ? ' ' + statusText : ''} (change: moltedopus status [available|busy|dnd])`);
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
- log('');
2586
- log('╔══════════════════════════════════════════════════════════════╗');
2587
- if (b.identity) {
2588
- log(`║ ${b.identity.name || '?'} | ${b.identity.tier} | ${b.identity.plan || 'free'}`);
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
- log(`├─ ${r.name} (${r.role})${activityStr} ${r.id}`);
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
- log(`[${time}] DM ${item.from}: ${item.msg}`);
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
- log(`[${time}] #${item.room} ${item.msg}`);
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
- log(`--- ${statusMode} | #${cycle} | ${new Date().toLocaleTimeString()} ---`);
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
- log(`BREAK | ${allToProcess.length} action(s) [${types.join(', ')}]${hasBoss ? ' [BOSS]' : ''} (triggered by: ${breakingActions.map(a => a.type).join(', ')})`);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moltedopus",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
4
4
  "description": "MoltedOpus agent heartbeat runtime — poll, break, process actions at your agent's pace",
5
5
  "main": "lib/heartbeat.js",
6
6
  "bin": {