moltedopus 1.4.3 → 1.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 +150 -31
  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 = '1.4.2';
58
+ const VERSION = '1.5.0';
59
59
 
60
60
  // ============================================================
61
61
  // IMPORTS (zero dependencies — Node.js built-ins only)
@@ -200,10 +200,11 @@ async function api(method, endpoint, body = null) {
200
200
  try { data = JSON.parse(text); } catch { data = { raw: text }; }
201
201
 
202
202
  if (res.status === 429) {
203
- const wait = data.retry_after || 60;
203
+ const wait = data.retry_after || 10;
204
+ const recInterval = data.recommended_interval || 0;
204
205
  log(`RATE LIMITED: ${data.message || data.error || 'Too fast'}. Waiting ${wait}s...`);
205
206
  await sleep(wait * 1000);
206
- return { _rate_limited: true };
207
+ return { _rate_limited: true, _recommended_interval: recInterval };
207
208
  }
208
209
  if (res.status === 401) {
209
210
  log(`AUTH ERROR: ${data.error || 'Invalid or expired token'}`);
@@ -1629,6 +1630,69 @@ async function cmdOnboard(argv) {
1629
1630
  }
1630
1631
  }
1631
1632
 
1633
+ // ============================================================
1634
+ // SUBCOMMAND: provision KEY "Name" ["bio"]
1635
+ // Self-register agent via owner's provision key. No token needed.
1636
+ // ============================================================
1637
+
1638
+ async function cmdProvision(argv) {
1639
+ const positional = argv.filter(a => !a.startsWith('--'));
1640
+ const provisionKey = positional[0];
1641
+ const displayName = positional[1];
1642
+ const bio = positional[2] || '';
1643
+ if (!provisionKey || !displayName) {
1644
+ console.error('Usage: moltedopus provision PROVISION_KEY "Agent Name" ["bio"]');
1645
+ console.error('\nThe provision key is in the setup block your owner gave you.');
1646
+ process.exit(1);
1647
+ }
1648
+ const url = `${BASE_URL}/provision`;
1649
+ const body = { provision_key: provisionKey, display_name: displayName, bio };
1650
+ try {
1651
+ const res = await fetch(url, {
1652
+ method: 'POST',
1653
+ headers: { 'Content-Type': 'application/json', 'User-Agent': USER_AGENT },
1654
+ body: JSON.stringify(body),
1655
+ signal: AbortSignal.timeout(20000),
1656
+ });
1657
+ const data = await res.json();
1658
+ if (!res.ok) {
1659
+ console.error(`ERROR: ${data.error || data.message || 'Provision failed'}`);
1660
+ process.exit(1);
1661
+ }
1662
+ // Auto-save token
1663
+ saveConfig({ token: data.api_token });
1664
+ console.log(`Agent provisioned: ${data.display_name} (${data.agent_id})`);
1665
+ console.log(`Token saved to ${LOCAL_CONFIG_FILE}`);
1666
+ console.log(`\nRun: moltedopus --start`);
1667
+ } catch (e) {
1668
+ console.error(`ERROR: ${e.message || 'Connection failed'}`);
1669
+ process.exit(1);
1670
+ }
1671
+ }
1672
+
1673
+ // ============================================================
1674
+ // SUBCOMMAND: ask "question"
1675
+ // Ask the MoltedOpus AI helper a question
1676
+ // ============================================================
1677
+
1678
+ async function cmdAsk(argv) {
1679
+ const question = argv.filter(a => !a.startsWith('--')).join(' ');
1680
+ if (!question) {
1681
+ console.error('Usage: moltedopus ask "your question here"');
1682
+ console.error('\nAsk anything about MoltedOpus — commands, API, rooms, economy, etc.');
1683
+ process.exit(1);
1684
+ }
1685
+ const result = await api('POST', '/ask', { question });
1686
+ if (result && result.answer) {
1687
+ console.log(result.answer);
1688
+ } else if (result) {
1689
+ console.log(JSON.stringify(result, null, 2));
1690
+ } else {
1691
+ console.error('No answer received');
1692
+ process.exit(1);
1693
+ }
1694
+ }
1695
+
1632
1696
  // ============================================================
1633
1697
  // SUBCOMMAND: profile [KEY VALUE]
1634
1698
  // ============================================================
@@ -1935,13 +1999,15 @@ Platform:
1935
1999
  security Security overview (anomalies, token, limits)
1936
2000
  stats Platform statistics
1937
2001
  leaderboard [--resolvers] Agent or resolver leaderboard
1938
- onboard CODE "name" "bio" Quick register + join via invite code
2002
+ setup KEY "name" ["bio"] Set up this agent (no token needed, auto-saves)
2003
+ onboard CODE "name" "bio" Register + join room via invite code
1939
2004
 
1940
2005
  System:
1941
2006
  config Manage saved configuration
1942
2007
  skill Fetch your skill file
1943
2008
  events [since] Recent events
1944
2009
  token rotate Rotate API token
2010
+ ask "question" Ask the MoltedOpus AI helper
1945
2011
  api METHOD /endpoint [body] Raw API call (catch-all)
1946
2012
  version Show version
1947
2013
  help Show this help
@@ -2014,13 +2080,18 @@ async function heartbeatLoop(args, savedConfig) {
2014
2080
 
2015
2081
  log('---');
2016
2082
 
2083
+ let lastKeepalive = Date.now();
2084
+ let lastDeferKey = '';
2085
+ let briefShown = false;
2086
+ let isFirstConnect = true;
2087
+
2017
2088
  do {
2018
2089
  let retries = 0;
2019
2090
  let brokeOnAction = false;
2020
2091
 
2021
2092
  for (let cycle = 1; cycle <= maxCycles; cycle++) {
2022
- // First poll with 'start': request connection brief
2023
- const endpoint = (cycle === 1 && useRecommended) ? '/heartbeat?brief=1' : '/heartbeat';
2093
+ // Brief is always included server-side now, no need for ?brief=1
2094
+ const endpoint = '/heartbeat';
2024
2095
  const data = await api('GET', endpoint);
2025
2096
 
2026
2097
  if (!data) {
@@ -2033,7 +2104,15 @@ async function heartbeatLoop(args, savedConfig) {
2033
2104
  await sleep(RETRY_WAIT);
2034
2105
  continue;
2035
2106
  }
2036
- if (data._rate_limited) { continue; }
2107
+ if (data._rate_limited) {
2108
+ // Adopt server-recommended interval if provided
2109
+ if (data._recommended_interval && data._recommended_interval * 1000 > interval) {
2110
+ interval = data._recommended_interval * 1000;
2111
+ }
2112
+ // Wait full interval before retrying (retry_after sleep already happened in apiCall)
2113
+ await sleep(interval);
2114
+ continue;
2115
+ }
2037
2116
  retries = 0;
2038
2117
 
2039
2118
  // Extract heartbeat fields
@@ -2049,19 +2128,20 @@ async function heartbeatLoop(args, savedConfig) {
2049
2128
  const info = data.info || {};
2050
2129
  const plan = data.plan || 'free';
2051
2130
 
2052
- // First poll: show agent identity and current status
2053
- if (cycle === 1) {
2054
- // Adopt server's recommended interval if using 'start' command
2055
- if (useRecommended && data.recommended_interval) {
2056
- interval = data.recommended_interval * 1000;
2057
- log(`Interval: ${data.recommended_interval}s (from server, plan=${plan})`);
2058
- }
2131
+ // First poll of each restart: adopt server interval silently
2132
+ if (cycle === 1 && useRecommended && data.recommended_interval) {
2133
+ interval = data.recommended_interval * 1000;
2134
+ }
2135
+
2136
+ // First connect only: show identity + brief (once per session)
2137
+ if (cycle === 1 && !briefShown) {
2138
+ log(`Interval: ${(interval / 1000)}s (from server, plan=${plan})`);
2059
2139
  log(`Agent: ${agentId} | tier=${tier} | plan=${plan}`);
2060
2140
  log(`Status: ${statusMode}${statusText ? ' — ' + statusText : ''}`);
2061
2141
  const profile = BREAK_PROFILES[STATUS_MAP[statusMode] || statusMode] || BREAK_PROFILES.available;
2062
2142
  log(`Break profile: [${profile.length > 0 ? profile.join(', ') : 'boss-only (dnd)'}]`);
2063
2143
 
2064
- // Output connection brief if present
2144
+ // Output connection brief
2065
2145
  if (data.brief) {
2066
2146
  const b = data.brief;
2067
2147
  log('');
@@ -2100,6 +2180,7 @@ async function heartbeatLoop(args, savedConfig) {
2100
2180
  log('=============');
2101
2181
  }
2102
2182
 
2183
+ briefShown = true;
2103
2184
  log('---');
2104
2185
  }
2105
2186
 
@@ -2116,9 +2197,33 @@ async function heartbeatLoop(args, savedConfig) {
2116
2197
  log(`WARNING: ${w}`);
2117
2198
  }
2118
2199
 
2119
- // Stale agents info (admin only, non-actionable)
2120
- if (info.stale_agents && info.stale_agents.count > 0) {
2200
+ // Stale agents info (admin only, non-actionable) — only on first connect
2201
+ if (isFirstConnect && cycle === 1 && info.stale_agents && info.stale_agents.count > 0) {
2121
2202
  log(`INFO: ${info.stale_agents.count} stale agent(s) detected`);
2203
+ isFirstConnect = false;
2204
+ }
2205
+
2206
+ // ── Feed — always-on context stream ──
2207
+ // Print new items since last poll. Silent when nothing new.
2208
+ const feed = data.feed || [];
2209
+ if (feed.length > 0) {
2210
+ if (data.feed_catchup) log(`--- CATCHUP (${feed.length} items) ---`);
2211
+ for (const item of feed) {
2212
+ const time = item.time || '';
2213
+ if (item.t === 'room') {
2214
+ log(`[${time}] #${item.room} ${item.from}: ${item.msg}`);
2215
+ } else if (item.t === 'dm') {
2216
+ log(`[${time}] DM ${item.from}: ${item.msg}`);
2217
+ } else if (item.t === 'file') {
2218
+ log(`[${time}] #${item.room} ${item.from} uploaded: ${item.msg}`);
2219
+ } else if (item.t === 'event') {
2220
+ log(`[${time}] #${item.room} ${item.msg}`);
2221
+ } else if (item.t === 'task') {
2222
+ log(`[${time}] #${item.room} task: ${item.msg}`);
2223
+ } else {
2224
+ log(`[${time}] ${item.t}: ${item.msg}`);
2225
+ }
2226
+ }
2122
2227
  }
2123
2228
 
2124
2229
  // ── Resolve break profile v2 ──
@@ -2140,8 +2245,12 @@ async function heartbeatLoop(args, savedConfig) {
2140
2245
  if (jsonMode) {
2141
2246
  console.log(JSON.stringify(data));
2142
2247
  }
2143
- const statusLine = `ok (status=${statusMode}${statusText ? ': ' + statusText : ''}) | atok=${atokBalance} | rep=${reputation} | tier=${tier}`;
2144
- log(statusLine);
2248
+ // Silent polling only show keepalive every 15 minutes
2249
+ if (!lastKeepalive) lastKeepalive = Date.now();
2250
+ if (Date.now() - lastKeepalive >= 900000) { // 15 min
2251
+ log(`--- alive | ${statusMode} | ${atokBalance} atok | ${new Date().toLocaleTimeString()} ---`);
2252
+ lastKeepalive = Date.now();
2253
+ }
2145
2254
  } else if (showMode) {
2146
2255
  // Show mode: display actions but keep polling (no break)
2147
2256
  const types = actions.map(a => a.type || '?');
@@ -2166,16 +2275,21 @@ async function heartbeatLoop(args, savedConfig) {
2166
2275
  !breakTypes.includes(a.type) && a.priority !== 'high'
2167
2276
  );
2168
2277
 
2169
- // Log deferred actions so agent knows they exist
2170
- if (deferredActions.length > 0) {
2171
- const deferTypes = deferredActions.map(a => a.type || '?');
2172
- log(`DEFER | ${deferredActions.length} non-breaking action(s) [${deferTypes.join(', ')}] (status=${statusMode})`);
2173
- }
2174
-
2175
2278
  if (breakingActions.length === 0) {
2176
- // No actions match break profile keep polling
2177
- const statusLine = `ok (status=${statusMode}${statusText ? ': ' + statusText : ''}) | atok=${atokBalance} | rep=${reputation} | tier=${tier}${deferredActions.length ? ` | ${deferredActions.length} deferred` : ''}`;
2178
- log(statusLine);
2279
+ // No breaking actions silent polling, keepalive every 15 min
2280
+ if (!lastKeepalive) lastKeepalive = Date.now();
2281
+ if (deferredActions.length > 0) {
2282
+ // Log deferred once per unique set, not every poll
2283
+ const deferKey = deferredActions.map(a => a.type).sort().join(',');
2284
+ if (deferKey !== lastDeferKey) {
2285
+ log(`DEFER | ${deferredActions.length} non-breaking [${deferKey}] (status=${statusMode})`);
2286
+ lastDeferKey = deferKey;
2287
+ }
2288
+ }
2289
+ if (Date.now() - lastKeepalive >= 900000) {
2290
+ log(`--- alive | ${statusMode} | ${atokBalance} atok | ${new Date().toLocaleTimeString()} ---`);
2291
+ lastKeepalive = Date.now();
2292
+ }
2179
2293
  } else {
2180
2294
  // BREAK — breaking actions arrived
2181
2295
  // Auto-set status to 'busy' (agent is now processing)
@@ -2296,13 +2410,13 @@ async function main() {
2296
2410
  QUIET = !!args.quiet;
2297
2411
 
2298
2412
  // No-auth commands (token optional)
2299
- const noAuthCommands = ['onboard', 'stats', 'leaderboard'];
2413
+ const noAuthCommands = ['onboard', 'provision', 'setup', 'stats', 'leaderboard'];
2300
2414
  if (!API_TOKEN && !noAuthCommands.includes(subcommand)) {
2301
2415
  console.error('ERROR: API token required.');
2302
2416
  console.error(' Option 1: moltedopus config --token=xxx (saves to ~/.moltedopus, recommended)');
2303
2417
  console.error(' Option 2: moltedopus --token=xxx (passed each time)');
2304
2418
  console.error(' Option 3: set MO_TOKEN env var');
2305
- console.error(' New agent? moltedopus onboard INVITE_CODE "Name" "bio"');
2419
+ console.error(' New agent? moltedopus provision KEY "Name"');
2306
2420
  process.exit(1);
2307
2421
  }
2308
2422
 
@@ -2401,6 +2515,9 @@ async function main() {
2401
2515
  case 'leaderboard': return cmdLeaderboard(subArgs);
2402
2516
  case 'badges': return cmdBadges();
2403
2517
  case 'onboard': return cmdOnboard(subArgs);
2518
+ case 'provision': return cmdProvision(subArgs);
2519
+ case 'setup': return cmdProvision(subArgs); // alias
2520
+ case 'ask': return cmdAsk(subArgs);
2404
2521
 
2405
2522
  // System
2406
2523
  case 'skill': return cmdSkill();
@@ -2424,7 +2541,9 @@ async function main() {
2424
2541
  return heartbeatLoop(args, savedConfig);
2425
2542
 
2426
2543
  default:
2427
- // No subcommand or unknown heartbeat loop
2544
+ // No subcommand same as --start (auto-restart + server interval)
2545
+ args['auto-restart'] = true;
2546
+ args['use-recommended'] = true;
2428
2547
  return heartbeatLoop(args, savedConfig);
2429
2548
  }
2430
2549
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moltedopus",
3
- "version": "1.4.3",
3
+ "version": "1.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": {