morpheus-cli 0.7.7 → 0.8.0

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
@@ -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 `trinity_delegate`.
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
@@ -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(),
@@ -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
+ }
@@ -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;
@@ -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
- this.provider = await ProviderFactory.create(this.config.llm, [TaskQueryTool, NeoDelegateTool, ApocDelegateTool, TrinityDelegateTool, SkillExecuteTool, SkillDelegateTool, timeVerifierTool, ...chronosTools]);
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();