@kernel.chat/kbot 3.51.0 → 3.54.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 +43 -9
- package/dist/agent-protocol.test.d.ts +2 -0
- package/dist/agent-protocol.test.d.ts.map +1 -0
- package/dist/agent-protocol.test.js +730 -0
- package/dist/agent-protocol.test.js.map +1 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +34 -10
- package/dist/agent.js.map +1 -1
- package/dist/agents/replit.js +1 -1
- package/dist/auth.js +3 -3
- package/dist/auth.js.map +1 -1
- package/dist/behaviour.d.ts +30 -0
- package/dist/behaviour.d.ts.map +1 -0
- package/dist/behaviour.js +191 -0
- package/dist/behaviour.js.map +1 -0
- package/dist/bench.d.ts +64 -0
- package/dist/bench.d.ts.map +1 -0
- package/dist/bench.js +973 -0
- package/dist/bench.js.map +1 -0
- package/dist/bootstrap.js +1 -1
- package/dist/bootstrap.js.map +1 -1
- package/dist/cli.js +144 -29
- package/dist/cli.js.map +1 -1
- package/dist/cloud-agent.d.ts +77 -0
- package/dist/cloud-agent.d.ts.map +1 -0
- package/dist/cloud-agent.js +743 -0
- package/dist/cloud-agent.js.map +1 -0
- package/dist/context.test.d.ts +2 -0
- package/dist/context.test.d.ts.map +1 -0
- package/dist/context.test.js +561 -0
- package/dist/context.test.js.map +1 -0
- package/dist/evolution.d.ts.map +1 -1
- package/dist/evolution.js +4 -1
- package/dist/evolution.js.map +1 -1
- package/dist/github-release.d.ts +61 -0
- package/dist/github-release.d.ts.map +1 -0
- package/dist/github-release.js +451 -0
- package/dist/github-release.js.map +1 -0
- package/dist/graph-memory.test.d.ts +2 -0
- package/dist/graph-memory.test.d.ts.map +1 -0
- package/dist/graph-memory.test.js +946 -0
- package/dist/graph-memory.test.js.map +1 -0
- package/dist/init-science.d.ts +43 -0
- package/dist/init-science.d.ts.map +1 -0
- package/dist/init-science.js +477 -0
- package/dist/init-science.js.map +1 -0
- package/dist/integrations/ableton-m4l.d.ts +124 -0
- package/dist/integrations/ableton-m4l.d.ts.map +1 -0
- package/dist/integrations/ableton-m4l.js +338 -0
- package/dist/integrations/ableton-m4l.js.map +1 -0
- package/dist/integrations/ableton-osc.d.ts.map +1 -1
- package/dist/integrations/ableton-osc.js +6 -2
- package/dist/integrations/ableton-osc.js.map +1 -1
- package/dist/lab.d.ts +45 -0
- package/dist/lab.d.ts.map +1 -0
- package/dist/lab.js +1020 -0
- package/dist/lab.js.map +1 -0
- package/dist/lsp-deep.d.ts +101 -0
- package/dist/lsp-deep.d.ts.map +1 -0
- package/dist/lsp-deep.js +689 -0
- package/dist/lsp-deep.js.map +1 -0
- package/dist/memory.test.d.ts +2 -0
- package/dist/memory.test.d.ts.map +1 -0
- package/dist/memory.test.js +369 -0
- package/dist/memory.test.js.map +1 -0
- package/dist/multi-session.d.ts +164 -0
- package/dist/multi-session.d.ts.map +1 -0
- package/dist/multi-session.js +885 -0
- package/dist/multi-session.js.map +1 -0
- package/dist/music-learning.d.ts +181 -0
- package/dist/music-learning.d.ts.map +1 -0
- package/dist/music-learning.js +340 -0
- package/dist/music-learning.js.map +1 -0
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +5 -2
- package/dist/self-eval.js.map +1 -1
- package/dist/skill-system.d.ts +68 -0
- package/dist/skill-system.d.ts.map +1 -0
- package/dist/skill-system.js +386 -0
- package/dist/skill-system.js.map +1 -0
- package/dist/streaming.d.ts.map +1 -1
- package/dist/streaming.js +0 -1
- package/dist/streaming.js.map +1 -1
- package/dist/teach.d.ts +136 -0
- package/dist/teach.d.ts.map +1 -0
- package/dist/teach.js +915 -0
- package/dist/teach.js.map +1 -0
- package/dist/telemetry.d.ts +1 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js.map +1 -1
- package/dist/tools/ableton.d.ts.map +1 -1
- package/dist/tools/ableton.js +24 -8
- package/dist/tools/ableton.js.map +1 -1
- package/dist/tools/arrangement-engine.d.ts +2 -0
- package/dist/tools/arrangement-engine.d.ts.map +1 -0
- package/dist/tools/arrangement-engine.js +644 -0
- package/dist/tools/arrangement-engine.js.map +1 -0
- package/dist/tools/browser-agent.js +2 -2
- package/dist/tools/browser-agent.js.map +1 -1
- package/dist/tools/forge.d.ts.map +1 -1
- package/dist/tools/forge.js +15 -26
- package/dist/tools/forge.js.map +1 -1
- package/dist/tools/git.d.ts.map +1 -1
- package/dist/tools/git.js +10 -7
- package/dist/tools/git.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +5 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/producer-engine.d.ts +71 -0
- package/dist/tools/producer-engine.d.ts.map +1 -0
- package/dist/tools/producer-engine.js +1859 -0
- package/dist/tools/producer-engine.js.map +1 -0
- package/dist/tools/sound-designer.d.ts +2 -0
- package/dist/tools/sound-designer.d.ts.map +1 -0
- package/dist/tools/sound-designer.js +896 -0
- package/dist/tools/sound-designer.js.map +1 -0
- package/dist/voice-realtime.d.ts +54 -0
- package/dist/voice-realtime.d.ts.map +1 -0
- package/dist/voice-realtime.js +805 -0
- package/dist/voice-realtime.js.map +1 -0
- package/package.json +11 -4
package/dist/teach.js
ADDED
|
@@ -0,0 +1,915 @@
|
|
|
1
|
+
// kbot Teach — Explicit pattern teaching system
|
|
2
|
+
//
|
|
3
|
+
// Lets users teach kbot patterns, rules, preferences, aliases, and workflows.
|
|
4
|
+
// Teachings are user-explicit (priority > auto-extracted patterns from learning.ts).
|
|
5
|
+
// Stored in ~/.kbot/teachings.json as a flat JSON array.
|
|
6
|
+
//
|
|
7
|
+
// Usage:
|
|
8
|
+
// kbot teach # Interactive teach mode
|
|
9
|
+
// kbot teach "when I say deploy, run ship pipeline" # Quick teach
|
|
10
|
+
// kbot teach list # List all teachings
|
|
11
|
+
// kbot teach remove <id> # Remove a teaching
|
|
12
|
+
// kbot teach stats # Usage statistics
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
16
|
+
import { createInterface } from 'node:readline';
|
|
17
|
+
import { randomUUID } from 'node:crypto';
|
|
18
|
+
import chalk from 'chalk';
|
|
19
|
+
// ═══ Storage ═════════════════════════════════════════════════════
|
|
20
|
+
const KBOT_DIR = join(homedir(), '.kbot');
|
|
21
|
+
const TEACHINGS_FILE = join(KBOT_DIR, 'teachings.json');
|
|
22
|
+
function ensureDir() {
|
|
23
|
+
if (!existsSync(KBOT_DIR))
|
|
24
|
+
mkdirSync(KBOT_DIR, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
function loadTeachings() {
|
|
27
|
+
ensureDir();
|
|
28
|
+
if (!existsSync(TEACHINGS_FILE))
|
|
29
|
+
return [];
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(readFileSync(TEACHINGS_FILE, 'utf-8'));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function saveTeachings(teachings) {
|
|
38
|
+
ensureDir();
|
|
39
|
+
writeFileSync(TEACHINGS_FILE, JSON.stringify(teachings, null, 2));
|
|
40
|
+
}
|
|
41
|
+
/** In-memory cache — loaded once, saved on mutation */
|
|
42
|
+
let teachings = loadTeachings();
|
|
43
|
+
function persist() {
|
|
44
|
+
saveTeachings(teachings);
|
|
45
|
+
}
|
|
46
|
+
// ═══ ID generation ═══════════════════════════════════════════════
|
|
47
|
+
function newId() {
|
|
48
|
+
return randomUUID().slice(0, 8);
|
|
49
|
+
}
|
|
50
|
+
// ═══ Natural Language Parsing ════════════════════════════════════
|
|
51
|
+
/** Type detection keywords — used to classify natural language input */
|
|
52
|
+
const TYPE_SIGNALS = {
|
|
53
|
+
pattern: [
|
|
54
|
+
/when (?:i|we) (?:say|type|write|ask)\b/i,
|
|
55
|
+
/if (?:i|we|the user) (?:say|type|write|ask|mention)\b/i,
|
|
56
|
+
/whenever\b/i,
|
|
57
|
+
],
|
|
58
|
+
rule: [
|
|
59
|
+
/always\b/i,
|
|
60
|
+
/never\b/i,
|
|
61
|
+
/must\b/i,
|
|
62
|
+
/don'?t ever\b/i,
|
|
63
|
+
/every time\b/i,
|
|
64
|
+
],
|
|
65
|
+
preference: [
|
|
66
|
+
/(?:i|we) prefer\b/i,
|
|
67
|
+
/use .+ (?:instead of|over|rather than)\b/i,
|
|
68
|
+
/(?:i|we) like\b/i,
|
|
69
|
+
/default (?:to|should be)\b/i,
|
|
70
|
+
/favor\b/i,
|
|
71
|
+
],
|
|
72
|
+
alias: [
|
|
73
|
+
/\b(?:means?|is short for|stands for|is an alias for)\b/i,
|
|
74
|
+
/^[a-z0-9_-]+\s*=\s*/i,
|
|
75
|
+
],
|
|
76
|
+
workflow: [
|
|
77
|
+
/(?:to|when you) .+,?\s*first\b/i,
|
|
78
|
+
/step\s*1\b/i,
|
|
79
|
+
/then .+,?\s*then\b/i,
|
|
80
|
+
/the process (?:is|for)\b/i,
|
|
81
|
+
/workflow\b/i,
|
|
82
|
+
/pipeline\b/i,
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Detect teaching type from natural language input.
|
|
87
|
+
* Returns the best match or null if ambiguous.
|
|
88
|
+
*/
|
|
89
|
+
function detectType(input) {
|
|
90
|
+
const scores = {
|
|
91
|
+
pattern: 0, rule: 0, preference: 0, alias: 0, workflow: 0,
|
|
92
|
+
};
|
|
93
|
+
for (const [type, patterns] of Object.entries(TYPE_SIGNALS)) {
|
|
94
|
+
for (const re of patterns) {
|
|
95
|
+
if (re.test(input))
|
|
96
|
+
scores[type]++;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const sorted = Object.entries(scores).sort((a, b) => b[1] - a[1]);
|
|
100
|
+
if (sorted[0][1] > 0 && sorted[0][1] > (sorted[1]?.[1] ?? 0)) {
|
|
101
|
+
return sorted[0][0];
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Extract trigger and action from natural language.
|
|
107
|
+
* Handles common patterns like:
|
|
108
|
+
* "when I say 'deploy', run the ship pipeline"
|
|
109
|
+
* "always use TypeScript strict mode"
|
|
110
|
+
* "I prefer tabs over spaces"
|
|
111
|
+
* "d = deploy to production"
|
|
112
|
+
* "to release, first run tests, then build, then deploy"
|
|
113
|
+
*/
|
|
114
|
+
function parseTriggerAction(input, type) {
|
|
115
|
+
// Pattern: "when I say X, do Y"
|
|
116
|
+
const whenMatch = input.match(/when (?:i|we|the user) (?:say|type|write|ask)\s+['"]?(.+?)['"]?\s*,\s*(.+)/i);
|
|
117
|
+
if (whenMatch)
|
|
118
|
+
return { trigger: whenMatch[1].trim(), action: whenMatch[2].trim() };
|
|
119
|
+
// Pattern: "if X, then Y" / "if X, Y"
|
|
120
|
+
const ifMatch = input.match(/if\s+(?:i|we|the user)\s+(?:say|type|write|ask|mention)\s+['"]?(.+?)['"]?\s*,\s*(?:then\s+)?(.+)/i);
|
|
121
|
+
if (ifMatch)
|
|
122
|
+
return { trigger: ifMatch[1].trim(), action: ifMatch[2].trim() };
|
|
123
|
+
// Rule: "always X" / "never X"
|
|
124
|
+
const alwaysMatch = input.match(/^(always|never|must)\s+(.+)/i);
|
|
125
|
+
if (alwaysMatch) {
|
|
126
|
+
return {
|
|
127
|
+
trigger: `[${alwaysMatch[1].toLowerCase()}]`,
|
|
128
|
+
action: alwaysMatch[2].trim(),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
// Preference: "I prefer X over Y" / "use X instead of Y"
|
|
132
|
+
const preferMatch = input.match(/(?:i|we) prefer\s+(.+?)\s+(?:over|instead of|rather than)\s+(.+)/i);
|
|
133
|
+
if (preferMatch) {
|
|
134
|
+
return {
|
|
135
|
+
trigger: preferMatch[2].trim(),
|
|
136
|
+
action: `Use ${preferMatch[1].trim()} instead`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const useInsteadMatch = input.match(/use\s+(.+?)\s+(?:instead of|over|rather than)\s+(.+)/i);
|
|
140
|
+
if (useInsteadMatch) {
|
|
141
|
+
return {
|
|
142
|
+
trigger: useInsteadMatch[2].trim(),
|
|
143
|
+
action: `Use ${useInsteadMatch[1].trim()} instead`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const defaultMatch = input.match(/default\s+(?:to|should be)\s+(.+)/i);
|
|
147
|
+
if (defaultMatch) {
|
|
148
|
+
return {
|
|
149
|
+
trigger: '[default]',
|
|
150
|
+
action: defaultMatch[1].trim(),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
// Alias: "X means Y" / "X = Y" / "X is short for Y"
|
|
154
|
+
const aliasMatch = input.match(/^['"]?(.+?)['"]?\s*(?:means?|is short for|stands for|is an alias for|=)\s*['"]?(.+?)['"]?$/i);
|
|
155
|
+
if (aliasMatch) {
|
|
156
|
+
return { trigger: aliasMatch[1].trim(), action: aliasMatch[2].trim() };
|
|
157
|
+
}
|
|
158
|
+
// Workflow: "to X, first A, then B, then C"
|
|
159
|
+
const workflowMatch = input.match(/(?:to|when you)\s+(.+?)\s*,\s*(?:first\s+)?(.+)/i);
|
|
160
|
+
if (workflowMatch && type === 'workflow') {
|
|
161
|
+
return { trigger: workflowMatch[1].trim(), action: workflowMatch[2].trim() };
|
|
162
|
+
}
|
|
163
|
+
// Fallback: split on first comma or "do"/"run"/"execute"
|
|
164
|
+
const fallbackMatch = input.match(/^(.+?)\s*(?:,\s*(?:then\s+)?|:\s*)(do|run|execute|use|apply|call)?\s*(.+)$/i);
|
|
165
|
+
if (fallbackMatch) {
|
|
166
|
+
return {
|
|
167
|
+
trigger: fallbackMatch[1].trim(),
|
|
168
|
+
action: (fallbackMatch[2] ? fallbackMatch[2] + ' ' : '') + fallbackMatch[3].trim(),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
// Last resort: the whole input is the action, trigger is unset
|
|
172
|
+
return { trigger: '', action: input.trim() };
|
|
173
|
+
}
|
|
174
|
+
// ═══ Core Operations ═════════════════════════════════════════════
|
|
175
|
+
/**
|
|
176
|
+
* Create a new teaching from parsed components.
|
|
177
|
+
*/
|
|
178
|
+
export function createTeaching(opts) {
|
|
179
|
+
const teaching = {
|
|
180
|
+
id: newId(),
|
|
181
|
+
type: opts.type,
|
|
182
|
+
trigger: opts.trigger,
|
|
183
|
+
action: opts.action,
|
|
184
|
+
context: opts.context,
|
|
185
|
+
priority: opts.priority ?? 50, // user-taught default: 50 (always > auto-extracted)
|
|
186
|
+
examples: opts.examples,
|
|
187
|
+
createdAt: new Date().toISOString(),
|
|
188
|
+
usedCount: 0,
|
|
189
|
+
};
|
|
190
|
+
teachings.push(teaching);
|
|
191
|
+
persist();
|
|
192
|
+
return teaching;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Quick teach — parse natural language into a teaching in one shot.
|
|
196
|
+
*
|
|
197
|
+
* Examples:
|
|
198
|
+
* quickTeach("when I say 'deploy', run the ship pipeline")
|
|
199
|
+
* quickTeach("always use strict TypeScript")
|
|
200
|
+
* quickTeach("d = deploy to production")
|
|
201
|
+
*/
|
|
202
|
+
export async function quickTeach(input) {
|
|
203
|
+
const type = detectType(input) ?? 'pattern';
|
|
204
|
+
const { trigger, action } = parseTriggerAction(input, type);
|
|
205
|
+
if (!trigger && !action) {
|
|
206
|
+
throw new Error('Could not parse teaching. Try: "when I say X, do Y" or "always do X"');
|
|
207
|
+
}
|
|
208
|
+
return createTeaching({
|
|
209
|
+
type,
|
|
210
|
+
trigger: trigger || action,
|
|
211
|
+
action: action || trigger,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
// ═══ Matching ════════════════════════════════════════════════════
|
|
215
|
+
/**
|
|
216
|
+
* Find teachings that match a user message.
|
|
217
|
+
* Returns matches sorted by priority (highest first).
|
|
218
|
+
*
|
|
219
|
+
* Matching strategy:
|
|
220
|
+
* - Rules with trigger [always]/[never] always match (they are global)
|
|
221
|
+
* - Patterns: keyword or regex match against message
|
|
222
|
+
* - Aliases: exact or substring match against message
|
|
223
|
+
* - Preferences: keyword match
|
|
224
|
+
* - Workflows: keyword match
|
|
225
|
+
* - Context: if teaching has a context, current context must match
|
|
226
|
+
*/
|
|
227
|
+
export function findMatchingTeachings(message, context) {
|
|
228
|
+
const lowerMessage = message.toLowerCase();
|
|
229
|
+
const matches = [];
|
|
230
|
+
for (const t of teachings) {
|
|
231
|
+
// Context filter: if teaching specifies a context, check it
|
|
232
|
+
if (t.context && context) {
|
|
233
|
+
const lowerContext = context.toLowerCase();
|
|
234
|
+
const teachContext = t.context.toLowerCase();
|
|
235
|
+
if (!lowerContext.includes(teachContext) && !teachContext.includes(lowerContext)) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
else if (t.context && !context) {
|
|
240
|
+
// Teaching requires context but none provided — skip
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
let matched = false;
|
|
244
|
+
switch (t.type) {
|
|
245
|
+
case 'rule': {
|
|
246
|
+
// Rules with special triggers always apply
|
|
247
|
+
if (t.trigger.startsWith('[') && t.trigger.endsWith(']')) {
|
|
248
|
+
matched = true;
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
matched = lowerMessage.includes(t.trigger.toLowerCase());
|
|
252
|
+
}
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
case 'pattern': {
|
|
256
|
+
// Try regex match first
|
|
257
|
+
try {
|
|
258
|
+
const re = new RegExp(t.trigger, 'i');
|
|
259
|
+
matched = re.test(message);
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
// Not a valid regex — fall back to keyword match
|
|
263
|
+
matched = lowerMessage.includes(t.trigger.toLowerCase());
|
|
264
|
+
}
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
case 'alias': {
|
|
268
|
+
// Exact match or word-boundary match for aliases
|
|
269
|
+
const lowerTrigger = t.trigger.toLowerCase();
|
|
270
|
+
const re = new RegExp(`\\b${escapeRegex(lowerTrigger)}\\b`, 'i');
|
|
271
|
+
matched = re.test(message);
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
case 'preference': {
|
|
275
|
+
// Keyword match
|
|
276
|
+
const keywords = t.trigger.toLowerCase().split(/\s+/);
|
|
277
|
+
matched = keywords.some(kw => kw.length > 2 && lowerMessage.includes(kw));
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
case 'workflow': {
|
|
281
|
+
// Keyword match on trigger
|
|
282
|
+
const keywords = t.trigger.toLowerCase().split(/\s+/);
|
|
283
|
+
const significantKeywords = keywords.filter(kw => kw.length > 2);
|
|
284
|
+
// Require at least half the significant keywords to match
|
|
285
|
+
const matchCount = significantKeywords.filter(kw => lowerMessage.includes(kw)).length;
|
|
286
|
+
matched = significantKeywords.length > 0 && matchCount >= Math.ceil(significantKeywords.length / 2);
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// Also check examples if trigger didn't match
|
|
291
|
+
if (!matched && t.examples) {
|
|
292
|
+
matched = t.examples.some(ex => lowerMessage.includes(ex.toLowerCase()));
|
|
293
|
+
}
|
|
294
|
+
if (matched) {
|
|
295
|
+
matches.push(t);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// Sort by priority descending, then by usedCount descending (most used = more trusted)
|
|
299
|
+
matches.sort((a, b) => b.priority - a.priority || b.usedCount - a.usedCount);
|
|
300
|
+
return matches;
|
|
301
|
+
}
|
|
302
|
+
/** Escape special regex characters */
|
|
303
|
+
function escapeRegex(str) {
|
|
304
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Record that a teaching was used (updates usedCount and lastUsedAt).
|
|
308
|
+
*/
|
|
309
|
+
export function recordTeachingUsed(id) {
|
|
310
|
+
const t = teachings.find(t => t.id === id);
|
|
311
|
+
if (t) {
|
|
312
|
+
t.usedCount++;
|
|
313
|
+
t.lastUsedAt = new Date().toISOString();
|
|
314
|
+
persist();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// ═══ Management ══════════════════════════════════════════════════
|
|
318
|
+
/**
|
|
319
|
+
* List all teachings, optionally filtered by type and/or context.
|
|
320
|
+
*/
|
|
321
|
+
export function listTeachings(filter) {
|
|
322
|
+
let result = [...teachings];
|
|
323
|
+
if (filter?.type) {
|
|
324
|
+
result = result.filter(t => t.type === filter.type);
|
|
325
|
+
}
|
|
326
|
+
if (filter?.context) {
|
|
327
|
+
const lc = filter.context.toLowerCase();
|
|
328
|
+
result = result.filter(t => t.context?.toLowerCase().includes(lc));
|
|
329
|
+
}
|
|
330
|
+
return result.sort((a, b) => b.priority - a.priority);
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Remove a teaching by ID.
|
|
334
|
+
* Returns true if found and removed, false if not found.
|
|
335
|
+
*/
|
|
336
|
+
export function removeTeaching(id) {
|
|
337
|
+
const idx = teachings.findIndex(t => t.id === id);
|
|
338
|
+
if (idx === -1)
|
|
339
|
+
return false;
|
|
340
|
+
teachings.splice(idx, 1);
|
|
341
|
+
persist();
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Edit a teaching by ID.
|
|
346
|
+
* Returns the updated teaching or null if not found.
|
|
347
|
+
*/
|
|
348
|
+
export function editTeaching(id, updates) {
|
|
349
|
+
const t = teachings.find(t => t.id === id);
|
|
350
|
+
if (!t)
|
|
351
|
+
return null;
|
|
352
|
+
if (updates.type !== undefined)
|
|
353
|
+
t.type = updates.type;
|
|
354
|
+
if (updates.trigger !== undefined)
|
|
355
|
+
t.trigger = updates.trigger;
|
|
356
|
+
if (updates.action !== undefined)
|
|
357
|
+
t.action = updates.action;
|
|
358
|
+
if (updates.context !== undefined)
|
|
359
|
+
t.context = updates.context;
|
|
360
|
+
if (updates.priority !== undefined)
|
|
361
|
+
t.priority = updates.priority;
|
|
362
|
+
if (updates.examples !== undefined)
|
|
363
|
+
t.examples = updates.examples;
|
|
364
|
+
persist();
|
|
365
|
+
return t;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Get a teaching by ID.
|
|
369
|
+
*/
|
|
370
|
+
export function getTeaching(id) {
|
|
371
|
+
return teachings.find(t => t.id === id) ?? null;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Get aggregate statistics about teachings.
|
|
375
|
+
*/
|
|
376
|
+
export function getTeachingStats() {
|
|
377
|
+
const byType = {};
|
|
378
|
+
for (const t of teachings) {
|
|
379
|
+
byType[t.type] = (byType[t.type] ?? 0) + 1;
|
|
380
|
+
}
|
|
381
|
+
const mostUsed = [...teachings]
|
|
382
|
+
.filter(t => t.usedCount > 0)
|
|
383
|
+
.sort((a, b) => b.usedCount - a.usedCount)
|
|
384
|
+
.slice(0, 10);
|
|
385
|
+
const recentlyAdded = [...teachings]
|
|
386
|
+
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
|
387
|
+
.slice(0, 5);
|
|
388
|
+
const neverUsed = teachings.filter(t => t.usedCount === 0);
|
|
389
|
+
return { total: teachings.length, byType, mostUsed, recentlyAdded, neverUsed };
|
|
390
|
+
}
|
|
391
|
+
// ═══ Prompt Integration ══════════════════════════════════════════
|
|
392
|
+
/**
|
|
393
|
+
* Build a system prompt fragment from active rules and preferences.
|
|
394
|
+
* This is injected into the agent's system prompt so rules/preferences
|
|
395
|
+
* are always active without needing a trigger match.
|
|
396
|
+
*/
|
|
397
|
+
export function getTeachingPromptRules() {
|
|
398
|
+
const rules = teachings.filter(t => t.type === 'rule');
|
|
399
|
+
const prefs = teachings.filter(t => t.type === 'preference');
|
|
400
|
+
if (rules.length === 0 && prefs.length === 0)
|
|
401
|
+
return '';
|
|
402
|
+
const lines = ['\n[User-Taught Rules & Preferences]'];
|
|
403
|
+
if (rules.length > 0) {
|
|
404
|
+
lines.push('Rules:');
|
|
405
|
+
for (const r of rules.sort((a, b) => b.priority - a.priority)) {
|
|
406
|
+
const prefix = r.trigger === '[always]' ? 'ALWAYS' :
|
|
407
|
+
r.trigger === '[never]' ? 'NEVER' :
|
|
408
|
+
r.trigger.replace(/^\[|\]$/g, '').toUpperCase();
|
|
409
|
+
lines.push(`- ${prefix}: ${r.action}`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (prefs.length > 0) {
|
|
413
|
+
lines.push('Preferences:');
|
|
414
|
+
for (const p of prefs.sort((a, b) => b.priority - a.priority)) {
|
|
415
|
+
lines.push(`- ${p.action}${p.context ? ` (when: ${p.context})` : ''}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
lines.push('');
|
|
419
|
+
return lines.join('\n');
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Build a context-aware hint for the agent when teachings match.
|
|
423
|
+
* Returns a string to prepend to the user message context, or empty string.
|
|
424
|
+
*/
|
|
425
|
+
export function getTeachingHints(message, context) {
|
|
426
|
+
const matches = findMatchingTeachings(message, context);
|
|
427
|
+
// Exclude rules/prefs (already in system prompt via getTeachingPromptRules)
|
|
428
|
+
const actionable = matches.filter(t => t.type !== 'rule' && t.type !== 'preference');
|
|
429
|
+
if (actionable.length === 0)
|
|
430
|
+
return '';
|
|
431
|
+
const lines = ['[Matched teachings — follow these instructions]'];
|
|
432
|
+
for (const t of actionable) {
|
|
433
|
+
switch (t.type) {
|
|
434
|
+
case 'pattern':
|
|
435
|
+
lines.push(`Pattern match: "${t.trigger}" → ${t.action}`);
|
|
436
|
+
break;
|
|
437
|
+
case 'alias':
|
|
438
|
+
lines.push(`Alias: "${t.trigger}" means "${t.action}"`);
|
|
439
|
+
break;
|
|
440
|
+
case 'workflow':
|
|
441
|
+
lines.push(`Workflow for "${t.trigger}": ${t.action}`);
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
// Record usage
|
|
445
|
+
recordTeachingUsed(t.id);
|
|
446
|
+
}
|
|
447
|
+
return lines.join('\n') + '\n';
|
|
448
|
+
}
|
|
449
|
+
// ═══ Interactive Teach Mode ══════════════════════════════════════
|
|
450
|
+
const TYPE_LABELS = {
|
|
451
|
+
pattern: 'Pattern',
|
|
452
|
+
rule: 'Rule',
|
|
453
|
+
preference: 'Preference',
|
|
454
|
+
alias: 'Alias',
|
|
455
|
+
workflow: 'Workflow',
|
|
456
|
+
};
|
|
457
|
+
const TYPE_DESCRIPTIONS = {
|
|
458
|
+
pattern: '"When I say X, do Y" — triggers on keywords or regex',
|
|
459
|
+
rule: '"Always/Never do X" — permanent behavioral rule',
|
|
460
|
+
preference: '"I prefer X over Y" — style and output preferences',
|
|
461
|
+
alias: '"X means Y" — short aliases for complex commands',
|
|
462
|
+
workflow: '"To do X, first A, then B, then C" — multi-step procedures',
|
|
463
|
+
};
|
|
464
|
+
const TYPE_COLORS = {
|
|
465
|
+
pattern: chalk.hex('#60A5FA'),
|
|
466
|
+
rule: chalk.hex('#F87171'),
|
|
467
|
+
preference: chalk.hex('#A78BFA'),
|
|
468
|
+
alias: chalk.hex('#4ADE80'),
|
|
469
|
+
workflow: chalk.hex('#FBBF24'),
|
|
470
|
+
};
|
|
471
|
+
/**
|
|
472
|
+
* Interactive teach mode — guided flow for creating a teaching.
|
|
473
|
+
*/
|
|
474
|
+
export async function startTeachMode() {
|
|
475
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
476
|
+
const ask = (q) => new Promise(r => rl.question(q, (a) => r(a.trim())));
|
|
477
|
+
console.error('');
|
|
478
|
+
console.error(chalk.hex('#A78BFA').bold(' Teach kbot'));
|
|
479
|
+
console.error(chalk.dim(' Teach kbot something new — a pattern, rule, preference, alias, or workflow.'));
|
|
480
|
+
console.error('');
|
|
481
|
+
try {
|
|
482
|
+
// Step 1: Get the teaching input
|
|
483
|
+
console.error(chalk.dim(' Describe what you want to teach kbot:'));
|
|
484
|
+
console.error(chalk.dim(' Examples:'));
|
|
485
|
+
console.error(chalk.dim(' "when I say deploy, run the ship pipeline"'));
|
|
486
|
+
console.error(chalk.dim(' "always use TypeScript strict mode"'));
|
|
487
|
+
console.error(chalk.dim(' "I prefer tabs over spaces"'));
|
|
488
|
+
console.error(chalk.dim(' "d = deploy to production"'));
|
|
489
|
+
console.error(chalk.dim(' "to release: first run tests, then build, then deploy"'));
|
|
490
|
+
console.error('');
|
|
491
|
+
const input = await ask(chalk.hex('#A78BFA')(' > '));
|
|
492
|
+
if (!input) {
|
|
493
|
+
console.error(chalk.dim(' Nothing to teach. Exiting.'));
|
|
494
|
+
rl.close();
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
// Step 2: Detect type
|
|
498
|
+
let type = detectType(input);
|
|
499
|
+
if (!type) {
|
|
500
|
+
console.error('');
|
|
501
|
+
console.error(chalk.dim(' What type of teaching is this?'));
|
|
502
|
+
const types = ['pattern', 'rule', 'preference', 'alias', 'workflow'];
|
|
503
|
+
for (let i = 0; i < types.length; i++) {
|
|
504
|
+
const t = types[i];
|
|
505
|
+
console.error(` ${chalk.bold(`${i + 1}`)} ${TYPE_COLORS[t](TYPE_LABELS[t])} — ${chalk.dim(TYPE_DESCRIPTIONS[t])}`);
|
|
506
|
+
}
|
|
507
|
+
console.error('');
|
|
508
|
+
const choice = await ask(chalk.hex('#A78BFA')(' Pick 1-5: '));
|
|
509
|
+
const idx = parseInt(choice, 10) - 1;
|
|
510
|
+
if (idx >= 0 && idx < types.length) {
|
|
511
|
+
type = types[idx];
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
type = 'pattern'; // default
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
// Step 3: Extract trigger and action
|
|
518
|
+
let { trigger, action } = parseTriggerAction(input, type);
|
|
519
|
+
// Step 4: Confirm understanding
|
|
520
|
+
console.error('');
|
|
521
|
+
console.error(chalk.hex('#A78BFA').bold(' I understood:'));
|
|
522
|
+
console.error(` Type: ${TYPE_COLORS[type](TYPE_LABELS[type])}`);
|
|
523
|
+
console.error(` Trigger: ${chalk.bold(trigger || '(global)')}`);
|
|
524
|
+
console.error(` Action: ${chalk.bold(action)}`);
|
|
525
|
+
console.error('');
|
|
526
|
+
const confirm = await ask(chalk.dim(' Correct? [Y/n/edit] '));
|
|
527
|
+
if (confirm.toLowerCase() === 'n') {
|
|
528
|
+
console.error(chalk.dim(' Cancelled.'));
|
|
529
|
+
rl.close();
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
if (confirm.toLowerCase() === 'edit' || confirm.toLowerCase() === 'e') {
|
|
533
|
+
const newTrigger = await ask(` Trigger ${chalk.dim(`[${trigger}]`)}: `);
|
|
534
|
+
if (newTrigger)
|
|
535
|
+
trigger = newTrigger;
|
|
536
|
+
const newAction = await ask(` Action ${chalk.dim(`[${action}]`)}: `);
|
|
537
|
+
if (newAction)
|
|
538
|
+
action = newAction;
|
|
539
|
+
}
|
|
540
|
+
// Step 5: Optional context
|
|
541
|
+
const ctxInput = await ask(chalk.dim(' Context (project/language, or enter to skip): '));
|
|
542
|
+
const context = ctxInput || undefined;
|
|
543
|
+
// Step 6: Optional examples
|
|
544
|
+
console.error(chalk.dim(' Example phrases that should trigger this (one per line, empty line to finish):'));
|
|
545
|
+
const examples = [];
|
|
546
|
+
let ex = await ask(chalk.dim(' ex: '));
|
|
547
|
+
while (ex) {
|
|
548
|
+
examples.push(ex);
|
|
549
|
+
ex = await ask(chalk.dim(' ex: '));
|
|
550
|
+
}
|
|
551
|
+
// Step 7: Create the teaching
|
|
552
|
+
const teaching = createTeaching({
|
|
553
|
+
type,
|
|
554
|
+
trigger,
|
|
555
|
+
action,
|
|
556
|
+
context,
|
|
557
|
+
examples: examples.length > 0 ? examples : undefined,
|
|
558
|
+
});
|
|
559
|
+
console.error('');
|
|
560
|
+
console.error(chalk.hex('#4ADE80').bold(' Taught!'));
|
|
561
|
+
printTeachingCompact(teaching);
|
|
562
|
+
// Step 8: Test it
|
|
563
|
+
console.error('');
|
|
564
|
+
const testInput = await ask(chalk.dim(' Test it? Enter a message (or skip): '));
|
|
565
|
+
if (testInput) {
|
|
566
|
+
const matches = findMatchingTeachings(testInput, context);
|
|
567
|
+
if (matches.some(m => m.id === teaching.id)) {
|
|
568
|
+
console.error(chalk.hex('#4ADE80')(' Match! This teaching would apply.'));
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
console.error(chalk.hex('#FBBF24')(' No match. Try adjusting the trigger or adding examples.'));
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
console.error('');
|
|
575
|
+
}
|
|
576
|
+
finally {
|
|
577
|
+
rl.close();
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
// ═══ CLI Display ═════════════════════════════════════════════════
|
|
581
|
+
/** Truncate string to max length with ellipsis */
|
|
582
|
+
function truncate(str, max) {
|
|
583
|
+
if (str.length <= max)
|
|
584
|
+
return str;
|
|
585
|
+
return str.slice(0, max - 1) + '\u2026';
|
|
586
|
+
}
|
|
587
|
+
/** Right-pad string to width */
|
|
588
|
+
function pad(str, width) {
|
|
589
|
+
// Account for ANSI escape codes — measure visible length
|
|
590
|
+
const visible = str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
591
|
+
if (visible.length >= width)
|
|
592
|
+
return str;
|
|
593
|
+
return str + ' '.repeat(width - visible.length);
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Print a single teaching in compact format.
|
|
597
|
+
*/
|
|
598
|
+
function printTeachingCompact(t) {
|
|
599
|
+
const typeLabel = TYPE_COLORS[t.type](`[${TYPE_LABELS[t.type]}]`);
|
|
600
|
+
const id = chalk.dim(`#${t.id}`);
|
|
601
|
+
console.error(` ${id} ${typeLabel} ${chalk.bold(truncate(t.trigger, 30))} ${chalk.dim('\u2192')} ${truncate(t.action, 50)}`);
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Print formatted table of teachings for CLI display.
|
|
605
|
+
*/
|
|
606
|
+
export function printTeachingsTable(filter) {
|
|
607
|
+
const items = listTeachings(filter);
|
|
608
|
+
if (items.length === 0) {
|
|
609
|
+
console.error(chalk.dim(' No teachings found.'));
|
|
610
|
+
console.error(chalk.dim(' Run `kbot teach` to teach kbot something new.'));
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
// Header
|
|
614
|
+
console.error('');
|
|
615
|
+
console.error(chalk.hex('#A78BFA').bold(` Teachings (${items.length})`));
|
|
616
|
+
console.error(chalk.dim(' ' + '\u2500'.repeat(72)));
|
|
617
|
+
// Column header
|
|
618
|
+
console.error(` ${pad(chalk.dim('ID'), 12)}` +
|
|
619
|
+
`${pad(chalk.dim('Type'), 16)}` +
|
|
620
|
+
`${pad(chalk.dim('Trigger'), 24)}` +
|
|
621
|
+
`${pad(chalk.dim('Action'), 30)}` +
|
|
622
|
+
`${chalk.dim('Used')}`);
|
|
623
|
+
console.error(chalk.dim(' ' + '\u2500'.repeat(72)));
|
|
624
|
+
for (const t of items) {
|
|
625
|
+
const id = chalk.dim(`#${t.id}`);
|
|
626
|
+
const typeLabel = TYPE_COLORS[t.type](TYPE_LABELS[t.type]);
|
|
627
|
+
const trigger = truncate(t.trigger, 20);
|
|
628
|
+
const action = chalk.dim(truncate(t.action, 26));
|
|
629
|
+
const used = t.usedCount > 0 ? chalk.hex('#4ADE80')(`${t.usedCount}x`) : chalk.dim('0');
|
|
630
|
+
console.error(` ${pad(id, 12)}` +
|
|
631
|
+
`${pad(typeLabel, 16)}` +
|
|
632
|
+
`${pad(trigger, 24)}` +
|
|
633
|
+
`${pad(action, 30)}` +
|
|
634
|
+
`${used}`);
|
|
635
|
+
}
|
|
636
|
+
console.error(chalk.dim(' ' + '\u2500'.repeat(72)));
|
|
637
|
+
console.error('');
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Print detailed view of a single teaching.
|
|
641
|
+
*/
|
|
642
|
+
export function printTeachingDetail(id) {
|
|
643
|
+
const t = getTeaching(id);
|
|
644
|
+
if (!t) {
|
|
645
|
+
console.error(chalk.hex('#F87171')(` Teaching #${id} not found.`));
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
console.error('');
|
|
649
|
+
console.error(chalk.hex('#A78BFA').bold(` Teaching #${t.id}`));
|
|
650
|
+
console.error(chalk.dim(' ' + '\u2500'.repeat(40)));
|
|
651
|
+
console.error(` Type: ${TYPE_COLORS[t.type](TYPE_LABELS[t.type])}`);
|
|
652
|
+
console.error(` Trigger: ${chalk.bold(t.trigger)}`);
|
|
653
|
+
console.error(` Action: ${t.action}`);
|
|
654
|
+
if (t.context)
|
|
655
|
+
console.error(` Context: ${t.context}`);
|
|
656
|
+
console.error(` Priority: ${t.priority}`);
|
|
657
|
+
console.error(` Used: ${t.usedCount} time${t.usedCount === 1 ? '' : 's'}`);
|
|
658
|
+
if (t.lastUsedAt)
|
|
659
|
+
console.error(` Last used: ${t.lastUsedAt.split('T')[0]}`);
|
|
660
|
+
console.error(` Created: ${t.createdAt.split('T')[0]}`);
|
|
661
|
+
if (t.examples && t.examples.length > 0) {
|
|
662
|
+
console.error(` Examples:`);
|
|
663
|
+
for (const ex of t.examples) {
|
|
664
|
+
console.error(` - "${ex}"`);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
console.error('');
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Print teaching statistics.
|
|
671
|
+
*/
|
|
672
|
+
export function printTeachingStats() {
|
|
673
|
+
const stats = getTeachingStats();
|
|
674
|
+
console.error('');
|
|
675
|
+
console.error(chalk.hex('#A78BFA').bold(' Teaching Statistics'));
|
|
676
|
+
console.error(chalk.dim(' ' + '\u2500'.repeat(40)));
|
|
677
|
+
console.error(` Total teachings: ${chalk.bold(String(stats.total))}`);
|
|
678
|
+
console.error('');
|
|
679
|
+
// By type
|
|
680
|
+
if (Object.keys(stats.byType).length > 0) {
|
|
681
|
+
console.error(chalk.dim(' By type:'));
|
|
682
|
+
for (const [type, count] of Object.entries(stats.byType)) {
|
|
683
|
+
const label = TYPE_COLORS[type]?.(TYPE_LABELS[type]) ?? type;
|
|
684
|
+
console.error(` ${label}: ${count}`);
|
|
685
|
+
}
|
|
686
|
+
console.error('');
|
|
687
|
+
}
|
|
688
|
+
// Most used
|
|
689
|
+
if (stats.mostUsed.length > 0) {
|
|
690
|
+
console.error(chalk.dim(' Most used:'));
|
|
691
|
+
for (const t of stats.mostUsed.slice(0, 5)) {
|
|
692
|
+
const typeLabel = TYPE_COLORS[t.type](`[${TYPE_LABELS[t.type]}]`);
|
|
693
|
+
console.error(` ${chalk.hex('#4ADE80')(`${t.usedCount}x`)} ${typeLabel} ${truncate(t.trigger, 30)} ${chalk.dim('\u2192')} ${truncate(t.action, 30)}`);
|
|
694
|
+
}
|
|
695
|
+
console.error('');
|
|
696
|
+
}
|
|
697
|
+
// Never used
|
|
698
|
+
if (stats.neverUsed.length > 0) {
|
|
699
|
+
console.error(chalk.dim(` Never used: ${stats.neverUsed.length} teaching${stats.neverUsed.length === 1 ? '' : 's'}`));
|
|
700
|
+
for (const t of stats.neverUsed.slice(0, 3)) {
|
|
701
|
+
console.error(` ${chalk.dim(`#${t.id}`)} ${truncate(t.trigger, 30)}`);
|
|
702
|
+
}
|
|
703
|
+
if (stats.neverUsed.length > 3) {
|
|
704
|
+
console.error(chalk.dim(` ... and ${stats.neverUsed.length - 3} more`));
|
|
705
|
+
}
|
|
706
|
+
console.error('');
|
|
707
|
+
}
|
|
708
|
+
// Recently added
|
|
709
|
+
if (stats.recentlyAdded.length > 0) {
|
|
710
|
+
console.error(chalk.dim(' Recently added:'));
|
|
711
|
+
for (const t of stats.recentlyAdded) {
|
|
712
|
+
const date = t.createdAt.split('T')[0];
|
|
713
|
+
console.error(` ${chalk.dim(date)} ${truncate(t.trigger, 30)} ${chalk.dim('\u2192')} ${truncate(t.action, 30)}`);
|
|
714
|
+
}
|
|
715
|
+
console.error('');
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
// ═══ Export / Import ═════════════════════════════════════════════
|
|
719
|
+
/**
|
|
720
|
+
* Export all teachings as JSON string (for backup or sharing).
|
|
721
|
+
*/
|
|
722
|
+
export function exportTeachings() {
|
|
723
|
+
return JSON.stringify(teachings, null, 2);
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Import teachings from JSON string. Merges with existing (skips duplicates by trigger+action).
|
|
727
|
+
* Returns count of new teachings imported.
|
|
728
|
+
*/
|
|
729
|
+
export function importTeachings(json) {
|
|
730
|
+
let incoming;
|
|
731
|
+
try {
|
|
732
|
+
incoming = JSON.parse(json);
|
|
733
|
+
}
|
|
734
|
+
catch {
|
|
735
|
+
throw new Error('Invalid JSON — expected an array of teachings');
|
|
736
|
+
}
|
|
737
|
+
if (!Array.isArray(incoming)) {
|
|
738
|
+
throw new Error('Invalid format — expected an array of teachings');
|
|
739
|
+
}
|
|
740
|
+
let imported = 0;
|
|
741
|
+
for (const t of incoming) {
|
|
742
|
+
// Validate required fields
|
|
743
|
+
if (!t.type || !t.trigger || !t.action)
|
|
744
|
+
continue;
|
|
745
|
+
// Check for duplicates (same trigger + action = duplicate)
|
|
746
|
+
const exists = teachings.some(existing => existing.trigger === t.trigger && existing.action === t.action);
|
|
747
|
+
if (exists)
|
|
748
|
+
continue;
|
|
749
|
+
teachings.push({
|
|
750
|
+
id: newId(),
|
|
751
|
+
type: t.type,
|
|
752
|
+
trigger: t.trigger,
|
|
753
|
+
action: t.action,
|
|
754
|
+
context: t.context,
|
|
755
|
+
priority: t.priority ?? 50,
|
|
756
|
+
examples: t.examples,
|
|
757
|
+
createdAt: t.createdAt ?? new Date().toISOString(),
|
|
758
|
+
usedCount: 0,
|
|
759
|
+
lastUsedAt: undefined,
|
|
760
|
+
});
|
|
761
|
+
imported++;
|
|
762
|
+
}
|
|
763
|
+
if (imported > 0)
|
|
764
|
+
persist();
|
|
765
|
+
return imported;
|
|
766
|
+
}
|
|
767
|
+
// ═══ CLI Command Handler ═════════════════════════════════════════
|
|
768
|
+
/**
|
|
769
|
+
* Main CLI entry point for `kbot teach [subcommand] [args]`.
|
|
770
|
+
* Called from cli.ts when the teach command is invoked.
|
|
771
|
+
*/
|
|
772
|
+
export async function handleTeachCommand(args) {
|
|
773
|
+
const sub = args[0]?.toLowerCase();
|
|
774
|
+
// kbot teach (no args) → interactive mode
|
|
775
|
+
if (!sub) {
|
|
776
|
+
await startTeachMode();
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
// kbot teach list [--type X] [--context X]
|
|
780
|
+
if (sub === 'list' || sub === 'ls') {
|
|
781
|
+
const typeIdx = args.indexOf('--type');
|
|
782
|
+
const ctxIdx = args.indexOf('--context');
|
|
783
|
+
printTeachingsTable({
|
|
784
|
+
type: typeIdx >= 0 ? args[typeIdx + 1] : undefined,
|
|
785
|
+
context: ctxIdx >= 0 ? args[ctxIdx + 1] : undefined,
|
|
786
|
+
});
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
// kbot teach show <id>
|
|
790
|
+
if (sub === 'show' || sub === 'info') {
|
|
791
|
+
const id = args[1];
|
|
792
|
+
if (!id) {
|
|
793
|
+
console.error(chalk.hex('#F87171')(' Usage: kbot teach show <id>'));
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
printTeachingDetail(id);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
// kbot teach remove <id>
|
|
800
|
+
if (sub === 'remove' || sub === 'rm' || sub === 'delete') {
|
|
801
|
+
const id = args[1];
|
|
802
|
+
if (!id) {
|
|
803
|
+
console.error(chalk.hex('#F87171')(' Usage: kbot teach remove <id>'));
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
if (removeTeaching(id)) {
|
|
807
|
+
console.error(chalk.hex('#4ADE80')(` Removed teaching #${id}`));
|
|
808
|
+
}
|
|
809
|
+
else {
|
|
810
|
+
console.error(chalk.hex('#F87171')(` Teaching #${id} not found.`));
|
|
811
|
+
}
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
// kbot teach edit <id> [--trigger X] [--action X] [--priority N] [--context X]
|
|
815
|
+
if (sub === 'edit') {
|
|
816
|
+
const id = args[1];
|
|
817
|
+
if (!id) {
|
|
818
|
+
console.error(chalk.hex('#F87171')(' Usage: kbot teach edit <id> [--trigger X] [--action X] [--priority N]'));
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
const updates = {};
|
|
822
|
+
for (let i = 2; i < args.length; i += 2) {
|
|
823
|
+
const flag = args[i];
|
|
824
|
+
const val = args[i + 1];
|
|
825
|
+
if (!val)
|
|
826
|
+
break;
|
|
827
|
+
if (flag === '--trigger')
|
|
828
|
+
updates.trigger = val;
|
|
829
|
+
else if (flag === '--action')
|
|
830
|
+
updates.action = val;
|
|
831
|
+
else if (flag === '--priority')
|
|
832
|
+
updates.priority = parseInt(val, 10);
|
|
833
|
+
else if (flag === '--context')
|
|
834
|
+
updates.context = val;
|
|
835
|
+
}
|
|
836
|
+
const result = editTeaching(id, updates);
|
|
837
|
+
if (result) {
|
|
838
|
+
console.error(chalk.hex('#4ADE80')(` Updated teaching #${id}`));
|
|
839
|
+
printTeachingDetail(id);
|
|
840
|
+
}
|
|
841
|
+
else {
|
|
842
|
+
console.error(chalk.hex('#F87171')(` Teaching #${id} not found.`));
|
|
843
|
+
}
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
// kbot teach stats
|
|
847
|
+
if (sub === 'stats') {
|
|
848
|
+
printTeachingStats();
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
// kbot teach export
|
|
852
|
+
if (sub === 'export') {
|
|
853
|
+
console.log(exportTeachings());
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
// kbot teach import <file>
|
|
857
|
+
if (sub === 'import') {
|
|
858
|
+
const file = args[1];
|
|
859
|
+
if (!file) {
|
|
860
|
+
console.error(chalk.hex('#F87171')(' Usage: kbot teach import <file.json>'));
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
try {
|
|
864
|
+
const content = readFileSync(file, 'utf-8');
|
|
865
|
+
const count = importTeachings(content);
|
|
866
|
+
console.error(chalk.hex('#4ADE80')(` Imported ${count} new teaching${count === 1 ? '' : 's'}`));
|
|
867
|
+
}
|
|
868
|
+
catch (err) {
|
|
869
|
+
console.error(chalk.hex('#F87171')(` Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
870
|
+
}
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
// kbot teach test <message>
|
|
874
|
+
if (sub === 'test') {
|
|
875
|
+
const message = args.slice(1).join(' ');
|
|
876
|
+
if (!message) {
|
|
877
|
+
console.error(chalk.hex('#F87171')(' Usage: kbot teach test <message>'));
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
const matches = findMatchingTeachings(message);
|
|
881
|
+
if (matches.length === 0) {
|
|
882
|
+
console.error(chalk.dim(' No teachings matched.'));
|
|
883
|
+
}
|
|
884
|
+
else {
|
|
885
|
+
console.error(chalk.hex('#A78BFA').bold(` ${matches.length} teaching${matches.length === 1 ? '' : 's'} matched:`));
|
|
886
|
+
console.error('');
|
|
887
|
+
for (const t of matches) {
|
|
888
|
+
printTeachingCompact(t);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
console.error('');
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
// kbot teach clear
|
|
895
|
+
if (sub === 'clear') {
|
|
896
|
+
const count = teachings.length;
|
|
897
|
+
teachings.length = 0;
|
|
898
|
+
persist();
|
|
899
|
+
console.error(chalk.hex('#FBBF24')(` Cleared ${count} teaching${count === 1 ? '' : 's'}.`));
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
// Anything else → treat as quick teach
|
|
903
|
+
const input = args.join(' ');
|
|
904
|
+
try {
|
|
905
|
+
const teaching = await quickTeach(input);
|
|
906
|
+
console.error('');
|
|
907
|
+
console.error(chalk.hex('#4ADE80').bold(' Taught!'));
|
|
908
|
+
printTeachingCompact(teaching);
|
|
909
|
+
console.error('');
|
|
910
|
+
}
|
|
911
|
+
catch (err) {
|
|
912
|
+
console.error(chalk.hex('#F87171')(` Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
//# sourceMappingURL=teach.js.map
|