morpheus-cli 0.7.7 → 0.8.2
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 +58 -5
- package/dist/cli/commands/smiths.js +110 -0
- package/dist/cli/commands/start.js +20 -0
- package/dist/cli/index.js +2 -0
- package/dist/config/manager.js +22 -0
- package/dist/config/schemas.js +16 -0
- package/dist/http/api.js +3 -0
- package/dist/http/routers/smiths.js +188 -0
- package/dist/runtime/display.js +3 -0
- package/dist/runtime/oracle.js +16 -3
- package/dist/runtime/smiths/connection.js +302 -0
- package/dist/runtime/smiths/delegator.js +214 -0
- package/dist/runtime/smiths/index.js +4 -0
- package/dist/runtime/smiths/registry.js +276 -0
- package/dist/runtime/smiths/types.js +6 -0
- package/dist/runtime/tasks/worker.js +18 -0
- package/dist/runtime/tools/index.js +1 -0
- package/dist/runtime/tools/morpheus-tools.js +122 -0
- package/dist/runtime/tools/smith-tool.js +147 -0
- package/dist/types/config.js +8 -0
- package/dist/ui/assets/index-C8uUR62u.css +1 -0
- package/dist/ui/assets/index-mIH_kbig.js +117 -0
- package/dist/ui/index.html +2 -2
- package/dist/ui/sw.js +1 -1
- package/package.json +3 -1
- package/dist/ui/assets/index-B6deYCij.css +0 -1
- package/dist/ui/assets/index-BTQ0jjvm.js +0 -117
package/README.md
CHANGED
|
@@ -9,9 +9,10 @@ It runs as a daemon and orchestrates LLMs, MCP tools, DevKit tools, memory, and
|
|
|
9
9
|
|
|
10
10
|
## Why Morpheus
|
|
11
11
|
- Local-first persistence (sessions, messages, usage, tasks).
|
|
12
|
-
- Multi-agent architecture (Oracle, Neo, Apoc, Sati, Trinity).
|
|
12
|
+
- Multi-agent architecture (Oracle, Neo, Apoc, Sati, Trinity, Smith).
|
|
13
13
|
- Async task execution with queue + worker + notifier.
|
|
14
14
|
- Chronos temporal scheduler for recurring and one-time Oracle executions.
|
|
15
|
+
- Smith remote agent system for DevKit execution on isolated machines via WebSocket.
|
|
15
16
|
- Multi-channel output via ChannelRegistry (Telegram, Discord) with per-job routing.
|
|
16
17
|
- Rich operational visibility in UI (chat traces, tasks, usage, logs).
|
|
17
18
|
|
|
@@ -23,6 +24,7 @@ It runs as a daemon and orchestrates LLMs, MCP tools, DevKit tools, memory, and
|
|
|
23
24
|
- `Trinity`: database specialist. Executes queries, introspects schemas, and manages registered databases (PostgreSQL, MySQL, SQLite, MongoDB).
|
|
24
25
|
- `Chronos`: temporal scheduler. Runs Oracle prompts on a recurring or one-time schedule.
|
|
25
26
|
- `Keymaker`: skill executor. Runs user-defined skills with full tool access (DevKit + MCP + internal tools).
|
|
27
|
+
- `Smith`: remote DevKit executor. Runs DevKit operations on isolated machines (Docker, VMs, cloud) via WebSocket.
|
|
26
28
|
|
|
27
29
|
## Installation
|
|
28
30
|
|
|
@@ -169,10 +171,10 @@ docker run -d \
|
|
|
169
171
|
Morpheus uses asynchronous delegation by default:
|
|
170
172
|
|
|
171
173
|
1. Oracle receives user request.
|
|
172
|
-
2. If execution is needed, Oracle calls `neo_delegate`, `apoc_delegate`, or `
|
|
174
|
+
2. If execution is needed, Oracle calls `neo_delegate`, `apoc_delegate`, `trinity_delegate`, or `smith_delegate`.
|
|
173
175
|
3. Delegate tool creates a row in `tasks` table with origin metadata (`channel`, `session`, `message`, `user`).
|
|
174
176
|
4. Oracle immediately acknowledges task creation.
|
|
175
|
-
5. `TaskWorker` executes pending tasks (routes `trinit` tasks to Trinity agent).
|
|
177
|
+
5. `TaskWorker` executes pending tasks (routes `trinit` tasks to Trinity agent, `smith` tasks to Smith delegator).
|
|
176
178
|
6. `TaskNotifier` sends completion/failure through `TaskDispatcher`.
|
|
177
179
|
|
|
178
180
|
Important behavior:
|
|
@@ -217,6 +219,44 @@ Chronos lets you schedule any Oracle prompt to run at a fixed time or on a recur
|
|
|
217
219
|
- `POST /api/chronos/preview` — preview next N run timestamps
|
|
218
220
|
- `GET/POST/DELETE /api/config/chronos`
|
|
219
221
|
|
|
222
|
+
## Smith — Remote Agent System
|
|
223
|
+
|
|
224
|
+
Smith enables remote DevKit execution on isolated machines (Docker, VMs, cloud) via WebSocket.
|
|
225
|
+
|
|
226
|
+
**Architecture:**
|
|
227
|
+
- `SmithRegistry` manages WebSocket connections to remote Smith instances (singleton, non-blocking startup).
|
|
228
|
+
- `SmithDelegator` creates a LangChain ReactAgent with **proxy tools** — local DevKit tools built for schema extraction, filtered by Smith's declared capabilities, wrapped in proxies that forward execution to the remote Smith via WebSocket.
|
|
229
|
+
- Oracle delegates via `smith_delegate` tool (sync or async, like other subagents).
|
|
230
|
+
|
|
231
|
+
**Config (`zaion.yaml`):**
|
|
232
|
+
```yaml
|
|
233
|
+
smiths:
|
|
234
|
+
enabled: true
|
|
235
|
+
execution_mode: sync # or async
|
|
236
|
+
entries:
|
|
237
|
+
- name: smith1
|
|
238
|
+
host: localhost
|
|
239
|
+
port: 7778
|
|
240
|
+
auth_token: secret-token
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**Key behaviors:**
|
|
244
|
+
- **Hot-reload:** Config changes connect/disconnect Smiths without restart (via `PUT /api/smiths/config` or `smith_manage` tool).
|
|
245
|
+
- **Reconnection:** Max 3 attempts. 401 auth failures stop retries immediately.
|
|
246
|
+
- **Non-blocking startup:** Smith connection failures don't block daemon boot.
|
|
247
|
+
|
|
248
|
+
**Oracle tools:** `smith_list` (list Smiths + state), `smith_manage` (add/remove/ping/enable/disable)
|
|
249
|
+
|
|
250
|
+
**API endpoints (protected):**
|
|
251
|
+
- `GET /api/smiths` — list all Smiths with status
|
|
252
|
+
- `GET /api/smiths/config` — get Smiths configuration
|
|
253
|
+
- `PUT /api/smiths/config` — update config (hot-reload)
|
|
254
|
+
- `GET /api/smiths/:name` — get specific Smith details
|
|
255
|
+
- `POST /api/smiths/:name/ping` — ping a Smith
|
|
256
|
+
- `DELETE /api/smiths/:name` — remove a Smith
|
|
257
|
+
|
|
258
|
+
> **Learn more:** The Smith standalone agent has its own repository with setup instructions, Docker support, and protocol documentation at [github.com/marcosnunesmbs/smith](https://github.com/marcosnunesmbs/smith).
|
|
259
|
+
|
|
220
260
|
## Telegram Experience
|
|
221
261
|
|
|
222
262
|
Telegram responses use rich HTML formatting conversion with:
|
|
@@ -276,7 +316,7 @@ Adding a new channel requires only implementing `IChannelAdapter` (`channel`, `s
|
|
|
276
316
|
The dashboard includes:
|
|
277
317
|
- Chat with session management
|
|
278
318
|
- Tasks page (stats, filters, details, retry)
|
|
279
|
-
- Agent settings (Oracle/Sati/Neo/Apoc/Trinity)
|
|
319
|
+
- Agent settings (Oracle/Sati/Neo/Apoc/Trinity/Smiths)
|
|
280
320
|
- MCP manager (add/edit/delete/toggle/reload)
|
|
281
321
|
- Sati memories (search, bulk delete)
|
|
282
322
|
- Usage stats and model pricing
|
|
@@ -337,6 +377,14 @@ chronos:
|
|
|
337
377
|
check_interval_ms: 60000 # polling interval in ms (minimum 60000)
|
|
338
378
|
default_timezone: UTC # IANA timezone used when none is specified
|
|
339
379
|
|
|
380
|
+
smiths:
|
|
381
|
+
enabled: false
|
|
382
|
+
execution_mode: async # 'sync' = inline response, 'async' = background task
|
|
383
|
+
heartbeat_interval_ms: 30000
|
|
384
|
+
connection_timeout_ms: 10000
|
|
385
|
+
task_timeout_ms: 300000
|
|
386
|
+
entries: [] # list of { name, host, port, auth_token }
|
|
387
|
+
|
|
340
388
|
runtime:
|
|
341
389
|
async_tasks:
|
|
342
390
|
enabled: true
|
|
@@ -593,11 +641,12 @@ Authenticated endpoints (`x-architect-pass`):
|
|
|
593
641
|
- Sessions: `/api/sessions*`
|
|
594
642
|
- Chat: `POST /api/chat`
|
|
595
643
|
- Tasks: `GET /api/tasks`, `GET /api/tasks/stats`, `GET /api/tasks/:id`, `POST /api/tasks/:id/retry`
|
|
596
|
-
- Config: `/api/config`, `/api/config/sati`, `/api/config/neo`, `/api/config/apoc`, `/api/config/trinity`, `/api/config/chronos`
|
|
644
|
+
- Config: `/api/config`, `/api/config/sati`, `/api/config/neo`, `/api/config/apoc`, `/api/config/trinity`, `/api/config/chronos`, `/api/config/smiths`
|
|
597
645
|
- MCP: `/api/mcp/*` (servers CRUD + reload + status)
|
|
598
646
|
- Sati memories: `/api/sati/memories*`
|
|
599
647
|
- Trinity databases: `GET/POST/PUT/DELETE /api/trinity/databases`, `POST /api/trinity/databases/:id/test`, `POST /api/trinity/databases/:id/refresh-schema`
|
|
600
648
|
- Chronos: `GET/POST /api/chronos`, `GET/PUT/DELETE /api/chronos/:id`, `PATCH /api/chronos/:id/enable`, `PATCH /api/chronos/:id/disable`, `GET /api/chronos/:id/executions`, `POST /api/chronos/preview`
|
|
649
|
+
- Smiths: `GET /api/smiths`, `GET/PUT /api/smiths/config`, `GET/DELETE /api/smiths/:name`, `POST /api/smiths/:name/ping`
|
|
601
650
|
- Usage/model pricing/logs/restart
|
|
602
651
|
- Webhook management and webhook notifications
|
|
603
652
|
|
|
@@ -711,6 +760,10 @@ src/
|
|
|
711
760
|
worker.ts # polling timer and job execution
|
|
712
761
|
repository.ts # SQLite-backed job and execution store
|
|
713
762
|
parser.ts # natural-language schedule parser
|
|
763
|
+
smiths/
|
|
764
|
+
registry.ts # SmithRegistry — manages all connections
|
|
765
|
+
connection.ts # WebSocket client per Smith instance
|
|
766
|
+
delegator.ts # LLM agent with proxy tools for remote execution
|
|
714
767
|
memory/
|
|
715
768
|
tasks/
|
|
716
769
|
tools/
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { ConfigManager } from '../../config/manager.js';
|
|
4
|
+
export const smithsCommand = new Command('smiths')
|
|
5
|
+
.description('Manage remote Smith agents')
|
|
6
|
+
.action(async () => {
|
|
7
|
+
// Default action: list all Smiths
|
|
8
|
+
await ConfigManager.getInstance().load();
|
|
9
|
+
const config = ConfigManager.getInstance().getSmithsConfig();
|
|
10
|
+
if (!config.enabled) {
|
|
11
|
+
console.log(chalk.yellow('⚠') + ' Smiths subsystem is disabled.');
|
|
12
|
+
console.log(chalk.gray(' Enable it in zaion.yaml: smiths.enabled: true'));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (config.entries.length === 0) {
|
|
16
|
+
console.log(chalk.yellow('⚠') + ' No Smiths configured.');
|
|
17
|
+
console.log(chalk.gray(' Add entries in zaion.yaml under smiths.entries'));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
console.log(chalk.bold('Registered Smiths'));
|
|
21
|
+
console.log(chalk.gray('═══════════════════'));
|
|
22
|
+
for (const entry of config.entries) {
|
|
23
|
+
const stateIcon = '⚪'; // Offline by default (not connected via CLI)
|
|
24
|
+
console.log(` ${stateIcon} ${chalk.cyan(entry.name)} — ${entry.host}:${entry.port}`);
|
|
25
|
+
}
|
|
26
|
+
console.log();
|
|
27
|
+
console.log(chalk.gray(` Execution mode: ${config.execution_mode}`));
|
|
28
|
+
console.log(chalk.gray(` Heartbeat: ${config.heartbeat_interval_ms}ms`));
|
|
29
|
+
console.log(chalk.gray(` Total: ${config.entries.length} smith(s)`));
|
|
30
|
+
});
|
|
31
|
+
smithsCommand
|
|
32
|
+
.command('list')
|
|
33
|
+
.description('List all configured Smiths')
|
|
34
|
+
.action(async () => {
|
|
35
|
+
await ConfigManager.getInstance().load();
|
|
36
|
+
const config = ConfigManager.getInstance().getSmithsConfig();
|
|
37
|
+
if (config.entries.length === 0) {
|
|
38
|
+
console.log('No Smiths configured.');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
console.log(chalk.bold('Name Host Port'));
|
|
42
|
+
console.log(chalk.gray('────────────────────────────────────────────'));
|
|
43
|
+
for (const entry of config.entries) {
|
|
44
|
+
const name = entry.name.padEnd(16);
|
|
45
|
+
const host = entry.host.padEnd(24);
|
|
46
|
+
console.log(`${chalk.cyan(name)}${host}${entry.port}`);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
smithsCommand
|
|
50
|
+
.command('ping <name>')
|
|
51
|
+
.description('Test connectivity to a Smith')
|
|
52
|
+
.action(async (name) => {
|
|
53
|
+
await ConfigManager.getInstance().load();
|
|
54
|
+
const config = ConfigManager.getInstance().getSmithsConfig();
|
|
55
|
+
const entry = config.entries.find(e => e.name === name);
|
|
56
|
+
if (!entry) {
|
|
57
|
+
console.log(chalk.red(`✗ Smith '${name}' not found in configuration.`));
|
|
58
|
+
const available = config.entries.map(e => e.name).join(', ');
|
|
59
|
+
if (available)
|
|
60
|
+
console.log(chalk.gray(` Available: ${available}`));
|
|
61
|
+
process.exit(1);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
console.log(chalk.gray(`Pinging ${entry.name} at ${entry.host}:${entry.port}...`));
|
|
65
|
+
try {
|
|
66
|
+
// Simple TCP connectivity check
|
|
67
|
+
const { createConnection } = await import('net');
|
|
68
|
+
const socket = createConnection({ host: entry.host, port: entry.port, timeout: 5000 });
|
|
69
|
+
await new Promise((resolve, reject) => {
|
|
70
|
+
socket.on('connect', () => {
|
|
71
|
+
const latency = Date.now();
|
|
72
|
+
console.log(chalk.green('✓') + ` Smith '${name}' is reachable at ${entry.host}:${entry.port}`);
|
|
73
|
+
socket.end();
|
|
74
|
+
resolve();
|
|
75
|
+
});
|
|
76
|
+
socket.on('timeout', () => {
|
|
77
|
+
socket.destroy();
|
|
78
|
+
reject(new Error('Connection timeout'));
|
|
79
|
+
});
|
|
80
|
+
socket.on('error', (err) => {
|
|
81
|
+
reject(err);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
console.log(chalk.red('✗') + ` Smith '${name}' is unreachable: ${err.message}`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
smithsCommand
|
|
91
|
+
.command('remove <name>')
|
|
92
|
+
.description('Remove a Smith from configuration')
|
|
93
|
+
.action(async (name) => {
|
|
94
|
+
const configManager = ConfigManager.getInstance();
|
|
95
|
+
await configManager.load();
|
|
96
|
+
const config = configManager.get();
|
|
97
|
+
const entries = config.smiths?.entries ?? [];
|
|
98
|
+
const idx = entries.findIndex(e => e.name === name);
|
|
99
|
+
if (idx === -1) {
|
|
100
|
+
console.log(chalk.red(`✗ Smith '${name}' not found in configuration.`));
|
|
101
|
+
process.exit(1);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
entries.splice(idx, 1);
|
|
105
|
+
await configManager.save({
|
|
106
|
+
...config,
|
|
107
|
+
smiths: { ...config.smiths, entries },
|
|
108
|
+
});
|
|
109
|
+
console.log(chalk.green('✓') + ` Smith '${name}' removed from configuration.`);
|
|
110
|
+
});
|
|
@@ -25,6 +25,7 @@ import { ChronosWorker } from '../../runtime/chronos/worker.js';
|
|
|
25
25
|
import { ChronosRepository } from '../../runtime/chronos/repository.js';
|
|
26
26
|
import { SkillRegistry } from '../../runtime/skills/index.js';
|
|
27
27
|
import { MCPToolCache } from '../../runtime/tools/cache.js';
|
|
28
|
+
import { SmithRegistry } from '../../runtime/smiths/registry.js';
|
|
28
29
|
// Load .env file explicitly in start command
|
|
29
30
|
const envPath = path.join(process.cwd(), '.env');
|
|
30
31
|
if (fs.existsSync(envPath)) {
|
|
@@ -158,6 +159,20 @@ export const startCommand = new Command('start')
|
|
|
158
159
|
display.stopSpinner();
|
|
159
160
|
display.log(chalk.yellow(`MCP cache warning: ${err.message}`), { source: 'MCP' });
|
|
160
161
|
}
|
|
162
|
+
// Initialize SmithRegistry before Oracle (so Smiths are available in system prompt)
|
|
163
|
+
try {
|
|
164
|
+
const smithsConfig = config.smiths;
|
|
165
|
+
if (smithsConfig?.enabled && smithsConfig.entries.length > 0) {
|
|
166
|
+
const smithRegistry = SmithRegistry.getInstance();
|
|
167
|
+
await smithRegistry.connectAll();
|
|
168
|
+
const online = smithRegistry.getOnline().length;
|
|
169
|
+
const total = smithRegistry.list().length;
|
|
170
|
+
display.log(chalk.green(`✓ Smiths connected: ${online}/${total}`), { source: 'Smiths' });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
display.log(chalk.yellow(`Smiths initialization warning: ${err.message}`), { source: 'Smiths' });
|
|
175
|
+
}
|
|
161
176
|
// Initialize Oracle
|
|
162
177
|
const oracle = new Oracle(config);
|
|
163
178
|
try {
|
|
@@ -267,6 +282,11 @@ export const startCommand = new Command('start')
|
|
|
267
282
|
taskWorker.stop();
|
|
268
283
|
taskNotifier.stop();
|
|
269
284
|
}
|
|
285
|
+
// Disconnect all Smiths
|
|
286
|
+
try {
|
|
287
|
+
await SmithRegistry.getInstance().disconnectAll();
|
|
288
|
+
}
|
|
289
|
+
catch { }
|
|
270
290
|
await clearPid();
|
|
271
291
|
process.exit(0);
|
|
272
292
|
};
|
package/dist/cli/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { doctorCommand } from './commands/doctor.js';
|
|
|
7
7
|
import { initCommand } from './commands/init.js';
|
|
8
8
|
import { restartCommand } from './commands/restart.js';
|
|
9
9
|
import { sessionCommand } from './commands/session.js';
|
|
10
|
+
import { smithsCommand } from './commands/smiths.js';
|
|
10
11
|
import { scaffold } from '../runtime/scaffold.js';
|
|
11
12
|
import { getVersion } from './utils/version.js';
|
|
12
13
|
export async function cli() {
|
|
@@ -26,6 +27,7 @@ export async function cli() {
|
|
|
26
27
|
program.addCommand(configCommand);
|
|
27
28
|
program.addCommand(doctorCommand);
|
|
28
29
|
program.addCommand(sessionCommand);
|
|
30
|
+
program.addCommand(smithsCommand);
|
|
29
31
|
await program.parseAsync(process.argv);
|
|
30
32
|
}
|
|
31
33
|
// Support direct execution via tsx
|
package/dist/config/manager.js
CHANGED
|
@@ -317,6 +317,14 @@ export class ConfigManager {
|
|
|
317
317
|
memory: memoryConfig,
|
|
318
318
|
chronos: chronosConfig,
|
|
319
319
|
devkit: devkitConfig,
|
|
320
|
+
smiths: {
|
|
321
|
+
enabled: resolveBoolean('MORPHEUS_SMITHS_ENABLED', config.smiths?.enabled, false),
|
|
322
|
+
execution_mode: resolveString('MORPHEUS_SMITHS_EXECUTION_MODE', config.smiths?.execution_mode, 'async'),
|
|
323
|
+
heartbeat_interval_ms: resolveNumeric('MORPHEUS_SMITHS_HEARTBEAT_INTERVAL_MS', config.smiths?.heartbeat_interval_ms, 30000),
|
|
324
|
+
connection_timeout_ms: resolveNumeric('MORPHEUS_SMITHS_CONNECTION_TIMEOUT_MS', config.smiths?.connection_timeout_ms, 10000),
|
|
325
|
+
task_timeout_ms: resolveNumeric('MORPHEUS_SMITHS_TASK_TIMEOUT_MS', config.smiths?.task_timeout_ms, 60000),
|
|
326
|
+
entries: config.smiths?.entries ?? [],
|
|
327
|
+
},
|
|
320
328
|
verbose_mode: resolveBoolean('MORPHEUS_VERBOSE_MODE', config.verbose_mode, true),
|
|
321
329
|
};
|
|
322
330
|
}
|
|
@@ -401,6 +409,20 @@ export class ConfigManager {
|
|
|
401
409
|
}
|
|
402
410
|
return defaults;
|
|
403
411
|
}
|
|
412
|
+
getSmithsConfig() {
|
|
413
|
+
const defaults = {
|
|
414
|
+
enabled: false,
|
|
415
|
+
execution_mode: 'async',
|
|
416
|
+
heartbeat_interval_ms: 30000,
|
|
417
|
+
connection_timeout_ms: 10000,
|
|
418
|
+
task_timeout_ms: 60000,
|
|
419
|
+
entries: [],
|
|
420
|
+
};
|
|
421
|
+
if (this.config.smiths) {
|
|
422
|
+
return { ...defaults, ...this.config.smiths };
|
|
423
|
+
}
|
|
424
|
+
return defaults;
|
|
425
|
+
}
|
|
404
426
|
getDevKitConfig() {
|
|
405
427
|
const defaults = {
|
|
406
428
|
sandbox_dir: process.cwd(),
|
package/dist/config/schemas.js
CHANGED
|
@@ -54,6 +54,21 @@ export const DevKitConfigSchema = z.object({
|
|
|
54
54
|
enable_network: z.boolean().default(true),
|
|
55
55
|
timeout_ms: z.number().int().positive().default(30000),
|
|
56
56
|
});
|
|
57
|
+
export const SmithEntrySchema = z.object({
|
|
58
|
+
name: z.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9_-]*$/, 'Smith name must be lowercase alphanumeric with hyphens/underscores'),
|
|
59
|
+
host: z.string().min(1),
|
|
60
|
+
port: z.number().int().min(1).max(65535).default(7900),
|
|
61
|
+
auth_token: z.string().min(1),
|
|
62
|
+
tls: z.boolean().default(false),
|
|
63
|
+
});
|
|
64
|
+
export const SmithsConfigSchema = z.object({
|
|
65
|
+
enabled: z.boolean().default(false),
|
|
66
|
+
execution_mode: z.enum(['sync', 'async']).default('async'),
|
|
67
|
+
heartbeat_interval_ms: z.number().int().min(5000).default(30000),
|
|
68
|
+
connection_timeout_ms: z.number().int().min(1000).default(10000),
|
|
69
|
+
task_timeout_ms: z.number().int().min(1000).default(60000),
|
|
70
|
+
entries: z.array(SmithEntrySchema).default([]),
|
|
71
|
+
});
|
|
57
72
|
// Zod Schema matching MorpheusConfig interface
|
|
58
73
|
export const ConfigSchema = z.object({
|
|
59
74
|
agent: z.object({
|
|
@@ -78,6 +93,7 @@ export const ConfigSchema = z.object({
|
|
|
78
93
|
}).optional(),
|
|
79
94
|
chronos: ChronosConfigSchema.optional(),
|
|
80
95
|
devkit: DevKitConfigSchema.optional(),
|
|
96
|
+
smiths: SmithsConfigSchema.optional(),
|
|
81
97
|
verbose_mode: z.boolean().default(true),
|
|
82
98
|
channels: z.object({
|
|
83
99
|
telegram: z.object({
|
package/dist/http/api.js
CHANGED
|
@@ -19,6 +19,7 @@ import { ChronosRepository } from '../runtime/chronos/repository.js';
|
|
|
19
19
|
import { ChronosWorker } from '../runtime/chronos/worker.js';
|
|
20
20
|
import { createChronosJobRouter, createChronosConfigRouter } from './routers/chronos.js';
|
|
21
21
|
import { createSkillsRouter } from './routers/skills.js';
|
|
22
|
+
import { createSmithsRouter } from './routers/smiths.js';
|
|
22
23
|
import { getActiveEnvOverrides } from '../config/precedence.js';
|
|
23
24
|
import { hotReloadConfig, getRestartRequiredChanges } from '../runtime/hot-reload.js';
|
|
24
25
|
async function readLastLines(filePath, n) {
|
|
@@ -45,6 +46,8 @@ export function createApiRouter(oracle, chronosWorker) {
|
|
|
45
46
|
}
|
|
46
47
|
// Mount Skills router
|
|
47
48
|
router.use('/skills', createSkillsRouter());
|
|
49
|
+
// Mount Smiths router
|
|
50
|
+
router.use('/smiths', createSmithsRouter());
|
|
48
51
|
// --- Session Management ---
|
|
49
52
|
router.get('/sessions', async (req, res) => {
|
|
50
53
|
try {
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { SmithRegistry } from '../../runtime/smiths/registry.js';
|
|
3
|
+
import { SmithDelegator } from '../../runtime/smiths/delegator.js';
|
|
4
|
+
import { ConfigManager } from '../../config/manager.js';
|
|
5
|
+
import { DisplayManager } from '../../runtime/display.js';
|
|
6
|
+
/**
|
|
7
|
+
* Creates the Smiths API router.
|
|
8
|
+
* Follows the factory-function pattern from chronos.ts.
|
|
9
|
+
*/
|
|
10
|
+
export function createSmithsRouter() {
|
|
11
|
+
const router = Router();
|
|
12
|
+
const registry = SmithRegistry.getInstance();
|
|
13
|
+
const delegator = SmithDelegator.getInstance();
|
|
14
|
+
const display = DisplayManager.getInstance();
|
|
15
|
+
/**
|
|
16
|
+
* GET /api/smiths — List all registered Smiths with status
|
|
17
|
+
*/
|
|
18
|
+
router.get('/', (_req, res) => {
|
|
19
|
+
try {
|
|
20
|
+
const smiths = registry.list().map(s => ({
|
|
21
|
+
name: s.name,
|
|
22
|
+
host: s.host,
|
|
23
|
+
port: s.port,
|
|
24
|
+
state: s.state,
|
|
25
|
+
capabilities: s.capabilities,
|
|
26
|
+
stats: s.stats ?? null,
|
|
27
|
+
lastSeen: s.lastSeen?.toISOString() ?? null,
|
|
28
|
+
error: s.error ?? null,
|
|
29
|
+
}));
|
|
30
|
+
res.json({
|
|
31
|
+
enabled: ConfigManager.getInstance().getSmithsConfig().enabled,
|
|
32
|
+
total: smiths.length,
|
|
33
|
+
online: smiths.filter(s => s.state === 'online').length,
|
|
34
|
+
smiths,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
res.status(500).json({ error: err.message });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
/**
|
|
42
|
+
* GET /api/smiths/config — Get Smiths configuration
|
|
43
|
+
* NOTE: Must be defined BEFORE /:name to avoid "config" matching as a name param.
|
|
44
|
+
*/
|
|
45
|
+
router.get('/config', (_req, res) => {
|
|
46
|
+
try {
|
|
47
|
+
const config = ConfigManager.getInstance().getSmithsConfig();
|
|
48
|
+
// Omit auth_tokens from response for security
|
|
49
|
+
const safeEntries = config.entries.map(({ auth_token, ...rest }) => ({
|
|
50
|
+
...rest,
|
|
51
|
+
auth_token: '***',
|
|
52
|
+
}));
|
|
53
|
+
res.json({
|
|
54
|
+
enabled: config.enabled,
|
|
55
|
+
execution_mode: config.execution_mode,
|
|
56
|
+
heartbeat_interval_ms: config.heartbeat_interval_ms,
|
|
57
|
+
connection_timeout_ms: config.connection_timeout_ms,
|
|
58
|
+
task_timeout_ms: config.task_timeout_ms,
|
|
59
|
+
entries: safeEntries,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
res.status(500).json({ error: err.message });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
/**
|
|
67
|
+
* PUT /api/smiths/config — Update Smiths configuration
|
|
68
|
+
*/
|
|
69
|
+
router.put('/config', async (req, res) => {
|
|
70
|
+
try {
|
|
71
|
+
const configManager = ConfigManager.getInstance();
|
|
72
|
+
const currentConfig = configManager.get();
|
|
73
|
+
// Preserve masked auth_tokens: if UI sends '***', keep the existing token
|
|
74
|
+
const incomingEntries = req.body.entries;
|
|
75
|
+
if (Array.isArray(incomingEntries)) {
|
|
76
|
+
const existingEntries = currentConfig.smiths?.entries ?? [];
|
|
77
|
+
req.body.entries = incomingEntries.map((entry) => {
|
|
78
|
+
// Preserve existing token when UI sends sentinel '***' or leaves field blank
|
|
79
|
+
if (entry.auth_token === '***' || entry.auth_token === '') {
|
|
80
|
+
const existing = existingEntries.find((e) => e.name === entry.name);
|
|
81
|
+
return { ...entry, auth_token: existing?.auth_token ?? '' };
|
|
82
|
+
}
|
|
83
|
+
return entry;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const updated = {
|
|
87
|
+
...currentConfig,
|
|
88
|
+
smiths: {
|
|
89
|
+
...currentConfig.smiths,
|
|
90
|
+
...req.body,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
await configManager.save(updated);
|
|
94
|
+
// Hot-reload: connect new Smiths, disconnect removed ones
|
|
95
|
+
const { added, removed } = await registry.reload();
|
|
96
|
+
res.json({ status: 'updated', added, removed });
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
res.status(500).json({ error: err.message });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
/**
|
|
103
|
+
* GET /api/smiths/:name — Get a specific Smith's details
|
|
104
|
+
*/
|
|
105
|
+
router.get('/:name', (req, res) => {
|
|
106
|
+
try {
|
|
107
|
+
const smith = registry.get(req.params.name);
|
|
108
|
+
if (!smith) {
|
|
109
|
+
return res.status(404).json({ error: `Smith '${req.params.name}' not found` });
|
|
110
|
+
}
|
|
111
|
+
res.json({
|
|
112
|
+
name: smith.name,
|
|
113
|
+
host: smith.host,
|
|
114
|
+
port: smith.port,
|
|
115
|
+
state: smith.state,
|
|
116
|
+
capabilities: smith.capabilities,
|
|
117
|
+
stats: smith.stats ?? null,
|
|
118
|
+
lastSeen: smith.lastSeen?.toISOString() ?? null,
|
|
119
|
+
error: smith.error ?? null,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
res.status(500).json({ error: err.message });
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
/**
|
|
127
|
+
* POST /api/smiths/register — Self-registration endpoint (Smith calls this on boot)
|
|
128
|
+
*/
|
|
129
|
+
router.post('/register', (req, res) => {
|
|
130
|
+
try {
|
|
131
|
+
const { name, host, port, auth_token, capabilities } = req.body;
|
|
132
|
+
if (!name || !host || !auth_token) {
|
|
133
|
+
return res.status(400).json({ error: 'Missing required fields: name, host, auth_token' });
|
|
134
|
+
}
|
|
135
|
+
// Validate auth token against configured entries
|
|
136
|
+
const config = ConfigManager.getInstance().getSmithsConfig();
|
|
137
|
+
const configEntry = config.entries.find(e => e.name === name);
|
|
138
|
+
if (configEntry && configEntry.auth_token !== auth_token) {
|
|
139
|
+
display.log(`Smith '${name}' registration rejected: invalid auth token`, {
|
|
140
|
+
source: 'SmithsAPI',
|
|
141
|
+
level: 'warning',
|
|
142
|
+
});
|
|
143
|
+
return res.status(401).json({ error: 'Invalid authentication token' });
|
|
144
|
+
}
|
|
145
|
+
registry.registerFromHandshake(name, host, port ?? 7900, capabilities ?? []);
|
|
146
|
+
display.log(`Smith '${name}' registered via HTTP handshake`, {
|
|
147
|
+
source: 'SmithsAPI',
|
|
148
|
+
level: 'info',
|
|
149
|
+
});
|
|
150
|
+
res.json({ status: 'registered', name });
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
res.status(500).json({ error: err.message });
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
/**
|
|
157
|
+
* POST /api/smiths/:name/ping — Manual ping to test connectivity
|
|
158
|
+
*/
|
|
159
|
+
router.post('/:name/ping', async (req, res) => {
|
|
160
|
+
try {
|
|
161
|
+
const result = await delegator.ping(req.params.name);
|
|
162
|
+
res.json({
|
|
163
|
+
online: result.online,
|
|
164
|
+
latency_ms: result.latencyMs ?? null,
|
|
165
|
+
error: result.error ?? null,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
res.status(500).json({ error: err.message });
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
/**
|
|
173
|
+
* DELETE /api/smiths/:name — Remove a Smith
|
|
174
|
+
*/
|
|
175
|
+
router.delete('/:name', (req, res) => {
|
|
176
|
+
try {
|
|
177
|
+
const removed = registry.unregister(req.params.name);
|
|
178
|
+
if (!removed) {
|
|
179
|
+
return res.status(404).json({ error: `Smith '${req.params.name}' not found` });
|
|
180
|
+
}
|
|
181
|
+
res.json({ status: 'removed', name: req.params.name });
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
res.status(500).json({ error: err.message });
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
return router;
|
|
188
|
+
}
|
package/dist/runtime/display.js
CHANGED
|
@@ -101,6 +101,9 @@ export class DisplayManager {
|
|
|
101
101
|
else if (options.source === 'Chronos') {
|
|
102
102
|
color = chalk.hex('#a855f7');
|
|
103
103
|
}
|
|
104
|
+
else if (options.source === 'SmithDelegateTool' || options.source === 'SmithRegistry' || options.source === 'Smiths' || options.source === 'SmithConnection' || options.source === 'SmithDelegator') {
|
|
105
|
+
color = chalk.hex('#ff007f');
|
|
106
|
+
}
|
|
104
107
|
prefix = color(`[${options.source}] `);
|
|
105
108
|
}
|
|
106
109
|
let formattedMessage = message;
|
package/dist/runtime/oracle.js
CHANGED
|
@@ -13,10 +13,12 @@ import { Trinity } from "./trinity.js";
|
|
|
13
13
|
import { NeoDelegateTool } from "./tools/neo-tool.js";
|
|
14
14
|
import { ApocDelegateTool } from "./tools/apoc-tool.js";
|
|
15
15
|
import { TrinityDelegateTool } from "./tools/trinity-tool.js";
|
|
16
|
+
import { SmithDelegateTool } from "./tools/smith-tool.js";
|
|
16
17
|
import { TaskQueryTool, chronosTools, timeVerifierTool } from "./tools/index.js";
|
|
17
18
|
import { Construtor } from "./tools/factory.js";
|
|
18
19
|
import { MCPManager } from "../config/mcp-manager.js";
|
|
19
20
|
import { SkillRegistry, SkillExecuteTool, SkillDelegateTool, updateSkillToolDescriptions } from "./skills/index.js";
|
|
21
|
+
import { SmithRegistry } from "./smiths/registry.js";
|
|
20
22
|
export class Oracle {
|
|
21
23
|
provider;
|
|
22
24
|
config;
|
|
@@ -110,7 +112,7 @@ export class Oracle {
|
|
|
110
112
|
const toolCalls = msg.tool_calls ?? [];
|
|
111
113
|
if (!Array.isArray(toolCalls))
|
|
112
114
|
continue;
|
|
113
|
-
if (toolCalls.some((tc) => tc?.name === "apoc_delegate" || tc?.name === "neo_delegate" || tc?.name === "trinity_delegate")) {
|
|
115
|
+
if (toolCalls.some((tc) => tc?.name === "apoc_delegate" || tc?.name === "neo_delegate" || tc?.name === "trinity_delegate" || tc?.name === "smith_delegate")) {
|
|
114
116
|
return true;
|
|
115
117
|
}
|
|
116
118
|
}
|
|
@@ -146,7 +148,13 @@ export class Oracle {
|
|
|
146
148
|
await Neo.refreshDelegateCatalog().catch(() => { });
|
|
147
149
|
await Trinity.refreshDelegateCatalog().catch(() => { });
|
|
148
150
|
updateSkillToolDescriptions();
|
|
149
|
-
|
|
151
|
+
// Build tool list — conditionally include SmithDelegateTool based on config
|
|
152
|
+
const coreTools = [TaskQueryTool, NeoDelegateTool, ApocDelegateTool, TrinityDelegateTool, SkillExecuteTool, SkillDelegateTool, timeVerifierTool, ...chronosTools];
|
|
153
|
+
const smithsConfig = ConfigManager.getInstance().getSmithsConfig();
|
|
154
|
+
if (smithsConfig.enabled && smithsConfig.entries.length > 0) {
|
|
155
|
+
coreTools.push(SmithDelegateTool);
|
|
156
|
+
}
|
|
157
|
+
this.provider = await ProviderFactory.create(this.config.llm, coreTools);
|
|
150
158
|
if (!this.provider) {
|
|
151
159
|
throw new Error("Provider factory returned undefined");
|
|
152
160
|
}
|
|
@@ -266,7 +274,11 @@ Use ONLY when user provides a cron expression or very specific recurring pattern
|
|
|
266
274
|
- "toda segunda e quarta às 3pm" → cron, "0 15 * * 1,3"
|
|
267
275
|
|
|
268
276
|
**IMPORTANT**: Default to "once" for reminders unless user explicitly indicates recurrence with "a cada", "todo", "diariamente", etc.
|
|
269
|
-
|
|
277
|
+
**ALWAYS** set the expression Schedule Expression "literal" when user say the specif day and/or hour, dont parse the timezone or full datetime.
|
|
278
|
+
Ex: - "me lembre de algo às 17h" → schedule_type: "once", expression: "today at 17:00"
|
|
279
|
+
- "me lembre de algo hoje às 9h" → schedule_type: "once", expression: "today at 9:00"
|
|
280
|
+
- "me lembre de algo amanhã às 14h" → schedule_type: "once", expression: "tomorrow at 14:00"
|
|
281
|
+
- "me lembre de algo na próxima segunda às 8h" → schedule_type: "once", expression: "next Monday at 8:00"
|
|
270
282
|
## Chronos Scheduled Execution
|
|
271
283
|
When the current user message starts with [CHRONOS EXECUTION], it means a Chronos scheduled job has just fired. The content after the prefix is the **job's saved prompt**, not a new live request from the user.
|
|
272
284
|
|
|
@@ -308,6 +320,7 @@ bad:
|
|
|
308
320
|
- delegate to "neo_delegate" or "apoc_delegate" to save the fact. (Sati handles this automatically in the background)
|
|
309
321
|
|
|
310
322
|
${SkillRegistry.getInstance().getSystemPromptSection()}
|
|
323
|
+
${SmithRegistry.getInstance().getSystemPromptSection()}
|
|
311
324
|
`);
|
|
312
325
|
// Load existing history from database in reverse order (most recent first)
|
|
313
326
|
let previousMessages = await this.history.getMessages();
|