morpheus-cli 0.9.1 → 0.9.4

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.
Files changed (54) hide show
  1. package/README.md +18 -4
  2. package/dist/config/manager.js +11 -0
  3. package/dist/config/schemas.js +5 -0
  4. package/dist/http/api.js +3 -0
  5. package/dist/http/routers/danger.js +137 -0
  6. package/dist/runtime/audit/repository.js +2 -0
  7. package/dist/runtime/memory/sati/index.js +1 -1
  8. package/dist/runtime/memory/sati/service.js +27 -0
  9. package/dist/runtime/memory/session-embedding-worker.js +43 -36
  10. package/dist/runtime/oracle.js +30 -2
  11. package/dist/runtime/setup/__tests__/repository.test.js +115 -0
  12. package/dist/runtime/setup/repository.js +87 -0
  13. package/dist/runtime/tools/setup-tool.js +57 -0
  14. package/dist/ui/assets/AuditDashboard-C1f6Hbdw.js +1 -0
  15. package/dist/ui/assets/{Chat-ChsmnZzq.js → Chat-5AeRYuRj.js} +2 -2
  16. package/dist/ui/assets/{Chronos-kgO7IkEj.js → Chronos-BrKldYVw.js} +1 -1
  17. package/dist/ui/assets/{ConfirmationModal-D1BYPXJ4.js → ConfirmationModal-DsbS3XkJ.js} +1 -1
  18. package/dist/ui/assets/{Dashboard-DWB5NwQn.js → Dashboard-DvrTXLdo.js} +1 -1
  19. package/dist/ui/assets/{DeleteConfirmationModal-CgIMbyB7.js → DeleteConfirmationModal-BfSjv04R.js} +1 -1
  20. package/dist/ui/assets/{Logs-DGdRnEFi.js → Logs-B0ZYWs5x.js} +1 -1
  21. package/dist/ui/assets/MCPManager-BwHGTeNs.js +1 -0
  22. package/dist/ui/assets/{ModelPricing-DAk1sS7D.js → ModelPricing-CYhGRQr8.js} +1 -1
  23. package/dist/ui/assets/{Notifications-DMEq6EZR.js → Notifications-BYMAtVMq.js} +1 -1
  24. package/dist/ui/assets/{Pagination-JsiwxVNQ.js → Pagination-oTGieBLM.js} +1 -1
  25. package/dist/ui/assets/SatiMemories-I1vsYtP2.js +1 -0
  26. package/dist/ui/assets/SessionAudit-BCecQWde.js +9 -0
  27. package/dist/ui/assets/Settings-Cu4D-7tb.js +47 -0
  28. package/dist/ui/assets/Skills-lGU3I5DO.js +7 -0
  29. package/dist/ui/assets/Smiths-DnEH3nID.js +1 -0
  30. package/dist/ui/assets/Tasks-Bz92GPWK.js +1 -0
  31. package/dist/ui/assets/{TrinityDatabases-BmM1S9aQ.js → TrinityDatabases-BUY-3j7Q.js} +1 -1
  32. package/dist/ui/assets/{UsageStats-aAu2DFlb.js → UsageStats-Dr5eSgJc.js} +1 -1
  33. package/dist/ui/assets/{WebhookManager-DdnRSWl9.js → WebhookManager-DIASAC-1.js} +1 -1
  34. package/dist/ui/assets/{audit-CqszEkOd.js → audit-CcAEDbZh.js} +1 -1
  35. package/dist/ui/assets/{chronos-CPwFWid9.js → chronos-2Z9E96_1.js} +1 -1
  36. package/dist/ui/assets/{config-D0DePxKu.js → config-DdfK4DX6.js} +1 -1
  37. package/dist/ui/assets/index-D4fzIKy1.css +1 -0
  38. package/dist/ui/assets/{index-BxVeRyTh.js → index-Dpd1Mkgp.js} +5 -5
  39. package/dist/ui/assets/{mcp-Gjc3IZpO.js → mcp-BWMt8aY7.js} +1 -1
  40. package/dist/ui/assets/{skills-B5DnmnHW.js → skills-D7JjK7JH.js} +1 -1
  41. package/dist/ui/assets/{stats-BAse7jj0.js → stats-DoIhtLot.js} +1 -1
  42. package/dist/ui/assets/{vendor-icons-BVuQI-6R.js → vendor-icons-DMd9RGvJ.js} +1 -1
  43. package/dist/ui/index.html +3 -3
  44. package/dist/ui/sw.js +1 -1
  45. package/package.json +1 -1
  46. package/dist/ui/assets/AuditDashboard-nVV9KKFp.js +0 -1
  47. package/dist/ui/assets/MCPManager-BDjWMRRX.js +0 -1
  48. package/dist/ui/assets/SatiMemories-BxicQE35.js +0 -1
  49. package/dist/ui/assets/SessionAudit-CKJQf9LU.js +0 -9
  50. package/dist/ui/assets/Settings-CulMd4Qr.js +0 -41
  51. package/dist/ui/assets/Skills-DPoqYa8Y.js +0 -7
  52. package/dist/ui/assets/Smiths-Clamjlph.js +0 -1
  53. package/dist/ui/assets/Tasks-BfTkhB1J.js +0 -1
  54. package/dist/ui/assets/index-OLhpm8I7.css +0 -1
package/README.md CHANGED
@@ -266,6 +266,13 @@ Telegram responses use rich HTML formatting conversion with:
266
266
 
267
267
  Task results are delivered proactively with metadata (task id, agent, status) and output/error body.
268
268
 
269
+ **Session commands:**
270
+ - `/session` — Show current session info
271
+ - `/session list` — List recent sessions
272
+ - `/session new` — Create new session
273
+ - `/session switch <id>` — Switch to existing session
274
+ - `/session rename <name>` — Rename current session
275
+
269
276
  **Voice messages:** Telegram voice messages are automatically transcribed (Gemini / Whisper / OpenRouter) and processed as text through the Oracle.
270
277
 
271
278
  ## Discord Experience
@@ -279,7 +286,11 @@ Discord bot responds to **DMs only** from authorized user IDs (`allowedUsers`).
279
286
  | `/help` | Show available commands |
280
287
  | `/status` | Check Morpheus status |
281
288
  | `/stats` | Token usage statistics |
282
- | `/newsession` | Start a new session |
289
+ | `/session` | Show current session info |
290
+ | `/session_list` | List recent sessions |
291
+ | `/session_new` | Start a new session |
292
+ | `/session_switch id:` | Switch to existing session |
293
+ | `/session_rename name:` | Rename current session |
283
294
  | `/mcps` | List MCP servers with tool counts |
284
295
  | `/mcpreload` | Reload MCP connections and tools |
285
296
  | `/mcp_enable name:` | Enable an MCP server |
@@ -314,16 +325,19 @@ Adding a new channel requires only implementing `IChannelAdapter` (`channel`, `s
314
325
  ## Web UI
315
326
 
316
327
  The dashboard includes:
317
- - Chat with session management
318
- - Tasks page (stats, filters, details, retry)
328
+ - Chat with session management and browser notifications
329
+ - Tasks page (stats, filters, details, retry, pagination)
319
330
  - Agent settings (Oracle/Sati/Neo/Apoc/Trinity/Smiths)
320
331
  - MCP manager (add/edit/delete/toggle/reload)
321
- - Sati memories (search, bulk delete)
332
+ - Sati memories (search, bulk delete, pagination)
322
333
  - Usage stats and model pricing
323
334
  - Trinity databases (register/test/refresh schema)
324
335
  - Chronos scheduler (create/edit/delete jobs, execution history)
336
+ - Smiths management (add/edit/delete, real-time status, ping)
337
+ - Audit dashboard (session audit, tool call tracking, cost breakdowns)
325
338
  - Webhooks and notification inbox
326
339
  - Logs viewer
340
+ - Danger Zone (Settings → reset sessions, tasks, jobs, audit, or factory reset)
327
341
 
328
342
  Chat-specific rendering:
329
343
  - AI messages rendered as markdown
@@ -328,6 +328,10 @@ export class ConfigManager {
328
328
  entries: config.smiths?.entries ?? [],
329
329
  },
330
330
  verbose_mode: resolveBoolean('MORPHEUS_VERBOSE_MODE', config.verbose_mode, true),
331
+ setup: {
332
+ enabled: resolveBoolean('MORPHEUS_SETUP_ENABLED', config.setup?.enabled, true),
333
+ fields: config.setup?.fields ?? ['name', 'timezone', 'preferred_language'],
334
+ },
331
335
  };
332
336
  }
333
337
  get() {
@@ -411,6 +415,13 @@ export class ConfigManager {
411
415
  }
412
416
  return defaults;
413
417
  }
418
+ getSetupConfig() {
419
+ const defaults = { enabled: true, fields: ['name', 'timezone', 'preferred_language'] };
420
+ if (this.config.setup) {
421
+ return { ...defaults, ...this.config.setup };
422
+ }
423
+ return defaults;
424
+ }
414
425
  getSmithsConfig() {
415
426
  const defaults = {
416
427
  enabled: false,
@@ -63,6 +63,10 @@ export const SmithEntrySchema = z.object({
63
63
  auth_token: z.string().min(1),
64
64
  tls: z.boolean().default(false),
65
65
  });
66
+ export const SetupConfigSchema = z.object({
67
+ enabled: z.boolean().default(true),
68
+ fields: z.array(z.string()).default(['name', 'timezone', 'preferred_language']),
69
+ });
66
70
  export const SmithsConfigSchema = z.object({
67
71
  enabled: z.boolean().default(false),
68
72
  execution_mode: z.enum(['sync', 'async']).default('async'),
@@ -96,6 +100,7 @@ export const ConfigSchema = z.object({
96
100
  chronos: ChronosConfigSchema.optional(),
97
101
  devkit: DevKitConfigSchema.optional(),
98
102
  smiths: SmithsConfigSchema.optional(),
103
+ setup: SetupConfigSchema.optional(),
99
104
  verbose_mode: z.boolean().default(true),
100
105
  channels: z.object({
101
106
  telegram: z.object({
package/dist/http/api.js CHANGED
@@ -20,6 +20,7 @@ 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
22
  import { createSmithsRouter } from './routers/smiths.js';
23
+ import { createDangerRouter } from './routers/danger.js';
23
24
  import { getActiveEnvOverrides } from '../config/precedence.js';
24
25
  import { hotReloadConfig, getRestartRequiredChanges } from '../runtime/hot-reload.js';
25
26
  import { AuditRepository } from '../runtime/audit/repository.js';
@@ -49,6 +50,8 @@ export function createApiRouter(oracle, chronosWorker) {
49
50
  router.use('/skills', createSkillsRouter());
50
51
  // Mount Smiths router
51
52
  router.use('/smiths', createSmithsRouter());
53
+ // Mount Danger Zone router
54
+ router.use('/danger', createDangerRouter());
52
55
  // --- Session Management ---
53
56
  router.get('/sessions', async (req, res) => {
54
57
  try {
@@ -0,0 +1,137 @@
1
+ import { Router } from 'express';
2
+ import Database from 'better-sqlite3';
3
+ import path from 'path';
4
+ import { homedir } from 'os';
5
+ import fs from 'fs-extra';
6
+ import { z } from 'zod';
7
+ import { SatiRepository } from '../../runtime/memory/sati/repository.js';
8
+ import { DisplayManager } from '../../runtime/display.js';
9
+ /**
10
+ * Valid categories the user can choose to delete.
11
+ */
12
+ const VALID_CATEGORIES = [
13
+ 'sessions', // sessions + messages
14
+ 'memories', // sati-memory.db (long-term memory, embeddings, session chunks)
15
+ 'tasks', // background tasks
16
+ 'audit', // audit_events
17
+ 'chronos', // chronos_jobs + chronos_executions
18
+ 'webhooks', // webhooks + webhook_notifications
19
+ ];
20
+ const ResetBodySchema = z.object({
21
+ categories: z.array(z.enum(VALID_CATEGORIES)).min(1, 'At least one category must be selected'),
22
+ });
23
+ /**
24
+ * Creates the Danger Zone API router.
25
+ * Provides destructive operations for resetting user data.
26
+ */
27
+ export function createDangerRouter() {
28
+ const router = Router();
29
+ const display = DisplayManager.getInstance();
30
+ /**
31
+ * GET /api/danger/categories — List available reset categories
32
+ */
33
+ router.get('/categories', (_req, res) => {
34
+ res.json({
35
+ categories: VALID_CATEGORIES.map((id) => ({ id })),
36
+ });
37
+ });
38
+ /**
39
+ * DELETE /api/danger/reset — Purge selected user data
40
+ *
41
+ * Body: { categories: ['sessions', 'memories', 'tasks', 'audit', 'chronos', 'webhooks'] }
42
+ */
43
+ router.delete('/reset', async (req, res) => {
44
+ // Validate body
45
+ const parsed = ResetBodySchema.safeParse(req.body);
46
+ if (!parsed.success) {
47
+ return res.status(400).json({
48
+ error: 'Invalid request',
49
+ details: parsed.error.issues.map((i) => i.message),
50
+ });
51
+ }
52
+ const { categories } = parsed.data;
53
+ try {
54
+ const memoryDir = path.join(homedir(), '.morpheus', 'memory');
55
+ const shortMemoryPath = path.join(memoryDir, 'short-memory.db');
56
+ const satiMemoryPath = path.join(memoryDir, 'sati-memory.db');
57
+ const counts = {};
58
+ // ─── 1. Purge short-memory.db tables based on selected categories ───
59
+ const needsShortDb = categories.some((c) => ['sessions', 'tasks', 'audit', 'chronos', 'webhooks'].includes(c));
60
+ if (needsShortDb && fs.existsSync(shortMemoryPath)) {
61
+ const db = new Database(shortMemoryPath, { timeout: 5000 });
62
+ db.pragma('journal_mode = WAL');
63
+ const transaction = db.transaction(() => {
64
+ if (categories.includes('sessions')) {
65
+ const msgResult = db.prepare('DELETE FROM messages').run();
66
+ counts.messages = msgResult.changes;
67
+ const sessResult = db.prepare('DELETE FROM sessions').run();
68
+ counts.sessions = sessResult.changes;
69
+ // Also clear first-time setup state so onboarding runs again after reset
70
+ try {
71
+ db.prepare('DELETE FROM setup_state').run();
72
+ }
73
+ catch {
74
+ // Table may not exist on older installations — safe to ignore
75
+ }
76
+ }
77
+ if (categories.includes('tasks')) {
78
+ const taskResult = db.prepare('DELETE FROM tasks').run();
79
+ counts.tasks = taskResult.changes;
80
+ }
81
+ if (categories.includes('audit')) {
82
+ const auditResult = db.prepare('DELETE FROM audit_events').run();
83
+ counts.audit_events = auditResult.changes;
84
+ }
85
+ if (categories.includes('chronos')) {
86
+ const jobsResult = db.prepare('DELETE FROM chronos_jobs').run();
87
+ counts.chronos_jobs = jobsResult.changes;
88
+ const execResult = db.prepare('DELETE FROM chronos_executions').run();
89
+ counts.chronos_executions = execResult.changes;
90
+ }
91
+ if (categories.includes('webhooks')) {
92
+ const notifResult = db.prepare('DELETE FROM webhook_notifications').run();
93
+ counts.webhook_notifications = notifResult.changes;
94
+ const whResult = db.prepare('DELETE FROM webhooks').run();
95
+ counts.webhooks = whResult.changes;
96
+ }
97
+ });
98
+ transaction();
99
+ db.close();
100
+ }
101
+ // ─── 2. Purge sati-memory.db (close, delete, recreate) ───
102
+ if (categories.includes('memories')) {
103
+ const satiRepo = SatiRepository.getInstance();
104
+ satiRepo.close();
105
+ if (fs.existsSync(satiMemoryPath)) {
106
+ fs.removeSync(satiMemoryPath);
107
+ fs.removeSync(satiMemoryPath + '-wal');
108
+ fs.removeSync(satiMemoryPath + '-shm');
109
+ counts.sati_memory = 1;
110
+ }
111
+ // Reinitialize so schema is recreated cleanly
112
+ satiRepo.initialize();
113
+ }
114
+ display.log(`🗑️ Data reset via Danger Zone: [${categories.join(', ')}]`, {
115
+ source: 'DangerZone',
116
+ level: 'warning',
117
+ });
118
+ res.json({
119
+ success: true,
120
+ message: 'Selected data has been reset successfully.',
121
+ categories,
122
+ deleted: counts,
123
+ });
124
+ }
125
+ catch (error) {
126
+ display.log(`❌ Danger Zone reset failed: ${error}`, {
127
+ source: 'DangerZone',
128
+ level: 'error',
129
+ });
130
+ res.status(500).json({
131
+ error: 'Failed to reset data',
132
+ details: error instanceof Error ? error.message : String(error),
133
+ });
134
+ }
135
+ });
136
+ return router;
137
+ }
@@ -190,6 +190,7 @@ export class AuditRepository {
190
190
  SUM(CASE WHEN ae.event_type = 'mcp_tool' THEN 1 ELSE 0 END) as mcpToolCount,
191
191
  SUM(CASE WHEN ae.event_type = 'skill_executed' THEN 1 ELSE 0 END) as skillCount,
192
192
  SUM(CASE WHEN ae.event_type = 'memory_recovery' THEN 1 ELSE 0 END) as memoryRecoveryCount,
193
+ SUM(CASE WHEN ae.event_type = 'memory_persist' THEN 1 ELSE 0 END) as memoryPersistCount,
193
194
  SUM(CASE WHEN ae.event_type = 'chronos_job' THEN 1 ELSE 0 END) as chronosJobCount,
194
195
  SUM(CASE WHEN ae.event_type = 'task_created' THEN 1 ELSE 0 END) as taskCreatedCount,
195
196
  SUM(CASE WHEN ae.event_type = 'task_completed' THEN 1 ELSE 0 END) as taskCompletedCount,
@@ -304,6 +305,7 @@ export class AuditRepository {
304
305
  mcpToolCount: totalsRow?.mcpToolCount ?? 0,
305
306
  skillCount: totalsRow?.skillCount ?? 0,
306
307
  memoryRecoveryCount: totalsRow?.memoryRecoveryCount ?? 0,
308
+ memoryPersistCount: totalsRow?.memoryPersistCount ?? 0,
307
309
  chronosJobCount: totalsRow?.chronosJobCount ?? 0,
308
310
  taskCreatedCount: totalsRow?.taskCreatedCount ?? 0,
309
311
  taskCompletedCount: totalsRow?.taskCompletedCount ?? 0,
@@ -65,7 +65,7 @@ export class SatiMemoryMiddleware {
65
65
  async afterAgent(generatedResponse, history, userSessionId) {
66
66
  try {
67
67
  await this.service.evaluateAndPersist([
68
- ...history.slice(-5).map(m => ({
68
+ ...history.map(m => ({
69
69
  role: m._getType() === 'human' ? 'user' : 'assistant',
70
70
  content: m.content.toString()
71
71
  })),
@@ -209,6 +209,33 @@ export class SatiService {
209
209
  display.log(`Deletion skipped — memory not found: ${deletion.id}`, { source: 'Sati', level: 'warning' });
210
210
  }
211
211
  }
212
+ // Emit audit event for memory persistence results
213
+ const inclusionsCount = (result.inclusions ?? []).filter(i => i.summary && i.category && i.importance).length;
214
+ const editsCount = (result.edits ?? []).filter(e => !!e.id).length;
215
+ const deletionsCount = (result.deletions ?? []).filter(d => !!d.id).length;
216
+ const totalOps = inclusionsCount + editsCount + deletionsCount;
217
+ if (totalOps > 0) {
218
+ try {
219
+ AuditRepository.getInstance().insert({
220
+ session_id: userSessionId ?? 'sati-persist',
221
+ event_type: 'memory_persist',
222
+ agent: 'sati',
223
+ duration_ms: Date.now() - satiStartMs,
224
+ status: 'success',
225
+ metadata: {
226
+ inclusions_count: inclusionsCount,
227
+ edits_count: editsCount,
228
+ deletions_count: deletionsCount,
229
+ inclusions: (result.inclusions ?? []).filter(i => i.summary && i.category && i.importance).map(i => ({ category: i.category, importance: i.importance, summary: i.summary })),
230
+ edits: (result.edits ?? []).filter(e => !!e.id).map(e => ({ id: e.id, summary: e.summary, reason: e.reason })),
231
+ deletions: (result.deletions ?? []).filter(d => !!d.id).map(d => ({ id: d.id, reason: d.reason })),
232
+ },
233
+ });
234
+ }
235
+ catch {
236
+ console.warn('[SatiService] Failed to log memory persistence audit event');
237
+ }
238
+ }
212
239
  }
213
240
  catch (error) {
214
241
  console.error('[SatiService] Evaluation failed:', error);
@@ -18,77 +18,84 @@ export async function runSessionEmbeddingWorker() {
18
18
  // 🔥 importante: carregar vec0 no DB onde existe a tabela vetorial
19
19
  loadVecExtension(satiDb);
20
20
  const embeddingService = await EmbeddingService.getInstance();
21
- while (true) {
22
- const sessions = shortDb.prepare(`
21
+ try {
22
+ while (true) {
23
+ const sessions = shortDb.prepare(`
23
24
  SELECT id
24
25
  FROM sessions
25
26
  WHERE ended_at IS NOT NULL
26
27
  AND embedding_status = 'pending'
27
28
  LIMIT ?
28
29
  `).all(BATCH_LIMIT);
29
- if (sessions.length === 0) {
30
- // display.log('✅ Nenhuma sessão pendente.', { level: 'debug', source: 'SessionEmbeddingWorker' });
31
- break;
32
- }
33
- for (const session of sessions) {
34
- const sessionId = session.id;
35
- display.log(`🧠 Processando sessão ${sessionId}...`, { source: 'SessionEmbeddingWorker' });
36
- try {
37
- // Skip setting 'processing' as it violates CHECK constraint
38
- // active_processing.add(sessionId); // If we needed concurrency control
39
- const chunks = satiDb.prepare(`
30
+ if (sessions.length === 0) {
31
+ // display.log('✅ Nenhuma sessão pendente.', { level: 'debug', source: 'SessionEmbeddingWorker' });
32
+ break;
33
+ }
34
+ for (const session of sessions) {
35
+ const sessionId = session.id;
36
+ display.log(`🧠 Processando sessão ${sessionId}...`, { source: 'SessionEmbeddingWorker' });
37
+ try {
38
+ // Skip setting 'processing' as it violates CHECK constraint
39
+ // active_processing.add(sessionId); // If we needed concurrency control
40
+ const chunks = satiDb.prepare(`
40
41
  SELECT id, content
41
42
  FROM session_chunks
42
43
  WHERE session_id = ?
43
44
  ORDER BY chunk_index
44
45
  `).all(sessionId);
45
- if (chunks.length === 0) {
46
- display.log(`⚠️ Sessão ${sessionId} não possui chunks.`, { source: 'SessionEmbeddingWorker' });
47
- shortDb.prepare(`
46
+ if (chunks.length === 0) {
47
+ display.log(`⚠️ Sessão ${sessionId} não possui chunks.`, { source: 'SessionEmbeddingWorker' });
48
+ shortDb.prepare(`
48
49
  UPDATE sessions
49
50
  SET embedding_status = 'embedded',
50
51
  embedded = 1
51
52
  WHERE id = ?
52
53
  `).run(sessionId);
53
- continue;
54
- }
55
- const insertVec = satiDb.prepare(`
54
+ continue;
55
+ }
56
+ const insertVec = satiDb.prepare(`
56
57
  INSERT INTO session_vec (embedding)
57
58
  VALUES (?)
58
59
  `);
59
- const insertMap = satiDb.prepare(`
60
+ const insertMap = satiDb.prepare(`
60
61
  INSERT OR REPLACE INTO session_embedding_map
61
62
  (session_chunk_id, vec_rowid)
62
63
  VALUES (?, ?)
63
64
  `);
64
- for (const chunk of chunks) {
65
- display.log(` ↳ Embedding chunk ${chunk.id}`, { source: 'SessionEmbeddingWorker' });
66
- const embedding = await embeddingService.generate(chunk.content);
67
- if (!embedding || embedding.length !== EMBEDDING_DIM) {
68
- throw new Error(`Embedding inválido. Esperado ${EMBEDDING_DIM}, recebido ${embedding?.length}`);
65
+ for (const chunk of chunks) {
66
+ display.log(` ↳ Embedding chunk ${chunk.id}`, { source: 'SessionEmbeddingWorker' });
67
+ const embedding = await embeddingService.generate(chunk.content);
68
+ if (!embedding || embedding.length !== EMBEDDING_DIM) {
69
+ throw new Error(`Embedding inválido. Esperado ${EMBEDDING_DIM}, recebido ${embedding?.length}`);
70
+ }
71
+ const result = insertVec.run(new Float32Array(embedding));
72
+ const vecRowId = result.lastInsertRowid;
73
+ insertMap.run(chunk.id, vecRowId);
69
74
  }
70
- const result = insertVec.run(new Float32Array(embedding));
71
- const vecRowId = result.lastInsertRowid;
72
- insertMap.run(chunk.id, vecRowId);
73
- }
74
- // ✅ finalizar sessão
75
- shortDb.prepare(`
75
+ // finalizar sessão
76
+ shortDb.prepare(`
76
77
  UPDATE sessions
77
78
  SET embedding_status = 'embedded',
78
79
  embedded = 1
79
80
  WHERE id = ?
80
81
  `).run(sessionId);
81
- display.log(`✅ Sessão ${sessionId} embedada com sucesso.`, { source: 'SessionEmbeddingWorker' });
82
- }
83
- catch (err) {
84
- display.log(`❌ Erro na sessão ${sessionId}: ${err}`, { source: 'SessionEmbeddingWorker' });
85
- shortDb.prepare(`
82
+ display.log(`✅ Sessão ${sessionId} embedada com sucesso.`, { source: 'SessionEmbeddingWorker' });
83
+ }
84
+ catch (err) {
85
+ display.log(`❌ Erro na sessão ${sessionId}: ${err}`, { source: 'SessionEmbeddingWorker' });
86
+ shortDb.prepare(`
86
87
  UPDATE sessions
87
88
  SET embedding_status = 'failed'
88
89
  WHERE id = ?
89
90
  `).run(sessionId);
91
+ }
90
92
  }
91
93
  }
92
94
  }
95
+ finally {
96
+ // Always close connections when done
97
+ shortDb.close();
98
+ satiDb.close();
99
+ }
93
100
  // display.log('🏁 Worker finalizado.', { source: 'SessionEmbeddingWorker' });
94
101
  }
@@ -17,6 +17,8 @@ import { MCPManager } from "../config/mcp-manager.js";
17
17
  import { SkillRegistry, SkillExecuteTool, SkillDelegateTool, updateSkillToolDescriptions } from "./skills/index.js";
18
18
  import { SmithRegistry } from "./smiths/registry.js";
19
19
  import { AuditRepository } from "./audit/repository.js";
20
+ import { SetupRepository } from './setup/repository.js';
21
+ import { buildSetupTool } from './tools/setup-tool.js';
20
22
  import { emitToolAuditEvents } from "./subagent-utils.js";
21
23
  const ORACLE_DELEGATION_TOOLS = new Set([
22
24
  'apoc_delegate', 'neo_delegate', 'trinity_delegate', 'smith_delegate',
@@ -154,7 +156,10 @@ export class Oracle {
154
156
  await Trinity.refreshDelegateCatalog().catch(() => { });
155
157
  updateSkillToolDescriptions();
156
158
  // Build tool list — conditionally include SmithDelegateTool based on config
159
+ // Initialize setup repository (creates table if needed)
160
+ SetupRepository.getInstance();
157
161
  const coreTools = [
162
+ buildSetupTool(),
158
163
  TaskQueryTool,
159
164
  Neo.getInstance().createDelegateTool(),
160
165
  Apoc.getInstance().createDelegateTool(),
@@ -222,8 +227,25 @@ export class Oracle {
222
227
  if (extraUsage) {
223
228
  userMessage.usage_metadata = extraUsage;
224
229
  }
225
- const systemMessage = new SystemMessage(`
226
- You are ${this.config.agent.name}, ${this.config.agent.personality}, the Oracle.
230
+ // Build first-time setup block if setup is not yet completed
231
+ const setupRepo = SetupRepository.getInstance();
232
+ let setupBlock = '';
233
+ if (!setupRepo.isCompleted()) {
234
+ const missingFields = setupRepo.getMissingFields();
235
+ if (missingFields.length > 0) {
236
+ setupBlock = `## [FIRST-TIME SETUP — ACTIVE]
237
+ Before responding to any other request, you MUST collect the user's basic information.
238
+ Ask for the following fields conversationally (one or two at a time — do NOT list them all at once):
239
+ ${missingFields.map((f) => `- ${f}`).join('\n')}
240
+
241
+ Once the user provides a value, immediately call \`setup_save\` with the collected fields.
242
+ Do NOT proceed with other tasks until all required fields have been collected and saved.
243
+ ---
244
+
245
+ `;
246
+ }
247
+ }
248
+ const systemMessage = new SystemMessage(`${setupBlock}You are ${this.config.agent.name}, ${this.config.agent.personality}, the Oracle.
227
249
 
228
250
  You are an orchestrator and task router.
229
251
 
@@ -360,6 +382,11 @@ ${SmithRegistry.getInstance().getSystemPromptSection()}
360
382
  databasePath: this.databasePath,
361
383
  limit: this.config.llm?.context_window ?? 100,
362
384
  });
385
+ // Ensure the session row exists in the DB before we try to persist
386
+ // messages. Without this, external callers (webhooks, chronos) that
387
+ // supply a custom session_id would hit "Sessão não encontrada" inside
388
+ // addMessages → setSessionTitleIfNeeded → renameSession.
389
+ callHistory.ensureSession(currentSessionId ?? 'default');
363
390
  // Load existing history from database in reverse order (most recent first)
364
391
  let previousMessages = await callHistory.getMessages();
365
392
  previousMessages = previousMessages.reverse();
@@ -623,6 +650,7 @@ Use it to inform your response and tool selection (if needed), but do not assume
623
650
  await Trinity.refreshDelegateCatalog().catch(() => { });
624
651
  updateSkillToolDescriptions();
625
652
  this.provider = await ProviderFactory.create(this.config.llm, [
653
+ buildSetupTool(),
626
654
  TaskQueryTool,
627
655
  Neo.getInstance().createDelegateTool(),
628
656
  Apoc.getInstance().createDelegateTool(),
@@ -0,0 +1,115 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import * as fs from 'fs-extra';
3
+ import * as path from 'path';
4
+ import { tmpdir } from 'os';
5
+ import { SetupRepository } from '../repository.js';
6
+ import { ConfigManager } from '../../../config/manager.js';
7
+ // Mock ConfigManager so tests are not coupled to the real config file
8
+ vi.mock('../../../config/manager.js', () => ({
9
+ ConfigManager: {
10
+ getInstance: vi.fn(() => ({
11
+ getSetupConfig: vi.fn(() => ({ enabled: true, fields: ['name', 'city'] })),
12
+ })),
13
+ },
14
+ }));
15
+ describe('SetupRepository', () => {
16
+ let testDbPath;
17
+ beforeEach(() => {
18
+ const tempDir = path.join(tmpdir(), 'morpheus-setup-test', Date.now().toString());
19
+ fs.ensureDirSync(tempDir);
20
+ testDbPath = path.join(tempDir, 'short-memory.db');
21
+ // Reset singleton so each test gets a fresh instance
22
+ SetupRepository.resetInstance();
23
+ });
24
+ afterEach(() => {
25
+ SetupRepository.resetInstance();
26
+ // Clean up temp DB
27
+ const tempDir = path.dirname(testDbPath);
28
+ try {
29
+ fs.removeSync(tempDir);
30
+ }
31
+ catch {
32
+ // ignore — Windows may delay file release
33
+ }
34
+ });
35
+ describe('initialize()', () => {
36
+ it('should create setup_state table on construction', () => {
37
+ const repo = SetupRepository.getInstance(testDbPath);
38
+ // If table was created, isCompleted() should not throw
39
+ expect(() => repo.isCompleted()).not.toThrow();
40
+ });
41
+ });
42
+ describe('isCompleted()', () => {
43
+ it('returns false when __completed__ record does not exist', () => {
44
+ const repo = SetupRepository.getInstance(testDbPath);
45
+ expect(repo.isCompleted()).toBe(false);
46
+ });
47
+ it('returns true after markCompleted() is called', () => {
48
+ const repo = SetupRepository.getInstance(testDbPath);
49
+ repo.markCompleted();
50
+ expect(repo.isCompleted()).toBe(true);
51
+ });
52
+ it('returns true when setup.enabled is false even without __completed__ record', () => {
53
+ // Override mock for this test only
54
+ vi.mocked(ConfigManager.getInstance).mockReturnValueOnce({
55
+ getSetupConfig: vi.fn(() => ({ enabled: false, fields: ['name'] })),
56
+ });
57
+ const repo = SetupRepository.getInstance(testDbPath);
58
+ expect(repo.isCompleted()).toBe(true);
59
+ });
60
+ });
61
+ describe('saveField() / getMissingFields()', () => {
62
+ it('getMissingFields returns all configured fields initially', () => {
63
+ const repo = SetupRepository.getInstance(testDbPath);
64
+ const missing = repo.getMissingFields();
65
+ expect(missing).toEqual(['name', 'city']);
66
+ });
67
+ it('removes a field from missing list after saving it', () => {
68
+ const repo = SetupRepository.getInstance(testDbPath);
69
+ repo.saveField('name', 'João');
70
+ const missing = repo.getMissingFields();
71
+ expect(missing).toEqual(['city']);
72
+ expect(missing).not.toContain('name');
73
+ });
74
+ it('returns empty array when all fields are saved', () => {
75
+ const repo = SetupRepository.getInstance(testDbPath);
76
+ repo.saveField('name', 'João');
77
+ repo.saveField('city', 'Brasília');
78
+ expect(repo.getMissingFields()).toEqual([]);
79
+ });
80
+ it('upserts a field on second save', () => {
81
+ const repo = SetupRepository.getInstance(testDbPath);
82
+ repo.saveField('name', 'João');
83
+ repo.saveField('name', 'Carlos');
84
+ // Should still have only one entry for name (no duplicate)
85
+ expect(repo.getMissingFields()).toEqual(['city']);
86
+ });
87
+ });
88
+ describe('markCompleted()', () => {
89
+ it('is idempotent (can be called multiple times without error)', () => {
90
+ const repo = SetupRepository.getInstance(testDbPath);
91
+ expect(() => {
92
+ repo.markCompleted();
93
+ repo.markCompleted();
94
+ repo.markCompleted();
95
+ }).not.toThrow();
96
+ expect(repo.isCompleted()).toBe(true);
97
+ });
98
+ });
99
+ describe('reset()', () => {
100
+ it('clears all records including __completed__', () => {
101
+ const repo = SetupRepository.getInstance(testDbPath);
102
+ repo.saveField('name', 'João');
103
+ repo.saveField('city', 'Brasília');
104
+ repo.markCompleted();
105
+ expect(repo.isCompleted()).toBe(true);
106
+ repo.reset();
107
+ expect(repo.isCompleted()).toBe(false);
108
+ expect(repo.getMissingFields()).toEqual(['name', 'city']);
109
+ });
110
+ it('is safe to call on empty table', () => {
111
+ const repo = SetupRepository.getInstance(testDbPath);
112
+ expect(() => repo.reset()).not.toThrow();
113
+ });
114
+ });
115
+ });