@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
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import * as clack from '@clack/prompts';
|
|
4
|
+
import * as readline from 'node:readline';
|
|
5
|
+
import type { Skill, Tool } from '@operor/core';
|
|
6
|
+
import { readConfig, configExists } from '../config.js';
|
|
7
|
+
import { applyLLMOverride } from './llm-override.js';
|
|
8
|
+
|
|
9
|
+
interface ChatOptions {
|
|
10
|
+
message?: string;
|
|
11
|
+
debug?: boolean;
|
|
12
|
+
kb?: boolean;
|
|
13
|
+
tools?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function buildPipeline(opts: ChatOptions) {
|
|
17
|
+
const config = readConfig();
|
|
18
|
+
|
|
19
|
+
if (!config.LLM_PROVIDER || !config.LLM_API_KEY) {
|
|
20
|
+
clack.log.error('LLM_PROVIDER and LLM_API_KEY required. Run "operor setup" first.');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { Operor, LLMIntentClassifier, AgentLoader } = await import('@operor/core');
|
|
25
|
+
const { AIProvider } = await import('@operor/llm');
|
|
26
|
+
const { MockProvider } = await import('@operor/provider-mock');
|
|
27
|
+
|
|
28
|
+
const llm = new AIProvider({
|
|
29
|
+
provider: config.LLM_PROVIDER as 'openai' | 'anthropic' | 'google' | 'groq' | 'ollama',
|
|
30
|
+
apiKey: config.LLM_API_KEY,
|
|
31
|
+
model: config.LLM_MODEL,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
let intentClassifier: any;
|
|
35
|
+
if (config.INTENT_CLASSIFIER === 'llm') {
|
|
36
|
+
intentClassifier = new LLMIntentClassifier(llm);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let skillsModule: any;
|
|
40
|
+
try { skillsModule = await import('@operor/skills'); } catch {}
|
|
41
|
+
|
|
42
|
+
const os = new Operor({
|
|
43
|
+
debug: !!opts.debug,
|
|
44
|
+
...(intentClassifier && { intentClassifier }),
|
|
45
|
+
...(skillsModule && { skillsModule }),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const provider = new MockProvider();
|
|
49
|
+
os.addProvider(provider);
|
|
50
|
+
|
|
51
|
+
// Load MCP skills + tools
|
|
52
|
+
const skillInstances: Skill[] = [];
|
|
53
|
+
|
|
54
|
+
if (config.SKILLS_ENABLED !== 'false') {
|
|
55
|
+
try {
|
|
56
|
+
const { SkillManager, loadSkillsConfig } = await import('@operor/skills');
|
|
57
|
+
const skillsConfig = loadSkillsConfig();
|
|
58
|
+
const skillManager = new SkillManager();
|
|
59
|
+
const skills = await skillManager.initialize(skillsConfig);
|
|
60
|
+
for (const skill of skills) {
|
|
61
|
+
os.addSkill(skill);
|
|
62
|
+
skillInstances.push(skill);
|
|
63
|
+
if (opts.debug) clack.log.info(`[debug] MCP skill loaded: ${skill.name}`);
|
|
64
|
+
}
|
|
65
|
+
} catch (error: any) {
|
|
66
|
+
if (opts.debug) clack.log.warn(`[debug] MCP skills not available: ${error.message}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const allTools: Tool[] = opts.tools !== false
|
|
71
|
+
? skillInstances.flatMap((skill) => Object.values(skill.tools))
|
|
72
|
+
: [];
|
|
73
|
+
|
|
74
|
+
// Helper: get prompt skill name+content for an agent (filtered by skills list)
|
|
75
|
+
const getPromptSkills = (agentSkillNames?: string[]): Array<{name: string, content: string}> => {
|
|
76
|
+
return skillInstances
|
|
77
|
+
.filter((s): s is any => typeof (s as any).getContent === 'function')
|
|
78
|
+
.filter((s) => !agentSkillNames || agentSkillNames.includes(s.name))
|
|
79
|
+
.map((s) => ({ name: s.name, content: (s as any).getContent() }));
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// KB runtime (optional) — same pattern as start.ts
|
|
83
|
+
let kbRuntime: any;
|
|
84
|
+
let kbStore: any = null;
|
|
85
|
+
if (opts.kb !== false && config.KB_ENABLED === 'true') {
|
|
86
|
+
try {
|
|
87
|
+
const {
|
|
88
|
+
SQLiteKnowledgeStore,
|
|
89
|
+
EmbeddingService,
|
|
90
|
+
TextChunker,
|
|
91
|
+
IngestionPipeline,
|
|
92
|
+
RetrievalPipeline,
|
|
93
|
+
QueryRewriter,
|
|
94
|
+
} = await import('@operor/knowledge');
|
|
95
|
+
|
|
96
|
+
const embedder = new EmbeddingService({
|
|
97
|
+
provider: (config.KB_EMBEDDING_PROVIDER as any) || 'openai',
|
|
98
|
+
apiKey: config.KB_EMBEDDING_API_KEY || config.LLM_API_KEY,
|
|
99
|
+
model: config.KB_EMBEDDING_MODEL,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
kbStore = new SQLiteKnowledgeStore(
|
|
103
|
+
config.KB_DB_PATH || './knowledge.db',
|
|
104
|
+
embedder.dimensions,
|
|
105
|
+
);
|
|
106
|
+
await kbStore.initialize();
|
|
107
|
+
|
|
108
|
+
// Setup optional LLM query rewriter
|
|
109
|
+
let queryRewriter: InstanceType<typeof QueryRewriter> | undefined;
|
|
110
|
+
try {
|
|
111
|
+
queryRewriter = new QueryRewriter({ model: llm.getModel() });
|
|
112
|
+
} catch { /* LLM not available for rewriting */ }
|
|
113
|
+
|
|
114
|
+
const retrieval = new RetrievalPipeline(kbStore, embedder, {
|
|
115
|
+
faqThreshold: 0.85,
|
|
116
|
+
queryRewriter,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
kbRuntime = {
|
|
120
|
+
retrieve: async (query: string) => retrieval.retrieve(query),
|
|
121
|
+
getStats: async () => kbStore.getStats(),
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
if (opts.debug) clack.log.info('[debug] Knowledge Base loaded');
|
|
125
|
+
} catch (e: any) {
|
|
126
|
+
clack.log.warn(`Knowledge Base configured but failed to load: ${e?.message}. Continuing without KB.`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check if agents/ directory exists for file-based agent definitions
|
|
131
|
+
const agentsDir = `${process.cwd()}/agents`;
|
|
132
|
+
const hasAgentsDir = fs.existsSync(agentsDir);
|
|
133
|
+
|
|
134
|
+
if (hasAgentsDir) {
|
|
135
|
+
// File-based agent definitions — same as start.ts
|
|
136
|
+
const loader = new AgentLoader(process.cwd());
|
|
137
|
+
const definitions = await loader.loadAll();
|
|
138
|
+
|
|
139
|
+
if (definitions.length === 0) {
|
|
140
|
+
clack.log.warn('agents/ directory found but no valid agent definitions. Falling back to default agent.');
|
|
141
|
+
} else {
|
|
142
|
+
for (const def of definitions) {
|
|
143
|
+
const agentTools: Tool[] = def.config.skills
|
|
144
|
+
? skillInstances
|
|
145
|
+
.filter((skill) => def.config.skills!.includes(skill.name))
|
|
146
|
+
.flatMap((skill) => Object.values(skill.tools))
|
|
147
|
+
: allTools;
|
|
148
|
+
|
|
149
|
+
const agent = os.createAgent({
|
|
150
|
+
...def.config,
|
|
151
|
+
tools: agentTools,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
applyLLMOverride(agent, llm, agentTools, {
|
|
155
|
+
systemPrompt: def.systemPrompt,
|
|
156
|
+
kbRuntime,
|
|
157
|
+
useKB: def.config.knowledgeBase,
|
|
158
|
+
guardrails: def.config.guardrails,
|
|
159
|
+
promptSkills: getPromptSkills(def.config.skills),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (opts.debug) {
|
|
163
|
+
clack.log.info(
|
|
164
|
+
`[debug] Loaded agent: ${def.config.name}` +
|
|
165
|
+
(def.config.skills ? ` (skills: ${def.config.skills.join(', ')})` : '') +
|
|
166
|
+
(def.config.knowledgeBase ? ' (KB)' : '')
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (opts.debug) clack.log.info(`[debug] Loaded ${definitions.length} agent(s) from agents/`);
|
|
172
|
+
|
|
173
|
+
// Wire up assistant message tracking
|
|
174
|
+
os.on('message:processed', (event) => {
|
|
175
|
+
os.addAssistantMessage(event.customer.id, event.response.text);
|
|
176
|
+
if (opts.debug) {
|
|
177
|
+
clack.log.info(`[debug] Agent: ${event.agent}, intent: ${event.intent?.name || 'default'}, ${event.duration}ms`);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return { os, provider, llm, kbStore };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Fallback: single default agent (no agents/ directory or empty)
|
|
186
|
+
const agent = os.createAgent({
|
|
187
|
+
name: 'support',
|
|
188
|
+
purpose: 'Customer support agent',
|
|
189
|
+
personality: 'Friendly, helpful, and professional',
|
|
190
|
+
triggers: ['*'],
|
|
191
|
+
tools: allTools,
|
|
192
|
+
...(kbRuntime && { knowledgeBase: true }),
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
applyLLMOverride(agent, llm, allTools, {
|
|
196
|
+
kbRuntime,
|
|
197
|
+
promptSkills: getPromptSkills(),
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Wire up assistant message tracking
|
|
201
|
+
os.on('message:processed', (event) => {
|
|
202
|
+
os.addAssistantMessage(event.customer.id, event.response.text);
|
|
203
|
+
if (opts.debug) {
|
|
204
|
+
clack.log.info(`[debug] Agent: ${event.agent}, intent: ${event.intent?.name || 'default'}, ${event.duration}ms`);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return { os, provider, llm, kbStore };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function registerChatCommand(program: Command): void {
|
|
212
|
+
program
|
|
213
|
+
.command('chat')
|
|
214
|
+
.description('Chat with your agent (full pipeline REPL)')
|
|
215
|
+
.option('-m, --message <text>', 'One-shot mode: send a single message and exit')
|
|
216
|
+
.option('--debug', 'Show intent, agent selection, and KB retrieval details')
|
|
217
|
+
.option('--no-kb', 'Skip Knowledge Base context injection')
|
|
218
|
+
.option('--no-tools', 'Disable tool calling')
|
|
219
|
+
.action(async (opts: ChatOptions) => {
|
|
220
|
+
if (!configExists()) {
|
|
221
|
+
clack.log.error('No configuration found. Run "operor setup" first.');
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
clack.intro('Operor — Chat');
|
|
226
|
+
|
|
227
|
+
const spinner = clack.spinner();
|
|
228
|
+
spinner.start('Loading pipeline...');
|
|
229
|
+
|
|
230
|
+
let pipeline: Awaited<ReturnType<typeof buildPipeline>>;
|
|
231
|
+
try {
|
|
232
|
+
pipeline = await buildPipeline(opts);
|
|
233
|
+
} catch (error: any) {
|
|
234
|
+
spinner.stop('Failed to load pipeline');
|
|
235
|
+
clack.log.error(error.message);
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const { os, provider, kbStore } = pipeline;
|
|
240
|
+
await os.start();
|
|
241
|
+
spinner.stop('Pipeline ready');
|
|
242
|
+
|
|
243
|
+
const cleanup = () => {
|
|
244
|
+
if (kbStore) kbStore.close();
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
if (opts.message) {
|
|
248
|
+
// One-shot mode
|
|
249
|
+
const responsePromise = new Promise<string>((resolve) => {
|
|
250
|
+
os.on('message:processed', (event) => {
|
|
251
|
+
resolve(event.response.text);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
provider.simulateIncomingMessage('+cli-user', opts.message);
|
|
256
|
+
const response = await responsePromise;
|
|
257
|
+
console.log(response);
|
|
258
|
+
cleanup();
|
|
259
|
+
clack.outro('');
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Interactive REPL
|
|
264
|
+
clack.log.info('Type your message and press Enter. Type "exit" or Ctrl+C to quit.\n');
|
|
265
|
+
|
|
266
|
+
const rl = readline.createInterface({
|
|
267
|
+
input: process.stdin,
|
|
268
|
+
output: process.stdout,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const askQuestion = (): void => {
|
|
272
|
+
rl.question('You: ', async (input) => {
|
|
273
|
+
const trimmed = input.trim();
|
|
274
|
+
if (!trimmed || trimmed === 'exit' || trimmed === 'quit') {
|
|
275
|
+
rl.close();
|
|
276
|
+
cleanup();
|
|
277
|
+
clack.outro('Goodbye');
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const responsePromise = new Promise<string>((resolve) => {
|
|
282
|
+
const handler = (event: any) => {
|
|
283
|
+
os.removeListener('message:processed', handler);
|
|
284
|
+
resolve(event.response.text);
|
|
285
|
+
};
|
|
286
|
+
os.on('message:processed', handler);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
provider.simulateIncomingMessage('+cli-user', trimmed);
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
const response = await responsePromise;
|
|
293
|
+
console.log(`\nAgent: ${response}\n`);
|
|
294
|
+
} catch (error: any) {
|
|
295
|
+
console.log(`\nError: ${error.message}\n`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
askQuestion();
|
|
299
|
+
});
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
rl.on('close', () => {
|
|
303
|
+
cleanup();
|
|
304
|
+
clack.outro('Goodbye');
|
|
305
|
+
process.exit(0);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
askQuestion();
|
|
309
|
+
});
|
|
310
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { readConfig, writeConfig, configExists } from '../config.js';
|
|
2
|
+
|
|
3
|
+
export function showConfig(): void {
|
|
4
|
+
if (!configExists()) {
|
|
5
|
+
console.error('No configuration found. Run "operor setup" first.');
|
|
6
|
+
process.exit(1);
|
|
7
|
+
}
|
|
8
|
+
const config = readConfig();
|
|
9
|
+
for (const [key, value] of Object.entries(config)) {
|
|
10
|
+
const display = key.includes('KEY') || key.includes('TOKEN') || key.includes('SECRET')
|
|
11
|
+
? maskValue(value || '')
|
|
12
|
+
: value;
|
|
13
|
+
console.log(` ${key}=${display}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function setConfigValue(key: string, value: string): void {
|
|
18
|
+
const config = readConfig();
|
|
19
|
+
config[key] = value;
|
|
20
|
+
writeConfig(config);
|
|
21
|
+
console.log(` Set ${key}=${key.includes('KEY') ? maskValue(value) : value}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function unsetConfigValue(key: string): void {
|
|
25
|
+
const config = readConfig();
|
|
26
|
+
delete config[key];
|
|
27
|
+
writeConfig(config);
|
|
28
|
+
console.log(` Removed ${key}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function maskValue(val: string): string {
|
|
32
|
+
if (val.length <= 8) return '••••••••';
|
|
33
|
+
return val.slice(0, 4) + '••••' + val.slice(-4);
|
|
34
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { readConfig } from '../config.js';
|
|
2
|
+
import { formatTimestamp } from '../utils.js';
|
|
3
|
+
|
|
4
|
+
export async function runConverse(options: {
|
|
5
|
+
scenario?: string;
|
|
6
|
+
file?: string;
|
|
7
|
+
turns?: number;
|
|
8
|
+
persona?: string;
|
|
9
|
+
verbose?: boolean;
|
|
10
|
+
json?: boolean;
|
|
11
|
+
real?: boolean;
|
|
12
|
+
allowWrites?: boolean;
|
|
13
|
+
dryRun?: boolean;
|
|
14
|
+
}): Promise<void> {
|
|
15
|
+
const config = readConfig();
|
|
16
|
+
const { Operor } = await import('@operor/core');
|
|
17
|
+
const { MockProvider } = await import('@operor/provider-mock');
|
|
18
|
+
const {
|
|
19
|
+
MockShopifySkill,
|
|
20
|
+
CustomerSimulator,
|
|
21
|
+
ConversationEvaluator,
|
|
22
|
+
ConversationRunner,
|
|
23
|
+
ECOMMERCE_SCENARIOS,
|
|
24
|
+
SkillTestHarness,
|
|
25
|
+
} = await import('@operor/testing');
|
|
26
|
+
|
|
27
|
+
// Set up LLM if configured
|
|
28
|
+
let llm: any;
|
|
29
|
+
if (config.LLM_PROVIDER && config.LLM_API_KEY) {
|
|
30
|
+
const { AIProvider } = await import('@operor/llm');
|
|
31
|
+
llm = new AIProvider({
|
|
32
|
+
provider: config.LLM_PROVIDER as any,
|
|
33
|
+
apiKey: config.LLM_API_KEY,
|
|
34
|
+
model: config.LLM_MODEL,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Determine which scenarios to run
|
|
39
|
+
let scenarios = [...ECOMMERCE_SCENARIOS];
|
|
40
|
+
|
|
41
|
+
if (options.file) {
|
|
42
|
+
const fs = await import('fs');
|
|
43
|
+
try {
|
|
44
|
+
const raw = fs.readFileSync(options.file, 'utf-8');
|
|
45
|
+
scenarios = JSON.parse(raw);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.error(`Failed to load scenarios from ${options.file}:`, err);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (options.scenario) {
|
|
53
|
+
const match = scenarios.filter(
|
|
54
|
+
(s) => s.name.toLowerCase().includes(options.scenario!.toLowerCase()) ||
|
|
55
|
+
s.id.toLowerCase().includes(options.scenario!.toLowerCase())
|
|
56
|
+
);
|
|
57
|
+
if (match.length === 0) {
|
|
58
|
+
console.error(`No scenario matching "${options.scenario}". Available:`);
|
|
59
|
+
scenarios.forEach((s) => console.error(` - ${s.id}: ${s.name}`));
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
scenarios = match;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Apply overrides
|
|
66
|
+
if (options.turns) {
|
|
67
|
+
scenarios = scenarios.map((s) => ({ ...s, maxTurns: options.turns! }));
|
|
68
|
+
}
|
|
69
|
+
if (options.persona) {
|
|
70
|
+
scenarios = scenarios.map((s) => ({ ...s, persona: options.persona! }));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Set up Operor with mocks
|
|
74
|
+
const os = new Operor({ debug: false, batchWindowMs: 0 });
|
|
75
|
+
const provider = new MockProvider();
|
|
76
|
+
const shopify = new MockShopifySkill();
|
|
77
|
+
|
|
78
|
+
// Wrap skill with safety harness if --real or --dry-run
|
|
79
|
+
let skill: any = shopify;
|
|
80
|
+
if (options.real || options.dryRun) {
|
|
81
|
+
skill = new SkillTestHarness(shopify, {
|
|
82
|
+
allowWrites: options.allowWrites ?? false,
|
|
83
|
+
dryRun: options.dryRun ?? false,
|
|
84
|
+
});
|
|
85
|
+
await skill.authenticate();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await os.addProvider(provider);
|
|
89
|
+
await os.addSkill(skill);
|
|
90
|
+
|
|
91
|
+
const allTools = [shopify.tools.get_order, shopify.tools.create_discount, shopify.tools.search_products];
|
|
92
|
+
|
|
93
|
+
const agent = os.createAgent({
|
|
94
|
+
name: 'Test Agent',
|
|
95
|
+
purpose: 'Handle customer support conversations',
|
|
96
|
+
personality: 'empathetic and solution-focused',
|
|
97
|
+
triggers: ['order_tracking', 'general'],
|
|
98
|
+
tools: allTools,
|
|
99
|
+
rules: [{
|
|
100
|
+
name: 'Auto-compensation',
|
|
101
|
+
condition: async (_ctx: any, toolResults: any[]) => {
|
|
102
|
+
const order = toolResults.find((t) => t.name === 'get_order');
|
|
103
|
+
return order?.success && order.result?.isDelayed && order.result?.delayDays >= 2;
|
|
104
|
+
},
|
|
105
|
+
action: async () => {
|
|
106
|
+
const discount = await shopify.tools.create_discount.execute({ percent: 10, validDays: 30 });
|
|
107
|
+
return { type: 'discount_created', code: discount.code, percent: 10, validDays: 30 };
|
|
108
|
+
},
|
|
109
|
+
}],
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Set up Knowledge Base if enabled
|
|
113
|
+
let kbRuntime: any;
|
|
114
|
+
if (config.KB_ENABLED === 'true') {
|
|
115
|
+
try {
|
|
116
|
+
const { SQLiteKnowledgeStore, EmbeddingService, RetrievalPipeline } = await import('@operor/knowledge');
|
|
117
|
+
const embedder = new EmbeddingService({
|
|
118
|
+
provider: (config.KB_EMBEDDING_PROVIDER || config.LLM_PROVIDER) as any,
|
|
119
|
+
apiKey: config.KB_EMBEDDING_API_KEY || config.LLM_API_KEY || '',
|
|
120
|
+
model: config.KB_EMBEDDING_MODEL,
|
|
121
|
+
});
|
|
122
|
+
const kbStore = new SQLiteKnowledgeStore(config.KB_DB_PATH || './knowledge.db', embedder.dimensions);
|
|
123
|
+
await kbStore.initialize();
|
|
124
|
+
const retrieval = new RetrievalPipeline(kbStore, embedder);
|
|
125
|
+
kbRuntime = { retrieve: (q: string) => retrieval.retrieve(q) };
|
|
126
|
+
console.log('[Operor] 📚 Knowledge Base enabled for conversations');
|
|
127
|
+
} catch (kbError: any) {
|
|
128
|
+
console.warn('[Operor] ⚠️ Failed to initialize Knowledge Base:', kbError.message);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Override agent.process with LLM-based implementation if LLM is configured
|
|
133
|
+
if (llm) {
|
|
134
|
+
const { applyLLMOverride } = await import('./llm-override.js');
|
|
135
|
+
applyLLMOverride(agent, llm, allTools, { kbRuntime });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
await os.start();
|
|
139
|
+
|
|
140
|
+
// Create runner components with LLM if configured
|
|
141
|
+
const customerSimulator = new CustomerSimulator({ llmProvider: llm });
|
|
142
|
+
const conversationEvaluator = new ConversationEvaluator({ llmProvider: llm });
|
|
143
|
+
const runner = new ConversationRunner({
|
|
144
|
+
agentOS: os,
|
|
145
|
+
customerSimulator,
|
|
146
|
+
conversationEvaluator,
|
|
147
|
+
verbose: options.verbose ?? false,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
if (!options.json) {
|
|
151
|
+
console.log(`\n Running ${scenarios.length} conversation scenario(s)...\n`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const results = await runner.runScenarios(scenarios);
|
|
155
|
+
await os.stop();
|
|
156
|
+
|
|
157
|
+
// Output results
|
|
158
|
+
if (options.json) {
|
|
159
|
+
console.log(JSON.stringify(results, null, 2));
|
|
160
|
+
} else {
|
|
161
|
+
let passed = 0;
|
|
162
|
+
let failed = 0;
|
|
163
|
+
|
|
164
|
+
for (const r of results) {
|
|
165
|
+
const icon = r.passed ? '✓' : '✗';
|
|
166
|
+
const status = r.passed ? 'PASS' : 'FAIL';
|
|
167
|
+
console.log(` ${icon} ${r.scenario.name} — ${status} (${r.turns.length} turns, ${r.duration}ms)`);
|
|
168
|
+
|
|
169
|
+
if (!r.passed && r.evaluation.feedback) {
|
|
170
|
+
console.log(` ${r.evaluation.feedback}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (r.passed) passed++;
|
|
174
|
+
else failed++;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log(`\n Results: ${passed} passed, ${failed} failed out of ${results.length}`);
|
|
178
|
+
console.log(` Status: ${failed === 0 ? '✓ ALL PASSED' : '✗ SOME FAILED'}\n`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
process.exit(results.every((r) => r.passed) ? 0 : 1);
|
|
182
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import * as clack from '@clack/prompts';
|
|
2
|
+
import { readConfig, configExists } from '../config.js';
|
|
3
|
+
|
|
4
|
+
export async function runDoctor(): Promise<void> {
|
|
5
|
+
clack.intro('Operor Doctor');
|
|
6
|
+
|
|
7
|
+
if (!configExists()) {
|
|
8
|
+
clack.log.error('.env file not found. Run "operor setup" first.');
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const config = readConfig();
|
|
13
|
+
let allPassed = true;
|
|
14
|
+
|
|
15
|
+
// 1. Check .env exists
|
|
16
|
+
clack.log.success('.env file found');
|
|
17
|
+
|
|
18
|
+
// 2. Validate LLM
|
|
19
|
+
if (config.LLM_PROVIDER && config.LLM_API_KEY) {
|
|
20
|
+
const spinner = clack.spinner();
|
|
21
|
+
spinner.start(`Testing ${config.LLM_PROVIDER} LLM connection...`);
|
|
22
|
+
try {
|
|
23
|
+
const { AIProvider } = await import('@operor/llm');
|
|
24
|
+
const llm = new AIProvider({
|
|
25
|
+
provider: config.LLM_PROVIDER as any,
|
|
26
|
+
apiKey: config.LLM_API_KEY,
|
|
27
|
+
model: config.LLM_MODEL,
|
|
28
|
+
});
|
|
29
|
+
await llm.complete([{ role: 'user', content: 'ping' }], { maxTokens: 5 });
|
|
30
|
+
spinner.stop(`${config.LLM_PROVIDER} LLM: OK`);
|
|
31
|
+
} catch (error: any) {
|
|
32
|
+
spinner.stop(`${config.LLM_PROVIDER} LLM: FAILED — ${error.message}`);
|
|
33
|
+
allPassed = false;
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
clack.log.warning('LLM not configured');
|
|
37
|
+
allPassed = false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 3. Validate MCP Skills
|
|
41
|
+
if (config.SKILLS_ENABLED !== 'false') {
|
|
42
|
+
const skillSpinner = clack.spinner();
|
|
43
|
+
skillSpinner.start('Checking MCP Skills...');
|
|
44
|
+
try {
|
|
45
|
+
const { loadSkillsConfig, SkillManager } = await import('@operor/skills');
|
|
46
|
+
const skillsConfig = loadSkillsConfig();
|
|
47
|
+
const enabledSkills = skillsConfig.skills.filter((s: any) => s.enabled !== false);
|
|
48
|
+
|
|
49
|
+
if (enabledSkills.length === 0) {
|
|
50
|
+
skillSpinner.stop('MCP Skills: No skills configured in mcp.json');
|
|
51
|
+
} else {
|
|
52
|
+
// Validate config only (don't start servers)
|
|
53
|
+
const errors: string[] = [];
|
|
54
|
+
for (const skill of enabledSkills) {
|
|
55
|
+
const error = SkillManager.validateConfig(skill);
|
|
56
|
+
if (error) errors.push(`${skill.name}: ${error}`);
|
|
57
|
+
}
|
|
58
|
+
if (errors.length > 0) {
|
|
59
|
+
skillSpinner.stop(`MCP Skills: ${errors.length} config error(s)`);
|
|
60
|
+
for (const err of errors) clack.log.warning(` ${err}`);
|
|
61
|
+
allPassed = false;
|
|
62
|
+
} else {
|
|
63
|
+
skillSpinner.stop(`MCP Skills: ${enabledSkills.length} skill(s) configured`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} catch (error: any) {
|
|
67
|
+
skillSpinner.stop('MCP Skills: Not available (mcp.json not found or @operor/skills not installed)');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 4. Validate channel credentials
|
|
72
|
+
if (config.CHANNEL === 'telegram' && config.TELEGRAM_BOT_TOKEN) {
|
|
73
|
+
clack.log.success('Telegram bot token configured');
|
|
74
|
+
} else if (config.CHANNEL === 'wati') {
|
|
75
|
+
if (config.WATI_API_TOKEN && config.WATI_TENANT_ID) {
|
|
76
|
+
clack.log.success('WATI credentials configured');
|
|
77
|
+
} else {
|
|
78
|
+
clack.log.error('WATI: missing WATI_API_TOKEN or WATI_TENANT_ID');
|
|
79
|
+
allPassed = false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 5. Knowledge Base checks
|
|
84
|
+
if (config.KB_ENABLED === 'true') {
|
|
85
|
+
const kbSpinner = clack.spinner();
|
|
86
|
+
kbSpinner.start('Checking Knowledge Base...');
|
|
87
|
+
try {
|
|
88
|
+
// Check DB path is writable
|
|
89
|
+
const { writeFileSync, unlinkSync, existsSync } = await import('node:fs');
|
|
90
|
+
const { dirname } = await import('node:path');
|
|
91
|
+
const { mkdirSync } = await import('node:fs');
|
|
92
|
+
const dbPath = config.KB_DB_PATH || './knowledge.db';
|
|
93
|
+
const dir = dirname(dbPath);
|
|
94
|
+
if (!existsSync(dir)) {
|
|
95
|
+
mkdirSync(dir, { recursive: true });
|
|
96
|
+
}
|
|
97
|
+
const testFile = dbPath + '.write-test';
|
|
98
|
+
writeFileSync(testFile, '');
|
|
99
|
+
unlinkSync(testFile);
|
|
100
|
+
|
|
101
|
+
// Check embedding API key (unless ollama)
|
|
102
|
+
if (config.KB_EMBEDDING_PROVIDER !== 'ollama' && !config.KB_EMBEDDING_API_KEY) {
|
|
103
|
+
kbSpinner.stop('Knowledge Base: WARNING — no embedding API key set');
|
|
104
|
+
allPassed = false;
|
|
105
|
+
} else {
|
|
106
|
+
// Try loading sqlite-vec
|
|
107
|
+
try {
|
|
108
|
+
await import('@operor/knowledge');
|
|
109
|
+
kbSpinner.stop('Knowledge Base: OK');
|
|
110
|
+
} catch {
|
|
111
|
+
kbSpinner.stop('Knowledge Base: OK (config valid, @operor/knowledge not installed yet)');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} catch (error: any) {
|
|
115
|
+
kbSpinner.stop(`Knowledge Base: FAILED — ${error.message}`);
|
|
116
|
+
allPassed = false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 6. Analytics checks
|
|
121
|
+
if (config.ANALYTICS_ENABLED !== 'false') {
|
|
122
|
+
const analyticsSpinner = clack.spinner();
|
|
123
|
+
analyticsSpinner.start('Checking Analytics...');
|
|
124
|
+
try {
|
|
125
|
+
const { writeFileSync, unlinkSync, existsSync } = await import('node:fs');
|
|
126
|
+
const { dirname } = await import('node:path');
|
|
127
|
+
const { mkdirSync } = await import('node:fs');
|
|
128
|
+
const dbPath = config.ANALYTICS_DB_PATH || './analytics.db';
|
|
129
|
+
const dir = dirname(dbPath);
|
|
130
|
+
if (!existsSync(dir)) {
|
|
131
|
+
mkdirSync(dir, { recursive: true });
|
|
132
|
+
}
|
|
133
|
+
const testFile = dbPath + '.write-test';
|
|
134
|
+
writeFileSync(testFile, '');
|
|
135
|
+
unlinkSync(testFile);
|
|
136
|
+
analyticsSpinner.stop('Analytics: OK (DB path writable)');
|
|
137
|
+
} catch (error: any) {
|
|
138
|
+
analyticsSpinner.stop(`Analytics: FAILED — ${error.message}`);
|
|
139
|
+
allPassed = false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 7. Check Node.js version
|
|
144
|
+
const nodeVersion = process.versions.node;
|
|
145
|
+
const major = parseInt(nodeVersion.split('.')[0]);
|
|
146
|
+
if (major >= 18) {
|
|
147
|
+
clack.log.success(`Node.js ${nodeVersion}`);
|
|
148
|
+
} else {
|
|
149
|
+
clack.log.error(`Node.js ${nodeVersion} — requires >= 18`);
|
|
150
|
+
allPassed = false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
clack.outro(allPassed ? 'All checks passed!' : 'Some checks failed. Fix the issues above.');
|
|
154
|
+
}
|