@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/dist/index.js
ADDED
|
@@ -0,0 +1,2268 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { t as applyLLMOverride } from "./llm-override-BIQl0V6H.js";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
import * as clack from "@clack/prompts";
|
|
7
|
+
import matter from "gray-matter";
|
|
8
|
+
import { AIProvider } from "@operor/llm";
|
|
9
|
+
import { catalogEntryToConfig, findSkillInCatalog, loadSkillCatalog } from "@operor/skills";
|
|
10
|
+
import fs from "fs";
|
|
11
|
+
import * as readline from "node:readline";
|
|
12
|
+
import { createInterface } from "node:readline";
|
|
13
|
+
import { access, cp, mkdir, readdir } from "node:fs/promises";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
|
|
16
|
+
//#region src/log-timestamps.ts
|
|
17
|
+
/**
|
|
18
|
+
* Global console override that prepends timestamps to all log output.
|
|
19
|
+
* Format: [YYYY-MM-DD HH:mm:ss.SSS] — similar to NLog/structured logging.
|
|
20
|
+
*
|
|
21
|
+
* Import this file once at the entry point (index.ts) for automatic timestamps.
|
|
22
|
+
*/
|
|
23
|
+
const originalLog = console.log;
|
|
24
|
+
const originalWarn = console.warn;
|
|
25
|
+
const originalError = console.error;
|
|
26
|
+
function timestamp() {
|
|
27
|
+
const now = /* @__PURE__ */ new Date();
|
|
28
|
+
return `[${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")} ${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}.${String(now.getMilliseconds()).padStart(3, "0")}]`;
|
|
29
|
+
}
|
|
30
|
+
console.log = (...args) => originalLog(timestamp(), ...args);
|
|
31
|
+
console.warn = (...args) => originalWarn(timestamp(), ...args);
|
|
32
|
+
console.error = (...args) => originalError(timestamp(), ...args);
|
|
33
|
+
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/config.ts
|
|
36
|
+
const ENV_PATH = resolve(process.cwd(), ".env");
|
|
37
|
+
function configExists() {
|
|
38
|
+
return existsSync(ENV_PATH);
|
|
39
|
+
}
|
|
40
|
+
function readConfig() {
|
|
41
|
+
if (!existsSync(ENV_PATH)) return {};
|
|
42
|
+
const content = readFileSync(ENV_PATH, "utf-8");
|
|
43
|
+
const config = {};
|
|
44
|
+
for (const line of content.split("\n")) {
|
|
45
|
+
const trimmed = line.trim();
|
|
46
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
47
|
+
const eqIndex = trimmed.indexOf("=");
|
|
48
|
+
if (eqIndex === -1) continue;
|
|
49
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
50
|
+
config[key] = trimmed.slice(eqIndex + 1).trim();
|
|
51
|
+
}
|
|
52
|
+
return config;
|
|
53
|
+
}
|
|
54
|
+
function writeConfig(config) {
|
|
55
|
+
const lines = [
|
|
56
|
+
"# Operor Configuration",
|
|
57
|
+
"# Generated by operor setup",
|
|
58
|
+
""
|
|
59
|
+
];
|
|
60
|
+
const sections = [
|
|
61
|
+
{
|
|
62
|
+
header: "LLM",
|
|
63
|
+
keys: [
|
|
64
|
+
"LLM_PROVIDER",
|
|
65
|
+
"LLM_API_KEY",
|
|
66
|
+
"LLM_MODEL",
|
|
67
|
+
"LLM_BASE_URL"
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
header: "Channel",
|
|
72
|
+
keys: ["CHANNEL"]
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
header: "Telegram",
|
|
76
|
+
keys: ["TELEGRAM_BOT_TOKEN"]
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
header: "Skills",
|
|
80
|
+
keys: ["SKILLS_ENABLED"]
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
header: "WATI",
|
|
84
|
+
keys: [
|
|
85
|
+
"WATI_API_TOKEN",
|
|
86
|
+
"WATI_TENANT_ID",
|
|
87
|
+
"WATI_WEBHOOK_PORT"
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
header: "Memory",
|
|
92
|
+
keys: ["MEMORY_TYPE", "MEMORY_DB_PATH"]
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
header: "Intent",
|
|
96
|
+
keys: ["INTENT_CLASSIFIER"]
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
header: "Knowledge Base",
|
|
100
|
+
keys: [
|
|
101
|
+
"KB_ENABLED",
|
|
102
|
+
"KB_DB_PATH",
|
|
103
|
+
"KB_EMBEDDING_PROVIDER",
|
|
104
|
+
"KB_EMBEDDING_MODEL",
|
|
105
|
+
"KB_EMBEDDING_API_KEY",
|
|
106
|
+
"KB_RENDER_JS",
|
|
107
|
+
"KB_CHUNK_SIZE",
|
|
108
|
+
"KB_CHUNK_OVERLAP"
|
|
109
|
+
]
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
header: "Training Mode",
|
|
113
|
+
keys: ["TRAINING_MODE_ENABLED", "TRAINING_MODE_WHITELIST"]
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
header: "Training Copilot",
|
|
117
|
+
keys: [
|
|
118
|
+
"COPILOT_ENABLED",
|
|
119
|
+
"COPILOT_DB_PATH",
|
|
120
|
+
"COPILOT_TRACKING_THRESHOLD",
|
|
121
|
+
"COPILOT_CLUSTER_THRESHOLD",
|
|
122
|
+
"COPILOT_DIGEST_INTERVAL",
|
|
123
|
+
"COPILOT_DIGEST_MAX_ITEMS",
|
|
124
|
+
"COPILOT_AUTO_SUGGEST"
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
header: "Analytics",
|
|
129
|
+
keys: [
|
|
130
|
+
"ANALYTICS_ENABLED",
|
|
131
|
+
"ANALYTICS_DB_PATH",
|
|
132
|
+
"ANALYTICS_DIGEST_ENABLED",
|
|
133
|
+
"ANALYTICS_DIGEST_SCHEDULE",
|
|
134
|
+
"ANALYTICS_DIGEST_TIME"
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
];
|
|
138
|
+
const knownKeys = /* @__PURE__ */ new Set();
|
|
139
|
+
for (const section of sections) {
|
|
140
|
+
const sectionLines = [];
|
|
141
|
+
for (const key of section.keys) {
|
|
142
|
+
knownKeys.add(key);
|
|
143
|
+
if (config[key]) sectionLines.push(`${key}=${config[key]}`);
|
|
144
|
+
}
|
|
145
|
+
if (sectionLines.length > 0) {
|
|
146
|
+
lines.push(`# ${section.header}`);
|
|
147
|
+
lines.push(...sectionLines);
|
|
148
|
+
lines.push("");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const extraLines = [];
|
|
152
|
+
for (const [key, value] of Object.entries(config)) if (value && !knownKeys.has(key)) extraLines.push(`${key}=${value}`);
|
|
153
|
+
if (extraLines.length > 0) {
|
|
154
|
+
lines.push("# Other");
|
|
155
|
+
lines.push(...extraLines);
|
|
156
|
+
lines.push("");
|
|
157
|
+
}
|
|
158
|
+
writeFileSync(ENV_PATH, lines.join("\n"));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
//#endregion
|
|
162
|
+
//#region src/setup.ts
|
|
163
|
+
function detectApiKey(provider) {
|
|
164
|
+
const vars = {
|
|
165
|
+
openai: ["OPENAI_API_KEY"],
|
|
166
|
+
anthropic: ["ANTHROPIC_API_KEY"],
|
|
167
|
+
google: ["GOOGLE_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY"],
|
|
168
|
+
groq: ["GROQ_API_KEY"]
|
|
169
|
+
}[provider] || [];
|
|
170
|
+
for (const varName of vars) {
|
|
171
|
+
const value = process.env[varName];
|
|
172
|
+
if (value) return value;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function maskApiKey(key) {
|
|
176
|
+
if (key.length <= 8) return "••••••••";
|
|
177
|
+
return key.slice(0, 4) + "••••" + key.slice(-4);
|
|
178
|
+
}
|
|
179
|
+
async function runQuickSetup() {
|
|
180
|
+
console.clear();
|
|
181
|
+
clack.intro("Operor - Quick Setup");
|
|
182
|
+
const llmProvider = await clack.select({
|
|
183
|
+
message: "Choose your LLM provider",
|
|
184
|
+
options: [
|
|
185
|
+
{
|
|
186
|
+
value: "openai",
|
|
187
|
+
label: "OpenAI"
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
value: "anthropic",
|
|
191
|
+
label: "Anthropic (Claude)"
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
value: "google",
|
|
195
|
+
label: "Google (Gemini)"
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
value: "groq",
|
|
199
|
+
label: "Groq"
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
value: "ollama",
|
|
203
|
+
label: "Ollama (local)"
|
|
204
|
+
}
|
|
205
|
+
]
|
|
206
|
+
});
|
|
207
|
+
if (clack.isCancel(llmProvider)) {
|
|
208
|
+
clack.cancel("Setup cancelled");
|
|
209
|
+
process.exit(0);
|
|
210
|
+
}
|
|
211
|
+
let llmApiKey = "";
|
|
212
|
+
if (llmProvider !== "ollama") {
|
|
213
|
+
const detectedKey = detectApiKey(llmProvider);
|
|
214
|
+
if (detectedKey) {
|
|
215
|
+
const useExisting = await clack.confirm({
|
|
216
|
+
message: `Found existing API key (${maskApiKey(detectedKey)}). Use it?`,
|
|
217
|
+
initialValue: true
|
|
218
|
+
});
|
|
219
|
+
if (clack.isCancel(useExisting)) {
|
|
220
|
+
clack.cancel("Setup cancelled");
|
|
221
|
+
process.exit(0);
|
|
222
|
+
}
|
|
223
|
+
llmApiKey = useExisting ? detectedKey : await clack.password({
|
|
224
|
+
message: "Enter your API key",
|
|
225
|
+
validate: (value) => !value ? "API key is required" : void 0
|
|
226
|
+
});
|
|
227
|
+
} else llmApiKey = await clack.password({
|
|
228
|
+
message: "Enter your API key",
|
|
229
|
+
validate: (value) => !value ? "API key is required" : void 0
|
|
230
|
+
});
|
|
231
|
+
if (clack.isCancel(llmApiKey)) {
|
|
232
|
+
clack.cancel("Setup cancelled");
|
|
233
|
+
process.exit(0);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const spinner = clack.spinner();
|
|
237
|
+
spinner.start("Validating LLM credentials");
|
|
238
|
+
try {
|
|
239
|
+
await new AIProvider({
|
|
240
|
+
provider: llmProvider,
|
|
241
|
+
apiKey: llmApiKey
|
|
242
|
+
}).complete([{
|
|
243
|
+
role: "user",
|
|
244
|
+
content: "test"
|
|
245
|
+
}]);
|
|
246
|
+
spinner.stop("LLM credentials validated");
|
|
247
|
+
} catch (error) {
|
|
248
|
+
spinner.stop("LLM validation failed");
|
|
249
|
+
clack.log.error(`Failed to validate LLM credentials: ${error.message}`);
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
const config = {
|
|
253
|
+
LLM_PROVIDER: llmProvider,
|
|
254
|
+
LLM_API_KEY: llmApiKey,
|
|
255
|
+
CHANNEL: "mock",
|
|
256
|
+
MEMORY_TYPE: "sqlite",
|
|
257
|
+
INTENT_CLASSIFIER: "llm",
|
|
258
|
+
KB_ENABLED: "true",
|
|
259
|
+
KB_EMBEDDING_PROVIDER: llmProvider,
|
|
260
|
+
KB_EMBEDDING_API_KEY: llmApiKey,
|
|
261
|
+
TRAINING_MODE_ENABLED: "false",
|
|
262
|
+
ANALYTICS_ENABLED: "true"
|
|
263
|
+
};
|
|
264
|
+
writeConfig({
|
|
265
|
+
...readConfig(),
|
|
266
|
+
...config
|
|
267
|
+
});
|
|
268
|
+
clack.log.success("Configuration saved to .env");
|
|
269
|
+
clack.log.info(` LLM Provider: ${llmProvider}`);
|
|
270
|
+
clack.log.info(` Channel: mock (testing)`);
|
|
271
|
+
clack.log.info(` Memory: sqlite`);
|
|
272
|
+
clack.log.info(` Intent: llm`);
|
|
273
|
+
clack.log.info(` Knowledge Base: enabled`);
|
|
274
|
+
clack.log.info(` Analytics: enabled`);
|
|
275
|
+
clack.log.info(` Training Mode: disabled`);
|
|
276
|
+
clack.outro("Quick setup complete. Starting Operor...");
|
|
277
|
+
}
|
|
278
|
+
async function runSetup() {
|
|
279
|
+
console.clear();
|
|
280
|
+
clack.intro("Operor - Setup");
|
|
281
|
+
const existingConfig = readConfig();
|
|
282
|
+
const llmProvider = await clack.select({
|
|
283
|
+
message: "Choose your LLM provider",
|
|
284
|
+
options: [
|
|
285
|
+
{
|
|
286
|
+
value: "openai",
|
|
287
|
+
label: "OpenAI"
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
value: "anthropic",
|
|
291
|
+
label: "Anthropic (Claude)"
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
value: "google",
|
|
295
|
+
label: "Google (Gemini)"
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
value: "groq",
|
|
299
|
+
label: "Groq"
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
value: "ollama",
|
|
303
|
+
label: "Ollama (local)"
|
|
304
|
+
}
|
|
305
|
+
],
|
|
306
|
+
initialValue: existingConfig.LLM_PROVIDER || void 0
|
|
307
|
+
});
|
|
308
|
+
if (clack.isCancel(llmProvider)) {
|
|
309
|
+
clack.cancel("Setup cancelled");
|
|
310
|
+
process.exit(0);
|
|
311
|
+
}
|
|
312
|
+
const detectedKey = detectApiKey(llmProvider);
|
|
313
|
+
const existingApiKey = existingConfig.LLM_API_KEY || detectedKey;
|
|
314
|
+
let llmApiKey;
|
|
315
|
+
if (existingApiKey && llmProvider !== "ollama") {
|
|
316
|
+
const useExisting = await clack.confirm({
|
|
317
|
+
message: `Found existing API key (${maskApiKey(existingApiKey)}). Use it?`,
|
|
318
|
+
initialValue: true
|
|
319
|
+
});
|
|
320
|
+
if (clack.isCancel(useExisting)) {
|
|
321
|
+
clack.cancel("Setup cancelled");
|
|
322
|
+
process.exit(0);
|
|
323
|
+
}
|
|
324
|
+
if (useExisting) llmApiKey = existingApiKey;
|
|
325
|
+
else llmApiKey = await clack.password({
|
|
326
|
+
message: "Enter your API key",
|
|
327
|
+
validate: (value) => !value ? "API key is required" : void 0
|
|
328
|
+
});
|
|
329
|
+
} else llmApiKey = await clack.password({
|
|
330
|
+
message: "Enter your API key",
|
|
331
|
+
validate: (value) => {
|
|
332
|
+
if (llmProvider === "ollama") return;
|
|
333
|
+
if (!value) return "API key is required";
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
if (clack.isCancel(llmApiKey)) {
|
|
337
|
+
clack.cancel("Setup cancelled");
|
|
338
|
+
process.exit(0);
|
|
339
|
+
}
|
|
340
|
+
const llmModel = await clack.text({
|
|
341
|
+
message: "Enter model name (optional, press Enter for default)",
|
|
342
|
+
placeholder: getDefaultModel(llmProvider),
|
|
343
|
+
initialValue: existingConfig.LLM_MODEL || void 0
|
|
344
|
+
});
|
|
345
|
+
if (clack.isCancel(llmModel)) {
|
|
346
|
+
clack.cancel("Setup cancelled");
|
|
347
|
+
process.exit(0);
|
|
348
|
+
}
|
|
349
|
+
const spinner = clack.spinner();
|
|
350
|
+
spinner.start("Validating LLM credentials");
|
|
351
|
+
try {
|
|
352
|
+
await new AIProvider({
|
|
353
|
+
provider: llmProvider,
|
|
354
|
+
apiKey: llmApiKey,
|
|
355
|
+
model: llmModel || void 0
|
|
356
|
+
}).complete([{
|
|
357
|
+
role: "user",
|
|
358
|
+
content: "test"
|
|
359
|
+
}]);
|
|
360
|
+
spinner.stop("LLM credentials validated");
|
|
361
|
+
} catch (error) {
|
|
362
|
+
spinner.stop("LLM validation failed");
|
|
363
|
+
clack.log.error(`Failed to validate LLM credentials: ${error.message}`);
|
|
364
|
+
process.exit(1);
|
|
365
|
+
}
|
|
366
|
+
const channel = await clack.select({
|
|
367
|
+
message: "Choose your messaging channel",
|
|
368
|
+
options: [
|
|
369
|
+
{
|
|
370
|
+
value: "whatsapp",
|
|
371
|
+
label: "WhatsApp (Baileys)"
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
value: "wati",
|
|
375
|
+
label: "WhatsApp via WATI (Business API)"
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
value: "telegram",
|
|
379
|
+
label: "Telegram (grammY)"
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
value: "mock",
|
|
383
|
+
label: "Mock (for testing)"
|
|
384
|
+
}
|
|
385
|
+
],
|
|
386
|
+
initialValue: existingConfig.CHANNEL || void 0
|
|
387
|
+
});
|
|
388
|
+
if (clack.isCancel(channel)) {
|
|
389
|
+
clack.cancel("Setup cancelled");
|
|
390
|
+
process.exit(0);
|
|
391
|
+
}
|
|
392
|
+
const channelConfig = {};
|
|
393
|
+
if (channel === "telegram") {
|
|
394
|
+
const botToken = await clack.password({
|
|
395
|
+
message: "Telegram Bot Token (from @BotFather)",
|
|
396
|
+
validate: (value) => !value ? "Bot token is required" : void 0
|
|
397
|
+
});
|
|
398
|
+
if (clack.isCancel(botToken)) {
|
|
399
|
+
clack.cancel("Setup cancelled");
|
|
400
|
+
process.exit(0);
|
|
401
|
+
}
|
|
402
|
+
channelConfig.TELEGRAM_BOT_TOKEN = botToken;
|
|
403
|
+
}
|
|
404
|
+
if (channel === "wati") {
|
|
405
|
+
const apiToken = await clack.password({
|
|
406
|
+
message: "WATI API Token (from your WATI dashboard)",
|
|
407
|
+
validate: (value) => !value ? "API token is required" : void 0
|
|
408
|
+
});
|
|
409
|
+
if (clack.isCancel(apiToken)) {
|
|
410
|
+
clack.cancel("Setup cancelled");
|
|
411
|
+
process.exit(0);
|
|
412
|
+
}
|
|
413
|
+
const tenantId = await clack.text({
|
|
414
|
+
message: "WATI Tenant ID",
|
|
415
|
+
validate: (value) => !value ? "Tenant ID is required" : void 0,
|
|
416
|
+
initialValue: existingConfig.WATI_TENANT_ID || void 0
|
|
417
|
+
});
|
|
418
|
+
if (clack.isCancel(tenantId)) {
|
|
419
|
+
clack.cancel("Setup cancelled");
|
|
420
|
+
process.exit(0);
|
|
421
|
+
}
|
|
422
|
+
channelConfig.WATI_API_TOKEN = apiToken;
|
|
423
|
+
channelConfig.WATI_TENANT_ID = tenantId;
|
|
424
|
+
}
|
|
425
|
+
const enableSkills = await clack.confirm({
|
|
426
|
+
message: "Configure MCP skills? (Shopify, Stripe, search, etc. via mcp.json)",
|
|
427
|
+
initialValue: existingConfig.SKILLS_ENABLED === "true"
|
|
428
|
+
});
|
|
429
|
+
if (clack.isCancel(enableSkills)) {
|
|
430
|
+
clack.cancel("Setup cancelled");
|
|
431
|
+
process.exit(0);
|
|
432
|
+
}
|
|
433
|
+
let selectedSkills = [];
|
|
434
|
+
if (enableSkills) {
|
|
435
|
+
const skillOptions = loadSkillCatalog().skills.map((s) => ({
|
|
436
|
+
value: s.name,
|
|
437
|
+
label: `${s.displayName} — ${s.description}`
|
|
438
|
+
}));
|
|
439
|
+
const skills = await clack.multiselect({
|
|
440
|
+
message: "Select MCP skills to enable (space to toggle)",
|
|
441
|
+
options: skillOptions,
|
|
442
|
+
required: false
|
|
443
|
+
});
|
|
444
|
+
if (clack.isCancel(skills)) {
|
|
445
|
+
clack.cancel("Setup cancelled");
|
|
446
|
+
process.exit(0);
|
|
447
|
+
}
|
|
448
|
+
selectedSkills = skills;
|
|
449
|
+
}
|
|
450
|
+
const memoryType = await clack.select({
|
|
451
|
+
message: "Choose storage backend",
|
|
452
|
+
options: [{
|
|
453
|
+
value: "sqlite",
|
|
454
|
+
label: "SQLite (recommended, zero config)"
|
|
455
|
+
}, {
|
|
456
|
+
value: "memory",
|
|
457
|
+
label: "In-memory (data lost on restart)"
|
|
458
|
+
}],
|
|
459
|
+
initialValue: existingConfig.MEMORY_TYPE || void 0
|
|
460
|
+
});
|
|
461
|
+
if (clack.isCancel(memoryType)) {
|
|
462
|
+
clack.cancel("Setup cancelled");
|
|
463
|
+
process.exit(0);
|
|
464
|
+
}
|
|
465
|
+
const intentClassifier = await clack.select({
|
|
466
|
+
message: "Choose intent classification method",
|
|
467
|
+
options: [{
|
|
468
|
+
value: "llm",
|
|
469
|
+
label: "LLM-based (uses your LLM provider)"
|
|
470
|
+
}, {
|
|
471
|
+
value: "keyword",
|
|
472
|
+
label: "Keyword matching (no LLM cost)"
|
|
473
|
+
}],
|
|
474
|
+
initialValue: existingConfig.INTENT_CLASSIFIER || void 0
|
|
475
|
+
});
|
|
476
|
+
if (clack.isCancel(intentClassifier)) {
|
|
477
|
+
clack.cancel("Setup cancelled");
|
|
478
|
+
process.exit(0);
|
|
479
|
+
}
|
|
480
|
+
const kbEnabled = await clack.confirm({
|
|
481
|
+
message: "Enable Knowledge Base (RAG)?",
|
|
482
|
+
initialValue: existingConfig.KB_ENABLED === "true"
|
|
483
|
+
});
|
|
484
|
+
if (clack.isCancel(kbEnabled)) {
|
|
485
|
+
clack.cancel("Setup cancelled");
|
|
486
|
+
process.exit(0);
|
|
487
|
+
}
|
|
488
|
+
let kbEmbeddingProvider;
|
|
489
|
+
let kbEmbeddingApiKey;
|
|
490
|
+
let kbEmbeddingModel;
|
|
491
|
+
let kbDbPath;
|
|
492
|
+
if (kbEnabled) {
|
|
493
|
+
const embProvider = await clack.select({
|
|
494
|
+
message: "Embedding provider",
|
|
495
|
+
options: [
|
|
496
|
+
{
|
|
497
|
+
value: "openai",
|
|
498
|
+
label: "OpenAI (text-embedding-3-small, 1536d)"
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
value: "google",
|
|
502
|
+
label: "Google (text-embedding-004, 768d)"
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
value: "mistral",
|
|
506
|
+
label: "Mistral (mistral-embed, 1024d)"
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
value: "cohere",
|
|
510
|
+
label: "Cohere (embed-english-v3.0, 1024d)"
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
value: "ollama",
|
|
514
|
+
label: "Ollama (local, nomic-embed-text, 768d)"
|
|
515
|
+
}
|
|
516
|
+
],
|
|
517
|
+
initialValue: existingConfig.KB_EMBEDDING_PROVIDER || void 0
|
|
518
|
+
});
|
|
519
|
+
if (clack.isCancel(embProvider)) {
|
|
520
|
+
clack.cancel("Setup cancelled");
|
|
521
|
+
process.exit(0);
|
|
522
|
+
}
|
|
523
|
+
kbEmbeddingProvider = embProvider;
|
|
524
|
+
if (kbEmbeddingProvider !== "ollama") {
|
|
525
|
+
const embKey = await clack.password({ message: "Embedding API key (or Enter to reuse LLM key)" });
|
|
526
|
+
if (clack.isCancel(embKey)) {
|
|
527
|
+
clack.cancel("Setup cancelled");
|
|
528
|
+
process.exit(0);
|
|
529
|
+
}
|
|
530
|
+
kbEmbeddingApiKey = embKey || llmApiKey;
|
|
531
|
+
}
|
|
532
|
+
const embModel = await clack.text({
|
|
533
|
+
message: "Embedding model (optional, press Enter for default)",
|
|
534
|
+
placeholder: getDefaultEmbeddingModel(kbEmbeddingProvider),
|
|
535
|
+
initialValue: existingConfig.KB_EMBEDDING_MODEL || void 0
|
|
536
|
+
});
|
|
537
|
+
if (clack.isCancel(embModel)) {
|
|
538
|
+
clack.cancel("Setup cancelled");
|
|
539
|
+
process.exit(0);
|
|
540
|
+
}
|
|
541
|
+
kbEmbeddingModel = embModel || void 0;
|
|
542
|
+
const dbPath = await clack.text({
|
|
543
|
+
message: "Knowledge base DB path",
|
|
544
|
+
placeholder: "./knowledge.db",
|
|
545
|
+
initialValue: existingConfig.KB_DB_PATH || void 0
|
|
546
|
+
});
|
|
547
|
+
if (clack.isCancel(dbPath)) {
|
|
548
|
+
clack.cancel("Setup cancelled");
|
|
549
|
+
process.exit(0);
|
|
550
|
+
}
|
|
551
|
+
kbDbPath = dbPath || "./knowledge.db";
|
|
552
|
+
}
|
|
553
|
+
const trainingEnabled = await clack.confirm({
|
|
554
|
+
message: "Enable training mode? (allows /teach command to add FAQ answers)",
|
|
555
|
+
initialValue: existingConfig.TRAINING_MODE_ENABLED === "true"
|
|
556
|
+
});
|
|
557
|
+
if (clack.isCancel(trainingEnabled)) {
|
|
558
|
+
clack.cancel("Setup cancelled");
|
|
559
|
+
process.exit(0);
|
|
560
|
+
}
|
|
561
|
+
let trainingWhitelist = "";
|
|
562
|
+
if (trainingEnabled) {
|
|
563
|
+
const whitelistInput = await clack.text({
|
|
564
|
+
message: "Whitelist phone numbers for training (comma-separated, e.g. 85253332683,85291234567)",
|
|
565
|
+
placeholder: "85253332683,85291234567",
|
|
566
|
+
initialValue: existingConfig.TRAINING_MODE_WHITELIST || void 0
|
|
567
|
+
});
|
|
568
|
+
if (clack.isCancel(whitelistInput)) {
|
|
569
|
+
clack.cancel("Setup cancelled");
|
|
570
|
+
process.exit(0);
|
|
571
|
+
}
|
|
572
|
+
trainingWhitelist = whitelistInput || "";
|
|
573
|
+
}
|
|
574
|
+
let copilotConfig = {};
|
|
575
|
+
if (kbEnabled) {
|
|
576
|
+
const copilotEnabled = await clack.confirm({
|
|
577
|
+
message: "Enable Training Copilot? (auto-tracks unanswered queries, suggests FAQ additions)",
|
|
578
|
+
initialValue: existingConfig.COPILOT_ENABLED !== "false"
|
|
579
|
+
});
|
|
580
|
+
if (clack.isCancel(copilotEnabled)) {
|
|
581
|
+
clack.cancel("Setup cancelled");
|
|
582
|
+
process.exit(0);
|
|
583
|
+
}
|
|
584
|
+
if (copilotEnabled) {
|
|
585
|
+
copilotConfig.COPILOT_ENABLED = "true";
|
|
586
|
+
const copilotDbPath = await clack.text({
|
|
587
|
+
message: "Copilot database path",
|
|
588
|
+
placeholder: "./copilot.db",
|
|
589
|
+
initialValue: existingConfig.COPILOT_DB_PATH || void 0
|
|
590
|
+
});
|
|
591
|
+
if (clack.isCancel(copilotDbPath)) {
|
|
592
|
+
clack.cancel("Setup cancelled");
|
|
593
|
+
process.exit(0);
|
|
594
|
+
}
|
|
595
|
+
copilotConfig.COPILOT_DB_PATH = copilotDbPath || "./copilot.db";
|
|
596
|
+
const trackingThreshold = await clack.text({
|
|
597
|
+
message: "Tracking threshold (KB score below which queries are tracked)",
|
|
598
|
+
placeholder: "0.70",
|
|
599
|
+
initialValue: existingConfig.COPILOT_TRACKING_THRESHOLD || void 0
|
|
600
|
+
});
|
|
601
|
+
if (clack.isCancel(trackingThreshold)) {
|
|
602
|
+
clack.cancel("Setup cancelled");
|
|
603
|
+
process.exit(0);
|
|
604
|
+
}
|
|
605
|
+
if (trackingThreshold) copilotConfig.COPILOT_TRACKING_THRESHOLD = trackingThreshold;
|
|
606
|
+
const autoSuggest = await clack.confirm({
|
|
607
|
+
message: "Auto-generate LLM answer suggestions for unanswered queries?",
|
|
608
|
+
initialValue: existingConfig.COPILOT_AUTO_SUGGEST !== "false"
|
|
609
|
+
});
|
|
610
|
+
if (clack.isCancel(autoSuggest)) {
|
|
611
|
+
clack.cancel("Setup cancelled");
|
|
612
|
+
process.exit(0);
|
|
613
|
+
}
|
|
614
|
+
copilotConfig.COPILOT_AUTO_SUGGEST = autoSuggest ? "true" : "false";
|
|
615
|
+
} else copilotConfig.COPILOT_ENABLED = "false";
|
|
616
|
+
}
|
|
617
|
+
let analyticsConfig = {};
|
|
618
|
+
const analyticsEnabled = await clack.confirm({
|
|
619
|
+
message: "Enable Analytics & Reporting? (tracks response times, token usage, conversation metrics)",
|
|
620
|
+
initialValue: existingConfig.ANALYTICS_ENABLED !== "false"
|
|
621
|
+
});
|
|
622
|
+
if (clack.isCancel(analyticsEnabled)) {
|
|
623
|
+
clack.cancel("Setup cancelled");
|
|
624
|
+
process.exit(0);
|
|
625
|
+
}
|
|
626
|
+
if (analyticsEnabled) {
|
|
627
|
+
analyticsConfig.ANALYTICS_ENABLED = "true";
|
|
628
|
+
const analyticsDbPath = await clack.text({
|
|
629
|
+
message: "Analytics database path",
|
|
630
|
+
placeholder: "./analytics.db",
|
|
631
|
+
initialValue: existingConfig.ANALYTICS_DB_PATH || void 0
|
|
632
|
+
});
|
|
633
|
+
if (clack.isCancel(analyticsDbPath)) {
|
|
634
|
+
clack.cancel("Setup cancelled");
|
|
635
|
+
process.exit(0);
|
|
636
|
+
}
|
|
637
|
+
analyticsConfig.ANALYTICS_DB_PATH = analyticsDbPath || "./analytics.db";
|
|
638
|
+
const digestEnabled = await clack.select({
|
|
639
|
+
message: "Enable daily digest reports via WhatsApp?",
|
|
640
|
+
options: [
|
|
641
|
+
{
|
|
642
|
+
value: "daily",
|
|
643
|
+
label: "Yes, daily digest"
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
value: "weekly",
|
|
647
|
+
label: "Weekly only"
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
value: "both",
|
|
651
|
+
label: "Both daily and weekly"
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
value: "false",
|
|
655
|
+
label: "No digest reports"
|
|
656
|
+
}
|
|
657
|
+
],
|
|
658
|
+
initialValue: existingConfig.ANALYTICS_DIGEST_ENABLED === "true" ? existingConfig.ANALYTICS_DIGEST_SCHEDULE || "daily" : "false"
|
|
659
|
+
});
|
|
660
|
+
if (clack.isCancel(digestEnabled)) {
|
|
661
|
+
clack.cancel("Setup cancelled");
|
|
662
|
+
process.exit(0);
|
|
663
|
+
}
|
|
664
|
+
if (digestEnabled !== "false") {
|
|
665
|
+
analyticsConfig.ANALYTICS_DIGEST_ENABLED = "true";
|
|
666
|
+
analyticsConfig.ANALYTICS_DIGEST_SCHEDULE = digestEnabled;
|
|
667
|
+
const digestTime = await clack.text({
|
|
668
|
+
message: "Digest delivery time (HH:MM, 24h format)",
|
|
669
|
+
placeholder: "09:00",
|
|
670
|
+
initialValue: existingConfig.ANALYTICS_DIGEST_TIME || void 0,
|
|
671
|
+
validate: (value) => {
|
|
672
|
+
if (!value) return void 0;
|
|
673
|
+
if (!/^\d{2}:\d{2}$/.test(value)) return "Use HH:MM format (e.g. 09:00)";
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
if (clack.isCancel(digestTime)) {
|
|
677
|
+
clack.cancel("Setup cancelled");
|
|
678
|
+
process.exit(0);
|
|
679
|
+
}
|
|
680
|
+
analyticsConfig.ANALYTICS_DIGEST_TIME = digestTime || "09:00";
|
|
681
|
+
} else analyticsConfig.ANALYTICS_DIGEST_ENABLED = "false";
|
|
682
|
+
} else analyticsConfig.ANALYTICS_ENABLED = "false";
|
|
683
|
+
const newConfig = {
|
|
684
|
+
LLM_PROVIDER: llmProvider,
|
|
685
|
+
LLM_API_KEY: llmApiKey,
|
|
686
|
+
LLM_MODEL: llmModel || void 0,
|
|
687
|
+
CHANNEL: channel,
|
|
688
|
+
SKILLS_ENABLED: enableSkills ? "true" : "false",
|
|
689
|
+
MEMORY_TYPE: memoryType,
|
|
690
|
+
INTENT_CLASSIFIER: intentClassifier,
|
|
691
|
+
...channelConfig,
|
|
692
|
+
...kbEnabled && {
|
|
693
|
+
KB_ENABLED: "true",
|
|
694
|
+
KB_DB_PATH: kbDbPath,
|
|
695
|
+
KB_EMBEDDING_PROVIDER: kbEmbeddingProvider,
|
|
696
|
+
KB_EMBEDDING_MODEL: kbEmbeddingModel,
|
|
697
|
+
KB_EMBEDDING_API_KEY: kbEmbeddingApiKey
|
|
698
|
+
},
|
|
699
|
+
TRAINING_MODE_ENABLED: trainingEnabled ? "true" : "false",
|
|
700
|
+
...trainingWhitelist && { TRAINING_MODE_WHITELIST: trainingWhitelist },
|
|
701
|
+
...copilotConfig,
|
|
702
|
+
...analyticsConfig
|
|
703
|
+
};
|
|
704
|
+
if (enableSkills && selectedSkills.length > 0) {
|
|
705
|
+
const catalog = loadSkillCatalog();
|
|
706
|
+
const mcpSkills = [];
|
|
707
|
+
const mcpPath = resolve(process.cwd(), "mcp.json");
|
|
708
|
+
let previousSkillNames = [];
|
|
709
|
+
try {
|
|
710
|
+
previousSkillNames = (JSON.parse(readFileSync(mcpPath, "utf-8")).skills || []).map((s) => s.name);
|
|
711
|
+
} catch {}
|
|
712
|
+
for (const skillName of selectedSkills) {
|
|
713
|
+
const entry = findSkillInCatalog(catalog, skillName);
|
|
714
|
+
if (!entry) continue;
|
|
715
|
+
for (const [varName, spec] of Object.entries(entry.envVars)) {
|
|
716
|
+
if (!spec.required) continue;
|
|
717
|
+
const value = await clack.password({
|
|
718
|
+
message: `${entry.displayName}: ${spec.description}`,
|
|
719
|
+
validate: (v) => !v ? `${varName} is required` : void 0
|
|
720
|
+
});
|
|
721
|
+
if (clack.isCancel(value)) {
|
|
722
|
+
clack.cancel("Setup cancelled");
|
|
723
|
+
process.exit(0);
|
|
724
|
+
}
|
|
725
|
+
newConfig[varName] = value;
|
|
726
|
+
}
|
|
727
|
+
mcpSkills.push(catalogEntryToConfig(entry));
|
|
728
|
+
}
|
|
729
|
+
const mcpConfig = { skills: mcpSkills };
|
|
730
|
+
writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + "\n");
|
|
731
|
+
clack.log.info(" MCP config saved to mcp.json");
|
|
732
|
+
const addedSkills = selectedSkills.filter((s) => !previousSkillNames.includes(s));
|
|
733
|
+
const removedSkills = previousSkillNames.filter((s) => !selectedSkills.includes(s));
|
|
734
|
+
const updatedAgents = syncSkillsToAgents(resolve(process.cwd(), "agents"), addedSkills, removedSkills);
|
|
735
|
+
if (updatedAgents.length > 0) clack.log.info(` Updated skills in: ${updatedAgents.join(", ")}`);
|
|
736
|
+
}
|
|
737
|
+
writeConfig({
|
|
738
|
+
...existingConfig,
|
|
739
|
+
...newConfig
|
|
740
|
+
});
|
|
741
|
+
clack.outro("Config saved to .env. Starting Operor...");
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Sync skill names into agent INSTRUCTIONS.md frontmatter.
|
|
745
|
+
* Only touches agents that already have a `skills:` field.
|
|
746
|
+
* If the resulting skills array is empty, removes the `skills` key entirely.
|
|
747
|
+
*/
|
|
748
|
+
function syncSkillsToAgents(agentsDir, addedSkills, removedSkills) {
|
|
749
|
+
if (addedSkills.length === 0 && removedSkills.length === 0) return [];
|
|
750
|
+
const updated = [];
|
|
751
|
+
let entries;
|
|
752
|
+
try {
|
|
753
|
+
entries = readdirSync(agentsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("_")).map((d) => d.name);
|
|
754
|
+
} catch {
|
|
755
|
+
return [];
|
|
756
|
+
}
|
|
757
|
+
for (const agentName of entries) {
|
|
758
|
+
const instrPath = join(agentsDir, agentName, "INSTRUCTIONS.md");
|
|
759
|
+
if (!existsSync(instrPath)) continue;
|
|
760
|
+
const parsed = matter(readFileSync(instrPath, "utf-8"));
|
|
761
|
+
if (!Array.isArray(parsed.data.skills)) continue;
|
|
762
|
+
const currentSkills = parsed.data.skills;
|
|
763
|
+
let newSkills = currentSkills.filter((s) => !removedSkills.includes(s));
|
|
764
|
+
for (const skill of addedSkills) if (!newSkills.includes(skill)) newSkills.push(skill);
|
|
765
|
+
if (newSkills.length === currentSkills.length && newSkills.every((s, i) => s === currentSkills[i])) continue;
|
|
766
|
+
if (newSkills.length === 0) delete parsed.data.skills;
|
|
767
|
+
else parsed.data.skills = newSkills;
|
|
768
|
+
writeFileSync(instrPath, matter.stringify(parsed.content, parsed.data));
|
|
769
|
+
updated.push(agentName);
|
|
770
|
+
}
|
|
771
|
+
return updated;
|
|
772
|
+
}
|
|
773
|
+
function getDefaultModel(provider) {
|
|
774
|
+
return {
|
|
775
|
+
openai: "gpt-5-mini",
|
|
776
|
+
anthropic: "claude-3-5-sonnet-20241022",
|
|
777
|
+
google: "gemini-2.0-flash-exp",
|
|
778
|
+
groq: "llama-3.3-70b-versatile",
|
|
779
|
+
ollama: "llama3.2"
|
|
780
|
+
}[provider] || "";
|
|
781
|
+
}
|
|
782
|
+
function getDefaultEmbeddingModel(provider) {
|
|
783
|
+
return {
|
|
784
|
+
openai: "text-embedding-3-small",
|
|
785
|
+
google: "text-embedding-004",
|
|
786
|
+
mistral: "mistral-embed",
|
|
787
|
+
cohere: "embed-english-v3.0",
|
|
788
|
+
ollama: "nomic-embed-text"
|
|
789
|
+
}[provider] || "";
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
//#endregion
|
|
793
|
+
//#region src/start.ts
|
|
794
|
+
async function startOperor() {
|
|
795
|
+
const config = readConfig();
|
|
796
|
+
if (!config.LLM_PROVIDER || !config.CHANNEL) {
|
|
797
|
+
console.error("Missing configuration. Run \"operor setup\" first.");
|
|
798
|
+
process.exit(1);
|
|
799
|
+
}
|
|
800
|
+
console.log("[Operor] Starting...");
|
|
801
|
+
const { Operor, LLMIntentClassifier, AgentLoader } = await import("@operor/core");
|
|
802
|
+
const { AIProvider } = await import("@operor/llm");
|
|
803
|
+
let memory;
|
|
804
|
+
if (config.MEMORY_TYPE === "sqlite") {
|
|
805
|
+
const { SQLiteMemory } = await import("@operor/memory");
|
|
806
|
+
memory = new SQLiteMemory(config.MEMORY_DB_PATH || "./operor.db");
|
|
807
|
+
console.log("[Operor] Using SQLite memory store");
|
|
808
|
+
}
|
|
809
|
+
let llmProvider = config.LLM_PROVIDER;
|
|
810
|
+
let llmModel = config.LLM_MODEL;
|
|
811
|
+
let llmApiKey = config.LLM_API_KEY;
|
|
812
|
+
if (memory?.getSetting) try {
|
|
813
|
+
const storedProvider = await memory.getSetting("llm_provider");
|
|
814
|
+
const storedModel = await memory.getSetting("llm_model");
|
|
815
|
+
if (storedProvider) llmProvider = storedProvider;
|
|
816
|
+
if (storedModel) llmModel = storedModel;
|
|
817
|
+
const storedKey = await memory.getSetting(`llm_apikey_${llmProvider}`);
|
|
818
|
+
if (storedKey) llmApiKey = storedKey;
|
|
819
|
+
} catch {}
|
|
820
|
+
const llm = new AIProvider({
|
|
821
|
+
provider: llmProvider,
|
|
822
|
+
apiKey: llmApiKey,
|
|
823
|
+
model: llmModel
|
|
824
|
+
});
|
|
825
|
+
if (memory?.getSetting) for (const p of [
|
|
826
|
+
"openai",
|
|
827
|
+
"anthropic",
|
|
828
|
+
"google",
|
|
829
|
+
"groq",
|
|
830
|
+
"ollama"
|
|
831
|
+
]) {
|
|
832
|
+
if (p === llmProvider) continue;
|
|
833
|
+
try {
|
|
834
|
+
const k = await memory.getSetting(`llm_apikey_${p}`);
|
|
835
|
+
if (k) llm.setApiKey(p, k);
|
|
836
|
+
} catch {}
|
|
837
|
+
}
|
|
838
|
+
let intentClassifier;
|
|
839
|
+
if (config.INTENT_CLASSIFIER === "llm") {
|
|
840
|
+
intentClassifier = new LLMIntentClassifier(llm);
|
|
841
|
+
console.log("[Operor] Using LLM intent classification");
|
|
842
|
+
}
|
|
843
|
+
let kbRuntime;
|
|
844
|
+
let embedder;
|
|
845
|
+
if (config.KB_ENABLED === "true") try {
|
|
846
|
+
const { SQLiteKnowledgeStore, EmbeddingService, TextChunker, IngestionPipeline, RetrievalPipeline, QueryRewriter, UrlIngestor, SiteCrawler, FileIngestor } = await import("@operor/knowledge");
|
|
847
|
+
embedder = new EmbeddingService({
|
|
848
|
+
provider: config.KB_EMBEDDING_PROVIDER || "openai",
|
|
849
|
+
apiKey: config.KB_EMBEDDING_API_KEY || config.LLM_API_KEY,
|
|
850
|
+
model: config.KB_EMBEDDING_MODEL
|
|
851
|
+
});
|
|
852
|
+
const kbStore = new SQLiteKnowledgeStore(config.KB_DB_PATH || "./knowledge.db", embedder.dimensions);
|
|
853
|
+
await kbStore.initialize();
|
|
854
|
+
const chunker = new TextChunker({
|
|
855
|
+
chunkSize: config.KB_CHUNK_SIZE ? parseInt(config.KB_CHUNK_SIZE) : void 0,
|
|
856
|
+
chunkOverlap: config.KB_CHUNK_OVERLAP ? parseInt(config.KB_CHUNK_OVERLAP) : void 0
|
|
857
|
+
});
|
|
858
|
+
const ingestion = new IngestionPipeline(kbStore, embedder, chunker);
|
|
859
|
+
let queryRewriter;
|
|
860
|
+
try {
|
|
861
|
+
queryRewriter = new QueryRewriter({ model: llm.getModel() });
|
|
862
|
+
} catch {}
|
|
863
|
+
const retrieval = new RetrievalPipeline(kbStore, embedder, {
|
|
864
|
+
faqThreshold: .85,
|
|
865
|
+
queryRewriter,
|
|
866
|
+
fusionStrategy: config.FUSION_STRATEGY || "rrf"
|
|
867
|
+
});
|
|
868
|
+
const crawl4aiUrl = config.CRAWL4AI_URL || void 0;
|
|
869
|
+
const urlIngestor = new UrlIngestor(ingestion, { crawl4aiUrl });
|
|
870
|
+
const siteCrawler = new SiteCrawler(ingestion, { crawl4aiUrl });
|
|
871
|
+
const fileIngestor = new FileIngestor(ingestion);
|
|
872
|
+
kbRuntime = {
|
|
873
|
+
ingestFaq: async (question, answer, metadata) => {
|
|
874
|
+
const result = await ingestion.ingestFaq(question, answer, metadata);
|
|
875
|
+
return {
|
|
876
|
+
id: result.id,
|
|
877
|
+
existingMatch: result.existingMatch
|
|
878
|
+
};
|
|
879
|
+
},
|
|
880
|
+
listDocuments: async () => {
|
|
881
|
+
return await kbStore.listDocuments();
|
|
882
|
+
},
|
|
883
|
+
deleteDocument: async (id) => {
|
|
884
|
+
return await kbStore.deleteDocument(id);
|
|
885
|
+
},
|
|
886
|
+
retrieve: async (query) => {
|
|
887
|
+
return await retrieval.retrieve(query);
|
|
888
|
+
},
|
|
889
|
+
getStats: async () => {
|
|
890
|
+
return await kbStore.getStats();
|
|
891
|
+
},
|
|
892
|
+
ingestUrl: async (url) => {
|
|
893
|
+
const doc = await urlIngestor.ingestUrl(url);
|
|
894
|
+
const faqCount = doc.metadata?.faqCount;
|
|
895
|
+
const chunks = faqCount ?? (kbStore.getChunkCount ? kbStore.getChunkCount(doc.id) : 0);
|
|
896
|
+
return {
|
|
897
|
+
id: doc.id,
|
|
898
|
+
title: doc.title,
|
|
899
|
+
chunks,
|
|
900
|
+
faqCount
|
|
901
|
+
};
|
|
902
|
+
},
|
|
903
|
+
ingestSite: async (url, options) => {
|
|
904
|
+
const docs = await siteCrawler.crawlSite(url, {
|
|
905
|
+
...options,
|
|
906
|
+
onProgress: options?.onProgress
|
|
907
|
+
});
|
|
908
|
+
const totalChunks = docs.reduce((sum, doc) => {
|
|
909
|
+
return sum + (kbStore.getChunkCount ? kbStore.getChunkCount(doc.id) : 0);
|
|
910
|
+
}, 0);
|
|
911
|
+
return {
|
|
912
|
+
documents: docs.length,
|
|
913
|
+
chunks: totalChunks
|
|
914
|
+
};
|
|
915
|
+
},
|
|
916
|
+
ingestFile: async (buffer, fileName) => {
|
|
917
|
+
const { writeFile, unlink } = await import("fs/promises");
|
|
918
|
+
const { join } = await import("path");
|
|
919
|
+
const { tmpdir } = await import("os");
|
|
920
|
+
const tempPath = join(tmpdir(), `operor-${Date.now()}-${fileName}`);
|
|
921
|
+
await writeFile(tempPath, buffer);
|
|
922
|
+
try {
|
|
923
|
+
const doc = await fileIngestor.ingestFile(tempPath, fileName);
|
|
924
|
+
const chunkCount = kbStore.getChunkCount ? kbStore.getChunkCount(doc.id) : 0;
|
|
925
|
+
return {
|
|
926
|
+
id: doc.id,
|
|
927
|
+
title: doc.title,
|
|
928
|
+
chunks: chunkCount
|
|
929
|
+
};
|
|
930
|
+
} finally {
|
|
931
|
+
await unlink(tempPath).catch(() => {});
|
|
932
|
+
}
|
|
933
|
+
},
|
|
934
|
+
rebuild: async () => {
|
|
935
|
+
return await ingestion.rebuild();
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
console.log("[Operor] Knowledge Base enabled");
|
|
939
|
+
} catch (error) {
|
|
940
|
+
console.error(`[Operor] Failed to initialize Knowledge Base: ${error.message}`);
|
|
941
|
+
}
|
|
942
|
+
let copilotHandler;
|
|
943
|
+
let copilotTracker;
|
|
944
|
+
if (config.COPILOT_ENABLED !== "false" && kbRuntime) try {
|
|
945
|
+
const { SQLiteCopilotStore, UnansweredQueryTracker, QueryClusterer, SuggestionEngine, CopilotCommandHandler, DigestScheduler, DEFAULT_COPILOT_CONFIG } = await import("@operor/copilot");
|
|
946
|
+
const copilotConfig = {
|
|
947
|
+
...DEFAULT_COPILOT_CONFIG,
|
|
948
|
+
enabled: true,
|
|
949
|
+
trackingThreshold: config.COPILOT_TRACKING_THRESHOLD ? parseFloat(config.COPILOT_TRACKING_THRESHOLD) : DEFAULT_COPILOT_CONFIG.trackingThreshold,
|
|
950
|
+
clusterThreshold: config.COPILOT_CLUSTER_THRESHOLD ? parseFloat(config.COPILOT_CLUSTER_THRESHOLD) : DEFAULT_COPILOT_CONFIG.clusterThreshold,
|
|
951
|
+
digestIntervalMs: config.COPILOT_DIGEST_INTERVAL ? parseInt(config.COPILOT_DIGEST_INTERVAL) : DEFAULT_COPILOT_CONFIG.digestIntervalMs,
|
|
952
|
+
digestMaxItems: config.COPILOT_DIGEST_MAX_ITEMS ? parseInt(config.COPILOT_DIGEST_MAX_ITEMS) : DEFAULT_COPILOT_CONFIG.digestMaxItems,
|
|
953
|
+
autoSuggest: config.COPILOT_AUTO_SUGGEST !== "false"
|
|
954
|
+
};
|
|
955
|
+
const copilotStore = new SQLiteCopilotStore(config.COPILOT_DB_PATH || "./copilot.db", embedder.dimensions);
|
|
956
|
+
await copilotStore.initialize();
|
|
957
|
+
const clusterer = new QueryClusterer(copilotStore, embedder, { clusterThreshold: copilotConfig.clusterThreshold });
|
|
958
|
+
copilotTracker = new UnansweredQueryTracker(copilotStore, copilotConfig, embedder, clusterer);
|
|
959
|
+
copilotHandler = new CopilotCommandHandler(copilotStore, copilotConfig.autoSuggest ? new SuggestionEngine({ generateText: (opts) => llm.complete([...opts.system ? [{
|
|
960
|
+
role: "system",
|
|
961
|
+
content: opts.system
|
|
962
|
+
}] : [], {
|
|
963
|
+
role: "user",
|
|
964
|
+
content: opts.prompt
|
|
965
|
+
}]) }, kbRuntime) : void 0, kbRuntime, clusterer);
|
|
966
|
+
const adminPhones = config.TRAINING_MODE_WHITELIST?.split(",").map((p) => p.trim()).filter(Boolean) || [];
|
|
967
|
+
if (adminPhones.length > 0) new DigestScheduler(copilotStore, copilotConfig, async (phone, text) => {
|
|
968
|
+
try {
|
|
969
|
+
if (provider && typeof provider.sendMessage === "function") await provider.sendMessage(phone, text);
|
|
970
|
+
} catch (err) {
|
|
971
|
+
console.warn("[Copilot] Failed to send digest:", err?.message);
|
|
972
|
+
}
|
|
973
|
+
}).start(adminPhones);
|
|
974
|
+
console.log("[Operor] Training Copilot enabled");
|
|
975
|
+
} catch (error) {
|
|
976
|
+
console.error(`[Operor] Failed to initialize Training Copilot: ${error.message}`);
|
|
977
|
+
console.error("[Operor] /review commands will not be available. Try running \"pnpm install\" if packages are missing.");
|
|
978
|
+
}
|
|
979
|
+
let analyticsCollector;
|
|
980
|
+
let analyticsStore;
|
|
981
|
+
if (config.ANALYTICS_ENABLED !== "false") try {
|
|
982
|
+
const { SQLiteAnalyticsStore, AnalyticsCollector } = await import("@operor/analytics");
|
|
983
|
+
analyticsStore = new SQLiteAnalyticsStore(config.ANALYTICS_DB_PATH || "./analytics.db");
|
|
984
|
+
await analyticsStore.initialize();
|
|
985
|
+
analyticsCollector = new AnalyticsCollector(analyticsStore, { debug: true });
|
|
986
|
+
console.log("[Operor] Analytics enabled");
|
|
987
|
+
} catch (error) {
|
|
988
|
+
console.error(`[Operor] Failed to initialize Analytics: ${error.message}`);
|
|
989
|
+
}
|
|
990
|
+
let trainingWhitelist = config.TRAINING_MODE_WHITELIST?.split(",").map((p) => p.trim()).filter(Boolean) || [];
|
|
991
|
+
if (config.TRAINING_MODE_ENABLED === "true" && memory?.getSetting) try {
|
|
992
|
+
const stored = await memory.getSetting("training_whitelist");
|
|
993
|
+
if (stored) trainingWhitelist = stored.split(",").map((p) => p.trim()).filter(Boolean);
|
|
994
|
+
else if (trainingWhitelist.length > 0) await memory.setSetting("training_whitelist", trainingWhitelist.join(","));
|
|
995
|
+
} catch {}
|
|
996
|
+
let skillsModule;
|
|
997
|
+
try {
|
|
998
|
+
skillsModule = await import("@operor/skills");
|
|
999
|
+
} catch {}
|
|
1000
|
+
const agentsDir = `${process.cwd()}/agents`;
|
|
1001
|
+
const hasAgentsDir = fs.existsSync(agentsDir);
|
|
1002
|
+
const os = new Operor({
|
|
1003
|
+
debug: true,
|
|
1004
|
+
llmProvider: llm,
|
|
1005
|
+
...memory && { memory },
|
|
1006
|
+
...intentClassifier && { intentClassifier },
|
|
1007
|
+
...kbRuntime && { kb: kbRuntime },
|
|
1008
|
+
...copilotHandler && { copilotHandler },
|
|
1009
|
+
...copilotTracker && { copilotTracker },
|
|
1010
|
+
...analyticsCollector && { analyticsCollector },
|
|
1011
|
+
...analyticsStore && { analyticsStore },
|
|
1012
|
+
...skillsModule && { skillsModule },
|
|
1013
|
+
...hasAgentsDir && { agentsDir },
|
|
1014
|
+
...config.TRAINING_MODE_ENABLED === "true" && { trainingMode: {
|
|
1015
|
+
enabled: true,
|
|
1016
|
+
whitelist: trainingWhitelist
|
|
1017
|
+
} }
|
|
1018
|
+
});
|
|
1019
|
+
let provider;
|
|
1020
|
+
if (config.CHANNEL === "whatsapp") {
|
|
1021
|
+
const { BaileysProvider } = await import("@operor/provider-baileys");
|
|
1022
|
+
provider = new BaileysProvider({ groupMode: config.WHATSAPP_GROUP_MODE || "mention-only" });
|
|
1023
|
+
} else if (config.CHANNEL === "telegram") {
|
|
1024
|
+
const { TelegramProvider } = await import("@operor/provider-telegram");
|
|
1025
|
+
provider = new TelegramProvider({ botToken: config.TELEGRAM_BOT_TOKEN });
|
|
1026
|
+
} else if (config.CHANNEL === "wati") {
|
|
1027
|
+
const { WatiProvider } = await import("@operor/provider-wati");
|
|
1028
|
+
provider = new WatiProvider({
|
|
1029
|
+
apiToken: config.WATI_API_TOKEN,
|
|
1030
|
+
tenantId: config.WATI_TENANT_ID,
|
|
1031
|
+
webhookPort: config.WATI_WEBHOOK_PORT ? parseInt(config.WATI_WEBHOOK_PORT) : void 0
|
|
1032
|
+
});
|
|
1033
|
+
} else {
|
|
1034
|
+
const { MockProvider } = await import("@operor/provider-mock");
|
|
1035
|
+
provider = new MockProvider();
|
|
1036
|
+
}
|
|
1037
|
+
os.addProvider(provider);
|
|
1038
|
+
let skillManager;
|
|
1039
|
+
const skillInstances = [];
|
|
1040
|
+
let loadSkillsConfig;
|
|
1041
|
+
try {
|
|
1042
|
+
const skillsMod = await import("@operor/skills");
|
|
1043
|
+
loadSkillsConfig = skillsMod.loadSkillsConfig;
|
|
1044
|
+
const { SkillManager } = skillsMod;
|
|
1045
|
+
const skillsConfig = loadSkillsConfig();
|
|
1046
|
+
skillManager = new SkillManager();
|
|
1047
|
+
const mcpSkills = await skillManager.initialize(skillsConfig);
|
|
1048
|
+
for (const skill of mcpSkills) {
|
|
1049
|
+
os.addSkill(skill);
|
|
1050
|
+
skillInstances.push(skill);
|
|
1051
|
+
}
|
|
1052
|
+
if (mcpSkills.length > 0) console.log(`[Operor] ${mcpSkills.length} MCP skill(s) loaded`);
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
if (config.SKILLS_ENABLED === "true") console.warn(`[Operor] Failed to load MCP skills: ${error.message}`);
|
|
1055
|
+
}
|
|
1056
|
+
const allTools = skillInstances.flatMap((skill) => Object.values(skill.tools));
|
|
1057
|
+
const getPromptSkills = (agentSkillNames) => {
|
|
1058
|
+
return skillInstances.filter((s) => typeof s.getContent === "function").filter((s) => !agentSkillNames || agentSkillNames.includes(s.name)).map((s) => ({
|
|
1059
|
+
name: s.name,
|
|
1060
|
+
content: s.getContent()
|
|
1061
|
+
}));
|
|
1062
|
+
};
|
|
1063
|
+
const agentToolsMap = /* @__PURE__ */ new Map();
|
|
1064
|
+
let definitions = [];
|
|
1065
|
+
if (hasAgentsDir) {
|
|
1066
|
+
definitions = await new AgentLoader(process.cwd()).loadAll();
|
|
1067
|
+
if (definitions.length === 0) {
|
|
1068
|
+
console.error("[Operor] agents/ directory found but no valid agent definitions. Check INSTRUCTIONS.md files.");
|
|
1069
|
+
process.exit(1);
|
|
1070
|
+
}
|
|
1071
|
+
for (const def of definitions) {
|
|
1072
|
+
const agentTools = def.config.skills ? skillInstances.filter((skill) => def.config.skills.includes(skill.name)).flatMap((skill) => Object.values(skill.tools)) : allTools;
|
|
1073
|
+
agentToolsMap.set(def.config.name, agentTools);
|
|
1074
|
+
applyLLMOverride(os.createAgent({
|
|
1075
|
+
...def.config,
|
|
1076
|
+
tools: agentTools
|
|
1077
|
+
}), llm, agentTools, {
|
|
1078
|
+
systemPrompt: def.systemPrompt,
|
|
1079
|
+
kbRuntime,
|
|
1080
|
+
useKB: def.config.knowledgeBase,
|
|
1081
|
+
guardrails: def.config.guardrails,
|
|
1082
|
+
promptSkills: getPromptSkills(def.config.skills)
|
|
1083
|
+
});
|
|
1084
|
+
console.log(`[Operor] Loaded agent: ${def.config.name}` + (def.config.channels ? ` (channels: ${def.config.channels.join(", ")})` : "") + (def.config.skills ? ` (skills: ${def.config.skills.join(", ")})` : "") + (def.config.knowledgeBase ? " (KB)" : "") + (def.config.priority ? ` (priority: ${def.config.priority})` : ""));
|
|
1085
|
+
}
|
|
1086
|
+
console.log(`[Operor] Loaded ${definitions.length} agent(s) from agents/ directory`);
|
|
1087
|
+
} else {
|
|
1088
|
+
const agent = os.createAgent({
|
|
1089
|
+
name: "support",
|
|
1090
|
+
purpose: "Customer support agent",
|
|
1091
|
+
personality: "Friendly, helpful, and professional",
|
|
1092
|
+
triggers: ["*"],
|
|
1093
|
+
tools: allTools
|
|
1094
|
+
});
|
|
1095
|
+
agentToolsMap.set("support", allTools);
|
|
1096
|
+
applyLLMOverride(agent, llm, allTools, {
|
|
1097
|
+
kbRuntime,
|
|
1098
|
+
promptSkills: getPromptSkills()
|
|
1099
|
+
});
|
|
1100
|
+
console.log("[Operor] Using default single-agent mode");
|
|
1101
|
+
}
|
|
1102
|
+
const reloadSkills = async () => {
|
|
1103
|
+
if (!skillManager || !loadSkillsConfig) return;
|
|
1104
|
+
console.log("[Operor] 🔄 Reloading MCP skills...");
|
|
1105
|
+
try {
|
|
1106
|
+
const newConfig = loadSkillsConfig();
|
|
1107
|
+
for (const skill of skillInstances) await os.removeSkill(skill.name);
|
|
1108
|
+
await skillManager.closeAll();
|
|
1109
|
+
const newSkills = await skillManager.initialize(newConfig);
|
|
1110
|
+
skillInstances.splice(0, skillInstances.length, ...newSkills);
|
|
1111
|
+
for (const skill of newSkills) await os.addSkill(skill);
|
|
1112
|
+
const freshTools = skillInstances.flatMap((skill) => Object.values(skill.tools));
|
|
1113
|
+
allTools.splice(0, allTools.length, ...freshTools);
|
|
1114
|
+
for (const def of definitions) {
|
|
1115
|
+
const agentTools = agentToolsMap.get(def.config.name);
|
|
1116
|
+
if (!agentTools) continue;
|
|
1117
|
+
const newAgentTools = def.config.skills ? skillInstances.filter((skill) => def.config.skills.includes(skill.name)).flatMap((skill) => Object.values(skill.tools)) : freshTools;
|
|
1118
|
+
agentTools.splice(0, agentTools.length, ...newAgentTools);
|
|
1119
|
+
}
|
|
1120
|
+
if (definitions.length === 0) {
|
|
1121
|
+
const fallbackTools = agentToolsMap.get("support");
|
|
1122
|
+
if (fallbackTools) fallbackTools.splice(0, fallbackTools.length, ...freshTools);
|
|
1123
|
+
}
|
|
1124
|
+
for (const def of definitions) {
|
|
1125
|
+
const agentTools = agentToolsMap.get(def.config.name);
|
|
1126
|
+
if (!agentTools) continue;
|
|
1127
|
+
const agent = os.getAgents().find((a) => a.getConfig?.().name === def.config.name || a.config?.name === def.config.name);
|
|
1128
|
+
if (agent) applyLLMOverride(agent, llm, agentTools, {
|
|
1129
|
+
systemPrompt: def.systemPrompt,
|
|
1130
|
+
kbRuntime,
|
|
1131
|
+
useKB: def.config.knowledgeBase,
|
|
1132
|
+
guardrails: def.config.guardrails,
|
|
1133
|
+
promptSkills: getPromptSkills(def.config.skills)
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
console.log(`[Operor] \u2705 Reloaded ${newSkills.length} skill(s)`);
|
|
1137
|
+
} catch (error) {
|
|
1138
|
+
console.error(`[Operor] \u274C Failed to reload MCP skills: ${error.message}`);
|
|
1139
|
+
}
|
|
1140
|
+
};
|
|
1141
|
+
await os.start();
|
|
1142
|
+
console.log("[Operor] Running. Press Ctrl+C to stop.");
|
|
1143
|
+
const mcpJsonPath = `${process.cwd()}/mcp.json`;
|
|
1144
|
+
if (fs.existsSync(mcpJsonPath) && loadSkillsConfig) {
|
|
1145
|
+
let reloadTimer = null;
|
|
1146
|
+
fs.watch(mcpJsonPath, () => {
|
|
1147
|
+
if (reloadTimer) clearTimeout(reloadTimer);
|
|
1148
|
+
reloadTimer = setTimeout(() => {
|
|
1149
|
+
reloadTimer = null;
|
|
1150
|
+
reloadSkills().catch((err) => {
|
|
1151
|
+
console.error(`[Operor] Skill reload error: ${err.message}`);
|
|
1152
|
+
});
|
|
1153
|
+
}, 500);
|
|
1154
|
+
});
|
|
1155
|
+
console.log("[Operor] Watching mcp.json for changes");
|
|
1156
|
+
}
|
|
1157
|
+
const shutdown = async () => {
|
|
1158
|
+
console.log("\n[Operor] Shutting down...");
|
|
1159
|
+
if (skillManager) await skillManager.closeAll();
|
|
1160
|
+
await os.stop();
|
|
1161
|
+
process.exit(0);
|
|
1162
|
+
};
|
|
1163
|
+
process.on("SIGINT", shutdown);
|
|
1164
|
+
process.on("SIGTERM", shutdown);
|
|
1165
|
+
if (config.CHANNEL === "mock") setTimeout(() => {
|
|
1166
|
+
provider.simulateIncomingMessage("+1234567890", "Hi, I need help with my order #1001");
|
|
1167
|
+
}, 2e3);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
//#endregion
|
|
1171
|
+
//#region src/commands/kb.ts
|
|
1172
|
+
function getKBConfig() {
|
|
1173
|
+
if (!configExists()) {
|
|
1174
|
+
clack.log.error(".env file not found. Run \"operor setup\" first.");
|
|
1175
|
+
process.exit(1);
|
|
1176
|
+
}
|
|
1177
|
+
const config = readConfig();
|
|
1178
|
+
if (config.KB_ENABLED !== "true") {
|
|
1179
|
+
clack.log.error("Knowledge Base is not enabled. Run \"operor setup\" or set KB_ENABLED=true.");
|
|
1180
|
+
process.exit(1);
|
|
1181
|
+
}
|
|
1182
|
+
return config;
|
|
1183
|
+
}
|
|
1184
|
+
async function loadKB(config) {
|
|
1185
|
+
try {
|
|
1186
|
+
const { SQLiteKnowledgeStore, EmbeddingService, TextChunker, IngestionPipeline, RetrievalPipeline, UrlIngestor, FileIngestor, SiteCrawler } = await import("@operor/knowledge");
|
|
1187
|
+
const embeddings = new EmbeddingService({
|
|
1188
|
+
provider: config.KB_EMBEDDING_PROVIDER || "openai",
|
|
1189
|
+
apiKey: config.KB_EMBEDDING_API_KEY,
|
|
1190
|
+
model: config.KB_EMBEDDING_MODEL
|
|
1191
|
+
});
|
|
1192
|
+
const store = new SQLiteKnowledgeStore(config.KB_DB_PATH || "./knowledge.db", embeddings.dimensions);
|
|
1193
|
+
await store.initialize();
|
|
1194
|
+
const chunker = new TextChunker({
|
|
1195
|
+
chunkSize: config.KB_CHUNK_SIZE ? parseInt(config.KB_CHUNK_SIZE) : void 0,
|
|
1196
|
+
chunkOverlap: config.KB_CHUNK_OVERLAP ? parseInt(config.KB_CHUNK_OVERLAP) : void 0
|
|
1197
|
+
});
|
|
1198
|
+
let llmProvider;
|
|
1199
|
+
if (config.LLM_PROVIDER && config.LLM_API_KEY) {
|
|
1200
|
+
const { AIProvider } = await import("@operor/llm");
|
|
1201
|
+
llmProvider = new AIProvider({
|
|
1202
|
+
provider: config.LLM_PROVIDER,
|
|
1203
|
+
apiKey: config.LLM_API_KEY,
|
|
1204
|
+
model: config.LLM_MODEL
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
const ingestion = new IngestionPipeline(store, embeddings, chunker, llmProvider);
|
|
1208
|
+
const retrieval = new RetrievalPipeline(store, embeddings);
|
|
1209
|
+
const crawl4aiUrl = config.CRAWL4AI_URL || void 0;
|
|
1210
|
+
return {
|
|
1211
|
+
store,
|
|
1212
|
+
embeddings,
|
|
1213
|
+
ingestion,
|
|
1214
|
+
retrieval,
|
|
1215
|
+
urlIngestor: new UrlIngestor(ingestion, { crawl4aiUrl }),
|
|
1216
|
+
fileIngestor: new FileIngestor(ingestion),
|
|
1217
|
+
siteCrawler: new SiteCrawler(ingestion, { crawl4aiUrl })
|
|
1218
|
+
};
|
|
1219
|
+
} catch {
|
|
1220
|
+
clack.log.error("@operor/knowledge package not found. Install it first:\n pnpm add @operor/knowledge");
|
|
1221
|
+
process.exit(1);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
function registerKBCommand(program) {
|
|
1225
|
+
const kb = program.command("kb").description("Knowledge Base management");
|
|
1226
|
+
kb.command("add-url").description("Ingest a URL into the knowledge base").argument("<url>", "URL to ingest").option("--priority <n>", "Document priority (1=official, 2=supplementary, 3=archived)", "2").option("--extract-qa", "Use LLM to extract Q&A pairs instead of chunking").action(async (url, opts) => {
|
|
1227
|
+
clack.intro("KB — Add URL");
|
|
1228
|
+
const { store, urlIngestor } = await loadKB(getKBConfig());
|
|
1229
|
+
const spinner = clack.spinner();
|
|
1230
|
+
spinner.start(`Fetching and ingesting ${url}`);
|
|
1231
|
+
try {
|
|
1232
|
+
const doc = await urlIngestor.ingestUrl(url, {
|
|
1233
|
+
priority: parseInt(opts.priority),
|
|
1234
|
+
extractQA: opts.extractQa
|
|
1235
|
+
});
|
|
1236
|
+
spinner.stop(`Ingested: ${doc.title || url}`);
|
|
1237
|
+
clack.log.success(`Document ID: ${doc.id}`);
|
|
1238
|
+
} catch (error) {
|
|
1239
|
+
spinner.stop(`Failed to ingest URL`);
|
|
1240
|
+
clack.log.error(error.message);
|
|
1241
|
+
process.exit(1);
|
|
1242
|
+
} finally {
|
|
1243
|
+
store.close();
|
|
1244
|
+
}
|
|
1245
|
+
clack.outro("Done");
|
|
1246
|
+
});
|
|
1247
|
+
kb.command("add-site").description("Crawl and ingest an entire website").argument("<url>", "Website URL to crawl").option("--depth <n>", "Maximum crawl depth", "2").option("--max-pages <n>", "Maximum pages to crawl", "50").option("--no-sitemap", "Skip sitemap.xml and use link crawling only").option("--delay <ms>", "Delay between requests in milliseconds", "500").action(async (url, opts) => {
|
|
1248
|
+
clack.intro("KB — Crawl Site");
|
|
1249
|
+
const { store, siteCrawler } = await loadKB(getKBConfig());
|
|
1250
|
+
const spinner = clack.spinner();
|
|
1251
|
+
spinner.start("Starting crawl...");
|
|
1252
|
+
try {
|
|
1253
|
+
const docs = await siteCrawler.crawlSite(url, {
|
|
1254
|
+
maxDepth: parseInt(opts.depth),
|
|
1255
|
+
maxPages: parseInt(opts.maxPages),
|
|
1256
|
+
useSitemap: opts.sitemap,
|
|
1257
|
+
delayMs: parseInt(opts.delay),
|
|
1258
|
+
onProgress: (crawled, discovered, currentUrl) => {
|
|
1259
|
+
const shortUrl = currentUrl.length > 60 ? currentUrl.slice(0, 57) + "..." : currentUrl;
|
|
1260
|
+
spinner.message(`Crawling... (${crawled}/${discovered} pages) ${shortUrl}`);
|
|
1261
|
+
}
|
|
1262
|
+
});
|
|
1263
|
+
spinner.stop(`Crawled ${docs.length} page(s)`);
|
|
1264
|
+
clack.log.success(`Ingested ${docs.length} document(s) from ${url}`);
|
|
1265
|
+
} catch (error) {
|
|
1266
|
+
spinner.stop("Crawl failed");
|
|
1267
|
+
clack.log.error(error.message);
|
|
1268
|
+
process.exit(1);
|
|
1269
|
+
} finally {
|
|
1270
|
+
store.close();
|
|
1271
|
+
}
|
|
1272
|
+
clack.outro("Done");
|
|
1273
|
+
});
|
|
1274
|
+
kb.command("add-file").description("Ingest a document file into the knowledge base").argument("<path>", "File path to ingest").option("--priority <n>", "Document priority (1=official, 2=supplementary, 3=archived)", "2").action(async (filePath, opts) => {
|
|
1275
|
+
clack.intro("KB — Add File");
|
|
1276
|
+
const { store, fileIngestor } = await loadKB(getKBConfig());
|
|
1277
|
+
const spinner = clack.spinner();
|
|
1278
|
+
spinner.start(`Ingesting ${filePath}`);
|
|
1279
|
+
try {
|
|
1280
|
+
const doc = await fileIngestor.ingestFile(filePath, void 0, { priority: parseInt(opts.priority) });
|
|
1281
|
+
spinner.stop(`Ingested: ${doc.title || filePath}`);
|
|
1282
|
+
clack.log.success(`Document ID: ${doc.id}`);
|
|
1283
|
+
} catch (error) {
|
|
1284
|
+
spinner.stop(`Failed to ingest file`);
|
|
1285
|
+
clack.log.error(error.message);
|
|
1286
|
+
process.exit(1);
|
|
1287
|
+
} finally {
|
|
1288
|
+
store.close();
|
|
1289
|
+
}
|
|
1290
|
+
clack.outro("Done");
|
|
1291
|
+
});
|
|
1292
|
+
kb.command("add-faq").description("Add a manual FAQ entry").argument("<question>", "FAQ question").argument("<answer>", "FAQ answer").action(async (question, answer) => {
|
|
1293
|
+
clack.intro("KB — Add FAQ");
|
|
1294
|
+
const { store, ingestion } = await loadKB(getKBConfig());
|
|
1295
|
+
const spinner = clack.spinner();
|
|
1296
|
+
spinner.start("Adding FAQ entry");
|
|
1297
|
+
try {
|
|
1298
|
+
let doc = await ingestion.ingestFaq(question, answer);
|
|
1299
|
+
if (doc.existingMatch) {
|
|
1300
|
+
const match = doc.existingMatch;
|
|
1301
|
+
clack.log.info(`Replacing similar FAQ (${(match.score * 100).toFixed(0)}% match): "${match.question}"`);
|
|
1302
|
+
doc = await ingestion.ingestFaq(question, answer, {
|
|
1303
|
+
forceReplace: true,
|
|
1304
|
+
replaceId: match.id
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
spinner.stop("FAQ entry added");
|
|
1308
|
+
clack.log.success(`Document ID: ${doc.id}`);
|
|
1309
|
+
} catch (error) {
|
|
1310
|
+
spinner.stop("Failed to add FAQ");
|
|
1311
|
+
clack.log.error(error.message);
|
|
1312
|
+
process.exit(1);
|
|
1313
|
+
} finally {
|
|
1314
|
+
store.close();
|
|
1315
|
+
}
|
|
1316
|
+
clack.outro("Done");
|
|
1317
|
+
});
|
|
1318
|
+
kb.command("list").description("List all KB documents").action(async () => {
|
|
1319
|
+
clack.intro("KB — Documents");
|
|
1320
|
+
const { store } = await loadKB(getKBConfig());
|
|
1321
|
+
try {
|
|
1322
|
+
const docs = await store.listDocuments();
|
|
1323
|
+
if (docs.length === 0) clack.log.info("No documents in the knowledge base.");
|
|
1324
|
+
else {
|
|
1325
|
+
const header = `${"ID".padEnd(40)} ${"Type".padEnd(12)} Title / Source`;
|
|
1326
|
+
clack.log.info(header);
|
|
1327
|
+
clack.log.info("─".repeat(header.length));
|
|
1328
|
+
for (const doc of docs) {
|
|
1329
|
+
const line = `${doc.id.padEnd(40)} ${doc.sourceType.padEnd(12)} ${doc.title || doc.sourceUrl || doc.fileName || "—"}`;
|
|
1330
|
+
clack.log.info(line);
|
|
1331
|
+
}
|
|
1332
|
+
clack.log.info(`\n${docs.length} document(s)`);
|
|
1333
|
+
}
|
|
1334
|
+
} catch (error) {
|
|
1335
|
+
clack.log.error(error.message);
|
|
1336
|
+
process.exit(1);
|
|
1337
|
+
} finally {
|
|
1338
|
+
store.close();
|
|
1339
|
+
}
|
|
1340
|
+
clack.outro("");
|
|
1341
|
+
});
|
|
1342
|
+
kb.command("search").description("Test search against the knowledge base").argument("<query>", "Search query").option("-n, --limit <n>", "Max results", "5").action(async (query, opts) => {
|
|
1343
|
+
clack.intro("KB — Search");
|
|
1344
|
+
const { store, retrieval } = await loadKB(getKBConfig());
|
|
1345
|
+
const spinner = clack.spinner();
|
|
1346
|
+
spinner.start(`Searching for "${query}"`);
|
|
1347
|
+
try {
|
|
1348
|
+
const result = await retrieval.retrieve(query, { limit: parseInt(opts.limit) });
|
|
1349
|
+
spinner.stop(`Found ${result.results.length} result(s)`);
|
|
1350
|
+
if (result.results.length === 0) clack.log.info("No results found.");
|
|
1351
|
+
else for (const r of result.results) {
|
|
1352
|
+
clack.log.info(`\n[Score: ${r.score.toFixed(4)}] Doc #${r.document.id}`);
|
|
1353
|
+
const preview = r.chunk.content.length > 200 ? r.chunk.content.slice(0, 200) + "…" : r.chunk.content;
|
|
1354
|
+
clack.log.message(preview);
|
|
1355
|
+
}
|
|
1356
|
+
} catch (error) {
|
|
1357
|
+
spinner.stop("Search failed");
|
|
1358
|
+
clack.log.error(error.message);
|
|
1359
|
+
process.exit(1);
|
|
1360
|
+
} finally {
|
|
1361
|
+
store.close();
|
|
1362
|
+
}
|
|
1363
|
+
clack.outro("");
|
|
1364
|
+
});
|
|
1365
|
+
kb.command("ask").description("Ask a question using KB retrieval + LLM generation (RAG)").argument("<question>", "Question to answer").option("-n, --limit <n>", "Max KB chunks to retrieve", "5").option("--no-sources", "Hide source attribution").action(async (question, opts) => {
|
|
1366
|
+
clack.intro("KB — Ask");
|
|
1367
|
+
const config = getKBConfig();
|
|
1368
|
+
const { store, retrieval } = await loadKB(config);
|
|
1369
|
+
const spinner = clack.spinner();
|
|
1370
|
+
spinner.start("Searching knowledge base...");
|
|
1371
|
+
try {
|
|
1372
|
+
const result = await retrieval.retrieve(question, { limit: parseInt(opts.limit) });
|
|
1373
|
+
if (result.isFaqMatch && result.results.length > 0) {
|
|
1374
|
+
spinner.stop("FAQ match found");
|
|
1375
|
+
clack.log.success(result.results[0].chunk.content);
|
|
1376
|
+
if (opts.sources) clack.log.info(`\nSource: FAQ (score: ${result.results[0].score.toFixed(4)})`);
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
if (result.results.length === 0) {
|
|
1380
|
+
spinner.stop("No relevant KB content found");
|
|
1381
|
+
clack.log.warn("Cannot answer — no matching documents.");
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
if (!config.LLM_PROVIDER || !config.LLM_API_KEY) {
|
|
1385
|
+
spinner.stop("KB results found, but no LLM configured");
|
|
1386
|
+
clack.log.error("LLM_PROVIDER and LLM_API_KEY required. Use \"operor kb search\" for retrieval-only.");
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
spinner.message("Generating answer...");
|
|
1390
|
+
const { AIProvider } = await import("@operor/llm");
|
|
1391
|
+
const response = await new AIProvider({
|
|
1392
|
+
provider: config.LLM_PROVIDER,
|
|
1393
|
+
apiKey: config.LLM_API_KEY,
|
|
1394
|
+
model: config.LLM_MODEL
|
|
1395
|
+
}).complete([{
|
|
1396
|
+
role: "system",
|
|
1397
|
+
content: `Answer using ONLY the provided context. If insufficient, say so.\n\n${result.context}`
|
|
1398
|
+
}, {
|
|
1399
|
+
role: "user",
|
|
1400
|
+
content: question
|
|
1401
|
+
}]);
|
|
1402
|
+
spinner.stop("Answer generated");
|
|
1403
|
+
clack.log.success(response.text);
|
|
1404
|
+
if (opts.sources) {
|
|
1405
|
+
clack.log.info("\nSources:");
|
|
1406
|
+
for (const r of result.results) {
|
|
1407
|
+
const title = r.document?.title || r.document?.id || "unknown";
|
|
1408
|
+
clack.log.info(` - ${title} (score: ${r.score.toFixed(4)})`);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
} catch (error) {
|
|
1412
|
+
spinner.stop("Failed");
|
|
1413
|
+
clack.log.error(error.message);
|
|
1414
|
+
process.exit(1);
|
|
1415
|
+
} finally {
|
|
1416
|
+
store.close();
|
|
1417
|
+
}
|
|
1418
|
+
clack.outro("");
|
|
1419
|
+
});
|
|
1420
|
+
kb.command("delete").description("Delete a document from the knowledge base").argument("<id>", "Document ID to delete").action(async (id) => {
|
|
1421
|
+
clack.intro("KB — Delete");
|
|
1422
|
+
const { store } = await loadKB(getKBConfig());
|
|
1423
|
+
try {
|
|
1424
|
+
await store.deleteDocument(id);
|
|
1425
|
+
clack.log.success(`Document #${id} deleted`);
|
|
1426
|
+
} catch (error) {
|
|
1427
|
+
clack.log.error(error.message);
|
|
1428
|
+
process.exit(1);
|
|
1429
|
+
} finally {
|
|
1430
|
+
store.close();
|
|
1431
|
+
}
|
|
1432
|
+
clack.outro("Done");
|
|
1433
|
+
});
|
|
1434
|
+
kb.command("rebuild").description("Rebuild all KB vector embeddings using the current embedding provider").action(async () => {
|
|
1435
|
+
clack.intro("KB — Rebuild Embeddings");
|
|
1436
|
+
const config = getKBConfig();
|
|
1437
|
+
const { store, ingestion } = await loadKB(config);
|
|
1438
|
+
const spinner = clack.spinner();
|
|
1439
|
+
try {
|
|
1440
|
+
const stats = await store.getStats();
|
|
1441
|
+
if (stats.documentCount === 0) {
|
|
1442
|
+
clack.log.warn("Knowledge Base is empty — nothing to rebuild.");
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
clack.log.info(`Provider: ${config.KB_EMBEDDING_PROVIDER || "openai"}`);
|
|
1446
|
+
clack.log.info(`Documents: ${stats.documentCount}, Chunks: ${stats.chunkCount}`);
|
|
1447
|
+
spinner.start("Rebuilding embeddings...");
|
|
1448
|
+
const result = await ingestion.rebuild((current, total, docTitle) => {
|
|
1449
|
+
if (current < total) spinner.message(`Rebuilding... (${current + 1}/${total}) ${docTitle}`);
|
|
1450
|
+
});
|
|
1451
|
+
spinner.stop("Rebuild complete");
|
|
1452
|
+
clack.log.success([
|
|
1453
|
+
`Documents rebuilt: ${result.documentsRebuilt}`,
|
|
1454
|
+
`Chunks rebuilt: ${result.chunksRebuilt}`,
|
|
1455
|
+
`Old dimensions: ${result.oldDimensions}`,
|
|
1456
|
+
`New dimensions: ${result.newDimensions}`
|
|
1457
|
+
].join("\n"));
|
|
1458
|
+
} catch (error) {
|
|
1459
|
+
spinner.stop("Rebuild failed");
|
|
1460
|
+
clack.log.error(error.message);
|
|
1461
|
+
process.exit(1);
|
|
1462
|
+
} finally {
|
|
1463
|
+
store.close();
|
|
1464
|
+
}
|
|
1465
|
+
clack.outro("Done");
|
|
1466
|
+
});
|
|
1467
|
+
kb.command("stats").description("Show knowledge base statistics").action(async () => {
|
|
1468
|
+
clack.intro("KB — Statistics");
|
|
1469
|
+
const config = getKBConfig();
|
|
1470
|
+
const { store } = await loadKB(config);
|
|
1471
|
+
try {
|
|
1472
|
+
const stats = await store.getStats();
|
|
1473
|
+
clack.log.info(`Documents: ${stats.documentCount}`);
|
|
1474
|
+
clack.log.info(`Chunks: ${stats.chunkCount}`);
|
|
1475
|
+
clack.log.info(`Embedding dims: ${stats.embeddingDimensions || "—"}`);
|
|
1476
|
+
clack.log.info(`DB size: ${formatBytes(stats.dbSizeBytes)}`);
|
|
1477
|
+
clack.log.info(`DB path: ${config.KB_DB_PATH || "./knowledge.db"}`);
|
|
1478
|
+
clack.log.info(`Embedding provider: ${config.KB_EMBEDDING_PROVIDER || "openai"}`);
|
|
1479
|
+
clack.log.info(`Embedding model: ${config.KB_EMBEDDING_MODEL || "default"}`);
|
|
1480
|
+
} catch (error) {
|
|
1481
|
+
clack.log.error(error.message);
|
|
1482
|
+
process.exit(1);
|
|
1483
|
+
} finally {
|
|
1484
|
+
store.close();
|
|
1485
|
+
}
|
|
1486
|
+
clack.outro("");
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
function formatBytes(bytes) {
|
|
1490
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
1491
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1492
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
//#endregion
|
|
1496
|
+
//#region src/commands/analytics.ts
|
|
1497
|
+
function getAnalyticsConfig() {
|
|
1498
|
+
if (!configExists()) {
|
|
1499
|
+
clack.log.error(".env file not found. Run \"operor setup\" first.");
|
|
1500
|
+
process.exit(1);
|
|
1501
|
+
}
|
|
1502
|
+
const config = readConfig();
|
|
1503
|
+
if (config.ANALYTICS_ENABLED === "false") {
|
|
1504
|
+
clack.log.error("Analytics is disabled. Set ANALYTICS_ENABLED=true in .env.");
|
|
1505
|
+
process.exit(1);
|
|
1506
|
+
}
|
|
1507
|
+
return config;
|
|
1508
|
+
}
|
|
1509
|
+
async function loadStore(config) {
|
|
1510
|
+
try {
|
|
1511
|
+
const { SQLiteAnalyticsStore } = await import("@operor/analytics");
|
|
1512
|
+
const store = new SQLiteAnalyticsStore(config.ANALYTICS_DB_PATH || "./analytics.db");
|
|
1513
|
+
await store.initialize();
|
|
1514
|
+
return store;
|
|
1515
|
+
} catch {
|
|
1516
|
+
clack.log.error("@operor/analytics package not found. Install it first:\n pnpm add @operor/analytics");
|
|
1517
|
+
process.exit(1);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
function makeRange(days) {
|
|
1521
|
+
const to = Date.now();
|
|
1522
|
+
return {
|
|
1523
|
+
from: to - days * 24 * 60 * 60 * 1e3,
|
|
1524
|
+
to
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
/** Simple sparkline from an array of numbers */
|
|
1528
|
+
function sparkline(values) {
|
|
1529
|
+
if (values.length === 0) return "";
|
|
1530
|
+
const chars = "▁▂▃▄▅▆▇█";
|
|
1531
|
+
const max = Math.max(...values);
|
|
1532
|
+
if (max === 0) return chars[0].repeat(values.length);
|
|
1533
|
+
return values.map((v) => chars[Math.min(Math.floor(v / max * 7), 7)]).join("");
|
|
1534
|
+
}
|
|
1535
|
+
/** Simple horizontal bar */
|
|
1536
|
+
function bar(value, max, width = 30) {
|
|
1537
|
+
if (max === 0) return "";
|
|
1538
|
+
const filled = Math.round(value / max * width);
|
|
1539
|
+
return "█".repeat(filled) + "░".repeat(width - filled);
|
|
1540
|
+
}
|
|
1541
|
+
function maskPhone(phone) {
|
|
1542
|
+
if (phone.length <= 6) return "***" + phone.slice(-3);
|
|
1543
|
+
return phone.slice(0, 3) + "***" + phone.slice(-3);
|
|
1544
|
+
}
|
|
1545
|
+
function fmtMs(ms) {
|
|
1546
|
+
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
1547
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
1548
|
+
}
|
|
1549
|
+
function fmtPct(pct) {
|
|
1550
|
+
return `${pct.toFixed(1)}%`;
|
|
1551
|
+
}
|
|
1552
|
+
function registerAnalyticsCommand(program) {
|
|
1553
|
+
const analytics = program.command("analytics").description("Usage analytics and insights");
|
|
1554
|
+
analytics.option("--days <n>", "Number of days to include", "7").action(async (opts) => {
|
|
1555
|
+
clack.intro("Analytics — Dashboard");
|
|
1556
|
+
const store = await loadStore(getAnalyticsConfig());
|
|
1557
|
+
const range = makeRange(parseInt(opts.days));
|
|
1558
|
+
try {
|
|
1559
|
+
const [summary, volume, agents] = await Promise.all([
|
|
1560
|
+
store.getSummary(range),
|
|
1561
|
+
store.getMessageVolume(range, "day"),
|
|
1562
|
+
store.getAgentStats(range)
|
|
1563
|
+
]);
|
|
1564
|
+
if (summary.totalMessages === 0) {
|
|
1565
|
+
clack.log.info("No messages recorded in this period.");
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
const trend = sparkline(volume.map((v) => v.count));
|
|
1569
|
+
clack.log.info(` Messages`);
|
|
1570
|
+
clack.log.info(` Total: ${summary.totalMessages}`);
|
|
1571
|
+
clack.log.info(` Avg/day: ${summary.avgPerDay.toFixed(1)}`);
|
|
1572
|
+
clack.log.info(` Peak day: ${summary.peakDay || "—"}`);
|
|
1573
|
+
clack.log.info(` Trend: ${trend}`);
|
|
1574
|
+
clack.log.info("");
|
|
1575
|
+
clack.log.info(` Response Quality`);
|
|
1576
|
+
clack.log.info(` KB answered: ${fmtPct(summary.kbAnsweredPct)}`);
|
|
1577
|
+
clack.log.info(` LLM fallback: ${fmtPct(summary.llmFallbackPct)}`);
|
|
1578
|
+
clack.log.info(` No answer: ${fmtPct(summary.noAnswerPct)}`);
|
|
1579
|
+
clack.log.info(` Avg response: ${fmtMs(summary.avgResponseTime)}`);
|
|
1580
|
+
clack.log.info(` FAQ hit rate: ${fmtPct(summary.faqHitRate)}`);
|
|
1581
|
+
clack.log.info("");
|
|
1582
|
+
clack.log.info(` Customers`);
|
|
1583
|
+
clack.log.info(` Unique: ${summary.uniqueCustomers}`);
|
|
1584
|
+
clack.log.info(` New: ${summary.newCustomers}`);
|
|
1585
|
+
clack.log.info(` Returning: ${summary.returningCustomers}`);
|
|
1586
|
+
if (summary.uniqueCustomers > 0) clack.log.info(` Avg msgs: ${(summary.totalMessages / summary.uniqueCustomers).toFixed(1)}/customer`);
|
|
1587
|
+
if (agents.length > 0) {
|
|
1588
|
+
clack.log.info("");
|
|
1589
|
+
clack.log.info(` Agents`);
|
|
1590
|
+
for (const a of agents) {
|
|
1591
|
+
const pct = summary.totalMessages > 0 ? (a.messageCount / summary.totalMessages * 100).toFixed(1) : "0.0";
|
|
1592
|
+
clack.log.info(` ${a.agentName.padEnd(20)} ${String(a.messageCount).padStart(5)} msgs (${pct}%) avg ${fmtMs(a.avgResponseTimeMs)}`);
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
if (summary.pendingReviews > 0) {
|
|
1596
|
+
clack.log.info("");
|
|
1597
|
+
clack.log.info(` Copilot`);
|
|
1598
|
+
clack.log.info(` Pending reviews: ${summary.pendingReviews}`);
|
|
1599
|
+
}
|
|
1600
|
+
} catch (error) {
|
|
1601
|
+
clack.log.error(error.message);
|
|
1602
|
+
process.exit(1);
|
|
1603
|
+
} finally {
|
|
1604
|
+
await store.close();
|
|
1605
|
+
}
|
|
1606
|
+
clack.outro("");
|
|
1607
|
+
});
|
|
1608
|
+
analytics.command("messages").description("Message volume breakdown").option("--days <n>", "Number of days to include", "7").action(async (opts) => {
|
|
1609
|
+
clack.intro("Analytics — Messages");
|
|
1610
|
+
const store = await loadStore(getAnalyticsConfig());
|
|
1611
|
+
const range = makeRange(parseInt(opts.days));
|
|
1612
|
+
try {
|
|
1613
|
+
const [daily, hourly] = await Promise.all([store.getMessageVolume(range, "day"), store.getMessageVolume(makeRange(1), "hour")]);
|
|
1614
|
+
if (daily.length === 0) {
|
|
1615
|
+
clack.log.info("No messages recorded in this period.");
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
const maxDaily = Math.max(...daily.map((d) => d.count));
|
|
1619
|
+
clack.log.info(" Daily Volume");
|
|
1620
|
+
for (const d of daily) clack.log.info(` ${d.date} ${bar(d.count, maxDaily, 25)} ${d.count}`);
|
|
1621
|
+
if (hourly.length > 0) {
|
|
1622
|
+
const maxHourly = Math.max(...hourly.map((h) => h.count));
|
|
1623
|
+
clack.log.info("");
|
|
1624
|
+
clack.log.info(" Today — Hourly");
|
|
1625
|
+
for (const h of hourly) {
|
|
1626
|
+
const hour = h.date.split(" ")[1] || h.date;
|
|
1627
|
+
clack.log.info(` ${hour} ${bar(h.count, maxHourly, 20)} ${h.count}`);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
} catch (error) {
|
|
1631
|
+
clack.log.error(error.message);
|
|
1632
|
+
process.exit(1);
|
|
1633
|
+
} finally {
|
|
1634
|
+
await store.close();
|
|
1635
|
+
}
|
|
1636
|
+
clack.outro("");
|
|
1637
|
+
});
|
|
1638
|
+
analytics.command("agents").description("Per-agent performance breakdown").option("--days <n>", "Number of days to include", "7").action(async (opts) => {
|
|
1639
|
+
clack.intro("Analytics — Agents");
|
|
1640
|
+
const store = await loadStore(getAnalyticsConfig());
|
|
1641
|
+
const range = makeRange(parseInt(opts.days));
|
|
1642
|
+
try {
|
|
1643
|
+
const [agents, summary] = await Promise.all([store.getAgentStats(range), store.getSummary(range)]);
|
|
1644
|
+
if (agents.length === 0) {
|
|
1645
|
+
clack.log.info("No agent activity in this period.");
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
const header = `${"Agent".padEnd(22)} ${"Msgs".padStart(6)} ${"%".padStart(6)} ${"Avg Time".padStart(10)} ${"Confidence".padStart(11)} ${"Escalations".padStart(12)} ${"Tools".padStart(6)}`;
|
|
1649
|
+
clack.log.info(header);
|
|
1650
|
+
clack.log.info("─".repeat(header.length));
|
|
1651
|
+
for (const a of agents) {
|
|
1652
|
+
const pct = summary.totalMessages > 0 ? (a.messageCount / summary.totalMessages * 100).toFixed(1) : "0.0";
|
|
1653
|
+
const line = `${a.agentName.padEnd(22)} ${String(a.messageCount).padStart(6)} ${(pct + "%").padStart(6)} ${fmtMs(a.avgResponseTimeMs).padStart(10)} ${fmtPct(a.avgConfidence * 100).padStart(11)} ${String(a.escalationCount).padStart(12)} ${String(a.toolCallCount).padStart(6)}`;
|
|
1654
|
+
clack.log.info(line);
|
|
1655
|
+
}
|
|
1656
|
+
} catch (error) {
|
|
1657
|
+
clack.log.error(error.message);
|
|
1658
|
+
process.exit(1);
|
|
1659
|
+
} finally {
|
|
1660
|
+
await store.close();
|
|
1661
|
+
}
|
|
1662
|
+
clack.outro("");
|
|
1663
|
+
});
|
|
1664
|
+
analytics.command("customers").description("Customer engagement metrics").option("--days <n>", "Number of days to include", "7").option("-n, --limit <n>", "Top customers to show", "10").action(async (opts) => {
|
|
1665
|
+
clack.intro("Analytics — Customers");
|
|
1666
|
+
const store = await loadStore(getAnalyticsConfig());
|
|
1667
|
+
const range = makeRange(parseInt(opts.days));
|
|
1668
|
+
try {
|
|
1669
|
+
const [customers, summary] = await Promise.all([store.getCustomerStats(range, parseInt(opts.limit)), store.getSummary(range)]);
|
|
1670
|
+
if (customers.length === 0) {
|
|
1671
|
+
clack.log.info("No customer activity in this period.");
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
clack.log.info(` Unique: ${summary.uniqueCustomers}`);
|
|
1675
|
+
clack.log.info(` New: ${summary.newCustomers}`);
|
|
1676
|
+
clack.log.info(` Returning: ${summary.returningCustomers}`);
|
|
1677
|
+
const buckets = {
|
|
1678
|
+
"1 msg": 0,
|
|
1679
|
+
"2-5": 0,
|
|
1680
|
+
"6-10": 0,
|
|
1681
|
+
"10+": 0
|
|
1682
|
+
};
|
|
1683
|
+
for (const c of customers) if (c.messageCount === 1) buckets["1 msg"]++;
|
|
1684
|
+
else if (c.messageCount <= 5) buckets["2-5"]++;
|
|
1685
|
+
else if (c.messageCount <= 10) buckets["6-10"]++;
|
|
1686
|
+
else buckets["10+"]++;
|
|
1687
|
+
clack.log.info("");
|
|
1688
|
+
clack.log.info(" Engagement Distribution");
|
|
1689
|
+
const maxBucket = Math.max(...Object.values(buckets));
|
|
1690
|
+
for (const [label, count] of Object.entries(buckets)) clack.log.info(` ${label.padEnd(8)} ${bar(count, maxBucket, 20)} ${count}`);
|
|
1691
|
+
clack.log.info("");
|
|
1692
|
+
clack.log.info(" Top Customers");
|
|
1693
|
+
const header = `${"Phone".padEnd(16)} ${"Msgs".padStart(6)} ${"Status".padStart(8)} ${"Last Seen".padStart(12)}`;
|
|
1694
|
+
clack.log.info(` ${header}`);
|
|
1695
|
+
clack.log.info(` ${"─".repeat(header.length)}`);
|
|
1696
|
+
for (const c of customers) {
|
|
1697
|
+
const lastSeen = new Date(c.lastSeen).toLocaleDateString();
|
|
1698
|
+
const status = c.isNew ? "new" : "return";
|
|
1699
|
+
clack.log.info(` ${maskPhone(c.customerPhone).padEnd(16)} ${String(c.messageCount).padStart(6)} ${status.padStart(8)} ${lastSeen.padStart(12)}`);
|
|
1700
|
+
}
|
|
1701
|
+
} catch (error) {
|
|
1702
|
+
clack.log.error(error.message);
|
|
1703
|
+
process.exit(1);
|
|
1704
|
+
} finally {
|
|
1705
|
+
await store.close();
|
|
1706
|
+
}
|
|
1707
|
+
clack.outro("");
|
|
1708
|
+
});
|
|
1709
|
+
analytics.command("kb").description("Knowledge Base performance metrics").option("--days <n>", "Number of days to include", "7").action(async (opts) => {
|
|
1710
|
+
clack.intro("Analytics — Knowledge Base");
|
|
1711
|
+
const store = await loadStore(getAnalyticsConfig());
|
|
1712
|
+
const range = makeRange(parseInt(opts.days));
|
|
1713
|
+
try {
|
|
1714
|
+
const kbStats = await store.getKbStats(range);
|
|
1715
|
+
if (kbStats.totalQueries === 0) {
|
|
1716
|
+
clack.log.info("No KB queries in this period.");
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
clack.log.info(` Total queries: ${kbStats.totalQueries}`);
|
|
1720
|
+
clack.log.info(` FAQ hits: ${kbStats.faqHits} (${fmtPct(kbStats.faqHitRate)})`);
|
|
1721
|
+
clack.log.info(` Avg top score: ${kbStats.avgTopScore.toFixed(4)}`);
|
|
1722
|
+
const hybridCount = kbStats.totalQueries - kbStats.faqHits;
|
|
1723
|
+
clack.log.info("");
|
|
1724
|
+
clack.log.info(" Pipeline Breakdown");
|
|
1725
|
+
clack.log.info(` FAQ fast-path: ${kbStats.faqHits}`);
|
|
1726
|
+
clack.log.info(` Hybrid/LLM: ${hybridCount}`);
|
|
1727
|
+
if (kbStats.topFaqHits.length > 0) {
|
|
1728
|
+
clack.log.info("");
|
|
1729
|
+
clack.log.info(" Top FAQ Hits");
|
|
1730
|
+
for (const h of kbStats.topFaqHits.slice(0, 10)) clack.log.info(` ${String(h.count).padStart(4)}x ${h.intent}`);
|
|
1731
|
+
}
|
|
1732
|
+
if (kbStats.topMisses.length > 0) {
|
|
1733
|
+
clack.log.info("");
|
|
1734
|
+
clack.log.info(" Top Misses (consider teaching these)");
|
|
1735
|
+
for (const m of kbStats.topMisses.slice(0, 10)) clack.log.info(` ${String(m.count).padStart(4)}x ${m.intent} (avg score: ${m.avgScore.toFixed(3)})`);
|
|
1736
|
+
clack.log.info("");
|
|
1737
|
+
clack.log.info(" Tip: Use \"operor kb add-faq <question> <answer>\" to teach missed queries.");
|
|
1738
|
+
}
|
|
1739
|
+
} catch (error) {
|
|
1740
|
+
clack.log.error(error.message);
|
|
1741
|
+
process.exit(1);
|
|
1742
|
+
} finally {
|
|
1743
|
+
await store.close();
|
|
1744
|
+
}
|
|
1745
|
+
clack.outro("");
|
|
1746
|
+
});
|
|
1747
|
+
analytics.command("export").description("Export analytics data as CSV or JSON").option("--days <n>", "Number of days to include", "7").option("--json", "Export as JSON instead of CSV").option("-o, --output <file>", "Output file path").action(async (opts) => {
|
|
1748
|
+
clack.intro("Analytics — Export");
|
|
1749
|
+
const store = await loadStore(getAnalyticsConfig());
|
|
1750
|
+
const range = makeRange(parseInt(opts.days));
|
|
1751
|
+
try {
|
|
1752
|
+
const [summary, volume, agents, customers, kbStats] = await Promise.all([
|
|
1753
|
+
store.getSummary(range),
|
|
1754
|
+
store.getMessageVolume(range, "day"),
|
|
1755
|
+
store.getAgentStats(range),
|
|
1756
|
+
store.getCustomerStats(range, 100),
|
|
1757
|
+
store.getKbStats(range)
|
|
1758
|
+
]);
|
|
1759
|
+
const data = {
|
|
1760
|
+
summary,
|
|
1761
|
+
volume,
|
|
1762
|
+
agents,
|
|
1763
|
+
customers,
|
|
1764
|
+
kbStats
|
|
1765
|
+
};
|
|
1766
|
+
let output;
|
|
1767
|
+
if (opts.json) output = JSON.stringify(data, null, 2);
|
|
1768
|
+
else {
|
|
1769
|
+
const lines = ["date,messages"];
|
|
1770
|
+
for (const v of volume) lines.push(`${v.date},${v.count}`);
|
|
1771
|
+
output = lines.join("\n");
|
|
1772
|
+
}
|
|
1773
|
+
if (opts.output) {
|
|
1774
|
+
const { writeFileSync } = await import("node:fs");
|
|
1775
|
+
writeFileSync(opts.output, output);
|
|
1776
|
+
clack.log.success(`Exported to ${opts.output}`);
|
|
1777
|
+
} else console.log(output);
|
|
1778
|
+
} catch (error) {
|
|
1779
|
+
clack.log.error(error.message);
|
|
1780
|
+
process.exit(1);
|
|
1781
|
+
} finally {
|
|
1782
|
+
await store.close();
|
|
1783
|
+
}
|
|
1784
|
+
clack.outro("");
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
//#endregion
|
|
1789
|
+
//#region src/commands/history.ts
|
|
1790
|
+
function registerHistoryCommand(program) {
|
|
1791
|
+
program.command("history").description("Conversation history management").command("clear").description("Clear conversation history from the memory database").option("--customer <id>", "Clear history for a specific customer ID").option("--agent <name>", "Clear history for a specific agent").option("--all", "Clear all conversation history").option("--yes", "Skip confirmation prompt").action(async (opts) => {
|
|
1792
|
+
if (!opts.customer && !opts.agent && !opts.all) {
|
|
1793
|
+
console.error("Specify --customer <id>, --agent <name>, --all, or a combination.");
|
|
1794
|
+
process.exit(1);
|
|
1795
|
+
}
|
|
1796
|
+
if (!configExists()) {
|
|
1797
|
+
console.error(".env file not found. Run \"operor setup\" first.");
|
|
1798
|
+
process.exit(1);
|
|
1799
|
+
}
|
|
1800
|
+
const config = readConfig();
|
|
1801
|
+
if (config.MEMORY_TYPE !== "sqlite") {
|
|
1802
|
+
console.error("History clear requires SQLite memory (MEMORY_TYPE=sqlite).");
|
|
1803
|
+
process.exit(1);
|
|
1804
|
+
}
|
|
1805
|
+
const { SQLiteMemory } = await import("@operor/memory");
|
|
1806
|
+
const memory = new SQLiteMemory(config.MEMORY_DB_PATH || "./operor.db");
|
|
1807
|
+
await memory.initialize();
|
|
1808
|
+
try {
|
|
1809
|
+
const scope = opts.all ? "ALL conversation history" : `history for ${[opts.customer && `customer=${opts.customer}`, opts.agent && `agent=${opts.agent}`].filter(Boolean).join(", ")}`;
|
|
1810
|
+
if (!opts.yes) {
|
|
1811
|
+
const rl = createInterface({
|
|
1812
|
+
input: process.stdin,
|
|
1813
|
+
output: process.stdout
|
|
1814
|
+
});
|
|
1815
|
+
const answer = await new Promise((r) => rl.question(`Delete ${scope}? Type "yes" to confirm: `, r));
|
|
1816
|
+
rl.close();
|
|
1817
|
+
if (answer.trim().toLowerCase() !== "yes") {
|
|
1818
|
+
console.log("Aborted.");
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
const { deletedCount } = await memory.clearHistory(opts.all ? void 0 : opts.customer, opts.agent);
|
|
1823
|
+
console.log(`Deleted ${deletedCount} message(s).`);
|
|
1824
|
+
} finally {
|
|
1825
|
+
await memory.close();
|
|
1826
|
+
}
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
//#endregion
|
|
1831
|
+
//#region src/commands/chat.ts
|
|
1832
|
+
async function buildPipeline(opts) {
|
|
1833
|
+
const config = readConfig();
|
|
1834
|
+
if (!config.LLM_PROVIDER || !config.LLM_API_KEY) {
|
|
1835
|
+
clack.log.error("LLM_PROVIDER and LLM_API_KEY required. Run \"operor setup\" first.");
|
|
1836
|
+
process.exit(1);
|
|
1837
|
+
}
|
|
1838
|
+
const { Operor, LLMIntentClassifier, AgentLoader } = await import("@operor/core");
|
|
1839
|
+
const { AIProvider } = await import("@operor/llm");
|
|
1840
|
+
const { MockProvider } = await import("@operor/provider-mock");
|
|
1841
|
+
const llm = new AIProvider({
|
|
1842
|
+
provider: config.LLM_PROVIDER,
|
|
1843
|
+
apiKey: config.LLM_API_KEY,
|
|
1844
|
+
model: config.LLM_MODEL
|
|
1845
|
+
});
|
|
1846
|
+
let intentClassifier;
|
|
1847
|
+
if (config.INTENT_CLASSIFIER === "llm") intentClassifier = new LLMIntentClassifier(llm);
|
|
1848
|
+
let skillsModule;
|
|
1849
|
+
try {
|
|
1850
|
+
skillsModule = await import("@operor/skills");
|
|
1851
|
+
} catch {}
|
|
1852
|
+
const os = new Operor({
|
|
1853
|
+
debug: !!opts.debug,
|
|
1854
|
+
...intentClassifier && { intentClassifier },
|
|
1855
|
+
...skillsModule && { skillsModule }
|
|
1856
|
+
});
|
|
1857
|
+
const provider = new MockProvider();
|
|
1858
|
+
os.addProvider(provider);
|
|
1859
|
+
const skillInstances = [];
|
|
1860
|
+
if (config.SKILLS_ENABLED !== "false") try {
|
|
1861
|
+
const { SkillManager, loadSkillsConfig } = await import("@operor/skills");
|
|
1862
|
+
const skillsConfig = loadSkillsConfig();
|
|
1863
|
+
const skills = await new SkillManager().initialize(skillsConfig);
|
|
1864
|
+
for (const skill of skills) {
|
|
1865
|
+
os.addSkill(skill);
|
|
1866
|
+
skillInstances.push(skill);
|
|
1867
|
+
if (opts.debug) clack.log.info(`[debug] MCP skill loaded: ${skill.name}`);
|
|
1868
|
+
}
|
|
1869
|
+
} catch (error) {
|
|
1870
|
+
if (opts.debug) clack.log.warn(`[debug] MCP skills not available: ${error.message}`);
|
|
1871
|
+
}
|
|
1872
|
+
const allTools = opts.tools !== false ? skillInstances.flatMap((skill) => Object.values(skill.tools)) : [];
|
|
1873
|
+
const getPromptSkills = (agentSkillNames) => {
|
|
1874
|
+
return skillInstances.filter((s) => typeof s.getContent === "function").filter((s) => !agentSkillNames || agentSkillNames.includes(s.name)).map((s) => ({
|
|
1875
|
+
name: s.name,
|
|
1876
|
+
content: s.getContent()
|
|
1877
|
+
}));
|
|
1878
|
+
};
|
|
1879
|
+
let kbRuntime;
|
|
1880
|
+
let kbStore = null;
|
|
1881
|
+
if (opts.kb !== false && config.KB_ENABLED === "true") try {
|
|
1882
|
+
const { SQLiteKnowledgeStore, EmbeddingService, TextChunker, IngestionPipeline, RetrievalPipeline, QueryRewriter } = await import("@operor/knowledge");
|
|
1883
|
+
const embedder = new EmbeddingService({
|
|
1884
|
+
provider: config.KB_EMBEDDING_PROVIDER || "openai",
|
|
1885
|
+
apiKey: config.KB_EMBEDDING_API_KEY || config.LLM_API_KEY,
|
|
1886
|
+
model: config.KB_EMBEDDING_MODEL
|
|
1887
|
+
});
|
|
1888
|
+
kbStore = new SQLiteKnowledgeStore(config.KB_DB_PATH || "./knowledge.db", embedder.dimensions);
|
|
1889
|
+
await kbStore.initialize();
|
|
1890
|
+
let queryRewriter;
|
|
1891
|
+
try {
|
|
1892
|
+
queryRewriter = new QueryRewriter({ model: llm.getModel() });
|
|
1893
|
+
} catch {}
|
|
1894
|
+
const retrieval = new RetrievalPipeline(kbStore, embedder, {
|
|
1895
|
+
faqThreshold: .85,
|
|
1896
|
+
queryRewriter
|
|
1897
|
+
});
|
|
1898
|
+
kbRuntime = {
|
|
1899
|
+
retrieve: async (query) => retrieval.retrieve(query),
|
|
1900
|
+
getStats: async () => kbStore.getStats()
|
|
1901
|
+
};
|
|
1902
|
+
if (opts.debug) clack.log.info("[debug] Knowledge Base loaded");
|
|
1903
|
+
} catch (e) {
|
|
1904
|
+
clack.log.warn(`Knowledge Base configured but failed to load: ${e?.message}. Continuing without KB.`);
|
|
1905
|
+
}
|
|
1906
|
+
const agentsDir = `${process.cwd()}/agents`;
|
|
1907
|
+
if (fs.existsSync(agentsDir)) {
|
|
1908
|
+
const definitions = await new AgentLoader(process.cwd()).loadAll();
|
|
1909
|
+
if (definitions.length === 0) clack.log.warn("agents/ directory found but no valid agent definitions. Falling back to default agent.");
|
|
1910
|
+
else {
|
|
1911
|
+
for (const def of definitions) {
|
|
1912
|
+
const agentTools = def.config.skills ? skillInstances.filter((skill) => def.config.skills.includes(skill.name)).flatMap((skill) => Object.values(skill.tools)) : allTools;
|
|
1913
|
+
applyLLMOverride(os.createAgent({
|
|
1914
|
+
...def.config,
|
|
1915
|
+
tools: agentTools
|
|
1916
|
+
}), llm, agentTools, {
|
|
1917
|
+
systemPrompt: def.systemPrompt,
|
|
1918
|
+
kbRuntime,
|
|
1919
|
+
useKB: def.config.knowledgeBase,
|
|
1920
|
+
guardrails: def.config.guardrails,
|
|
1921
|
+
promptSkills: getPromptSkills(def.config.skills)
|
|
1922
|
+
});
|
|
1923
|
+
if (opts.debug) clack.log.info(`[debug] Loaded agent: ${def.config.name}` + (def.config.skills ? ` (skills: ${def.config.skills.join(", ")})` : "") + (def.config.knowledgeBase ? " (KB)" : ""));
|
|
1924
|
+
}
|
|
1925
|
+
if (opts.debug) clack.log.info(`[debug] Loaded ${definitions.length} agent(s) from agents/`);
|
|
1926
|
+
os.on("message:processed", (event) => {
|
|
1927
|
+
os.addAssistantMessage(event.customer.id, event.response.text);
|
|
1928
|
+
if (opts.debug) clack.log.info(`[debug] Agent: ${event.agent}, intent: ${event.intent?.name || "default"}, ${event.duration}ms`);
|
|
1929
|
+
});
|
|
1930
|
+
return {
|
|
1931
|
+
os,
|
|
1932
|
+
provider,
|
|
1933
|
+
llm,
|
|
1934
|
+
kbStore
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
applyLLMOverride(os.createAgent({
|
|
1939
|
+
name: "support",
|
|
1940
|
+
purpose: "Customer support agent",
|
|
1941
|
+
personality: "Friendly, helpful, and professional",
|
|
1942
|
+
triggers: ["*"],
|
|
1943
|
+
tools: allTools,
|
|
1944
|
+
...kbRuntime && { knowledgeBase: true }
|
|
1945
|
+
}), llm, allTools, {
|
|
1946
|
+
kbRuntime,
|
|
1947
|
+
promptSkills: getPromptSkills()
|
|
1948
|
+
});
|
|
1949
|
+
os.on("message:processed", (event) => {
|
|
1950
|
+
os.addAssistantMessage(event.customer.id, event.response.text);
|
|
1951
|
+
if (opts.debug) clack.log.info(`[debug] Agent: ${event.agent}, intent: ${event.intent?.name || "default"}, ${event.duration}ms`);
|
|
1952
|
+
});
|
|
1953
|
+
return {
|
|
1954
|
+
os,
|
|
1955
|
+
provider,
|
|
1956
|
+
llm,
|
|
1957
|
+
kbStore
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
function registerChatCommand(program) {
|
|
1961
|
+
program.command("chat").description("Chat with your agent (full pipeline REPL)").option("-m, --message <text>", "One-shot mode: send a single message and exit").option("--debug", "Show intent, agent selection, and KB retrieval details").option("--no-kb", "Skip Knowledge Base context injection").option("--no-tools", "Disable tool calling").action(async (opts) => {
|
|
1962
|
+
if (!configExists()) {
|
|
1963
|
+
clack.log.error("No configuration found. Run \"operor setup\" first.");
|
|
1964
|
+
process.exit(1);
|
|
1965
|
+
}
|
|
1966
|
+
clack.intro("Operor — Chat");
|
|
1967
|
+
const spinner = clack.spinner();
|
|
1968
|
+
spinner.start("Loading pipeline...");
|
|
1969
|
+
let pipeline;
|
|
1970
|
+
try {
|
|
1971
|
+
pipeline = await buildPipeline(opts);
|
|
1972
|
+
} catch (error) {
|
|
1973
|
+
spinner.stop("Failed to load pipeline");
|
|
1974
|
+
clack.log.error(error.message);
|
|
1975
|
+
process.exit(1);
|
|
1976
|
+
}
|
|
1977
|
+
const { os, provider, kbStore } = pipeline;
|
|
1978
|
+
await os.start();
|
|
1979
|
+
spinner.stop("Pipeline ready");
|
|
1980
|
+
const cleanup = () => {
|
|
1981
|
+
if (kbStore) kbStore.close();
|
|
1982
|
+
};
|
|
1983
|
+
if (opts.message) {
|
|
1984
|
+
const responsePromise = new Promise((resolve) => {
|
|
1985
|
+
os.on("message:processed", (event) => {
|
|
1986
|
+
resolve(event.response.text);
|
|
1987
|
+
});
|
|
1988
|
+
});
|
|
1989
|
+
provider.simulateIncomingMessage("+cli-user", opts.message);
|
|
1990
|
+
const response = await responsePromise;
|
|
1991
|
+
console.log(response);
|
|
1992
|
+
cleanup();
|
|
1993
|
+
clack.outro("");
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
clack.log.info("Type your message and press Enter. Type \"exit\" or Ctrl+C to quit.\n");
|
|
1997
|
+
const rl = readline.createInterface({
|
|
1998
|
+
input: process.stdin,
|
|
1999
|
+
output: process.stdout
|
|
2000
|
+
});
|
|
2001
|
+
const askQuestion = () => {
|
|
2002
|
+
rl.question("You: ", async (input) => {
|
|
2003
|
+
const trimmed = input.trim();
|
|
2004
|
+
if (!trimmed || trimmed === "exit" || trimmed === "quit") {
|
|
2005
|
+
rl.close();
|
|
2006
|
+
cleanup();
|
|
2007
|
+
clack.outro("Goodbye");
|
|
2008
|
+
return;
|
|
2009
|
+
}
|
|
2010
|
+
const responsePromise = new Promise((resolve) => {
|
|
2011
|
+
const handler = (event) => {
|
|
2012
|
+
os.removeListener("message:processed", handler);
|
|
2013
|
+
resolve(event.response.text);
|
|
2014
|
+
};
|
|
2015
|
+
os.on("message:processed", handler);
|
|
2016
|
+
});
|
|
2017
|
+
provider.simulateIncomingMessage("+cli-user", trimmed);
|
|
2018
|
+
try {
|
|
2019
|
+
const response = await responsePromise;
|
|
2020
|
+
console.log(`\nAgent: ${response}\n`);
|
|
2021
|
+
} catch (error) {
|
|
2022
|
+
console.log(`\nError: ${error.message}\n`);
|
|
2023
|
+
}
|
|
2024
|
+
askQuestion();
|
|
2025
|
+
});
|
|
2026
|
+
};
|
|
2027
|
+
rl.on("close", () => {
|
|
2028
|
+
cleanup();
|
|
2029
|
+
clack.outro("Goodbye");
|
|
2030
|
+
process.exit(0);
|
|
2031
|
+
});
|
|
2032
|
+
askQuestion();
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
//#endregion
|
|
2037
|
+
//#region src/commands/init.ts
|
|
2038
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
2039
|
+
function getTemplatesDir() {
|
|
2040
|
+
let dir = __dirname;
|
|
2041
|
+
for (let i = 0; i < 5; i++) {
|
|
2042
|
+
const candidate = join(dir, "templates", "agents");
|
|
2043
|
+
if (existsSync(candidate)) return candidate;
|
|
2044
|
+
dir = dirname(dir);
|
|
2045
|
+
}
|
|
2046
|
+
throw new Error("Could not find templates directory. Ensure packages/cli/templates/agents/ exists.");
|
|
2047
|
+
}
|
|
2048
|
+
const TEMPLATES = [
|
|
2049
|
+
"customer-support",
|
|
2050
|
+
"sales",
|
|
2051
|
+
"faq-bot"
|
|
2052
|
+
];
|
|
2053
|
+
async function copyDir(src, dest) {
|
|
2054
|
+
await mkdir(dest, { recursive: true });
|
|
2055
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
2056
|
+
for (const entry of entries) {
|
|
2057
|
+
const srcPath = join(src, entry.name);
|
|
2058
|
+
const destPath = join(dest, entry.name);
|
|
2059
|
+
if (entry.isDirectory()) await copyDir(srcPath, destPath);
|
|
2060
|
+
else await cp(srcPath, destPath);
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
async function dirExists(path) {
|
|
2064
|
+
try {
|
|
2065
|
+
await access(path);
|
|
2066
|
+
return true;
|
|
2067
|
+
} catch {
|
|
2068
|
+
return false;
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
async function runInit(templateArg) {
|
|
2072
|
+
clack.intro("Operor — Initialize Agent");
|
|
2073
|
+
const templatesDir = getTemplatesDir();
|
|
2074
|
+
let template;
|
|
2075
|
+
if (templateArg && TEMPLATES.includes(templateArg)) template = templateArg;
|
|
2076
|
+
else if (templateArg === "blank") {
|
|
2077
|
+
const name = await clack.text({
|
|
2078
|
+
message: "Agent name:",
|
|
2079
|
+
placeholder: "my-agent",
|
|
2080
|
+
validate: (v) => v.length === 0 ? "Name is required" : void 0
|
|
2081
|
+
});
|
|
2082
|
+
if (clack.isCancel(name)) {
|
|
2083
|
+
clack.cancel("Cancelled.");
|
|
2084
|
+
process.exit(0);
|
|
2085
|
+
}
|
|
2086
|
+
const agentDir = join(process.cwd(), "agents", name);
|
|
2087
|
+
if (await dirExists(agentDir)) {
|
|
2088
|
+
clack.log.error(`Directory already exists: agents/${name}`);
|
|
2089
|
+
process.exit(1);
|
|
2090
|
+
}
|
|
2091
|
+
await mkdir(agentDir, { recursive: true });
|
|
2092
|
+
const defaultsDir = join(templatesDir, "_defaults");
|
|
2093
|
+
if (await dirExists(defaultsDir)) {
|
|
2094
|
+
const defaultsTarget = join(process.cwd(), "agents", "_defaults");
|
|
2095
|
+
if (!await dirExists(defaultsTarget)) {
|
|
2096
|
+
await copyDir(defaultsDir, defaultsTarget);
|
|
2097
|
+
clack.log.info("Created agents/_defaults/");
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
clack.log.success(`Created blank agent at agents/${name}/`);
|
|
2101
|
+
clack.log.info("Add INSTRUCTIONS.md, IDENTITY.md, and SOUL.md to configure your agent.");
|
|
2102
|
+
clack.outro("Done!");
|
|
2103
|
+
return;
|
|
2104
|
+
} else {
|
|
2105
|
+
const choice = await clack.select({
|
|
2106
|
+
message: "Choose a template:",
|
|
2107
|
+
options: [
|
|
2108
|
+
{
|
|
2109
|
+
value: "customer-support",
|
|
2110
|
+
label: "Customer Support",
|
|
2111
|
+
hint: "E-commerce support with Shopify"
|
|
2112
|
+
},
|
|
2113
|
+
{
|
|
2114
|
+
value: "sales",
|
|
2115
|
+
label: "Sales",
|
|
2116
|
+
hint: "Lead qualification and demo booking"
|
|
2117
|
+
},
|
|
2118
|
+
{
|
|
2119
|
+
value: "faq-bot",
|
|
2120
|
+
label: "FAQ Bot",
|
|
2121
|
+
hint: "Simple knowledge base Q&A"
|
|
2122
|
+
},
|
|
2123
|
+
{
|
|
2124
|
+
value: "blank",
|
|
2125
|
+
label: "Blank",
|
|
2126
|
+
hint: "Empty agent directory"
|
|
2127
|
+
}
|
|
2128
|
+
]
|
|
2129
|
+
});
|
|
2130
|
+
if (clack.isCancel(choice)) {
|
|
2131
|
+
clack.cancel("Cancelled.");
|
|
2132
|
+
process.exit(0);
|
|
2133
|
+
}
|
|
2134
|
+
if (choice === "blank") {
|
|
2135
|
+
await runInit("blank");
|
|
2136
|
+
return;
|
|
2137
|
+
}
|
|
2138
|
+
template = choice;
|
|
2139
|
+
}
|
|
2140
|
+
const name = await clack.text({
|
|
2141
|
+
message: "Agent name:",
|
|
2142
|
+
placeholder: template,
|
|
2143
|
+
defaultValue: template,
|
|
2144
|
+
validate: (v) => v.length === 0 ? "Name is required" : void 0
|
|
2145
|
+
});
|
|
2146
|
+
if (clack.isCancel(name)) {
|
|
2147
|
+
clack.cancel("Cancelled.");
|
|
2148
|
+
process.exit(0);
|
|
2149
|
+
}
|
|
2150
|
+
const agentName = name;
|
|
2151
|
+
const agentDir = join(process.cwd(), "agents", agentName);
|
|
2152
|
+
if (await dirExists(agentDir)) {
|
|
2153
|
+
clack.log.error(`Directory already exists: agents/${agentName}`);
|
|
2154
|
+
process.exit(1);
|
|
2155
|
+
}
|
|
2156
|
+
const templateDir = join(templatesDir, template);
|
|
2157
|
+
if (!await dirExists(templateDir)) {
|
|
2158
|
+
clack.log.error(`Template not found: ${template}`);
|
|
2159
|
+
process.exit(1);
|
|
2160
|
+
}
|
|
2161
|
+
await copyDir(templateDir, agentDir);
|
|
2162
|
+
clack.log.success(`Created agent from "${template}" template at agents/${agentName}/`);
|
|
2163
|
+
const defaultsDir = join(templatesDir, "_defaults");
|
|
2164
|
+
if (await dirExists(defaultsDir)) {
|
|
2165
|
+
const defaultsTarget = join(process.cwd(), "agents", "_defaults");
|
|
2166
|
+
if (!await dirExists(defaultsTarget)) {
|
|
2167
|
+
await copyDir(defaultsDir, defaultsTarget);
|
|
2168
|
+
clack.log.info("Created agents/_defaults/");
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
const userMdSrc = join(templatesDir, "_defaults", "USER.md");
|
|
2172
|
+
const userMdDest = join(process.cwd(), "USER.md");
|
|
2173
|
+
if (await dirExists(userMdSrc) && !await dirExists(userMdDest)) {
|
|
2174
|
+
await cp(userMdSrc, userMdDest);
|
|
2175
|
+
clack.log.info("Created USER.md");
|
|
2176
|
+
}
|
|
2177
|
+
clack.outro("Done! Edit the files in agents/" + agentName + "/ to customize your agent.");
|
|
2178
|
+
}
|
|
2179
|
+
function registerInitCommand(program) {
|
|
2180
|
+
program.command("init [template]").description("Initialize a new agent from a template (customer-support, sales, faq-bot, blank)").action(async (template) => {
|
|
2181
|
+
await runInit(template);
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
//#endregion
|
|
2186
|
+
//#region src/index.ts
|
|
2187
|
+
const program = new Command();
|
|
2188
|
+
program.name("operor").description("Operor - AI Agent Operating System").version("0.1.0");
|
|
2189
|
+
program.command("setup").description("Run the interactive setup wizard").option("--quick", "Quick setup with sensible defaults (3 questions)").action(async (opts) => {
|
|
2190
|
+
if (opts.quick) await runQuickSetup();
|
|
2191
|
+
else await runSetup();
|
|
2192
|
+
await startOperor();
|
|
2193
|
+
});
|
|
2194
|
+
program.command("start").description("Start Operor with existing configuration").action(async () => {
|
|
2195
|
+
if (!configExists()) {
|
|
2196
|
+
console.error("No configuration found. Run \"operor setup\" first.");
|
|
2197
|
+
process.exit(1);
|
|
2198
|
+
}
|
|
2199
|
+
await startOperor();
|
|
2200
|
+
});
|
|
2201
|
+
program.command("status").description("Show Operor configuration status").action(async () => {
|
|
2202
|
+
const { runStatus } = await import("./status-D6LIZvQa.js");
|
|
2203
|
+
await runStatus();
|
|
2204
|
+
});
|
|
2205
|
+
program.command("doctor").description("Validate Operor configuration and test connections").action(async () => {
|
|
2206
|
+
const { runDoctor } = await import("./doctor-98gPl743.js");
|
|
2207
|
+
await runDoctor();
|
|
2208
|
+
});
|
|
2209
|
+
program.command("test").description("Run Operor test scenarios").option("--csv <path>", "Load test cases from a CSV file").option("--tag <tag>", "Filter test cases by tag").option("--report <path>", "Save JSON results to file").option("--real", "Use real skills instead of mocks").option("--allow-writes", "Allow write operations with real skills").option("--dry-run", "Real reads, mock writes").action(async (opts) => {
|
|
2210
|
+
const { runTest } = await import("./test-DYjkxbtK.js");
|
|
2211
|
+
await runTest(opts);
|
|
2212
|
+
});
|
|
2213
|
+
program.command("test-suite <file>").description("Run a test suite from a CSV or JSON file").option("--strategy <type>", "Evaluation strategy: exact, contains, semantic", "contains").option("--timeout <ms>", "Per-test timeout in milliseconds", parseInt).option("--parallel", "Run tests in parallel").option("--verbose", "Show detailed output for failed tests").option("--json", "Output results as JSON").option("--real", "Use real skills instead of mocks").option("--allow-writes", "Allow write operations with real skills").option("--dry-run", "Real reads, mock writes").option("--llm", "Use LLM-based agent processing instead of pattern matcher").action(async (file, opts) => {
|
|
2214
|
+
const { runTestSuite } = await import("./test-suite-D8H_5uKs.js");
|
|
2215
|
+
await runTestSuite(file, opts);
|
|
2216
|
+
});
|
|
2217
|
+
program.command("converse [scenario]").description("Run multi-turn conversation test scenarios").option("--file <path>", "Load scenarios from a JSON file").option("--turns <n>", "Override max turns per scenario", parseInt).option("--persona <style>", "Override persona (polite|frustrated|confused|terse|verbose)").option("--verbose", "Show full conversation transcripts").option("--json", "Output results as JSON").option("--real", "Use real skills instead of mocks").option("--allow-writes", "Allow write operations with real skills").option("--dry-run", "Real reads, mock writes").action(async (scenario, opts) => {
|
|
2218
|
+
const { runConverse } = await import("./converse-C_PB7-JH.js");
|
|
2219
|
+
await runConverse({
|
|
2220
|
+
scenario,
|
|
2221
|
+
...opts
|
|
2222
|
+
});
|
|
2223
|
+
});
|
|
2224
|
+
program.command("simulate").description("Run pre-deployment simulation (test suites + conversation scenarios)").option("--tests <files...>", "CSV/JSON test suite files to include").option("--scenarios <names...>", "Scenario names to run (or \"all\" for built-in)").option("--conversations <n>", "Number of conversations to run (default: 10)", parseInt).option("--strategy <type>", "Evaluation strategy: exact, similarity, llm_judge", "similarity").option("--real", "Use real skills instead of mocks").option("--allow-writes", "Allow write operations with real skills").option("--dry-run", "Real reads, mock writes").option("--timeout <ms>", "Per-test timeout in milliseconds", parseInt).option("--parallel", "Run tests in parallel").option("--json", "Output full report as JSON").option("--report <file>", "Save detailed JSON report to file").action(async (opts) => {
|
|
2225
|
+
const { runSimulate } = await import("./simulate-BKv62GJc.js");
|
|
2226
|
+
await runSimulate({
|
|
2227
|
+
...opts,
|
|
2228
|
+
output: opts.report
|
|
2229
|
+
});
|
|
2230
|
+
});
|
|
2231
|
+
registerKBCommand(program);
|
|
2232
|
+
registerAnalyticsCommand(program);
|
|
2233
|
+
registerHistoryCommand(program);
|
|
2234
|
+
registerChatCommand(program);
|
|
2235
|
+
registerInitCommand(program);
|
|
2236
|
+
program.command("reset").description("Delete all data (KB, memory, analytics, copilot, skills, auth) to start fresh").option("--yes", "Skip confirmation prompt").option("--keep-config", "Keep .env and mcp.json, only delete databases").action(async (opts) => {
|
|
2237
|
+
const { runReset } = await import("./reset-DT8SBgFS.js");
|
|
2238
|
+
await runReset(opts);
|
|
2239
|
+
});
|
|
2240
|
+
program.command("vibe [action]").description("AI-powered agent copilot for creating and customizing agents").option("--agent <name>", "Target a specific agent").action(async (action, opts) => {
|
|
2241
|
+
const { runVibe } = await import("./vibe-Bl_js3Jo.js");
|
|
2242
|
+
await runVibe({
|
|
2243
|
+
action,
|
|
2244
|
+
agent: opts.agent
|
|
2245
|
+
});
|
|
2246
|
+
});
|
|
2247
|
+
const configCmd = program.command("config").description("View and modify configuration");
|
|
2248
|
+
configCmd.command("show").description("Show current config").action(async () => {
|
|
2249
|
+
const { showConfig } = await import("./config-Bn2pbORi.js");
|
|
2250
|
+
showConfig();
|
|
2251
|
+
});
|
|
2252
|
+
configCmd.command("set <key> <value>").description("Set a config value").action(async (k, v) => {
|
|
2253
|
+
const { setConfigValue } = await import("./config-Bn2pbORi.js");
|
|
2254
|
+
setConfigValue(k, v);
|
|
2255
|
+
});
|
|
2256
|
+
configCmd.command("unset <key>").description("Remove a config value").action(async (k) => {
|
|
2257
|
+
const { unsetConfigValue } = await import("./config-Bn2pbORi.js");
|
|
2258
|
+
unsetConfigValue(k);
|
|
2259
|
+
});
|
|
2260
|
+
program.action(async () => {
|
|
2261
|
+
if (!configExists()) await runSetup();
|
|
2262
|
+
await startOperor();
|
|
2263
|
+
});
|
|
2264
|
+
program.parse();
|
|
2265
|
+
|
|
2266
|
+
//#endregion
|
|
2267
|
+
export { readConfig as n, writeConfig as r, configExists as t };
|
|
2268
|
+
//# sourceMappingURL=index.js.map
|