@kernel.chat/kbot 3.98.0 → 3.99.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/dist/agent.js +1 -1
- package/dist/cli.js +44 -0
- package/dist/skills-loader.d.ts +37 -5
- package/dist/skills-loader.js +342 -50
- package/dist/train-self.js +1 -1
- package/package.json +2 -1
- package/skills/deployment/daemon-deployment/SKILL.md +70 -0
- package/skills/deployment/ship-pipeline/SKILL.md +81 -0
- package/skills/emergent/forge-reflex/SKILL.md +53 -0
- package/skills/emergent/mimic-hybrid/SKILL.md +56 -0
- package/skills/memory/dream-to-commit/SKILL.md +52 -0
- package/skills/memory/memory-cascade/SKILL.md +59 -0
- package/skills/music-production/ableton-session-build/SKILL.md +61 -0
- package/skills/orchestration/cross-agent-blackboard/SKILL.md +58 -0
- package/skills/orchestration/specialist-routing/SKILL.md +57 -0
- package/skills/self-improvement/autopoiesis-loop/SKILL.md +47 -0
- package/skills/self-improvement/skill-self-authorship/SKILL.md +70 -0
- package/skills/self-improvement/teacher-trace-curation/SKILL.md +54 -0
- package/skills/software-development/systematic-debugging/SKILL.md +86 -0
- package/skills/software-development/test-driven-development/SKILL.md +74 -0
package/dist/agent.js
CHANGED
|
@@ -875,7 +875,7 @@ export async function runAgent(message, options = {}) {
|
|
|
875
875
|
// Step 2: Build context (cached — only rebuilt when inputs change)
|
|
876
876
|
const matrixPrompt = options.agent ? getMatrixSystemPrompt(options.agent) : null;
|
|
877
877
|
const contextSnippet = options.context ? formatContextForPrompt(options.context) : '';
|
|
878
|
-
const skillsSnippet = loadSkills(process.cwd());
|
|
878
|
+
const skillsSnippet = loadSkills(process.cwd(), message);
|
|
879
879
|
const memorySnippet = getMemoryPrompt();
|
|
880
880
|
const learningContext = buildFullLearningContext(message, process.cwd());
|
|
881
881
|
const synthesisSnippet = getSynthesisContext(8); // Three-tier memory: reflection layer insights
|
package/dist/cli.js
CHANGED
|
@@ -835,6 +835,50 @@ async function main() {
|
|
|
835
835
|
ignorePatterns,
|
|
836
836
|
});
|
|
837
837
|
});
|
|
838
|
+
const skillsCmd = program.command('skills').description('Manage agent skills (agentskills.io format — compatible with Claude Skills, Hermes, Copilot)');
|
|
839
|
+
skillsCmd
|
|
840
|
+
.command('list')
|
|
841
|
+
.description('List all discovered skills')
|
|
842
|
+
.action(async () => {
|
|
843
|
+
const { discoverSkillFiles } = await import('./skills-loader.js');
|
|
844
|
+
const skills = discoverSkillFiles(process.cwd());
|
|
845
|
+
if (skills.length === 0) {
|
|
846
|
+
printInfo('No skills found. Run `kbot skills import --from hermes` to import 76 Hermes skills.');
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
printInfo(`Found ${skills.length} skill${skills.length === 1 ? '' : 's'}:\n`);
|
|
850
|
+
for (const s of skills) {
|
|
851
|
+
const desc = s.description ? ` — ${s.description.slice(0, 70)}` : '';
|
|
852
|
+
printInfo(` ${s.name}${desc}`);
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
skillsCmd
|
|
856
|
+
.command('import')
|
|
857
|
+
.description('Import skills from an external agent (Hermes, Claude, or a custom directory)')
|
|
858
|
+
.option('--from <source>', 'Source: hermes, claude, or an absolute path', 'hermes')
|
|
859
|
+
.action(async (opts) => {
|
|
860
|
+
const { importExternalSkills } = await import('./skills-loader.js');
|
|
861
|
+
const { homedir } = await import('node:os');
|
|
862
|
+
const { join } = await import('node:path');
|
|
863
|
+
const { existsSync } = await import('node:fs');
|
|
864
|
+
const sources = {
|
|
865
|
+
hermes: join(homedir(), '.hermes', 'skills'),
|
|
866
|
+
claude: join(homedir(), '.claude', 'skills'),
|
|
867
|
+
};
|
|
868
|
+
const from = opts.from || 'hermes';
|
|
869
|
+
const src = sources[from] || from;
|
|
870
|
+
if (!existsSync(src)) {
|
|
871
|
+
printError(`Source not found: ${src}`);
|
|
872
|
+
if (from === 'hermes')
|
|
873
|
+
printInfo('Install Hermes: `ollama launch hermes --yes --model hermes3:8b`');
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
const result = await importExternalSkills(src);
|
|
877
|
+
printSuccess(`Imported ${result.imported} skill${result.imported === 1 ? '' : 's'} from ${src}`);
|
|
878
|
+
if (result.skipped > 0)
|
|
879
|
+
printInfo(` ${result.skipped} skipped (existing user-authored files preserved)`);
|
|
880
|
+
printInfo(` → ${result.destination}`);
|
|
881
|
+
});
|
|
838
882
|
program
|
|
839
883
|
.command('doctor')
|
|
840
884
|
.description('Diagnose your kbot setup — check everything is working')
|
package/dist/skills-loader.d.ts
CHANGED
|
@@ -2,16 +2,48 @@ export interface SkillFile {
|
|
|
2
2
|
name: string;
|
|
3
3
|
path: string;
|
|
4
4
|
content: string;
|
|
5
|
+
description: string;
|
|
6
|
+
tags: string[];
|
|
5
7
|
tokens: number;
|
|
8
|
+
/** Skill only activates when these toolsets are available (Hermes: requires_toolsets) */
|
|
9
|
+
requiresToolsets: string[];
|
|
10
|
+
/** Skill only activates when these toolsets are UNAVAILABLE (fallback path) */
|
|
11
|
+
fallbackForToolsets: string[];
|
|
12
|
+
/** OS platforms this skill supports; empty = all */
|
|
13
|
+
platforms: string[];
|
|
14
|
+
/** Related skill names */
|
|
15
|
+
relatedSkills: string[];
|
|
16
|
+
/** True for skills that ship with kbot or declare `metadata.kbot.*` — boosted in ranking */
|
|
17
|
+
native: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface SkillLoadContext {
|
|
20
|
+
/** Toolsets currently available — skills can require/fall-back based on these */
|
|
21
|
+
availableToolsets?: string[];
|
|
22
|
+
/** Current OS platform, e.g. 'darwin' | 'linux' | 'win32' */
|
|
23
|
+
platform?: string;
|
|
6
24
|
}
|
|
7
25
|
/**
|
|
8
|
-
* Discover and load skill files
|
|
9
|
-
*
|
|
26
|
+
* Discover and load skill files. Returns a prompt-ready string.
|
|
27
|
+
* When `message` is provided, skills are scored for relevance and only the
|
|
28
|
+
* most relevant are included (keeps token budget tight with a large library).
|
|
10
29
|
*/
|
|
11
|
-
export declare function loadSkills(projectRoot: string): string;
|
|
30
|
+
export declare function loadSkills(projectRoot: string, message?: string, ctx?: SkillLoadContext): string;
|
|
12
31
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
32
|
+
* Walk both skill roots and return every skill document found.
|
|
33
|
+
* Handles flat files (name.md) AND subdirectory layouts (cat/name/SKILL.md).
|
|
34
|
+
* Project skills take precedence over global skills with the same name.
|
|
15
35
|
*/
|
|
16
36
|
export declare function discoverSkillFiles(projectRoot: string): SkillFile[];
|
|
37
|
+
export interface ImportResult {
|
|
38
|
+
imported: number;
|
|
39
|
+
skipped: number;
|
|
40
|
+
source: string;
|
|
41
|
+
destination: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Copy (as symlinks) every SKILL.md under a foreign skills directory
|
|
45
|
+
* into ~/.kbot/skills/imported/<category>/<name>/SKILL.md.
|
|
46
|
+
* Non-destructive: existing symlinks are replaced, real files are skipped.
|
|
47
|
+
*/
|
|
48
|
+
export declare function importExternalSkills(sourceRoot: string): Promise<ImportResult>;
|
|
17
49
|
//# sourceMappingURL=skills-loader.d.ts.map
|
package/dist/skills-loader.js
CHANGED
|
@@ -1,92 +1,384 @@
|
|
|
1
|
-
// Skills Loader — Auto-discover and load
|
|
1
|
+
// Skills Loader — Auto-discover and load skill documents
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// Supports two formats:
|
|
4
|
+
// 1. kbot native: ~/.kbot/skills/<name>.md (flat)
|
|
5
|
+
// 2. agentskills.io: ~/.kbot/skills/<category>/<name>/SKILL.md (Claude/Hermes/Copilot standard)
|
|
6
|
+
//
|
|
7
|
+
// Frontmatter fields recognized (either format):
|
|
8
|
+
// name | title — skill identifier
|
|
9
|
+
// description — 1-line "when to use this"
|
|
10
|
+
// keywords | tags — list for relevance matching
|
|
11
|
+
// metadata.hermes.tags — agentskills.io tag list (Hermes/Claude)
|
|
12
|
+
// domain — kbot category
|
|
6
13
|
//
|
|
7
14
|
// Discovery locations (in priority order):
|
|
8
|
-
// 1. ./.kbot/skills
|
|
9
|
-
// 2. ~/.kbot/skills
|
|
15
|
+
// 1. ./.kbot/skills/ — project-specific
|
|
16
|
+
// 2. ~/.kbot/skills/ — user global (includes imported Hermes/Claude skills)
|
|
10
17
|
//
|
|
11
|
-
// Token budget: 2000 tokens max
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
import {
|
|
18
|
+
// Token budget: 2000 tokens max. When a message is provided, skills are
|
|
19
|
+
// scored for relevance and only the top matches are injected — so a user
|
|
20
|
+
// with 200 imported skills doesn't blow the budget on irrelevant docs.
|
|
21
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
22
|
+
import { join, basename, dirname } from 'node:path';
|
|
15
23
|
import { homedir } from 'node:os';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
/** Package-bundled skills directory — ships with the npm tarball. */
|
|
26
|
+
function getBundledSkillsDir() {
|
|
27
|
+
// dist/skills-loader.js → ../skills (package root)
|
|
28
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
return join(here, '..', 'skills');
|
|
30
|
+
}
|
|
16
31
|
const MAX_SKILL_TOKENS = 2000;
|
|
32
|
+
const MAX_SKILLS_WITHOUT_MESSAGE = 6; // unfiltered cap
|
|
33
|
+
const MAX_SKILLS_WITH_MESSAGE = 4; // relevance-filtered cap
|
|
17
34
|
const estimateTokens = (text) => Math.ceil(text.length / 4);
|
|
18
35
|
/**
|
|
19
|
-
* Discover and load skill files
|
|
20
|
-
*
|
|
36
|
+
* Discover and load skill files. Returns a prompt-ready string.
|
|
37
|
+
* When `message` is provided, skills are scored for relevance and only the
|
|
38
|
+
* most relevant are included (keeps token budget tight with a large library).
|
|
21
39
|
*/
|
|
22
|
-
export function loadSkills(projectRoot) {
|
|
23
|
-
const
|
|
24
|
-
if (
|
|
40
|
+
export function loadSkills(projectRoot, message, ctx) {
|
|
41
|
+
const all = discoverSkillFiles(projectRoot);
|
|
42
|
+
if (all.length === 0)
|
|
25
43
|
return '';
|
|
26
|
-
|
|
44
|
+
const filtered = applyConditionalActivation(all, ctx);
|
|
45
|
+
if (filtered.length === 0)
|
|
46
|
+
return '';
|
|
47
|
+
const selected = message ? rankByRelevance(filtered, message) : filtered;
|
|
48
|
+
return formatSkillsForPrompt(selected, message ? MAX_SKILLS_WITH_MESSAGE : MAX_SKILLS_WITHOUT_MESSAGE);
|
|
27
49
|
}
|
|
28
50
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
51
|
+
* Filter skills by platform and toolset conditions.
|
|
52
|
+
* Matches Hermes's activation semantics:
|
|
53
|
+
* - `platforms: [darwin]` — only loads on macOS
|
|
54
|
+
* - `requires_toolsets: [browser]` — only loads when browser tools are available
|
|
55
|
+
* - `fallback_for_toolsets: [browser]` — only loads when browser tools are NOT available
|
|
56
|
+
*/
|
|
57
|
+
function applyConditionalActivation(skills, ctx) {
|
|
58
|
+
const platform = ctx?.platform ?? process.platform;
|
|
59
|
+
const toolsets = new Set((ctx?.availableToolsets ?? []).map(t => t.toLowerCase()));
|
|
60
|
+
return skills.filter(s => {
|
|
61
|
+
if (s.platforms.length > 0 && !s.platforms.some(p => platform.startsWith(p)))
|
|
62
|
+
return false;
|
|
63
|
+
if (s.requiresToolsets.length > 0 && !s.requiresToolsets.every(t => toolsets.has(t.toLowerCase())))
|
|
64
|
+
return false;
|
|
65
|
+
if (s.fallbackForToolsets.length > 0 && s.fallbackForToolsets.some(t => toolsets.has(t.toLowerCase())))
|
|
66
|
+
return false;
|
|
67
|
+
return true;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Walk both skill roots and return every skill document found.
|
|
72
|
+
* Handles flat files (name.md) AND subdirectory layouts (cat/name/SKILL.md).
|
|
73
|
+
* Project skills take precedence over global skills with the same name.
|
|
31
74
|
*/
|
|
32
75
|
export function discoverSkillFiles(projectRoot) {
|
|
76
|
+
// Precedence (first wins on name collision):
|
|
77
|
+
// 1. project-local — most specific, author's own
|
|
78
|
+
// 2. bundled — kbot-curated skills shipping with the package
|
|
79
|
+
// 3. user-global — includes imported third-party skills (symlinks)
|
|
33
80
|
const locations = [
|
|
34
|
-
join(projectRoot, '.kbot', 'skills'),
|
|
35
|
-
|
|
81
|
+
join(projectRoot, '.kbot', 'skills'),
|
|
82
|
+
getBundledSkillsDir(),
|
|
83
|
+
join(homedir(), '.kbot', 'skills'),
|
|
36
84
|
];
|
|
37
85
|
const skills = [];
|
|
38
|
-
const seen = new Set();
|
|
39
|
-
for (const
|
|
40
|
-
if (!existsSync(
|
|
86
|
+
const seen = new Set();
|
|
87
|
+
for (const root of locations) {
|
|
88
|
+
if (!existsSync(root))
|
|
89
|
+
continue;
|
|
90
|
+
walkSkillRoot(root, skills, seen);
|
|
91
|
+
}
|
|
92
|
+
return skills;
|
|
93
|
+
}
|
|
94
|
+
function walkSkillRoot(root, out, seen, depth = 0) {
|
|
95
|
+
if (depth > 3)
|
|
96
|
+
return; // guard against deep nesting
|
|
97
|
+
let entries;
|
|
98
|
+
try {
|
|
99
|
+
entries = readdirSync(root);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
if (entry.startsWith('.') || entry === 'node_modules')
|
|
41
106
|
continue;
|
|
107
|
+
const full = join(root, entry);
|
|
108
|
+
let stat;
|
|
42
109
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (content.length === 0)
|
|
54
|
-
continue;
|
|
55
|
-
skills.push({
|
|
56
|
-
name: basename(file, '.md'),
|
|
57
|
-
path,
|
|
58
|
-
content,
|
|
59
|
-
tokens: estimateTokens(content),
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
catch { /* permission denied, etc. */ }
|
|
110
|
+
stat = statSync(full);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (stat.isDirectory()) {
|
|
116
|
+
// Subdirectory layout: look for SKILL.md first (agentskills.io standard)
|
|
117
|
+
const skillMd = join(full, 'SKILL.md');
|
|
118
|
+
if (existsSync(skillMd)) {
|
|
119
|
+
tryAddSkill(skillMd, entry, out, seen);
|
|
63
120
|
}
|
|
121
|
+
else {
|
|
122
|
+
// Category directory — recurse
|
|
123
|
+
walkSkillRoot(full, out, seen, depth + 1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else if (entry.endsWith('.md') && entry !== 'README.md') {
|
|
127
|
+
const name = basename(entry, '.md');
|
|
128
|
+
tryAddSkill(full, name, out, seen);
|
|
64
129
|
}
|
|
65
|
-
catch { /* directory read error */ }
|
|
66
130
|
}
|
|
67
|
-
|
|
131
|
+
}
|
|
132
|
+
function tryAddSkill(path, fallbackName, out, seen) {
|
|
133
|
+
let content;
|
|
134
|
+
try {
|
|
135
|
+
content = readFileSync(path, 'utf-8').trim();
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (!content)
|
|
141
|
+
return;
|
|
142
|
+
const parsed = parseFrontmatter(content);
|
|
143
|
+
const name = String(parsed.fm.name ?? parsed.fm.title ?? fallbackName).trim() || fallbackName;
|
|
144
|
+
if (seen.has(name))
|
|
145
|
+
return;
|
|
146
|
+
seen.add(name);
|
|
147
|
+
const description = String(parsed.fm.description ?? '').trim();
|
|
148
|
+
const tags = extractTags(parsed.fm);
|
|
149
|
+
const hermesMeta = parsed.fm.metadata?.hermes ?? {};
|
|
150
|
+
const kbotMeta = parsed.fm.metadata?.kbot ?? {};
|
|
151
|
+
const asList = (v) => {
|
|
152
|
+
if (Array.isArray(v))
|
|
153
|
+
return v.map(String).map(s => s.trim()).filter(Boolean);
|
|
154
|
+
if (typeof v === 'string')
|
|
155
|
+
return v.split(',').map(s => s.trim()).filter(Boolean);
|
|
156
|
+
return [];
|
|
157
|
+
};
|
|
158
|
+
const bundledDir = getBundledSkillsDir();
|
|
159
|
+
const native = path.startsWith(bundledDir) || Object.keys(kbotMeta).length > 0;
|
|
160
|
+
out.push({
|
|
161
|
+
name,
|
|
162
|
+
path,
|
|
163
|
+
content,
|
|
164
|
+
description,
|
|
165
|
+
tags,
|
|
166
|
+
tokens: estimateTokens(content),
|
|
167
|
+
requiresToolsets: [...asList(kbotMeta.requires_toolsets), ...asList(hermesMeta.requires_toolsets)],
|
|
168
|
+
fallbackForToolsets: [...asList(kbotMeta.fallback_for_toolsets), ...asList(hermesMeta.fallback_for_toolsets)],
|
|
169
|
+
platforms: asList(parsed.fm.platforms),
|
|
170
|
+
relatedSkills: [...asList(kbotMeta.related_skills), ...asList(hermesMeta.related_skills)],
|
|
171
|
+
native,
|
|
172
|
+
});
|
|
68
173
|
}
|
|
69
174
|
/**
|
|
70
|
-
*
|
|
175
|
+
* Lightweight YAML-frontmatter parser. Handles the subset skills use:
|
|
176
|
+
* key: value
|
|
177
|
+
* key: [a, b, c]
|
|
178
|
+
* metadata:
|
|
179
|
+
* hermes:
|
|
180
|
+
* tags: [x, y]
|
|
181
|
+
* Avoids pulling in a full YAML dep. Good enough for flat skill metadata.
|
|
71
182
|
*/
|
|
72
|
-
function
|
|
183
|
+
function parseFrontmatter(content) {
|
|
184
|
+
if (!content.startsWith('---'))
|
|
185
|
+
return { fm: {}, body: content };
|
|
186
|
+
const end = content.indexOf('\n---', 3);
|
|
187
|
+
if (end < 0)
|
|
188
|
+
return { fm: {}, body: content };
|
|
189
|
+
const yaml = content.slice(3, end).trim();
|
|
190
|
+
const body = content.slice(end + 4).trim();
|
|
191
|
+
const fm = {};
|
|
192
|
+
const stack = [{ indent: -1, obj: fm }];
|
|
193
|
+
for (const rawLine of yaml.split('\n')) {
|
|
194
|
+
const line = rawLine.replace(/\s+$/, '');
|
|
195
|
+
if (!line.trim() || line.trim().startsWith('#'))
|
|
196
|
+
continue;
|
|
197
|
+
const indent = line.match(/^ */)[0].length;
|
|
198
|
+
while (stack.length > 1 && indent <= stack[stack.length - 1].indent)
|
|
199
|
+
stack.pop();
|
|
200
|
+
const parent = stack[stack.length - 1].obj;
|
|
201
|
+
const m = line.trim().match(/^([A-Za-z0-9_\-]+)\s*:\s*(.*)$/);
|
|
202
|
+
if (!m)
|
|
203
|
+
continue;
|
|
204
|
+
const [, key, valueRaw] = m;
|
|
205
|
+
const value = valueRaw.trim();
|
|
206
|
+
if (!value) {
|
|
207
|
+
// Nested object
|
|
208
|
+
const child = {};
|
|
209
|
+
parent[key] = child;
|
|
210
|
+
stack.push({ indent, obj: child });
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
parent[key] = parseScalar(value);
|
|
214
|
+
}
|
|
215
|
+
return { fm, body };
|
|
216
|
+
}
|
|
217
|
+
function parseScalar(value) {
|
|
218
|
+
// Inline list: [a, b, "c"]
|
|
219
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
220
|
+
return value.slice(1, -1)
|
|
221
|
+
.split(',')
|
|
222
|
+
.map(s => s.trim().replace(/^["']|["']$/g, ''))
|
|
223
|
+
.filter(Boolean);
|
|
224
|
+
}
|
|
225
|
+
// Quoted string
|
|
226
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
227
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
228
|
+
return value.slice(1, -1);
|
|
229
|
+
}
|
|
230
|
+
return value;
|
|
231
|
+
}
|
|
232
|
+
function extractTags(fm) {
|
|
233
|
+
const tags = [];
|
|
234
|
+
const push = (v) => {
|
|
235
|
+
if (Array.isArray(v))
|
|
236
|
+
tags.push(...v.map(String));
|
|
237
|
+
else if (typeof v === 'string')
|
|
238
|
+
tags.push(...v.split(',').map(s => s.trim()).filter(Boolean));
|
|
239
|
+
};
|
|
240
|
+
push(fm.keywords);
|
|
241
|
+
push(fm.tags);
|
|
242
|
+
const meta = fm.metadata;
|
|
243
|
+
if (meta && typeof meta === 'object') {
|
|
244
|
+
push(meta.hermes?.tags);
|
|
245
|
+
push(meta.claude?.tags);
|
|
246
|
+
}
|
|
247
|
+
return [...new Set(tags.map(t => t.toLowerCase()))];
|
|
248
|
+
}
|
|
249
|
+
// ── Relevance scoring ────────────────────────────────────────────────
|
|
250
|
+
function rankByRelevance(skills, message) {
|
|
251
|
+
const terms = tokenize(message);
|
|
252
|
+
if (terms.size === 0)
|
|
253
|
+
return skills;
|
|
254
|
+
const scored = skills.map(s => ({ skill: s, score: scoreSkill(s, terms) }));
|
|
255
|
+
scored.sort((a, b) => b.score - a.score);
|
|
256
|
+
// Keep skills with at least one hit; fall back to top-N if nothing matches
|
|
257
|
+
const hits = scored.filter(x => x.score > 0);
|
|
258
|
+
return (hits.length > 0 ? hits : scored).map(x => x.skill);
|
|
259
|
+
}
|
|
260
|
+
function scoreSkill(skill, terms) {
|
|
261
|
+
let score = 0;
|
|
262
|
+
const nameTokens = tokenize(skill.name);
|
|
263
|
+
const descTokens = tokenize(skill.description);
|
|
264
|
+
for (const t of terms) {
|
|
265
|
+
if (nameTokens.has(t))
|
|
266
|
+
score += 3;
|
|
267
|
+
if (skill.tags.some(tag => tag === t || tag.includes(t)))
|
|
268
|
+
score += 2;
|
|
269
|
+
if (descTokens.has(t))
|
|
270
|
+
score += 1;
|
|
271
|
+
}
|
|
272
|
+
// Bundled kbot-native skills get a curated-content boost over imported third-party skills.
|
|
273
|
+
// Only applied when the skill already matched on something (score > 0) — we don't surface
|
|
274
|
+
// unrelated native skills over perfectly-matched imported ones.
|
|
275
|
+
if (score > 0 && skill.native)
|
|
276
|
+
score += 2;
|
|
277
|
+
return score;
|
|
278
|
+
}
|
|
279
|
+
const STOPWORDS = new Set([
|
|
280
|
+
'the', 'a', 'an', 'and', 'or', 'but', 'if', 'then', 'of', 'to', 'for', 'in', 'on', 'at',
|
|
281
|
+
'with', 'by', 'is', 'are', 'was', 'were', 'be', 'been', 'this', 'that', 'it', 'i', 'you',
|
|
282
|
+
'we', 'they', 'how', 'what', 'when', 'why', 'can', 'do', 'does', 'please',
|
|
283
|
+
]);
|
|
284
|
+
function tokenize(text) {
|
|
285
|
+
const out = new Set();
|
|
286
|
+
// Split on non-alphanumeric AND on kebab/snake separators so "daemon-deployment"
|
|
287
|
+
// and "skill_self_authorship" both yield useful word tokens.
|
|
288
|
+
for (const raw of text.toLowerCase().split(/[^a-z0-9]+/)) {
|
|
289
|
+
if (raw.length < 3)
|
|
290
|
+
continue;
|
|
291
|
+
if (STOPWORDS.has(raw))
|
|
292
|
+
continue;
|
|
293
|
+
out.add(raw);
|
|
294
|
+
}
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
297
|
+
// ── Prompt formatting ────────────────────────────────────────────────
|
|
298
|
+
function formatSkillsForPrompt(skills, maxSkills) {
|
|
73
299
|
const parts = [];
|
|
74
300
|
let currentTokens = 0;
|
|
301
|
+
let included = 0;
|
|
75
302
|
for (const skill of skills) {
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
|
|
303
|
+
if (included >= maxSkills)
|
|
304
|
+
break;
|
|
305
|
+
if (currentTokens + skill.tokens > MAX_SKILL_TOKENS) {
|
|
306
|
+
const remaining = MAX_SKILL_TOKENS - currentTokens;
|
|
79
307
|
if (remaining > 100) {
|
|
80
308
|
const truncated = skill.content.slice(0, remaining * 4) + '\n...(truncated)';
|
|
81
309
|
parts.push(`## Skill: ${skill.name}\n${truncated}`);
|
|
310
|
+
included++;
|
|
82
311
|
}
|
|
83
312
|
break;
|
|
84
313
|
}
|
|
85
314
|
parts.push(`## Skill: ${skill.name}\n${skill.content}`);
|
|
86
315
|
currentTokens += skill.tokens;
|
|
316
|
+
included++;
|
|
87
317
|
}
|
|
88
318
|
if (parts.length === 0)
|
|
89
319
|
return '';
|
|
90
320
|
return `\n\n[Custom Skills]\n${parts.join('\n---\n')}`;
|
|
91
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* Copy (as symlinks) every SKILL.md under a foreign skills directory
|
|
324
|
+
* into ~/.kbot/skills/imported/<category>/<name>/SKILL.md.
|
|
325
|
+
* Non-destructive: existing symlinks are replaced, real files are skipped.
|
|
326
|
+
*/
|
|
327
|
+
export async function importExternalSkills(sourceRoot) {
|
|
328
|
+
const { mkdirSync, symlinkSync, unlinkSync, lstatSync } = await import('node:fs');
|
|
329
|
+
const destRoot = join(homedir(), '.kbot', 'skills', 'imported');
|
|
330
|
+
mkdirSync(destRoot, { recursive: true });
|
|
331
|
+
let imported = 0;
|
|
332
|
+
let skipped = 0;
|
|
333
|
+
const walk = (dir, rel) => {
|
|
334
|
+
let entries;
|
|
335
|
+
try {
|
|
336
|
+
entries = readdirSync(dir);
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
for (const e of entries) {
|
|
342
|
+
if (e.startsWith('.'))
|
|
343
|
+
continue;
|
|
344
|
+
const full = join(dir, e);
|
|
345
|
+
let st;
|
|
346
|
+
try {
|
|
347
|
+
st = statSync(full);
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (st.isDirectory()) {
|
|
353
|
+
const skillMd = join(full, 'SKILL.md');
|
|
354
|
+
if (existsSync(skillMd)) {
|
|
355
|
+
const destDir = join(destRoot, rel, e);
|
|
356
|
+
mkdirSync(destDir, { recursive: true });
|
|
357
|
+
const destFile = join(destDir, 'SKILL.md');
|
|
358
|
+
try {
|
|
359
|
+
if (existsSync(destFile)) {
|
|
360
|
+
const ls = lstatSync(destFile);
|
|
361
|
+
if (ls.isSymbolicLink())
|
|
362
|
+
unlinkSync(destFile);
|
|
363
|
+
else {
|
|
364
|
+
skipped++;
|
|
365
|
+
continue;
|
|
366
|
+
} // don't clobber user-authored file
|
|
367
|
+
}
|
|
368
|
+
symlinkSync(skillMd, destFile);
|
|
369
|
+
imported++;
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
skipped++;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
walk(full, join(rel, e));
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
walk(sourceRoot, '');
|
|
382
|
+
return { imported, skipped, source: sourceRoot, destination: destRoot };
|
|
383
|
+
}
|
|
92
384
|
//# sourceMappingURL=skills-loader.js.map
|
package/dist/train-self.js
CHANGED
|
@@ -193,7 +193,7 @@ export async function trainSelf(opts = {}) {
|
|
|
193
193
|
`SYSTEM "You are kbot's self-trained assistant (${mode} mode). You were fine-tuned on the operator's own agent sessions."`,
|
|
194
194
|
].join('\n');
|
|
195
195
|
try {
|
|
196
|
-
|
|
196
|
+
writeFileSync(modelfilePath, modelfile);
|
|
197
197
|
const cmd = `ollama create ${outputName} -f ${modelfilePath}`;
|
|
198
198
|
const r = shell(cmd);
|
|
199
199
|
log('deploy', r.ok, Date.now() - t0, r.output.split('\n').slice(-8).join('\n'));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kernel.chat/kbot",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.99.0",
|
|
4
4
|
"description": "Open-source terminal AI agent. 787+ tools, 35 agents, 20 providers. Dreams, learns, watches your system. Controls your phone. Fully local, fully sovereign. MIT.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -103,6 +103,7 @@
|
|
|
103
103
|
"!dist/**/*.test.d.ts",
|
|
104
104
|
"!dist/**/*.js.map",
|
|
105
105
|
"!dist/**/*.d.ts.map",
|
|
106
|
+
"skills/**/*.md",
|
|
106
107
|
"README.md",
|
|
107
108
|
"install.sh",
|
|
108
109
|
"ollama-manifest.json"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: daemon-deployment
|
|
3
|
+
description: Use when setting up 24/7 background workers. kbot's compound improvement depends on daemons running even when nobody is looking.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
author: kbot
|
|
6
|
+
license: MIT
|
|
7
|
+
platforms: [darwin, linux]
|
|
8
|
+
metadata:
|
|
9
|
+
kbot:
|
|
10
|
+
tags: [daemon, launchd, background, 24-7, compound]
|
|
11
|
+
related_skills: [autopoiesis-loop, teacher-trace-curation]
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Daemon Deployment
|
|
15
|
+
|
|
16
|
+
kbot's intelligence doesn't sleep. Three daemons run continuously:
|
|
17
|
+
- `kbot-daemon` — code quality, i18n sync, embeddings, docs gaps (every 15 min)
|
|
18
|
+
- `kbot-discovery-daemon` — self-advocacy, field intelligence (every 15 min → 24 hr cycles)
|
|
19
|
+
- `kbot-social-daemon` — autonomous posting to X/Bluesky/Mastodon/LinkedIn (daily)
|
|
20
|
+
|
|
21
|
+
Plus a weekly `train-self` that fine-tunes the local model on curated traces.
|
|
22
|
+
|
|
23
|
+
## Iron Law
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
VERIFY THE DAEMON IS ACTUALLY RUNNING AFTER INSTALL.
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
launchd plists that fail silently are the single most common cause of "kbot feels stale" — the daemon was never loaded, so no compound improvement happened.
|
|
30
|
+
|
|
31
|
+
## Install Sequence (macOS)
|
|
32
|
+
|
|
33
|
+
1. `npm run daemon` — run once manually to confirm it works at all.
|
|
34
|
+
2. `npm run daemon:start` — loads the launchd plist.
|
|
35
|
+
3. **Verify**: `launchctl list | grep kernel.kbot` — should show a running entry with PID.
|
|
36
|
+
4. **Confirm output**: `tail -f tools/daemon-reports/daemon.log` — should show activity within 15 min.
|
|
37
|
+
5. **Check state**: `npm run daemon:stats` — shows task timestamps, token usage, cost savings.
|
|
38
|
+
|
|
39
|
+
If any of steps 3–5 fail, the daemon is not actually running. Debug before moving on.
|
|
40
|
+
|
|
41
|
+
## Per-Daemon Triggers
|
|
42
|
+
|
|
43
|
+
- `com.kernel.kbot-daemon.plist` → every 15 min
|
|
44
|
+
- `com.kernel.kbot-discovery.plist` → every 15 min (internal sub-cycles stagger)
|
|
45
|
+
- `com.kernel.kbot-social.plist` → daily at 9am
|
|
46
|
+
- `com.kernel.kbot-train-self.plist` → Sundays 3am
|
|
47
|
+
|
|
48
|
+
## What You Gain
|
|
49
|
+
|
|
50
|
+
- Daily digest of codebase activity (without asking).
|
|
51
|
+
- i18n stays in sync across 24 languages automatically.
|
|
52
|
+
- Embedding index for semantic search rebuilds overnight.
|
|
53
|
+
- Social presence grows without manual posting.
|
|
54
|
+
- Local fine-tune stays current with your actual work.
|
|
55
|
+
|
|
56
|
+
Cost: zero. All daemon work routes through local Ollama models.
|
|
57
|
+
|
|
58
|
+
## Failure Modes
|
|
59
|
+
|
|
60
|
+
- **Mac sleep blocks launchd** — use `caffeinate` or enable "wake for network access" in Energy Saver if you need guaranteed intervals.
|
|
61
|
+
- **Ollama not running** — daemons depend on `localhost:11434`. Either start Ollama at login OR the daemon silently no-ops. Add Ollama to Login Items.
|
|
62
|
+
- **Filesystem permission errors** — daemon's user context may differ from your shell. Absolute paths in plist, check `~/.kbot/` is writable by the daemon user.
|
|
63
|
+
|
|
64
|
+
## Rollback
|
|
65
|
+
|
|
66
|
+
`npm run daemon:stop` unloads the plist. Work resumes manually. No state is lost — `tools/daemon-reports/state.json` persists until the daemon re-enables.
|
|
67
|
+
|
|
68
|
+
## What Emerges
|
|
69
|
+
|
|
70
|
+
After two weeks of active daemons, the user finds: i18n is always current, the daily digest email is actually read, social posts have engagement, the local model passes a basic task without Claude. The compound output is larger than any single feature could produce.
|