morpheus-cli 0.5.1 → 0.5.3
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
CHANGED
|
@@ -64,6 +64,81 @@ morpheus session new
|
|
|
64
64
|
morpheus session status
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
## Docker
|
|
68
|
+
|
|
69
|
+
### Docker Compose (recommended)
|
|
70
|
+
|
|
71
|
+
**1. Create an `env.docker` file** (referenced by `docker-compose.yml`):
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
cp .env.example env.docker # or create manually
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Minimal `env.docker`:
|
|
78
|
+
|
|
79
|
+
```env
|
|
80
|
+
OPENAI_API_KEY=sk-...
|
|
81
|
+
THE_ARCHITECT_PASS=changeme
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**2. Run:**
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
docker compose up -d
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The dashboard will be available at `http://localhost:3333`.
|
|
91
|
+
|
|
92
|
+
**Useful commands:**
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
docker compose logs -f # follow logs
|
|
96
|
+
docker compose restart morpheus # restart the agent
|
|
97
|
+
docker compose down # stop
|
|
98
|
+
docker compose down -v # stop and remove persistent data
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Data persistence:** configuration and databases are stored in the `morpheus_data` Docker volume (`/root/.morpheus` inside the container). They survive container restarts and rebuilds.
|
|
102
|
+
|
|
103
|
+
**Override variables** at runtime via the `environment` block in `docker-compose.yml` or directly in `env.docker`. All `MORPHEUS_*` env vars work the same as in a native install. See the [Environment Variables](#environment-variables) section for the full list.
|
|
104
|
+
|
|
105
|
+
### Docker (standalone)
|
|
106
|
+
|
|
107
|
+
**Build:**
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
docker build -t morpheus .
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Run:**
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
docker run -d \
|
|
117
|
+
--name morpheus-agent \
|
|
118
|
+
-p 3333:3333 \
|
|
119
|
+
-e OPENAI_API_KEY=sk-... \
|
|
120
|
+
-e THE_ARCHITECT_PASS=changeme \
|
|
121
|
+
-v morpheus_data:/root/.morpheus \
|
|
122
|
+
morpheus
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**With Telegram:**
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
docker run -d \
|
|
129
|
+
--name morpheus-agent \
|
|
130
|
+
-p 3333:3333 \
|
|
131
|
+
-e OPENAI_API_KEY=sk-... \
|
|
132
|
+
-e THE_ARCHITECT_PASS=changeme \
|
|
133
|
+
-e MORPHEUS_TELEGRAM_ENABLED=true \
|
|
134
|
+
-e MORPHEUS_TELEGRAM_TOKEN=<bot-token> \
|
|
135
|
+
-e MORPHEUS_TELEGRAM_ALLOWED_USERS=123456789 \
|
|
136
|
+
-v morpheus_data:/root/.morpheus \
|
|
137
|
+
morpheus
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Health check:** the container exposes `GET /health` and Docker will probe it every 30s (60s start period, 3 retries before marking unhealthy).
|
|
141
|
+
|
|
67
142
|
## Async Task Execution
|
|
68
143
|
|
|
69
144
|
Morpheus uses asynchronous delegation by default:
|
|
@@ -162,6 +162,10 @@ export const startCommand = new Command('start')
|
|
|
162
162
|
taskWorker.start();
|
|
163
163
|
taskNotifier.start();
|
|
164
164
|
}
|
|
165
|
+
// Recover webhook notifications stuck in 'pending' from previous runs
|
|
166
|
+
WebhookDispatcher.recoverStale().catch((err) => {
|
|
167
|
+
display.log(`Webhook recovery error: ${err.message}`, { source: 'Webhooks', level: 'error' });
|
|
168
|
+
});
|
|
165
169
|
// Handle graceful shutdown
|
|
166
170
|
const shutdown = async (signal) => {
|
|
167
171
|
display.stopSpinner();
|
|
@@ -148,6 +148,10 @@ export class TaskRepository {
|
|
|
148
148
|
const row = this.db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
|
|
149
149
|
return row ? this.deserializeTask(row) : null;
|
|
150
150
|
}
|
|
151
|
+
findTaskByOriginMessageId(originMessageId) {
|
|
152
|
+
const row = this.db.prepare('SELECT * FROM tasks WHERE origin_message_id = ? LIMIT 1').get(originMessageId);
|
|
153
|
+
return row ? this.deserializeTask(row) : null;
|
|
154
|
+
}
|
|
151
155
|
listTasks(filters) {
|
|
152
156
|
const params = [];
|
|
153
157
|
let query = 'SELECT * FROM tasks WHERE 1=1';
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { WebhookRepository } from './repository.js';
|
|
2
|
+
import { TaskRepository } from '../tasks/repository.js';
|
|
2
3
|
import { DisplayManager } from '../display.js';
|
|
4
|
+
const STALE_NOTIFICATION_THRESHOLD_MS = 2 * 60 * 1000; // 2 minutes
|
|
3
5
|
export class WebhookDispatcher {
|
|
4
6
|
static telegramAdapter = null;
|
|
5
7
|
static oracle = null;
|
|
@@ -35,17 +37,35 @@ export class WebhookDispatcher {
|
|
|
35
37
|
}
|
|
36
38
|
const message = this.buildPrompt(webhook.prompt, payload);
|
|
37
39
|
try {
|
|
38
|
-
await oracle.chat(message, undefined, false, {
|
|
40
|
+
const response = await oracle.chat(message, undefined, false, {
|
|
39
41
|
origin_channel: 'webhook',
|
|
40
42
|
session_id: `webhook-${webhook.id}`,
|
|
41
43
|
origin_message_id: notificationId,
|
|
42
44
|
});
|
|
43
|
-
|
|
45
|
+
// Check whether Oracle delegated a task for this notification.
|
|
46
|
+
// If a task exists with this origin_message_id, TaskNotifier will update
|
|
47
|
+
// the notification when the task completes. If not (Oracle answered
|
|
48
|
+
// directly), mark it completed now with the direct response.
|
|
49
|
+
const taskRepo = TaskRepository.getInstance();
|
|
50
|
+
const delegatedTask = taskRepo.findTaskByOriginMessageId(notificationId);
|
|
51
|
+
if (delegatedTask) {
|
|
52
|
+
this.display.log(`Webhook "${webhook.name}" accepted and queued (notification: ${notificationId})`, { source: 'Webhooks', level: 'success' });
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
repo.updateNotificationResult(notificationId, 'completed', response);
|
|
56
|
+
this.display.log(`Webhook "${webhook.name}" completed with direct response (notification: ${notificationId})`, { source: 'Webhooks', level: 'success' });
|
|
57
|
+
if (webhook.notification_channels.includes('telegram')) {
|
|
58
|
+
await this.sendTelegram(webhook.name, response, 'completed');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
44
61
|
}
|
|
45
62
|
catch (err) {
|
|
46
63
|
const result = `Execution error: ${err.message}`;
|
|
47
64
|
this.display.log(`Webhook "${webhook.name}" failed: ${err.message}`, { source: 'Webhooks', level: 'error' });
|
|
48
65
|
repo.updateNotificationResult(notificationId, 'failed', result);
|
|
66
|
+
if (webhook.notification_channels.includes('telegram')) {
|
|
67
|
+
await this.sendTelegram(webhook.name, result, 'failed');
|
|
68
|
+
}
|
|
49
69
|
}
|
|
50
70
|
}
|
|
51
71
|
/**
|
|
@@ -63,6 +83,62 @@ ${payloadStr}
|
|
|
63
83
|
|
|
64
84
|
Analyze the payload above and follow the instructions provided. Be concise and actionable in your response.`;
|
|
65
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Called at startup to re-dispatch webhook notifications that got stuck in
|
|
88
|
+
* 'pending' status (e.g. from a previous crash or from the direct-response
|
|
89
|
+
* bug). Skips notifications that already have an active task running.
|
|
90
|
+
*/
|
|
91
|
+
static async recoverStale() {
|
|
92
|
+
const display = DisplayManager.getInstance();
|
|
93
|
+
const oracle = WebhookDispatcher.oracle;
|
|
94
|
+
if (!oracle) {
|
|
95
|
+
display.log('Webhook recovery skipped — Oracle not available.', {
|
|
96
|
+
source: 'Webhooks',
|
|
97
|
+
level: 'warning',
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const repo = WebhookRepository.getInstance();
|
|
102
|
+
const taskRepo = TaskRepository.getInstance();
|
|
103
|
+
const stale = repo.findStaleNotifications(STALE_NOTIFICATION_THRESHOLD_MS);
|
|
104
|
+
if (stale.length === 0)
|
|
105
|
+
return;
|
|
106
|
+
display.log(`Recovering ${stale.length} stale webhook notification(s)...`, {
|
|
107
|
+
source: 'Webhooks',
|
|
108
|
+
level: 'warning',
|
|
109
|
+
});
|
|
110
|
+
for (const notification of stale) {
|
|
111
|
+
// Skip if a task is still active for this notification
|
|
112
|
+
const activeTask = taskRepo.findTaskByOriginMessageId(notification.id);
|
|
113
|
+
if (activeTask && (activeTask.status === 'pending' || activeTask.status === 'running')) {
|
|
114
|
+
display.log(`Webhook notification ${notification.id} has active task ${activeTask.id} — skipping recovery.`, { source: 'Webhooks' });
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const webhook = repo.getWebhookById(notification.webhook_id);
|
|
118
|
+
if (!webhook || !webhook.enabled) {
|
|
119
|
+
repo.updateNotificationResult(notification.id, 'failed', webhook ? 'Webhook was disabled.' : 'Webhook no longer exists.');
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
let payload;
|
|
123
|
+
try {
|
|
124
|
+
payload = JSON.parse(notification.payload);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
payload = {};
|
|
128
|
+
}
|
|
129
|
+
display.log(`Re-dispatching stale notification ${notification.id} for webhook "${webhook.name}".`, { source: 'Webhooks' });
|
|
130
|
+
// Sequential await — recovery dispatches run one at a time.
|
|
131
|
+
// Firing all at once would flood Oracle and cause concurrent
|
|
132
|
+
// EmbeddingService initializations, leading to repeated ONNX errors.
|
|
133
|
+
try {
|
|
134
|
+
const dispatcher = new WebhookDispatcher();
|
|
135
|
+
await dispatcher.dispatch(webhook, payload, notification.id);
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
display.log(`Recovery dispatch error for notification ${notification.id}: ${err.message}`, { source: 'Webhooks', level: 'error' });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
66
142
|
/**
|
|
67
143
|
* Sends a formatted Telegram message to all allowed users.
|
|
68
144
|
* Silently skips if the adapter is not connected.
|
|
@@ -180,6 +180,13 @@ export class WebhookRepository {
|
|
|
180
180
|
const row = this.db.prepare('SELECT COUNT(*) as cnt FROM webhook_notifications WHERE read = 0').get();
|
|
181
181
|
return row?.cnt ?? 0;
|
|
182
182
|
}
|
|
183
|
+
findStaleNotifications(olderThanMs) {
|
|
184
|
+
const cutoff = Date.now() - olderThanMs;
|
|
185
|
+
const rows = this.db.prepare(`SELECT * FROM webhook_notifications
|
|
186
|
+
WHERE status = 'pending' AND created_at < ?
|
|
187
|
+
ORDER BY created_at ASC`).all(cutoff);
|
|
188
|
+
return rows.map(this.deserializeNotification);
|
|
189
|
+
}
|
|
183
190
|
deserializeNotification(row) {
|
|
184
191
|
return {
|
|
185
192
|
id: row.id,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "morpheus-cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "Morpheus is a local AI agent for developers, running as a CLI daemon that connects to LLMs, local tools, and MCPs, enabling interaction via Terminal, Telegram, and Discord. Inspired by the character Morpheus from *The Matrix*, the project acts as an intelligent orchestrator, bridging the gap between the developer and complex systems.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"morpheus": "./bin/morpheus.js"
|