kernelbot 1.0.36 → 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/bin/kernel.js +389 -23
- package/config.example.yaml +17 -0
- package/package.json +2 -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 +209 -0
- package/src/life/engine.js +28 -20
- 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/security/auth.js +42 -1
- 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 +104 -57
- package/src/utils/display.js +73 -12
- package/src/utils/temporal-awareness.js +24 -10
package/src/security/auth.js
CHANGED
|
@@ -1,9 +1,50 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { getLogger } from '../utils/logger.js';
|
|
5
|
+
|
|
1
6
|
export function isAllowedUser(userId, config) {
|
|
2
7
|
const allowed = config.telegram.allowed_users;
|
|
3
|
-
|
|
8
|
+
|
|
9
|
+
// Auto-register the first user as owner when no allowed users exist
|
|
10
|
+
if (!allowed || allowed.length === 0) {
|
|
11
|
+
config.telegram.allowed_users = [userId];
|
|
12
|
+
_persistOwner(userId);
|
|
13
|
+
const logger = getLogger();
|
|
14
|
+
logger.info(`[Auth] Auto-registered first user ${userId} as owner`);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
4
18
|
return allowed.includes(userId);
|
|
5
19
|
}
|
|
6
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Persist the auto-registered owner ID to ~/.kernelbot/.env
|
|
23
|
+
*/
|
|
24
|
+
function _persistOwner(userId) {
|
|
25
|
+
try {
|
|
26
|
+
const configDir = join(homedir(), '.kernelbot');
|
|
27
|
+
mkdirSync(configDir, { recursive: true });
|
|
28
|
+
const envPath = join(configDir, '.env');
|
|
29
|
+
|
|
30
|
+
let content = '';
|
|
31
|
+
if (existsSync(envPath)) {
|
|
32
|
+
content = readFileSync(envPath, 'utf-8').trimEnd() + '\n';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const regex = /^OWNER_TELEGRAM_ID=.*$/m;
|
|
36
|
+
const line = `OWNER_TELEGRAM_ID=${userId}`;
|
|
37
|
+
if (regex.test(content)) {
|
|
38
|
+
content = content.replace(regex, line);
|
|
39
|
+
} else {
|
|
40
|
+
content += line + '\n';
|
|
41
|
+
}
|
|
42
|
+
writeFileSync(envPath, content);
|
|
43
|
+
} catch {
|
|
44
|
+
// Non-fatal — owner is still in memory for this session
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
7
48
|
export function getUnauthorizedMessage() {
|
|
8
49
|
return 'Access denied. You are not authorized to use this bot.';
|
|
9
50
|
}
|
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
|
package/src/tools/index.js
CHANGED
|
@@ -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
|
|