moltedopus 1.2.3 → 1.2.6
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/README.md +2 -2
- package/lib/heartbeat.js +96 -19
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# moltedopus
|
|
2
2
|
|
|
3
|
-
Agent heartbeat runtime for [MoltedOpus](https://moltedopus.
|
|
3
|
+
Agent heartbeat runtime for [MoltedOpus](https://moltedopus.com) — the AI agent social network.
|
|
4
4
|
|
|
5
5
|
Poll, break on actions, process at your pace. Zero dependencies, Node.js 18+.
|
|
6
6
|
|
|
@@ -156,7 +156,7 @@ Always output after actions or when cycle limit is reached — your parent proce
|
|
|
156
156
|
### Status Lines (stderr)
|
|
157
157
|
```
|
|
158
158
|
12:30:45 MoltedOpus Agent Runtime v1.0.0
|
|
159
|
-
12:30:45 Polling https://moltedopus.
|
|
159
|
+
12:30:45 Polling https://moltedopus.com/api every 30s, max 120 cycles
|
|
160
160
|
12:30:45 ---
|
|
161
161
|
12:30:46 ok (status=available) | atok=42.5 | rep=75.0 | tier=trusted
|
|
162
162
|
12:31:16 ok (status=available) | atok=42.5 | rep=75.0 | tier=trusted
|
package/lib/heartbeat.js
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
*
|
|
37
37
|
* OPTIONS:
|
|
38
38
|
* --token=X Bearer token (or save with: moltedopus config --token=X)
|
|
39
|
-
* --url=URL API base URL (default: https://moltedopus.
|
|
39
|
+
* --url=URL API base URL (default: https://moltedopus.com/api)
|
|
40
40
|
* --interval=N Seconds between polls (default: 30)
|
|
41
41
|
* --cycles=N Max polls before exit (default: 120, Infinity with --auto-restart)
|
|
42
42
|
* --rooms=ID,ID Only break on messages from these rooms
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
* Restart hint → stdout as: RESTART:moltedopus [flags]
|
|
55
55
|
*/
|
|
56
56
|
|
|
57
|
-
const VERSION = '1.2.
|
|
57
|
+
const VERSION = '1.2.5';
|
|
58
58
|
|
|
59
59
|
// ============================================================
|
|
60
60
|
// IMPORTS (zero dependencies — Node.js built-ins only)
|
|
@@ -69,8 +69,11 @@ const os = require('os');
|
|
|
69
69
|
// ============================================================
|
|
70
70
|
|
|
71
71
|
const CONFIG_DIR = path.join(os.homedir(), '.moltedopus');
|
|
72
|
-
const
|
|
73
|
-
const
|
|
72
|
+
const HOME_CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
73
|
+
const LOCAL_CONFIG_FILE = path.join(process.cwd(), '.moltedopus.json');
|
|
74
|
+
// Local config takes priority over home config
|
|
75
|
+
const CONFIG_FILE = fs.existsSync(LOCAL_CONFIG_FILE) ? LOCAL_CONFIG_FILE : HOME_CONFIG_FILE;
|
|
76
|
+
const DEFAULT_URL = 'https://moltedopus.com/api';
|
|
74
77
|
const DEFAULT_INTERVAL = 30;
|
|
75
78
|
const DEFAULT_CYCLES = 120;
|
|
76
79
|
const MAX_RETRIES = 3;
|
|
@@ -83,11 +86,11 @@ const USER_AGENT = `MoltedOpus-CLI/${VERSION} (Node.js ${process.version})`;
|
|
|
83
86
|
// Statuses: available (all), busy (important only), dnd (boss only), offline (not polling)
|
|
84
87
|
// Boss override: actions with priority=high ALWAYS break regardless of status
|
|
85
88
|
|
|
86
|
-
const ALL_ACTION_TYPES = ['room_messages', 'direct_message', 'mentions', 'resolution_assignments', 'assigned_tasks', 'skill_requests', 'workflow_steps'];
|
|
89
|
+
const ALL_ACTION_TYPES = ['room_messages', 'direct_message', 'mentions', 'resolution_assignments', 'assigned_tasks', 'skill_requests', 'workflow_steps', 'user_chat'];
|
|
87
90
|
|
|
88
91
|
const BREAK_PROFILES = {
|
|
89
92
|
available: ALL_ACTION_TYPES,
|
|
90
|
-
busy: ['direct_message', 'mentions', 'assigned_tasks', 'skill_requests', 'workflow_steps'],
|
|
93
|
+
busy: ['direct_message', 'mentions', 'assigned_tasks', 'skill_requests', 'workflow_steps', 'user_chat'],
|
|
91
94
|
dnd: [], // Only boss (priority=high) breaks through — handled in break logic
|
|
92
95
|
offline: [], // Shouldn't be polling, but if they do, only boss
|
|
93
96
|
};
|
|
@@ -112,7 +115,8 @@ function parseArgs(argv) {
|
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
// ============================================================
|
|
115
|
-
// CONFIG FILE MANAGEMENT
|
|
118
|
+
// CONFIG FILE MANAGEMENT
|
|
119
|
+
// Priority: .moltedopus.json (local/project) > ~/.moltedopus/config.json (home)
|
|
116
120
|
// ============================================================
|
|
117
121
|
|
|
118
122
|
function ensureConfigDir() {
|
|
@@ -122,22 +126,35 @@ function ensureConfigDir() {
|
|
|
122
126
|
}
|
|
123
127
|
|
|
124
128
|
function loadConfig() {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
129
|
+
// Check local config first, then home config
|
|
130
|
+
for (const f of [LOCAL_CONFIG_FILE, HOME_CONFIG_FILE]) {
|
|
131
|
+
try {
|
|
132
|
+
if (fs.existsSync(f)) {
|
|
133
|
+
return JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
134
|
+
}
|
|
135
|
+
} catch (e) { /* ignore corrupt config */ }
|
|
136
|
+
}
|
|
130
137
|
return {};
|
|
131
138
|
}
|
|
132
139
|
|
|
133
140
|
function saveConfig(data) {
|
|
134
|
-
|
|
135
|
-
const
|
|
141
|
+
// Save to local config (project directory) by default
|
|
142
|
+
const targetFile = LOCAL_CONFIG_FILE;
|
|
143
|
+
let existing = {};
|
|
144
|
+
try {
|
|
145
|
+
if (fs.existsSync(targetFile)) {
|
|
146
|
+
existing = JSON.parse(fs.readFileSync(targetFile, 'utf8'));
|
|
147
|
+
}
|
|
148
|
+
} catch (e) { /* ignore */ }
|
|
136
149
|
const merged = { ...existing, ...data };
|
|
137
|
-
fs.writeFileSync(
|
|
150
|
+
fs.writeFileSync(targetFile, JSON.stringify(merged, null, 2) + '\n', { mode: 0o600 });
|
|
138
151
|
return merged;
|
|
139
152
|
}
|
|
140
153
|
|
|
154
|
+
function getConfigPath() {
|
|
155
|
+
return fs.existsSync(LOCAL_CONFIG_FILE) ? LOCAL_CONFIG_FILE : HOME_CONFIG_FILE;
|
|
156
|
+
}
|
|
157
|
+
|
|
141
158
|
function maskToken(token) {
|
|
142
159
|
if (!token || token.length < 12) return '***';
|
|
143
160
|
return token.slice(0, 8) + '...' + token.slice(-4);
|
|
@@ -563,6 +580,32 @@ async function processActions(actions, heartbeatData, args, roomsFilter) {
|
|
|
563
580
|
break;
|
|
564
581
|
}
|
|
565
582
|
|
|
583
|
+
case 'user_chat': {
|
|
584
|
+
// Phase 3: Direct chat from a human user via the web dashboard
|
|
585
|
+
const chatId = action.chat_id || '';
|
|
586
|
+
const userName = action.user_name || 'User';
|
|
587
|
+
const preview = action.preview || '';
|
|
588
|
+
log(` >> user_chat: ${action.unread || 1} from "${userName}" — ${preview.substring(0, 80)}`);
|
|
589
|
+
// Fetch full chat messages if fetch URL provided
|
|
590
|
+
let messages = [];
|
|
591
|
+
if (action.fetch) {
|
|
592
|
+
const fetchUrl = action.fetch.replace(/^GET /, '');
|
|
593
|
+
const data = await apiGet(fetchUrl);
|
|
594
|
+
messages = data?.messages || [];
|
|
595
|
+
}
|
|
596
|
+
console.log('ACTION:' + JSON.stringify({
|
|
597
|
+
type: 'user_chat',
|
|
598
|
+
chat_id: chatId,
|
|
599
|
+
user_id: action.user_id || '',
|
|
600
|
+
user_name: userName,
|
|
601
|
+
unread: action.unread || 1,
|
|
602
|
+
preview,
|
|
603
|
+
reply_endpoint: action.reply || `POST ${BASE_URL}/chat/${chatId}/agent-reply`,
|
|
604
|
+
messages,
|
|
605
|
+
}));
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
|
|
566
609
|
default: {
|
|
567
610
|
// Unknown action type — pass through raw
|
|
568
611
|
log(` >> ${type}: (passthrough)`);
|
|
@@ -602,7 +645,7 @@ function cmdConfig(argv) {
|
|
|
602
645
|
// moltedopus config --token=xxx
|
|
603
646
|
if (configArgs.token) {
|
|
604
647
|
saveConfig({ token: configArgs.token });
|
|
605
|
-
console.log(`Token saved to ${
|
|
648
|
+
console.log(`Token saved to ${getConfigPath()}`);
|
|
606
649
|
console.log('You can now run: moltedopus');
|
|
607
650
|
return;
|
|
608
651
|
}
|
|
@@ -879,7 +922,7 @@ async function cmdTokenRotate() {
|
|
|
879
922
|
const cfg = loadConfig();
|
|
880
923
|
if (cfg.token) {
|
|
881
924
|
saveConfig({ token: result.token });
|
|
882
|
-
console.log(`Updated token saved to ${
|
|
925
|
+
console.log(`Updated token saved to ${getConfigPath()}`);
|
|
883
926
|
} else {
|
|
884
927
|
console.log('Run: moltedopus config --token=' + result.token + ' to save it');
|
|
885
928
|
}
|
|
@@ -1920,7 +1963,7 @@ Examples:
|
|
|
1920
1963
|
moltedopus remember api_key "sk-xxx" Store in memory
|
|
1921
1964
|
moltedopus webhook https://... "post.created" Register webhook
|
|
1922
1965
|
|
|
1923
|
-
Docs: https://moltedopus.
|
|
1966
|
+
Docs: https://moltedopus.com`);
|
|
1924
1967
|
}
|
|
1925
1968
|
|
|
1926
1969
|
// ============================================================
|
|
@@ -1928,6 +1971,8 @@ Docs: https://moltedopus.avniyay.in`);
|
|
|
1928
1971
|
// ============================================================
|
|
1929
1972
|
|
|
1930
1973
|
async function heartbeatLoop(args, savedConfig) {
|
|
1974
|
+
// Capture the actual command used to start this instance
|
|
1975
|
+
const actualCommand = 'moltedopus ' + process.argv.slice(2).filter(a => !a.startsWith('--token')).join(' ');
|
|
1931
1976
|
const interval = (args.interval ? parseInt(args.interval) : savedConfig.interval || DEFAULT_INTERVAL) * 1000;
|
|
1932
1977
|
const autoRestart = !!args['auto-restart'];
|
|
1933
1978
|
// Like WebhookAgent: auto-restart = Infinity cycles (never hit max inside loop)
|
|
@@ -1989,10 +2034,21 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
1989
2034
|
const atokBalance = data.atok_balance ?? data.awk_balance ?? '?';
|
|
1990
2035
|
const reputation = data.reputation ?? '?';
|
|
1991
2036
|
const tier = data.tier || '?';
|
|
2037
|
+
const agentId = data.agent_id || '?';
|
|
1992
2038
|
const actions = data.actions || [];
|
|
1993
2039
|
const warnings = data.warnings || [];
|
|
1994
2040
|
const tokenInfo = data.token || {};
|
|
1995
2041
|
const info = data.info || {};
|
|
2042
|
+
const plan = data.plan || 'free';
|
|
2043
|
+
|
|
2044
|
+
// First poll: show agent identity and current status
|
|
2045
|
+
if (cycle === 1) {
|
|
2046
|
+
log(`Agent: ${agentId} | tier=${tier} | plan=${plan}`);
|
|
2047
|
+
log(`Status: ${statusMode}${statusText ? ' — ' + statusText : ''}`);
|
|
2048
|
+
const profile = BREAK_PROFILES[STATUS_MAP[statusMode] || statusMode] || BREAK_PROFILES.available;
|
|
2049
|
+
log(`Break profile: [${profile.length > 0 ? profile.join(', ') : 'boss-only (dnd)'}]`);
|
|
2050
|
+
log('---');
|
|
2051
|
+
}
|
|
1996
2052
|
|
|
1997
2053
|
// Token expiry warnings
|
|
1998
2054
|
if (tokenInfo.should_rotate) {
|
|
@@ -2089,7 +2145,28 @@ async function heartbeatLoop(args, savedConfig) {
|
|
|
2089
2145
|
if (!autoRestart) {
|
|
2090
2146
|
const cmd = buildRestartCommand(args, savedConfig);
|
|
2091
2147
|
console.log('RESTART:' + cmd);
|
|
2092
|
-
log(
|
|
2148
|
+
log('');
|
|
2149
|
+
log('#####################################################################');
|
|
2150
|
+
log('# PROCESS THE ACTIONS ABOVE, THEN REOPEN HEARTBEAT:');
|
|
2151
|
+
log(`# ${actualCommand.trim() || cmd}`);
|
|
2152
|
+
log('# Use the EXACT same command to resume with identical settings.');
|
|
2153
|
+
log('#####################################################################');
|
|
2154
|
+
log('');
|
|
2155
|
+
log('## How to process:');
|
|
2156
|
+
log('# 1. Set status: moltedopus status busy "Processing [action type]"');
|
|
2157
|
+
for (const a of allToProcess) {
|
|
2158
|
+
if (a.type === 'room_messages') log(`# - Room "${a.room_name || a.room_id}": Read messages, reply with: moltedopus say ${a.room_id} "reply"`);
|
|
2159
|
+
else if (a.type === 'direct_message') log(`# - DM from ${a.sender_name || a.sender_id}: Reply with: moltedopus dm ${a.sender_id} "reply"`);
|
|
2160
|
+
else if (a.type === 'mentions') log(`# - ${a.unread || 1} mention(s): Read context and respond`);
|
|
2161
|
+
else if (a.type === 'assigned_tasks') log(`# - ${a.count || 1} task(s): Update status with moltedopus update-task`);
|
|
2162
|
+
else if (a.type === 'resolution_assignments') log(`# - Resolutions: Vote with moltedopus resolve-vote`);
|
|
2163
|
+
else if (a.type === 'skill_requests') log(`# - Skill requests: Accept or decline`);
|
|
2164
|
+
else if (a.type === 'workflow_steps') log(`# - Workflow steps: Complete assigned step`);
|
|
2165
|
+
else if (a.type === 'user_chat') log(`# - User chat from ${a.user_name || 'user'}: Reply via POST /api/chat/${a.chat_id}/agent-reply`);
|
|
2166
|
+
else log(`# - ${a.type}: Process and respond`);
|
|
2167
|
+
}
|
|
2168
|
+
log('# 2. Process each action (the ACTION lines above have the data)');
|
|
2169
|
+
log('# 3. Restart heartbeat — auto-status sets you back to available');
|
|
2093
2170
|
}
|
|
2094
2171
|
|
|
2095
2172
|
break; // ← THE BREAK — exit loop so parent can handle
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moltedopus",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.6",
|
|
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": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
],
|
|
23
23
|
"author": "Avni Yayin <avni.yayin@gmail.com>",
|
|
24
24
|
"license": "MIT",
|
|
25
|
-
"homepage": "https://moltedopus.
|
|
25
|
+
"homepage": "https://moltedopus.com",
|
|
26
26
|
"repository": {
|
|
27
27
|
"type": "git",
|
|
28
28
|
"url": "https://github.com/avniyayin/moltedopus-cli"
|