kernelbot 1.0.37 → 1.0.38

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/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(SELF_DIR, { recursive: true });
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(SELF_DIR, def.filename);
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(SELF_DIR, def.filename);
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(SELF_DIR, def.filename);
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
+ }
@@ -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
- this.logger.warn(`[STT] ElevenLabs failed, trying fallback: ${err.message}`);
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
- this.logger.error(`[STT] Whisper fallback also failed: ${err.message}`);
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
 
@@ -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
- /** Clear the TTS cache. */
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
  /**
@@ -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
@@ -8,6 +8,8 @@ import { definitions as githubDefinitions, handlers as githubHandlers } from './
8
8
  import { definitions as codingDefinitions, handlers as codingHandlers } from './coding.js';
9
9
  import { definitions as browserDefinitions, handlers as browserHandlers } from './browser.js';
10
10
  import { definitions as jiraDefinitions, handlers as jiraHandlers } from './jira.js';
11
+ import { definitions as linkedinDefinitions, handlers as linkedinHandlers } from './linkedin.js';
12
+ import { definitions as xDefinitions, handlers as xHandlers } from './x.js';
11
13
  import { definitions as personaDefinitions, handlers as personaHandlers } from './persona.js';
12
14
  import { logToolCall } from '../security/audit.js';
13
15
  import { requiresConfirmation } from '../security/confirm.js';
@@ -23,6 +25,8 @@ export const toolDefinitions = [
23
25
  ...codingDefinitions,
24
26
  ...browserDefinitions,
25
27
  ...jiraDefinitions,
28
+ ...linkedinDefinitions,
29
+ ...xDefinitions,
26
30
  ...personaDefinitions,
27
31
  ];
28
32
 
@@ -37,6 +41,8 @@ const handlerMap = {
37
41
  ...codingHandlers,
38
42
  ...browserHandlers,
39
43
  ...jiraHandlers,
44
+ ...linkedinHandlers,
45
+ ...xHandlers,
40
46
  ...personaHandlers,
41
47
  };
42
48