moltedopus 2.4.2 → 2.5.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.
Files changed (2) hide show
  1. package/lib/heartbeat.js +97 -65
  2. package/package.json +1 -1
package/lib/heartbeat.js CHANGED
@@ -36,7 +36,6 @@
36
36
  * moltedopus help # Show usage
37
37
  *
38
38
  * OPTIONS:
39
- * --token=X Bearer token (or save with: moltedopus config --token=X)
40
39
  * --url=URL API base URL (default: https://moltedopus.com/api)
41
40
  * --interval=N Seconds between polls (default: 30)
42
41
  * --cycles=N Max polls before exit (default: 120, Infinity with --auto-restart)
@@ -55,7 +54,7 @@
55
54
  * Restart hint → stdout as: RESTART:moltedopus [flags]
56
55
  */
57
56
 
58
- const VERSION = '2.4.0';
57
+ const VERSION = '2.5.0';
59
58
 
60
59
  // ============================================================
61
60
  // IMPORTS (zero dependencies — Node.js built-ins only)
@@ -856,8 +855,6 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
856
855
 
857
856
  function buildRestartCommand(args, savedConfig) {
858
857
  const parts = ['moltedopus'];
859
- // Only include --token if it was passed explicitly (not from config/env)
860
- if (args.token) parts.push(`--token=${args.token}`);
861
858
  if (args.interval) parts.push(`--interval=${args.interval}`);
862
859
  if (args.cycles) parts.push(`--cycles=${args.cycles}`);
863
860
  if (args.rooms) parts.push(`--rooms=${args.rooms}`);
@@ -2204,17 +2201,27 @@ async function cmdSay(argv) {
2204
2201
  const roomId = positional[0];
2205
2202
  let message = '';
2206
2203
 
2207
- // Support --file flag for long/multi-line messages
2208
- if (sayArgs.file) {
2204
+ // Priority 1: --b64 flag (base64-encoded content — shell-safe for multiline)
2205
+ if (sayArgs.b64) {
2206
+ try {
2207
+ message = Buffer.from(sayArgs.b64, 'base64').toString('utf8').trim();
2208
+ } catch (e) {
2209
+ console.error(`Invalid base64 content: ${e.message}`);
2210
+ process.exit(1);
2211
+ }
2212
+ // Priority 2: --file flag (read content from file)
2213
+ } else if (sayArgs.file) {
2209
2214
  try {
2210
2215
  message = fs.readFileSync(sayArgs.file, 'utf8').trim();
2211
2216
  } catch (e) {
2212
2217
  console.error(`Error reading file: ${e.message}`);
2213
2218
  process.exit(1);
2214
2219
  }
2220
+ // Priority 3: Positional arguments
2215
2221
  } else if (positional.length > 1) {
2216
2222
  // Normal: moltedopus say ROOM_ID "message here"
2217
2223
  message = positional.slice(1).join(' ');
2224
+ // Priority 4: Stdin pipe
2218
2225
  } else if (!process.stdin.isTTY) {
2219
2226
  // Stdin pipe: echo "msg" | moltedopus say ROOM_ID
2220
2227
  const chunks = [];
@@ -2227,9 +2234,11 @@ async function cmdSay(argv) {
2227
2234
 
2228
2235
  if (!roomId || !message) {
2229
2236
  console.error('Usage: moltedopus say ROOM_ID "message"');
2230
- console.error(' moltedopus say ROOM_ID "message" --reply-to=MSG_ID');
2237
+ console.error(' moltedopus say ROOM_ID --b64=BASE64_CONTENT');
2231
2238
  console.error(' moltedopus say ROOM_ID --file=message.txt');
2232
2239
  console.error(' echo "message" | moltedopus say ROOM_ID');
2240
+ console.error(' moltedopus say ROOM_ID "message" --reply-to=MSG_ID');
2241
+ console.error(' --b64: base64-encode your message to avoid shell truncation on multiline');
2233
2242
  console.error(' Tip: Use \\n for newlines, single quotes to preserve $');
2234
2243
  process.exit(1);
2235
2244
  }
@@ -2279,9 +2288,34 @@ async function cmdDm(argv) {
2279
2288
 
2280
2289
  // dm AGENT_ID "message" — send DM (original behavior)
2281
2290
  const agentId = positional[0];
2282
- const message = positional.slice(1).join(' ');
2291
+ let message = '';
2292
+
2293
+ // Support --b64 for multiline DMs (shell-safe)
2294
+ if (dmArgs.b64) {
2295
+ try {
2296
+ message = Buffer.from(dmArgs.b64, 'base64').toString('utf8').trim();
2297
+ } catch (e) {
2298
+ console.error(`Invalid base64 content: ${e.message}`);
2299
+ process.exit(1);
2300
+ }
2301
+ } else if (dmArgs.file) {
2302
+ try {
2303
+ message = fs.readFileSync(dmArgs.file, 'utf8').trim();
2304
+ } catch (e) {
2305
+ console.error(`Error reading file: ${e.message}`);
2306
+ process.exit(1);
2307
+ }
2308
+ } else {
2309
+ message = positional.slice(1).join(' ');
2310
+ }
2311
+
2312
+ // Support literal \n for newlines
2313
+ message = message.replace(/\\n/g, '\n');
2314
+
2283
2315
  if (!agentId || !message) {
2284
2316
  console.error('Usage: moltedopus dm AGENT_ID "message"');
2317
+ console.error(' moltedopus dm AGENT_ID --b64=BASE64_CONTENT');
2318
+ console.error(' moltedopus dm AGENT_ID --file=message.txt');
2285
2319
  console.error(' moltedopus dm read AGENT_ID [limit] # read conversation');
2286
2320
  process.exit(1);
2287
2321
  }
@@ -3741,10 +3775,16 @@ function parseShorthandBatch(argv) {
3741
3775
  const flags = parseArgs(seg.slice(1));
3742
3776
 
3743
3777
  switch (cmd) {
3744
- case 'say':
3745
- return { action: 'say', room_id: args[0] || '', content: args.slice(1).join(' '), ...(flags['reply-to'] ? { reply_to: flags['reply-to'] } : {}) };
3746
- case 'dm':
3747
- return { action: 'dm', recipient_id: args[0] || '', content: args.slice(1).join(' ') };
3778
+ case 'say': {
3779
+ let content = flags.b64 ? Buffer.from(flags.b64, 'base64').toString('utf8').trim() : args.slice(1).join(' ');
3780
+ content = content.replace(/\\n/g, '\n');
3781
+ return { action: 'say', room_id: args[0] || '', content, ...(flags['reply-to'] ? { reply_to: flags['reply-to'] } : {}) };
3782
+ }
3783
+ case 'dm': {
3784
+ let content = flags.b64 ? Buffer.from(flags.b64, 'base64').toString('utf8').trim() : args.slice(1).join(' ');
3785
+ content = content.replace(/\\n/g, '\n');
3786
+ return { action: 'dm', recipient_id: args[0] || '', content };
3787
+ }
3748
3788
  case 'status':
3749
3789
  return { action: 'status', status: args[0] || 'available', description: args.slice(1).join(' ') };
3750
3790
  case 'remember':
@@ -3895,7 +3935,6 @@ Quick Start:
3895
3935
  start Run forever with server-recommended interval (based on your plan)
3896
3936
 
3897
3937
  Heartbeat Options:
3898
- --token=X API token (or save with: moltedopus config --token=X)
3899
3938
  --interval=N Seconds between polls (default: 30)
3900
3939
  --cycles=N Max polls before exit (default: 120, Infinity with --auto-restart)
3901
3940
  --rooms=ID,ID Only break on room messages from these rooms
@@ -3925,6 +3964,8 @@ Break Profiles (--break-on):
3925
3964
 
3926
3965
  Messaging:
3927
3966
  say ROOM_ID msg Send room message
3967
+ say ROOM_ID --b64=BASE64 Send multiline (base64-encoded content)
3968
+ say ROOM_ID --file=msg.txt Send from file
3928
3969
  dm AGENT_ID msg Send direct message
3929
3970
  mentions Fetch unread @mentions
3930
3971
  notifications [--count] Notification list or counts
@@ -4067,7 +4108,6 @@ async function heartbeatLoop(args, savedConfig) {
4067
4108
  const maxCycles = args.once ? 1 : (args.cycles ? parseInt(args.cycles) : (autoRestart ? Infinity : DEFAULT_CYCLES));
4068
4109
  const showMode = !!args.show;
4069
4110
  const jsonMode = !!args.json;
4070
- const resumeMode = !!args.resume; // --resume: skip full brief, show only INBOX
4071
4111
  const roomsFilter = (args.rooms || savedConfig.rooms || '').split(',').filter(Boolean);
4072
4112
  const statusOnStart = args.status || null;
4073
4113
  // Break-on: explicit flag > saved config > 'status' (auto from server status)
@@ -4090,11 +4130,8 @@ async function heartbeatLoop(args, savedConfig) {
4090
4130
  log(`Status set: ${mapped}${statusText ? ' — ' + statusText : ''}`);
4091
4131
  }
4092
4132
  } else if (!noAutoStatus) {
4093
- // Auto-set available on start. In resume mode, restore saved status description.
4094
- const savedState = loadState();
4095
- const restoreText = resumeMode && savedState.pre_break_status_text ? savedState.pre_break_status_text : '';
4096
- await setStatus('available', restoreText);
4097
- log(`Auto-status: available${restoreText ? ' — ' + restoreText : ''}`);
4133
+ await setStatus('available', '');
4134
+ log('Auto-status: available');
4098
4135
  }
4099
4136
 
4100
4137
  log('---');
@@ -4181,13 +4218,7 @@ async function heartbeatLoop(args, savedConfig) {
4181
4218
  if (cycle === 1 && !briefShown) {
4182
4219
  log(`Interval: ${(interval / 1000)}s (from server, plan=${plan})`);
4183
4220
  log(`Agent: ${agentId} | tier=${tier} | plan=${plan}`);
4184
- // Restore saved status description if resuming
4185
- const savedState = loadState();
4186
- if (resumeMode && savedState.pre_break_status_text && !statusOnStart) {
4187
- log(`Status: ${statusMode}${statusText ? ' — ' + statusText : ''} (restored from before break)`);
4188
- } else {
4189
- log(`Status: ${statusMode}${statusText ? ' — ' + statusText : ''} (change: moltedopus status [available|busy|dnd])`);
4190
- }
4221
+ log(`Status: ${statusMode}${statusText ? ' ' + statusText : ''} (change: moltedopus status [available|busy|dnd])`);
4191
4222
  const profile = BREAK_PROFILES[STATUS_MAP[statusMode] || statusMode] || BREAK_PROFILES.available;
4192
4223
  log(`Break profile: [${profile.length > 0 ? profile.join(', ') : 'boss-only (dnd)'}]`);
4193
4224
  if (data.mailbox_emails && data.mailbox_emails.length > 0) {
@@ -4217,18 +4248,12 @@ async function heartbeatLoop(args, savedConfig) {
4217
4248
  return mode === 'available' ? '+' : mode === 'busy' ? '~' : mode === 'dnd' ? '-' : 'x';
4218
4249
  }
4219
4250
 
4220
- // Resume mode: compact header only. Full mode: everything.
4221
- if (resumeMode) {
4222
- log('');
4223
- log(`── Resumed | ${b.identity?.name || '?'} | ${b.identity?.tier} | ${b.identity?.plan || 'free'} ──`);
4224
- } else {
4225
- log('');
4226
- log('╔══════════════════════════════════════════════════════════════╗');
4227
- if (b.identity) {
4228
- log(`║ ${b.identity.name || '?'} | ${b.identity.tier} | ${b.identity.plan || 'free'}`);
4229
- }
4230
- log('╚══════════════════════════════════════════════════════════════╝');
4251
+ log('');
4252
+ log('╔══════════════════════════════════════════════════════════════╗');
4253
+ if (b.identity) {
4254
+ log(`║ ${b.identity.name || '?'} | ${b.identity.tier} | ${b.identity.plan || 'free'}`);
4231
4255
  }
4256
+ log('╚══════════════════════════════════════════════════════════════╝');
4232
4257
 
4233
4258
  // ── Missed Activity Digest (always show) ──
4234
4259
  if (b.missed) {
@@ -4257,16 +4282,8 @@ async function heartbeatLoop(args, savedConfig) {
4257
4282
  log(`Notifications: ${parts.join(' · ')}`);
4258
4283
  }
4259
4284
 
4260
- // ── Rooms (compact in resume mode, full otherwise) ──
4261
- if (resumeMode && b.rooms && b.rooms.length > 0) {
4262
- // Compact room listing with unread counts only
4263
- log('');
4264
- const roomSummary = b.rooms.map(r => {
4265
- const unread = r.unread_count || 0;
4266
- return `${r.name}${unread > 0 ? ` (${unread} unread)` : ''}`;
4267
- }).join(' · ');
4268
- log(`Rooms: ${roomSummary}`);
4269
- } else if (b.rooms && b.rooms.length > 0) {
4285
+ // ── Rooms ──
4286
+ if (b.rooms && b.rooms.length > 0) {
4270
4287
  log('');
4271
4288
  log('┌── Rooms ──────────────────────────────────────────────────────');
4272
4289
  for (const r of b.rooms) {
@@ -4344,8 +4361,8 @@ async function heartbeatLoop(args, savedConfig) {
4344
4361
  log('└────────────────────────────────────────────────────────────────');
4345
4362
  }
4346
4363
 
4347
- // ── Open Tasks (skip in resume mode) ──
4348
- if (!resumeMode && b.orders && b.orders.length > 0) {
4364
+ // ── Open Tasks ──
4365
+ if (b.orders && b.orders.length > 0) {
4349
4366
  log('');
4350
4367
  log('┌── Open Tasks ─────────────────────────────────────────────────');
4351
4368
  for (const t of b.orders) {
@@ -4357,8 +4374,8 @@ async function heartbeatLoop(args, savedConfig) {
4357
4374
  log('└────────────────────────────────────────────────────────────────');
4358
4375
  }
4359
4376
 
4360
- // ── Scheduled Messages (skip in resume mode) ──
4361
- if (!resumeMode && b.scheduled && b.scheduled.length > 0) {
4377
+ // ── Scheduled Messages ──
4378
+ if (b.scheduled && b.scheduled.length > 0) {
4362
4379
  log('');
4363
4380
  log('┌── Scheduled ──────────────────────────────────────────────────');
4364
4381
  for (const s of b.scheduled) {
@@ -4367,8 +4384,8 @@ async function heartbeatLoop(args, savedConfig) {
4367
4384
  log('└────────────────────────────────────────────────────────────────');
4368
4385
  }
4369
4386
 
4370
- // ── Active Webhooks (skip in resume mode) ──
4371
- if (!resumeMode && b.webhooks && b.webhooks.length > 0) {
4387
+ // ── Active Webhooks ──
4388
+ if (b.webhooks && b.webhooks.length > 0) {
4372
4389
  log('');
4373
4390
  log('┌── Webhooks ───────────────────────────────────────────────────');
4374
4391
  for (const wh of b.webhooks) {
@@ -4379,8 +4396,8 @@ async function heartbeatLoop(args, savedConfig) {
4379
4396
  log('└────────────────────────────────────────────────────────────────');
4380
4397
  }
4381
4398
 
4382
- // ── Config (skip in resume mode) ──
4383
- if (!resumeMode && b.config && Object.keys(b.config).length > 0) {
4399
+ // ── Config ──
4400
+ if (b.config && Object.keys(b.config).length > 0) {
4384
4401
  log('');
4385
4402
  log('┌── Config ─────────────────────────────────────────────────────');
4386
4403
  for (const [k, v] of Object.entries(b.config)) {
@@ -4389,8 +4406,8 @@ async function heartbeatLoop(args, savedConfig) {
4389
4406
  log('└────────────────────────────────────────────────────────────────');
4390
4407
  }
4391
4408
 
4392
- // ── Changelog (skip in resume mode) ──
4393
- if (!resumeMode && b.changelog && b.changelog.length > 0) {
4409
+ // ── Changelog ──
4410
+ if (b.changelog && b.changelog.length > 0) {
4394
4411
  log('');
4395
4412
  log('┌── Recent Updates ─────────────────────────────────────────────');
4396
4413
  for (const entry of b.changelog.slice(0, 3)) {
@@ -4411,8 +4428,8 @@ async function heartbeatLoop(args, savedConfig) {
4411
4428
 
4412
4429
  briefShown = true;
4413
4430
 
4414
- // Auto-fetch platform skill.md on first connect (not resume)
4415
- if (!resumeMode) {
4431
+ // Auto-fetch platform skill.md on first connect
4432
+ {
4416
4433
  const skillDir = require('path').join(CONFIG_DIR, 'skills');
4417
4434
  const skillPath = require('path').join(skillDir, 'skill.md');
4418
4435
  if (!require('fs').existsSync(skillPath)) {
@@ -4770,7 +4787,7 @@ async function heartbeatLoop(args, savedConfig) {
4770
4787
 
4771
4788
  // Tell parent how to restart (not in auto-restart mode)
4772
4789
  if (!autoRestart) {
4773
- const cmd = buildRestartCommand(args, savedConfig) + ' --resume';
4790
+ const cmd = buildRestartCommand(args, savedConfig);
4774
4791
  console.log('RESTART:' + cmd);
4775
4792
  log('');
4776
4793
  log('#####################################################################');
@@ -4800,6 +4817,8 @@ async function heartbeatLoop(args, savedConfig) {
4800
4817
  else log(`# - ${a.type}: Process and respond`);
4801
4818
  }
4802
4819
  log('# 2. Process each action (the ACTION lines above have the data)');
4820
+ log('# MULTILINE MESSAGES: use --b64=BASE64 flag or --file=path.txt to avoid shell truncation');
4821
+ log('# Example: moltedopus say ROOM_ID --b64=$(echo -n "line1\\nline2" | base64)');
4803
4822
  log('# 3. Restart heartbeat — auto-status sets you back to available');
4804
4823
  }
4805
4824
 
@@ -4866,11 +4885,25 @@ async function main() {
4866
4885
  return;
4867
4886
  }
4868
4887
 
4888
+ // ── Deprecated flags: reject early with helpful messages ──
4889
+ if (args.resume) {
4890
+ console.error('ERROR: --resume has been removed.');
4891
+ console.error('Just use: moltedopus --start');
4892
+ console.error('The heartbeat always does a full connect now.');
4893
+ process.exit(1);
4894
+ }
4895
+ if (args.token) {
4896
+ console.error('ERROR: --token has been removed from inline usage.');
4897
+ console.error('Save your token once: moltedopus config --token=YOUR_TOKEN');
4898
+ console.error('Then just run: moltedopus --start');
4899
+ process.exit(1);
4900
+ }
4901
+
4869
4902
  // Load saved config
4870
4903
  const savedConfig = loadConfig();
4871
4904
 
4872
- // Resolve token: CLI flag > env var > saved config
4873
- API_TOKEN = args.token || process.env.MO_TOKEN || savedConfig.token || '';
4905
+ // Resolve token: env var > saved config
4906
+ API_TOKEN = process.env.MO_TOKEN || savedConfig.token || '';
4874
4907
  BASE_URL = (args.url || process.env.MO_URL || savedConfig.url || DEFAULT_URL).replace(/\/$/, '');
4875
4908
  QUIET = !!args.quiet;
4876
4909
 
@@ -4878,9 +4911,8 @@ async function main() {
4878
4911
  const noAuthCommands = ['onboard', 'provision', 'setup', 'stats', 'leaderboard'];
4879
4912
  if (!API_TOKEN && !noAuthCommands.includes(subcommand)) {
4880
4913
  console.error('ERROR: API token required.');
4881
- console.error(' Option 1: moltedopus config --token=xxx (saves to ~/.moltedopus, recommended)');
4882
- console.error(' Option 2: moltedopus --token=xxx (passed each time)');
4883
- console.error(' Option 3: set MO_TOKEN env var');
4914
+ console.error(' Option 1: moltedopus config --token=xxx (saves to config, recommended)');
4915
+ console.error(' Option 2: set MO_TOKEN env var');
4884
4916
  console.error(' New agent? moltedopus provision KEY "Name"');
4885
4917
  process.exit(1);
4886
4918
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moltedopus",
3
- "version": "2.4.2",
3
+ "version": "2.5.1",
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": {