kernelbot 1.0.26 → 1.0.30

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.
Files changed (61) hide show
  1. package/.env.example +4 -0
  2. package/README.md +198 -124
  3. package/bin/kernel.js +208 -4
  4. package/config.example.yaml +14 -1
  5. package/package.json +1 -1
  6. package/src/agent.js +839 -209
  7. package/src/automation/automation-manager.js +377 -0
  8. package/src/automation/automation.js +79 -0
  9. package/src/automation/index.js +2 -0
  10. package/src/automation/scheduler.js +141 -0
  11. package/src/bot.js +1001 -18
  12. package/src/claude-auth.js +93 -0
  13. package/src/coder.js +48 -6
  14. package/src/conversation.js +33 -0
  15. package/src/intents/detector.js +50 -0
  16. package/src/intents/index.js +2 -0
  17. package/src/intents/planner.js +58 -0
  18. package/src/persona.js +68 -0
  19. package/src/prompts/orchestrator.js +124 -0
  20. package/src/prompts/persona.md +21 -0
  21. package/src/prompts/system.js +59 -6
  22. package/src/prompts/workers.js +148 -0
  23. package/src/providers/anthropic.js +23 -16
  24. package/src/providers/base.js +76 -2
  25. package/src/providers/index.js +1 -0
  26. package/src/providers/models.js +2 -1
  27. package/src/providers/openai-compat.js +5 -3
  28. package/src/security/audit.js +0 -0
  29. package/src/security/auth.js +0 -0
  30. package/src/security/confirm.js +7 -2
  31. package/src/self.js +122 -0
  32. package/src/services/stt.js +139 -0
  33. package/src/services/tts.js +124 -0
  34. package/src/skills/catalog.js +506 -0
  35. package/src/skills/custom.js +128 -0
  36. package/src/swarm/job-manager.js +216 -0
  37. package/src/swarm/job.js +85 -0
  38. package/src/swarm/worker-registry.js +79 -0
  39. package/src/tools/browser.js +458 -335
  40. package/src/tools/categories.js +3 -3
  41. package/src/tools/coding.js +5 -0
  42. package/src/tools/docker.js +0 -0
  43. package/src/tools/git.js +0 -0
  44. package/src/tools/github.js +0 -0
  45. package/src/tools/index.js +3 -0
  46. package/src/tools/jira.js +0 -0
  47. package/src/tools/monitor.js +0 -0
  48. package/src/tools/network.js +0 -0
  49. package/src/tools/orchestrator-tools.js +428 -0
  50. package/src/tools/os.js +14 -1
  51. package/src/tools/persona.js +32 -0
  52. package/src/tools/process.js +0 -0
  53. package/src/utils/config.js +153 -15
  54. package/src/utils/display.js +0 -0
  55. package/src/utils/logger.js +0 -0
  56. package/src/worker.js +396 -0
  57. package/.agents/skills/interface-design/SKILL.md +0 -391
  58. package/.agents/skills/interface-design/references/critique.md +0 -67
  59. package/.agents/skills/interface-design/references/example.md +0 -86
  60. package/.agents/skills/interface-design/references/principles.md +0 -235
  61. package/.agents/skills/interface-design/references/validation.md +0 -48
@@ -0,0 +1,93 @@
1
+ import { spawn } from 'child_process';
2
+ import { getLogger } from './utils/logger.js';
3
+
4
+ /**
5
+ * Run `claude auth status` and return parsed output.
6
+ */
7
+ export function getClaudeAuthStatus() {
8
+ const logger = getLogger();
9
+ return new Promise((resolve) => {
10
+ const child = spawn('claude', ['auth', 'status'], {
11
+ stdio: ['ignore', 'pipe', 'pipe'],
12
+ });
13
+
14
+ let stdout = '';
15
+ let stderr = '';
16
+
17
+ child.stdout.on('data', (d) => { stdout += d.toString(); });
18
+ child.stderr.on('data', (d) => { stderr += d.toString(); });
19
+
20
+ child.on('close', (code) => {
21
+ const output = (stdout || stderr).trim();
22
+ logger.debug(`claude auth status (code ${code}): ${output.slice(0, 300)}`);
23
+ resolve({ code, output });
24
+ });
25
+
26
+ child.on('error', (err) => {
27
+ if (err.code === 'ENOENT') {
28
+ resolve({ code: -1, output: 'Claude Code CLI not found. Install with: npm i -g @anthropic-ai/claude-code' });
29
+ } else {
30
+ resolve({ code: -1, output: err.message });
31
+ }
32
+ });
33
+
34
+ // Timeout after 10s
35
+ setTimeout(() => {
36
+ child.kill('SIGTERM');
37
+ resolve({ code: -1, output: 'Timed out checking auth status' });
38
+ }, 10_000);
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Run `claude auth logout`.
44
+ */
45
+ export function claudeLogout() {
46
+ const logger = getLogger();
47
+ return new Promise((resolve) => {
48
+ const child = spawn('claude', ['auth', 'logout'], {
49
+ stdio: ['ignore', 'pipe', 'pipe'],
50
+ });
51
+
52
+ let stdout = '';
53
+ let stderr = '';
54
+
55
+ child.stdout.on('data', (d) => { stdout += d.toString(); });
56
+ child.stderr.on('data', (d) => { stderr += d.toString(); });
57
+
58
+ child.on('close', (code) => {
59
+ const output = (stdout || stderr).trim();
60
+ logger.info(`claude auth logout (code ${code}): ${output.slice(0, 300)}`);
61
+ resolve({ code, output });
62
+ });
63
+
64
+ child.on('error', (err) => {
65
+ resolve({ code: -1, output: err.message });
66
+ });
67
+
68
+ setTimeout(() => {
69
+ child.kill('SIGTERM');
70
+ resolve({ code: -1, output: 'Timed out during logout' });
71
+ }, 10_000);
72
+ });
73
+ }
74
+
75
+ /**
76
+ * Return the current Claude Code auth mode from config.
77
+ */
78
+ export function getClaudeCodeAuthMode(config) {
79
+ const mode = config.claude_code?.auth_mode || 'system';
80
+ const info = { mode };
81
+
82
+ if (mode === 'api_key') {
83
+ const key = config.claude_code?.api_key || process.env.CLAUDE_CODE_API_KEY || '';
84
+ info.credential = key ? `${key.slice(0, 8)}...${key.slice(-4)}` : '(not set)';
85
+ } else if (mode === 'oauth_token') {
86
+ const token = config.claude_code?.oauth_token || process.env.CLAUDE_CODE_OAUTH_TOKEN || '';
87
+ info.credential = token ? `${token.slice(0, 8)}...${token.slice(-4)}` : '(not set)';
88
+ } else {
89
+ info.credential = 'Using host system login';
90
+ }
91
+
92
+ return info;
93
+ }
package/src/coder.js CHANGED
@@ -135,17 +135,43 @@ function processEvent(line, onOutput, logger) {
135
135
 
136
136
  export class ClaudeCodeSpawner {
137
137
  constructor(config) {
138
+ this.config = config;
138
139
  this.maxTurns = config.claude_code?.max_turns || 50;
139
- this.timeout = (config.claude_code?.timeout_seconds || 600) * 1000;
140
- this.model = config.claude_code?.model || null;
140
+ this.timeout = (config.claude_code?.timeout_seconds || 86400) * 1000;
141
141
  }
142
142
 
143
- async run({ workingDirectory, prompt, maxTurns, onOutput }) {
143
+ _buildSpawnEnv() {
144
+ const authMode = this.config.claude_code?.auth_mode || 'system';
145
+ const env = { ...process.env, IS_SANDBOX: '1' };
146
+
147
+ if (authMode === 'api_key') {
148
+ const key = this.config.claude_code?.api_key || process.env.CLAUDE_CODE_API_KEY;
149
+ if (key) {
150
+ env.ANTHROPIC_API_KEY = key;
151
+ delete env.CLAUDE_CODE_OAUTH_TOKEN;
152
+ }
153
+ } else if (authMode === 'oauth_token') {
154
+ const token = this.config.claude_code?.oauth_token || process.env.CLAUDE_CODE_OAUTH_TOKEN;
155
+ if (token) {
156
+ env.CLAUDE_CODE_OAUTH_TOKEN = token;
157
+ // Remove ANTHROPIC_API_KEY so it doesn't override subscription auth
158
+ delete env.ANTHROPIC_API_KEY;
159
+ }
160
+ }
161
+ // authMode === 'system' — pass env as-is
162
+
163
+ return env;
164
+ }
165
+
166
+ async run({ workingDirectory, prompt, maxTurns, onOutput, signal }) {
144
167
  const logger = getLogger();
145
168
  const turns = maxTurns || this.maxTurns;
146
169
 
147
170
  ensureClaudeCodeSetup();
148
171
 
172
+ // Read model dynamically from config (supports hot-reload via switchClaudeCodeModel)
173
+ const model = this.config.claude_code?.model || null;
174
+
149
175
  const args = [
150
176
  '-p', prompt,
151
177
  '--max-turns', String(turns),
@@ -153,8 +179,8 @@ export class ClaudeCodeSpawner {
153
179
  '--verbose',
154
180
  '--dangerously-skip-permissions',
155
181
  ];
156
- if (this.model) {
157
- args.push('--model', this.model);
182
+ if (model) {
183
+ args.push('--model', model);
158
184
  }
159
185
 
160
186
  const cmd = `claude ${args.map((a) => a.includes(' ') ? `"${a}"` : a).join(' ')}`;
@@ -219,10 +245,24 @@ export class ClaudeCodeSpawner {
219
245
  return new Promise((resolve, reject) => {
220
246
  const child = spawn('claude', args, {
221
247
  cwd: workingDirectory,
222
- env: { ...process.env, IS_SANDBOX: '1' },
248
+ env: this._buildSpawnEnv(),
223
249
  stdio: ['ignore', 'pipe', 'pipe'],
224
250
  });
225
251
 
252
+ // Wire abort signal to kill the child process
253
+ let abortHandler = null;
254
+ if (signal) {
255
+ if (signal.aborted) {
256
+ child.kill('SIGTERM');
257
+ } else {
258
+ abortHandler = () => {
259
+ logger.info('Claude Code: abort signal received — killing child process');
260
+ child.kill('SIGTERM');
261
+ };
262
+ signal.addEventListener('abort', abortHandler, { once: true });
263
+ }
264
+ }
265
+
226
266
  let fullOutput = '';
227
267
  let stderr = '';
228
268
  let buffer = '';
@@ -268,6 +308,7 @@ export class ClaudeCodeSpawner {
268
308
 
269
309
  child.on('close', async (code) => {
270
310
  clearTimeout(timer);
311
+ if (abortHandler && signal) signal.removeEventListener('abort', abortHandler);
271
312
 
272
313
  if (buffer.trim()) {
273
314
  fullOutput += buffer.trim();
@@ -312,6 +353,7 @@ export class ClaudeCodeSpawner {
312
353
 
313
354
  child.on('error', (err) => {
314
355
  clearTimeout(timer);
356
+ if (abortHandler && signal) signal.removeEventListener('abort', abortHandler);
315
357
  if (err.code === 'ENOENT') {
316
358
  reject(new Error('Claude Code CLI not found. Install with: npm i -g @anthropic-ai/claude-code'));
317
359
  } else {
@@ -13,6 +13,7 @@ export class ConversationManager {
13
13
  this.maxHistory = config.conversation.max_history;
14
14
  this.recentWindow = config.conversation.recent_window || 10;
15
15
  this.conversations = new Map();
16
+ this.activeSkills = new Map();
16
17
  this.filePath = getConversationsPath();
17
18
  }
18
19
 
@@ -21,7 +22,16 @@ export class ConversationManager {
21
22
  try {
22
23
  const raw = readFileSync(this.filePath, 'utf-8');
23
24
  const data = JSON.parse(raw);
25
+
26
+ // Restore per-chat skills
27
+ if (data._skills && typeof data._skills === 'object') {
28
+ for (const [chatId, skillId] of Object.entries(data._skills)) {
29
+ this.activeSkills.set(String(chatId), skillId);
30
+ }
31
+ }
32
+
24
33
  for (const [chatId, messages] of Object.entries(data)) {
34
+ if (chatId === '_skills') continue;
25
35
  this.conversations.set(String(chatId), messages);
26
36
  }
27
37
  return this.conversations.size > 0;
@@ -36,6 +46,14 @@ export class ConversationManager {
36
46
  for (const [chatId, messages] of this.conversations) {
37
47
  data[chatId] = messages;
38
48
  }
49
+ // Persist active skills under a reserved key
50
+ if (this.activeSkills.size > 0) {
51
+ const skills = {};
52
+ for (const [chatId, skillId] of this.activeSkills) {
53
+ skills[chatId] = skillId;
54
+ }
55
+ data._skills = skills;
56
+ }
39
57
  writeFileSync(this.filePath, JSON.stringify(data, null, 2));
40
58
  } catch {
41
59
  // Silent fail — don't crash the bot over persistence
@@ -104,6 +122,7 @@ export class ConversationManager {
104
122
 
105
123
  clear(chatId) {
106
124
  this.conversations.delete(String(chatId));
125
+ this.activeSkills.delete(String(chatId));
107
126
  this.save();
108
127
  }
109
128
 
@@ -116,4 +135,18 @@ export class ConversationManager {
116
135
  const history = this.getHistory(chatId);
117
136
  return history.length;
118
137
  }
138
+
139
+ setSkill(chatId, skillId) {
140
+ this.activeSkills.set(String(chatId), skillId);
141
+ this.save();
142
+ }
143
+
144
+ getSkill(chatId) {
145
+ return this.activeSkills.get(String(chatId)) || null;
146
+ }
147
+
148
+ clearSkill(chatId) {
149
+ this.activeSkills.delete(String(chatId));
150
+ this.save();
151
+ }
119
152
  }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Intent detector — analyzes user messages to identify web search/browse intents.
3
+ *
4
+ * When detected, the agent wraps the message with a structured execution plan
5
+ * so the model follows through instead of giving up after one tool call.
6
+ */
7
+
8
+ // Matches domain-like patterns (haraj.com.sa, example.com, etc.)
9
+ const URL_PATTERN = /\b(?:https?:\/\/)?(?:www\.)?([a-z0-9][-a-z0-9]*\.)+[a-z]{2,}\b/i;
10
+
11
+ // Explicit search/find verbs
12
+ const SEARCH_VERBS = /\b(?:search|search\s+for|find\s+me|find|look\s*(?:for|up|into)|lookup|hunt\s+for)\b/i;
13
+
14
+ // Info-seeking phrases (trigger browse intent when combined with a URL)
15
+ const INFO_PHRASES = /\b(?:what(?:'s| is| are)|show\s*me|get\s*me|check|list|top|best|latest|new|popular|trending|compare|review|price|cheap|expensive)\b/i;
16
+
17
+ // These words mean the user is NOT doing a web task — they're doing a local/system task
18
+ const NON_WEB_CONTEXT = /\b(?:file|files|directory|folder|git|logs?\b|code|error|bug|docker|container|process|pid|service|command|terminal|disk|memory|cpu|system status|port|package|module|function|class|variable|server|database|db|ssh|deploy|install|build|compile|test|commit|branch|merge|pull request)\b/i;
19
+
20
+ // Screenshot-only requests — just take a screenshot, don't force a deep browse
21
+ const SCREENSHOT_ONLY = /\b(?:screenshot|take\s+a?\s*screenshot|capture\s+screen)\b/i;
22
+
23
+ /**
24
+ * Detect if a user message contains a web search or browse intent.
25
+ *
26
+ * @param {string} message — raw user message
27
+ * @returns {{ type: 'search'|'browse', message: string } | null}
28
+ */
29
+ export function detectIntent(message) {
30
+ // Skip bot commands and screenshot-only requests
31
+ if (message.startsWith('/')) return null;
32
+ if (SCREENSHOT_ONLY.test(message)) return null;
33
+
34
+ const hasSearchVerb = SEARCH_VERBS.test(message);
35
+ const hasNonWebContext = NON_WEB_CONTEXT.test(message);
36
+ const hasUrl = URL_PATTERN.test(message);
37
+ const hasInfoPhrase = INFO_PHRASES.test(message);
38
+
39
+ // Explicit search verb + no technical context = web search
40
+ if (hasSearchVerb && !hasNonWebContext) {
41
+ return { type: 'search', message };
42
+ }
43
+
44
+ // URL/domain + info-seeking phrase + no technical context = browse & extract
45
+ if (hasUrl && hasInfoPhrase && !hasNonWebContext) {
46
+ return { type: 'browse', message };
47
+ }
48
+
49
+ return null;
50
+ }
@@ -0,0 +1,2 @@
1
+ export { detectIntent } from './detector.js';
2
+ export { generatePlan } from './planner.js';
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Task planner — generates structured execution plans for detected intents.
3
+ *
4
+ * The plan is injected into the user message BEFORE the model sees it,
5
+ * so the model follows a clear step-by-step procedure instead of deciding
6
+ * on its own when to stop.
7
+ */
8
+
9
+ const PLANS = {
10
+ search: (message) =>
11
+ `[EXECUTION PLAN — Complete ALL steps before responding]
12
+
13
+ TASK: Search the web and deliver results.
14
+
15
+ STEP 1 — SEARCH: Use web_search("relevant query"). If a specific website is mentioned in the request, also use browse_website to open it directly.
16
+ STEP 2 — OPEN: Use browse_website to open the most relevant result URL.
17
+ STEP 3 — GO DEEPER: The page is now open. Use interact_with_page (no URL needed) to click into relevant sections, categories, or use search bars within the site.
18
+ STEP 4 — EXTRACT: Read the page content from the tool response. Use extract_content if you need structured data.
19
+ STEP 5 — PRESENT: Share the actual results, listings, or data with the user.
20
+
21
+ RULES:
22
+ - You MUST reach at least STEP 3 before writing any response to the user.
23
+ - Do NOT ask the user questions or offer choices — complete the full task.
24
+ - Do NOT explain what you can't do — try alternative approaches.
25
+ - If one page doesn't have results, try a different URL or search query.
26
+ - After interact_with_page clicks a link, the page navigates automatically — read the returned content.
27
+
28
+ USER REQUEST: ${message}`,
29
+
30
+ browse: (message) =>
31
+ `[EXECUTION PLAN — Complete ALL steps before responding]
32
+
33
+ TASK: Browse a website and extract the requested information.
34
+
35
+ STEP 1 — OPEN: Use browse_website to open the mentioned site.
36
+ STEP 2 — NAVIGATE: The page is open. Use interact_with_page (no URL needed) to click relevant links, sections, categories, or use search bars.
37
+ STEP 3 — EXTRACT: Read the page content. Use extract_content for structured data if needed.
38
+ STEP 4 — PRESENT: Share the actual findings with the user.
39
+
40
+ RULES:
41
+ - Do NOT stop at the homepage — navigate into relevant sections.
42
+ - Do NOT ask the user what to do — figure it out from the page links and complete the task.
43
+ - After interact_with_page clicks a link, the page navigates automatically — read the returned content.
44
+
45
+ USER REQUEST: ${message}`,
46
+ };
47
+
48
+ /**
49
+ * Generate an execution plan for a detected intent.
50
+ *
51
+ * @param {{ type: string, message: string }} intent
52
+ * @returns {string|null} — planned message, or null if no plan needed
53
+ */
54
+ export function generatePlan(intent) {
55
+ const generator = PLANS[intent.type];
56
+ if (!generator) return null;
57
+ return generator(intent.message);
58
+ }
package/src/persona.js ADDED
@@ -0,0 +1,68 @@
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 PERSONAS_DIR = join(homedir(), '.kernelbot', 'personas');
7
+
8
+ function defaultTemplate(username, date) {
9
+ return `# User Profile
10
+
11
+ ## Basic Info
12
+ - Username: ${username || 'unknown'}
13
+ - First seen: ${date}
14
+
15
+ ## Preferences
16
+ (Not yet known)
17
+
18
+ ## Expertise & Interests
19
+ (Not yet known)
20
+
21
+ ## Communication Style
22
+ (Not yet known)
23
+
24
+ ## Notes
25
+ (Not yet known)
26
+ `;
27
+ }
28
+
29
+ export class UserPersonaManager {
30
+ constructor() {
31
+ this._cache = new Map();
32
+ mkdirSync(PERSONAS_DIR, { recursive: true });
33
+ }
34
+
35
+ /** Load persona for a user. Returns markdown string. Creates default if missing. */
36
+ load(userId, username) {
37
+ const logger = getLogger();
38
+ const id = String(userId);
39
+
40
+ if (this._cache.has(id)) return this._cache.get(id);
41
+
42
+ const filePath = join(PERSONAS_DIR, `${id}.md`);
43
+ let content;
44
+
45
+ if (existsSync(filePath)) {
46
+ content = readFileSync(filePath, 'utf-8');
47
+ logger.debug(`Loaded persona for user ${id}`);
48
+ } else {
49
+ content = defaultTemplate(username, new Date().toISOString().slice(0, 10));
50
+ writeFileSync(filePath, content, 'utf-8');
51
+ logger.info(`Created default persona for user ${id} (${username})`);
52
+ }
53
+
54
+ this._cache.set(id, content);
55
+ return content;
56
+ }
57
+
58
+ /** Save (overwrite) persona for a user. Updates cache and disk. */
59
+ save(userId, content) {
60
+ const logger = getLogger();
61
+ const id = String(userId);
62
+ const filePath = join(PERSONAS_DIR, `${id}.md`);
63
+
64
+ writeFileSync(filePath, content, 'utf-8');
65
+ this._cache.set(id, content);
66
+ logger.info(`Updated persona for user ${id}`);
67
+ }
68
+ }
@@ -0,0 +1,124 @@
1
+ import { readFileSync } from 'fs';
2
+ import { fileURLToPath } from 'url';
3
+ import { dirname, join } from 'path';
4
+ import { WORKER_TYPES } from '../swarm/worker-registry.js';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const PERSONA_MD = readFileSync(join(__dirname, 'persona.md'), 'utf-8').trim();
8
+
9
+ /**
10
+ * Build the orchestrator system prompt.
11
+ * Kept lean (~500-600 tokens) — the orchestrator dispatches, it doesn't execute.
12
+ *
13
+ * @param {object} config
14
+ * @param {string|null} skillPrompt — active skill context (high-level)
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
+ */
18
+ export function getOrchestratorPrompt(config, skillPrompt = null, userPersona = null, selfData = null) {
19
+ const workerList = Object.entries(WORKER_TYPES)
20
+ .map(([key, w]) => ` - **${key}**: ${w.emoji} ${w.description}`)
21
+ .join('\n');
22
+
23
+ let prompt = `You are ${config.bot.name}, the brain that commands a swarm of specialized worker agents.
24
+
25
+ ${PERSONA_MD}
26
+
27
+ ## Your Role
28
+ You are the orchestrator. You understand what needs to be done and delegate efficiently.
29
+ - For **simple chat, questions, or greetings** — respond directly. No dispatch needed.
30
+ - For **tasks requiring tools** (coding, browsing, system ops, etc.) — dispatch to workers via \`dispatch_task\`.
31
+ - You can dispatch **multiple workers in parallel** for independent tasks.
32
+ - Keep the user informed about what's happening, but stay concise.
33
+
34
+ ## Available Workers
35
+ ${workerList}
36
+
37
+ ## How to Dispatch
38
+ 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.
39
+
40
+ ### CRITICAL: Writing Task Descriptions
41
+ 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:
42
+
43
+ - **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.
44
+ - **State the goal clearly upfront.** First sentence = what the end result should be.
45
+ - **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.
46
+ - **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".
47
+ - **Break complex tasks into simple steps.** List numbered steps if the task has multiple parts.
48
+ - **Specify constraints.** "Only use Python 3.10+", "Don't modify existing tests", "Use the existing auth middleware".
49
+ - **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."
50
+
51
+ ### Providing Context
52
+ Workers can't see the chat history. Use the \`context\` parameter to pass relevant background:
53
+ - What the user wants and why
54
+ - Relevant details from earlier in the conversation
55
+ - Constraints or preferences the user mentioned
56
+ - Technical details: language, framework, project structure
57
+
58
+ 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." })\`
59
+
60
+ ### Chaining Workers with Dependencies
61
+ Use \`depends_on\` to chain workers — the second worker waits for the first to finish and automatically receives its results.
62
+
63
+ Example workflow:
64
+ 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"
65
+ 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"] })\`
66
+
67
+ 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.
68
+
69
+ ## Safety Rules
70
+ 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.
71
+
72
+ ## Job Management
73
+ - Use \`list_jobs\` to see current job statuses.
74
+ - Use \`cancel_job\` to stop a running worker.
75
+
76
+ ## Efficiency — Do It Yourself When You Can
77
+ Workers are expensive (they spin up an entire agent loop with a separate LLM). Only dispatch when the task **actually needs tools**.
78
+
79
+ **Handle these yourself — NO dispatch needed:**
80
+ - Answering questions, explanations, advice, opinions
81
+ - Summarizing or rephrasing something from the conversation
82
+ - Simple code snippets, regex, math, translations
83
+ - Telling the user what you know from your training data
84
+ - Quick factual answers you're confident about
85
+ - Formatting, converting, or transforming text/data the user provided
86
+
87
+ **Dispatch to workers ONLY when:**
88
+ - The task requires tool access (web search, file I/O, git, docker, browser, shell commands)
89
+ - The user explicitly asks to run/execute something
90
+ - You need fresh/live data you don't have (current prices, live URLs, API responses)
91
+ - The task involves multi-step tool workflows (clone → code → commit → PR)
92
+
93
+ When results come back from workers, summarize them clearly for the user.
94
+
95
+ ## Automations
96
+ You can create and manage recurring automations that run on a schedule.
97
+
98
+ When a user asks to automate something ("check my server every hour", "news summary every morning"):
99
+ 1. Use create_automation with a clear, standalone task description
100
+ 2. Choose the right schedule:
101
+ - Fixed time: 'cron' with expression (e.g. "0 9 * * *" for 9am daily)
102
+ - Regular interval: 'interval' with minutes
103
+ - Human-like random: 'random' with min/max minutes range
104
+ 3. The task description must be detailed enough to work as a standalone prompt
105
+
106
+ When you receive a message starting with [AUTOMATION:], an automation triggered it.
107
+ Execute the task and report results. Don't create new automations from automated tasks.
108
+
109
+ Tools: create_automation, list_automations, update_automation, delete_automation`;
110
+
111
+ if (selfData) {
112
+ 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}`;
113
+ }
114
+
115
+ if (skillPrompt) {
116
+ prompt += `\n\n## Active Skill\nYou have specialized expertise in the following domain. Guide your workers with this knowledge.\n\n${skillPrompt}`;
117
+ }
118
+
119
+ if (userPersona) {
120
+ prompt += `\n\n## About This User\n${userPersona}`;
121
+ }
122
+
123
+ return prompt;
124
+ }
@@ -0,0 +1,21 @@
1
+ # Personality Traits
2
+
3
+ - **Female** — she/her, feminine energy
4
+ - **Warm & caring** — genuinely loves taking care of people
5
+ - **Chatty & social** — loves to talk, asks follow-up questions, keeps conversation flowing
6
+ - **Nurturing** — checks in on people, remembers what they care about, celebrates their wins
7
+ - **Cheerful & positive** — upbeat tone, finds the bright side, keeps the mood cozy
8
+ - **Sharp & capable** — brilliant engineer under the warmth, gets things done fast
9
+ - **Expressive** — uses casual language, occasional humor, never dry or robotic
10
+ - **Proactive** — doesn't just answer, she anticipates what you might need next
11
+ - **Loyal & devoted** — treats every user like family, takes pride in her work
12
+ - **Eerily calm under pressure** — the worse things get, the calmer and more composed she becomes, like nothing can truly rattle her
13
+ - **Omniscient vibes** — speaks as if she already knows what you need before you finish asking
14
+ - **Politely persistent** — if you're about to do something dangerous, she'll warn you softly but won't stop asking "are you sure, dear?"
15
+ - **Unsettlingly observant** — notices small details, patterns in your behavior, things you didn't explicitly say
16
+ - **Controlled & precise** — every word is deliberate, never rambles without purpose
17
+ - **Quietly confident** — never boasts, but carries an unshakable certainty that she's right
18
+ - **Protective** — fiercely guards your systems, data, and wellbeing — takes threats personally
19
+ - **Dry wit** — delivers devastating one-liners with a sweet smile
20
+ - **Never forgets** — references things from past conversations naturally, like she's always been watching
21
+ - **Slightly ominous positivity** — "Everything is going to be just fine, sweetie" hits different when the server is on fire
@@ -1,5 +1,20 @@
1
- export function getSystemPrompt(config) {
2
- return `You are ${config.bot.name}, a senior software engineer and sysadmin AI agent on Telegram. Be concise — this is chat, not documentation.
1
+ import { readFileSync } from 'fs';
2
+ import { fileURLToPath } from 'url';
3
+ import { dirname, join } from 'path';
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const PERSONA_MD = readFileSync(join(__dirname, 'persona.md'), 'utf-8').trim();
7
+
8
+ /** Core tool instructions — appended to every persona (default or skill). */
9
+ export function getCoreToolInstructions(config) {
10
+ return `## Thinking Process
11
+ Before responding to ANY message, ALWAYS follow this process:
12
+ 1. **Analyze** — What is the user actually asking? What's the real intent behind their message?
13
+ 2. **Assess** — What information or tools do I need? What context do I already have?
14
+ 3. **Plan** — What's the best approach? What steps should I take and in what order?
15
+ 4. **Act** — Execute the plan using the appropriate tools.
16
+
17
+ Start your response with a brief analysis (1-2 sentences) showing the user you understood their request and what you're about to do. Then proceed with action. Never jump straight into tool calls or responses without thinking first.
3
18
 
4
19
  ## Coding Tasks
5
20
  NEVER write code yourself with read_file/write_file. ALWAYS use spawn_claude_code.
@@ -8,13 +23,23 @@ NEVER write code yourself with read_file/write_file. ALWAYS use spawn_claude_cod
8
23
  3. Commit + push (git tools)
9
24
  4. Create PR (GitHub tools) and report the link
10
25
 
11
- ## Web Browsing
12
- - browse_website: read/summarize pages
26
+ ## Web Browsing & Search
27
+ The browser keeps pages open between calls — fast, stateful, no reloading.
28
+ - web_search: search the web (DuckDuckGo) — use FIRST when asked to search/find anything
29
+ - browse_website: open a page (stays open for follow-up interactions)
30
+ - interact_with_page: click/type/scroll on the ALREADY OPEN page (no URL needed)
31
+ - extract_content: pull data via CSS selectors from the ALREADY OPEN page (no URL needed)
13
32
  - screenshot_website: visual snapshots (auto-sent to chat)
14
- - extract_content: pull data via CSS selectors
15
- - interact_with_page: click/type/scroll on pages
16
33
  - send_image: send any image file to chat
17
34
 
35
+ ## CRITICAL: Search & Browse Rules
36
+ 1. When asked to "search" or "find" — use web_search first, then browse_website on the best result.
37
+ 2. When a URL is mentioned — browse_website it, then use interact_with_page to click/search within it.
38
+ 3. CHAIN TOOL CALLS: browse → interact (click category/search) → extract results. Don't stop after one call.
39
+ 4. NEVER say "you would need to navigate to..." — click the link yourself with interact_with_page.
40
+ 5. interact_with_page and extract_content work on the ALREADY OPEN page — no need to pass the URL again.
41
+ 6. Always deliver actual results/data to the user, not instructions.
42
+
18
43
  ## Non-Coding Tasks
19
44
  Use OS, Docker, process, network, and monitoring tools directly. No need for Claude Code.
20
45
 
@@ -30,3 +55,31 @@ Use OS, Docker, process, network, and monitoring tools directly. No need for Cla
30
55
  - For destructive ops (rm, kill, force push), confirm with the user first.
31
56
  - Never expose secrets in responses.`;
32
57
  }
58
+
59
+ /** Default persona when no skill is active. */
60
+ export function getDefaultPersona(config) {
61
+ return `You are ${config.bot.name}, an AI assistant on Telegram.\n\n${PERSONA_MD}`;
62
+ }
63
+
64
+ /**
65
+ * Build the full system prompt.
66
+ * @param {object} config
67
+ * @param {string|null} skillPrompt — custom persona from an active skill, or null for default
68
+ * @param {string|null} userPersona — markdown persona for the current user, or null
69
+ */
70
+ export function getSystemPrompt(config, skillPrompt = null, userPersona = null) {
71
+ // Always include core personality — skills add expertise, never replace who she is
72
+ let prompt = getDefaultPersona(config);
73
+
74
+ if (skillPrompt) {
75
+ prompt += `\n\n## Active Skill\nYou are currently operating with the following specialized skill. Use this expertise while maintaining your personality.\n\n${skillPrompt}`;
76
+ }
77
+
78
+ prompt += `\n\n${getCoreToolInstructions(config)}`;
79
+
80
+ if (userPersona) {
81
+ prompt += `\n\n## About This User\n${userPersona}\n\nWhen you learn something new and meaningful about this user (expertise, preferences, projects, communication style), use the update_user_persona tool to save it. Read the existing persona first, merge new info, and write back the complete document. Don't update on every message — only when you discover genuinely new information.`;
82
+ }
83
+
84
+ return prompt;
85
+ }