@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.
- package/README.md +76 -0
- package/dist/config-Bn2pbORi.js +34 -0
- package/dist/config-Bn2pbORi.js.map +1 -0
- package/dist/converse-C_PB7-JH.js +142 -0
- package/dist/converse-C_PB7-JH.js.map +1 -0
- package/dist/doctor-98gPl743.js +122 -0
- package/dist/doctor-98gPl743.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2268 -0
- package/dist/index.js.map +1 -0
- package/dist/llm-override-BIQl0V6H.js +445 -0
- package/dist/llm-override-BIQl0V6H.js.map +1 -0
- package/dist/reset-DT8SBgFS.js +87 -0
- package/dist/reset-DT8SBgFS.js.map +1 -0
- package/dist/simulate-BKv62GJc.js +144 -0
- package/dist/simulate-BKv62GJc.js.map +1 -0
- package/dist/status-D6LIZvQa.js +82 -0
- package/dist/status-D6LIZvQa.js.map +1 -0
- package/dist/test-DYjkxbtK.js +177 -0
- package/dist/test-DYjkxbtK.js.map +1 -0
- package/dist/test-suite-D8H_5uKs.js +209 -0
- package/dist/test-suite-D8H_5uKs.js.map +1 -0
- package/dist/utils-BuV4q7f6.js +11 -0
- package/dist/utils-BuV4q7f6.js.map +1 -0
- package/dist/vibe-Bl_js3Jo.js +395 -0
- package/dist/vibe-Bl_js3Jo.js.map +1 -0
- package/package.json +43 -0
- package/src/commands/analytics.ts +408 -0
- package/src/commands/chat.ts +310 -0
- package/src/commands/config.ts +34 -0
- package/src/commands/converse.ts +182 -0
- package/src/commands/doctor.ts +154 -0
- package/src/commands/history.ts +60 -0
- package/src/commands/init.ts +163 -0
- package/src/commands/kb.ts +429 -0
- package/src/commands/llm-override.ts +480 -0
- package/src/commands/reset.ts +72 -0
- package/src/commands/simulate.ts +187 -0
- package/src/commands/status.ts +112 -0
- package/src/commands/test-suite.ts +247 -0
- package/src/commands/test.ts +177 -0
- package/src/commands/vibe.ts +478 -0
- package/src/config.ts +127 -0
- package/src/index.ts +190 -0
- package/src/log-timestamps.ts +26 -0
- package/src/setup.ts +712 -0
- package/src/start.ts +573 -0
- package/src/utils.ts +6 -0
- package/templates/agents/_defaults/SOUL.md +20 -0
- package/templates/agents/_defaults/USER.md +16 -0
- package/templates/agents/customer-support/IDENTITY.md +6 -0
- package/templates/agents/customer-support/INSTRUCTIONS.md +79 -0
- package/templates/agents/customer-support/SOUL.md +26 -0
- package/templates/agents/faq-bot/IDENTITY.md +6 -0
- package/templates/agents/faq-bot/INSTRUCTIONS.md +53 -0
- package/templates/agents/faq-bot/SOUL.md +19 -0
- package/templates/agents/sales/IDENTITY.md +6 -0
- package/templates/agents/sales/INSTRUCTIONS.md +67 -0
- package/templates/agents/sales/SOUL.md +20 -0
- package/tsconfig.json +9 -0
- package/tsdown.config.ts +13 -0
- 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,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.
|