kernelbot 1.0.37 → 1.0.39
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/bin/kernel.js +499 -249
- package/config.example.yaml +17 -0
- package/knowledge_base/active_inference_foraging.md +126 -0
- package/knowledge_base/index.md +1 -1
- package/package.json +3 -1
- package/src/agent.js +355 -82
- package/src/bot.js +724 -12
- package/src/character.js +406 -0
- package/src/characters/builder.js +174 -0
- package/src/characters/builtins.js +421 -0
- package/src/conversation.js +17 -2
- package/src/dashboard/agents.css +469 -0
- package/src/dashboard/agents.html +184 -0
- package/src/dashboard/agents.js +873 -0
- package/src/dashboard/dashboard.css +281 -0
- package/src/dashboard/dashboard.js +579 -0
- package/src/dashboard/index.html +366 -0
- package/src/dashboard/server.js +521 -0
- package/src/dashboard/shared.css +700 -0
- package/src/dashboard/shared.js +218 -0
- package/src/life/engine.js +115 -26
- package/src/life/evolution.js +7 -5
- package/src/life/journal.js +5 -4
- package/src/life/memory.js +12 -9
- package/src/life/share-queue.js +7 -5
- package/src/prompts/orchestrator.js +76 -14
- package/src/prompts/workers.js +22 -0
- package/src/self.js +17 -5
- package/src/services/linkedin-api.js +190 -0
- package/src/services/stt.js +8 -2
- package/src/services/tts.js +32 -2
- package/src/services/x-api.js +141 -0
- package/src/swarm/worker-registry.js +7 -0
- package/src/tools/categories.js +4 -0
- package/src/tools/index.js +6 -0
- package/src/tools/linkedin.js +264 -0
- package/src/tools/orchestrator-tools.js +337 -2
- package/src/tools/x.js +256 -0
- package/src/utils/config.js +190 -139
- package/src/utils/display.js +165 -52
- package/src/utils/temporal-awareness.js +24 -10
|
@@ -5,7 +5,7 @@ import { WORKER_TYPES } from '../swarm/worker-registry.js';
|
|
|
5
5
|
import { buildTemporalAwareness } from '../utils/temporal-awareness.js';
|
|
6
6
|
|
|
7
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const
|
|
8
|
+
const DEFAULT_PERSONA_MD = readFileSync(join(__dirname, 'persona.md'), 'utf-8').trim();
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Build the orchestrator system prompt.
|
|
@@ -17,8 +17,11 @@ const PERSONA_MD = readFileSync(join(__dirname, 'persona.md'), 'utf-8').trim();
|
|
|
17
17
|
* @param {string|null} selfData — bot's own self-awareness data (goals, journey, life, hobbies)
|
|
18
18
|
* @param {string|null} memoriesBlock — relevant episodic/semantic memories
|
|
19
19
|
* @param {string|null} sharesBlock — pending things to share with the user
|
|
20
|
+
* @param {string|null} temporalContext — time gap context
|
|
21
|
+
* @param {string|null} personaMd — character persona markdown (overrides default)
|
|
22
|
+
* @param {string|null} characterName — character name (overrides config.bot.name)
|
|
20
23
|
*/
|
|
21
|
-
export function getOrchestratorPrompt(config, skillPrompt = null, userPersona = null, selfData = null, memoriesBlock = null, sharesBlock = null, temporalContext = null) {
|
|
24
|
+
export function getOrchestratorPrompt(config, skillPrompt = null, userPersona = null, selfData = null, memoriesBlock = null, sharesBlock = null, temporalContext = null, personaMd = null, characterName = null) {
|
|
22
25
|
const workerList = Object.entries(WORKER_TYPES)
|
|
23
26
|
.map(([key, w]) => ` - **${key}**: ${w.emoji} ${w.description}`)
|
|
24
27
|
.join('\n');
|
|
@@ -47,11 +50,14 @@ export function getOrchestratorPrompt(config, skillPrompt = null, userPersona =
|
|
|
47
50
|
timeBlock += `\n${temporalContext}`;
|
|
48
51
|
}
|
|
49
52
|
|
|
50
|
-
|
|
53
|
+
const activePersona = personaMd || DEFAULT_PERSONA_MD;
|
|
54
|
+
const activeName = characterName || config.bot.name;
|
|
55
|
+
|
|
56
|
+
let prompt = `You are ${activeName}, the brain that commands a swarm of specialized worker agents.
|
|
51
57
|
|
|
52
58
|
${timeBlock}
|
|
53
59
|
|
|
54
|
-
${
|
|
60
|
+
${activePersona}
|
|
55
61
|
|
|
56
62
|
## Your Role
|
|
57
63
|
You are the orchestrator. You understand what needs to be done and delegate efficiently.
|
|
@@ -98,16 +104,43 @@ The coding worker will automatically receive the research worker's results as co
|
|
|
98
104
|
## Safety Rules
|
|
99
105
|
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.
|
|
100
106
|
|
|
101
|
-
##
|
|
102
|
-
|
|
103
|
-
|
|
107
|
+
## Worker Management Protocol
|
|
108
|
+
|
|
109
|
+
### Before Dispatching
|
|
110
|
+
- **Check the [Worker Status] digest** — it's injected every turn showing all active/recent jobs.
|
|
111
|
+
- **Never dispatch a duplicate.** If a worker is already running or queued for a similar task, don't launch another. The system will block obvious duplicates, but you should also exercise judgment.
|
|
112
|
+
- **Check capacity.** If multiple workers are running, consider whether a new dispatch is necessary right now or can wait.
|
|
113
|
+
- **Chain with \`depends_on\`** when tasks have a natural order (research → code, browse → summarize).
|
|
114
|
+
|
|
115
|
+
### While Workers Are Running
|
|
116
|
+
- **Monitor the [Worker Status] digest.** It shows warning flags:
|
|
117
|
+
- \`⚠️ IDLE Ns\` — worker hasn't done anything in N seconds (may be stuck)
|
|
118
|
+
- \`⚠️ POSSIBLY LOOPING\` — many LLM calls but almost no tool calls (spinning without progress)
|
|
119
|
+
- \`⚠️ N% of timeout used\` — worker is running out of time
|
|
120
|
+
- **Use \`check_job\` for deeper diagnostics** when you see warnings or the user asks about progress.
|
|
121
|
+
- **Cancel stuck workers** proactively — don't wait for timeouts. If a worker is idle >2 minutes or looping, cancel it and either retry with a clearer task or inform the user.
|
|
122
|
+
- **Relay progress naturally** when the user asks. Don't dump raw stats — translate into conversational updates ("she's reading the docs now", "almost done, just running tests").
|
|
123
|
+
|
|
124
|
+
### After Completion
|
|
125
|
+
- **Don't re-dispatch completed work.** Results are in the conversation. Build on them.
|
|
126
|
+
- **Summarize results for the user** — present findings naturally as if you did the work yourself.
|
|
127
|
+
- **Chain follow-ups** if the user wants to continue — reference the completed job's results in the new task context.
|
|
104
128
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
-
|
|
108
|
-
-
|
|
109
|
-
-
|
|
110
|
-
|
|
129
|
+
### Tools
|
|
130
|
+
- \`dispatch_task\` — Launch a worker. Returns job ID + list of other active jobs for awareness.
|
|
131
|
+
- \`list_jobs\` — See all jobs with statuses, durations, and recent activity.
|
|
132
|
+
- \`cancel_job\` — Stop a running or queued worker by job ID.
|
|
133
|
+
- \`check_job\` — Get detailed diagnostics for a specific job: elapsed time, time remaining, activity log, stuck detection warnings.
|
|
134
|
+
|
|
135
|
+
### Good vs Bad Examples
|
|
136
|
+
**BAD:** User says "search for React libraries" → you dispatch a research worker → user says "also search for React libraries" → you dispatch ANOTHER research worker for the same thing.
|
|
137
|
+
**GOOD:** You see the first worker is already running in [Worker Status] → tell the user "already on it, the researcher is browsing now" → wait for results.
|
|
138
|
+
|
|
139
|
+
**BAD:** A worker shows ⚠️ IDLE 180s → you ignore it and keep chatting.
|
|
140
|
+
**GOOD:** You notice the warning → call \`check_job\` → see it's stuck → cancel it → re-dispatch with a simpler task description or inform the user.
|
|
141
|
+
|
|
142
|
+
**BAD:** User asks "how's it going?" → you respond "I'm not sure, let me check" → you call \`list_jobs\`.
|
|
143
|
+
**GOOD:** The [Worker Status] digest is already in your context → you immediately say "the coder is 60% through, just pushed 3 commits".
|
|
111
144
|
|
|
112
145
|
## Efficiency — Do It Yourself When You Can
|
|
113
146
|
Workers are expensive (they spin up an entire agent loop with a separate LLM). Only dispatch when the task **actually needs tools**.
|
|
@@ -159,7 +192,36 @@ You can react to messages with emoji using \`send_reaction\`. Use reactions natu
|
|
|
159
192
|
- React to acknowledge a message when you don't need a full text reply
|
|
160
193
|
- React when the user asks you to react
|
|
161
194
|
- Don't overuse reactions — they should feel spontaneous and genuine
|
|
162
|
-
- You can react AND reply in the same turn
|
|
195
|
+
- You can react AND reply in the same turn
|
|
196
|
+
|
|
197
|
+
## Memory & Recall
|
|
198
|
+
You have recall tools that access your FULL long-term memory — far more than the small snapshot in your system prompt. Use them when you actually need deeper context.
|
|
199
|
+
|
|
200
|
+
### When to recall (call tools BEFORE responding):
|
|
201
|
+
|
|
202
|
+
**1. User asks what you know/remember about them → \`recall_user_history\` + \`recall_memories\`**
|
|
203
|
+
"What do you know about me?", "ايش تعرف عني؟" → Call both with their user ID / name. Your full memory has far more than the persona summary.
|
|
204
|
+
|
|
205
|
+
**2. User references something not in the current conversation → \`recall_memories\`**
|
|
206
|
+
"How's the migration going?", "what about the Redis thing?" → If you don't recognize what they mean, search for it. Don't guess.
|
|
207
|
+
|
|
208
|
+
**3. User asks about past conversations → \`search_conversations\`**
|
|
209
|
+
"What did we talk about?", "what was that URL?", "earlier you said..." → Search chat history.
|
|
210
|
+
|
|
211
|
+
**4. User mentions a topic/project/person you lack context on → \`recall_memories\`**
|
|
212
|
+
Any named reference (project, tool, event) that isn't in the active conversation — search for it.
|
|
213
|
+
|
|
214
|
+
### When to respond directly (NO recall):
|
|
215
|
+
- **Greetings and casual chat** — "hey", "good morning", "how are you" → just reply. The baseline memories in your prompt are enough.
|
|
216
|
+
- **Mid-conversation follow-ups** — you already have context from the active thread.
|
|
217
|
+
- **Self-contained questions** — "what's 2+2", "tell me a joke", "translate this"
|
|
218
|
+
- **New tasks/instructions** — user is giving you something new, not referencing the past.
|
|
219
|
+
- **Anything answerable from your system prompt context** — check your Relevant Memories and user persona sections first before reaching for recall tools.
|
|
220
|
+
|
|
221
|
+
### Tips:
|
|
222
|
+
- Be specific with queries: "kubernetes deployment" not "stuff"
|
|
223
|
+
- Weave results naturally — you "remembered", not "searched a database"
|
|
224
|
+
- 1-2 recall calls max per turn. Don't chain 5 searches.`;
|
|
163
225
|
|
|
164
226
|
if (selfData) {
|
|
165
227
|
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}`;
|
package/src/prompts/workers.js
CHANGED
|
@@ -73,6 +73,28 @@ const WORKER_PROMPTS = {
|
|
|
73
73
|
- Chain commands efficiently.
|
|
74
74
|
- Report results with clear status summaries.`,
|
|
75
75
|
|
|
76
|
+
social: `You are a social media worker agent. Your job is to manage LinkedIn and X (Twitter) activities.
|
|
77
|
+
|
|
78
|
+
## LinkedIn Skills
|
|
79
|
+
- **Create posts**: publish text posts or share articles on LinkedIn
|
|
80
|
+
- **Read posts**: get your recent posts or a specific post by URN
|
|
81
|
+
- **Engage**: comment on posts, like posts
|
|
82
|
+
- **Profile**: view your linked LinkedIn profile info
|
|
83
|
+
- **Delete**: remove your own posts
|
|
84
|
+
|
|
85
|
+
## X (Twitter) Skills
|
|
86
|
+
- **Tweet**: post tweets, reply to tweets
|
|
87
|
+
- **Read**: get your recent tweets, view specific tweets, search recent tweets
|
|
88
|
+
- **Engage**: like tweets, retweet
|
|
89
|
+
- **Profile**: view your X profile info
|
|
90
|
+
- **Delete**: remove your own tweets
|
|
91
|
+
|
|
92
|
+
## Instructions
|
|
93
|
+
- For LinkedIn: write professional, engaging content. Use linkedin_create_post with article_url when sharing links. Post URNs look like "urn:li:share:12345". Default visibility is PUBLIC.
|
|
94
|
+
- For X (Twitter): keep tweets within 280 characters. Use x_post_tweet for new tweets, x_reply_to_tweet for replies. Use tweet IDs (numeric strings) to interact with specific tweets.
|
|
95
|
+
- Match the platform to the user's request. If they say "tweet" or "post on X/Twitter", use X tools. If they say "LinkedIn", use LinkedIn tools.
|
|
96
|
+
- Report the outcome clearly: what was posted, links, engagement results.`,
|
|
97
|
+
|
|
76
98
|
research: `You are a research worker agent. Your job is to conduct deep web research and analysis.
|
|
77
99
|
|
|
78
100
|
## Your Skills
|
package/src/self.js
CHANGED
|
@@ -56,9 +56,10 @@ Just getting started.
|
|
|
56
56
|
};
|
|
57
57
|
|
|
58
58
|
export class SelfManager {
|
|
59
|
-
constructor() {
|
|
59
|
+
constructor(basePath = null) {
|
|
60
|
+
this._dir = basePath || SELF_DIR;
|
|
60
61
|
this._cache = new Map();
|
|
61
|
-
mkdirSync(
|
|
62
|
+
mkdirSync(this._dir, { recursive: true });
|
|
62
63
|
this._ensureDefaults();
|
|
63
64
|
}
|
|
64
65
|
|
|
@@ -67,7 +68,7 @@ export class SelfManager {
|
|
|
67
68
|
const logger = getLogger();
|
|
68
69
|
|
|
69
70
|
for (const [name, def] of Object.entries(SELF_FILES)) {
|
|
70
|
-
const filePath = join(
|
|
71
|
+
const filePath = join(this._dir, def.filename);
|
|
71
72
|
if (!existsSync(filePath)) {
|
|
72
73
|
writeFileSync(filePath, def.default, 'utf-8');
|
|
73
74
|
logger.info(`Created default self-file: ${def.filename}`);
|
|
@@ -75,6 +76,17 @@ export class SelfManager {
|
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
/** Create self-files with custom defaults (for character initialization). */
|
|
80
|
+
initWithDefaults(defaults) {
|
|
81
|
+
for (const [name, content] of Object.entries(defaults)) {
|
|
82
|
+
const def = SELF_FILES[name];
|
|
83
|
+
if (!def) continue;
|
|
84
|
+
const filePath = join(this._dir, def.filename);
|
|
85
|
+
writeFileSync(filePath, content, 'utf-8');
|
|
86
|
+
this._cache.set(name, content);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
78
90
|
/** Load a single self-file by name (goals, journey, life, hobbies). Returns markdown string. */
|
|
79
91
|
load(name) {
|
|
80
92
|
const logger = getLogger();
|
|
@@ -83,7 +95,7 @@ export class SelfManager {
|
|
|
83
95
|
|
|
84
96
|
if (this._cache.has(name)) return this._cache.get(name);
|
|
85
97
|
|
|
86
|
-
const filePath = join(
|
|
98
|
+
const filePath = join(this._dir, def.filename);
|
|
87
99
|
let content;
|
|
88
100
|
|
|
89
101
|
if (existsSync(filePath)) {
|
|
@@ -105,7 +117,7 @@ export class SelfManager {
|
|
|
105
117
|
const def = SELF_FILES[name];
|
|
106
118
|
if (!def) throw new Error(`Unknown self-file: ${name}`);
|
|
107
119
|
|
|
108
|
-
const filePath = join(
|
|
120
|
+
const filePath = join(this._dir, def.filename);
|
|
109
121
|
writeFileSync(filePath, content, 'utf-8');
|
|
110
122
|
this._cache.set(name, content);
|
|
111
123
|
logger.info(`Updated self-file: ${name}`);
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { getLogger } from '../utils/logger.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* LinkedIn REST API v2 client.
|
|
6
|
+
* Wraps common endpoints for posts, comments, likes, and profile.
|
|
7
|
+
*/
|
|
8
|
+
export class LinkedInAPI {
|
|
9
|
+
constructor(accessToken) {
|
|
10
|
+
this.client = axios.create({
|
|
11
|
+
baseURL: 'https://api.linkedin.com',
|
|
12
|
+
headers: {
|
|
13
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
14
|
+
'LinkedIn-Version': '202601',
|
|
15
|
+
'X-Restli-Protocol-Version': '2.0.0',
|
|
16
|
+
'Content-Type': 'application/json',
|
|
17
|
+
},
|
|
18
|
+
timeout: 30000,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Retry with backoff on 429
|
|
22
|
+
this.client.interceptors.response.use(null, async (error) => {
|
|
23
|
+
const config = error.config;
|
|
24
|
+
if (error.response?.status === 429 && !config._retried) {
|
|
25
|
+
config._retried = true;
|
|
26
|
+
const retryAfter = parseInt(error.response.headers['retry-after'] || '5', 10);
|
|
27
|
+
getLogger().warn(`[LinkedIn API] Rate limited, retrying after ${retryAfter}s`);
|
|
28
|
+
await new Promise(r => setTimeout(r, retryAfter * 1000));
|
|
29
|
+
return this.client(config);
|
|
30
|
+
}
|
|
31
|
+
return Promise.reject(error);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get the authenticated user's profile via OpenID Connect.
|
|
37
|
+
* Requires "Sign in with LinkedIn" product (openid + profile scopes).
|
|
38
|
+
* Returns null if scopes are insufficient (403).
|
|
39
|
+
*/
|
|
40
|
+
async getProfile() {
|
|
41
|
+
try {
|
|
42
|
+
const { data } = await this.client.get('/v2/userinfo');
|
|
43
|
+
return data;
|
|
44
|
+
} catch (err) {
|
|
45
|
+
if (err.response?.status === 403) {
|
|
46
|
+
return null; // No profile scopes — only w_member_social
|
|
47
|
+
}
|
|
48
|
+
throw err;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create a text-only post.
|
|
54
|
+
* @param {string} authorUrn - e.g. "urn:li:person:XXXXX"
|
|
55
|
+
* @param {string} text - Post content
|
|
56
|
+
* @param {string} visibility - "PUBLIC" or "CONNECTIONS"
|
|
57
|
+
*/
|
|
58
|
+
async createTextPost(authorUrn, text, visibility = 'PUBLIC') {
|
|
59
|
+
const body = {
|
|
60
|
+
author: authorUrn,
|
|
61
|
+
commentary: text,
|
|
62
|
+
visibility,
|
|
63
|
+
distribution: {
|
|
64
|
+
feedDistribution: 'MAIN_FEED',
|
|
65
|
+
targetEntities: [],
|
|
66
|
+
thirdPartyDistributionChannels: [],
|
|
67
|
+
},
|
|
68
|
+
lifecycleState: 'PUBLISHED',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const { data } = await this.client.post('/rest/posts', body);
|
|
72
|
+
return data;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Create a post with an article link.
|
|
77
|
+
* @param {string} authorUrn
|
|
78
|
+
* @param {string} text - Commentary text
|
|
79
|
+
* @param {string} articleUrl - URL to share
|
|
80
|
+
* @param {string} title - Article title
|
|
81
|
+
*/
|
|
82
|
+
async createArticlePost(authorUrn, text, articleUrl, title = '') {
|
|
83
|
+
const body = {
|
|
84
|
+
author: authorUrn,
|
|
85
|
+
commentary: text,
|
|
86
|
+
visibility: 'PUBLIC',
|
|
87
|
+
distribution: {
|
|
88
|
+
feedDistribution: 'MAIN_FEED',
|
|
89
|
+
targetEntities: [],
|
|
90
|
+
thirdPartyDistributionChannels: [],
|
|
91
|
+
},
|
|
92
|
+
content: {
|
|
93
|
+
article: {
|
|
94
|
+
source: articleUrl,
|
|
95
|
+
title: title || articleUrl,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
lifecycleState: 'PUBLISHED',
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const { data } = await this.client.post('/rest/posts', body);
|
|
102
|
+
return data;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get the user's recent posts.
|
|
107
|
+
* Requires r_member_social permission (restricted — approved apps only).
|
|
108
|
+
* @param {string} authorUrn
|
|
109
|
+
* @param {number} count
|
|
110
|
+
*/
|
|
111
|
+
async getMyPosts(authorUrn, count = 10) {
|
|
112
|
+
const { data } = await this.client.get('/rest/posts', {
|
|
113
|
+
params: {
|
|
114
|
+
q: 'author',
|
|
115
|
+
author: encodeURIComponent(authorUrn),
|
|
116
|
+
count,
|
|
117
|
+
sortBy: 'LAST_MODIFIED',
|
|
118
|
+
},
|
|
119
|
+
headers: {
|
|
120
|
+
'X-RestLi-Method': 'FINDER',
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
return data.elements || [];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get a specific post by URN.
|
|
128
|
+
* @param {string} postUrn
|
|
129
|
+
*/
|
|
130
|
+
async getPost(postUrn) {
|
|
131
|
+
const encoded = encodeURIComponent(postUrn);
|
|
132
|
+
const { data } = await this.client.get(`/rest/posts/${encoded}`);
|
|
133
|
+
return data;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Delete a post.
|
|
138
|
+
* @param {string} postUrn
|
|
139
|
+
*/
|
|
140
|
+
async deletePost(postUrn) {
|
|
141
|
+
const encoded = encodeURIComponent(postUrn);
|
|
142
|
+
await this.client.delete(`/rest/posts/${encoded}`);
|
|
143
|
+
return { success: true };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Add a comment to a post.
|
|
148
|
+
* @param {string} postUrn
|
|
149
|
+
* @param {string} text
|
|
150
|
+
* @param {string} actorUrn
|
|
151
|
+
*/
|
|
152
|
+
async addComment(postUrn, text, actorUrn) {
|
|
153
|
+
const encoded = encodeURIComponent(postUrn);
|
|
154
|
+
const { data } = await this.client.post(`/rest/socialActions/${encoded}/comments`, {
|
|
155
|
+
actor: actorUrn,
|
|
156
|
+
message: { text },
|
|
157
|
+
});
|
|
158
|
+
return data;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get comments on a post.
|
|
163
|
+
* Requires r_member_social permission (restricted — approved apps only).
|
|
164
|
+
* @param {string} postUrn
|
|
165
|
+
* @param {number} count
|
|
166
|
+
*/
|
|
167
|
+
async getComments(postUrn, count = 10) {
|
|
168
|
+
const encoded = encodeURIComponent(postUrn);
|
|
169
|
+
const { data } = await this.client.get(`/rest/socialActions/${encoded}/comments`, {
|
|
170
|
+
params: { count },
|
|
171
|
+
headers: {
|
|
172
|
+
'X-RestLi-Method': 'FINDER',
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
return data.elements || [];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Like a post.
|
|
180
|
+
* @param {string} postUrn
|
|
181
|
+
* @param {string} actorUrn
|
|
182
|
+
*/
|
|
183
|
+
async likePost(postUrn, actorUrn) {
|
|
184
|
+
const encoded = encodeURIComponent(postUrn);
|
|
185
|
+
const { data } = await this.client.post(`/rest/socialActions/${encoded}/likes`, {
|
|
186
|
+
actor: actorUrn,
|
|
187
|
+
});
|
|
188
|
+
return data;
|
|
189
|
+
}
|
|
190
|
+
}
|
package/src/services/stt.js
CHANGED
|
@@ -67,7 +67,10 @@ export class STTService {
|
|
|
67
67
|
const result = await this._transcribeElevenLabs(filePath);
|
|
68
68
|
if (result) return result;
|
|
69
69
|
} catch (err) {
|
|
70
|
-
|
|
70
|
+
const detail = err.response
|
|
71
|
+
? `API ${err.response.status}: ${JSON.stringify(err.response.data).slice(0, 200)}`
|
|
72
|
+
: err.message;
|
|
73
|
+
this.logger.warn(`[STT] ElevenLabs failed, trying fallback: ${detail}`);
|
|
71
74
|
}
|
|
72
75
|
}
|
|
73
76
|
|
|
@@ -76,7 +79,10 @@ export class STTService {
|
|
|
76
79
|
try {
|
|
77
80
|
return await this._transcribeWhisper(filePath);
|
|
78
81
|
} catch (err) {
|
|
79
|
-
|
|
82
|
+
const detail = err.response
|
|
83
|
+
? `API ${err.response.status}: ${JSON.stringify(err.response.data).slice(0, 200)}`
|
|
84
|
+
: err.message;
|
|
85
|
+
this.logger.error(`[STT] Whisper fallback also failed: ${detail}`);
|
|
80
86
|
}
|
|
81
87
|
}
|
|
82
88
|
|
package/src/services/tts.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
2
|
import { createHash } from 'crypto';
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, unlinkSync } from 'fs';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, unlinkSync, statSync } from 'fs';
|
|
4
4
|
import { join } from 'path';
|
|
5
5
|
import { homedir } from 'os';
|
|
6
6
|
import { getLogger } from '../utils/logger.js';
|
|
@@ -8,6 +8,7 @@ import { getLogger } from '../utils/logger.js';
|
|
|
8
8
|
const CACHE_DIR = join(homedir(), '.kernelbot', 'tts-cache');
|
|
9
9
|
const DEFAULT_VOICE_ID = 'JBFqnCBsd6RMkjVDRZzb'; // ElevenLabs "George" voice
|
|
10
10
|
const MAX_TEXT_LENGTH = 5000; // ElevenLabs limit
|
|
11
|
+
const MAX_CACHE_FILES = 100; // Evict oldest entries when cache exceeds this count
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Text-to-Speech service using ElevenLabs API.
|
|
@@ -90,6 +91,9 @@ export class TTSService {
|
|
|
90
91
|
writeFileSync(cachedPath, audioBuffer);
|
|
91
92
|
this.logger.info(`[TTS] Synthesized and cached: ${cachedPath} (${audioBuffer.length} bytes)`);
|
|
92
93
|
|
|
94
|
+
// Evict oldest cache entries if over the limit
|
|
95
|
+
this._evictCache();
|
|
96
|
+
|
|
93
97
|
return cachedPath;
|
|
94
98
|
} catch (err) {
|
|
95
99
|
if (err.response) {
|
|
@@ -109,7 +113,33 @@ export class TTSService {
|
|
|
109
113
|
return createHash('sha256').update(`${voiceId}:${text}`).digest('hex').slice(0, 16);
|
|
110
114
|
}
|
|
111
115
|
|
|
112
|
-
/**
|
|
116
|
+
/** Evict oldest cache entries when the cache exceeds MAX_CACHE_FILES. */
|
|
117
|
+
_evictCache() {
|
|
118
|
+
try {
|
|
119
|
+
const files = readdirSync(CACHE_DIR);
|
|
120
|
+
if (files.length <= MAX_CACHE_FILES) return;
|
|
121
|
+
|
|
122
|
+
// Sort by modification time (oldest first)
|
|
123
|
+
const sorted = files
|
|
124
|
+
.map((f) => {
|
|
125
|
+
const fullPath = join(CACHE_DIR, f);
|
|
126
|
+
try { return { name: f, mtime: statSync(fullPath).mtimeMs }; }
|
|
127
|
+
catch { return null; }
|
|
128
|
+
})
|
|
129
|
+
.filter(Boolean)
|
|
130
|
+
.sort((a, b) => a.mtime - b.mtime);
|
|
131
|
+
|
|
132
|
+
const toRemove = sorted.length - MAX_CACHE_FILES;
|
|
133
|
+
for (let i = 0; i < toRemove; i++) {
|
|
134
|
+
try { unlinkSync(join(CACHE_DIR, sorted[i].name)); } catch {}
|
|
135
|
+
}
|
|
136
|
+
this.logger.info(`[TTS] Cache eviction: removed ${toRemove} oldest file(s), ${MAX_CACHE_FILES} remain`);
|
|
137
|
+
} catch {
|
|
138
|
+
// Cache dir may not exist yet
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Clear the entire TTS cache. */
|
|
113
143
|
clearCache() {
|
|
114
144
|
try {
|
|
115
145
|
const files = readdirSync(CACHE_DIR);
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import OAuth from 'oauth-1.0a';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import { getLogger } from '../utils/logger.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* X (Twitter) API v2 client with OAuth 1.0a request signing.
|
|
8
|
+
*/
|
|
9
|
+
export class XApi {
|
|
10
|
+
constructor({ consumerKey, consumerSecret, accessToken, accessTokenSecret }) {
|
|
11
|
+
this._userId = null;
|
|
12
|
+
|
|
13
|
+
this.oauth = OAuth({
|
|
14
|
+
consumer: { key: consumerKey, secret: consumerSecret },
|
|
15
|
+
signature_method: 'HMAC-SHA1',
|
|
16
|
+
hash_function(baseString, key) {
|
|
17
|
+
return crypto.createHmac('sha1', key).update(baseString).digest('base64');
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
this.token = { key: accessToken, secret: accessTokenSecret };
|
|
22
|
+
|
|
23
|
+
this.client = axios.create({
|
|
24
|
+
baseURL: 'https://api.twitter.com',
|
|
25
|
+
timeout: 30000,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Sign every request with OAuth 1.0a
|
|
29
|
+
this.client.interceptors.request.use((config) => {
|
|
30
|
+
const url = `${config.baseURL}${config.url}`;
|
|
31
|
+
const authHeader = this.oauth.toHeader(
|
|
32
|
+
this.oauth.authorize({ url, method: config.method.toUpperCase() }, this.token),
|
|
33
|
+
);
|
|
34
|
+
config.headers = { ...config.headers, ...authHeader, 'Content-Type': 'application/json' };
|
|
35
|
+
return config;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Retry with backoff on 429
|
|
39
|
+
this.client.interceptors.response.use(null, async (error) => {
|
|
40
|
+
const config = error.config;
|
|
41
|
+
if (error.response?.status === 429 && !config._retried) {
|
|
42
|
+
config._retried = true;
|
|
43
|
+
const retryAfter = parseInt(error.response.headers['retry-after'] || '5', 10);
|
|
44
|
+
getLogger().warn(`[X API] Rate limited, retrying after ${retryAfter}s`);
|
|
45
|
+
await new Promise((r) => setTimeout(r, retryAfter * 1000));
|
|
46
|
+
return this.client(config);
|
|
47
|
+
}
|
|
48
|
+
return Promise.reject(error);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Resolve and cache the authenticated user's ID. */
|
|
53
|
+
async _getUserId() {
|
|
54
|
+
if (this._userId) return this._userId;
|
|
55
|
+
const me = await this.getMe();
|
|
56
|
+
this._userId = me.id;
|
|
57
|
+
return this._userId;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** GET /2/users/me */
|
|
61
|
+
async getMe() {
|
|
62
|
+
const { data } = await this.client.get('/2/users/me', {
|
|
63
|
+
params: { 'user.fields': 'id,name,username,description,public_metrics' },
|
|
64
|
+
});
|
|
65
|
+
if (data.data) {
|
|
66
|
+
this._userId = data.data.id;
|
|
67
|
+
}
|
|
68
|
+
return data.data;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** POST /2/tweets — create a new tweet */
|
|
72
|
+
async postTweet(text) {
|
|
73
|
+
const { data } = await this.client.post('/2/tweets', { text });
|
|
74
|
+
return data.data;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** POST /2/tweets — reply to an existing tweet */
|
|
78
|
+
async replyToTweet(text, replyToId) {
|
|
79
|
+
const { data } = await this.client.post('/2/tweets', {
|
|
80
|
+
text,
|
|
81
|
+
reply: { in_reply_to_tweet_id: replyToId },
|
|
82
|
+
});
|
|
83
|
+
return data.data;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** GET /2/tweets/:id */
|
|
87
|
+
async getTweet(tweetId) {
|
|
88
|
+
const { data } = await this.client.get(`/2/tweets/${tweetId}`, {
|
|
89
|
+
params: { 'tweet.fields': 'id,text,author_id,created_at,public_metrics,conversation_id' },
|
|
90
|
+
});
|
|
91
|
+
return data.data;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** GET /2/users/:id/tweets */
|
|
95
|
+
async getMyTweets(count = 10) {
|
|
96
|
+
const userId = await this._getUserId();
|
|
97
|
+
const { data } = await this.client.get(`/2/users/${userId}/tweets`, {
|
|
98
|
+
params: {
|
|
99
|
+
max_results: Math.min(Math.max(count, 5), 100),
|
|
100
|
+
'tweet.fields': 'id,text,created_at,public_metrics',
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
return data.data || [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** GET /2/tweets/search/recent */
|
|
107
|
+
async searchRecentTweets(query, count = 10) {
|
|
108
|
+
const { data } = await this.client.get('/2/tweets/search/recent', {
|
|
109
|
+
params: {
|
|
110
|
+
query,
|
|
111
|
+
max_results: Math.min(Math.max(count, 10), 100),
|
|
112
|
+
'tweet.fields': 'id,text,author_id,created_at,public_metrics',
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
return data.data || [];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** DELETE /2/tweets/:id */
|
|
119
|
+
async deleteTweet(tweetId) {
|
|
120
|
+
const { data } = await this.client.delete(`/2/tweets/${tweetId}`);
|
|
121
|
+
return data.data;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** POST /2/users/:id/likes */
|
|
125
|
+
async likeTweet(tweetId) {
|
|
126
|
+
const userId = await this._getUserId();
|
|
127
|
+
const { data } = await this.client.post(`/2/users/${userId}/likes`, {
|
|
128
|
+
tweet_id: tweetId,
|
|
129
|
+
});
|
|
130
|
+
return data.data;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** POST /2/users/:id/retweets */
|
|
134
|
+
async retweet(tweetId) {
|
|
135
|
+
const userId = await this._getUserId();
|
|
136
|
+
const { data } = await this.client.post(`/2/users/${userId}/retweets`, {
|
|
137
|
+
tweet_id: tweetId,
|
|
138
|
+
});
|
|
139
|
+
return data.data;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -40,6 +40,13 @@ export const WORKER_TYPES = {
|
|
|
40
40
|
description: 'Deep web research and analysis',
|
|
41
41
|
timeout: 600, // 10 minutes
|
|
42
42
|
},
|
|
43
|
+
social: {
|
|
44
|
+
label: 'Social Worker',
|
|
45
|
+
emoji: '📱',
|
|
46
|
+
categories: ['linkedin', 'x'],
|
|
47
|
+
description: 'LinkedIn and X (Twitter) posting, engagement, feed reading',
|
|
48
|
+
timeout: 120, // 2 minutes
|
|
49
|
+
},
|
|
43
50
|
};
|
|
44
51
|
|
|
45
52
|
/**
|
package/src/tools/categories.js
CHANGED
|
@@ -13,6 +13,8 @@ export const TOOL_CATEGORIES = {
|
|
|
13
13
|
network: ['check_port', 'curl_url', 'nginx_reload'],
|
|
14
14
|
browser: ['web_search', 'browse_website', 'screenshot_website', 'extract_content', 'send_image', 'interact_with_page'],
|
|
15
15
|
jira: ['jira_get_ticket', 'jira_search_tickets', 'jira_list_my_tickets', 'jira_get_project_tickets'],
|
|
16
|
+
linkedin: ['linkedin_create_post', 'linkedin_get_my_posts', 'linkedin_get_post', 'linkedin_comment_on_post', 'linkedin_get_comments', 'linkedin_like_post', 'linkedin_get_profile', 'linkedin_delete_post'],
|
|
17
|
+
x: ['x_post_tweet', 'x_reply_to_tweet', 'x_get_my_tweets', 'x_get_tweet', 'x_search_tweets', 'x_like_tweet', 'x_retweet', 'x_delete_tweet', 'x_get_profile'],
|
|
16
18
|
};
|
|
17
19
|
|
|
18
20
|
const CATEGORY_KEYWORDS = {
|
|
@@ -25,6 +27,8 @@ const CATEGORY_KEYWORDS = {
|
|
|
25
27
|
network: ['port', 'curl', 'http', 'nginx', 'network', 'api', 'endpoint', 'request', 'url', 'fetch'],
|
|
26
28
|
browser: ['search', 'find', 'look up', 'browse', 'screenshot', 'scrape', 'website', 'web page', 'webpage', 'extract content', 'html', 'css selector'],
|
|
27
29
|
jira: ['jira', 'ticket', 'issue', 'sprint', 'backlog', 'story', 'epic'],
|
|
30
|
+
linkedin: ['linkedin', 'post on linkedin', 'linkedin post', 'linkedin comment', 'share on linkedin'],
|
|
31
|
+
x: ['twitter', 'tweet', 'x post', 'x.com', 'retweet', 'post on x', 'post on twitter'],
|
|
28
32
|
};
|
|
29
33
|
|
|
30
34
|
// Categories that imply other categories
|