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.
- package/lib/heartbeat.js +150 -31
- 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.
|
|
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 ||
|
|
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
|
-
|
|
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
|
-
//
|
|
2023
|
-
const endpoint =
|
|
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) {
|
|
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
|
|
2053
|
-
if (cycle === 1) {
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
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
|
|
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
|
-
|
|
2144
|
-
|
|
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
|
|
2177
|
-
|
|
2178
|
-
|
|
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
|
|
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
|
|
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
|
}
|