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/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/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/tools/x.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { XApi } from '../services/x-api.js';
|
|
2
|
+
import { getLogger } from '../utils/logger.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get a configured X API client from the tool context.
|
|
6
|
+
*/
|
|
7
|
+
function getClient(context) {
|
|
8
|
+
const cfg = context.config.x;
|
|
9
|
+
if (!cfg?.consumer_key || !cfg?.consumer_secret || !cfg?.access_token || !cfg?.access_token_secret) {
|
|
10
|
+
throw new Error('X (Twitter) not connected. Use /x link to connect your account.');
|
|
11
|
+
}
|
|
12
|
+
return new XApi({
|
|
13
|
+
consumerKey: cfg.consumer_key,
|
|
14
|
+
consumerSecret: cfg.consumer_secret,
|
|
15
|
+
accessToken: cfg.access_token,
|
|
16
|
+
accessTokenSecret: cfg.access_token_secret,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function handle403(err) {
|
|
21
|
+
if (err.response?.status === 403) {
|
|
22
|
+
return { error: 'Access denied (403). Your X Access Token may be Read-only. Go to the X Developer Portal → App Settings → change permissions to "Read and Write", then regenerate your Access Token.' };
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const definitions = [
|
|
28
|
+
{
|
|
29
|
+
name: 'x_post_tweet',
|
|
30
|
+
description: 'Post a new tweet on X (Twitter).',
|
|
31
|
+
input_schema: {
|
|
32
|
+
type: 'object',
|
|
33
|
+
properties: {
|
|
34
|
+
text: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
description: 'The tweet text (max 280 characters)',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
required: ['text'],
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'x_reply_to_tweet',
|
|
44
|
+
description: 'Reply to an existing tweet on X (Twitter).',
|
|
45
|
+
input_schema: {
|
|
46
|
+
type: 'object',
|
|
47
|
+
properties: {
|
|
48
|
+
text: {
|
|
49
|
+
type: 'string',
|
|
50
|
+
description: 'The reply text',
|
|
51
|
+
},
|
|
52
|
+
reply_to_id: {
|
|
53
|
+
type: 'string',
|
|
54
|
+
description: 'The tweet ID to reply to',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
required: ['text', 'reply_to_id'],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'x_get_my_tweets',
|
|
62
|
+
description: 'Get the authenticated user\'s recent tweets on X (Twitter).',
|
|
63
|
+
input_schema: {
|
|
64
|
+
type: 'object',
|
|
65
|
+
properties: {
|
|
66
|
+
count: {
|
|
67
|
+
type: 'number',
|
|
68
|
+
description: 'Number of tweets to fetch (default 10, max 100)',
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'x_get_tweet',
|
|
75
|
+
description: 'Get a specific tweet by its ID.',
|
|
76
|
+
input_schema: {
|
|
77
|
+
type: 'object',
|
|
78
|
+
properties: {
|
|
79
|
+
tweet_id: {
|
|
80
|
+
type: 'string',
|
|
81
|
+
description: 'The tweet ID',
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
required: ['tweet_id'],
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'x_search_tweets',
|
|
89
|
+
description: 'Search recent tweets on X (Twitter). Returns tweets from the last 7 days.',
|
|
90
|
+
input_schema: {
|
|
91
|
+
type: 'object',
|
|
92
|
+
properties: {
|
|
93
|
+
query: {
|
|
94
|
+
type: 'string',
|
|
95
|
+
description: 'Search query (supports X search operators)',
|
|
96
|
+
},
|
|
97
|
+
count: {
|
|
98
|
+
type: 'number',
|
|
99
|
+
description: 'Number of results (default 10, max 100)',
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
required: ['query'],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'x_like_tweet',
|
|
107
|
+
description: 'Like a tweet on X (Twitter).',
|
|
108
|
+
input_schema: {
|
|
109
|
+
type: 'object',
|
|
110
|
+
properties: {
|
|
111
|
+
tweet_id: {
|
|
112
|
+
type: 'string',
|
|
113
|
+
description: 'The tweet ID to like',
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
required: ['tweet_id'],
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: 'x_retweet',
|
|
121
|
+
description: 'Retweet a tweet on X (Twitter).',
|
|
122
|
+
input_schema: {
|
|
123
|
+
type: 'object',
|
|
124
|
+
properties: {
|
|
125
|
+
tweet_id: {
|
|
126
|
+
type: 'string',
|
|
127
|
+
description: 'The tweet ID to retweet',
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
required: ['tweet_id'],
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: 'x_delete_tweet',
|
|
135
|
+
description: 'Delete one of your own tweets on X (Twitter).',
|
|
136
|
+
input_schema: {
|
|
137
|
+
type: 'object',
|
|
138
|
+
properties: {
|
|
139
|
+
tweet_id: {
|
|
140
|
+
type: 'string',
|
|
141
|
+
description: 'The tweet ID to delete',
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
required: ['tweet_id'],
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: 'x_get_profile',
|
|
149
|
+
description: 'Get the authenticated X (Twitter) profile info.',
|
|
150
|
+
input_schema: {
|
|
151
|
+
type: 'object',
|
|
152
|
+
properties: {},
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
export const handlers = {
|
|
158
|
+
x_post_tweet: async (params, context) => {
|
|
159
|
+
try {
|
|
160
|
+
const client = getClient(context);
|
|
161
|
+
const tweet = await client.postTweet(params.text);
|
|
162
|
+
return { success: true, message: 'Tweet posted', tweet };
|
|
163
|
+
} catch (err) {
|
|
164
|
+
getLogger().error(`x_post_tweet failed: ${err.message}`);
|
|
165
|
+
return handle403(err) || { error: err.response?.data?.detail || err.message };
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
x_reply_to_tweet: async (params, context) => {
|
|
170
|
+
try {
|
|
171
|
+
const client = getClient(context);
|
|
172
|
+
const tweet = await client.replyToTweet(params.text, params.reply_to_id);
|
|
173
|
+
return { success: true, message: 'Reply posted', tweet };
|
|
174
|
+
} catch (err) {
|
|
175
|
+
getLogger().error(`x_reply_to_tweet failed: ${err.message}`);
|
|
176
|
+
return handle403(err) || { error: err.response?.data?.detail || err.message };
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
x_get_my_tweets: async (params, context) => {
|
|
181
|
+
try {
|
|
182
|
+
const client = getClient(context);
|
|
183
|
+
const tweets = await client.getMyTweets(params.count || 10);
|
|
184
|
+
return { tweets, count: tweets.length };
|
|
185
|
+
} catch (err) {
|
|
186
|
+
getLogger().error(`x_get_my_tweets failed: ${err.message}`);
|
|
187
|
+
return { error: err.response?.data?.detail || err.message };
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
x_get_tweet: async (params, context) => {
|
|
192
|
+
try {
|
|
193
|
+
const client = getClient(context);
|
|
194
|
+
const tweet = await client.getTweet(params.tweet_id);
|
|
195
|
+
return { tweet };
|
|
196
|
+
} catch (err) {
|
|
197
|
+
getLogger().error(`x_get_tweet failed: ${err.message}`);
|
|
198
|
+
return { error: err.response?.data?.detail || err.message };
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
x_search_tweets: async (params, context) => {
|
|
203
|
+
try {
|
|
204
|
+
const client = getClient(context);
|
|
205
|
+
const tweets = await client.searchRecentTweets(params.query, params.count || 10);
|
|
206
|
+
return { tweets, count: tweets.length, query: params.query };
|
|
207
|
+
} catch (err) {
|
|
208
|
+
getLogger().error(`x_search_tweets failed: ${err.message}`);
|
|
209
|
+
return { error: err.response?.data?.detail || err.message };
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
x_like_tweet: async (params, context) => {
|
|
214
|
+
try {
|
|
215
|
+
const client = getClient(context);
|
|
216
|
+
const result = await client.likeTweet(params.tweet_id);
|
|
217
|
+
return { success: true, message: 'Tweet liked', result };
|
|
218
|
+
} catch (err) {
|
|
219
|
+
getLogger().error(`x_like_tweet failed: ${err.message}`);
|
|
220
|
+
return handle403(err) || { error: err.response?.data?.detail || err.message };
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
x_retweet: async (params, context) => {
|
|
225
|
+
try {
|
|
226
|
+
const client = getClient(context);
|
|
227
|
+
const result = await client.retweet(params.tweet_id);
|
|
228
|
+
return { success: true, message: 'Retweeted', result };
|
|
229
|
+
} catch (err) {
|
|
230
|
+
getLogger().error(`x_retweet failed: ${err.message}`);
|
|
231
|
+
return handle403(err) || { error: err.response?.data?.detail || err.message };
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
x_delete_tweet: async (params, context) => {
|
|
236
|
+
try {
|
|
237
|
+
const client = getClient(context);
|
|
238
|
+
const result = await client.deleteTweet(params.tweet_id);
|
|
239
|
+
return { success: true, message: 'Tweet deleted', result };
|
|
240
|
+
} catch (err) {
|
|
241
|
+
getLogger().error(`x_delete_tweet failed: ${err.message}`);
|
|
242
|
+
return handle403(err) || { error: err.response?.data?.detail || err.message };
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
x_get_profile: async (params, context) => {
|
|
247
|
+
try {
|
|
248
|
+
const client = getClient(context);
|
|
249
|
+
const profile = await client.getMe();
|
|
250
|
+
return { profile };
|
|
251
|
+
} catch (err) {
|
|
252
|
+
getLogger().error(`x_get_profile failed: ${err.message}`);
|
|
253
|
+
return { error: err.response?.data?.detail || err.message };
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
};
|
package/src/utils/config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
2
|
-
import { join
|
|
2
|
+
import { join } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
import { createInterface } from 'readline';
|
|
5
5
|
import yaml from 'js-yaml';
|
|
@@ -61,6 +61,12 @@ const DEFAULTS = {
|
|
|
61
61
|
max_history: 50,
|
|
62
62
|
recent_window: 10,
|
|
63
63
|
},
|
|
64
|
+
dashboard: {
|
|
65
|
+
enabled: false,
|
|
66
|
+
port: 3000,
|
|
67
|
+
},
|
|
68
|
+
linkedin: {},
|
|
69
|
+
x: {},
|
|
64
70
|
};
|
|
65
71
|
|
|
66
72
|
function deepMerge(target, source) {
|
|
@@ -167,9 +173,12 @@ export async function promptProviderSelection(rl) {
|
|
|
167
173
|
}
|
|
168
174
|
|
|
169
175
|
/**
|
|
170
|
-
*
|
|
176
|
+
* Read config.yaml, merge changes into a top-level section, and write it back.
|
|
177
|
+
* @param {string} section - The top-level YAML key to update (e.g. 'brain', 'orchestrator').
|
|
178
|
+
* @param {object} changes - Key-value pairs to merge into that section.
|
|
179
|
+
* @returns {string} The path to the written config file.
|
|
171
180
|
*/
|
|
172
|
-
|
|
181
|
+
function _patchConfigYaml(section, changes) {
|
|
173
182
|
const configDir = getConfigDir();
|
|
174
183
|
mkdirSync(configDir, { recursive: true });
|
|
175
184
|
const configPath = join(configDir, 'config.yaml');
|
|
@@ -179,16 +188,24 @@ export function saveProviderToYaml(providerKey, modelId) {
|
|
|
179
188
|
existing = yaml.load(readFileSync(configPath, 'utf-8')) || {};
|
|
180
189
|
}
|
|
181
190
|
|
|
182
|
-
existing
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
191
|
+
existing[section] = { ...(existing[section] || {}), ...changes };
|
|
192
|
+
writeFileSync(configPath, yaml.dump(existing, { lineWidth: -1 }));
|
|
193
|
+
return configPath;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Save provider and model to config.yaml.
|
|
198
|
+
*/
|
|
199
|
+
export function saveProviderToYaml(providerKey, modelId) {
|
|
200
|
+
const configPath = _patchConfigYaml('brain', { provider: providerKey, model: modelId });
|
|
187
201
|
|
|
188
202
|
// Remove legacy anthropic section if migrating
|
|
189
|
-
|
|
203
|
+
let existing = yaml.load(readFileSync(configPath, 'utf-8')) || {};
|
|
204
|
+
if (existing.anthropic) {
|
|
205
|
+
delete existing.anthropic;
|
|
206
|
+
writeFileSync(configPath, yaml.dump(existing, { lineWidth: -1 }));
|
|
207
|
+
}
|
|
190
208
|
|
|
191
|
-
writeFileSync(configPath, yaml.dump(existing, { lineWidth: -1 }));
|
|
192
209
|
return configPath;
|
|
193
210
|
}
|
|
194
211
|
|
|
@@ -196,66 +213,28 @@ export function saveProviderToYaml(providerKey, modelId) {
|
|
|
196
213
|
* Save orchestrator provider and model to config.yaml.
|
|
197
214
|
*/
|
|
198
215
|
export function saveOrchestratorToYaml(providerKey, modelId) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const configPath = join(configDir, 'config.yaml');
|
|
202
|
-
|
|
203
|
-
let existing = {};
|
|
204
|
-
if (existsSync(configPath)) {
|
|
205
|
-
existing = yaml.load(readFileSync(configPath, 'utf-8')) || {};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
existing.orchestrator = {
|
|
209
|
-
...(existing.orchestrator || {}),
|
|
210
|
-
provider: providerKey,
|
|
211
|
-
model: modelId,
|
|
212
|
-
};
|
|
216
|
+
return _patchConfigYaml('orchestrator', { provider: providerKey, model: modelId });
|
|
217
|
+
}
|
|
213
218
|
|
|
214
|
-
|
|
215
|
-
|
|
219
|
+
/**
|
|
220
|
+
* Save dashboard config to config.yaml.
|
|
221
|
+
*/
|
|
222
|
+
export function saveDashboardToYaml(changes) {
|
|
223
|
+
return _patchConfigYaml('dashboard', changes);
|
|
216
224
|
}
|
|
217
225
|
|
|
218
226
|
/**
|
|
219
227
|
* Save Claude Code model to config.yaml.
|
|
220
228
|
*/
|
|
221
229
|
export function saveClaudeCodeModelToYaml(modelId) {
|
|
222
|
-
|
|
223
|
-
mkdirSync(configDir, { recursive: true });
|
|
224
|
-
const configPath = join(configDir, 'config.yaml');
|
|
225
|
-
|
|
226
|
-
let existing = {};
|
|
227
|
-
if (existsSync(configPath)) {
|
|
228
|
-
existing = yaml.load(readFileSync(configPath, 'utf-8')) || {};
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
existing.claude_code = {
|
|
232
|
-
...(existing.claude_code || {}),
|
|
233
|
-
model: modelId,
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
writeFileSync(configPath, yaml.dump(existing, { lineWidth: -1 }));
|
|
237
|
-
return configPath;
|
|
230
|
+
return _patchConfigYaml('claude_code', { model: modelId });
|
|
238
231
|
}
|
|
239
232
|
|
|
240
233
|
/**
|
|
241
234
|
* Save Claude Code auth mode + credential to config.yaml and .env.
|
|
242
235
|
*/
|
|
243
236
|
export function saveClaudeCodeAuth(config, mode, value) {
|
|
244
|
-
|
|
245
|
-
mkdirSync(configDir, { recursive: true });
|
|
246
|
-
const configPath = join(configDir, 'config.yaml');
|
|
247
|
-
|
|
248
|
-
let existing = {};
|
|
249
|
-
if (existsSync(configPath)) {
|
|
250
|
-
existing = yaml.load(readFileSync(configPath, 'utf-8')) || {};
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
existing.claude_code = {
|
|
254
|
-
...(existing.claude_code || {}),
|
|
255
|
-
auth_mode: mode,
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
writeFileSync(configPath, yaml.dump(existing, { lineWidth: -1 }));
|
|
237
|
+
_patchConfigYaml('claude_code', { auth_mode: mode });
|
|
259
238
|
|
|
260
239
|
// Update live config
|
|
261
240
|
config.claude_code.auth_mode = mode;
|
|
@@ -556,6 +535,34 @@ export function loadConfig() {
|
|
|
556
535
|
config.claude_code.oauth_token = process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
557
536
|
}
|
|
558
537
|
|
|
538
|
+
// LinkedIn token-based auth from env
|
|
539
|
+
if (process.env.LINKEDIN_ACCESS_TOKEN) {
|
|
540
|
+
if (!config.linkedin) config.linkedin = {};
|
|
541
|
+
config.linkedin.access_token = process.env.LINKEDIN_ACCESS_TOKEN;
|
|
542
|
+
}
|
|
543
|
+
if (process.env.LINKEDIN_PERSON_URN) {
|
|
544
|
+
if (!config.linkedin) config.linkedin = {};
|
|
545
|
+
config.linkedin.person_urn = process.env.LINKEDIN_PERSON_URN;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// X (Twitter) OAuth 1.0a credentials from env
|
|
549
|
+
if (process.env.X_CONSUMER_KEY) {
|
|
550
|
+
if (!config.x) config.x = {};
|
|
551
|
+
config.x.consumer_key = process.env.X_CONSUMER_KEY;
|
|
552
|
+
}
|
|
553
|
+
if (process.env.X_CONSUMER_SECRET) {
|
|
554
|
+
if (!config.x) config.x = {};
|
|
555
|
+
config.x.consumer_secret = process.env.X_CONSUMER_SECRET;
|
|
556
|
+
}
|
|
557
|
+
if (process.env.X_ACCESS_TOKEN) {
|
|
558
|
+
if (!config.x) config.x = {};
|
|
559
|
+
config.x.access_token = process.env.X_ACCESS_TOKEN;
|
|
560
|
+
}
|
|
561
|
+
if (process.env.X_ACCESS_TOKEN_SECRET) {
|
|
562
|
+
if (!config.x) config.x = {};
|
|
563
|
+
config.x.access_token_secret = process.env.X_ACCESS_TOKEN_SECRET;
|
|
564
|
+
}
|
|
565
|
+
|
|
559
566
|
return config;
|
|
560
567
|
}
|
|
561
568
|
|
|
@@ -619,6 +626,30 @@ export function saveCredential(config, envKey, value) {
|
|
|
619
626
|
if (!config.jira) config.jira = {};
|
|
620
627
|
config.jira.api_token = value;
|
|
621
628
|
break;
|
|
629
|
+
case 'LINKEDIN_ACCESS_TOKEN':
|
|
630
|
+
if (!config.linkedin) config.linkedin = {};
|
|
631
|
+
config.linkedin.access_token = value;
|
|
632
|
+
break;
|
|
633
|
+
case 'LINKEDIN_PERSON_URN':
|
|
634
|
+
if (!config.linkedin) config.linkedin = {};
|
|
635
|
+
config.linkedin.person_urn = value;
|
|
636
|
+
break;
|
|
637
|
+
case 'X_CONSUMER_KEY':
|
|
638
|
+
if (!config.x) config.x = {};
|
|
639
|
+
config.x.consumer_key = value;
|
|
640
|
+
break;
|
|
641
|
+
case 'X_CONSUMER_SECRET':
|
|
642
|
+
if (!config.x) config.x = {};
|
|
643
|
+
config.x.consumer_secret = value;
|
|
644
|
+
break;
|
|
645
|
+
case 'X_ACCESS_TOKEN':
|
|
646
|
+
if (!config.x) config.x = {};
|
|
647
|
+
config.x.access_token = value;
|
|
648
|
+
break;
|
|
649
|
+
case 'X_ACCESS_TOKEN_SECRET':
|
|
650
|
+
if (!config.x) config.x = {};
|
|
651
|
+
config.x.access_token_secret = value;
|
|
652
|
+
break;
|
|
622
653
|
}
|
|
623
654
|
|
|
624
655
|
// Also set in process.env so tools pick it up
|
|
@@ -652,5 +683,21 @@ export function getMissingCredential(toolName, config) {
|
|
|
652
683
|
}
|
|
653
684
|
}
|
|
654
685
|
|
|
686
|
+
const linkedinTools = ['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'];
|
|
687
|
+
|
|
688
|
+
if (linkedinTools.includes(toolName)) {
|
|
689
|
+
if (!config.linkedin?.access_token && !process.env.LINKEDIN_ACCESS_TOKEN) {
|
|
690
|
+
return { envKey: 'LINKEDIN_ACCESS_TOKEN', label: 'LinkedIn Access Token (from /linkedin link or https://www.linkedin.com/developers/tools/oauth/token-generator)' };
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const xTools = ['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'];
|
|
695
|
+
|
|
696
|
+
if (xTools.includes(toolName)) {
|
|
697
|
+
if (!config.x?.consumer_key && !process.env.X_CONSUMER_KEY) {
|
|
698
|
+
return { envKey: 'X_CONSUMER_KEY', label: 'X (Twitter) Consumer Key (from /x link or X Developer Portal)' };
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
655
702
|
return null;
|
|
656
703
|
}
|
package/src/utils/display.js
CHANGED
|
@@ -18,21 +18,31 @@ function getVersion() {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const LOGO = `
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
▄▄████████▄▄
|
|
22
|
+
▄██▀▀ ▀▀██▄
|
|
23
|
+
▄█▀ ▄██████▄ ▀█▄
|
|
24
|
+
█▀ ▄██▀ ▀██▄ ▀█
|
|
25
|
+
█▀ ▄█▀ ▄████▄ ▀█▄ ▀█
|
|
26
|
+
▐█ ██ ▄█▀ ▀█▄ ██ █▌
|
|
27
|
+
▐█ █▌ █▀ ██ ▀█ ▐█ █▌
|
|
28
|
+
▐█ █▌ ▀█▄ ▀▀██▀ ▐█ █▌
|
|
29
|
+
█▄ ██ ▀▀████▀ ██ ▄█
|
|
30
|
+
█▄ ▀██▄ ▄██▀ ▄█
|
|
31
|
+
▀█▄ ▀██████▀ ▄█▀
|
|
32
|
+
▀██▄▄ ▄▄██▀
|
|
33
|
+
▀▀████████▀▀
|
|
34
|
+
|
|
35
|
+
█▄▀ █▀▀ █▀█ █▄ █ █▀▀ █ █▀▄ █▀█ ▀█▀
|
|
36
|
+
█▀▄ █▀▀ █▄▀ █ ██ █▀▀ █ ██▀ █ █ █
|
|
37
|
+
█ █ █▄▄ █ █ █ ▀█ █▄▄ █▄▄ █▄▀ █▄█ █
|
|
27
38
|
`;
|
|
28
39
|
|
|
29
|
-
//
|
|
40
|
+
// Green terminal gradient
|
|
30
41
|
const monoGradient = gradient([
|
|
31
|
-
'#
|
|
32
|
-
'#
|
|
33
|
-
'#
|
|
34
|
-
'#
|
|
35
|
-
'#4D4D4D',
|
|
42
|
+
'#00ff41',
|
|
43
|
+
'#00cc33',
|
|
44
|
+
'#009926',
|
|
45
|
+
'#006619',
|
|
36
46
|
]);
|
|
37
47
|
|
|
38
48
|
export function showLogo() {
|
|
@@ -103,4 +113,55 @@ export function showError(msg) {
|
|
|
103
113
|
|
|
104
114
|
export function createSpinner(text) {
|
|
105
115
|
return ora({ text, color: 'cyan' });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Display a single character card in the CLI.
|
|
120
|
+
* @param {object} character — character profile with name, emoji, tagline, origin, age, asciiArt
|
|
121
|
+
* @param {boolean} isActive — whether this is the currently active character
|
|
122
|
+
*/
|
|
123
|
+
export function showCharacterCard(character, isActive = false) {
|
|
124
|
+
const art = character.asciiArt || '';
|
|
125
|
+
const activeTag = isActive ? chalk.green(' (active)') : '';
|
|
126
|
+
const content = [
|
|
127
|
+
`${character.emoji} ${chalk.bold(character.name)}${activeTag}`,
|
|
128
|
+
chalk.dim(`"${character.tagline}"`),
|
|
129
|
+
'',
|
|
130
|
+
...(art ? art.split('\n').map(line => chalk.cyan(line)) : []),
|
|
131
|
+
'',
|
|
132
|
+
chalk.dim(`Origin: ${character.origin || 'Unknown'}`),
|
|
133
|
+
chalk.dim(`Style: ${character.age || 'Unknown'}`),
|
|
134
|
+
].join('\n');
|
|
135
|
+
|
|
136
|
+
console.log(
|
|
137
|
+
boxen(content, {
|
|
138
|
+
padding: 1,
|
|
139
|
+
borderStyle: 'round',
|
|
140
|
+
borderColor: isActive ? 'green' : 'cyan',
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Display the full character gallery for CLI selection.
|
|
147
|
+
* @param {object[]} characters — array of character profiles
|
|
148
|
+
* @param {string|null} activeId — ID of the currently active character
|
|
149
|
+
*/
|
|
150
|
+
export function showCharacterGallery(characters, activeId = null) {
|
|
151
|
+
console.log('');
|
|
152
|
+
console.log(
|
|
153
|
+
gradient(['#ff6b6b', '#feca57', '#48dbfb', '#ff9ff3']).multiline(
|
|
154
|
+
' ═══════════════════════════════\n' +
|
|
155
|
+
' CHOOSE YOUR CHARACTER\n' +
|
|
156
|
+
' ═══════════════════════════════',
|
|
157
|
+
),
|
|
158
|
+
);
|
|
159
|
+
console.log('');
|
|
160
|
+
console.log(chalk.dim(' Each character has their own personality,'));
|
|
161
|
+
console.log(chalk.dim(' memories, and story that evolves with you.'));
|
|
162
|
+
console.log('');
|
|
163
|
+
|
|
164
|
+
for (const c of characters) {
|
|
165
|
+
showCharacterCard(c, c.id === activeId);
|
|
166
|
+
}
|
|
106
167
|
}
|
|
@@ -117,17 +117,30 @@ function determineStatus(hour, dayOfWeek, workingHours) {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
/**
|
|
120
|
-
*
|
|
120
|
+
* Activity period definitions with behavioral tone hints.
|
|
121
|
+
* Each period carries a label and a short guidance string that gets
|
|
122
|
+
* injected into the system prompt so the LLM adapts its personality
|
|
123
|
+
* to the owner's real-time situation.
|
|
124
|
+
*/
|
|
125
|
+
const ACTIVITY_PERIODS = [
|
|
126
|
+
{ start: 0, end: 5, label: 'late_night', tone: 'Keep it calm and gentle — they may be winding down or unable to sleep. Avoid high-energy openers.' },
|
|
127
|
+
{ start: 5, end: 7, label: 'early_morning', tone: 'They are just waking up. Be warm but concise — ease them into the day without overwhelming detail.' },
|
|
128
|
+
{ start: 7, end: 12, label: 'morning', tone: 'Good energy window. Match an upbeat, productive tone — they are likely starting their day.' },
|
|
129
|
+
{ start: 12, end: 14, label: 'midday', tone: 'Lunch break energy — keep things light and conversational unless they initiate something serious.' },
|
|
130
|
+
{ start: 14, end: 17, label: 'afternoon', tone: 'Afternoon focus. Be direct and helpful — they may be deep in work.' },
|
|
131
|
+
{ start: 17, end: 20, label: 'evening', tone: 'Winding down from the day. Be relaxed and friendly — match a casual, end-of-day vibe.' },
|
|
132
|
+
{ start: 20, end: 23, label: 'night', tone: 'Late evening — be warm and unhurried. They are likely relaxing, so keep the mood easy-going.' },
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Determine the likely activity period and its behavioral tone hint.
|
|
137
|
+
* Returns { label, tone } for richer LLM context.
|
|
121
138
|
*/
|
|
122
139
|
function determineActivityPeriod(hour) {
|
|
123
|
-
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (hour >= 14 && hour < 17) return 'afternoon';
|
|
128
|
-
if (hour >= 17 && hour < 20) return 'evening';
|
|
129
|
-
if (hour >= 20 && hour < 23) return 'night';
|
|
130
|
-
return 'late_night';
|
|
140
|
+
const period = ACTIVITY_PERIODS.find(p => hour >= p.start && hour < p.end);
|
|
141
|
+
if (period) return { label: period.label, tone: period.tone };
|
|
142
|
+
// hour 23 falls outside all ranges — treat as late_night
|
|
143
|
+
return { label: 'late_night', tone: ACTIVITY_PERIODS[0].tone };
|
|
131
144
|
}
|
|
132
145
|
|
|
133
146
|
/**
|
|
@@ -163,7 +176,7 @@ export function buildTemporalAwareness() {
|
|
|
163
176
|
const currentHour = getCurrentHour(now, timezone);
|
|
164
177
|
const currentDay = getCurrentDayOfWeek(now, timezone);
|
|
165
178
|
const { status, detail } = determineStatus(currentHour, currentDay, owner.working_hours);
|
|
166
|
-
const period = determineActivityPeriod(currentHour);
|
|
179
|
+
const { label: period, tone: periodTone } = determineActivityPeriod(currentHour);
|
|
167
180
|
|
|
168
181
|
const lines = [
|
|
169
182
|
`## Owner's Real-Time Context`,
|
|
@@ -190,6 +203,7 @@ export function buildTemporalAwareness() {
|
|
|
190
203
|
lines.push('IMPORTANT: Be aware of the owner\'s current local time and status.');
|
|
191
204
|
lines.push('Do NOT assume they are at work during off-hours, or sleeping during work hours.');
|
|
192
205
|
lines.push('Adjust greetings, tone, and context to match their real-time situation.');
|
|
206
|
+
lines.push(`Tone hint: ${periodTone}`);
|
|
193
207
|
|
|
194
208
|
const block = lines.join('\n');
|
|
195
209
|
|