morpheus-cli 0.9.13 → 0.9.20

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 (86) hide show
  1. package/README.md +48 -17
  2. package/dist/channels/discord.js +93 -6
  3. package/dist/channels/telegram.js +109 -9
  4. package/dist/cli/commands/start.js +15 -0
  5. package/dist/config/manager.js +20 -1
  6. package/dist/config/paths.js +4 -0
  7. package/dist/config/schemas.js +15 -0
  8. package/dist/http/api.js +2 -1
  9. package/dist/http/routers/danger.js +4 -5
  10. package/dist/http/routers/link.js +2 -2
  11. package/dist/runtime/__tests__/telephonist-tts.test.js +84 -0
  12. package/dist/runtime/adapters/AuditRepositoryAdapter.js +6 -0
  13. package/dist/runtime/adapters/ChannelNotifierAdapter.js +9 -0
  14. package/dist/runtime/adapters/LangChainProviderAdapter.js +9 -0
  15. package/dist/runtime/adapters/SQLiteChatHistoryAdapter.js +15 -0
  16. package/dist/runtime/adapters/SQLiteTaskEnqueuerAdapter.js +6 -0
  17. package/dist/runtime/adapters/index.js +5 -0
  18. package/dist/runtime/audit/repository.js +6 -2
  19. package/dist/runtime/chronos/repository.js +2 -2
  20. package/dist/runtime/container.js +50 -0
  21. package/dist/runtime/hot-reload.js +6 -9
  22. package/dist/runtime/memory/backfill-embeddings.js +2 -3
  23. package/dist/runtime/memory/sati/repository.js +3 -3
  24. package/dist/runtime/memory/sqlite.js +3 -3
  25. package/dist/runtime/memory/trinity-db.js +2 -2
  26. package/dist/runtime/ports/IAuditEmitter.js +1 -0
  27. package/dist/runtime/ports/IChatHistory.js +1 -0
  28. package/dist/runtime/ports/ILLMProviderFactory.js +1 -0
  29. package/dist/runtime/ports/INotifier.js +1 -0
  30. package/dist/runtime/ports/ITaskEnqueuer.js +1 -0
  31. package/dist/runtime/ports/index.js +1 -0
  32. package/dist/runtime/providers/factory.js +8 -52
  33. package/dist/runtime/providers/strategies.js +66 -0
  34. package/dist/runtime/setup/repository.js +2 -2
  35. package/dist/runtime/subagents/apoc.js +2 -2
  36. package/dist/runtime/subagents/link/link.js +2 -2
  37. package/dist/runtime/subagents/link/repository.js +2 -2
  38. package/dist/runtime/subagents/link/worker.js +3 -3
  39. package/dist/runtime/subagents/neo.js +2 -2
  40. package/dist/runtime/subagents/trinity/trinity.js +2 -2
  41. package/dist/runtime/tasks/repository.js +2 -2
  42. package/dist/runtime/telephonist.js +160 -0
  43. package/dist/runtime/tools/delegation-utils.js +5 -7
  44. package/dist/runtime/tools/morpheus-tools.js +6 -7
  45. package/dist/runtime/tools/smith-tool.js +5 -7
  46. package/dist/runtime/webhooks/repository.js +2 -2
  47. package/dist/types/config.js +6 -0
  48. package/dist/ui/assets/AuditDashboard-Cu33zb_7.js +1 -0
  49. package/dist/ui/assets/{Chat-UVoDlqqM.js → Chat-mt1j5V55.js} +1 -1
  50. package/dist/ui/assets/{Chronos-Dfs_pOsc.js → Chronos-Bq_h41cw.js} +1 -1
  51. package/dist/ui/assets/{ConfirmationModal-BBIjVef7.js → ConfirmationModal-CxLP8iC6.js} +1 -1
  52. package/dist/ui/assets/{Dashboard-BdSQDB14.js → Dashboard-D0LAlHtG.js} +1 -1
  53. package/dist/ui/assets/{DeleteConfirmationModal-Du85q5u2.js → DeleteConfirmationModal-kZ_c3sFk.js} +1 -1
  54. package/dist/ui/assets/{Documents-DguILrI8.js → Documents-nlQNoUcq.js} +1 -1
  55. package/dist/ui/assets/{Logs-BDup2FET.js → Logs-C1tlg574.js} +1 -1
  56. package/dist/ui/assets/{MCPManager-WBdh1rum.js → MCPManager-Do7isizG.js} +1 -1
  57. package/dist/ui/assets/ModelPricing-BeJ7oXBA.js +1 -0
  58. package/dist/ui/assets/{Notifications-BslO2Ect.js → Notifications-Cg5CMlY0.js} +1 -1
  59. package/dist/ui/assets/{SatiMemories-DzaLaZ6M.js → SatiMemories-D9l6s8Pc.js} +1 -1
  60. package/dist/ui/assets/SessionAudit-Da1ySlYg.js +9 -0
  61. package/dist/ui/assets/Settings-DpXwpEhO.js +49 -0
  62. package/dist/ui/assets/{Skills-BnDg1HCb.js → Skills-DaqCY8QH.js} +1 -1
  63. package/dist/ui/assets/Smiths-DA-x4KFT.js +1 -0
  64. package/dist/ui/assets/Switch-CJTE4ZQm.js +1 -0
  65. package/dist/ui/assets/{Tasks-BuoNCvI-.js → Tasks-DU49M9U-.js} +1 -1
  66. package/dist/ui/assets/{TrinityDatabases-DYHJunk7.js → TrinityDatabases-CoKzKTL-.js} +1 -1
  67. package/dist/ui/assets/{UsageStats-BpGXaHgW.js → UsageStats-cds352Pj.js} +1 -1
  68. package/dist/ui/assets/{WebhookManager-D2muhYy9.js → WebhookManager-DdAdHQUk.js} +1 -1
  69. package/dist/ui/assets/{agents-CgqJea9n.js → agents-B1z_dlQC.js} +1 -1
  70. package/dist/ui/assets/{audit-Dc3YW0-4.js → audit-BAhaGrKY.js} +1 -1
  71. package/dist/ui/assets/{chronos-CZvGhZQB.js → chronos-DGD_Md9M.js} +1 -1
  72. package/dist/ui/assets/config-BwTXe5M2.js +1 -0
  73. package/dist/ui/assets/{index-Bta9YXEm.js → index-BcX5O7kY.js} +2 -2
  74. package/dist/ui/assets/{mcp-vIffcwd6.js → mcp-BlkruPaA.js} +1 -1
  75. package/dist/ui/assets/{skills-wANsorUj.js → skills-CtCb-52u.js} +1 -1
  76. package/dist/ui/assets/{stats-xnlA4NwX.js → stats-BiPI2kaw.js} +1 -1
  77. package/dist/ui/assets/useCurrency-BCdG-pHx.js +1 -0
  78. package/dist/ui/index.html +1 -1
  79. package/dist/ui/sw.js +1 -1
  80. package/package.json +1 -1
  81. package/dist/ui/assets/AuditDashboard-BVyKnpVm.js +0 -1
  82. package/dist/ui/assets/ModelPricing-BQPw0r6z.js +0 -1
  83. package/dist/ui/assets/SessionAudit-CBDThjBi.js +0 -9
  84. package/dist/ui/assets/Settings-JPTCA7C7.js +0 -49
  85. package/dist/ui/assets/Smiths-DR6g_o3D.js +0 -1
  86. package/dist/ui/assets/config-pKL8Y4V9.js +0 -1
@@ -0,0 +1,84 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { createTtsTelephonist, TTS_MAX_CHARS } from '../telephonist.js';
3
+ // ─── createTtsTelephonist factory ─────────────────────────────────────────────
4
+ describe('createTtsTelephonist', () => {
5
+ it('returns an instance with synthesize() for openai provider', () => {
6
+ const telephonist = createTtsTelephonist({
7
+ enabled: true,
8
+ provider: 'openai',
9
+ model: 'tts-1',
10
+ voice: 'alloy',
11
+ });
12
+ expect(telephonist).toBeDefined();
13
+ expect(typeof telephonist.synthesize).toBe('function');
14
+ });
15
+ it('returns an instance with synthesize() for google provider', () => {
16
+ const telephonist = createTtsTelephonist({
17
+ enabled: true,
18
+ provider: 'google',
19
+ model: 'gemini-2.5-flash',
20
+ voice: 'Kore',
21
+ });
22
+ expect(telephonist).toBeDefined();
23
+ expect(typeof telephonist.synthesize).toBe('function');
24
+ });
25
+ it('throws for unsupported provider', () => {
26
+ expect(() => createTtsTelephonist({
27
+ enabled: true,
28
+ provider: 'ollama',
29
+ model: 'some-model',
30
+ voice: 'default',
31
+ })).toThrow(/Unsupported TTS provider/);
32
+ });
33
+ it('does not expose transcribe() meaningfully (throws)', async () => {
34
+ const telephonist = createTtsTelephonist({
35
+ enabled: true,
36
+ provider: 'openai',
37
+ model: 'tts-1',
38
+ voice: 'alloy',
39
+ });
40
+ await expect(telephonist.transcribe('', '', '')).rejects.toThrow();
41
+ });
42
+ });
43
+ // ─── Text truncation ──────────────────────────────────────────────────────────
44
+ describe('TTS text truncation', () => {
45
+ it('TTS_MAX_CHARS constant is 4096', () => {
46
+ expect(TTS_MAX_CHARS).toBe(4096);
47
+ });
48
+ it('short text (under 4096 chars) passes through unchanged in OpenAI synthesize', async () => {
49
+ // We test the truncation logic indirectly by checking that the SDK call
50
+ // receives the correct (non-truncated) text. We mock the OpenAI client.
51
+ const mockCreate = vi.fn().mockResolvedValue({
52
+ arrayBuffer: async () => new ArrayBuffer(8),
53
+ });
54
+ vi.mock('openai', () => ({
55
+ default: vi.fn().mockImplementation(() => ({
56
+ audio: {
57
+ speech: { create: mockCreate },
58
+ },
59
+ })),
60
+ }));
61
+ vi.mock('fs-extra', () => ({
62
+ default: { writeFile: vi.fn().mockResolvedValue(undefined) },
63
+ }));
64
+ const telephonist = createTtsTelephonist({
65
+ enabled: true,
66
+ provider: 'openai',
67
+ model: 'tts-1',
68
+ voice: 'alloy',
69
+ });
70
+ const shortText = 'Hello world';
71
+ await telephonist.synthesize(shortText, 'fake-key').catch(() => { });
72
+ // Verify the mock was called with the short text (not truncated)
73
+ if (mockCreate.mock.calls.length > 0) {
74
+ expect(mockCreate.mock.calls[0][0].input).toBe(shortText);
75
+ }
76
+ });
77
+ it('long text (over 4096 chars) is truncated to 4096', () => {
78
+ // Test the truncation logic directly by re-creating what truncateForTts does
79
+ const longText = 'a'.repeat(5000);
80
+ const truncated = longText.slice(0, 4096);
81
+ expect(truncated.length).toBe(4096);
82
+ expect(longText.length).toBeGreaterThan(4096);
83
+ });
84
+ });
@@ -0,0 +1,6 @@
1
+ import { AuditRepository } from '../audit/repository.js';
2
+ export class AuditRepositoryAdapter {
3
+ emit(event) {
4
+ AuditRepository.getInstance().insert(event);
5
+ }
6
+ }
@@ -0,0 +1,9 @@
1
+ import { ChannelRegistry } from '../../channels/registry.js';
2
+ export class ChannelNotifierAdapter {
3
+ async sendToUser(channel, userId, text) {
4
+ await ChannelRegistry.sendToUser(channel, userId, text);
5
+ }
6
+ async broadcast(text) {
7
+ await ChannelRegistry.broadcast(text);
8
+ }
9
+ }
@@ -0,0 +1,9 @@
1
+ import { ProviderFactory } from '../providers/factory.js';
2
+ export class LangChainProviderAdapter {
3
+ async createBare(config, tools = []) {
4
+ return ProviderFactory.createBare(config, tools);
5
+ }
6
+ async create(config, tools = []) {
7
+ return ProviderFactory.createBare(config, tools);
8
+ }
9
+ }
@@ -0,0 +1,15 @@
1
+ import { SQLiteChatMessageHistory } from '../memory/sqlite.js';
2
+ export class SQLiteChatHistoryAdapter {
3
+ async getMessages(sessionId) {
4
+ const history = new SQLiteChatMessageHistory({ sessionId });
5
+ return history.getMessages();
6
+ }
7
+ async addMessage(sessionId, message) {
8
+ const history = new SQLiteChatMessageHistory({ sessionId });
9
+ await history.addMessage(message);
10
+ }
11
+ async clear(sessionId) {
12
+ const history = new SQLiteChatMessageHistory({ sessionId });
13
+ await history.clear();
14
+ }
15
+ }
@@ -0,0 +1,6 @@
1
+ import { TaskRepository } from '../tasks/repository.js';
2
+ export class SQLiteTaskEnqueuerAdapter {
3
+ enqueue(input) {
4
+ return TaskRepository.getInstance().createTask(input);
5
+ }
6
+ }
@@ -0,0 +1,5 @@
1
+ export { ChannelNotifierAdapter } from './ChannelNotifierAdapter.js';
2
+ export { SQLiteTaskEnqueuerAdapter } from './SQLiteTaskEnqueuerAdapter.js';
3
+ export { SQLiteChatHistoryAdapter } from './SQLiteChatHistoryAdapter.js';
4
+ export { LangChainProviderAdapter } from './LangChainProviderAdapter.js';
5
+ export { AuditRepositoryAdapter } from './AuditRepositoryAdapter.js';
@@ -1,14 +1,14 @@
1
1
  import Database from 'better-sqlite3';
2
2
  import fs from 'fs-extra';
3
3
  import path from 'path';
4
- import { homedir } from 'os';
5
4
  import { randomUUID } from 'crypto';
6
5
  import { DisplayManager } from '../display.js';
6
+ import { PATHS } from '../../config/paths.js';
7
7
  export class AuditRepository {
8
8
  static instance = null;
9
9
  db;
10
10
  constructor() {
11
- const dbPath = path.join(homedir(), '.morpheus', 'memory', 'short-memory.db');
11
+ const dbPath = PATHS.shortMemoryDb;
12
12
  fs.ensureDirSync(path.dirname(dbPath));
13
13
  this.db = new Database(dbPath, { timeout: 5000 });
14
14
  this.db.pragma('journal_mode = WAL');
@@ -59,6 +59,10 @@ export class AuditRepository {
59
59
  DisplayManager.getInstance().log(`AuditRepository.insert failed: ${err?.message ?? String(err)}`, { source: 'Audit', level: 'error' });
60
60
  }
61
61
  }
62
+ countBySession(sessionId) {
63
+ const row = this.db.prepare(`SELECT COUNT(*) as n FROM audit_events WHERE session_id = ?`).get(sessionId);
64
+ return row?.n ?? 0;
65
+ }
62
66
  getBySession(sessionId, opts) {
63
67
  const limit = opts?.limit ?? 500;
64
68
  const offset = opts?.offset ?? 0;
@@ -1,9 +1,9 @@
1
1
  import Database from 'better-sqlite3';
2
2
  import fs from 'fs-extra';
3
3
  import path from 'path';
4
- import { homedir } from 'os';
5
4
  import { randomUUID } from 'crypto';
6
5
  import { ConfigManager } from '../../config/manager.js';
6
+ import { PATHS } from '../../config/paths.js';
7
7
  export class ChronosError extends Error {
8
8
  constructor(message) {
9
9
  super(message);
@@ -14,7 +14,7 @@ export class ChronosRepository {
14
14
  static instance = null;
15
15
  db;
16
16
  constructor() {
17
- const dbPath = path.join(homedir(), '.morpheus', 'memory', 'short-memory.db');
17
+ const dbPath = PATHS.shortMemoryDb;
18
18
  fs.ensureDirSync(path.dirname(dbPath));
19
19
  this.db = new Database(dbPath, { timeout: 5000 });
20
20
  this.db.pragma('journal_mode = WAL');
@@ -0,0 +1,50 @@
1
+ /**
2
+ * ServiceContainer — Composition Root
3
+ *
4
+ * Simple typed registry for application services (ports).
5
+ * Configured once at startup in start.ts. No DI framework magic —
6
+ * just a Map with type-safe get/register.
7
+ *
8
+ * Usage:
9
+ * // Registration (start.ts):
10
+ * ServiceContainer.register('notifier', new ChannelNotifierAdapter());
11
+ *
12
+ * // Consumption:
13
+ * const notifier = ServiceContainer.get<INotifier>('notifier');
14
+ */
15
+ export class ServiceContainer {
16
+ static services = new Map();
17
+ /** Register a service under a given key. Overwrites if already registered. */
18
+ static register(key, service) {
19
+ ServiceContainer.services.set(key, service);
20
+ }
21
+ /**
22
+ * Retrieve a registered service.
23
+ * @throws if the key is not registered.
24
+ */
25
+ static get(key) {
26
+ const service = ServiceContainer.services.get(key);
27
+ if (service === undefined) {
28
+ throw new Error(`ServiceContainer: "${key}" is not registered. Did you call register() in start.ts?`);
29
+ }
30
+ return service;
31
+ }
32
+ /** Returns true if a service is registered under the given key. */
33
+ static has(key) {
34
+ return ServiceContainer.services.has(key);
35
+ }
36
+ /** Remove all registered services (useful in tests). */
37
+ static reset() {
38
+ ServiceContainer.services.clear();
39
+ }
40
+ }
41
+ /**
42
+ * Well-known service keys — use these constants to avoid typo bugs.
43
+ */
44
+ export const SERVICE_KEYS = {
45
+ notifier: 'notifier',
46
+ taskEnqueuer: 'taskEnqueuer',
47
+ chatHistory: 'chatHistory',
48
+ providerFactory: 'providerFactory',
49
+ auditEmitter: 'auditEmitter',
50
+ };
@@ -7,9 +7,7 @@
7
7
  */
8
8
  import { ConfigManager } from '../config/manager.js';
9
9
  import { DisplayManager } from './display.js';
10
- import { Apoc } from './subagents/apoc.js';
11
- import { Neo } from './subagents/neo.js';
12
- import { Trinity } from './subagents/trinity/trinity.js';
10
+ import { SubagentRegistry } from './subagents/registry.js';
13
11
  let currentOracle = null;
14
12
  /**
15
13
  * Register the current Oracle instance for hot-reload.
@@ -44,12 +42,11 @@ export async function hotReloadConfig() {
44
42
  reinitialized.push('Oracle');
45
43
  display.log('Oracle reinitialized with new config', { source: 'HotReload', level: 'info' });
46
44
  }
47
- // 3. Reset subagent singletons - they will reinitialize with new config on next use
48
- Apoc.resetInstance();
49
- Neo.resetInstance();
50
- Trinity.resetInstance();
51
- reinitialized.push('Apoc', 'Neo', 'Trinity');
52
- display.log('Subagent singletons reset (will reinitialize on next use)', { source: 'HotReload', level: 'info' });
45
+ // 3. Reload all registered subagents via SubagentRegistry
46
+ await SubagentRegistry.reloadAll();
47
+ const agentNames = SubagentRegistry.getAll().map(r => r.label);
48
+ reinitialized.push(...agentNames);
49
+ display.log(`Subagents reloaded: ${agentNames.join(', ')}`, { source: 'HotReload', level: 'info' });
53
50
  return {
54
51
  success: true,
55
52
  reinitialized,
@@ -1,9 +1,8 @@
1
1
  import Database from 'better-sqlite3';
2
2
  import { EmbeddingService } from './embedding.service.js';
3
- import path from 'path';
4
- import { homedir } from 'os';
5
3
  import loadVecExtension from './sqlite-vec.js';
6
- const db = new Database(path.join(homedir(), '.morpheus', 'memory', 'sati-memory.db'));
4
+ import { PATHS } from '../../config/paths.js';
5
+ const db = new Database(PATHS.satiDb);
7
6
  db.pragma('journal_mode = WAL');
8
7
  // 🔥 ISSO AQUI É O QUE ESTÁ FALTANDO
9
8
  loadVecExtension(db);
@@ -1,11 +1,11 @@
1
1
  import Database from 'better-sqlite3';
2
- import path from 'path';
3
- import { homedir } from 'os';
4
2
  import fs from 'fs-extra';
3
+ import path from 'path';
5
4
  import { randomUUID } from 'crypto';
6
5
  import loadVecExtension from '../sqlite-vec.js';
7
6
  import { DisplayManager } from '../../display.js';
8
7
  import { ConfigManager } from '../../../config/manager.js';
8
+ import { PATHS } from '../../../config/paths.js';
9
9
  const EMBEDDING_DIM = 384;
10
10
  export class SatiRepository {
11
11
  db = null;
@@ -14,7 +14,7 @@ export class SatiRepository {
14
14
  display = DisplayManager.getInstance();
15
15
  constructor(dbPath) {
16
16
  this.dbPath =
17
- dbPath || path.join(homedir(), '.morpheus', 'memory', 'sati-memory.db');
17
+ dbPath || PATHS.satiDb;
18
18
  }
19
19
  static getInstance(dbPath) {
20
20
  if (!SatiRepository.instance) {
@@ -3,7 +3,7 @@ import { HumanMessage, AIMessage, SystemMessage, ToolMessage } from "@langchain/
3
3
  import Database from "better-sqlite3";
4
4
  import fs from "fs-extra";
5
5
  import * as path from "path";
6
- import { homedir } from "os";
6
+ import { PATHS } from "../../config/paths.js";
7
7
  import { randomUUID } from 'crypto';
8
8
  import { DisplayManager } from "../display.js";
9
9
  export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
@@ -22,7 +22,7 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
22
22
  this.sessionId = fields.sessionId && fields.sessionId !== '' ? fields.sessionId : '';
23
23
  this.limit = fields.limit ? fields.limit : 20;
24
24
  // Default path: ~/.morpheus/memory/short-memory.db
25
- const dbPath = fields.databasePath || path.join(homedir(), ".morpheus", "memory", "short-memory.db");
25
+ const dbPath = fields.databasePath || PATHS.shortMemoryDb;
26
26
  // Ensure the directory exists
27
27
  this.ensureDirectory(dbPath);
28
28
  // Initialize database with retry logic for locked databases
@@ -874,7 +874,7 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
874
874
  const sessionText = tx(); // Executar a transação
875
875
  // Criar chunks no banco Sati — conexão aberta localmente e fechada ao fim
876
876
  if (sessionText) {
877
- const dbSatiPath = path.join(homedir(), '.morpheus', 'memory', 'sati-memory.db');
877
+ const dbSatiPath = PATHS.satiDb;
878
878
  this.ensureDirectory(dbSatiPath);
879
879
  const dbSati = new Database(dbSatiPath, { timeout: 5000 });
880
880
  dbSati.pragma('journal_mode = WAL');
@@ -1,8 +1,8 @@
1
1
  import Database from 'better-sqlite3';
2
2
  import fs from 'fs-extra';
3
3
  import path from 'path';
4
- import { homedir } from 'os';
5
4
  import { encrypt, decrypt, canEncrypt } from '../trinity-crypto.js';
5
+ import { PATHS } from '../../config/paths.js';
6
6
  function safeDecrypt(value) {
7
7
  if (!value)
8
8
  return null;
@@ -39,7 +39,7 @@ export class DatabaseRegistry {
39
39
  static instance = null;
40
40
  db;
41
41
  constructor() {
42
- const dbPath = path.join(homedir(), '.morpheus', 'memory', 'trinity.db');
42
+ const dbPath = PATHS.trinityDb;
43
43
  fs.ensureDirSync(path.dirname(dbPath));
44
44
  this.db = new Database(dbPath, { timeout: 5000 });
45
45
  this.db.pragma('journal_mode = WAL');
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -1,16 +1,13 @@
1
- import { ChatOpenAI } from "@langchain/openai";
2
- import { ChatAnthropic } from "@langchain/anthropic";
3
- import { ChatOllama } from "@langchain/ollama";
4
- import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
5
1
  import { ProviderError } from "../errors.js";
6
2
  import { createAgent, createMiddleware } from "langchain";
7
3
  import { DisplayManager } from "../display.js";
8
- import { getUsableApiKey } from "../trinity-crypto.js";
9
4
  import { ConfigManager } from "../../config/manager.js";
10
5
  import { TaskRequestContext } from "../tasks/context.js";
11
6
  import { ChannelRegistry } from "../../channels/registry.js";
7
+ import { getStrategy, registerStrategy } from "./strategies.js";
12
8
  /** Channels that should NOT receive verbose tool notifications */
13
9
  const SILENT_CHANNELS = new Set(['api', 'ui']);
10
+ export { registerStrategy };
14
11
  export class ProviderFactory {
15
12
  static buildMonitoringMiddleware() {
16
13
  const display = DisplayManager.getInstance();
@@ -41,44 +38,11 @@ export class ProviderFactory {
41
38
  });
42
39
  }
43
40
  static buildModel(config) {
44
- const usableApiKey = getUsableApiKey(config.api_key);
45
- switch (config.provider) {
46
- case 'openai':
47
- return new ChatOpenAI({
48
- modelName: config.model,
49
- temperature: config.temperature,
50
- apiKey: process.env.OPENAI_API_KEY || usableApiKey,
51
- });
52
- case 'anthropic':
53
- return new ChatAnthropic({
54
- modelName: config.model,
55
- temperature: config.temperature,
56
- apiKey: process.env.ANTHROPIC_API_KEY || usableApiKey,
57
- });
58
- case 'openrouter':
59
- return new ChatOpenAI({
60
- modelName: config.model,
61
- temperature: config.temperature,
62
- apiKey: process.env.OPENROUTER_API_KEY || usableApiKey,
63
- configuration: {
64
- baseURL: config.base_url || 'https://openrouter.ai/api/v1'
65
- }
66
- });
67
- case 'ollama':
68
- return new ChatOllama({
69
- model: config.model,
70
- temperature: config.temperature,
71
- baseUrl: config.base_url || usableApiKey,
72
- });
73
- case 'gemini':
74
- return new ChatGoogleGenerativeAI({
75
- model: config.model,
76
- temperature: config.temperature,
77
- apiKey: process.env.GOOGLE_API_KEY || usableApiKey
78
- });
79
- default:
80
- throw new Error(`Unsupported provider: ${config.provider}`);
41
+ const strategy = getStrategy(config.provider);
42
+ if (!strategy) {
43
+ throw new Error(`Unsupported provider: ${config.provider}`);
81
44
  }
45
+ return strategy.build(config);
82
46
  }
83
47
  static handleProviderError(config, error) {
84
48
  let suggestion = "Check your configuration and API keys.";
@@ -114,14 +78,6 @@ export class ProviderFactory {
114
78
  ProviderFactory.handleProviderError(config, error);
115
79
  }
116
80
  }
117
- static async create(config, tools = []) {
118
- try {
119
- const model = ProviderFactory.buildModel(config);
120
- const middleware = ProviderFactory.buildMonitoringMiddleware();
121
- return createAgent({ model, tools, middleware: [middleware] });
122
- }
123
- catch (error) {
124
- ProviderFactory.handleProviderError(config, error);
125
- }
126
- }
81
+ /** Alias for createBare both methods are identical. */
82
+ static create = ProviderFactory.createBare;
127
83
  }
@@ -0,0 +1,66 @@
1
+ import { ChatOpenAI } from "@langchain/openai";
2
+ import { ChatAnthropic } from "@langchain/anthropic";
3
+ import { ChatOllama } from "@langchain/ollama";
4
+ import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
5
+ import { getUsableApiKey } from "../trinity-crypto.js";
6
+ class OpenAIStrategy {
7
+ build(config) {
8
+ return new ChatOpenAI({
9
+ modelName: config.model,
10
+ temperature: config.temperature,
11
+ apiKey: process.env.OPENAI_API_KEY || getUsableApiKey(config.api_key),
12
+ });
13
+ }
14
+ }
15
+ class AnthropicStrategy {
16
+ build(config) {
17
+ return new ChatAnthropic({
18
+ modelName: config.model,
19
+ temperature: config.temperature,
20
+ apiKey: process.env.ANTHROPIC_API_KEY || getUsableApiKey(config.api_key),
21
+ });
22
+ }
23
+ }
24
+ class OpenRouterStrategy {
25
+ build(config) {
26
+ return new ChatOpenAI({
27
+ modelName: config.model,
28
+ temperature: config.temperature,
29
+ apiKey: process.env.OPENROUTER_API_KEY || getUsableApiKey(config.api_key),
30
+ configuration: {
31
+ baseURL: config.base_url || 'https://openrouter.ai/api/v1'
32
+ }
33
+ });
34
+ }
35
+ }
36
+ class OllamaStrategy {
37
+ build(config) {
38
+ return new ChatOllama({
39
+ model: config.model,
40
+ temperature: config.temperature,
41
+ baseUrl: config.base_url || getUsableApiKey(config.api_key),
42
+ });
43
+ }
44
+ }
45
+ class GeminiStrategy {
46
+ build(config) {
47
+ return new ChatGoogleGenerativeAI({
48
+ model: config.model,
49
+ temperature: config.temperature,
50
+ apiKey: process.env.GOOGLE_API_KEY || getUsableApiKey(config.api_key),
51
+ });
52
+ }
53
+ }
54
+ const strategies = new Map([
55
+ ['openai', new OpenAIStrategy()],
56
+ ['anthropic', new AnthropicStrategy()],
57
+ ['openrouter', new OpenRouterStrategy()],
58
+ ['ollama', new OllamaStrategy()],
59
+ ['gemini', new GeminiStrategy()],
60
+ ]);
61
+ export function registerStrategy(provider, strategy) {
62
+ strategies.set(provider, strategy);
63
+ }
64
+ export function getStrategy(provider) {
65
+ return strategies.get(provider);
66
+ }
@@ -1,13 +1,13 @@
1
1
  import Database from 'better-sqlite3';
2
2
  import fs from 'fs-extra';
3
3
  import path from 'path';
4
- import { homedir } from 'os';
5
4
  import { ConfigManager } from '../../config/manager.js';
5
+ import { PATHS } from '../../config/paths.js';
6
6
  export class SetupRepository {
7
7
  static instance = null;
8
8
  db;
9
9
  constructor(dbPath) {
10
- const resolvedPath = dbPath ?? path.join(homedir(), '.morpheus', 'memory', 'short-memory.db');
10
+ const resolvedPath = dbPath ?? PATHS.shortMemoryDb;
11
11
  fs.ensureDirSync(path.dirname(resolvedPath));
12
12
  this.db = new Database(resolvedPath, { timeout: 5000 });
13
13
  this.db.pragma('journal_mode = WAL');
@@ -1,6 +1,6 @@
1
1
  import { HumanMessage, SystemMessage, AIMessage } from "@langchain/core/messages";
2
2
  import { ConfigManager } from "../../config/manager.js";
3
- import { ProviderFactory } from "../providers/factory.js";
3
+ import { ServiceContainer, SERVICE_KEYS } from "../container.js";
4
4
  import { ProviderError } from "../errors.js";
5
5
  import { DisplayManager } from "../display.js";
6
6
  import { buildDevKit } from "morpheus-devkit";
@@ -78,7 +78,7 @@ export class Apoc {
78
78
  const tools = instrumentDevKitTools(rawTools, () => Apoc.currentSessionId, () => 'apoc');
79
79
  this.display.log(`Apoc initialized with ${tools.length} DevKit tools (sandbox_dir: ${devkit.sandbox_dir}, personality: ${personality})`, { source: "Apoc" });
80
80
  try {
81
- this.agent = await ProviderFactory.createBare(apocConfig, tools);
81
+ this.agent = await ServiceContainer.get(SERVICE_KEYS.providerFactory).createBare(apocConfig, tools);
82
82
  }
83
83
  catch (err) {
84
84
  throw new ProviderError(apocConfig.provider, err, "Apoc subagent initialization failed");
@@ -4,7 +4,7 @@ import { DynamicStructuredTool } from "@langchain/core/tools";
4
4
  import { ConfigManager } from '../../../config/manager.js';
5
5
  import { LinkRepository } from './repository.js';
6
6
  import { LinkSearch } from './search.js';
7
- import { ProviderFactory } from '../../providers/factory.js';
7
+ import { ServiceContainer, SERVICE_KEYS } from '../../container.js';
8
8
  import { ProviderError } from '../../errors.js';
9
9
  import { DisplayManager } from '../../display.js';
10
10
  import { TaskRequestContext } from '../../tasks/context.js';
@@ -268,7 +268,7 @@ export class Link {
268
268
  }
269
269
  this.display.log(`Link initialized with personality: ${personality}.`, { source: 'Link' });
270
270
  try {
271
- this.agent = await ProviderFactory.create(linkConfig, tools);
271
+ this.agent = await ServiceContainer.get(SERVICE_KEYS.providerFactory).create(linkConfig, tools);
272
272
  }
273
273
  catch (err) {
274
274
  throw new ProviderError(linkConfig.provider, err, 'Link subagent initialization failed');
@@ -1,10 +1,10 @@
1
1
  import Database from 'better-sqlite3';
2
2
  import fs from 'fs-extra';
3
3
  import path from 'path';
4
- import { homedir } from 'os';
5
4
  import { randomUUID } from 'crypto';
6
5
  import loadVecExtension from '../../memory/sqlite-vec.js';
7
6
  import { DisplayManager } from '../../display.js';
7
+ import { PATHS } from '../../../config/paths.js';
8
8
  // ─── Repository ──────────────────────────────────────────────────────────────
9
9
  const EMBEDDING_DIM = 384;
10
10
  export class LinkRepository {
@@ -13,7 +13,7 @@ export class LinkRepository {
13
13
  dbPath;
14
14
  display = DisplayManager.getInstance();
15
15
  constructor(dbPath) {
16
- this.dbPath = dbPath || path.join(homedir(), '.morpheus', 'memory', 'link.db');
16
+ this.dbPath = dbPath || PATHS.linkDb;
17
17
  }
18
18
  static getInstance(dbPath) {
19
19
  if (!LinkRepository.instance) {
@@ -1,13 +1,13 @@
1
- import { homedir } from 'os';
2
- import path from 'path';
3
1
  import fs from 'fs-extra';
4
2
  import fsSync from 'fs';
3
+ import path from 'path';
5
4
  import { LinkRepository } from './repository.js';
6
5
  import { LinkSearch } from './search.js';
7
6
  import { hashFile, processDocument, isSupportedFormat } from './chunker.js';
8
7
  import { EmbeddingService } from '../../memory/embedding.service.js';
9
8
  import { ConfigManager } from '../../../config/manager.js';
10
9
  import { DisplayManager } from '../../display.js';
10
+ import { PATHS } from '../../../config/paths.js';
11
11
  /**
12
12
  * LinkWorker - Background worker for document indexing
13
13
  *
@@ -26,7 +26,7 @@ export class LinkWorker {
26
26
  constructor() {
27
27
  this.repository = LinkRepository.getInstance();
28
28
  this.search = LinkSearch.getInstance();
29
- this.docsPath = path.join(homedir(), '.morpheus', 'docs');
29
+ this.docsPath = PATHS.docs;
30
30
  }
31
31
  static getInstance() {
32
32
  if (!LinkWorker.instance) {
@@ -1,6 +1,6 @@
1
1
  import { HumanMessage, SystemMessage, AIMessage } from "@langchain/core/messages";
2
2
  import { ConfigManager } from "../../config/manager.js";
3
- import { ProviderFactory } from "../providers/factory.js";
3
+ import { ServiceContainer, SERVICE_KEYS } from "../container.js";
4
4
  import { ProviderError } from "../errors.js";
5
5
  import { DisplayManager } from "../display.js";
6
6
  import { Construtor } from "../tools/factory.js";
@@ -101,7 +101,7 @@ export class Neo {
101
101
  }
102
102
  this.display.log(`Neo initialized with ${tools.length} tools (personality: ${personality}).`, { source: "Neo" });
103
103
  try {
104
- this.agent = await ProviderFactory.create(neoConfig, tools);
104
+ this.agent = await ServiceContainer.get(SERVICE_KEYS.providerFactory).create(neoConfig, tools);
105
105
  }
106
106
  catch (err) {
107
107
  throw new ProviderError(neoConfig.provider, err, "Neo subagent initialization failed");