obol-ai 0.3.10 → 0.3.12
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/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/src/claude/chat.js +3 -2
- package/src/claude/constants.js +6 -0
- package/src/claude/tool-registry.js +12 -1
- package/src/claude/tools/knowledge.js +18 -0
- package/src/claude/tools/memory.js +10 -0
- package/src/claude/tools/patterns.js +52 -0
- package/src/cli/config.js +8 -1
- package/src/cli/init.js +33 -11
- package/src/cli/manage-users.js +36 -2
- package/src/config.js +14 -0
- package/src/curiosity/impulse.js +3 -2
- package/src/runtime/heartbeat.js +19 -22
- package/src/telegram/commands/tools.js +8 -6
- package/src/telegram/handlers/media.js +3 -1
- package/src/telegram/handlers/special.js +3 -1
- package/src/telegram/handlers/text.js +3 -1
- package/src/tenant.js +1 -1
- package/src/toolprefs.js +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## 0.3.12
|
|
2
|
+
- add patterns_view, patterns_delete, knowledge_remove, memory_stats tools
|
|
3
|
+
|
|
4
|
+
## 0.3.11
|
|
5
|
+
- add model stats toggle to /tools menu
|
|
6
|
+
- per-user timezone support for all engagement cycles
|
|
7
|
+
|
|
1
8
|
## 0.3.10
|
|
2
9
|
- add CLI commands for evolve and curiosity, verbose logging
|
|
3
10
|
- fix pattern analysis: enforce factual observations, use incrementObservation
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "obol-ai",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.12",
|
|
4
4
|
"description": "Self-evolving AI assistant that learns, remembers, and acts on its own. Persistent vector memory, self-rewriting personality, proactive heartbeats.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
package/src/claude/chat.js
CHANGED
|
@@ -8,7 +8,7 @@ const { buildTools, buildRunnableTools, addToolCache } = require('./tool-registr
|
|
|
8
8
|
const { withCacheBreakpoints, sanitizeMessages, stripToolBlocks } = require('./cache');
|
|
9
9
|
const { getMaxToolIterations } = require('./constants');
|
|
10
10
|
|
|
11
|
-
function createClaude(anthropicConfig, { personality, memory, selfMemory, userDir = OBOL_DIR, bridgeEnabled, botName }) {
|
|
11
|
+
function createClaude(anthropicConfig, { personality, memory, selfMemory, patterns, userDir = OBOL_DIR, bridgeEnabled, botName }) {
|
|
12
12
|
let client = createAnthropicClient(anthropicConfig);
|
|
13
13
|
|
|
14
14
|
let baseSystemPrompt = buildSystemPrompt(personality, userDir, { bridgeEnabled, botName });
|
|
@@ -18,7 +18,7 @@ function createClaude(anthropicConfig, { personality, memory, selfMemory, userDi
|
|
|
18
18
|
const chatAbortControllers = new Map();
|
|
19
19
|
const chatForceControllers = new Map();
|
|
20
20
|
|
|
21
|
-
const tools = buildTools(memory, { bridgeEnabled, selfMemory });
|
|
21
|
+
const tools = buildTools(memory, { bridgeEnabled, selfMemory, patterns });
|
|
22
22
|
|
|
23
23
|
function acquireChatLock(chatId) {
|
|
24
24
|
if (!chatLocks.has(chatId)) chatLocks.set(chatId, { promise: Promise.resolve(), busy: false });
|
|
@@ -105,6 +105,7 @@ function createClaude(anthropicConfig, { personality, memory, selfMemory, userDi
|
|
|
105
105
|
context._forceSignal = forceController.signal;
|
|
106
106
|
context.claude = { chat, clearHistory, client };
|
|
107
107
|
context.selfMemory = selfMemory;
|
|
108
|
+
context.patterns = patterns;
|
|
108
109
|
const runnableTools = buildRunnableTools(tools, memory, context, vlog);
|
|
109
110
|
let activeModel = model;
|
|
110
111
|
|
package/src/claude/constants.js
CHANGED
|
@@ -15,6 +15,7 @@ const historyTool = require('./tools/history');
|
|
|
15
15
|
const agentTool = require('./tools/agent');
|
|
16
16
|
const sttTool = require('./tools/stt');
|
|
17
17
|
const mermaidTool = require('./tools/mermaid');
|
|
18
|
+
const patternsTool = require('./tools/patterns');
|
|
18
19
|
|
|
19
20
|
const TOOL_MODULES = [
|
|
20
21
|
execTool,
|
|
@@ -54,6 +55,10 @@ const INPUT_SUMMARIES = {
|
|
|
54
55
|
create_pdf: (i) => i.filename || 'document',
|
|
55
56
|
text_to_speech: (i) => i.text?.substring(0, 60),
|
|
56
57
|
tts_voices: (i) => i.language || 'all',
|
|
58
|
+
memory_stats: () => 'stats',
|
|
59
|
+
knowledge_remove: (i) => i.ids?.join(', '),
|
|
60
|
+
patterns_view: (i) => i.dimension || 'all',
|
|
61
|
+
patterns_delete: (i) => i.key,
|
|
57
62
|
chat_history: (i) => `${i.date}${i.role ? ` [${i.role}]` : ''}`,
|
|
58
63
|
};
|
|
59
64
|
|
|
@@ -77,6 +82,10 @@ function buildTools(memory, opts = {}) {
|
|
|
77
82
|
tools.push(...knowledgeTool.definitions);
|
|
78
83
|
}
|
|
79
84
|
|
|
85
|
+
if (opts.patterns) {
|
|
86
|
+
tools.push(...patternsTool.definitions);
|
|
87
|
+
}
|
|
88
|
+
|
|
80
89
|
if (opts.bridgeEnabled) {
|
|
81
90
|
tools.push(...bridgeTool.getDefinitions());
|
|
82
91
|
}
|
|
@@ -91,6 +100,7 @@ function buildHandlerMap() {
|
|
|
91
100
|
}
|
|
92
101
|
Object.assign(map, memoryTool.handlers);
|
|
93
102
|
Object.assign(map, knowledgeTool.handlers);
|
|
103
|
+
Object.assign(map, patternsTool.handlers);
|
|
94
104
|
Object.assign(map, bridgeTool.handlers);
|
|
95
105
|
return map;
|
|
96
106
|
}
|
|
@@ -103,7 +113,8 @@ function buildRunnableTools(tools, memory, context, vlog) {
|
|
|
103
113
|
if (toolPrefs) {
|
|
104
114
|
for (const [featureKey, feature] of Object.entries(OPTIONAL_TOOLS)) {
|
|
105
115
|
const pref = toolPrefs.get(featureKey);
|
|
106
|
-
|
|
116
|
+
const enabled = pref ? pref.enabled : (feature.defaultEnabled || false);
|
|
117
|
+
if (!enabled) {
|
|
107
118
|
for (const t of feature.tools) disabledTools.add(t);
|
|
108
119
|
}
|
|
109
120
|
}
|
|
@@ -49,6 +49,17 @@ const definitions = [
|
|
|
49
49
|
required: ['content'],
|
|
50
50
|
},
|
|
51
51
|
},
|
|
52
|
+
{
|
|
53
|
+
name: 'knowledge_remove',
|
|
54
|
+
description: 'Remove one or more entries from your own knowledge/self-memory by ID. Use knowledge_search first to find IDs.',
|
|
55
|
+
input_schema: {
|
|
56
|
+
type: 'object',
|
|
57
|
+
properties: {
|
|
58
|
+
ids: { type: 'array', items: { type: 'string' }, description: 'IDs to remove' },
|
|
59
|
+
},
|
|
60
|
+
required: ['ids'],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
52
63
|
];
|
|
53
64
|
|
|
54
65
|
function formatEntry(m) {
|
|
@@ -100,6 +111,13 @@ const handlers = {
|
|
|
100
111
|
});
|
|
101
112
|
return `Interest stored: ${result.id}`;
|
|
102
113
|
},
|
|
114
|
+
|
|
115
|
+
async knowledge_remove(input, _memory, context) {
|
|
116
|
+
const selfMemory = context.selfMemory;
|
|
117
|
+
if (!selfMemory) return 'Self memory not available.';
|
|
118
|
+
await Promise.all(input.ids.map(id => selfMemory.forget(id)));
|
|
119
|
+
return `Removed ${input.ids.length} entr${input.ids.length !== 1 ? 'ies' : 'y'}`;
|
|
120
|
+
},
|
|
103
121
|
};
|
|
104
122
|
|
|
105
123
|
const requiresSelfMemory = true;
|
|
@@ -53,6 +53,11 @@ const definitions = [
|
|
|
53
53
|
},
|
|
54
54
|
},
|
|
55
55
|
},
|
|
56
|
+
{
|
|
57
|
+
name: 'memory_stats',
|
|
58
|
+
description: 'Show memory statistics — total count and breakdown by category.',
|
|
59
|
+
input_schema: { type: 'object', properties: {} },
|
|
60
|
+
},
|
|
56
61
|
];
|
|
57
62
|
|
|
58
63
|
function formatMemory(m) {
|
|
@@ -96,6 +101,11 @@ const handlers = {
|
|
|
96
101
|
});
|
|
97
102
|
return JSON.stringify(results.map(formatMemory));
|
|
98
103
|
},
|
|
104
|
+
|
|
105
|
+
async memory_stats(_input, memory) {
|
|
106
|
+
const { total, breakdown } = await memory.stats();
|
|
107
|
+
return `Total memories: ${total}\n\nBy category:\n${breakdown}`;
|
|
108
|
+
},
|
|
99
109
|
};
|
|
100
110
|
|
|
101
111
|
const requiresMemory = true;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const definitions = [
|
|
2
|
+
{
|
|
3
|
+
name: 'patterns_view',
|
|
4
|
+
description: 'View behavioral patterns learned about the user. Shows timing, mood, humor, engagement, communication, and topic patterns.',
|
|
5
|
+
input_schema: {
|
|
6
|
+
type: 'object',
|
|
7
|
+
properties: {
|
|
8
|
+
dimension: { type: 'string', enum: ['timing', 'mood', 'humor', 'engagement', 'communication', 'topics'], description: 'Filter by dimension (omit for all)' },
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'patterns_delete',
|
|
14
|
+
description: 'Delete a behavioral pattern by its key. Use patterns_view first to see available keys.',
|
|
15
|
+
input_schema: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: {
|
|
18
|
+
key: { type: 'string', description: 'Pattern key to delete (e.g. "timing.active_hours", "mood.stress_signals")' },
|
|
19
|
+
},
|
|
20
|
+
required: ['key'],
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function formatPattern(p) {
|
|
26
|
+
const obs = p.observation_count ? ` (${p.observation_count} observations)` : '';
|
|
27
|
+
const conf = p.confidence != null ? ` [confidence: ${p.confidence}]` : '';
|
|
28
|
+
return `[${p.dimension}] ${p.key}: ${p.summary}${conf}${obs}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const handlers = {
|
|
32
|
+
async patterns_view(_input, _memory, context) {
|
|
33
|
+
if (!context.patterns) return 'Patterns not available (Supabase not configured).';
|
|
34
|
+
const patterns = _input.dimension
|
|
35
|
+
? await context.patterns.getByDimension(_input.dimension)
|
|
36
|
+
: await context.patterns.getAll();
|
|
37
|
+
if (!patterns.length) return _input.dimension ? `No ${_input.dimension} patterns found.` : 'No patterns learned yet.';
|
|
38
|
+
return patterns.map(formatPattern).join('\n');
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
async patterns_delete(input, _memory, context) {
|
|
42
|
+
if (!context.patterns) return 'Patterns not available (Supabase not configured).';
|
|
43
|
+
const existing = await context.patterns.get(input.key);
|
|
44
|
+
if (!existing) return `Pattern not found: ${input.key}`;
|
|
45
|
+
await context.patterns.remove(input.key);
|
|
46
|
+
return `Deleted pattern: ${input.key}`;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const requiresPatterns = true;
|
|
51
|
+
|
|
52
|
+
module.exports = { definitions, handlers, requiresPatterns };
|
package/src/cli/config.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
const inquirer = require('inquirer');
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
|
-
const { loadConfig, saveConfig, CONFIG_FILE, USERS_DIR } = require('../config');
|
|
4
|
+
const { loadConfig, saveConfig, CONFIG_FILE, USERS_DIR, isValidTimezone } = require('../config');
|
|
5
5
|
const { getNestedValue, setNestedValue, formatValue, updatePassSecret } = require('./config-utils');
|
|
6
6
|
const { manageUsers } = require('./manage-users');
|
|
7
7
|
const { runOAuthFlow } = require('./oauth');
|
|
8
8
|
|
|
9
9
|
const SECTIONS = [
|
|
10
|
+
{
|
|
11
|
+
name: 'General',
|
|
12
|
+
fields: [
|
|
13
|
+
{ key: 'timezone', label: 'Timezone (IANA)', secret: false, validate: (v) => isValidTimezone(v.trim()) ? true : 'Invalid IANA timezone (e.g. Europe/Brussels, America/New_York)' },
|
|
14
|
+
],
|
|
15
|
+
},
|
|
10
16
|
{
|
|
11
17
|
name: 'Anthropic',
|
|
12
18
|
fields: [
|
|
@@ -210,6 +216,7 @@ async function config() {
|
|
|
210
216
|
} else {
|
|
211
217
|
opts.default = currentVal != null ? String(currentVal) : '';
|
|
212
218
|
}
|
|
219
|
+
if (field.validate) opts.validate = field.validate;
|
|
213
220
|
const { newVal } = await inquirer.prompt([opts]);
|
|
214
221
|
|
|
215
222
|
if (isPassRef) {
|
package/src/cli/init.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const inquirer = require('inquirer');
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
|
-
const { getConfigDir, saveConfig, loadConfig, CONFIG_FILE, ensureUserDir } = require('../config');
|
|
4
|
+
const { getConfigDir, saveConfig, loadConfig, CONFIG_FILE, ensureUserDir, isValidTimezone } = require('../config');
|
|
5
5
|
const { validateAnthropic, validateTelegram, validateSupabase } = require('../auth/validators');
|
|
6
6
|
const { setupAnthropicOAuth } = require('./oauth');
|
|
7
7
|
const { setupSupabaseNew, setupSupabaseExisting } = require('./supabase-setup');
|
|
@@ -64,7 +64,7 @@ async function init(opts = {}) {
|
|
|
64
64
|
|
|
65
65
|
const config = {};
|
|
66
66
|
let step = 0;
|
|
67
|
-
const totalSteps =
|
|
67
|
+
const totalSteps = 6;
|
|
68
68
|
const stepLabel = (name) => `─── Step ${++step}/${totalSteps}: ${name} ───`;
|
|
69
69
|
|
|
70
70
|
// Step 1: Anthropic
|
|
@@ -178,7 +178,20 @@ async function init(opts = {}) {
|
|
|
178
178
|
config.owner = { name: ownerName };
|
|
179
179
|
config.bot = { name: botName };
|
|
180
180
|
|
|
181
|
-
// Step 5:
|
|
181
|
+
// Step 5: Timezone
|
|
182
|
+
console.log('\n' + stepLabel('Timezone') + '\n');
|
|
183
|
+
const detectedTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
184
|
+
console.log(` Your system timezone: ${detectedTz}\n`);
|
|
185
|
+
const { timezone } = await inquirer.prompt([{
|
|
186
|
+
type: 'input',
|
|
187
|
+
name: 'timezone',
|
|
188
|
+
message: 'Timezone (IANA):',
|
|
189
|
+
default: detectedTz,
|
|
190
|
+
validate: (v) => isValidTimezone(v.trim()) ? true : 'Invalid IANA timezone (e.g. Europe/Brussels, America/New_York)',
|
|
191
|
+
}]);
|
|
192
|
+
config.timezone = timezone.trim();
|
|
193
|
+
|
|
194
|
+
// Step 6: Access control
|
|
182
195
|
console.log('\n' + stepLabel('Access control') + '\n');
|
|
183
196
|
console.log(' Each allowed user gets their own isolated brain — separate');
|
|
184
197
|
console.log(' personality, memory, evolution cycle, and workspace.');
|
|
@@ -190,14 +203,23 @@ async function init(opts = {}) {
|
|
|
190
203
|
console.log(' Since you have multiple users, name each one:\n');
|
|
191
204
|
config.users = {};
|
|
192
205
|
for (const userId of config.telegram.allowedUsers) {
|
|
193
|
-
const { userName } = await inquirer.prompt([
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
206
|
+
const { userName, userTz } = await inquirer.prompt([
|
|
207
|
+
{
|
|
208
|
+
type: 'input',
|
|
209
|
+
name: 'userName',
|
|
210
|
+
message: `Name for user ${userId}:`,
|
|
211
|
+
default: config.owner.name,
|
|
212
|
+
validate: (v) => v.length > 0,
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
type: 'input',
|
|
216
|
+
name: 'userTz',
|
|
217
|
+
message: `Timezone for user ${userId}:`,
|
|
218
|
+
default: config.timezone,
|
|
219
|
+
validate: (v) => isValidTimezone(v.trim()) ? true : 'Invalid IANA timezone',
|
|
220
|
+
},
|
|
221
|
+
]);
|
|
222
|
+
config.users[String(userId)] = { name: userName, timezone: userTz.trim() };
|
|
201
223
|
}
|
|
202
224
|
|
|
203
225
|
const { bridgeEnabled } = await inquirer.prompt([{
|
package/src/cli/manage-users.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const inquirer = require('inquirer');
|
|
2
2
|
const fs = require('fs');
|
|
3
|
-
const { ensureUserDir, getUserDir } = require('../config');
|
|
3
|
+
const { ensureUserDir, getUserDir, isValidTimezone } = require('../config');
|
|
4
4
|
const { detectTelegramUserId } = require('./init-utils');
|
|
5
5
|
const { setNestedValue } = require('./config-utils');
|
|
6
6
|
|
|
@@ -28,7 +28,10 @@ async function manageUsers(cfg, saveConfig) {
|
|
|
28
28
|
} else {
|
|
29
29
|
for (const id of currentUsers) {
|
|
30
30
|
const hasDir = fs.existsSync(getUserDir(id));
|
|
31
|
-
|
|
31
|
+
const name = cfg.users?.[String(id)]?.name;
|
|
32
|
+
const tz = cfg.users?.[String(id)]?.timezone;
|
|
33
|
+
const label = [name, tz].filter(Boolean).join(' — ');
|
|
34
|
+
console.log(` ${id}${label ? ` (${label})` : ''}${hasDir ? ' ✅' : ' (no workspace yet)'}`);
|
|
32
35
|
}
|
|
33
36
|
}
|
|
34
37
|
console.log('');
|
|
@@ -41,6 +44,7 @@ async function manageUsers(cfg, saveConfig) {
|
|
|
41
44
|
{ name: 'Add user (detect from bot messages)', value: 'detect' },
|
|
42
45
|
{ name: 'Add user (enter ID manually)', value: 'manual' },
|
|
43
46
|
...(currentUsers.length > 0 ? [{ name: 'Rename user', value: 'rename' }] : []),
|
|
47
|
+
...(currentUsers.length > 0 ? [{ name: 'Set user timezone', value: 'timezone' }] : []),
|
|
44
48
|
...(currentUsers.length > 0 ? [{ name: 'Remove user', value: 'remove' }] : []),
|
|
45
49
|
new inquirer.Separator(),
|
|
46
50
|
{ name: 'Back', value: 'back' },
|
|
@@ -140,6 +144,36 @@ async function manageUsers(cfg, saveConfig) {
|
|
|
140
144
|
}
|
|
141
145
|
}
|
|
142
146
|
|
|
147
|
+
if (action === 'timezone') {
|
|
148
|
+
const { tzId } = await inquirer.prompt([{
|
|
149
|
+
type: 'list',
|
|
150
|
+
name: 'tzId',
|
|
151
|
+
message: 'Set timezone for which user?',
|
|
152
|
+
choices: [
|
|
153
|
+
...currentUsers.map(id => {
|
|
154
|
+
const name = cfg.users?.[String(id)]?.name;
|
|
155
|
+
const tz = cfg.users?.[String(id)]?.timezone || cfg.timezone || 'UTC';
|
|
156
|
+
return { name: `${id}${name ? ` — ${name}` : ''} (${tz})`, value: id };
|
|
157
|
+
}),
|
|
158
|
+
new inquirer.Separator(),
|
|
159
|
+
{ name: 'Cancel', value: null },
|
|
160
|
+
],
|
|
161
|
+
}]);
|
|
162
|
+
if (tzId !== null) {
|
|
163
|
+
const currentTz = cfg.users?.[String(tzId)]?.timezone || cfg.timezone || '';
|
|
164
|
+
const { newTz } = await inquirer.prompt([{
|
|
165
|
+
type: 'input',
|
|
166
|
+
name: 'newTz',
|
|
167
|
+
message: `Timezone for user ${tzId} (IANA):`,
|
|
168
|
+
default: currentTz,
|
|
169
|
+
validate: (v) => isValidTimezone(v.trim()) ? true : 'Invalid IANA timezone (e.g. Europe/Brussels, America/New_York)',
|
|
170
|
+
}]);
|
|
171
|
+
if (!cfg.users) cfg.users = {};
|
|
172
|
+
if (!cfg.users[String(tzId)]) cfg.users[String(tzId)] = {};
|
|
173
|
+
cfg.users[String(tzId)].timezone = newTz.trim();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
143
177
|
if (action === 'remove') {
|
|
144
178
|
const { removeId } = await inquirer.prompt([{
|
|
145
179
|
type: 'list',
|
package/src/config.js
CHANGED
|
@@ -164,6 +164,18 @@ function ensureUserDir(userId) {
|
|
|
164
164
|
return dir;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
/** @param {object} config @param {number|string} userId @returns {string} */
|
|
168
|
+
function getUserTimezone(config, userId) {
|
|
169
|
+
return config.users?.[String(userId)]?.timezone || config.timezone || 'UTC';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isValidTimezone(tz) {
|
|
173
|
+
try {
|
|
174
|
+
Intl.DateTimeFormat(undefined, { timeZone: tz });
|
|
175
|
+
return true;
|
|
176
|
+
} catch { return false; }
|
|
177
|
+
}
|
|
178
|
+
|
|
167
179
|
function listUsers() {
|
|
168
180
|
if (!fs.existsSync(USERS_DIR)) return [];
|
|
169
181
|
return fs.readdirSync(USERS_DIR).filter(f =>
|
|
@@ -184,4 +196,6 @@ module.exports = {
|
|
|
184
196
|
getUserDir,
|
|
185
197
|
ensureUserDir,
|
|
186
198
|
listUsers,
|
|
199
|
+
getUserTimezone,
|
|
200
|
+
isValidTimezone,
|
|
187
201
|
};
|
package/src/curiosity/impulse.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const Anthropic = require('@anthropic-ai/sdk');
|
|
2
2
|
const { createSelfMemory } = require('../memory/self');
|
|
3
3
|
const { getTenant } = require('../tenant');
|
|
4
|
+
const { getUserTimezone } = require('../config');
|
|
4
5
|
|
|
5
6
|
const IMPULSE_MODEL = 'claude-haiku-4-5-20251001';
|
|
6
7
|
const COOLDOWN_MS = 24 * 60 * 60 * 1000;
|
|
@@ -109,7 +110,7 @@ async function maybeImpulse(bot, config, tenant, chatId, lastUserMsg, lastAssist
|
|
|
109
110
|
const { text: context, hasSubstance } = await gatherContext(tenant, config.supabase, {
|
|
110
111
|
lastUserMsg,
|
|
111
112
|
lastAssistantMsg,
|
|
112
|
-
timezone: config.
|
|
113
|
+
timezone: getUserTimezone(config, tenant.userId),
|
|
113
114
|
});
|
|
114
115
|
if (!hasSubstance) return;
|
|
115
116
|
|
|
@@ -130,7 +131,7 @@ async function maybePeriodicImpulse(bot, config, userId) {
|
|
|
130
131
|
|
|
131
132
|
const { text: context, hasSubstance } = await gatherContext(tenant, config.supabase, {
|
|
132
133
|
periodic: true,
|
|
133
|
-
timezone: config
|
|
134
|
+
timezone: getUserTimezone(config, userId),
|
|
134
135
|
});
|
|
135
136
|
if (!hasSubstance) return;
|
|
136
137
|
|
package/src/runtime/heartbeat.js
CHANGED
|
@@ -2,7 +2,7 @@ const cron = require('node-cron');
|
|
|
2
2
|
const { createScheduler } = require('./scheduler');
|
|
3
3
|
const { getTenant } = require('../tenant');
|
|
4
4
|
const { shouldEvolveNow, evolve } = require('../evolve');
|
|
5
|
-
const { ensureUserDir } = require('../config');
|
|
5
|
+
const { ensureUserDir, getUserTimezone } = require('../config');
|
|
6
6
|
const { runAnalysis } = require('../analysis');
|
|
7
7
|
const { runCuriosity } = require('../curiosity');
|
|
8
8
|
const { runCuriosityDispatch } = require('../curiosity/dispatch');
|
|
@@ -34,7 +34,7 @@ function getLocalHour(timezone) {
|
|
|
34
34
|
async function runEvolutionForUser(bot, config, userId) {
|
|
35
35
|
if (_evolutionRunning.has(userId)) return;
|
|
36
36
|
|
|
37
|
-
const timezone = config
|
|
37
|
+
const timezone = getUserTimezone(config, userId);
|
|
38
38
|
const userDir = ensureUserDir(userId);
|
|
39
39
|
|
|
40
40
|
if (!shouldEvolveNow(userDir, timezone)) return;
|
|
@@ -127,7 +127,7 @@ async function runCuriosityOnce(config, allowedUsers) {
|
|
|
127
127
|
? await tenant.scheduler.list({ status: 'pending', limit: 5 }).catch(() => [])
|
|
128
128
|
: [];
|
|
129
129
|
const userProfile = tenant.personality?.user || null;
|
|
130
|
-
return { userId, chatId: userId, timezone: config
|
|
130
|
+
return { userId, chatId: userId, timezone: getUserTimezone(config, userId), patterns, events, scheduler: tenant.scheduler, userProfile };
|
|
131
131
|
} catch { return null; }
|
|
132
132
|
}));
|
|
133
133
|
await runCuriosityDispatch(client, selfMemory, userDispatchData.filter(Boolean));
|
|
@@ -140,7 +140,7 @@ async function runCuriosityOnce(config, allowedUsers) {
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
async function runAnalysisForUser(bot, config, userId) {
|
|
143
|
-
const timezone = config
|
|
143
|
+
const timezone = getUserTimezone(config, userId);
|
|
144
144
|
try {
|
|
145
145
|
const tenant = await getTenant(userId, config);
|
|
146
146
|
if (!tenant.messageLog || !tenant.scheduler || !tenant.patterns) return;
|
|
@@ -203,34 +203,32 @@ function setupHeartbeat(bot, config) {
|
|
|
203
203
|
const allowedUsers = config?.telegram?.allowedUsers || [];
|
|
204
204
|
if (allowedUsers.length > 0) {
|
|
205
205
|
cron.schedule('* * * * *', async () => {
|
|
206
|
-
const timezone = config.timezone || 'UTC';
|
|
207
|
-
const { hour, minute } = getLocalHour(timezone);
|
|
208
|
-
if (hour !== 3 || minute !== 0) return;
|
|
209
|
-
|
|
210
206
|
for (const userId of allowedUsers) {
|
|
207
|
+
const tz = getUserTimezone(config, userId);
|
|
208
|
+
const { hour, minute } = getLocalHour(tz);
|
|
209
|
+
if (hour !== 3 || minute !== 0) continue;
|
|
211
210
|
runEvolutionForUser(bot, config, userId).catch(e =>
|
|
212
211
|
console.error(`[evolution] Unhandled error for user ${userId}:`, e.message)
|
|
213
212
|
);
|
|
214
213
|
}
|
|
215
214
|
});
|
|
216
|
-
console.log(
|
|
215
|
+
console.log(' ✅ Evolution cron running (daily 3am per-user timezone)');
|
|
217
216
|
|
|
218
217
|
cron.schedule('* * * * *', async () => {
|
|
219
|
-
const timezone = config.timezone || 'UTC';
|
|
220
|
-
const { hour, minute } = getLocalHour(timezone);
|
|
221
|
-
if (!ANALYSIS_HOURS.has(hour) || minute !== 0) return;
|
|
222
|
-
|
|
223
218
|
for (const userId of allowedUsers) {
|
|
219
|
+
const tz = getUserTimezone(config, userId);
|
|
220
|
+
const { hour, minute } = getLocalHour(tz);
|
|
221
|
+
if (!ANALYSIS_HOURS.has(hour) || minute !== 0) continue;
|
|
224
222
|
runAnalysisForUser(bot, config, userId).catch(e =>
|
|
225
223
|
console.error(`[analysis] Unhandled error for user ${userId}:`, e.message)
|
|
226
224
|
);
|
|
227
225
|
}
|
|
228
226
|
});
|
|
229
|
-
console.log(
|
|
227
|
+
console.log(' ✅ Analysis cron running (every 3h per-user timezone)');
|
|
230
228
|
|
|
231
229
|
cron.schedule('* * * * *', async () => {
|
|
232
|
-
const
|
|
233
|
-
const { hour, minute } = getLocalHour(
|
|
230
|
+
const tz = config.timezone || 'UTC';
|
|
231
|
+
const { hour, minute } = getLocalHour(tz);
|
|
234
232
|
if (!CURIOSITY_HOURS.has(hour) || minute !== 0) return;
|
|
235
233
|
|
|
236
234
|
runCuriosityOnce(config, allowedUsers).catch(e =>
|
|
@@ -240,17 +238,16 @@ function setupHeartbeat(bot, config) {
|
|
|
240
238
|
console.log(` ✅ Curiosity cron running (every 6h ${config.timezone || 'UTC'})`);
|
|
241
239
|
|
|
242
240
|
cron.schedule('* * * * *', async () => {
|
|
243
|
-
const timezone = config.timezone || 'UTC';
|
|
244
|
-
const { hour, minute } = getLocalHour(timezone);
|
|
245
|
-
if (!IMPULSE_HOURS.has(hour) || minute !== 0) return;
|
|
246
|
-
|
|
247
241
|
for (const userId of allowedUsers) {
|
|
242
|
+
const tz = getUserTimezone(config, userId);
|
|
243
|
+
const { hour, minute } = getLocalHour(tz);
|
|
244
|
+
if (!IMPULSE_HOURS.has(hour) || minute !== 0) continue;
|
|
248
245
|
maybePeriodicImpulse(bot, config, userId).catch(e =>
|
|
249
246
|
console.error(`[impulse] Periodic error for user ${userId}:`, e.message)
|
|
250
247
|
);
|
|
251
248
|
}
|
|
252
249
|
});
|
|
253
|
-
console.log(
|
|
250
|
+
console.log(' ✅ Impulse cron running (every 6h per-user timezone)');
|
|
254
251
|
}
|
|
255
252
|
|
|
256
253
|
console.log(' ✅ Heartbeat running (every 1min)');
|
|
@@ -301,7 +298,7 @@ async function buildProactiveContext(tenant, timezone, query) {
|
|
|
301
298
|
|
|
302
299
|
async function runAgenticEvent(bot, config, event) {
|
|
303
300
|
const tenant = await getTenant(event.user_id, config);
|
|
304
|
-
const timezone = event.timezone || config.
|
|
301
|
+
const timezone = event.timezone || getUserTimezone(config, event.user_id);
|
|
305
302
|
|
|
306
303
|
const query = event.description || event.instructions;
|
|
307
304
|
const context = await buildProactiveContext(tenant, timezone, query).catch(() => '');
|
|
@@ -4,11 +4,14 @@ const { OPTIONAL_TOOLS } = require('../../claude');
|
|
|
4
4
|
const { clearVoiceFlow, sendVoiceLanguagePicker } = require('../voice');
|
|
5
5
|
const { TERM_SEP } = require('../constants');
|
|
6
6
|
|
|
7
|
+
function isEnabled(pref, feature) {
|
|
8
|
+
return pref ? pref.enabled : (feature.defaultEnabled || false);
|
|
9
|
+
}
|
|
10
|
+
|
|
7
11
|
function buildToolsMessage(toolPrefs) {
|
|
8
12
|
const lines = [`◈ TOOLS`, TERM_SEP, ``];
|
|
9
13
|
for (const [key, feature] of Object.entries(OPTIONAL_TOOLS)) {
|
|
10
|
-
const
|
|
11
|
-
const enabled = pref?.enabled || false;
|
|
14
|
+
const enabled = isEnabled(toolPrefs.get(key), feature);
|
|
12
15
|
lines.push(` ${enabled ? '◉' : '○'} ${feature.label}`);
|
|
13
16
|
}
|
|
14
17
|
lines.push(``, TERM_SEP);
|
|
@@ -20,8 +23,7 @@ function buildToolsKeyboard(toolPrefs) {
|
|
|
20
23
|
const entries = Object.entries(OPTIONAL_TOOLS);
|
|
21
24
|
for (let i = 0; i < entries.length; i++) {
|
|
22
25
|
const [key, feature] = entries[i];
|
|
23
|
-
const
|
|
24
|
-
const enabled = pref?.enabled || false;
|
|
26
|
+
const enabled = isEnabled(toolPrefs.get(key), feature);
|
|
25
27
|
keyboard.text(`${enabled ? '◉' : '○'} ${feature.label}`, `tool:${key}`);
|
|
26
28
|
if ((i + 1) % 2 === 0 && i < entries.length - 1) keyboard.row();
|
|
27
29
|
}
|
|
@@ -49,10 +51,10 @@ async function handleToolCallback(ctx, featureKey, answer, { getTenant: gt, conf
|
|
|
49
51
|
const tenant = await gt(ctx.from.id, cfg);
|
|
50
52
|
if (!tenant.toolPrefsApi) return answer({ text: 'Not available' });
|
|
51
53
|
|
|
52
|
-
const
|
|
54
|
+
const feature = OPTIONAL_TOOLS[featureKey];
|
|
55
|
+
const newEnabled = await tenant.toolPrefsApi.toggle(featureKey, feature.defaultEnabled);
|
|
53
56
|
await tenant.reloadToolPrefs();
|
|
54
57
|
|
|
55
|
-
const feature = OPTIONAL_TOOLS[featureKey];
|
|
56
58
|
await answer({ text: `${feature.label}: ${newEnabled ? 'ON' : 'OFF'}` });
|
|
57
59
|
|
|
58
60
|
const text = buildToolsMessage(tenant.toolPrefs);
|
|
@@ -124,7 +124,9 @@ async function processMediaItems(ctx, items, { config, allowedUsers, bot, create
|
|
|
124
124
|
await sendHtml(ctx, response).catch(() => {});
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
const statsPref = tenant.toolPrefs?.get('model_stats');
|
|
128
|
+
const showStats = statsPref ? statsPref.enabled : true;
|
|
129
|
+
if (showStats && usage && model) {
|
|
128
130
|
const tag = model.includes('opus') ? 'opus' : model.includes('haiku') ? 'haiku' : 'sonnet';
|
|
129
131
|
const tokIn = usage.input_tokens >= 1000 ? `${(usage.input_tokens/1000).toFixed(1)}k` : usage.input_tokens;
|
|
130
132
|
const tokOut = usage.output_tokens >= 1000 ? `${(usage.output_tokens/1000).toFixed(1)}k` : usage.output_tokens;
|
|
@@ -120,7 +120,9 @@ async function processSpecial(ctx, prompt, deps) {
|
|
|
120
120
|
await sendHtml(ctx, response).catch(() => {});
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
const statsPref = tenant.toolPrefs?.get('model_stats');
|
|
124
|
+
const showStats = statsPref ? statsPref.enabled : true;
|
|
125
|
+
if (showStats && usage && model) {
|
|
124
126
|
const tag = model.includes('opus') ? 'opus' : model.includes('haiku') ? 'haiku' : 'sonnet';
|
|
125
127
|
const tokIn = usage.input_tokens >= 1000 ? `${(usage.input_tokens / 1000).toFixed(1)}k` : usage.input_tokens;
|
|
126
128
|
const tokOut = usage.output_tokens >= 1000 ? `${(usage.output_tokens / 1000).toFixed(1)}k` : usage.output_tokens;
|
|
@@ -230,7 +230,9 @@ async function processTextMessage(ctx, fullMessage, { config, allowedUsers, bot,
|
|
|
230
230
|
sendTtsVoiceSummary(ctx, tenant, response).catch(e => console.error('[tts] Auto-summary failed:', e.message));
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
-
|
|
233
|
+
const statsPref = tenant.toolPrefs?.get('model_stats');
|
|
234
|
+
const showStats = statsPref ? statsPref.enabled : true;
|
|
235
|
+
if (showStats && usage && model) {
|
|
234
236
|
const tag = model.includes('opus') ? 'opus' : model.includes('haiku') ? 'haiku' : 'sonnet';
|
|
235
237
|
const tokIn = usage.input_tokens >= 1000 ? `${(usage.input_tokens/1000).toFixed(1)}k` : usage.input_tokens;
|
|
236
238
|
const tokOut = usage.output_tokens >= 1000 ? `${(usage.output_tokens/1000).toFixed(1)}k` : usage.output_tokens;
|
package/src/tenant.js
CHANGED
|
@@ -38,7 +38,7 @@ async function createTenant(userId, config) {
|
|
|
38
38
|
const selfMemory = config.supabase ? await createSelfMemory(config.supabase, userId) : null;
|
|
39
39
|
const patterns = config.supabase ? await createPatterns(config.supabase, userId) : null;
|
|
40
40
|
const bridgeEnabled = isBridgeEnabled(config) && (config.telegram?.allowedUsers?.length || 0) >= 2;
|
|
41
|
-
const claude = createClaude(config.anthropic, { personality, memory, selfMemory, userDir, bridgeEnabled, botName: config.bot?.name });
|
|
41
|
+
const claude = createClaude(config.anthropic, { personality, memory, selfMemory, patterns, userDir, bridgeEnabled, botName: config.bot?.name });
|
|
42
42
|
const scheduler = config.supabase ? createScheduler(config.supabase, userId) : null;
|
|
43
43
|
const messageLog = config.supabase ? createMessageLog(config.supabase, memory, config.anthropic, userId, userDir) : null;
|
|
44
44
|
const toolPrefsApi = config.supabase ? createToolPrefs(config.supabase, userId) : null;
|
package/src/toolprefs.js
CHANGED
|
@@ -39,10 +39,11 @@ function createToolPrefs(supabaseConfig, userId) {
|
|
|
39
39
|
return data[0];
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
async function toggle(toolName) {
|
|
42
|
+
async function toggle(toolName, defaultEnabled = false) {
|
|
43
43
|
const all = await getAll();
|
|
44
44
|
const current = all.get(toolName);
|
|
45
|
-
const
|
|
45
|
+
const currentEnabled = current ? current.enabled : defaultEnabled;
|
|
46
|
+
const newEnabled = !currentEnabled;
|
|
46
47
|
await set(toolName, newEnabled, current?.config || {});
|
|
47
48
|
return newEnabled;
|
|
48
49
|
}
|