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.
- package/.env.example +4 -0
- package/README.md +198 -124
- package/bin/kernel.js +208 -4
- package/config.example.yaml +14 -1
- package/package.json +1 -1
- package/src/agent.js +839 -209
- package/src/automation/automation-manager.js +377 -0
- package/src/automation/automation.js +79 -0
- package/src/automation/index.js +2 -0
- package/src/automation/scheduler.js +141 -0
- package/src/bot.js +1001 -18
- package/src/claude-auth.js +93 -0
- package/src/coder.js +48 -6
- package/src/conversation.js +33 -0
- package/src/intents/detector.js +50 -0
- package/src/intents/index.js +2 -0
- package/src/intents/planner.js +58 -0
- package/src/persona.js +68 -0
- package/src/prompts/orchestrator.js +124 -0
- package/src/prompts/persona.md +21 -0
- package/src/prompts/system.js +59 -6
- package/src/prompts/workers.js +148 -0
- package/src/providers/anthropic.js +23 -16
- package/src/providers/base.js +76 -2
- package/src/providers/index.js +1 -0
- package/src/providers/models.js +2 -1
- package/src/providers/openai-compat.js +5 -3
- package/src/security/audit.js +0 -0
- package/src/security/auth.js +0 -0
- package/src/security/confirm.js +7 -2
- package/src/self.js +122 -0
- package/src/services/stt.js +139 -0
- package/src/services/tts.js +124 -0
- package/src/skills/catalog.js +506 -0
- package/src/skills/custom.js +128 -0
- package/src/swarm/job-manager.js +216 -0
- package/src/swarm/job.js +85 -0
- package/src/swarm/worker-registry.js +79 -0
- package/src/tools/browser.js +458 -335
- package/src/tools/categories.js +3 -3
- package/src/tools/coding.js +5 -0
- package/src/tools/docker.js +0 -0
- package/src/tools/git.js +0 -0
- package/src/tools/github.js +0 -0
- package/src/tools/index.js +3 -0
- package/src/tools/jira.js +0 -0
- package/src/tools/monitor.js +0 -0
- package/src/tools/network.js +0 -0
- package/src/tools/orchestrator-tools.js +428 -0
- package/src/tools/os.js +14 -1
- package/src/tools/persona.js +32 -0
- package/src/tools/process.js +0 -0
- package/src/utils/config.js +153 -15
- package/src/utils/display.js +0 -0
- package/src/utils/logger.js +0 -0
- package/src/worker.js +396 -0
- package/.agents/skills/interface-design/SKILL.md +0 -391
- package/.agents/skills/interface-design/references/critique.md +0 -67
- package/.agents/skills/interface-design/references/example.md +0 -86
- package/.agents/skills/interface-design/references/principles.md +0 -235
- 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 ||
|
|
140
|
-
this.model = config.claude_code?.model || null;
|
|
140
|
+
this.timeout = (config.claude_code?.timeout_seconds || 86400) * 1000;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
|
|
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 (
|
|
157
|
-
args.push('--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:
|
|
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 {
|
package/src/conversation.js
CHANGED
|
@@ -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,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
|
package/src/prompts/system.js
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
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
|
+
}
|