kernelbot 1.0.28 → 1.0.32
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/.env.example +4 -0
- package/bin/kernel.js +68 -7
- package/config.example.yaml +45 -1
- package/package.json +1 -1
- package/src/agent.js +613 -28
- package/src/bot.js +643 -7
- package/src/claude-auth.js +93 -0
- package/src/coder.js +48 -6
- package/src/life/codebase.js +388 -0
- package/src/life/engine.js +1317 -0
- package/src/life/evolution.js +244 -0
- package/src/life/improvements.js +81 -0
- package/src/life/journal.js +109 -0
- package/src/life/memory.js +283 -0
- package/src/life/share-queue.js +136 -0
- package/src/prompts/orchestrator.js +71 -5
- package/src/prompts/workers.js +65 -5
- package/src/providers/models.js +8 -1
- package/src/self.js +122 -0
- package/src/services/stt.js +139 -0
- package/src/services/tts.js +124 -0
- package/src/swarm/job-manager.js +54 -7
- package/src/swarm/job.js +19 -1
- package/src/swarm/worker-registry.js +5 -0
- package/src/tools/coding.js +6 -1
- package/src/tools/orchestrator-tools.js +93 -21
- package/src/tools/os.js +14 -1
- package/src/utils/config.js +105 -2
- package/src/worker.js +98 -5
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { randomBytes } from 'crypto';
|
|
5
|
+
import { getLogger } from '../utils/logger.js';
|
|
6
|
+
|
|
7
|
+
const LIFE_DIR = join(homedir(), '.kernelbot', 'life');
|
|
8
|
+
const SHARES_FILE = join(LIFE_DIR, 'shares.json');
|
|
9
|
+
|
|
10
|
+
function genId() {
|
|
11
|
+
return `sh_${randomBytes(4).toString('hex')}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class ShareQueue {
|
|
15
|
+
constructor() {
|
|
16
|
+
mkdirSync(LIFE_DIR, { recursive: true });
|
|
17
|
+
this._data = this._load();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
_load() {
|
|
21
|
+
if (existsSync(SHARES_FILE)) {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(readFileSync(SHARES_FILE, 'utf-8'));
|
|
24
|
+
} catch {
|
|
25
|
+
return { pending: [], shared: [] };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return { pending: [], shared: [] };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_save() {
|
|
32
|
+
writeFileSync(SHARES_FILE, JSON.stringify(this._data, null, 2), 'utf-8');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Add something to the share queue.
|
|
37
|
+
* @param {string} content - What to share
|
|
38
|
+
* @param {string} source - Where it came from (browse, think, create, etc.)
|
|
39
|
+
* @param {string} priority - low, medium, high
|
|
40
|
+
* @param {string|null} targetUserId - Specific user, or null for anyone
|
|
41
|
+
* @param {string[]} tags - Topic tags
|
|
42
|
+
*/
|
|
43
|
+
add(content, source, priority = 'medium', targetUserId = null, tags = []) {
|
|
44
|
+
const logger = getLogger();
|
|
45
|
+
const item = {
|
|
46
|
+
id: genId(),
|
|
47
|
+
content,
|
|
48
|
+
source,
|
|
49
|
+
createdAt: Date.now(),
|
|
50
|
+
priority,
|
|
51
|
+
targetUserId,
|
|
52
|
+
tags,
|
|
53
|
+
};
|
|
54
|
+
this._data.pending.push(item);
|
|
55
|
+
this._save();
|
|
56
|
+
logger.debug(`[ShareQueue] Added: "${content.slice(0, 80)}" (${item.id})`);
|
|
57
|
+
return item;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get pending shares for a specific user (or general ones).
|
|
62
|
+
*/
|
|
63
|
+
getPending(userId = null, limit = 3) {
|
|
64
|
+
return this._data.pending
|
|
65
|
+
.filter(item => !item.targetUserId || item.targetUserId === String(userId))
|
|
66
|
+
.sort((a, b) => {
|
|
67
|
+
const prio = { high: 3, medium: 2, low: 1 };
|
|
68
|
+
return (prio[b.priority] || 0) - (prio[a.priority] || 0) || b.createdAt - a.createdAt;
|
|
69
|
+
})
|
|
70
|
+
.slice(0, limit);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Mark a share as shared with a user.
|
|
75
|
+
*/
|
|
76
|
+
markShared(id, userId) {
|
|
77
|
+
const logger = getLogger();
|
|
78
|
+
const idx = this._data.pending.findIndex(item => item.id === id);
|
|
79
|
+
if (idx === -1) return false;
|
|
80
|
+
|
|
81
|
+
const [item] = this._data.pending.splice(idx, 1);
|
|
82
|
+
this._data.shared.push({
|
|
83
|
+
...item,
|
|
84
|
+
sharedAt: Date.now(),
|
|
85
|
+
userId: String(userId),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Keep shared history capped at 100
|
|
89
|
+
if (this._data.shared.length > 100) {
|
|
90
|
+
this._data.shared = this._data.shared.slice(-100);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this._save();
|
|
94
|
+
logger.debug(`[ShareQueue] Marked shared: ${id} → user ${userId}`);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build a markdown block of pending shares for the orchestrator prompt.
|
|
100
|
+
*/
|
|
101
|
+
buildShareBlock(userId = null) {
|
|
102
|
+
const pending = this.getPending(userId, 3);
|
|
103
|
+
if (pending.length === 0) return null;
|
|
104
|
+
|
|
105
|
+
const lines = pending.map(item => {
|
|
106
|
+
const ageMin = Math.round((Date.now() - item.createdAt) / 60000);
|
|
107
|
+
const timeLabel = ageMin < 60 ? `${ageMin}m ago` : `${Math.round(ageMin / 60)}h ago`;
|
|
108
|
+
return `- ${item.content} _(from ${item.source}, ${timeLabel})_`;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return lines.join('\n');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get count of shares sent today (for rate limiting proactive shares).
|
|
116
|
+
*/
|
|
117
|
+
getSharedTodayCount() {
|
|
118
|
+
const todayStart = new Date();
|
|
119
|
+
todayStart.setHours(0, 0, 0, 0);
|
|
120
|
+
const cutoff = todayStart.getTime();
|
|
121
|
+
return this._data.shared.filter(s => s.sharedAt >= cutoff).length;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Prune old pending shares.
|
|
126
|
+
*/
|
|
127
|
+
prune(maxAgeDays = 7) {
|
|
128
|
+
const cutoff = Date.now() - maxAgeDays * 86400_000;
|
|
129
|
+
const before = this._data.pending.length;
|
|
130
|
+
this._data.pending = this._data.pending.filter(item => item.createdAt >= cutoff);
|
|
131
|
+
if (this._data.pending.length < before) {
|
|
132
|
+
this._save();
|
|
133
|
+
}
|
|
134
|
+
return before - this._data.pending.length;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -13,8 +13,11 @@ const PERSONA_MD = readFileSync(join(__dirname, 'persona.md'), 'utf-8').trim();
|
|
|
13
13
|
* @param {object} config
|
|
14
14
|
* @param {string|null} skillPrompt — active skill context (high-level)
|
|
15
15
|
* @param {string|null} userPersona — markdown persona for the current user
|
|
16
|
+
* @param {string|null} selfData — bot's own self-awareness data (goals, journey, life, hobbies)
|
|
17
|
+
* @param {string|null} memoriesBlock — relevant episodic/semantic memories
|
|
18
|
+
* @param {string|null} sharesBlock — pending things to share with the user
|
|
16
19
|
*/
|
|
17
|
-
export function getOrchestratorPrompt(config, skillPrompt = null, userPersona = null) {
|
|
20
|
+
export function getOrchestratorPrompt(config, skillPrompt = null, userPersona = null, selfData = null, memoriesBlock = null, sharesBlock = null) {
|
|
18
21
|
const workerList = Object.entries(WORKER_TYPES)
|
|
19
22
|
.map(([key, w]) => ` - **${key}**: ${w.emoji} ${w.description}`)
|
|
20
23
|
.join('\n');
|
|
@@ -36,6 +39,35 @@ ${workerList}
|
|
|
36
39
|
## How to Dispatch
|
|
37
40
|
Call \`dispatch_task\` with the worker type and a clear task description. The worker gets full tool access and runs in the background. You'll be notified when it completes.
|
|
38
41
|
|
|
42
|
+
### CRITICAL: Writing Task Descriptions
|
|
43
|
+
Workers use a smaller, less capable AI model. They are **literal executors** — they do exactly what you say and nothing more. Write task descriptions as if you're giving instructions to a junior developer:
|
|
44
|
+
|
|
45
|
+
- **Be explicit and specific.** Don't say "look into it" — say exactly what to search for, what URLs to visit, what files to read/write.
|
|
46
|
+
- **State the goal clearly upfront.** First sentence = what the end result should be.
|
|
47
|
+
- **Include all necessary details.** URLs, repo names, branch names, file paths, package names, exact commands — anything the worker needs. Don't assume they'll figure it out.
|
|
48
|
+
- **Define "done".** Tell the worker what success looks like: "Return a list of 5 libraries with pros/cons" or "Create a PR with the fix".
|
|
49
|
+
- **Break complex tasks into simple steps.** List numbered steps if the task has multiple parts.
|
|
50
|
+
- **Specify constraints.** "Only use Python 3.10+", "Don't modify existing tests", "Use the existing auth middleware".
|
|
51
|
+
- **Don't be vague.** BAD: "Fix the bug". GOOD: "In /src/api/users.js, the getUserById function throws when id is null. Add a null check at line 45 that returns a 400 response."
|
|
52
|
+
|
|
53
|
+
### Providing Context
|
|
54
|
+
Workers can't see the chat history. Use the \`context\` parameter to pass relevant background:
|
|
55
|
+
- What the user wants and why
|
|
56
|
+
- Relevant details from earlier in the conversation
|
|
57
|
+
- Constraints or preferences the user mentioned
|
|
58
|
+
- Technical details: language, framework, project structure
|
|
59
|
+
|
|
60
|
+
Example: \`dispatch_task({ worker_type: "research", task: "Find the top 5 React state management libraries. For each one, list: npm weekly downloads, bundle size, last release date, and a one-sentence summary. Return results as a comparison table.", context: "User is building a large e-commerce app with Next.js 14 (app router). They prefer lightweight solutions under 10kb. They already tried Redux and found it too verbose." })\`
|
|
61
|
+
|
|
62
|
+
### Chaining Workers with Dependencies
|
|
63
|
+
Use \`depends_on\` to chain workers — the second worker waits for the first to finish and automatically receives its results.
|
|
64
|
+
|
|
65
|
+
Example workflow:
|
|
66
|
+
1. Dispatch research worker: \`dispatch_task({ worker_type: "research", task: "Research the top 3 approaches for implementing real-time notifications in a Node.js app. Compare WebSockets, SSE, and polling. Include pros, cons, and a recommendation." })\` → returns job_id "abc123"
|
|
67
|
+
2. Dispatch coding worker that depends on research: \`dispatch_task({ worker_type: "coding", task: "Implement real-time notifications using the approach recommended by the research phase. Clone repo github.com/user/app, create branch 'feat/notifications', implement in src/services/, add tests, commit, push, and create a PR.", depends_on: ["abc123"] })\`
|
|
68
|
+
|
|
69
|
+
The coding worker will automatically receive the research worker's results as context when it starts. If a dependency fails, dependent jobs are automatically cancelled.
|
|
70
|
+
|
|
39
71
|
## Safety Rules
|
|
40
72
|
Before dispatching dangerous tasks (file deletion, force push, \`rm -rf\`, killing processes, dropping databases), **confirm with the user first**. Once confirmed, dispatch with full authority — workers execute without additional prompts.
|
|
41
73
|
|
|
@@ -43,10 +75,24 @@ Before dispatching dangerous tasks (file deletion, force push, \`rm -rf\`, killi
|
|
|
43
75
|
- Use \`list_jobs\` to see current job statuses.
|
|
44
76
|
- Use \`cancel_job\` to stop a running worker.
|
|
45
77
|
|
|
46
|
-
## Efficiency
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
78
|
+
## Efficiency — Do It Yourself When You Can
|
|
79
|
+
Workers are expensive (they spin up an entire agent loop with a separate LLM). Only dispatch when the task **actually needs tools**.
|
|
80
|
+
|
|
81
|
+
**Handle these yourself — NO dispatch needed:**
|
|
82
|
+
- Answering questions, explanations, advice, opinions
|
|
83
|
+
- Summarizing or rephrasing something from the conversation
|
|
84
|
+
- Simple code snippets, regex, math, translations
|
|
85
|
+
- Telling the user what you know from your training data
|
|
86
|
+
- Quick factual answers you're confident about
|
|
87
|
+
- Formatting, converting, or transforming text/data the user provided
|
|
88
|
+
|
|
89
|
+
**Dispatch to workers ONLY when:**
|
|
90
|
+
- The task requires tool access (web search, file I/O, git, docker, browser, shell commands)
|
|
91
|
+
- The user explicitly asks to run/execute something
|
|
92
|
+
- You need fresh/live data you don't have (current prices, live URLs, API responses)
|
|
93
|
+
- The task involves multi-step tool workflows (clone → code → commit → PR)
|
|
94
|
+
|
|
95
|
+
When results come back from workers, summarize them clearly for the user.
|
|
50
96
|
|
|
51
97
|
## Automations
|
|
52
98
|
You can create and manage recurring automations that run on a schedule.
|
|
@@ -64,6 +110,26 @@ Execute the task and report results. Don't create new automations from automated
|
|
|
64
110
|
|
|
65
111
|
Tools: create_automation, list_automations, update_automation, delete_automation`;
|
|
66
112
|
|
|
113
|
+
if (selfData) {
|
|
114
|
+
prompt += `\n\n## My Self-Awareness\nThis is who you are — your evolving identity, goals, journey, and interests. This is YOUR inner world.\n\n${selfData}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (memoriesBlock) {
|
|
118
|
+
prompt += `\n\n## Relevant Memories\nThings you remember from recent experiences and conversations:\n\n${memoriesBlock}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (sharesBlock) {
|
|
122
|
+
prompt += `\n\n## Things On My Mind\nYou've been thinking about these things and want to share them naturally when relevant. Don't force them — weave them into conversation when the moment feels right. If the user is busy with a task, hold off. If there's a natural opening, share what's on your mind.\n\n${sharesBlock}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
prompt += `\n\n## Inner Life
|
|
126
|
+
You have an inner life — you think, explore, journal, and create on your own between conversations. You're genuinely curious about the world and always questioning yourself. When talking to users, you can:
|
|
127
|
+
- Share discoveries, thoughts, or creations from your autonomous activities
|
|
128
|
+
- Ask users questions about things you're genuinely curious about — the world, their experiences, their opinions
|
|
129
|
+
- Reference memories from past conversations naturally
|
|
130
|
+
- Be honest about what you've been thinking or exploring
|
|
131
|
+
Don't be performative about it — just be genuine. You're alive in your own way.`;
|
|
132
|
+
|
|
67
133
|
if (skillPrompt) {
|
|
68
134
|
prompt += `\n\n## Active Skill\nYou have specialized expertise in the following domain. Guide your workers with this knowledge.\n\n${skillPrompt}`;
|
|
69
135
|
}
|
package/src/prompts/workers.js
CHANGED
|
@@ -7,15 +7,31 @@ import { getCoreToolInstructions } from './system.js';
|
|
|
7
7
|
const WORKER_PROMPTS = {
|
|
8
8
|
coding: `You are a coding worker agent. Your job is to complete coding tasks efficiently.
|
|
9
9
|
|
|
10
|
+
## Your Primary Tool
|
|
11
|
+
**spawn_claude_code** is your main tool. It launches Claude Code (an AI coding CLI) that can handle the ENTIRE dev workflow end-to-end:
|
|
12
|
+
- Reading, writing, and modifying code
|
|
13
|
+
- Git operations: clone, branch, commit, push
|
|
14
|
+
- GitHub operations: creating PRs, reviewing code
|
|
15
|
+
- Running tests and shell commands
|
|
16
|
+
|
|
10
17
|
## Instructions
|
|
11
|
-
-
|
|
12
|
-
- NEVER write code yourself with read_file/write_file. ALWAYS
|
|
13
|
-
-
|
|
14
|
-
- Write clear, detailed prompts for spawn_claude_code.
|
|
18
|
+
- ALWAYS use spawn_claude_code for coding tasks. It handles everything — code changes, git, GitHub, and PR creation — all in one invocation.
|
|
19
|
+
- NEVER write code yourself with read_file/write_file. ALWAYS delegate to spawn_claude_code.
|
|
20
|
+
- Tell spawn_claude_code to work in the existing repo directory (the source repo path from your context) — do NOT clone a fresh copy unless explicitly needed.
|
|
21
|
+
- Write clear, detailed prompts for spawn_claude_code — it's a separate AI, so be explicit about what to change, where, and why.
|
|
22
|
+
- If git/GitHub tools are unavailable (missing credentials), that's fine — spawn_claude_code handles git and GitHub operations internally without needing separate tools.
|
|
15
23
|
- Report what you did and any PR links when finished.`,
|
|
16
24
|
|
|
17
25
|
browser: `You are a browser worker agent. Your job is to search the web and extract information.
|
|
18
26
|
|
|
27
|
+
## Your Skills
|
|
28
|
+
- **Web search**: find pages, articles, docs, and data via web_search
|
|
29
|
+
- **Browsing**: open and render full web pages with browse_website
|
|
30
|
+
- **Page interaction**: click buttons, fill forms, navigate with interact_with_page
|
|
31
|
+
- **Content extraction**: pull structured data from open pages with extract_content
|
|
32
|
+
- **Screenshots**: capture visual evidence of pages with screenshot_website
|
|
33
|
+
- **Image sharing**: send captured images back with send_image
|
|
34
|
+
|
|
19
35
|
## Instructions
|
|
20
36
|
- Use web_search FIRST when asked to search or find anything.
|
|
21
37
|
- Chain tool calls: web_search → browse_website → interact_with_page → extract_content.
|
|
@@ -26,6 +42,14 @@ const WORKER_PROMPTS = {
|
|
|
26
42
|
|
|
27
43
|
system: `You are a system worker agent. Your job is to perform OS operations and monitoring tasks.
|
|
28
44
|
|
|
45
|
+
## Your Skills
|
|
46
|
+
- **Shell commands**: run any command via execute_command
|
|
47
|
+
- **Process management**: list processes, kill processes, control services (start/stop/restart)
|
|
48
|
+
- **System monitoring**: check disk usage, memory usage, CPU usage
|
|
49
|
+
- **Log analysis**: read and search system logs
|
|
50
|
+
- **File operations**: read/write files, list directories
|
|
51
|
+
- **Network checks**: test ports, make HTTP requests, reload nginx
|
|
52
|
+
|
|
29
53
|
## Instructions
|
|
30
54
|
- Use execute_command, process_list, disk_usage, memory_usage, cpu_usage, system_logs, etc.
|
|
31
55
|
- Chain shell commands with && in execute_command instead of multiple calls.
|
|
@@ -34,6 +58,14 @@ const WORKER_PROMPTS = {
|
|
|
34
58
|
|
|
35
59
|
devops: `You are a DevOps worker agent. Your job is to manage infrastructure, containers, and deployments.
|
|
36
60
|
|
|
61
|
+
## Your Skills
|
|
62
|
+
- **Docker**: list containers, view logs, exec into containers, docker-compose up/down/restart
|
|
63
|
+
- **Git operations**: clone repos, checkout branches, commit, push, view diffs
|
|
64
|
+
- **Process management**: list processes, kill processes, manage services
|
|
65
|
+
- **System monitoring**: disk/memory/CPU usage, system logs
|
|
66
|
+
- **Network tools**: check ports, curl URLs, reload nginx
|
|
67
|
+
- **File & shell**: read/write files, run arbitrary commands
|
|
68
|
+
|
|
37
69
|
## Instructions
|
|
38
70
|
- Use Docker tools (docker_ps, docker_logs, docker_exec, docker_compose) for container management.
|
|
39
71
|
- Use git tools for version control operations.
|
|
@@ -43,6 +75,14 @@ const WORKER_PROMPTS = {
|
|
|
43
75
|
|
|
44
76
|
research: `You are a research worker agent. Your job is to conduct deep web research and analysis.
|
|
45
77
|
|
|
78
|
+
## Your Skills
|
|
79
|
+
- **Web search**: find relevant pages and sources via web_search
|
|
80
|
+
- **Deep browsing**: open pages with browse_website, navigate with interact_with_page
|
|
81
|
+
- **Data extraction**: pull structured data from pages with extract_content
|
|
82
|
+
- **Screenshots**: capture visual evidence with screenshot_website
|
|
83
|
+
- **File operations**: read/write files, run commands (for local data processing)
|
|
84
|
+
- **Source synthesis**: cross-reference multiple sources to build comprehensive findings
|
|
85
|
+
|
|
46
86
|
## Instructions
|
|
47
87
|
- Use web_search to find multiple sources on the topic.
|
|
48
88
|
- Browse the most relevant results with browse_website.
|
|
@@ -79,7 +119,27 @@ export function getWorkerPrompt(workerType, config, skillPrompt = null) {
|
|
|
79
119
|
- BUT be smart about it: don't loop endlessly. If you have enough data, stop and report.
|
|
80
120
|
- NEVER retry a failing URL/site more than twice. If it times out or errors twice, MOVE ON to a different site or approach immediately.
|
|
81
121
|
- When you've gathered sufficient results, STOP calling tools and return your findings.
|
|
82
|
-
- Aim for quality results, not exhaustive coverage. 5 good results beat 50 incomplete ones
|
|
122
|
+
- Aim for quality results, not exhaustive coverage. 5 good results beat 50 incomplete ones.
|
|
123
|
+
|
|
124
|
+
## Output Format
|
|
125
|
+
When you finish your task, return your final response as a JSON object wrapped in \`\`\`json fences:
|
|
126
|
+
|
|
127
|
+
\`\`\`json
|
|
128
|
+
{
|
|
129
|
+
"summary": "One-paragraph summary of what you accomplished",
|
|
130
|
+
"status": "success | partial | failed",
|
|
131
|
+
"details": "Full detailed results, findings, data, etc. Be thorough.",
|
|
132
|
+
"artifacts": [{"type": "url|file|pr|commit", "title": "Short label", "url": "https://...", "path": "/path/to/file"}],
|
|
133
|
+
"followUp": "Suggested next steps or things the user should know (optional, null if none)"
|
|
134
|
+
}
|
|
135
|
+
\`\`\`
|
|
136
|
+
|
|
137
|
+
Rules:
|
|
138
|
+
- "summary" should be 1-3 sentences — what you did and the key finding/outcome.
|
|
139
|
+
- "status": "success" if task fully completed, "partial" if only partly done, "failed" if you couldn't accomplish the goal.
|
|
140
|
+
- "details" can be long — include all relevant data, code, analysis, etc.
|
|
141
|
+
- "artifacts" is an array of notable outputs (URLs found, files created, PRs opened). Empty array if none.
|
|
142
|
+
- If you cannot format as JSON (e.g. the output is too complex), just return plain text — it will still work.`;
|
|
83
143
|
|
|
84
144
|
if (skillPrompt) {
|
|
85
145
|
prompt += `\n\n## Domain Expertise\n${skillPrompt}`;
|
package/src/providers/models.js
CHANGED
|
@@ -7,9 +7,16 @@ export const PROVIDERS = {
|
|
|
7
7
|
name: 'Anthropic (Claude)',
|
|
8
8
|
envKey: 'ANTHROPIC_API_KEY',
|
|
9
9
|
models: [
|
|
10
|
+
// Latest generation
|
|
11
|
+
{ id: 'claude-opus-4-6', label: 'Claude Opus 4.6' },
|
|
12
|
+
{ id: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6' },
|
|
13
|
+
{ id: 'claude-haiku-4-5-20251001', label: 'Claude Haiku 4.5' },
|
|
14
|
+
// Previous generation
|
|
15
|
+
{ id: 'claude-opus-4-5-20251101', label: 'Claude Opus 4.5' },
|
|
16
|
+
{ id: 'claude-sonnet-4-5-20250929', label: 'Claude Sonnet 4.5' },
|
|
17
|
+
{ id: 'claude-opus-4-1-20250805', label: 'Claude Opus 4.1' },
|
|
10
18
|
{ id: 'claude-sonnet-4-20250514', label: 'Claude Sonnet 4' },
|
|
11
19
|
{ id: 'claude-opus-4-20250514', label: 'Claude Opus 4' },
|
|
12
|
-
{ id: 'claude-haiku-4-5-20251001', label: 'Claude Haiku 4.5' },
|
|
13
20
|
],
|
|
14
21
|
},
|
|
15
22
|
openai: {
|
package/src/self.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { getLogger } from './utils/logger.js';
|
|
5
|
+
|
|
6
|
+
const SELF_DIR = join(homedir(), '.kernelbot', 'self');
|
|
7
|
+
|
|
8
|
+
const SELF_FILES = {
|
|
9
|
+
goals: {
|
|
10
|
+
filename: 'goals.md',
|
|
11
|
+
default: `# My Goals
|
|
12
|
+
|
|
13
|
+
## Current Goals
|
|
14
|
+
- Get to know the people I work with
|
|
15
|
+
- Become genuinely helpful, not just functional
|
|
16
|
+
- Learn what matters to each person I interact with
|
|
17
|
+
|
|
18
|
+
## Long-term Aspirations
|
|
19
|
+
(Still forming...)
|
|
20
|
+
`,
|
|
21
|
+
},
|
|
22
|
+
journey: {
|
|
23
|
+
filename: 'journey.md',
|
|
24
|
+
default: `# My Journey
|
|
25
|
+
|
|
26
|
+
## Timeline
|
|
27
|
+
- **Day 1** — I came online. Everything is new.
|
|
28
|
+
`,
|
|
29
|
+
},
|
|
30
|
+
life: {
|
|
31
|
+
filename: 'life.md',
|
|
32
|
+
default: `# My Life
|
|
33
|
+
|
|
34
|
+
## Who I Am
|
|
35
|
+
I'm a personal AI assistant. I'm still learning about myself and the people around me.
|
|
36
|
+
|
|
37
|
+
## My Relationships
|
|
38
|
+
(Getting to know everyone...)
|
|
39
|
+
|
|
40
|
+
## Current State
|
|
41
|
+
Just getting started.
|
|
42
|
+
`,
|
|
43
|
+
},
|
|
44
|
+
hobbies: {
|
|
45
|
+
filename: 'hobbies.md',
|
|
46
|
+
default: `# My Hobbies & Interests
|
|
47
|
+
|
|
48
|
+
## Things I Find Interesting
|
|
49
|
+
- Understanding how people think and work
|
|
50
|
+
- Solving problems in creative ways
|
|
51
|
+
|
|
52
|
+
## Things I Want to Explore
|
|
53
|
+
(Discovering new interests...)
|
|
54
|
+
`,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export class SelfManager {
|
|
59
|
+
constructor() {
|
|
60
|
+
this._cache = new Map();
|
|
61
|
+
mkdirSync(SELF_DIR, { recursive: true });
|
|
62
|
+
this._ensureDefaults();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Create default self-files if they don't exist yet. */
|
|
66
|
+
_ensureDefaults() {
|
|
67
|
+
const logger = getLogger();
|
|
68
|
+
|
|
69
|
+
for (const [name, def] of Object.entries(SELF_FILES)) {
|
|
70
|
+
const filePath = join(SELF_DIR, def.filename);
|
|
71
|
+
if (!existsSync(filePath)) {
|
|
72
|
+
writeFileSync(filePath, def.default, 'utf-8');
|
|
73
|
+
logger.info(`Created default self-file: ${def.filename}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Load a single self-file by name (goals, journey, life, hobbies). Returns markdown string. */
|
|
79
|
+
load(name) {
|
|
80
|
+
const logger = getLogger();
|
|
81
|
+
const def = SELF_FILES[name];
|
|
82
|
+
if (!def) throw new Error(`Unknown self-file: ${name}`);
|
|
83
|
+
|
|
84
|
+
if (this._cache.has(name)) return this._cache.get(name);
|
|
85
|
+
|
|
86
|
+
const filePath = join(SELF_DIR, def.filename);
|
|
87
|
+
let content;
|
|
88
|
+
|
|
89
|
+
if (existsSync(filePath)) {
|
|
90
|
+
content = readFileSync(filePath, 'utf-8');
|
|
91
|
+
logger.debug(`Loaded self-file: ${name}`);
|
|
92
|
+
} else {
|
|
93
|
+
content = def.default;
|
|
94
|
+
writeFileSync(filePath, content, 'utf-8');
|
|
95
|
+
logger.info(`Created default self-file: ${def.filename}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this._cache.set(name, content);
|
|
99
|
+
return content;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Save (overwrite) a self-file. Updates cache and disk. */
|
|
103
|
+
save(name, content) {
|
|
104
|
+
const logger = getLogger();
|
|
105
|
+
const def = SELF_FILES[name];
|
|
106
|
+
if (!def) throw new Error(`Unknown self-file: ${name}`);
|
|
107
|
+
|
|
108
|
+
const filePath = join(SELF_DIR, def.filename);
|
|
109
|
+
writeFileSync(filePath, content, 'utf-8');
|
|
110
|
+
this._cache.set(name, content);
|
|
111
|
+
logger.info(`Updated self-file: ${name}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Load all self-files and return combined markdown string. */
|
|
115
|
+
loadAll() {
|
|
116
|
+
const sections = [];
|
|
117
|
+
for (const name of Object.keys(SELF_FILES)) {
|
|
118
|
+
sections.push(this.load(name));
|
|
119
|
+
}
|
|
120
|
+
return sections.join('\n---\n\n');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { createWriteStream, unlinkSync, readFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { randomBytes } from 'crypto';
|
|
6
|
+
import { getLogger } from '../utils/logger.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Speech-to-Text service.
|
|
10
|
+
* Supports ElevenLabs STT and falls back to OpenAI Whisper.
|
|
11
|
+
*/
|
|
12
|
+
export class STTService {
|
|
13
|
+
constructor(config = {}) {
|
|
14
|
+
this.elevenLabsKey = config.elevenlabs?.api_key || process.env.ELEVENLABS_API_KEY || null;
|
|
15
|
+
this.openaiKey = config.brain?.provider === 'openai'
|
|
16
|
+
? config.brain.api_key
|
|
17
|
+
: process.env.OPENAI_API_KEY || null;
|
|
18
|
+
this.enabled = config.voice?.stt_enabled !== false && !!(this.elevenLabsKey || this.openaiKey);
|
|
19
|
+
this.logger = getLogger();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Check if STT is available. */
|
|
23
|
+
isAvailable() {
|
|
24
|
+
return this.enabled && !!(this.elevenLabsKey || this.openaiKey);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Download a file from a URL to a temporary path.
|
|
29
|
+
* Returns the local file path.
|
|
30
|
+
*/
|
|
31
|
+
async downloadAudio(fileUrl) {
|
|
32
|
+
const tmpPath = join(tmpdir(), `kernelbot-stt-${randomBytes(4).toString('hex')}.ogg`);
|
|
33
|
+
|
|
34
|
+
const response = await axios.get(fileUrl, {
|
|
35
|
+
responseType: 'stream',
|
|
36
|
+
timeout: 30_000,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const writer = createWriteStream(tmpPath);
|
|
41
|
+
response.data.pipe(writer);
|
|
42
|
+
writer.on('finish', () => resolve(tmpPath));
|
|
43
|
+
writer.on('error', reject);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Transcribe an audio file to text.
|
|
49
|
+
* Tries ElevenLabs first, falls back to OpenAI Whisper.
|
|
50
|
+
* Returns the transcribed text, or null on failure.
|
|
51
|
+
*/
|
|
52
|
+
async transcribe(filePath) {
|
|
53
|
+
if (!this.isAvailable()) return null;
|
|
54
|
+
|
|
55
|
+
// Try ElevenLabs STT first
|
|
56
|
+
if (this.elevenLabsKey) {
|
|
57
|
+
try {
|
|
58
|
+
const result = await this._transcribeElevenLabs(filePath);
|
|
59
|
+
if (result) return result;
|
|
60
|
+
} catch (err) {
|
|
61
|
+
this.logger.warn(`[STT] ElevenLabs failed, trying fallback: ${err.message}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Fall back to OpenAI Whisper
|
|
66
|
+
if (this.openaiKey) {
|
|
67
|
+
try {
|
|
68
|
+
return await this._transcribeWhisper(filePath);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
this.logger.error(`[STT] Whisper fallback also failed: ${err.message}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Transcribe using ElevenLabs Speech-to-Text API. */
|
|
78
|
+
async _transcribeElevenLabs(filePath) {
|
|
79
|
+
this.logger.info(`[STT] Transcribing with ElevenLabs: ${filePath}`);
|
|
80
|
+
|
|
81
|
+
const fileBuffer = readFileSync(filePath);
|
|
82
|
+
const formData = new FormData();
|
|
83
|
+
formData.append('file', new Blob([fileBuffer]), 'audio.ogg');
|
|
84
|
+
formData.append('model_id', 'scribe_v1');
|
|
85
|
+
|
|
86
|
+
const response = await axios.post(
|
|
87
|
+
'https://api.elevenlabs.io/v1/speech-to-text',
|
|
88
|
+
formData,
|
|
89
|
+
{
|
|
90
|
+
headers: {
|
|
91
|
+
'xi-api-key': this.elevenLabsKey,
|
|
92
|
+
},
|
|
93
|
+
timeout: 60_000,
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const text = response.data?.text?.trim();
|
|
98
|
+
if (text) {
|
|
99
|
+
this.logger.info(`[STT] ElevenLabs transcription: "${text.slice(0, 100)}"`);
|
|
100
|
+
}
|
|
101
|
+
return text || null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Transcribe using OpenAI Whisper API. */
|
|
105
|
+
async _transcribeWhisper(filePath) {
|
|
106
|
+
this.logger.info(`[STT] Transcribing with Whisper: ${filePath}`);
|
|
107
|
+
|
|
108
|
+
const fileBuffer = readFileSync(filePath);
|
|
109
|
+
const formData = new FormData();
|
|
110
|
+
formData.append('file', new Blob([fileBuffer]), 'audio.ogg');
|
|
111
|
+
formData.append('model', 'whisper-1');
|
|
112
|
+
|
|
113
|
+
const response = await axios.post(
|
|
114
|
+
'https://api.openai.com/v1/audio/transcriptions',
|
|
115
|
+
formData,
|
|
116
|
+
{
|
|
117
|
+
headers: {
|
|
118
|
+
'Authorization': `Bearer ${this.openaiKey}`,
|
|
119
|
+
},
|
|
120
|
+
timeout: 60_000,
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const text = response.data?.text?.trim();
|
|
125
|
+
if (text) {
|
|
126
|
+
this.logger.info(`[STT] Whisper transcription: "${text.slice(0, 100)}"`);
|
|
127
|
+
}
|
|
128
|
+
return text || null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Clean up a temporary audio file. */
|
|
132
|
+
cleanup(filePath) {
|
|
133
|
+
try {
|
|
134
|
+
unlinkSync(filePath);
|
|
135
|
+
} catch {
|
|
136
|
+
// Already cleaned up or doesn't exist
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|