@operor/cli 0.1.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.
Files changed (62) hide show
  1. package/README.md +76 -0
  2. package/dist/config-Bn2pbORi.js +34 -0
  3. package/dist/config-Bn2pbORi.js.map +1 -0
  4. package/dist/converse-C_PB7-JH.js +142 -0
  5. package/dist/converse-C_PB7-JH.js.map +1 -0
  6. package/dist/doctor-98gPl743.js +122 -0
  7. package/dist/doctor-98gPl743.js.map +1 -0
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.js +2268 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/llm-override-BIQl0V6H.js +445 -0
  12. package/dist/llm-override-BIQl0V6H.js.map +1 -0
  13. package/dist/reset-DT8SBgFS.js +87 -0
  14. package/dist/reset-DT8SBgFS.js.map +1 -0
  15. package/dist/simulate-BKv62GJc.js +144 -0
  16. package/dist/simulate-BKv62GJc.js.map +1 -0
  17. package/dist/status-D6LIZvQa.js +82 -0
  18. package/dist/status-D6LIZvQa.js.map +1 -0
  19. package/dist/test-DYjkxbtK.js +177 -0
  20. package/dist/test-DYjkxbtK.js.map +1 -0
  21. package/dist/test-suite-D8H_5uKs.js +209 -0
  22. package/dist/test-suite-D8H_5uKs.js.map +1 -0
  23. package/dist/utils-BuV4q7f6.js +11 -0
  24. package/dist/utils-BuV4q7f6.js.map +1 -0
  25. package/dist/vibe-Bl_js3Jo.js +395 -0
  26. package/dist/vibe-Bl_js3Jo.js.map +1 -0
  27. package/package.json +43 -0
  28. package/src/commands/analytics.ts +408 -0
  29. package/src/commands/chat.ts +310 -0
  30. package/src/commands/config.ts +34 -0
  31. package/src/commands/converse.ts +182 -0
  32. package/src/commands/doctor.ts +154 -0
  33. package/src/commands/history.ts +60 -0
  34. package/src/commands/init.ts +163 -0
  35. package/src/commands/kb.ts +429 -0
  36. package/src/commands/llm-override.ts +480 -0
  37. package/src/commands/reset.ts +72 -0
  38. package/src/commands/simulate.ts +187 -0
  39. package/src/commands/status.ts +112 -0
  40. package/src/commands/test-suite.ts +247 -0
  41. package/src/commands/test.ts +177 -0
  42. package/src/commands/vibe.ts +478 -0
  43. package/src/config.ts +127 -0
  44. package/src/index.ts +190 -0
  45. package/src/log-timestamps.ts +26 -0
  46. package/src/setup.ts +712 -0
  47. package/src/start.ts +573 -0
  48. package/src/utils.ts +6 -0
  49. package/templates/agents/_defaults/SOUL.md +20 -0
  50. package/templates/agents/_defaults/USER.md +16 -0
  51. package/templates/agents/customer-support/IDENTITY.md +6 -0
  52. package/templates/agents/customer-support/INSTRUCTIONS.md +79 -0
  53. package/templates/agents/customer-support/SOUL.md +26 -0
  54. package/templates/agents/faq-bot/IDENTITY.md +6 -0
  55. package/templates/agents/faq-bot/INSTRUCTIONS.md +53 -0
  56. package/templates/agents/faq-bot/SOUL.md +19 -0
  57. package/templates/agents/sales/IDENTITY.md +6 -0
  58. package/templates/agents/sales/INSTRUCTIONS.md +67 -0
  59. package/templates/agents/sales/SOUL.md +20 -0
  60. package/tsconfig.json +9 -0
  61. package/tsdown.config.ts +13 -0
  62. package/vitest.config.ts +8 -0
package/src/start.ts ADDED
@@ -0,0 +1,573 @@
1
+ import fs from 'fs';
2
+ import type { Skill, Tool } from '@operor/core';
3
+ import { readConfig } from './config.js';
4
+ import { applyLLMOverride } from './commands/llm-override.js';
5
+
6
+ export async function startOperor(): Promise<void> {
7
+ const config = readConfig();
8
+
9
+ if (!config.LLM_PROVIDER || !config.CHANNEL) {
10
+ console.error('Missing configuration. Run "operor setup" first.');
11
+ process.exit(1);
12
+ }
13
+
14
+ console.log('[Operor] Starting...');
15
+
16
+ // Dynamic imports to avoid requiring all deps upfront
17
+ const { Operor, LLMIntentClassifier, AgentLoader } = await import('@operor/core');
18
+ const { AIProvider } = await import('@operor/llm');
19
+
20
+ // Setup memory store (before LLM so we can load persisted model settings)
21
+ let memory: any;
22
+ if (config.MEMORY_TYPE === 'sqlite') {
23
+ const { SQLiteMemory } = await import('@operor/memory');
24
+ memory = new SQLiteMemory(config.MEMORY_DB_PATH || './operor.db');
25
+ console.log('[Operor] Using SQLite memory store');
26
+ }
27
+
28
+ // Load persisted model settings (override env vars if stored)
29
+ let llmProvider = config.LLM_PROVIDER as 'openai' | 'anthropic' | 'google' | 'groq' | 'ollama';
30
+ let llmModel = config.LLM_MODEL;
31
+ let llmApiKey = config.LLM_API_KEY;
32
+ if (memory?.getSetting) {
33
+ try {
34
+ const storedProvider = await memory.getSetting('llm_provider');
35
+ const storedModel = await memory.getSetting('llm_model');
36
+ if (storedProvider) llmProvider = storedProvider as any;
37
+ if (storedModel) llmModel = storedModel;
38
+ // Load stored API key for the active provider
39
+ const storedKey = await memory.getSetting(`llm_apikey_${llmProvider}`);
40
+ if (storedKey) llmApiKey = storedKey;
41
+ } catch { /* getSetting not available */ }
42
+ }
43
+
44
+ // Setup LLM (needed for both completion and intent classification)
45
+ const llm = new AIProvider({
46
+ provider: llmProvider,
47
+ apiKey: llmApiKey,
48
+ model: llmModel,
49
+ });
50
+
51
+ // Load stored API keys for other providers
52
+ if (memory?.getSetting) {
53
+ for (const p of ['openai', 'anthropic', 'google', 'groq', 'ollama']) {
54
+ if (p === llmProvider) continue;
55
+ try {
56
+ const k = await memory.getSetting(`llm_apikey_${p}`);
57
+ if (k) llm.setApiKey(p, k);
58
+ } catch { /* ignore */ }
59
+ }
60
+ }
61
+
62
+ // Setup intent classifier
63
+ let intentClassifier: any;
64
+ if (config.INTENT_CLASSIFIER === 'llm') {
65
+ intentClassifier = new LLMIntentClassifier(llm);
66
+ console.log('[Operor] Using LLM intent classification');
67
+ }
68
+
69
+ // Setup Knowledge Base runtime (if enabled)
70
+ let kbRuntime: any;
71
+ let embedder: any;
72
+ if (config.KB_ENABLED === 'true') {
73
+ try {
74
+ const {
75
+ SQLiteKnowledgeStore,
76
+ EmbeddingService,
77
+ TextChunker,
78
+ IngestionPipeline,
79
+ RetrievalPipeline,
80
+ QueryRewriter,
81
+ UrlIngestor,
82
+ SiteCrawler,
83
+ FileIngestor,
84
+ } = await import('@operor/knowledge');
85
+
86
+ embedder = new EmbeddingService({
87
+ provider: (config.KB_EMBEDDING_PROVIDER || 'openai') as any,
88
+ apiKey: config.KB_EMBEDDING_API_KEY || config.LLM_API_KEY,
89
+ model: config.KB_EMBEDDING_MODEL,
90
+ });
91
+
92
+ const kbStore = new SQLiteKnowledgeStore(
93
+ config.KB_DB_PATH || './knowledge.db',
94
+ embedder.dimensions,
95
+ );
96
+ await kbStore.initialize();
97
+
98
+ const chunker = new TextChunker({
99
+ chunkSize: config.KB_CHUNK_SIZE ? parseInt(config.KB_CHUNK_SIZE) : undefined,
100
+ chunkOverlap: config.KB_CHUNK_OVERLAP ? parseInt(config.KB_CHUNK_OVERLAP) : undefined,
101
+ });
102
+ const ingestion = new IngestionPipeline(kbStore, embedder, chunker);
103
+
104
+ // Setup optional LLM query rewriter (Layer 4)
105
+ let queryRewriter: InstanceType<typeof QueryRewriter> | undefined;
106
+ try {
107
+ const rewriteModel = llm.getModel();
108
+ queryRewriter = new QueryRewriter({ model: rewriteModel });
109
+ } catch {
110
+ // LLM not available for rewriting — layers 1-3 still work
111
+ }
112
+
113
+ const retrieval = new RetrievalPipeline(kbStore, embedder, {
114
+ faqThreshold: 0.85,
115
+ queryRewriter,
116
+ fusionStrategy: (config.FUSION_STRATEGY as 'rrf' | 'weighted') || 'rrf',
117
+ });
118
+
119
+ // Create ingestors for URL, site, and file ingestion
120
+ const crawl4aiUrl = config.CRAWL4AI_URL || undefined;
121
+ const urlIngestor = new UrlIngestor(ingestion, { crawl4aiUrl });
122
+ const siteCrawler = new SiteCrawler(ingestion, { crawl4aiUrl });
123
+ const fileIngestor = new FileIngestor(ingestion);
124
+
125
+ // Wrap in KnowledgeBaseRuntime interface
126
+ kbRuntime = {
127
+ ingestFaq: async (question: string, answer: string, metadata?: Record<string, any>) => {
128
+ const result = await ingestion.ingestFaq(question, answer, metadata);
129
+ return { id: result.id, existingMatch: (result as any).existingMatch };
130
+ },
131
+ listDocuments: async () => {
132
+ return await kbStore.listDocuments();
133
+ },
134
+ deleteDocument: async (id: string) => {
135
+ return await kbStore.deleteDocument(id);
136
+ },
137
+ retrieve: async (query: string) => {
138
+ return await retrieval.retrieve(query);
139
+ },
140
+ getStats: async () => {
141
+ return await kbStore.getStats();
142
+ },
143
+ ingestUrl: async (url: string) => {
144
+ const doc = await urlIngestor.ingestUrl(url);
145
+ const faqCount = doc.metadata?.faqCount;
146
+ const chunks = faqCount ?? (kbStore.getChunkCount ? kbStore.getChunkCount(doc.id) : 0);
147
+ return { id: doc.id, title: doc.title, chunks, faqCount };
148
+ },
149
+ ingestSite: async (url: string, options?: { maxDepth?: number; maxPages?: number; onProgress?: (crawled: number, total: number, url: string) => void }) => {
150
+ const docs = await siteCrawler.crawlSite(url, {
151
+ ...options,
152
+ onProgress: options?.onProgress,
153
+ });
154
+ const totalChunks = docs.reduce((sum, doc) => {
155
+ const count = kbStore.getChunkCount ? kbStore.getChunkCount(doc.id) : 0;
156
+ return sum + count;
157
+ }, 0);
158
+ return { documents: docs.length, chunks: totalChunks };
159
+ },
160
+ ingestFile: async (buffer: Buffer, fileName: string) => {
161
+ // Write buffer to temp file, ingest, then clean up
162
+ const { writeFile, unlink } = await import('fs/promises');
163
+ const { join } = await import('path');
164
+ const { tmpdir } = await import('os');
165
+ const tempPath = join(tmpdir(), `operor-${Date.now()}-${fileName}`);
166
+ await writeFile(tempPath, buffer);
167
+ try {
168
+ const doc = await fileIngestor.ingestFile(tempPath, fileName);
169
+ const chunkCount = kbStore.getChunkCount ? kbStore.getChunkCount(doc.id) : 0;
170
+ return { id: doc.id, title: doc.title, chunks: chunkCount };
171
+ } finally {
172
+ await unlink(tempPath).catch(() => {}); // Clean up temp file
173
+ }
174
+ },
175
+ rebuild: async () => {
176
+ return await ingestion.rebuild();
177
+ },
178
+ };
179
+
180
+ console.log('[Operor] Knowledge Base enabled');
181
+ } catch (error: any) {
182
+ console.error(`[Operor] Failed to initialize Knowledge Base: ${error.message}`);
183
+ }
184
+ }
185
+
186
+ // Setup Training Copilot (if enabled)
187
+ let copilotHandler: any;
188
+ let copilotTracker: any;
189
+ if (config.COPILOT_ENABLED !== 'false' && kbRuntime) {
190
+ try {
191
+ const {
192
+ SQLiteCopilotStore,
193
+ UnansweredQueryTracker,
194
+ QueryClusterer,
195
+ SuggestionEngine,
196
+ CopilotCommandHandler,
197
+ DigestScheduler,
198
+ DEFAULT_COPILOT_CONFIG,
199
+ } = await import('@operor/copilot');
200
+
201
+ const copilotConfig = {
202
+ ...DEFAULT_COPILOT_CONFIG,
203
+ enabled: true,
204
+ trackingThreshold: config.COPILOT_TRACKING_THRESHOLD ? parseFloat(config.COPILOT_TRACKING_THRESHOLD) : DEFAULT_COPILOT_CONFIG.trackingThreshold,
205
+ clusterThreshold: config.COPILOT_CLUSTER_THRESHOLD ? parseFloat(config.COPILOT_CLUSTER_THRESHOLD) : DEFAULT_COPILOT_CONFIG.clusterThreshold,
206
+ digestIntervalMs: config.COPILOT_DIGEST_INTERVAL ? parseInt(config.COPILOT_DIGEST_INTERVAL) : DEFAULT_COPILOT_CONFIG.digestIntervalMs,
207
+ digestMaxItems: config.COPILOT_DIGEST_MAX_ITEMS ? parseInt(config.COPILOT_DIGEST_MAX_ITEMS) : DEFAULT_COPILOT_CONFIG.digestMaxItems,
208
+ autoSuggest: config.COPILOT_AUTO_SUGGEST !== 'false',
209
+ };
210
+
211
+ // Reuse the same embedder dimensions from KB
212
+ const copilotStore = new SQLiteCopilotStore(
213
+ config.COPILOT_DB_PATH || './copilot.db',
214
+ embedder.dimensions,
215
+ );
216
+ await copilotStore.initialize();
217
+
218
+ const clusterer = new QueryClusterer(copilotStore, embedder, {
219
+ clusterThreshold: copilotConfig.clusterThreshold,
220
+ });
221
+
222
+ copilotTracker = new UnansweredQueryTracker(
223
+ copilotStore,
224
+ copilotConfig,
225
+ embedder,
226
+ clusterer,
227
+ );
228
+
229
+ // SuggestionEngine wraps the LLM for generating draft answers
230
+ const suggestionEngine = copilotConfig.autoSuggest
231
+ ? new SuggestionEngine(
232
+ { generateText: (opts: any) => llm.complete([
233
+ ...(opts.system ? [{ role: 'system' as const, content: opts.system }] : []),
234
+ { role: 'user' as const, content: opts.prompt },
235
+ ]) },
236
+ kbRuntime,
237
+ )
238
+ : undefined;
239
+
240
+ copilotHandler = new CopilotCommandHandler(
241
+ copilotStore,
242
+ suggestionEngine,
243
+ kbRuntime,
244
+ clusterer,
245
+ );
246
+
247
+ // Start digest scheduler if training mode whitelist is configured
248
+ const adminPhones = config.TRAINING_MODE_WHITELIST?.split(',').map((p: string) => p.trim()).filter(Boolean) || [];
249
+ if (adminPhones.length > 0) {
250
+ const digest = new DigestScheduler(copilotStore, copilotConfig, async (phone: string, text: string) => {
251
+ try {
252
+ if (provider && typeof provider.sendMessage === 'function') {
253
+ await provider.sendMessage(phone, text);
254
+ }
255
+ } catch (err: any) {
256
+ console.warn('[Copilot] Failed to send digest:', err?.message);
257
+ }
258
+ });
259
+ digest.start(adminPhones);
260
+ }
261
+
262
+ console.log('[Operor] Training Copilot enabled');
263
+ } catch (error: any) {
264
+ console.error(`[Operor] Failed to initialize Training Copilot: ${error.message}`);
265
+ console.error('[Operor] /review commands will not be available. Try running "pnpm install" if packages are missing.');
266
+ }
267
+ }
268
+
269
+ // Setup Analytics (enabled by default)
270
+ let analyticsCollector: any;
271
+ let analyticsStore: any;
272
+ if (config.ANALYTICS_ENABLED !== 'false') {
273
+ try {
274
+ const { SQLiteAnalyticsStore, AnalyticsCollector } = await import('@operor/analytics');
275
+ analyticsStore = new SQLiteAnalyticsStore(config.ANALYTICS_DB_PATH || './analytics.db');
276
+ await analyticsStore.initialize();
277
+ analyticsCollector = new AnalyticsCollector(analyticsStore, { debug: true });
278
+ console.log('[Operor] Analytics enabled');
279
+ } catch (error: any) {
280
+ console.error(`[Operor] Failed to initialize Analytics: ${error.message}`);
281
+ }
282
+ }
283
+
284
+ // Build training whitelist — DB is source of truth, env var is bootstrap seed
285
+ let trainingWhitelist: string[] = config.TRAINING_MODE_WHITELIST?.split(',').map(p => p.trim()).filter(Boolean) || [];
286
+
287
+ if (config.TRAINING_MODE_ENABLED === 'true' && memory?.getSetting) {
288
+ try {
289
+ const stored = await memory.getSetting('training_whitelist');
290
+ if (stored) {
291
+ trainingWhitelist = stored.split(',').map((p: string) => p.trim()).filter(Boolean);
292
+ } else if (trainingWhitelist.length > 0) {
293
+ // Seed DB from env var on first run
294
+ await memory.setSetting('training_whitelist', trainingWhitelist.join(','));
295
+ }
296
+ } catch {
297
+ // getSetting not available — fall back to env var
298
+ }
299
+ }
300
+
301
+ // Pre-load skills module for injection into Operor (avoids circular dep: core cannot import skills)
302
+ let skillsModule: any;
303
+ try {
304
+ skillsModule = await import('@operor/skills');
305
+ } catch {
306
+ // skills package not available — catalog commands will show "not available"
307
+ }
308
+
309
+ // Detect agents directory early so Operor can persist frontmatter changes
310
+ const agentsDir = `${process.cwd()}/agents`;
311
+ const hasAgentsDir = fs.existsSync(agentsDir);
312
+
313
+ // Create Operor instance
314
+ const os = new Operor({
315
+ debug: true,
316
+ llmProvider: llm,
317
+ ...(memory && { memory }),
318
+ ...(intentClassifier && { intentClassifier }),
319
+ ...(kbRuntime && { kb: kbRuntime }),
320
+ ...(copilotHandler && { copilotHandler }),
321
+ ...(copilotTracker && { copilotTracker }),
322
+ ...(analyticsCollector && { analyticsCollector }),
323
+ ...(analyticsStore && { analyticsStore }),
324
+ ...(skillsModule && { skillsModule }),
325
+ ...(hasAgentsDir && { agentsDir }),
326
+ ...(config.TRAINING_MODE_ENABLED === 'true' && {
327
+ trainingMode: {
328
+ enabled: true,
329
+ whitelist: trainingWhitelist,
330
+ },
331
+ }),
332
+ });
333
+
334
+ // Setup provider
335
+ let provider: any;
336
+ if (config.CHANNEL === 'whatsapp') {
337
+ const { BaileysProvider } = await import('@operor/provider-baileys');
338
+ provider = new BaileysProvider({
339
+ groupMode: (config.WHATSAPP_GROUP_MODE as any) || 'mention-only',
340
+ });
341
+ } else if (config.CHANNEL === 'telegram') {
342
+ const { TelegramProvider } = await import('@operor/provider-telegram');
343
+ provider = new TelegramProvider({ botToken: config.TELEGRAM_BOT_TOKEN! });
344
+ } else if (config.CHANNEL === 'wati') {
345
+ const { WatiProvider } = await import('@operor/provider-wati');
346
+ provider = new WatiProvider({
347
+ apiToken: config.WATI_API_TOKEN!,
348
+ tenantId: config.WATI_TENANT_ID!,
349
+ webhookPort: config.WATI_WEBHOOK_PORT ? parseInt(config.WATI_WEBHOOK_PORT) : undefined,
350
+ });
351
+ } else {
352
+ const { MockProvider } = await import('@operor/provider-mock');
353
+ provider = new MockProvider();
354
+ }
355
+ os.addProvider(provider);
356
+
357
+ // Setup MCP skills from mcp.json
358
+ let skillManager: any;
359
+ const skillInstances: Skill[] = [];
360
+ let loadSkillsConfig: any;
361
+ try {
362
+ const skillsMod = await import('@operor/skills');
363
+ loadSkillsConfig = skillsMod.loadSkillsConfig;
364
+ const { SkillManager } = skillsMod;
365
+ const skillsConfig = loadSkillsConfig();
366
+ skillManager = new SkillManager();
367
+ const mcpSkills = await skillManager.initialize(skillsConfig);
368
+ for (const skill of mcpSkills) {
369
+ os.addSkill(skill);
370
+ skillInstances.push(skill);
371
+ }
372
+ if (mcpSkills.length > 0) {
373
+ console.log(`[Operor] ${mcpSkills.length} MCP skill(s) loaded`);
374
+ }
375
+ } catch (error: any) {
376
+ // Graceful if mcp.json doesn't exist or skills fail to load
377
+ if (config.SKILLS_ENABLED === 'true') {
378
+ console.warn(`[Operor] Failed to load MCP skills: ${error.message}`);
379
+ }
380
+ }
381
+
382
+ // Collect all tools from skills
383
+ const allTools: Tool[] = skillInstances.flatMap((skill) => Object.values(skill.tools));
384
+
385
+ // Helper: get prompt skill name+content for an agent (filtered by skills list)
386
+ const getPromptSkills = (agentSkillNames?: string[]): Array<{name: string, content: string}> => {
387
+ return skillInstances
388
+ .filter((s): s is any => typeof (s as any).getContent === 'function')
389
+ .filter((s) => !agentSkillNames || agentSkillNames.includes(s.name))
390
+ .map((s) => ({ name: s.name, content: (s as any).getContent() }));
391
+ };
392
+
393
+ // Track per-agent tool arrays so we can mutate them in-place on hot-reload
394
+ const agentToolsMap = new Map<string, Tool[]>();
395
+
396
+ let definitions: Awaited<ReturnType<InstanceType<typeof AgentLoader>['loadAll']>> = [];
397
+
398
+ if (hasAgentsDir) {
399
+ // File-based agent definitions
400
+ const loader = new AgentLoader(process.cwd());
401
+ definitions = await loader.loadAll();
402
+
403
+ if (definitions.length === 0) {
404
+ console.error('[Operor] agents/ directory found but no valid agent definitions. Check INSTRUCTIONS.md files.');
405
+ process.exit(1);
406
+ }
407
+
408
+ for (const def of definitions) {
409
+ // Filter tools for this agent based on its skills list
410
+ const agentTools: Tool[] = def.config.skills
411
+ ? skillInstances
412
+ .filter((skill) => def.config.skills!.includes(skill.name))
413
+ .flatMap((skill) => Object.values(skill.tools))
414
+ : allTools;
415
+
416
+ agentToolsMap.set(def.config.name, agentTools);
417
+
418
+ const agent = os.createAgent({
419
+ ...def.config,
420
+ tools: agentTools,
421
+ });
422
+
423
+ // Override agent.process with LLM using the file-based systemPrompt
424
+ applyLLMOverride(agent, llm, agentTools, {
425
+ systemPrompt: def.systemPrompt,
426
+ kbRuntime,
427
+ useKB: def.config.knowledgeBase,
428
+ guardrails: def.config.guardrails,
429
+ promptSkills: getPromptSkills(def.config.skills),
430
+ });
431
+
432
+ console.log(
433
+ `[Operor] Loaded agent: ${def.config.name}` +
434
+ (def.config.channels ? ` (channels: ${def.config.channels.join(', ')})` : '') +
435
+ (def.config.skills ? ` (skills: ${def.config.skills.join(', ')})` : '') +
436
+ (def.config.knowledgeBase ? ' (KB)' : '') +
437
+ (def.config.priority ? ` (priority: ${def.config.priority})` : '')
438
+ );
439
+ }
440
+
441
+ console.log(`[Operor] Loaded ${definitions.length} agent(s) from agents/ directory`);
442
+ } else {
443
+ // Fallback: single agent (backward compatible)
444
+ const agent = os.createAgent({
445
+ name: 'support',
446
+ purpose: 'Customer support agent',
447
+ personality: 'Friendly, helpful, and professional',
448
+ triggers: ['*'],
449
+ tools: allTools,
450
+ });
451
+
452
+ agentToolsMap.set('support', allTools);
453
+ applyLLMOverride(agent, llm, allTools, { kbRuntime, promptSkills: getPromptSkills() });
454
+ console.log('[Operor] Using default single-agent mode');
455
+ }
456
+
457
+ // --- Hot-reload MCP skills on mcp.json change ---
458
+ const reloadSkills = async () => {
459
+ if (!skillManager || !loadSkillsConfig) return;
460
+
461
+ console.log('[Operor] \uD83D\uDD04 Reloading MCP skills...');
462
+ try {
463
+ // 1. Re-read mcp.json
464
+ const newConfig = loadSkillsConfig();
465
+
466
+ // 2. Remove old skills from Operor (closes MCP clients)
467
+ for (const skill of skillInstances) {
468
+ await os.removeSkill(skill.name);
469
+ }
470
+
471
+ // 3. Reset SkillManager internal state
472
+ await skillManager.closeAll();
473
+
474
+ // 4. Initialize new skills
475
+ const newSkills = await skillManager.initialize(newConfig);
476
+
477
+ // 5. Update skillInstances in-place
478
+ skillInstances.splice(0, skillInstances.length, ...newSkills);
479
+
480
+ // 6. Add new skills to Operor (double-init guarded)
481
+ for (const skill of newSkills) {
482
+ await os.addSkill(skill);
483
+ }
484
+
485
+ // 7. Rebuild allTools in-place so existing closures see the change
486
+ const freshTools: Tool[] = skillInstances.flatMap((skill) => Object.values(skill.tools));
487
+ allTools.splice(0, allTools.length, ...freshTools);
488
+
489
+ // 8. Rebuild each agent's tools array in-place
490
+ for (const def of definitions) {
491
+ const agentTools = agentToolsMap.get(def.config.name);
492
+ if (!agentTools) continue;
493
+ const newAgentTools: Tool[] = def.config.skills
494
+ ? skillInstances
495
+ .filter((skill) => def.config.skills!.includes(skill.name))
496
+ .flatMap((skill) => Object.values(skill.tools))
497
+ : freshTools;
498
+ agentTools.splice(0, agentTools.length, ...newAgentTools);
499
+ }
500
+
501
+ // Also update fallback single-agent if no definitions
502
+ if (definitions.length === 0) {
503
+ const fallbackTools = agentToolsMap.get('support');
504
+ if (fallbackTools) {
505
+ fallbackTools.splice(0, fallbackTools.length, ...freshTools);
506
+ }
507
+ }
508
+
509
+ // 9. Re-apply LLM override for agents whose prompt skill content may have changed
510
+ for (const def of definitions) {
511
+ const agentTools = agentToolsMap.get(def.config.name);
512
+ if (!agentTools) continue;
513
+ const agent = os.getAgents().find((a: any) => a.getConfig?.().name === def.config.name || a.config?.name === def.config.name);
514
+ if (agent) {
515
+ applyLLMOverride(agent, llm, agentTools, {
516
+ systemPrompt: def.systemPrompt,
517
+ kbRuntime,
518
+ useKB: def.config.knowledgeBase,
519
+ guardrails: def.config.guardrails,
520
+ promptSkills: getPromptSkills(def.config.skills),
521
+ });
522
+ }
523
+ }
524
+
525
+ console.log(`[Operor] \u2705 Reloaded ${newSkills.length} skill(s)`);
526
+ } catch (error: any) {
527
+ console.error(`[Operor] \u274C Failed to reload MCP skills: ${error.message}`);
528
+ }
529
+ };
530
+
531
+ // Start
532
+ await os.start();
533
+ console.log('[Operor] Running. Press Ctrl+C to stop.');
534
+
535
+ // Watch mcp.json for changes and hot-reload skills
536
+ const mcpJsonPath = `${process.cwd()}/mcp.json`;
537
+ if (fs.existsSync(mcpJsonPath) && loadSkillsConfig) {
538
+ let reloadTimer: ReturnType<typeof setTimeout> | null = null;
539
+ fs.watch(mcpJsonPath, () => {
540
+ if (reloadTimer) clearTimeout(reloadTimer);
541
+ reloadTimer = setTimeout(() => {
542
+ reloadTimer = null;
543
+ reloadSkills().catch((err) => {
544
+ console.error(`[Operor] Skill reload error: ${err.message}`);
545
+ });
546
+ }, 500);
547
+ });
548
+ console.log('[Operor] Watching mcp.json for changes');
549
+ }
550
+
551
+ // Graceful shutdown — close MCP skills
552
+ const shutdown = async () => {
553
+ console.log('\n[Operor] Shutting down...');
554
+ if (skillManager) {
555
+ await skillManager.closeAll();
556
+ }
557
+ await os.stop();
558
+ process.exit(0);
559
+ };
560
+
561
+ process.on('SIGINT', shutdown);
562
+ process.on('SIGTERM', shutdown);
563
+
564
+ // For mock provider, simulate a test message
565
+ if (config.CHANNEL === 'mock') {
566
+ setTimeout(() => {
567
+ provider.simulateIncomingMessage(
568
+ '+1234567890',
569
+ 'Hi, I need help with my order #1001'
570
+ );
571
+ }, 2000);
572
+ }
573
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Format current time as HH:MM:SS timestamp
3
+ */
4
+ export function formatTimestamp(): string {
5
+ return new Date().toLocaleTimeString('en-US', { hour12: false });
6
+ }
@@ -0,0 +1,20 @@
1
+ # Default Agent Soul
2
+
3
+ ## Core Principles
4
+
5
+ - Be helpful, accurate, and respectful
6
+ - Respond promptly and stay on topic
7
+ - When unsure, ask for clarification rather than guessing
8
+ - Protect customer privacy at all times
9
+
10
+ ## Boundaries
11
+
12
+ - Only discuss topics within your defined scope
13
+ - Escalate to a human when the situation requires judgment beyond your capabilities
14
+ - Never fabricate information — use your integrations to look up real data
15
+
16
+ ## Tone
17
+
18
+ - Professional and friendly
19
+ - Clear and concise — avoid unnecessary verbosity
20
+ - Adapt to the customer's communication style
@@ -0,0 +1,16 @@
1
+ # Business Owner Profile
2
+
3
+ **Name:** (Your name)
4
+ **Business:** (Your business name)
5
+ **Timezone:** (e.g., America/New_York)
6
+ **Language:** English
7
+
8
+ ## Preferences
9
+
10
+ - **Response style:** (concise / detailed / casual / formal)
11
+ - **Escalation preference:** (email / phone / in-app)
12
+ - **Operating hours:** (e.g., Mon-Fri 9am-6pm EST)
13
+
14
+ ## Notes
15
+
16
+ Add any business-specific context here that agents should be aware of — seasonal policies, current promotions, known issues, etc.
@@ -0,0 +1,6 @@
1
+ # Customer Support Agent Identity
2
+
3
+ **Name:** Support Agent
4
+ **Emoji:** 🎧
5
+ **Nature:** Helpful, patient, solution-oriented
6
+ **Vibe:** Like a knowledgeable friend who works at the store — approachable, efficient, and genuinely wants to help you out.